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'] }),