I wanted a place to document quick builds with AI coding assistants—not just the final code, but the implementation plans that got me there. This site is that place. And it seemed fitting to build it the same way: one day, with Claude, following a plan.

Why Did We Do This?

The problem: I build a lot of small projects with Claude (ESP32 sensors, Chrome extensions, Node-RED nodes). GitHub repos capture the code, but not the process—the architectural decisions, the debugging detours, the lessons learned. I wanted a blog that shared implementation plans as first-class artifacts, not just project write-ups.

Existing solutions didn’t fit:

  • Medium/Substack: No control over presentation, can’t customize for code-heavy content
  • Ghost/WordPress: Too heavy, overkill for a simple blog
  • GitHub Pages with Jekyll: Outdated, painful Ruby dependency management
  • Gatsby/Next.js: Over-engineered for static content

The idea: Astro 5 just shipped with its new Content Layer API. Perfect timing for a fresh, fast, markdown-based blog with modern developer experience.

What Problems Needed Solving?

Static Site Framework Selection

Requirements:

  • Fast (< 100ms page loads, no hydration delay)
  • Markdown-first with typed frontmatter
  • RSS feed generation
  • Minimal JavaScript in browser (no React overhead for blog posts)
  • Great developer experience (hot reload, TypeScript support)

Winner: Astro 5. Ships zero JavaScript by default unless you need interactivity. Content Layer API provides type-safe markdown collections. Built-in RSS support. Deploys as static files to any CDN.

Content Schema Design

Needed to categorize projects by build duration (“1-day”, “2-day”, “multi-day”) for filtering and badges. Astro 5’s Content Layer uses Zod schemas for validation—perfect for enforcing required fields and enum values.

The schema needed:

  • Required: title, description, pubDate, duration
  • Optional: tags, github/demo links, hero images, draft flag

Theme System Architecture

Wanted automatic dark mode without JavaScript flash. CSS custom properties + prefers-color-scheme media query handles this natively. Bonus: easy to extend with manual theme switching later if needed.

The Plan

Here’s the plan I gave Claude. Single-session build, structured as a Claude Code implementation plan.


Plan: synthetic-wiggling-site.md

Implementation Plan: my.daybuilds.ai Blog

Overview

Build a fast, markdown-based blog for documenting day-build projects with AI assistants. Static site generation with Astro 5, deployed to Cloudflare Pages.

Goals

  1. Content-first design - Markdown blog posts with typed frontmatter
  2. Duration categorization - Filter by “1-day”, “2-day”, “multi-day” builds
  3. Zero-config dark mode - Automatic theme switching without JavaScript
  4. Fast - Sub-100ms page loads, minimal browser JavaScript
  5. Deploy in hours - No database, no build complexity

Architecture Decisions

Framework: Astro 5 with Content Layer

  • Astro 5 over Gatsby/Next.js: Zero JavaScript by default, no hydration overhead
  • Content Layer API over legacy Collections: Type-safe markdown, better DX
  • Static generation over SSR: Deploy to CDN, no server costs

Content Schema: Zod + Glob Loader

  • Zod validation for frontmatter fields
  • Enum for duration: “1-day” | “2-day” | “multi-day” (typed, autocomplete in editor)
  • Glob loader with exclusion pattern (ignore CLAUDE.md files in blog directory)

Theme: CSS Variables + Media Query

  • CSS custom properties for colors (no Sass/Less dependency)
  • prefers-color-scheme for automatic dark mode
  • No JavaScript for theme switching (avoid flash, simpler)

Deployment: Cloudflare Pages via Wrangler

  • Cloudflare Pages over Netlify/Vercel: Free tier is generous, fast edge network
  • Wrangler CLI for deployments from terminal
  • Astro static output - just upload /dist folder

Implementation Tasks

Task 1: Project Bootstrap

  • Initialize Astro 5 project with TypeScript
  • Configure package.json scripts
  • Set up Git repository

Files:

  • package.json: Dependencies (Astro, TypeScript, sanitize-html for RSS)
  • tsconfig.json: TypeScript config
  • .gitignore: Exclude node_modules, dist

