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