From a5281a7750cfcf1f79de7e17062d585a24a836be Mon Sep 17 00:00:00 2001 From: hugo Date: Sat, 21 Feb 2026 08:50:01 +0100 Subject: [PATCH] fix: behaviour of exclude-from-list categories and posts therein --- src/main/engine/BlogGenerationEngine.ts | 2 +- .../engine/templates/partials/head.liquid | 2 + tests/engine/BlogGenerationEngine.test.ts | 92 ++++++++++++++++++- 3 files changed, 92 insertions(+), 4 deletions(-) diff --git a/src/main/engine/BlogGenerationEngine.ts b/src/main/engine/BlogGenerationEngine.ts index 7fc0dc8..e9dbd2d 100644 --- a/src/main/engine/BlogGenerationEngine.ts +++ b/src/main/engine/BlogGenerationEngine.ts @@ -288,7 +288,7 @@ export class BlogGenerationEngine { } const publishedListPosts = Array.from(publishedListPostById.values()) .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); - const feedPosts = publishedPosts.slice(0, maxPostsPerPage); + const feedPosts = publishedListPosts.slice(0, maxPostsPerPage); onProgress(3, `Found ${publishedPosts.length} published posts`); diff --git a/src/main/engine/templates/partials/head.liquid b/src/main/engine/templates/partials/head.liquid index 7f9b55c..3d59fdb 100644 --- a/src/main/engine/templates/partials/head.liquid +++ b/src/main/engine/templates/partials/head.liquid @@ -5,6 +5,8 @@ {% assign resolved_pico_stylesheet_href = pico_stylesheet_href | default: '/assets/pico.min.css' %} + + {% render 'partials/styles' %} diff --git a/tests/engine/BlogGenerationEngine.test.ts b/tests/engine/BlogGenerationEngine.test.ts index cbe16c9..103e795 100644 --- a/tests/engine/BlogGenerationEngine.test.ts +++ b/tests/engine/BlogGenerationEngine.test.ts @@ -153,8 +153,21 @@ describe('BlogGenerationEngine', () => { }); function setupPosts(posts: PostData[]): void { - mockPostEngine.getPostsFiltered.mockImplementation(async (filter: { status?: string }) => { - return posts.filter((p) => p.status === (filter.status ?? p.status)); + mockPostEngine.getPostsFiltered.mockImplementation(async (filter: { status?: string; excludeCategories?: string[] }) => { + return posts.filter((p) => { + if (p.status !== (filter.status ?? p.status)) { + return false; + } + + if (Array.isArray(filter.excludeCategories) && filter.excludeCategories.length > 0) { + const categories = Array.isArray(p.categories) ? p.categories : []; + if (categories.some((category) => filter.excludeCategories?.includes(category))) { + return false; + } + } + + return true; + }); }); mockPostEngine.getPublishedVersion.mockResolvedValue(null); mockPostEngine.getPost.mockImplementation(async (id: string) => { @@ -162,7 +175,15 @@ describe('BlogGenerationEngine', () => { }); } - async function generate(posts: PostData[], options?: Partial<{ maxPostsPerPage: number; language: string; pageTitle: string }>) { + async function generate( + posts: PostData[], + options?: Partial<{ + maxPostsPerPage: number; + language: string; + pageTitle: string; + categorySettings: Record; + }>, + ) { setupPosts(posts); const { BlogGenerationEngine } = await import('../../src/main/engine/BlogGenerationEngine'); const engine = new BlogGenerationEngine(); @@ -175,6 +196,7 @@ describe('BlogGenerationEngine', () => { maxPostsPerPage: options?.maxPostsPerPage, language: options?.language, pageTitle: options?.pageTitle, + categorySettings: options?.categorySettings, }, onProgress); } @@ -211,6 +233,10 @@ describe('BlogGenerationEngine', () => { expect(html).toContain('/assets/pico.min.css'); expect(html).toContain('/assets/lightbox.min.css'); expect(html).toContain('/assets/tag-cloud.js'); + expect(html).toContain('rel="alternate" type="application/rss+xml"'); + expect(html).toContain('href="/rss.xml"'); + expect(html).toContain('rel="alternate" type="application/atom+xml"'); + expect(html).toContain('href="/atom.xml"'); expect(html).not.toContain('function parseWords('); expect(html).toContain('archive-day-marker'); expect(html).toContain('15.01.2025'); @@ -418,6 +444,66 @@ describe('BlogGenerationEngine', () => { expect(await fileExists(path.join(tempDir, 'html', 'category', 'my%20category', 'index.html'))).toBe(true); }); + it('omits excluded categories from category archives and sitemap', async () => { + const posts = [ + makePost({ id: '1', slug: 'aside-post', title: 'Aside Post', categories: ['aside'] }), + ]; + + await generate(posts, { + categorySettings: { + aside: { renderInLists: false, showTitle: false }, + }, + }); + + const categoryArchivePath = path.join(tempDir, 'html', 'category', 'aside', 'index.html'); + expect(await fileExists(categoryArchivePath)).toBe(false); + + const sitemap = await readFile(path.join(tempDir, 'html', 'sitemap.xml'), 'utf-8'); + expect(sitemap).not.toContain('https://example.com/category/aside'); + }); + + it('omits excluded-category posts from RSS and Atom feeds', async () => { + const posts = [ + makePost({ id: '1', slug: 'aside-post', title: 'Aside Post', categories: ['aside'] }), + ]; + + await generate(posts, { + categorySettings: { + aside: { renderInLists: false, showTitle: false }, + }, + }); + + const rss = await readFile(path.join(tempDir, 'html', 'rss.xml'), 'utf-8'); + const atom = await readFile(path.join(tempDir, 'html', 'atom.xml'), 'utf-8'); + + expect(rss).not.toContain('Aside Post'); + expect(atom).not.toContain('Aside Post'); + }); + + it('omits posts that mix included and excluded categories from list outputs and feeds', async () => { + const posts = [ + makePost({ id: '1', slug: 'mixed-post', title: 'Mixed Post', categories: ['news', 'aside'] }), + ]; + + await generate(posts, { + categorySettings: { + aside: { renderInLists: false, showTitle: false }, + }, + }); + + expect(await fileExists(path.join(tempDir, 'html', 'category', 'news', 'index.html'))).toBe(false); + expect(await fileExists(path.join(tempDir, 'html', 'category', 'aside', 'index.html'))).toBe(false); + + const rss = await readFile(path.join(tempDir, 'html', 'rss.xml'), 'utf-8'); + const atom = await readFile(path.join(tempDir, 'html', 'atom.xml'), 'utf-8'); + const sitemap = await readFile(path.join(tempDir, 'html', 'sitemap.xml'), 'utf-8'); + + expect(rss).not.toContain('Mixed Post'); + expect(atom).not.toContain('Mixed Post'); + expect(sitemap).not.toContain('https://example.com/category/news'); + expect(sitemap).not.toContain('https://example.com/category/aside'); + }); + it('generates static page routes at /{slug}/index.html for posts in category page', async () => { const posts = [ makePost({ id: 'page-1', slug: 'about', title: 'About', categories: ['page'] }),