feat: categories with titles

This commit is contained in:
2026-02-22 07:18:43 +01:00
parent 2a83df1962
commit 9dacd6fca5
20 changed files with 735 additions and 207 deletions

View File

@@ -33,11 +33,16 @@ export interface BlogGenerationOptions {
language?: string;
pageTitle?: string;
picoTheme?: PicoThemeName;
categoryMetadata?: Record<string, CategoryMetadata>;
categorySettings?: Record<string, CategoryRenderSettings>;
menu?: MenuDocument;
sections?: BlogGenerationSection[];
}
export interface CategoryMetadata extends CategoryRenderSettings {
title: string;
}
export type BlogGenerationSection = 'core' | 'single' | 'category' | 'tag' | 'date';
export interface BlogGenerationResult {
@@ -105,6 +110,7 @@ function clampMaxPostsPerPage(value: unknown): number {
}
function resolveCategorySettings(
categoryMetadata: Record<string, CategoryMetadata> | undefined,
value: Record<string, CategoryRenderSettings> | undefined,
): Record<string, CategoryRenderSettings> {
const defaults: Record<string, CategoryRenderSettings> = {
@@ -114,11 +120,20 @@ function resolveCategorySettings(
page: { renderInLists: false, showTitle: true },
};
if (!value) {
return defaults;
const merged = { ...defaults };
if (categoryMetadata) {
for (const [category, metadata] of Object.entries(categoryMetadata)) {
merged[category] = {
renderInLists: metadata?.renderInLists !== false,
showTitle: metadata?.showTitle !== false,
};
}
}
if (!value) {
return merged;
}
const merged = { ...defaults };
for (const [category, settings] of Object.entries(value)) {
merged[category] = {
renderInLists: settings?.renderInLists !== false,
@@ -128,6 +143,15 @@ function resolveCategorySettings(
return merged;
}
function resolveCategoryDisplayTitle(
category: string,
categoryMetadata: Record<string, CategoryMetadata> | undefined,
): string {
const title = categoryMetadata?.[category]?.title;
const trimmed = typeof title === 'string' ? title.trim() : '';
return trimmed.length > 0 ? trimmed : category;
}
function buildCanonicalPreviewPath(createdAt: Date, slug: string): string {
const year = createdAt.getFullYear();
const month = String(createdAt.getMonth() + 1).padStart(2, '0');
@@ -332,7 +356,7 @@ export class BlogGenerationEngine {
const includeTag = selectedSections.has('tag');
const includeDate = selectedSections.has('date');
const categorySettings = resolveCategorySettings(options.categorySettings);
const categorySettings = resolveCategorySettings(options.categoryMetadata, options.categorySettings);
const listExcludedCategories = Object.entries(categorySettings)
.filter(([, settings]) => settings.renderInLists === false)
.map(([category]) => category);
@@ -658,7 +682,7 @@ export class BlogGenerationEngine {
const pageContext = {
page_title: pageTitle,
language,
menu_items: buildTemplateMenuItems(options.menu),
menu_items: buildTemplateMenuItems(options.menu, options.categoryMetadata),
pico_stylesheet_href: getPicoStylesheetHref(sanitizePicoTheme(options.picoTheme)),
};
@@ -680,7 +704,7 @@ export class BlogGenerationEngine {
if (includeCategory) {
onProgress(50, 'Generating category pages...');
pagesGenerated += await this.generateCategoryPages(options.projectId, publishedListPosts, allCategories, rewriteContext, maxPostsPerPage, htmlDir, pageContext, pageRenderer, categorySettings, reportUnitProgress);
pagesGenerated += await this.generateCategoryPages(options.projectId, publishedListPosts, allCategories, rewriteContext, maxPostsPerPage, htmlDir, pageContext, pageRenderer, categorySettings, options.categoryMetadata, reportUnitProgress);
}
if (includeTag) {
@@ -723,7 +747,7 @@ export class BlogGenerationEngine {
onProgress(0, 'Collecting sitemap URLs...');
const maxPostsPerPage = clampMaxPostsPerPage(options.maxPostsPerPage);
const categorySettings = resolveCategorySettings(options.categorySettings);
const categorySettings = resolveCategorySettings(options.categoryMetadata, options.categorySettings);
const listExcludedCategories = Object.entries(categorySettings)
.filter(([, settings]) => settings.renderInLists === false)
.map(([category]) => category);
@@ -1117,7 +1141,7 @@ export class BlogGenerationEngine {
renderedUrlCount += generationResult.pagesGenerated;
}
} else {
const categorySettings = resolveCategorySettings(options.categorySettings);
const categorySettings = resolveCategorySettings(options.categoryMetadata, options.categorySettings);
const listExcludedCategories = Object.entries(categorySettings)
.filter(([, settings]) => settings.renderInLists === false)
.map(([category]) => category);
@@ -1274,7 +1298,7 @@ export class BlogGenerationEngine {
const pageContext = {
page_title: pageTitle,
language,
menu_items: buildTemplateMenuItems(options.menu),
menu_items: buildTemplateMenuItems(options.menu, options.categoryMetadata),
pico_stylesheet_href: getPicoStylesheetHref(sanitizePicoTheme(options.picoTheme)),
};
const pageRenderer = new PageRenderer(this.mediaEngine, this.postMediaEngine, this.postEngine);
@@ -1367,6 +1391,7 @@ export class BlogGenerationEngine {
pageContext,
pageRenderer,
categorySettings,
options.categoryMetadata,
onPageGenerated,
);
}
@@ -1560,6 +1585,7 @@ export class BlogGenerationEngine {
pageContext: { page_title: string; language: string; menu_items?: TemplateMenuItem[]; pico_stylesheet_href?: string },
pageRenderer: PageRenderer,
categorySettings: Record<string, CategoryRenderSettings>,
categoryMetadata: Record<string, CategoryMetadata> | undefined,
onPageGenerated: (message: string) => void,
): Promise<number> {
let count = 0;
@@ -1568,6 +1594,8 @@ export class BlogGenerationEngine {
const categoryPosts = posts.filter((post) => (post.categories || []).includes(category));
if (categoryPosts.length === 0) continue;
const categoryDisplayTitle = resolveCategoryDisplayTitle(category, categoryMetadata);
const totalPages = Math.max(1, Math.ceil(categoryPosts.length / maxPostsPerPage));
const encodedCategory = encodeURIComponent(category);
const basePathname = `/category/${encodedCategory}`;
@@ -1580,7 +1608,7 @@ export class BlogGenerationEngine {
const html = await pageRenderer.renderPostList(pagePosts, rewriteContext, {
archiveGrouping: true,
routeKind: 'non-date',
archiveContext: { kind: 'category', name: category },
archiveContext: { kind: 'category', name: categoryDisplayTitle },
basePathname,
pagination: { page, maxPostsPerPage, totalPosts: categoryPosts.length },
categorySettings,