This site is my portfolio and my place to write. I also use it as the testbed for the stack I want to run on Workhorse and other client work, so it has to actually hold up end to end.
The stack
Astro 5 with server-side rendering. Static pages are pre-rendered; dynamic pages render on Cloudflare Workers at request time. React 19 powers the few interactive islands. Tailwind and shadcn/ui handle styling, with design tokens piped through CSS variables so the same values drive every surface.
Long-form content (posts, case studies, work pages) is MDX in the repo. The schema lives in src/content/config.ts, so writing is just files and frontmatter, versioned with the rest of the site.
Payload CMS v3 is the data layer for messages from visitors: contact submissions and newsletter subscribers. Collections live in the same TypeScript repo, so the schema, the routes that read it, and the deployment all share one source of truth. The full multi-tenant Payload build runs on the Workhorse studio platform, where authoring, brand assets, and agentic generation go through Payload. This site uses a smaller subset of that.
Cloudflare Pages runs the site on the Workers runtime. Production data lives in Cloudflare D1, a SQLite-on-the-edge primitive the Payload adapter speaks to natively. Local dev uses a SQLite file on disk, and npm run db:pull brings prod data down for debugging.
Astro and MDX for delivery, Payload for capture, Cloudflare for runtime and data.
The chat agent
The hero “terminal” on every page is actually a live chat. Type a question and the response streams in. On per-recipient dossiers (like the Ramp application) the same surface swaps to an inline chat panel that opens with a seeded line framing the role.
The model is Llama 3.3 70B Instruct, called through the Hugging Face Inference Router so I can swap providers without changing client code. A small Cloudflare Workers AI fallback covers upstream outages.
Grounding is a build-time chunk index. A script walks the blog, the dossiers, the about letter, and the résumé, splits each on H2/H3, and writes a JSON file. At request time the server runs MiniSearch over the chunks and prepends the top hits to the system prompt. No vectors, no embeddings, no extra service.
The system prompt sets the voice (dry, direct, no marketing-speak) and includes the site map so the model can reference real paths. A second prompt fires inside a dossier so the model treats the turn as a working interview rather than a marketing chat. A regex pass classifies each visitor turn (hire, recruit, collaborate, curious) so the UI can offer the right next step.
Replies can append a fenced actions block. The client parses it into chips below the message: a Cal link, an internal nav, or an inline email field. Captures land in the same Payload collection the contact form uses. Per-IP rate limits and a kill switch on agent logging keep things contained.
How I work in it
I open the repo in Claude Code and describe what I want built: a new page, a refactor, a bug fix. The model proposes the route, the data shape, the component, the styles. I read the diff, push back where it’s wrong, accept where it’s right. Then I run it.
I’m still making the design and product decisions. Claude Code handles the typing, the boilerplate, and the cross-file refactors that would otherwise take an afternoon.
When a pattern shows up in two or three places, I lift it into a shared component or helper.
What this enables
Fast iteration. A bug fix or copy change usually takes minutes from spotting it to shipping it.
One source of truth. The same design tokens drive web pages, printable PDFs, and dossier documents. Content and form data each have one schema, so nothing drifts between surfaces.
A reusable pattern. This repo is private, but the architecture isn’t a secret. Workhorse uses the same shape at a larger scale: multi-tenant, with agentic content generation in the middle.
What’s next
The Workhorse migration: the same architecture scaled up to multi-tenant, with more of the content pipeline going through Claude. Sacred Grove is the iOS version of the same approach, built in SwiftUI, exploring how the same workflow runs against a native runtime instead of the edge.
This site is intentionally small. The point is that it works, and that the pattern scales.
The toolbelt
The full bill of materials, with links.
Framework. Astro 5, React 19, TypeScript, with the MDX, React, sitemap, RSS, and Partytown integrations. Vite underneath.
UI. Tailwind CSS with Typography and Animate. shadcn/ui primitives vendored into the repo, sitting on Radix UI. Lucide for icons. class-variance-authority, clsx, and tailwind-merge for variant glue.
Forms. React Hook Form, Zod via @hookform/resolvers, and react-day-picker.
Motion. Framer Motion, GSAP, and Swiper where the page earns it.
Data and content. Payload CMS v3 with the D1 and SQLite adapters, Lexical richtext, the form-builder plugin, and Resend for transactional email. gray-matter for MDX frontmatter, date-fns for dates, MiniSearch for client-side search.
Edge. @astrojs/cloudflare, Cloudflare Pages and Workers, D1, and Wrangler to deploy.
Agent. Llama 3.3 70B Instruct via the Hugging Face Inference Router, with Workers AI as a fallback path.
Embeds. Cal.com embed for booking.
Build and scripts. tsx for TypeScript scripts, concurrently for parallel dev processes, Playwright to render the resume and cover-letter PDFs.
Editor. Claude Code, used daily.
