About This Site
The technical decisions behind this portfolio.
This page explains the engineering choices that shape codyanthony.dev. Each one reflects the docs-as-code, docs-as-product philosophy I apply to enterprise documentation work.
Stack at a glance
- Framework: Astro 6 with the official MDX integration.
Output is
static: pages are prerendered at build time. Two routes opt out and run on demand as Worker code, the per-page OG image endpoint and the R2 asset proxy. - Content: Typed collections under
src/content/case-studies/. Frontmatter is validated against a Zod schema at build time. - Styling: Hand-rolled CSS in a single
global.css. No Tailwind, no CSS-in-JS, no theme dependency. - UI framework runtime: None today. The site ships zero JavaScript files by default. Astro's islands architecture supports adding React/Vue/Svelte/Solid components later if an interactive embed needs one.
- Hosting: Cloudflare Workers, via the official
@astrojs/cloudflareadapter. A static-asset binding serves the prerendered pages; the on-demand routes run as Worker code. Bindings and deployment config start inwrangler.jsonc. - Image pipeline: Sharp via Astro's image pipeline. Hash-stamped WebP variants generated per-density at build time.
- Social cards: Generated on demand per page by a Worker endpoint. Satori renders the page title into SVG, resvg rasterizes it to a 1200×630 PNG, and the result is cached in R2 so later requests skip the render.
- AI tooling: Claude with project context (via Model Context Protocol servers) for code generation, refactoring, and review.
Content as typed data
Case studies live in a typed collection. Each entry's frontmatter is validated against a Zod schema at build time. Missing or malformed frontmatter fails the build with a typed error pointing at the offending file.
// src/content.config.ts
const caseStudies = defineCollection({
loader: glob({
pattern: "**/*.{md,mdx}",
base: "./src/content/case-studies",
}),
schema: z.object({
title: z.string(),
description: z.string(),
section: z.enum([
"content-design",
"technical-documentation",
"documentation-strategy",
]),
order: z.number().default(100),
draft: z.boolean().default(false),
pdf: z.object({
href: z.string(),
label: z.string().default("Download PDF sample"),
}).optional(),
}),
}); The schema is the source of truth for the taxonomy. Adding a new section means updating the enum; the build fails until existing entries match the new shape. The pattern catches taxonomy drift before it ships.
Deployment as code
Deployment starts in wrangler.jsonc: the app name,
compatibility flags, and binding declarations, including the R2 bucket the
asset endpoints read from. No web-console clicks, no .env files.
// wrangler.jsonc (source config)
{
"name": "codyanthony-dev",
"compatibility_date": "2025-12-14",
"compatibility_flags": ["nodejs_compat"],
"assets": {
"directory": "./dist",
"not_found_handling": "404-page"
},
"r2_buckets": [
{ "binding": "PORTFOLIO_ASSETS", "bucket_name": "codyanthony-dev-bucket" }
],
"observability": { "enabled": true }
}
The build emits two trees: dist/client/ (prerendered HTML and
hashed assets) and dist/server/ (the Worker entry). The adapter
also writes dist/server/wrangler.json, a generated deploy
config that wires the two together, and that generated file is what wrangler deploy ships. Most requests are served as static
files from the edge; the OG-image and R2 endpoints run as Worker code.
Trade-offs made
Workers over Cloudflare Pages
Pages would have worked for the static portfolio. I chose Workers because
the extension path mattered, and I have since taken it: the site renders OG
images and serves R2-backed artifacts from Worker code on the same domain,
with one configuration surface (wrangler.jsonc). On Pages,
adding that compute would have meant a second configuration surface and a
migration.
Astro over Next.js
Next.js is the industry default. For a static-first content portfolio, Astro ships fewer kilobytes by default and treats Markdown/MDX as first-class content without third-party loaders. The trade-off is a smaller ecosystem; for this scope, that hasn't mattered.
No UI framework runtime
Adding @astrojs/react ships roughly 95 KB gzipped of
framework code to every visitor before any of my code runs. The site's
interactive needs amount to a mobile menu toggle (a six-line inline
script) and image hover effects (CSS). That runtime cost isn't
justified. The architecture still supports adding any framework via
Astro's islands model if a future case study needs an interactive embed.
From Starlight to bare Astro
An earlier iteration ran on Astro's Starlight documentation theme. Starlight is the right tool when you're building developer docs at scale; it's the wrong tool when the brief is a minimalist editorial portfolio. The rebrand stripped Starlight, retaining Astro itself, the MDX pipeline, and content collections, the parts that genuinely belong in a content-first codebase.
By the numbers
- Prerendered pages: 16. 12 case studies plus 4 standalone pages, all built to static HTML. The OG-image and asset endpoints render on demand instead.
- JavaScript files shipped to the browser: 0. No framework runtime, no client-side router, no hydration cost. The only inline JavaScript is the six-line mobile menu toggle.
- Headshot: 7.2 KB at 1× WebP, 11 KB at 2× retina WebP. Generated from a single source JPEG via Astro's image pipeline.
Local workflow
pnpm dev # Local dev server at http://localhost:4321
pnpm build # Build into ./dist/ (client + server trees)
pnpm preview # Build, then serve the Worker locally via wrangler dev
pnpm astro check # TypeScript + Astro diagnostics
pnpm deploy # Build, then deploy the Worker to Cloudflare AI-assisted development
This site was developed using Claude as a co-author for code generation, refactoring, and review. Each component module, route handler, and CSS rewrite was proposed by the model. I conducted code review, integration testing, and version control. Model context flows through MCP (Model Context Protocol) servers that keep the agent aligned with the project's framework conventions and content guardrails.
Source
This portfolio is built completely in the open. View the repository on GitHub →