Plan 0001: Core Publishing Site
Spec: codev/specs/0001-core-publishing-site.md
Implementation Strategy
Build incrementally with working deployments at each phase. Each phase produces a testable artifact.
Phase 1: Project Scaffolding
Goal: Working Astro project with correct structure.
Tasks
-
Initialize Astro project in repository root:
npm create astro@latest . -- --template minimal --typescript strict -
Create directory structure:
content/ ├── essays/ ├── notes/ └── pages/ └── about.md assets/ └── images/ -
Configure
astro.config.mjs:import { defineConfig } from 'astro/config'; export default defineConfig({ site: 'https://thelongrun.work', output: 'static', }); -
Add sample content file
content/essays/001-sample.mdwith proper frontmatter (status: draft) -
Verify:
npm run devstarts without errors
Deliverables
- Astro project initialized
- Directory structure created
- Sample essay with frontmatter
- Dev server runs
Phase 2: Content Collections
Goal: Type-safe content model with draft filtering.
Tasks
-
Create
src/content/config.ts:import { z, defineCollection } from 'astro:content'; const essays = defineCollection({ type: 'content', schema: z.object({ id: z.string(), title: z.string(), subtitle: z.string().optional(), date: z.date(), status: z.enum(['draft', 'review', 'published']), canonical: z.string().url(), tags: z.array(z.string()).default([]), }), }); const pages = defineCollection({ type: 'content', schema: z.object({ title: z.string(), }), }); export const collections = { essays, pages }; -
Move content to Astro’s expected location:
content/essays/→src/content/essays/content/pages/→src/content/pages/
-
Create helper for filtering published content:
// src/lib/content.ts import { getCollection } from 'astro:content'; export async function getPublishedEssays() { const essays = await getCollection('essays', ({ data }) => { return data.status === 'published'; }); return essays.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf()); } -
Add a second essay with
status: publishedfor testing -
Verify: Content loads with type checking, draft filtering works
Deliverables
- Content collections configured
- Schema validation works
- Published/draft filtering works
- TypeScript types inferred correctly
Phase 3: Page Templates
Goal: Functional pages for home, essays, and about.
Tasks
-
Create base layout
src/layouts/Base.astro:- HTML boilerplate
- Slot for content
- Minimal CSS (system fonts, readable line length)
-
Create home page
src/pages/index.astro:- List published essays
- Show title, date, subtitle
- Link to essay page
-
Create essay page
src/pages/essays/[...slug].astro:- Dynamic route for all essays
- Render title, subtitle, date, body
- Only generate pages for published essays
-
Create about page
src/pages/about.astro:- Render from
src/content/pages/about.md
- Render from
-
Add minimal styles
src/styles/global.css:- Typography: system font stack
- Max-width container
- Responsive padding
-
Verify: All pages render correctly, navigation works
Deliverables
- Base layout with minimal styling
- Home page lists published essays
- Essay pages render correctly
- About page works
- Drafts do NOT generate pages
Phase 4: SEO and RSS
Goal: Proper meta tags and RSS feed.
Tasks
-
Add SEO component
src/components/SEO.astro:--- interface Props { title: string; description?: string; canonical: string; type?: 'website' | 'article'; } const { title, description, canonical, type = 'website' } = Astro.props; --- <link rel="canonical" href={canonical} /> <meta property="og:title" content={title} /> <meta property="og:url" content={canonical} /> <meta property="og:type" content={type} /> {description && <meta property="og:description" content={description} />} -
Integrate SEO into layouts/pages
-
Create RSS feed
src/pages/rss.xml.ts:import rss from '@astrojs/rss'; import { getPublishedEssays } from '../lib/content'; export async function GET(context) { const essays = await getPublishedEssays(); return rss({ title: 'The Long Run', description: 'Essays on software, work, and AI', site: context.site, items: essays.map((essay) => ({ title: essay.data.title, pubDate: essay.data.date, link: `/essays/${essay.slug}/`, })), }); } -
Install RSS package:
npm install @astrojs/rss -
Verify: View source shows canonical tags, RSS validates
Deliverables
- Canonical links on all pages
- OpenGraph meta tags
- RSS feed at
/rss.xml - Feed validates (use W3C validator)
Phase 5: Cloudflare Deployment
Goal: Automated deployment on push to main.
Tasks
-
Create
wrangler.toml(if needed) or use Cloudflare dashboard -
Configure Cloudflare Pages:
- Connect GitHub repository
- Build command:
npm run build - Output directory:
dist - Node version: 20 (LTS)
-
Set up custom domain:
- Add
thelongrun.workto Cloudflare - Configure DNS to point to Pages
- Enable HTTPS
- Add
-
Test deployment:
- Push to main, verify build succeeds
- Create PR, verify preview build
-
Add deployment badge to README
Deliverables
- Cloudflare Pages project created
- Auto-deploy on push to main
- Preview builds for PRs
- Custom domain configured
- HTTPS working
Testing Strategy
Manual Testing Checklist
Before each phase completion:
-
npm run buildsucceeds with no errors -
npm run devworks for local development - No TypeScript errors
- Draft essays do not appear anywhere public
Final Acceptance Testing
- Visit
https://thelongrun.work- home page loads - Published essays appear, drafts do not
- Individual essay pages work
- Canonical links match frontmatter
- RSS feed at
/rss.xmlis valid - About page renders
- Mobile responsive
File Checklist
Files to create:
astro.config.mjs
package.json
tsconfig.json
src/content/config.ts
src/lib/content.ts
src/layouts/Base.astro
src/components/SEO.astro
src/pages/index.astro
src/pages/about.astro
src/pages/essays/[...slug].astro
src/pages/rss.xml.ts
src/styles/global.css
src/content/essays/001-sample.md
src/content/pages/about.md
public/favicon.png (placeholder)
README.md
Notes for Builder
- Use Astro 5.x (latest stable)
- Keep dependencies minimal - only add what’s necessary
- No client-side JavaScript unless absolutely required
- Test draft filtering at every phase
- The spec says “5 year horizon” - avoid trendy patterns that may not last