feat: date and separator styling
This commit is contained in:
@@ -309,6 +309,20 @@ function buildCanonicalPostPath(post: PostData): string {
|
|||||||
return `/${year}/${month}/${day}/${post.slug}`;
|
return `/${year}/${month}/${day}/${post.slug}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatArchiveDate(date: Date): string {
|
||||||
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const year = String(date.getFullYear());
|
||||||
|
return `${day}.${month}.${year}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getArchiveDateKey(date: Date): string {
|
||||||
|
const year = String(date.getFullYear());
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
}
|
||||||
|
|
||||||
function getPageHtml(content: string, title: string, language: string): string {
|
function getPageHtml(content: string, title: string, language: string): string {
|
||||||
return `<!doctype html>
|
return `<!doctype html>
|
||||||
<html lang="${escapeHtml(language)}">
|
<html lang="${escapeHtml(language)}">
|
||||||
@@ -325,6 +339,12 @@ function getPageHtml(content: string, title: string, language: string): string {
|
|||||||
.post { border: 1px solid var(--muted-border-color); padding: 1rem; background: var(--card-background-color); }
|
.post { border: 1px solid var(--muted-border-color); padding: 1rem; background: var(--card-background-color); }
|
||||||
.post iframe { width: 100%; min-height: 20rem; }
|
.post iframe { width: 100%; min-height: 20rem; }
|
||||||
.macro-gallery, .macro-photo-archive { border: 1px dashed var(--muted-border-color); padding: .75rem; }
|
.macro-gallery, .macro-photo-archive { border: 1px dashed var(--muted-border-color); padding: .75rem; }
|
||||||
|
.archive-day-group { display: grid; grid-template-columns: 5.25rem 1fr; gap: 1.25rem; align-items: stretch; }
|
||||||
|
.archive-day-marker { display: flex; justify-content: center; align-items: center; color: var(--muted-color); }
|
||||||
|
.archive-day-marker span { writing-mode: vertical-rl; transform: rotate(180deg); letter-spacing: .16em; font-size: 1.05rem; font-weight: 600; text-transform: uppercase; }
|
||||||
|
.archive-day-posts { display: grid; gap: 1rem; }
|
||||||
|
.archive-day-separator { position: relative; height: 2px; width: 100%; color: var(--color); border-top: 1px solid currentColor; opacity: .18; margin: .45rem 0 .65rem; }
|
||||||
|
.archive-day-separator::before { content: ''; position: absolute; inset: 0; background: linear-gradient(to right, transparent 0%, transparent 18%, currentColor 58%, transparent 92%, transparent 100%); opacity: .85; }
|
||||||
</style>
|
</style>
|
||||||
<script defer src="/assets/lightbox.min.js"></script>
|
<script defer src="/assets/lightbox.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
@@ -549,14 +569,14 @@ export class PreviewServer {
|
|||||||
if (tagMatch) {
|
if (tagMatch) {
|
||||||
const tag = tagMatch[1];
|
const tag = tagMatch[1];
|
||||||
const posts = await this.loadPublishedSnapshots({ status: 'published', tags: [tag] }, pageOptions);
|
const posts = await this.loadPublishedSnapshots({ status: 'published', tags: [tag] }, pageOptions);
|
||||||
return this.renderPostList(posts, rewriteContext);
|
return this.renderPostList(posts, rewriteContext, { archiveGrouping: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
const categoryMatch = pagedPathname.match(/^\/category\/([^/]+)$/);
|
const categoryMatch = pagedPathname.match(/^\/category\/([^/]+)$/);
|
||||||
if (categoryMatch) {
|
if (categoryMatch) {
|
||||||
const category = categoryMatch[1];
|
const category = categoryMatch[1];
|
||||||
const posts = await this.loadPublishedSnapshots({ status: 'published', categories: [category] }, pageOptions);
|
const posts = await this.loadPublishedSnapshots({ status: 'published', categories: [category] }, pageOptions);
|
||||||
return this.renderPostList(posts, rewriteContext);
|
return this.renderPostList(posts, rewriteContext, { archiveGrouping: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
const daySlugMatch = pagedPathname.match(/^\/(\d{4})\/(\d{1,2})\/(\d{1,2})\/([^/]+)$/);
|
const daySlugMatch = pagedPathname.match(/^\/(\d{4})\/(\d{1,2})\/(\d{1,2})\/([^/]+)$/);
|
||||||
@@ -577,7 +597,7 @@ export class PreviewServer {
|
|||||||
const month = Number(dayMatch[2]);
|
const month = Number(dayMatch[2]);
|
||||||
const day = Number(dayMatch[3]);
|
const day = Number(dayMatch[3]);
|
||||||
const posts = await this.loadPostsForDay(year, month, day, pageOptions);
|
const posts = await this.loadPostsForDay(year, month, day, pageOptions);
|
||||||
return this.renderPostList(posts, rewriteContext);
|
return this.renderPostList(posts, rewriteContext, { archiveGrouping: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
const monthMatch = pagedPathname.match(/^\/(\d{4})\/(\d{1,2})$/);
|
const monthMatch = pagedPathname.match(/^\/(\d{4})\/(\d{1,2})$/);
|
||||||
@@ -586,14 +606,14 @@ export class PreviewServer {
|
|||||||
const month = Number(monthMatch[2]);
|
const month = Number(monthMatch[2]);
|
||||||
if (month < 1 || month > 12) return null;
|
if (month < 1 || month > 12) return null;
|
||||||
const posts = await this.loadPublishedSnapshots({ status: 'published', year, month: month - 1 }, pageOptions);
|
const posts = await this.loadPublishedSnapshots({ status: 'published', year, month: month - 1 }, pageOptions);
|
||||||
return this.renderPostList(posts, rewriteContext);
|
return this.renderPostList(posts, rewriteContext, { archiveGrouping: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
const yearMatch = pagedPathname.match(/^\/(\d{4})$/);
|
const yearMatch = pagedPathname.match(/^\/(\d{4})$/);
|
||||||
if (yearMatch) {
|
if (yearMatch) {
|
||||||
const year = Number(yearMatch[1]);
|
const year = Number(yearMatch[1]);
|
||||||
const posts = await this.loadPublishedSnapshots({ status: 'published', year }, pageOptions);
|
const posts = await this.loadPublishedSnapshots({ status: 'published', year }, pageOptions);
|
||||||
return this.renderPostList(posts, rewriteContext);
|
return this.renderPostList(posts, rewriteContext, { archiveGrouping: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
const pageSlugMatch = pagedPathname.match(/^\/([^/]+)$/);
|
const pageSlugMatch = pagedPathname.match(/^\/([^/]+)$/);
|
||||||
@@ -719,7 +739,11 @@ export class PreviewServer {
|
|||||||
return snapshots;
|
return snapshots;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async renderPostList(posts: PostData[], rewriteContext: HtmlRewriteContext): Promise<string> {
|
private async renderPostList(
|
||||||
|
posts: PostData[],
|
||||||
|
rewriteContext: HtmlRewriteContext,
|
||||||
|
options?: { archiveGrouping?: boolean },
|
||||||
|
): Promise<string> {
|
||||||
const renderablePosts = await Promise.all(posts.map(async (post) => {
|
const renderablePosts = await Promise.all(posts.map(async (post) => {
|
||||||
if (post.status === 'published' && !post.content) {
|
if (post.status === 'published' && !post.content) {
|
||||||
const fullPost = await this.postEngine.getPost(post.id);
|
const fullPost = await this.postEngine.getPost(post.id);
|
||||||
@@ -728,8 +752,41 @@ export class PreviewServer {
|
|||||||
return post;
|
return post;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const rendered = await Promise.all(renderablePosts.map((post) => renderPostHtml(post, rewriteContext)));
|
if (!options?.archiveGrouping) {
|
||||||
return rendered.join('\n');
|
const rendered = await Promise.all(renderablePosts.map((post) => renderPostHtml(post, rewriteContext)));
|
||||||
|
return rendered.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
const groups: Array<{ dateLabel: string; posts: PostData[] }> = [];
|
||||||
|
let currentGroup: { key: string; dateLabel: string; posts: PostData[] } | null = null;
|
||||||
|
|
||||||
|
for (const post of renderablePosts) {
|
||||||
|
const key = getArchiveDateKey(post.createdAt);
|
||||||
|
if (!currentGroup || currentGroup.key !== key) {
|
||||||
|
currentGroup = {
|
||||||
|
key,
|
||||||
|
dateLabel: formatArchiveDate(post.createdAt),
|
||||||
|
posts: [],
|
||||||
|
};
|
||||||
|
groups.push(currentGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
currentGroup.posts.push(post);
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderedGroups = await Promise.all(groups.map(async (group) => {
|
||||||
|
const renderedPosts = await Promise.all(group.posts.map((post) => renderPostHtml(post, rewriteContext)));
|
||||||
|
return `<section class="archive-day-group"><aside class="archive-day-marker"><span>${escapeHtml(group.dateLabel)}</span></aside><div class="archive-day-posts">${renderedPosts.join('\n')}</div></section>`;
|
||||||
|
}));
|
||||||
|
|
||||||
|
return renderedGroups
|
||||||
|
.map((groupHtml, index) => {
|
||||||
|
if (index === renderedGroups.length - 1) {
|
||||||
|
return groupHtml;
|
||||||
|
}
|
||||||
|
return `${groupHtml}\n<div class="archive-day-separator" aria-hidden="true"></div>`;
|
||||||
|
})
|
||||||
|
.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
private async buildHtmlRewriteContext(): Promise<HtmlRewriteContext> {
|
private async buildHtmlRewriteContext(): Promise<HtmlRewriteContext> {
|
||||||
|
|||||||
@@ -238,6 +238,42 @@ describe('PreviewServer', () => {
|
|||||||
expect(dayHtml).not.toContain('Month Post');
|
expect(dayHtml).not.toContain('Month Post');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('renders archive pages grouped by day with rotated date markers and separators', async () => {
|
||||||
|
const posts = [
|
||||||
|
makePost({ id: 'a1', slug: 'a1', title: 'A1', createdAt: new Date('2025-02-14T12:00:00.000Z') }),
|
||||||
|
makePost({ id: 'a2', slug: 'a2', title: 'A2', createdAt: new Date('2025-02-14T08:00:00.000Z') }),
|
||||||
|
makePost({ id: 'b1', slug: 'b1', title: 'B1', createdAt: new Date('2025-02-13T09:00:00.000Z') }),
|
||||||
|
];
|
||||||
|
|
||||||
|
server = new PreviewServer({
|
||||||
|
postEngine: makeEngine(posts),
|
||||||
|
settingsEngine: makeSettings(50),
|
||||||
|
getActiveProjectContext: async () => ({ projectId: 'default' }),
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.start(0);
|
||||||
|
|
||||||
|
const html = await (await fetch(`${server.getBaseUrl()}/2025/2/`)).text();
|
||||||
|
|
||||||
|
expect(html).toContain('archive-day-group');
|
||||||
|
expect(html).toContain('archive-day-marker');
|
||||||
|
expect(html).toContain('14.02.2025');
|
||||||
|
expect(html).toContain('13.02.2025');
|
||||||
|
|
||||||
|
const markerCount = (html.match(/class="archive-day-marker"/g) || []).length;
|
||||||
|
expect(markerCount).toBe(2);
|
||||||
|
|
||||||
|
const separatorCount = (html.match(/class="archive-day-separator"/g) || []).length;
|
||||||
|
expect(separatorCount).toBe(1);
|
||||||
|
|
||||||
|
expect(html).toContain('.archive-day-separator { position: relative; height: 2px;');
|
||||||
|
expect(html).toContain('color: var(--color);');
|
||||||
|
expect(html).toContain('border-top: 1px solid currentColor;');
|
||||||
|
expect(html).toContain('opacity: .18;');
|
||||||
|
expect(html).toContain('.archive-day-separator::before');
|
||||||
|
expect(html).toContain('linear-gradient(to right, transparent 0%, transparent 18%, currentColor 58%, transparent 92%, transparent 100%)');
|
||||||
|
});
|
||||||
|
|
||||||
it('supports day-and-slug post route', async () => {
|
it('supports day-and-slug post route', async () => {
|
||||||
const post = makePost({ id: 'one', title: 'Single Post', slug: 'single-post', createdAt: new Date('2025-02-14T10:00:00.000Z') });
|
const post = makePost({ id: 'one', title: 'Single Post', slug: 'single-post', createdAt: new Date('2025-02-14T10:00:00.000Z') });
|
||||||
|
|
||||||
@@ -284,6 +320,50 @@ describe('PreviewServer', () => {
|
|||||||
expect(pageHtml).not.toContain('About Blog Post');
|
expect(pageHtml).not.toContain('About Blog Post');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('renders tag and category pages with archive-style day grouping', async () => {
|
||||||
|
const tagDayOneA = makePost({
|
||||||
|
id: 'tag-day-one-a',
|
||||||
|
title: 'Tag Day One A',
|
||||||
|
slug: 'tag-day-one-a',
|
||||||
|
tags: ['dev'],
|
||||||
|
categories: ['news'],
|
||||||
|
createdAt: new Date('2025-03-10T14:00:00.000Z'),
|
||||||
|
});
|
||||||
|
const tagDayOneB = makePost({
|
||||||
|
id: 'tag-day-one-b',
|
||||||
|
title: 'Tag Day One B',
|
||||||
|
slug: 'tag-day-one-b',
|
||||||
|
tags: ['dev'],
|
||||||
|
categories: ['news'],
|
||||||
|
createdAt: new Date('2025-03-10T08:00:00.000Z'),
|
||||||
|
});
|
||||||
|
const tagDayTwo = makePost({
|
||||||
|
id: 'tag-day-two',
|
||||||
|
title: 'Tag Day Two',
|
||||||
|
slug: 'tag-day-two',
|
||||||
|
tags: ['dev'],
|
||||||
|
categories: ['news'],
|
||||||
|
createdAt: new Date('2025-03-09T09:00:00.000Z'),
|
||||||
|
});
|
||||||
|
|
||||||
|
server = new PreviewServer({
|
||||||
|
postEngine: makeEngine([tagDayOneA, tagDayOneB, tagDayTwo]),
|
||||||
|
settingsEngine: makeSettings(50),
|
||||||
|
getActiveProjectContext: async () => ({ projectId: 'default' }),
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.start(0);
|
||||||
|
|
||||||
|
const tagHtml = await (await fetch(`${server.getBaseUrl()}/tag/dev/`)).text();
|
||||||
|
expect(tagHtml).toContain('class="archive-day-group"');
|
||||||
|
expect(tagHtml).toContain('10.03.2025');
|
||||||
|
expect(tagHtml).toContain('09.03.2025');
|
||||||
|
|
||||||
|
const categoryHtml = await (await fetch(`${server.getBaseUrl()}/category/news/`)).text();
|
||||||
|
expect(categoryHtml).toContain('class="archive-day-group"');
|
||||||
|
expect(categoryHtml).toContain('class="archive-day-separator"');
|
||||||
|
});
|
||||||
|
|
||||||
it('supports /page/<num> suffix on list routes', async () => {
|
it('supports /page/<num> suffix on list routes', async () => {
|
||||||
const baseTimestamp = Date.UTC(2020, 9, 31, 23, 59, 59);
|
const baseTimestamp = Date.UTC(2020, 9, 31, 23, 59, 59);
|
||||||
const posts = Array.from({ length: 120 }).map((_, index) => {
|
const posts = Array.from({ length: 120 }).map((_, index) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user