Feature/post media translations (#42)

* chore: updated todo with translation ideas

* feat: first take at the implementation of translations

* fix: small addition for the translation feature

* feat: support language switching in the editor and preview

* feat: better handling of long bodies by not running them through a json envelope

* fix: unknown macros have better fallback

* feat: api for python to get translations

* fix: strip dumb prefix of content in translation

* feat: extend meta diff for translations

* feat: hook up translations to rebuild-from-disk

* feat: generation of the website prefers project language, falling back to canonical language

* fix: crashes during rendering

* feat: translation validation report

* fix: made the translation validation actually work

* chore: reorganization of menu

* fix: some topics cleanup

* chore: updated doc

* feat: translations for media

* feat: more aligned in UI/UX

* feat: edit translations possible

* chore: added full multi-language todo

* chore: updated todo for clarity

* feat: implementation of full multi-linguality

* fix: page creation creates pages

* fix: flags on every page

* fix: better prompt

* feat: made MCP server aware of language content

* feat: python tools for translations

* fix: better fill-in-translations

* fix: better prompt for translation. maybe.

* fix: losing posts from search due to translation process

* fix: translation validation handles in-db content and fill-in of missing translations fixed to flush

* fix: faster scanning for infilling of missing translations

* chore: updated agent instructions

* feat: calendar and tag cloud respect current language now

* fix: retries going up

* fix: got metadata-diff and rebuild into sync

* fix: extended meta-diff for timestamps

* fix: made website validation look at translated content, too

* fix: multi-lingual search

* chore: refactor Editor.tsx into two separate editors

* feat: do language detection when no explicit language given

---------

Co-authored-by: hugo <hugoms@me.com>
This commit is contained in:
Georg Bauer
2026-03-09 14:43:18 +01:00
committed by GitHub
parent f1c9038803
commit b855d61524
116 changed files with 19954 additions and 2094 deletions

View File

@@ -1,6 +1,6 @@
import * as path from 'path';
import * as fs from 'fs/promises';
import type { PostEngine, PostData } from './PostEngine';
import type { PostData, PostTranslationData } from './PostEngine';
import type { MediaEngine, MediaData } from './MediaEngine';
import type { PostMediaEngine } from './PostMediaEngine';
import {
@@ -15,7 +15,7 @@ import { getPicoStylesheetHref, sanitizePicoTheme, type PicoThemeName } from '..
import type { MenuDocument } from './MenuEngine';
import type { ProjectMetadata } from './MetaEngine';
import { loadPublishedGenerationSets } from './GenerationPostSnapshotService';
import { buildCalendarArchiveData, buildSitemapAndFeeds, collectSitemapArchiveMetadata } from './GenerationSitemapFeedService';
import { buildCalendarArchiveData, buildSitemapAndFeeds, collectSitemapArchiveMetadata, buildMultiLanguageSitemap } from './GenerationSitemapFeedService';
import { buildTargetedValidationPlan, planMissingValidationPaths } from './ValidationApplyPlannerService';
import { compareSitemapToHtml } from './SiteValidationDiffService';
import {
@@ -58,6 +58,7 @@ export interface BlogGenerationOptions {
baseUrl: string;
maxPostsPerPage?: number;
language?: string;
blogLanguages?: string[];
pageTitle?: string;
picoTheme?: PicoThemeName;
categoryMetadata?: Record<string, CategoryMetadata>;
@@ -194,17 +195,79 @@ function resolvePostCreatedAt(post: { createdAt: Date | string }): Date {
return Number.isNaN(parsed.getTime()) ? new Date() : parsed;
}
type PublishedTranslationVariant = PostData & {
translationSourceSlug: string;
translationCanonicalLanguage?: string;
translationFilePath: string;
};
interface BlogGenerationPostEngineContract {
getPostsFiltered: (filter: { status?: 'draft' | 'published' | 'archived'; excludeCategories?: string[] }) => Promise<PostData[]>;
getPublishedVersion: (id: string) => Promise<PostData | null>;
getPost: (postId: string) => Promise<PostData | null>;
hasPublishedVersion: (postId: string) => Promise<boolean>;
getLinkedBy?: (postId: string) => Promise<{ id: string; title: string; slug: string }[]>;
getPostTranslations?: (postId: string) => Promise<PostTranslationData[]>;
setProjectContext: (projectId: string, dataDir?: string) => void;
}
export class BlogGenerationEngine {
private readonly postEngine: PostEngine;
private readonly postEngine: BlogGenerationPostEngineContract;
private readonly mediaEngine: MediaEngine;
private readonly postMediaEngine: PostMediaEngine;
constructor(postEngine: PostEngine, mediaEngine: MediaEngine, postMediaEngine: PostMediaEngine) {
constructor(postEngine: BlogGenerationPostEngineContract, mediaEngine: MediaEngine, postMediaEngine: PostMediaEngine) {
this.postEngine = postEngine;
this.mediaEngine = mediaEngine;
this.postMediaEngine = postMediaEngine;
}
private buildPublishedTranslationVariant(sourcePost: PostData, translation: PostTranslationData): PublishedTranslationVariant {
const canonicalLanguage = typeof sourcePost.language === 'string' ? sourcePost.language.trim() : '';
const variantLanguages = Array.from(new Set([
canonicalLanguage,
...(Array.isArray(sourcePost.availableLanguages) ? sourcePost.availableLanguages : []),
translation.language,
].filter((language) => typeof language === 'string' && language.trim().length > 0)));
return {
...sourcePost,
id: translation.id,
slug: `${sourcePost.slug}.${translation.language}`,
title: translation.title,
excerpt: translation.excerpt,
content: translation.content,
language: translation.language,
updatedAt: translation.updatedAt,
publishedAt: translation.publishedAt ?? sourcePost.publishedAt,
availableLanguages: variantLanguages,
translationSourceSlug: sourcePost.slug,
translationCanonicalLanguage: canonicalLanguage || undefined,
translationFilePath: translation.filePath,
};
}
private async buildPublishedRoutePosts(publishedPosts: PostData[]): Promise<PostData[]> {
const routePosts: PostData[] = [...publishedPosts];
if (typeof this.postEngine.getPostTranslations !== 'function') {
return routePosts;
}
for (const post of publishedPosts) {
const translations = await this.postEngine.getPostTranslations(post.id);
for (const translation of translations) {
if (translation.status !== 'published') {
continue;
}
routePosts.push(this.buildPublishedTranslationVariant(post, translation));
}
}
return routePosts;
}
async generate(options: BlogGenerationOptions, onProgress: (progress: number, message?: string) => void): Promise<BlogGenerationResult> {
onProgress(0, 'Loading posts...');
@@ -226,6 +289,7 @@ export class BlogGenerationEngine {
const maxPostsPerPage = clampMaxPostsPerPage(options.maxPostsPerPage);
const { publishedPosts, publishedListPosts } = await loadPublishedGenerationSets(this.postEngine, listExcludedCategories);
const publishedRoutePosts = await this.buildPublishedRoutePosts(publishedPosts);
onProgress(3, `Found ${publishedPosts.length} published posts`);
@@ -250,7 +314,7 @@ export class BlogGenerationEngine {
projectName: options.projectName,
projectDescription: options.projectDescription,
maxPostsPerPage,
publishedPosts,
publishedPosts: publishedRoutePosts,
publishedListPosts,
postIndex: generationPostIndex,
includeFeeds: true,
@@ -273,7 +337,7 @@ export class BlogGenerationEngine {
const archiveMetadata = collectSitemapArchiveMetadata({
baseUrl: options.baseUrl,
maxPostsPerPage,
publishedPosts,
publishedPosts: publishedRoutePosts,
publishedListPosts,
});
@@ -368,7 +432,7 @@ export class BlogGenerationEngine {
const renderRoute = createPreviewBackedGenerationRouteRenderer({
options,
maxPostsPerPage,
publishedPostsForLookup: publishedPosts,
publishedPostsForLookup: publishedRoutePosts,
engines: {
postEngine: this.postEngine,
mediaEngine: this.mediaEngine,
@@ -400,7 +464,7 @@ export class BlogGenerationEngine {
});
pagesGenerated += await generatePageRoutes({
projectId: options.projectId,
posts: publishedPosts,
posts: publishedRoutePosts,
renderRoute,
writePage,
onPageGenerated: reportUnitProgress,
@@ -411,7 +475,7 @@ export class BlogGenerationEngine {
onProgress(35, 'Generating single post pages...');
pagesGenerated += await generateSinglePostPages({
projectId: options.projectId,
posts: publishedPosts,
posts: publishedRoutePosts,
renderRoute,
writePage,
onPageGenerated: reportUnitProgress,
@@ -464,6 +528,173 @@ export class BlogGenerationEngine {
});
}
// --- Alternative language subtree generation ---
const mainLanguage = (options.language ?? 'en').trim().toLowerCase();
const additionalLanguages = (options.blogLanguages ?? [])
.map((lang) => lang.trim().toLowerCase())
.filter((lang) => lang.length > 0 && lang !== mainLanguage);
for (const lang of additionalLanguages) {
onProgress(85, `Generating ${lang} language subtree...`);
// Filter out doNotTranslate posts
const langPosts = publishedPosts.filter((p) => !(p as PostData & { doNotTranslate?: boolean }).doNotTranslate);
const langListPosts = publishedListPosts.filter((p) => !(p as PostData & { doNotTranslate?: boolean }).doNotTranslate);
const langPostIndex = buildGenerationPostIndex(langListPosts);
const langArchiveMetadata = collectSitemapArchiveMetadata({
baseUrl: options.baseUrl,
maxPostsPerPage,
publishedPosts: langPosts,
publishedListPosts: langListPosts,
});
// Build per-language feeds
if (includeCore) {
const langFeedResult = buildSitemapAndFeeds({
baseUrl: `${options.baseUrl}/${lang}`,
projectName: options.projectName,
projectDescription: options.projectDescription,
maxPostsPerPage,
publishedPosts: langPosts,
publishedListPosts: langListPosts,
postIndex: langPostIndex,
includeFeeds: true,
feedLanguage: lang,
});
const langRssPath = path.join(htmlDir, lang, 'rss.xml');
const langAtomPath = path.join(htmlDir, lang, 'atom.xml');
await fs.mkdir(path.join(htmlDir, lang), { recursive: true });
await writeFileIfHashChanged({ projectId: options.projectId, filePath: langRssPath, relativePath: `${lang}/rss.xml`, content: langFeedResult.rssXml });
await writeFileIfHashChanged({ projectId: options.projectId, filePath: langAtomPath, relativePath: `${lang}/atom.xml`, content: langFeedResult.atomXml });
}
const langRenderRoute = createPreviewBackedGenerationRouteRenderer({
options: { ...options, language: lang },
maxPostsPerPage,
publishedPostsForLookup: langPosts,
languagePrefix: `/${lang}`,
engines: {
postEngine: this.postEngine,
mediaEngine: this.mediaEngine,
postMediaEngine: this.postMediaEngine,
},
});
const langWritePage = (projectId: string, urlPath: string, content: string) => writeHtmlPage({
projectId,
htmlDir,
urlPath: `${lang}/${urlPath}`,
content,
knownDirectories: knownOutputDirectories,
hashCache: generatedHashCache,
refreshHashTimestampOnUnchanged: true,
});
const langReportProgress = (message: string) => reportUnitProgress(`[${lang}] ${message}`);
if (includeCore) {
pagesGenerated += await generateRootPages({
projectId: options.projectId,
posts: langListPosts,
maxPostsPerPage,
renderRoute: langRenderRoute,
writePage: langWritePage,
onPageGenerated: langReportProgress,
});
pagesGenerated += await generatePageRoutes({
projectId: options.projectId,
posts: langPosts,
renderRoute: langRenderRoute,
writePage: langWritePage,
onPageGenerated: langReportProgress,
});
}
if (includeSingle) {
pagesGenerated += await generateSinglePostPages({
projectId: options.projectId,
posts: langPosts,
renderRoute: langRenderRoute,
writePage: langWritePage,
onPageGenerated: langReportProgress,
});
}
if (includeCategory) {
pagesGenerated += await generateCategoryPages({
projectId: options.projectId,
posts: langListPosts,
allCategories: langArchiveMetadata.allCategories,
maxPostsPerPage,
renderRoute: langRenderRoute,
writePage: langWritePage,
onPageGenerated: langReportProgress,
postsByCategory: langPostIndex.postsByCategory,
});
}
if (includeTag) {
pagesGenerated += await generateTagPages({
projectId: options.projectId,
posts: langListPosts,
allTags: langArchiveMetadata.allTags,
maxPostsPerPage,
renderRoute: langRenderRoute,
writePage: langWritePage,
onPageGenerated: langReportProgress,
postsByTag: langPostIndex.postsByTag,
});
}
if (includeDate) {
pagesGenerated += await generateDateArchivePages({
projectId: options.projectId,
posts: langListPosts,
yearsMap: langArchiveMetadata.years,
yearMonthsMap: langArchiveMetadata.yearMonths,
yearMonthDaysMap: langArchiveMetadata.yearMonthDays,
maxPostsPerPage,
renderRoute: langRenderRoute,
writePage: langWritePage,
onPageGenerated: langReportProgress,
postsByYear: langPostIndex.postsByYear,
postsByYearMonth: langPostIndex.postsByYearMonth,
postsByYearMonthDay: langPostIndex.postsByYearMonthDay,
});
}
}
// --- Combined sitemap with hreflang (if multiple languages) ---
if (includeCore && additionalLanguages.length > 0) {
const allLanguages = [mainLanguage, ...additionalLanguages];
const langFilteredPosts = publishedPosts.filter((p) => !(p as PostData & { doNotTranslate?: boolean }).doNotTranslate);
const doNotTranslateIds = new Set(
publishedPosts
.filter((p) => (p as PostData & { doNotTranslate?: boolean }).doNotTranslate)
.map((p) => p.id),
);
const hreflangSitemapXml = buildMultiLanguageSitemap({
baseUrl: options.baseUrl,
mainLanguage,
allLanguages,
translatablePosts: langFilteredPosts,
doNotTranslatePosts: publishedPosts.filter((p) => doNotTranslateIds.has(p.id)),
publishedListPosts,
maxPostsPerPage,
postIndex: generationPostIndex,
});
sitemapWritten = await writeFileIfHashChanged({
projectId: options.projectId,
filePath: sitemapPath,
relativePath: 'sitemap.xml',
content: hreflangSitemapXml,
});
}
onProgress(100, `Site generated (${publishedPosts.length} posts, ${pagesGenerated} pages)`);
return {
@@ -535,6 +766,7 @@ export class BlogGenerationEngine {
.map(([category]) => category);
const { publishedPosts, publishedListPosts } = await loadPublishedGenerationSets(this.postEngine, listExcludedCategories);
const publishedRoutePosts = await this.buildPublishedRoutePosts(publishedPosts);
const generationPostIndex = buildGenerationPostIndex(publishedListPosts);
const { sitemapXml } = buildSitemapAndFeeds({
@@ -542,7 +774,7 @@ export class BlogGenerationEngine {
projectName: options.projectName,
projectDescription: options.projectDescription,
maxPostsPerPage,
publishedPosts,
publishedPosts: publishedRoutePosts,
publishedListPosts,
postIndex: generationPostIndex,
includeFeeds: false,
@@ -551,20 +783,111 @@ export class BlogGenerationEngine {
const htmlDir = path.join(options.dataDir, 'html');
await fs.mkdir(htmlDir, { recursive: true });
const sitemapPath = path.join(htmlDir, 'sitemap.xml');
// --- Build per-language expected paths ---
const mainLanguage = (options.language ?? 'en').trim().toLowerCase();
const additionalLanguages = (options.blogLanguages ?? [])
.map((lang) => lang.trim().toLowerCase())
.filter((lang) => lang.length > 0 && lang !== mainLanguage);
let sitemapToWrite = sitemapXml;
const additionalExpectedPaths: string[] = [];
const additionalPostTimestampChecks: Array<{
postUrlPath: string;
postFilePath: string;
generatedUpdatedAtMs?: number;
}> = [];
if (additionalLanguages.length > 0) {
const langPosts = publishedPosts.filter((p) => !(p as PostData & { doNotTranslate?: boolean }).doNotTranslate);
const langListPosts = publishedListPosts.filter((p) => !(p as PostData & { doNotTranslate?: boolean }).doNotTranslate);
for (const lang of additionalLanguages) {
const langPostIndex = buildGenerationPostIndex(langListPosts);
const langSitemapResult = buildSitemapAndFeeds({
baseUrl: `${options.baseUrl}/${lang}`,
projectName: options.projectName,
projectDescription: options.projectDescription,
maxPostsPerPage,
publishedPosts: langPosts,
publishedListPosts: langListPosts,
postIndex: langPostIndex,
includeFeeds: false,
});
// Extract expected paths from the per-language sitemap, stripping base URL
const langLocMatches = langSitemapResult.sitemapXml.matchAll(/<loc>(.*?)<\/loc>/g);
for (const match of langLocMatches) {
const loc = match[1]?.trim();
if (!loc) continue;
try {
const locUrl = new URL(loc);
const base = new URL(options.baseUrl);
let locPath = locUrl.pathname.replace(/\/+$/, '');
const basePath = base.pathname.replace(/\/+$/, '');
if (basePath && locPath.startsWith(basePath)) {
locPath = locPath.slice(basePath.length);
}
additionalExpectedPaths.push(locPath || '/');
} catch {
additionalExpectedPaths.push(loc);
}
}
// Build per-language post timestamp checks
for (const post of langPosts) {
const createdAt = resolvePostCreatedAt(post);
const year = String(createdAt.getFullYear());
const month = String(createdAt.getMonth() + 1).padStart(2, '0');
const postFilePath = path.join(options.dataDir, 'posts', year, month, `${post.slug}.md`);
const postUrlPath = `/${lang}${buildCanonicalPostPath(post)}`;
const relativePath = `${postUrlPath.replace(/^\//, '')}/index.html`;
const generatedRecord = await getGeneratedFileHashRecord(options.projectId, relativePath);
additionalPostTimestampChecks.push({
postUrlPath,
postFilePath,
generatedUpdatedAtMs: generatedRecord?.updatedAt,
});
}
}
// Write multi-language sitemap
const allLanguages = [mainLanguage, ...additionalLanguages];
const langFilteredPosts = publishedPosts.filter((p) => !(p as PostData & { doNotTranslate?: boolean }).doNotTranslate);
const doNotTranslateIds = new Set(
publishedPosts
.filter((p) => (p as PostData & { doNotTranslate?: boolean }).doNotTranslate)
.map((p) => p.id),
);
sitemapToWrite = buildMultiLanguageSitemap({
baseUrl: options.baseUrl,
mainLanguage,
allLanguages,
translatablePosts: langFilteredPosts,
doNotTranslatePosts: publishedPosts.filter((p) => doNotTranslateIds.has(p.id)),
publishedListPosts,
maxPostsPerPage,
postIndex: generationPostIndex,
});
}
const sitemapChanged = await writeFileIfHashChanged({
projectId: options.projectId,
filePath: sitemapPath,
relativePath: 'sitemap.xml',
content: sitemapXml,
content: sitemapToWrite,
});
onProgress(50, 'Comparing sitemap to html pages...');
const postTimestampChecks = await Promise.all(publishedPosts.map(async (post) => {
const postTimestampChecks = await Promise.all(publishedRoutePosts.map(async (post) => {
const createdAt = resolvePostCreatedAt(post);
const year = String(createdAt.getFullYear());
const month = String(createdAt.getMonth() + 1).padStart(2, '0');
const postFilePath = path.join(options.dataDir, 'posts', year, month, `${post.slug}.md`);
const postFilePath = (post as PublishedTranslationVariant).translationFilePath
?? path.join(options.dataDir, 'posts', year, month, `${post.slug}.md`);
const postUrlPath = buildCanonicalPostPath(post);
const relativePath = `${postUrlPath.replace(/^\//, '')}/index.html`;
const generatedRecord = await getGeneratedFileHashRecord(options.projectId, relativePath);
@@ -580,7 +903,8 @@ export class BlogGenerationEngine {
sitemapXml,
baseUrl: options.baseUrl,
htmlDir,
postTimestampChecks,
postTimestampChecks: [...postTimestampChecks, ...additionalPostTimestampChecks],
additionalExpectedPaths,
});
onProgress(
@@ -613,7 +937,12 @@ export class BlogGenerationEngine {
onProgress(10, 'Planning validation apply steps...');
const missingPathPlan = planMissingValidationPaths(rerenderPaths);
const mainLanguage = (options.language ?? 'en').trim().toLowerCase();
const additionalLanguages = (options.blogLanguages ?? [])
.map((lang) => lang.trim().toLowerCase())
.filter((lang) => lang.length > 0 && lang !== mainLanguage);
const missingPathPlan = planMissingValidationPaths(rerenderPaths, additionalLanguages);
onProgress(20, 'Deleting extra URLs...');
@@ -687,13 +1016,14 @@ export class BlogGenerationEngine {
const maxPostsPerPage = clampMaxPostsPerPage(options.maxPostsPerPage);
const { publishedPosts, publishedListPosts } = await loadPublishedGenerationSets(this.postEngine, listExcludedCategories);
const publishedRoutePosts = await this.buildPublishedRoutePosts(publishedPosts);
const generationPostIndex = buildGenerationPostIndex(publishedListPosts);
const { allCategories, allTags, years, yearMonths, yearMonthDays } = buildApplyValidationArchives(publishedListPosts);
const targetedPlan = buildTargetedValidationPlan({
initialPlan: missingPathPlan,
publishedPosts,
publishedPosts: publishedRoutePosts,
allCategories,
allTags,
availableYearMonths: yearMonths.keys(),
@@ -706,7 +1036,7 @@ export class BlogGenerationEngine {
const renderRoute = createPreviewBackedGenerationRouteRenderer({
options,
maxPostsPerPage,
publishedPostsForLookup: publishedPosts,
publishedPostsForLookup: publishedRoutePosts,
engines: {
postEngine: this.postEngine,
mediaEngine: this.mediaEngine,
@@ -725,7 +1055,7 @@ export class BlogGenerationEngine {
};
const { requestedSinglePosts, requestedPagePosts } = selectRequestedPosts({
publishedPosts,
publishedPosts: publishedRoutePosts,
requestedPostIds: targetedPlan.requestedPostIds,
requestedPageSlugs: targetedPlan.requestedPageSlugs,
});
@@ -819,6 +1149,136 @@ export class BlogGenerationEngine {
postsByYearMonthDay: generationPostIndex.postsByYearMonthDay,
});
}
// --- Render missing per-language subtree pages ---
for (const [lang, langMissingPlan] of missingPathPlan.languagePlans) {
const langPosts = publishedPosts.filter((p) => !(p as PostData & { doNotTranslate?: boolean }).doNotTranslate);
const langListPosts = publishedListPosts.filter((p) => !(p as PostData & { doNotTranslate?: boolean }).doNotTranslate);
const langPostIndex = buildGenerationPostIndex(langListPosts);
const langArchives = buildApplyValidationArchives(langListPosts);
const langTargetedPlan = buildTargetedValidationPlan({
initialPlan: langMissingPlan,
publishedPosts: langPosts,
allCategories: langArchives.allCategories,
allTags: langArchives.allTags,
availableYearMonths: langArchives.yearMonths.keys(),
availableYearMonthDays: langArchives.yearMonthDays.keys(),
});
const langRenderRoute = createPreviewBackedGenerationRouteRenderer({
options: { ...options, language: lang },
maxPostsPerPage,
publishedPostsForLookup: langPosts,
languagePrefix: `/${lang}`,
engines: {
postEngine: this.postEngine,
mediaEngine: this.mediaEngine,
postMediaEngine: this.postMediaEngine,
},
});
const langWritePage = (projectId: string, urlPath: string, content: string) => writeHtmlPage({
projectId,
htmlDir,
urlPath: `${lang}/${urlPath}`,
content,
refreshHashTimestampOnUnchanged: true,
});
if (langTargetedPlan.requestRootRoutes) {
renderedUrlCount += await generateRootPages({
projectId: options.projectId,
posts: langListPosts,
maxPostsPerPage,
renderRoute: langRenderRoute,
writePage: langWritePage,
onPageGenerated,
});
const langRequestedPagePosts = selectRequestedPosts({
publishedPosts: langPosts,
requestedPostIds: new Set(),
requestedPageSlugs: langTargetedPlan.requestedPageSlugs,
}).requestedPagePosts;
if (langRequestedPagePosts.length > 0) {
renderedUrlCount += await generatePageRoutes({
projectId: options.projectId,
posts: langRequestedPagePosts,
renderRoute: langRenderRoute,
writePage: langWritePage,
onPageGenerated,
});
}
}
if (langTargetedPlan.requestedCategorySet.size > 0) {
renderedUrlCount += await generateCategoryPages({
projectId: options.projectId,
posts: langListPosts,
allCategories: langTargetedPlan.requestedCategorySet,
maxPostsPerPage,
renderRoute: langRenderRoute,
writePage: langWritePage,
onPageGenerated,
postsByCategory: langPostIndex.postsByCategory,
});
}
if (langTargetedPlan.requestedTagSet.size > 0) {
renderedUrlCount += await generateTagPages({
projectId: options.projectId,
posts: langListPosts,
allTags: langTargetedPlan.requestedTagSet,
maxPostsPerPage,
renderRoute: langRenderRoute,
writePage: langWritePage,
onPageGenerated,
postsByTag: langPostIndex.postsByTag,
});
}
const langRequestedSinglePosts = selectRequestedPosts({
publishedPosts: langPosts,
requestedPostIds: langTargetedPlan.requestedPostIds,
requestedPageSlugs: new Set(),
}).requestedSinglePosts;
if (langRequestedSinglePosts.length > 0) {
renderedUrlCount += await generateSinglePostPages({
projectId: options.projectId,
posts: langRequestedSinglePosts,
renderRoute: langRenderRoute,
writePage: langWritePage,
onPageGenerated,
});
}
const langRequestedArchives = buildRequestedArchiveMaps({
requestedYears: langTargetedPlan.requestedYears,
requestedYearMonths: langTargetedPlan.requestedYearMonths,
requestedYearMonthDays: langTargetedPlan.requestedYearMonthDays,
years: langArchives.years,
yearMonths: langArchives.yearMonths,
yearMonthDays: langArchives.yearMonthDays,
});
if (langRequestedArchives.requestedYearsMap.size > 0 || langRequestedArchives.requestedYearMonthsMap.size > 0 || langRequestedArchives.requestedYearMonthDaysMap.size > 0) {
renderedUrlCount += await generateDateArchivePages({
projectId: options.projectId,
posts: langListPosts,
yearsMap: langRequestedArchives.requestedYearsMap,
yearMonthsMap: langRequestedArchives.requestedYearMonthsMap,
yearMonthDaysMap: langRequestedArchives.requestedYearMonthDaysMap,
maxPostsPerPage,
renderRoute: langRenderRoute,
writePage: langWritePage,
onPageGenerated,
postsByYear: langPostIndex.postsByYear,
postsByYearMonth: langPostIndex.postsByYearMonth,
postsByYearMonthDay: langPostIndex.postsByYearMonthDay,
});
}
}
}
if (renderedUrlCount > 0 || deletedUrlCount > 0) {