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
- Content-first design - Markdown blog posts with typed frontmatter
- Duration categorization - Filter by “1-day”, “2-day”, “multi-day” builds
- Zero-config dark mode - Automatic theme switching without JavaScript
- Fast - Sub-100ms page loads, minimal browser JavaScript
- 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 schemasrc/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 shellsrc/layouts/BlogPost.astro: Blog post templatesrc/components/SiteHeader.astro: Navigationsrc/components/SiteFooter.astro: Footersrc/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: Homepagesrc/pages/blog/index.astro: Blog listsrc/pages/blog/[...slug].astro: Dynamic post renderingsrc/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 generatorsanitize-html: Clean markdown HTMLmarkdown-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 scriptastro.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)