fix: 0-byte index.html day archives

This commit is contained in:
2026-02-22 15:34:16 +01:00
parent b4109d7210
commit a7e7ae5b1a
8 changed files with 211 additions and 7 deletions

View File

@@ -1,5 +1,28 @@
import { describe, expect, it, vi } from 'vitest';
import { createGenerationRouteRenderer } from '../../src/main/engine/GenerationRouteRendererFactory';
import type { PostData } from '../../src/main/engine/PostEngine';
import {
createGenerationRouteRenderer,
createPreviewBackedGenerationRouteRenderer,
} from '../../src/main/engine/GenerationRouteRendererFactory';
function makePost(overrides: Partial<PostData> = {}): PostData {
const createdAt = overrides.createdAt ?? new Date('2025-01-15T10:00:00.000Z');
return {
id: overrides.id ?? 'post-1',
projectId: overrides.projectId ?? 'project',
title: overrides.title ?? 'Title',
slug: overrides.slug ?? 'title',
excerpt: overrides.excerpt,
content: overrides.content ?? 'Body',
status: overrides.status ?? 'published',
author: overrides.author,
createdAt,
updatedAt: overrides.updatedAt ?? createdAt,
publishedAt: overrides.publishedAt ?? createdAt,
tags: overrides.tags ?? [],
categories: overrides.categories ?? [],
};
}
describe('GenerationRouteRendererFactory', () => {
it('normalizes route keys and memoizes html rendering calls', async () => {
@@ -31,4 +54,86 @@ describe('GenerationRouteRendererFactory', () => {
expect(renderWithContext).toHaveBeenCalledTimes(1);
expect(renderWithContext).toHaveBeenCalledWith('/foo', expect.any(Object));
});
it('keeps day archive query caches distinct across different dates', async () => {
const posts = [
makePost({
id: 'day-1',
slug: 'day-1',
title: 'Post On Day One',
createdAt: new Date('2025-01-15T10:00:00.000Z'),
}),
makePost({
id: 'day-2',
slug: 'day-2',
title: 'Post On Day Two',
createdAt: new Date('2025-01-16T10:00:00.000Z'),
}),
];
const postEngine = {
getPostsFiltered: vi.fn(async (filter: {
status?: 'draft' | 'published' | 'archived';
startDate?: Date;
endDate?: Date;
year?: number;
month?: number;
}) => {
let filtered = posts.filter((post) => post.status === (filter.status ?? post.status));
if (typeof filter.year === 'number') {
filtered = filtered.filter((post) => post.createdAt.getFullYear() === filter.year);
}
if (typeof filter.month === 'number') {
filtered = filtered.filter((post) => post.createdAt.getMonth() === filter.month);
}
if (filter.startDate) {
filtered = filtered.filter((post) => post.createdAt >= filter.startDate as Date);
}
if (filter.endDate) {
filtered = filtered.filter((post) => post.createdAt <= filter.endDate as Date);
}
return filtered;
}),
getPublishedVersion: vi.fn(async () => null),
findPublishedBySlug: vi.fn(async (slug: string) => posts.find((post) => post.slug === slug) ?? null),
getPost: vi.fn(async (id: string) => posts.find((post) => post.id === id) ?? null),
hasPublishedVersion: vi.fn(async () => false),
setProjectContext: vi.fn(),
};
const renderRoute = createPreviewBackedGenerationRouteRenderer({
options: {
projectId: 'project',
dataDir: '/tmp',
projectName: 'Project',
},
maxPostsPerPage: 50,
publishedPostsForLookup: posts,
engines: {
postEngine,
mediaEngine: {
getAllMedia: vi.fn(async () => []),
setProjectContext: vi.fn(),
},
postMediaEngine: {
setProjectContext: vi.fn(),
getLinkedMediaForPost: vi.fn(async () => []),
getLinkedMediaDataForPost: vi.fn(async () => []),
},
},
});
const dayOneHtml = await renderRoute('/2025/01/15');
const dayTwoHtml = await renderRoute('/2025/01/16');
expect(dayOneHtml).toContain('Post On Day One');
expect(dayOneHtml).not.toContain('Post On Day Two');
expect(dayTwoHtml).toContain('Post On Day Two');
expect(dayTwoHtml).not.toContain('Post On Day One');
});
});

View File

@@ -0,0 +1,34 @@
import { describe, expect, it } from 'vitest';
import { PageRenderer, type HtmlRewriteContext } from '../../src/main/engine/PageRenderer';
const rewriteContext: HtmlRewriteContext = {
canonicalPostPathBySlug: new Map<string, string>(),
canonicalMediaPathBySourcePath: new Map<string, string>(),
};
describe('PageRenderer.renderPostList', () => {
it('renders base framework for empty day archive pages instead of returning empty html', async () => {
const renderer = new PageRenderer(
{ getAllMedia: async () => [] },
{
getLinkedMediaDataForPost: async () => [],
setProjectContext: () => {},
},
);
const html = await renderer.renderPostList([], rewriteContext, {
archiveGrouping: true,
routeKind: 'date',
archiveContext: { kind: 'day', year: 2026, month: 2, day: 22 },
basePathname: '/2026/02/22',
page_title: 'Test Blog',
language: 'en',
menu_items: [],
renderEmptyState: true,
});
expect(html).toContain('<!doctype html>');
expect(html).toContain('<section class="post-list"');
expect(html).toContain('<h1 class="archive-heading">Archive 22. February 2026</h1>');
});
});

View File

@@ -63,4 +63,31 @@ describe('SiteValidationDiffService', () => {
expect(result.expectedUrlCount).toBe(2);
expect(result.existingHtmlUrlCount).toBe(0);
});
it('treats zero-byte index pages as missing routes that need regeneration', async () => {
const tempRoot = path.join('/tmp', makeTempName());
const htmlDir = path.join(tempRoot, 'html');
await mkdir(path.join(htmlDir, '2026', '02', '22'), { recursive: true });
await writeFile(path.join(htmlDir, '2026', '02', '22', 'index.html'), '', 'utf-8');
const sitemapXml = [
'<?xml version="1.0" encoding="UTF-8"?>',
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
' <url><loc>https://example.com/2026/02/22/</loc></url>',
'</urlset>',
'',
].join('\n');
const result = await compareSitemapToHtml({
sitemapXml,
baseUrl: 'https://example.com',
htmlDir,
});
expect(result.missingUrlPaths).toEqual(['/2026/02/22']);
expect(result.extraUrlPaths).toEqual([]);
expect(result.expectedUrlCount).toBe(1);
expect(result.existingHtmlUrlCount).toBe(0);
});
});