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/cloudflare adapter. A static-asset binding serves the prerendered pages; the on-demand routes run as Worker code. Bindings and deployment config start in wrangler.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 →