From 8849f638a53d218df892607301f843e7b88caed3 Mon Sep 17 00:00:00 2001 From: Ken Date: Tue, 26 May 2026 23:06:36 +0000 Subject: [PATCH] feat: create post page template with dynamic routing and prose wrapper --- src/pages/posts/[...slug].astro | 49 ++++++++++++ tests/post-page.test.mjs | 138 ++++++++++++++++++++++++++++++++ 2 files changed, 187 insertions(+) create mode 100644 src/pages/posts/[...slug].astro create mode 100644 tests/post-page.test.mjs diff --git a/src/pages/posts/[...slug].astro b/src/pages/posts/[...slug].astro new file mode 100644 index 0000000..3ff1d24 --- /dev/null +++ b/src/pages/posts/[...slug].astro @@ -0,0 +1,49 @@ +--- +import { getCollection, render } from 'astro:content'; +import type { CollectionEntry } from 'astro:content'; +import BaseLayout from '../../layouts/BaseLayout.astro'; +import NavBar from '../../components/NavBar.astro'; +import PostHeader from '../../components/PostHeader.astro'; +import { getReadingTime } from '../../utils/reading-time'; +import '../../styles/prose.css'; + +export async function getStaticPaths() { + const posts = await getCollection('posts'); + return posts.map((post) => ({ + params: { slug: post.id }, + props: { post }, + })); +} + +interface Props { + post: CollectionEntry<'posts'>; +} + +const { post } = Astro.props; +const { Content } = await render(post); +const readingTime = post.body ? getReadingTime(post.body) : undefined; +--- + + + + +
+
+ +
+
+
+ + \ No newline at end of file diff --git a/tests/post-page.test.mjs b/tests/post-page.test.mjs new file mode 100644 index 0000000..f03f27c --- /dev/null +++ b/tests/post-page.test.mjs @@ -0,0 +1,138 @@ +import { describe, it } from 'node:test'; +import { strict as assert } from 'node:assert'; +import { readFileSync, existsSync } from 'node:fs'; +import { resolve } from 'node:path'; + +const pagePath = resolve('src/pages/posts/[...slug].astro'); + +function readPage() { + return readFileSync(pagePath, 'utf-8'); +} + +describe('Post page template', () => { + it('should exist at src/pages/posts/[...slug].astro', () => { + assert.ok(existsSync(pagePath), 'Post page file should exist'); + }); + + describe('Imports', () => { + it('should import getCollection and render from astro:content', () => { + const content = readPage(); + assert.match(content, /import\s+.*getCollection.*from\s+['"]astro:content['"]/, 'should import getCollection'); + assert.match(content, /import\s+.*render.*from\s+['"]astro:content['"]/, 'should import render'); + }); + + it('should import CollectionEntry type from astro:content', () => { + const content = readPage(); + assert.match(content, /import\s+type\s+.*CollectionEntry.*from\s+['"]astro:content['"]/, 'should import CollectionEntry type'); + }); + + it('should import BaseLayout', () => { + const content = readPage(); + assert.match(content, /import\s+BaseLayout\s+from\s+['"].*BaseLayout\.astro['"]/, 'should import BaseLayout'); + }); + + it('should import NavBar', () => { + const content = readPage(); + assert.match(content, /import\s+NavBar\s+from\s+['"].*NavBar\.astro['"]/, 'should import NavBar'); + }); + + it('should import PostHeader', () => { + const content = readPage(); + assert.match(content, /import\s+PostHeader\s+from\s+['"].*PostHeader\.astro['"]/, 'should import PostHeader'); + }); + + it('should import getReadingTime', () => { + const content = readPage(); + assert.match(content, /import\s+.*getReadingTime.*from\s+['"].*reading-time['"]/, 'should import getReadingTime'); + }); + + it('should import prose.css', () => { + const content = readPage(); + assert.match(content, /import\s+['"].*prose\.css['"]/, 'should import prose.css'); + }); + }); + + describe('getStaticPaths', () => { + it('should export getStaticPaths function', () => { + const content = readPage(); + assert.match(content, /export\s+(async\s+)?function\s+getStaticPaths/, 'should export getStaticPaths'); + }); + + it('should call getCollection for posts', () => { + const content = readPage(); + assert.match(content, /getCollection\s*\(\s*['"]posts['"]\s*\)/, 'should call getCollection("posts")'); + }); + + it('should map posts to params with slug from post.id and props with post', () => { + const content = readPage(); + assert.match(content, /params\s*:\s*\{\s*slug\s*:\s*post\.id\s*\}/, 'should map slug to post.id'); + assert.match(content, /props\s*:\s*\{\s*post\s*\}/, 'should pass post as props'); + }); + }); + + describe('Props interface', () => { + it('should define Props interface with post: CollectionEntry<"posts">', () => { + const content = readPage(); + assert.match(content, /interface\s+Props/, 'should define Props interface'); + assert.match(content, /post\s*:\s*CollectionEntry\s*<\s*['"]posts['"]\s*>/, 'should have post: CollectionEntry<"posts">'); + }); + }); + + describe('Component logic', () => { + it('should destructure post from Astro.props', () => { + const content = readPage(); + assert.match(content, /const\s+\{\s*post\s*\}\s*=\s*Astro\.props/, 'should destructure post from Astro.props'); + }); + + it('should await render(post) for Content', () => { + const content = readPage(); + assert.match(content, /await\s+render\s*\(\s*post\s*\)/, 'should await render(post)'); + assert.match(content, /Content/, 'should destructure Content'); + }); + + it('should compute readingTime from post.body', () => { + const content = readPage(); + assert.match(content, /getReadingTime\s*\(\s*post\.body/, 'should call getReadingTime(post.body...)'); + }); + }); + + describe('Template', () => { + it('should use BaseLayout with title and description from post.data', () => { + const content = readPage(); + assert.match(content, / { + const content = readPage(); + assert.match(content, / { + const content = readPage(); + assert.match(content, / div.prose > Content', () => { + const content = readPage(); + assert.match(content, //, 'should render Content'); + }); + }); + + describe('Style', () => { + it('should have .prose-wrapper with prose max-width, auto margins, and space-12 bottom padding', () => { + const content = readPage(); + assert.match(content, /\.prose-wrapper/, 'should style .prose-wrapper'); + assert.match(content, /max-width\s*:\s*var\(--width-prose\)/, 'should have prose max-width'); + assert.match(content, /margin-left\s*:\s*auto/, 'should have auto left margin'); + assert.match(content, /margin-right\s*:\s*auto/, 'should have auto right margin'); + assert.match(content, /padding-bottom\s*:\s*var\(--space-12\)/, 'should have space-12 bottom padding'); + }); + }); +}); \ No newline at end of file