Feature/worker threads generation (#43)

* Add worker threads architecture plan for blog generation

* fix: tries to optimize rendering, still slow

* feat: moved site rendering into web worker

* fix: calendar grabs from central data source for calendar

* fix: feeds now use blog language content and not canonical content

---------

Co-authored-by: hugo <hugoms@me.com>
This commit is contained in:
Georg Bauer
2026-03-09 22:49:25 +01:00
committed by GitHub
parent b855d61524
commit 4f9be93c6d
42 changed files with 3617 additions and 346 deletions

View File

@@ -57,6 +57,7 @@ vi.mock('../../src/main/database/generatedFileHashStore', () => ({
getGeneratedFileHash: getGeneratedFileHashMock,
getGeneratedFileHashRecord: getGeneratedFileHashRecordMock,
setGeneratedFileHash: setGeneratedFileHashMock,
getAllGeneratedFileHashes: vi.fn(async () => new Map<string, string>()),
}));
vi.mock('../../src/main/database', () => ({
@@ -76,7 +77,7 @@ vi.mock('../../src/main/engine/PostEngine', async (importOriginal) => {
getPostTranslation: vi.fn(async () => null),
getPostTranslations: vi.fn(async () => []),
setProjectContext: vi.fn(),
};
} as Record<string, any>;
return {
...actual,
getPostEngine: vi.fn(() => mockPostEngine),
@@ -211,6 +212,7 @@ describe('BlogGenerationEngine', () => {
options?: Partial<{
maxPostsPerPage: number;
language: string;
blogLanguages: string[];
pageTitle: string;
picoTheme: string;
categorySettings: Record<string, { renderInLists: boolean; showTitle: boolean }>;
@@ -229,6 +231,7 @@ describe('BlogGenerationEngine', () => {
baseUrl: 'https://example.com',
maxPostsPerPage: options?.maxPostsPerPage,
language: options?.language,
blogLanguages: options?.blogLanguages,
pageTitle: options?.pageTitle,
picoTheme: options?.picoTheme as any,
categorySettings: options?.categorySettings,
@@ -2148,6 +2151,154 @@ describe('BlogGenerationEngine', () => {
expect(result.postCount).toBe(0);
});
it('language subtree list pages show translated title and excerpt, not canonical language', async () => {
const posts = [
makePost({
id: 'de-post-1',
slug: 'german-post',
title: 'Deutscher Titel',
excerpt: 'Deutscher Auszug',
content: '# Deutscher Inhalt',
language: 'de',
categories: ['tech'],
createdAt: new Date('2025-06-10T10:00:00Z'),
availableLanguages: ['de', 'en'],
}),
];
const translationMap = new Map<string, PostTranslationData[]>();
translationMap.set('de-post-1', [{
id: 'en-trans-1',
projectId: 'test',
translationFor: 'de-post-1',
language: 'en',
title: 'English Title',
excerpt: 'English excerpt',
content: '# English Content',
status: 'published',
createdAt: new Date('2025-06-10T10:00:00Z'),
updatedAt: new Date('2025-06-10T10:00:00Z'),
publishedAt: new Date('2025-06-10T10:00:00Z'),
filePath: '',
}]);
mockPostEngine.getPublishedTranslationsForRoutePosts = vi.fn().mockResolvedValue(translationMap);
await generate(posts, { language: 'de', blogLanguages: ['de', 'en'] });
// /en/ subtree list page should show English title and excerpt
const enIndex = await readFile(path.join(tempDir, 'html', 'en', 'index.html'), 'utf-8');
expect(enIndex).toContain('English Title');
expect(enIndex).toContain('English excerpt');
expect(enIndex).not.toContain('Deutscher Titel');
expect(enIndex).not.toContain('Deutscher Auszug');
// Main blog list page should still show German
const deIndex = await readFile(path.join(tempDir, 'html', 'index.html'), 'utf-8');
expect(deIndex).toContain('Deutscher Titel');
expect(deIndex).not.toContain('English Title');
});
it('main blog list pages show translated content when canonical language differs from project language', async () => {
const posts = [
makePost({
id: 'en-post-1',
slug: 'english-post',
title: 'English Title',
excerpt: 'English excerpt',
content: '# English Content',
language: 'en',
categories: ['tech'],
createdAt: new Date('2025-06-10T10:00:00Z'),
availableLanguages: ['en', 'de'],
}),
];
const translationMap = new Map<string, PostTranslationData[]>();
translationMap.set('en-post-1', [{
id: 'de-trans-1',
projectId: 'test',
translationFor: 'en-post-1',
language: 'de',
title: 'Deutscher Titel',
excerpt: 'Deutscher Auszug',
content: '# Deutscher Inhalt',
status: 'published',
createdAt: new Date('2025-06-10T10:00:00Z'),
updatedAt: new Date('2025-06-10T10:00:00Z'),
publishedAt: new Date('2025-06-10T10:00:00Z'),
filePath: '',
}]);
mockPostEngine.getPublishedTranslationsForRoutePosts = vi.fn().mockResolvedValue(translationMap);
await generate(posts, { language: 'de', blogLanguages: ['de', 'en'] });
// Main blog (de) should show German translated title, not English canonical
const deIndex = await readFile(path.join(tempDir, 'html', 'index.html'), 'utf-8');
expect(deIndex).toContain('Deutscher Titel');
expect(deIndex).not.toContain('English Title');
// /en/ subtree should show English canonical title
const enIndex = await readFile(path.join(tempDir, 'html', 'en', 'index.html'), 'utf-8');
expect(enIndex).toContain('English Title');
expect(enIndex).not.toContain('Deutscher Titel');
});
it('language subtree RSS and Atom feeds use translated titles and content', async () => {
const posts = [
makePost({
id: 'de-post-1',
slug: 'german-post',
title: 'Deutscher Titel',
content: '# Deutscher Inhalt\n\nDeutscher Body Text',
language: 'de',
categories: ['tech'],
createdAt: new Date('2025-06-10T10:00:00Z'),
availableLanguages: ['de', 'en'],
}),
];
const translationFilePath = path.join(tempDir, 'posts', 'german-post.en.md');
await mkdir(path.join(tempDir, 'posts'), { recursive: true });
await writeFile(translationFilePath, '---\ntranslationFor: de-post-1\nlanguage: en\ntitle: English Title\n---\n# English Content\n\nEnglish Body Text');
const translationMap = new Map<string, PostTranslationData[]>();
translationMap.set('de-post-1', [{
id: 'en-trans-1',
projectId: 'test',
translationFor: 'de-post-1',
language: 'en',
title: 'English Title',
content: '# English Content\n\nEnglish Body Text',
status: 'published',
createdAt: new Date('2025-06-10T10:00:00Z'),
updatedAt: new Date('2025-06-10T10:00:00Z'),
publishedAt: new Date('2025-06-10T10:00:00Z'),
filePath: translationFilePath,
}]);
mockPostEngine.getPublishedTranslationsForRoutePosts = vi.fn().mockResolvedValue(translationMap);
await generate(posts, { language: 'de', blogLanguages: ['de', 'en'] });
// /en/ RSS feed should use English translated title and content
const enRss = await readFile(path.join(tempDir, 'html', 'en', 'rss.xml'), 'utf-8');
expect(enRss).toContain('English Title');
expect(enRss).not.toContain('Deutscher Titel');
expect(enRss).toContain('English Body Text');
expect(enRss).not.toContain('Deutscher Body Text');
// /en/ Atom feed should use English translated title and content
const enAtom = await readFile(path.join(tempDir, 'html', 'en', 'atom.xml'), 'utf-8');
expect(enAtom).toContain('English Title');
expect(enAtom).not.toContain('Deutscher Titel');
expect(enAtom).toContain('English Body Text');
expect(enAtom).not.toContain('Deutscher Body Text');
// Root RSS should keep German canonical content
const deRss = await readFile(path.join(tempDir, 'html', 'rss.xml'), 'utf-8');
expect(deRss).toContain('Deutscher Titel');
expect(deRss).not.toContain('English Title');
});
it('generates pagination links in list pages', async () => {
const posts: PostData[] = [];
for (let i = 0; i < 4; i++) {