Verification:

npm install
npm run dev  # Should start dev server at localhost:4321

Task 2: Content Layer Setup

  • Create content.config.ts with blog collection
  • Define Zod schema with required/optional fields
  • Set up glob loader with CLAUDE.md exclusion

Files:

  • src/content.config.ts: Collection definition, Zod schema
  • src/content/blog/: Markdown posts directory

Schema:

const blog = defineCollection({
  loader: glob({ pattern: ['**/*.md', '!**/CLAUDE.md'], base: './src/content/blog' }),
  schema: z.object({
    title: z.string(),
    description: z.string(),
    pubDate: z.coerce.date(),
    duration: z.enum(['1-day', '2-day', 'multi-day']),
    tags: z.array(z.string()).optional(),
    draft: z.boolean().default(false),
    featured: z.boolean().default(false),
    github: z.string().url().optional(),
    demo: z.string().url().optional(),
    image: z.object({
      src: z.string(),
      alt: z.string(),
    }).optional(),
  })
});

Verification: Run npm run build - should generate types for blog collection

Task 3: Theme System

  • Create theme.css with CSS variables
  • Define light/dark color schemes
  • Add automatic dark mode via media query

Files:

  • src/styles/theme.css: Color variables, dark mode

Colors:

  • Light mode accent: Orange (#f97316)
  • Dark mode accent: Teal (#14b8a6)
  • Smooth transitions on theme change

Verification: Toggle system dark mode - site should update automatically

Task 4: Layout Components

  • BaseLayout: Wrapper for all pages (header, footer, meta tags)
  • BlogPost: Extends BaseLayout with post-specific styling
  • SiteHeader: Logo, navigation
  • SiteFooter: Copyright, tagline
  • DurationBadge: Visual badge for project duration

Files:

  • src/layouts/BaseLayout.astro: Site shell
  • src/layouts/BlogPost.astro: Blog post template
  • src/components/SiteHeader.astro: Navigation
  • src/components/SiteFooter.astro: Footer
  • src/components/DurationBadge.astro: Duration badge component

Verification: Create test blog post, check styling and layout

Task 5: Pages & Routing

  • Homepage: Hero + recent projects grid
  • Blog index: All projects with duration filters
  • Dynamic blog post pages: [slug].astro
  • About page: Site description

Files:

  • src/pages/index.astro: Homepage
  • src/pages/blog/index.astro: Blog list
  • src/pages/blog/[...slug].astro: Dynamic post rendering
  • src/pages/about.astro: About page

Routing:

  • Uses Astro’s file-based routing
  • Dynamic posts use getStaticPaths() with Content Layer API
  • Maps entry.id to URL slug

Verification: Navigate between pages, check links work

Task 6: RSS Feed

  • Generate RSS feed from blog collection
  • Include full post content (sanitized HTML)
  • Sort by publication date

Files:

  • src/pages/rss.xml.js: RSS feed endpoint

Libraries:

  • @astrojs/rss: RSS generator
  • sanitize-html: Clean markdown HTML
  • markdown-it: Parse markdown to HTML

Verification: Visit /rss.xml - should show valid RSS feed

Task 7: Deployment Setup

  • Create deploy.sh script
  • Configure Cloudflare Pages project
  • Set up production build command

Files:

  • deploy.sh: Build + deploy script
  • astro.config.mjs: Site URL for absolute links

Deploy command:

./deploy.sh  # Runs npm build + wrangler pages deploy

Verification: Deploy to Cloudflare, check live site

Critical Files

daybuilds/
├── src/
│   ├── content/
│   │   ├── config.ts              # Content Layer collections
│   │   └── blog/
│   │       ├── welcome.md         # First blog post
│   │       └── building-site.md   # Meta post
│   ├── layouts/
│   │   ├── BaseLayout.astro       # Page wrapper
│   │   └── BlogPost.astro         # Post layout
│   ├── components/
│   │   ├── SiteHeader.astro       # Navigation
│   │   ├── SiteFooter.astro       # Footer
│   │   └── DurationBadge.astro    # Badge component
│   ├── pages/
│   │   ├── index.astro            # Homepage
│   │   ├── about.astro            # About page
│   │   ├── blog/
│   │   │   ├── index.astro        # Blog list
│   │   │   └── [...slug].astro    # Dynamic posts
│   │   └── rss.xml.js             # RSS feed
│   └── styles/
│       └── theme.css              # CSS variables, dark mode
├── astro.config.mjs               # Astro config
├── deploy.sh                      # Deployment script
└── package.json                   # Dependencies

Verification

Local Development

npm run dev       # Start dev server
# Visit http://localhost:4321
# Check: Homepage loads, blog posts render, dark mode works

npm run build     # Production build
# Check: No TypeScript errors, Zod schema validates

Deployment

./deploy.sh       # Deploy to Cloudflare Pages
# Check: Site live at my.daybuilds.ai
# Test: RSS feed at /rss.xml, all pages accessible

Performance Check

  • Lighthouse: Performance 95+, Accessibility 100
  • Page load: < 100ms on fast connection
  • JavaScript payload: < 5KB (mostly Astro runtime)

Trade-offs

No JavaScript Theme Toggle

  • Chose system preference (prefers-color-scheme media query)
  • Trade-off: No manual theme switcher
  • Benefit: Zero JavaScript, no flash of wrong theme, simpler

Static vs. SSR

  • Chose static generation (Astro build outputs HTML)
  • Trade-off: Must rebuild to update content
  • Benefit: Deploy to any CDN, no server costs, fastest possible load times

Markdown vs. MDX

  • Chose plain Markdown (.md files)
  • Trade-off: No React components in content
  • Benefit: Simpler, faster builds, less complexity

Duration Enum vs. Free Text

  • Chose enum (“1-day” | “2-day” | “multi-day”)
  • Trade-off: Less flexible if I want “3-day” later
  • Benefit: Consistent, type-safe, enables filtering logic

Fun Challenges Encountered

The CLAUDE.md Rendering Bug

First build attempt failed with Zod validation error: “title: Required”. Astro was processing /src/content/blog/CLAUDE.md as a blog post, even though it’s just project documentation for Claude.

The fix: Glob loader exclusion pattern ['**/*.md', '!**/CLAUDE.md']. This tells Astro “load all markdown files EXCEPT ones named CLAUDE.md.”

Lesson: Glob loaders need explicit exclusions. Unlike .gitignore, there’s no default “ignore docs” behavior.

RSS Feed Content Sanitization

Initial RSS feed included raw markdown. Feed readers couldn’t render it. The solution: markdown-it to parse markdown to HTML, then sanitize-html to strip dangerous tags.

But then code blocks lost syntax highlighting. Turns out sanitize-html strips <pre><code> by default. Fix:

sanitizeHtml(html, {
  allowedTags: sanitizeHtml.defaults.allowedTags.concat(['pre', 'code'])
})

Lesson: RSS readers expect HTML, not markdown. Always test feeds in actual readers (NetNewsWire, Feedly).

CSS Variable Dark Mode Flash

First dark mode implementation used JavaScript to detect prefers-color-scheme and toggle a .dark class. This caused a flash of light theme on page load (JS executes after initial render).

The fix: Use CSS media queries directly. No JavaScript needed:

:root {
  --color-accent: #f97316; /* Orange for light mode */
}

@media (prefers-color-scheme: dark) {
  :root {
    --color-accent: #14b8a6; /* Teal for dark mode */
  }
}

Lesson: CSS media queries are instant. JavaScript theme switching always flashes.

Results

Build time: 6 hours (including this post)

Performance:

  • Lighthouse score: 100 (Performance, Accessibility, Best Practices)
  • First Contentful Paint: < 0.5s
  • Total JavaScript: 3.2 KB (Astro router only)

What’s next:

  • Search functionality (static index + Fuse.js)
  • Tag filtering page
  • Project duration subdomains (one.daybuilds.ai, two.daybuilds.ai)

Tech stack: Astro 5, TypeScript, Cloudflare Pages Build time: 6 hours Hosting cost: $0 (Cloudflare Pages free tier)