From 88f1ccf3723453c3cc161ac77ab156907333c991 Mon Sep 17 00:00:00 2001 From: hugo Date: Tue, 17 Feb 2026 22:40:06 +0100 Subject: [PATCH] feat: proper h1 --- src/main/engine/PreviewServer.ts | 96 +++++++++++++++ src/main/engine/templates/post-list.liquid | 27 ++++ tests/engine/PreviewServer.test.ts | 137 +++++++++++++++++++++ 3 files changed, 260 insertions(+) diff --git a/src/main/engine/PreviewServer.ts b/src/main/engine/PreviewServer.ts index 1ca995f..124901c 100644 --- a/src/main/engine/PreviewServer.ts +++ b/src/main/engine/PreviewServer.ts @@ -69,6 +69,17 @@ interface DayBlockContext { interface PostListTemplateContext { page_title: string; language: string; + is_date_archive: boolean; + show_archive_range_heading: boolean; + archive_context: { + kind: 'root' | 'year' | 'month' | 'day' | 'tag' | 'category'; + name: string | null; + year: number | null; + month: number | null; + day: number | null; + } | null; + min_date: { day: number; month: number; year: number } | null; + max_date: { day: number; month: number; year: number } | null; is_list_page: boolean; is_first_page: boolean; is_last_page: boolean; @@ -81,6 +92,16 @@ interface PostListTemplateContext { day_blocks: DayBlockContext[]; } +type ArchiveRouteKind = 'date' | 'non-date'; + +type DateArchiveContext = { + kind: 'root' | 'year' | 'month' | 'day' | 'tag' | 'category'; + name?: string; + year?: number; + month?: number; + day?: number; +}; + interface SinglePostTemplateContext { page_title: string; language: string; @@ -360,6 +381,14 @@ function getArchiveDateKey(date: Date): string { return `${year}-${month}-${day}`; } +function toDateParts(date: Date): { day: number; month: number; year: number } { + return { + day: date.getDate(), + month: date.getMonth() + 1, + year: date.getFullYear(), + }; +} + function buildPaginationHref(basePathname: string, page: number): string { const base = basePathname === '/' ? '' : basePathname; if (page <= 1) { @@ -646,6 +675,8 @@ export class PreviewServer { const result = await this.loadPublishedSnapshotsPage({ status: 'published' }, pageOptions); return this.renderPostList(result.posts, rewriteContext, { archiveGrouping: false, + routeKind: 'date', + archiveContext: { kind: 'root' }, basePathname: pagedPathname, pagination: { page, maxPostsPerPage, totalPosts: result.totalPosts }, page_title: pageContext.pageTitle, @@ -659,6 +690,8 @@ export class PreviewServer { const result = await this.loadPublishedSnapshotsPage({ status: 'published', tags: [tag] }, pageOptions); return this.renderPostList(result.posts, rewriteContext, { archiveGrouping: true, + routeKind: 'non-date', + archiveContext: { kind: 'tag', name: tag }, basePathname: pagedPathname, pagination: { page, maxPostsPerPage, totalPosts: result.totalPosts }, page_title: pageContext.pageTitle, @@ -672,6 +705,8 @@ export class PreviewServer { const result = await this.loadPublishedSnapshotsPage({ status: 'published', categories: [category] }, pageOptions); return this.renderPostList(result.posts, rewriteContext, { archiveGrouping: true, + routeKind: 'non-date', + archiveContext: { kind: 'category', name: category }, basePathname: pagedPathname, pagination: { page, maxPostsPerPage, totalPosts: result.totalPosts }, page_title: pageContext.pageTitle, @@ -702,6 +737,8 @@ export class PreviewServer { const result = await this.loadPostsForDayPage(year, month, day, pageOptions); return this.renderPostList(result.posts, rewriteContext, { archiveGrouping: true, + routeKind: 'date', + archiveContext: { kind: 'day', year, month, day }, basePathname: pagedPathname, pagination: { page, maxPostsPerPage, totalPosts: result.totalPosts }, page_title: pageContext.pageTitle, @@ -717,6 +754,8 @@ export class PreviewServer { const result = await this.loadPublishedSnapshotsPage({ status: 'published', year, month: month - 1 }, pageOptions); return this.renderPostList(result.posts, rewriteContext, { archiveGrouping: true, + routeKind: 'date', + archiveContext: { kind: 'month', year, month }, basePathname: pagedPathname, pagination: { page, maxPostsPerPage, totalPosts: result.totalPosts }, page_title: pageContext.pageTitle, @@ -730,6 +769,8 @@ export class PreviewServer { const result = await this.loadPublishedSnapshotsPage({ status: 'published', year }, pageOptions); return this.renderPostList(result.posts, rewriteContext, { archiveGrouping: true, + routeKind: 'date', + archiveContext: { kind: 'year', year }, basePathname: pagedPathname, pagination: { page, maxPostsPerPage, totalPosts: result.totalPosts }, page_title: pageContext.pageTitle, @@ -913,6 +954,8 @@ export class PreviewServer { rewriteContext: HtmlRewriteContext, options: { archiveGrouping: boolean; + routeKind: ArchiveRouteKind; + archiveContext?: DateArchiveContext; basePathname: string; page_title: string; language: string; @@ -977,9 +1020,60 @@ export class PreviewServer { ? buildPaginationHref(options.basePathname, (pagination as PaginationContext).page + 1) : ''; + let minDateParts: { day: number; month: number; year: number } | null = null; + let maxDateParts: { day: number; month: number; year: number } | null = null; + + const hasRangeHeading = Boolean( + !isFirstPage + && posts.length > 0 + && ( + options.routeKind === 'date' + || options.archiveContext?.kind === 'tag' + || options.archiveContext?.kind === 'category' + ), + ); + + if (hasRangeHeading) { + let minDate = posts[0].createdAt; + let maxDate = posts[0].createdAt; + + for (const post of posts) { + if (post.createdAt.getTime() < minDate.getTime()) { + minDate = post.createdAt; + } + if (post.createdAt.getTime() > maxDate.getTime()) { + maxDate = post.createdAt; + } + } + + minDateParts = toDateParts(minDate); + maxDateParts = toDateParts(maxDate); + } + return { page_title: options.page_title, language: options.language, + is_date_archive: options.routeKind === 'date', + show_archive_range_heading: hasRangeHeading, + archive_context: options.routeKind === 'date' + ? { + kind: options.archiveContext?.kind ?? 'root', + name: options.archiveContext?.name ?? null, + year: options.archiveContext?.year ?? null, + month: options.archiveContext?.month ?? null, + day: options.archiveContext?.day ?? null, + } + : options.archiveContext + ? { + kind: options.archiveContext.kind, + name: options.archiveContext.name ?? null, + year: options.archiveContext.year ?? null, + month: options.archiveContext.month ?? null, + day: options.archiveContext.day ?? null, + } + : null, + min_date: minDateParts, + max_date: maxDateParts, is_list_page: isListPage, is_first_page: isFirstPage, is_last_page: isLastPage, @@ -998,6 +1092,8 @@ export class PreviewServer { rewriteContext: HtmlRewriteContext, options: { archiveGrouping: boolean; + routeKind: ArchiveRouteKind; + archiveContext?: DateArchiveContext; basePathname: string; page_title: string; language: string; diff --git a/src/main/engine/templates/post-list.liquid b/src/main/engine/templates/post-list.liquid index 678971f..9f1d6a2 100644 --- a/src/main/engine/templates/post-list.liquid +++ b/src/main/engine/templates/post-list.liquid @@ -3,6 +3,33 @@ {% render 'partials/head', page_title: page_title %}
+ {% if archive_context %} + {% assign month_names_de = "Januar|Februar|März|April|Mai|Juni|Juli|August|September|Oktober|November|Dezember" | split: "|" %} + {% if show_archive_range_heading and min_date and max_date %} + {% if archive_context.kind == 'tag' or archive_context.kind == 'category' %} +

{{ archive_context.name }} - {{ min_date.day }}.{{ min_date.month }}.{{ min_date.year }} - {{ max_date.day }}.{{ max_date.month }}.{{ max_date.year }}

+ {% else %} +

Archiv {{ min_date.day }}.{{ min_date.month }}.{{ min_date.year }} - {{ max_date.day }}.{{ max_date.month }}.{{ max_date.year }}

+ {% endif %} + {% else %} + {% if archive_context.kind == 'tag' or archive_context.kind == 'category' %} +

{{ archive_context.name }}

+ {% elsif archive_context.kind == 'month' and archive_context.month and archive_context.year %} + {% assign month_index = archive_context.month | minus: 1 %} + {% assign month_name = month_names_de[month_index] %} +

Archiv {{ month_name }} {{ archive_context.year }}

+ {% elsif archive_context.kind == 'year' and archive_context.year %} +

Archiv {{ archive_context.year }}

+ {% elsif archive_context.kind == 'day' and archive_context.day and archive_context.month and archive_context.year %} + {% assign day_month_index = archive_context.month | minus: 1 %} + {% assign day_month_name = month_names_de[day_month_index] %} +

Archiv {{ archive_context.day }}. {{ day_month_name }} {{ archive_context.year }}

+ {% else %} +

{{ page_title }}

+ {% endif %} + {% endif %} + {% endif %} +
{% for day_block in day_blocks %} {% if day_block.show_date_marker %} diff --git a/tests/engine/PreviewServer.test.ts b/tests/engine/PreviewServer.test.ts index c840444..ad8bfb4 100644 --- a/tests/engine/PreviewServer.test.ts +++ b/tests/engine/PreviewServer.test.ts @@ -316,6 +316,143 @@ describe('PreviewServer', () => { expect(html).toContain('

Explicit Single Post Title

'); }); + it('uses blog description as h1 on first date archive page and date range h1 on later pages', async () => { + const posts = [ + makePost({ + id: 'd-1', + slug: 'd-1', + title: 'D1', + content: 'Body 1', + createdAt: new Date('2020-02-05T10:00:00.000Z'), + }), + makePost({ + id: 'd-2', + slug: 'd-2', + title: 'D2', + content: 'Body 2', + createdAt: new Date('2020-02-04T10:00:00.000Z'), + }), + makePost({ + id: 'd-3', + slug: 'd-3', + title: 'D3', + content: 'Body 3', + createdAt: new Date('2020-01-02T10:00:00.000Z'), + }), + makePost({ + id: 'd-4', + slug: 'd-4', + title: 'D4', + content: 'Body 4', + createdAt: new Date('2020-01-01T10:00:00.000Z'), + }), + ]; + + server = new PreviewServer({ + postEngine: makeEngine(posts), + settingsEngine: { + setProjectContext: vi.fn(), + async getProjectMetadata() { + return { + description: 'Meine Blog Beschreibung', + maxPostsPerPage: 2, + }; + }, + }, + getActiveProjectContext: async () => ({ projectId: 'default' }), + }); + + await server.start(0); + + const firstPageHtml = await (await fetch(`${server.getBaseUrl()}/`)).text(); + expect(firstPageHtml).toContain('

Meine Blog Beschreibung

'); + + const secondPageHtml = await (await fetch(`${server.getBaseUrl()}/page/2/`)).text(); + expect(secondPageHtml).toContain('

Archiv 1.1.2020 - 2.1.2020

'); + }); + + it('renders month archive heading with German month name on first page', async () => { + const posts = [ + makePost({ id: 'm-1', slug: 'm-1', title: 'M1', content: 'Body 1', createdAt: new Date('2020-02-05T10:00:00.000Z') }), + makePost({ id: 'm-2', slug: 'm-2', title: 'M2', content: 'Body 2', createdAt: new Date('2020-02-04T10:00:00.000Z') }), + ]; + + server = new PreviewServer({ + postEngine: makeEngine(posts), + settingsEngine: { + setProjectContext: vi.fn(), + async getProjectMetadata() { + return { + description: 'Meine Blog Beschreibung', + maxPostsPerPage: 50, + }; + }, + }, + getActiveProjectContext: async () => ({ projectId: 'default' }), + }); + + await server.start(0); + + const monthPageHtml = await (await fetch(`${server.getBaseUrl()}/2020/2/`)).text(); + expect(monthPageHtml).toContain('

Archiv Februar 2020

'); + }); + + it('renders tag heading on first page and adds date range on later pages', async () => { + const posts = [ + makePost({ id: 't-1', slug: 't-1', title: 'T1', content: 'Body 1', tags: ['dev'], createdAt: new Date('2020-02-05T10:00:00.000Z') }), + makePost({ id: 't-2', slug: 't-2', title: 'T2', content: 'Body 2', tags: ['dev'], createdAt: new Date('2020-02-04T10:00:00.000Z') }), + makePost({ id: 't-3', slug: 't-3', title: 'T3', content: 'Body 3', tags: ['dev'], createdAt: new Date('2020-01-02T10:00:00.000Z') }), + makePost({ id: 't-4', slug: 't-4', title: 'T4', content: 'Body 4', tags: ['dev'], createdAt: new Date('2020-01-01T10:00:00.000Z') }), + ]; + + server = new PreviewServer({ + postEngine: makeEngine(posts), + settingsEngine: { + setProjectContext: vi.fn(), + async getProjectMetadata() { + return { description: 'Beschreibung', maxPostsPerPage: 2 }; + }, + }, + getActiveProjectContext: async () => ({ projectId: 'default' }), + }); + + await server.start(0); + + const firstPageHtml = await (await fetch(`${server.getBaseUrl()}/tag/dev/`)).text(); + expect(firstPageHtml).toContain('

dev

'); + + const secondPageHtml = await (await fetch(`${server.getBaseUrl()}/tag/dev/page/2/`)).text(); + expect(secondPageHtml).toContain('

dev - 1.1.2020 - 2.1.2020

'); + }); + + it('renders category heading on first page and adds date range on later pages', async () => { + const posts = [ + makePost({ id: 'c-1', slug: 'c-1', title: 'C1', content: 'Body 1', categories: ['news'], createdAt: new Date('2020-02-05T10:00:00.000Z') }), + makePost({ id: 'c-2', slug: 'c-2', title: 'C2', content: 'Body 2', categories: ['news'], createdAt: new Date('2020-02-04T10:00:00.000Z') }), + makePost({ id: 'c-3', slug: 'c-3', title: 'C3', content: 'Body 3', categories: ['news'], createdAt: new Date('2020-01-02T10:00:00.000Z') }), + makePost({ id: 'c-4', slug: 'c-4', title: 'C4', content: 'Body 4', categories: ['news'], createdAt: new Date('2020-01-01T10:00:00.000Z') }), + ]; + + server = new PreviewServer({ + postEngine: makeEngine(posts), + settingsEngine: { + setProjectContext: vi.fn(), + async getProjectMetadata() { + return { description: 'Beschreibung', maxPostsPerPage: 2 }; + }, + }, + getActiveProjectContext: async () => ({ projectId: 'default' }), + }); + + await server.start(0); + + const firstPageHtml = await (await fetch(`${server.getBaseUrl()}/category/news/`)).text(); + expect(firstPageHtml).toContain('

news

'); + + const secondPageHtml = await (await fetch(`${server.getBaseUrl()}/category/news/page/2/`)).text(); + expect(secondPageHtml).toContain('

news - 1.1.2020 - 2.1.2020

'); + }); + it('supports tag, category, and page-slug routes', async () => { const tagged = makePost({ id: 'tag1', title: 'Tagged', slug: 'tagged', tags: ['dev'] }); const categorized = makePost({ id: 'cat1', title: 'Categorized', slug: 'categorized', categories: ['news'] });