feat: proper h1

This commit is contained in:
2026-02-17 22:40:06 +01:00
parent c0b944241e
commit 88f1ccf372
3 changed files with 260 additions and 0 deletions

View File

@@ -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;

View File

@@ -3,6 +3,33 @@
{% render 'partials/head', page_title: page_title %}
<body>
<main>
{% 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' %}
<h1 class="archive-heading">{{ archive_context.name }} - {{ min_date.day }}.{{ min_date.month }}.{{ min_date.year }} - {{ max_date.day }}.{{ max_date.month }}.{{ max_date.year }}</h1>
{% else %}
<h1 class="archive-heading">Archiv {{ min_date.day }}.{{ min_date.month }}.{{ min_date.year }} - {{ max_date.day }}.{{ max_date.month }}.{{ max_date.year }}</h1>
{% endif %}
{% else %}
{% if archive_context.kind == 'tag' or archive_context.kind == 'category' %}
<h1 class="archive-heading">{{ archive_context.name }}</h1>
{% 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] %}
<h1 class="archive-heading">Archiv {{ month_name }} {{ archive_context.year }}</h1>
{% elsif archive_context.kind == 'year' and archive_context.year %}
<h1 class="archive-heading">Archiv {{ archive_context.year }}</h1>
{% 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] %}
<h1 class="archive-heading">Archiv {{ archive_context.day }}. {{ day_month_name }} {{ archive_context.year }}</h1>
{% else %}
<h1 class="archive-heading">{{ page_title }}</h1>
{% endif %}
{% endif %}
{% endif %}
<section class="post-list" data-template="post-list" data-list-page="{{ is_list_page }}" data-first-page="{{ is_first_page }}" data-last-page="{{ is_last_page }}">
{% for day_block in day_blocks %}
{% if day_block.show_date_marker %}

View File

@@ -316,6 +316,143 @@ describe('PreviewServer', () => {
expect(html).toContain('<h1>Explicit Single Post Title</h1>');
});
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('<h1 class="archive-heading">Meine Blog Beschreibung</h1>');
const secondPageHtml = await (await fetch(`${server.getBaseUrl()}/page/2/`)).text();
expect(secondPageHtml).toContain('<h1 class="archive-heading">Archiv 1.1.2020 - 2.1.2020</h1>');
});
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('<h1 class="archive-heading">Archiv Februar 2020</h1>');
});
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('<h1 class="archive-heading">dev</h1>');
const secondPageHtml = await (await fetch(`${server.getBaseUrl()}/tag/dev/page/2/`)).text();
expect(secondPageHtml).toContain('<h1 class="archive-heading">dev - 1.1.2020 - 2.1.2020</h1>');
});
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('<h1 class="archive-heading">news</h1>');
const secondPageHtml = await (await fetch(`${server.getBaseUrl()}/category/news/page/2/`)).text();
expect(secondPageHtml).toContain('<h1 class="archive-heading">news - 1.1.2020 - 2.1.2020</h1>');
});
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'] });