---
title: "How this site is built: Astro 7, Tailwind 4, and Workers"
description: "The stack behind protrailblazer.com: Astro 7 on Cloudflare Workers, Tailwind 4 tokens, content as JSON blocks, Polar checkout, and Claude Code doing the typing."
published: 2026-09-03
updated: 2026-10-19
canonical: https://protrailblazer.com/posts/how-this-site-is-built/
tags: [astro, cloudflare-workers, tailwind, json-content, polar, biome, claude-code, colophon]
---

# How this site is built: Astro 7, Tailwind 4, and Workers

**Answer:** Pro Trailblazer is an Astro 7 site running on Cloudflare Workers. Every post, page, glossary term, product, and recommendation is a JSON file in a Git repository, validated by Zod, styled with Tailwind 4 design tokens, and deployed automatically when a commit lands on main. Claude Code writes most of the files, and a human reviews the diffs.

This is the colophon, written as a build log. As of September 2026 the site holds 31 posts, 31 interactive demos, 45 glossary terms, and 21 recommendations, and every one of them is a file in a Git repository. There is no database and no admin panel. The repository is the site. This post walks through each layer, what it does, and what it cost to get right.

## Why is the content JSON instead of Markdown or a CMS?

Post and page bodies are stored as a typed array of blocks: paragraphs, headings, lists, code, callouts, FAQ sections, glossary references, and demo embeds. A Zod schema in the content config validates every file at build time, so a typo in a block type fails the build instead of rendering as nothing.

The reason is that one source of truth has to feed several renderers. The web layout walks the tree and emits HTML. A markdown serializer walks the same tree to produce a text twin of every post at `/posts/<slug>.md` and the site-wide `/llms-full.txt` for AI crawlers. The same shape is designed to feed an iOS app and a newsletter later. Markdown would have worked for the web alone, but typed blocks make glossary references and demo embeds first-class data instead of custom syntax that every renderer has to parse.

```
{
  "type": "paragraph",
  "children": [
    { "type": "text", "value": "Each chip is one " },
    { "type": "term-ref", "slug": "tokens", "label": "token" },
    { "type": "text", "value": ". See " },
    {
      "type": "link",
      "href": "/posts/how-llms-generate-text",
      "children": [{ "type": "text", "value": "how LLMs generate text" }]
    },
    { "type": "text", "value": "." }
  ]
}
```

The cost is obvious: nobody wants to write JSON by hand. That is why the authoring workflow runs through Claude Code, covered below. A human edits prose in a diff, not in a text box.

Eight collections live under `src/content`, each with its own schema:

- **site**: one config file with the site name, author, and social links.
- **posts** and **pages**: block-array bodies plus frontmatter for title, description, dates, hero image, categories, tags, and a direct answer.
- **terms**: glossary entries that render on one filterable page at [/terms](https://protrailblazer.com/terms/) and open as modals inside posts. There are no individual term pages on purpose.
- **products** and **recommendations**: Pro Trailblazer's own products, sold through Polar, and external tools with outbound links. They never share a card grid, because visitors should always know which is which.
- **categories**: one taxonomy shared by posts, products, and recommendations.
- **demos**: metadata for each interactive, with the standalone HTML living in `public/demos/<slug>/`.

## How does a post get rendered?

The post layout does the same things for every post so the JSON never has to. It prints the hero image, the last-updated date, and the direct answer as a callout, builds a table of contents from the H2 headings, then hands the body to the block renderer.

Three block types do more than print text. A glossary reference renders as a modal trigger, and the modal shows the definition and an optional call to action. A demo block renders an iframe pointing at the standalone HTML, with a caption, a fullscreen toggle, and a link to open it in a new tab. A FAQ block renders as visible questions and answers, and the layout emits `FAQPage` JSON-LD for it automatically, alongside the `BlogPosting` and `BreadcrumbList` schema every post gets.

Answer engines get their own surfaces. `/llms.txt` lists every published post and is generated at request time from the collection, so it cannot go stale. `/llms-full.txt` carries the clean text of every post. Each post links to its markdown twin with an alternate link tag, and `robots.txt` explicitly allows the major AI crawlers. The interactive explainers, for example [how LLMs generate text](https://protrailblazer.com/posts/how-llms-generate-text/), are where that structure pays off, because the demo caption and the direct answer give a crawler something quotable even though it cannot run the simulation.

## What does the styling system look like?

Tailwind 4 is configured in CSS, not JavaScript. A single `@theme` block in the global stylesheet defines the tokens, and there is no Tailwind config file anywhere in the repository. The palette is anchored on the logo: one red accent, near-black ink for text and brand marks, and a plain white page.

```
@theme {
  --color-brand-red: #E10E0E;
  --color-bg: #FFFFFF;
  --color-text: #0E0E10;
  --font-sans: "Host Grotesk", ui-sans-serif, system-ui, sans-serif;
  --font-mono: "JetBrains Mono", ui-monospace, monospace;
  --container-measure: 42rem;
  --radius-md: 4px;
}
```

The site is light only. It shipped with a dark mode and a three-state toggle, and I removed both in a redesign: one color scheme is one thing to get right, and the interactive demos already supply the dark surfaces as plates set into the white page. The palette is held to WCAG AA contrast, with AAA for body text where the tokens allow it.

## How does it deploy?

The site runs on Cloudflare Workers, not Cloudflare Pages. Astro is set to server output with the [Cloudflare](https://protrailblazer.com/recommendations/cloudflare/) adapter, and every page that can be static is prerendered, so at request time the Worker only runs for API routes, redirects, and paths that have no file. The adapter reads a build-time `wrangler.toml` at the project root and writes the real deploy config into the build output.

Deploys are automatic. Workers Builds watches the main branch, runs `npm run build`, then runs the deploy command against the generated config. Nothing is pushed by hand. The middleware handles the redirects that have to live in code, such as legacy URLs from an earlier version of the site, and Cloudflare zone rules handle host, scheme, and trailing-slash normalization. The choices that were not obvious, including the one setting that broke local development, are in [deploying Astro to Cloudflare Workers, not Pages](https://protrailblazer.com/posts/astro-cloudflare-workers-not-pages/).

## Where do payments fit?

[Polar](https://protrailblazer.com/recommendations/polar/) handles checkout as the Merchant of Record, which means it collects the payment and owns the tax compliance. Two API routes run on the Worker. The checkout route takes a product slug, looks the product up in the content collection, and asks Polar to create a checkout session for that product's Polar ID. The webhook route verifies the signature with the Polar SDK before it trusts a single byte of the event, and it answers quickly because Polar wants a response within a couple of seconds.

Secrets never touch the repository. The access token and webhook secret are set with `wrangler secret put` and read from the Worker environment at request time. A non-secret variable keeps the integration pointed at Polar's sandbox until the product catalog is ready for real payments. As of this writing the checkout path is wired and tested and the catalog is still being built, so the products section is honest about being short.

## What tooling holds it together?

- **Astro check** validates every content file against its schema. It runs before every publish, and it is the reason a malformed post cannot reach production.
- **Vitest** covers the block schema and the markdown serializer, the two pieces every renderer depends on.
- **Playwright** runs a smoke suite in a real browser: the homepage renders, the posts index lists posts, the glossary search filters and clears, a post opens with its demo as the first figure, and post navigation works after client-side navigation.
- [Biome](https://protrailblazer.com/recommendations/biome/) formats the source and the content JSON. There is no config file, so the defaults are the style, and a single normalization commit put every post file into that canonical shape.
- **Node 22** is pinned in `.nvmrc` and in the package engines field, because the Astro 7 upgrade in July 2026 raised the floor.

## How does Claude Code fit in?

[Claude Code](https://protrailblazer.com/recommendations/anthropic-claude/) built most of this site and writes most of the content, running in the terminal as an agent that reads the repository, edits files, runs the checks, and stages commits. What makes that workable is not the model alone. It is the conventions the repository carries with it.

A `CLAUDE.md` file at the root is the single source of truth for how the site works: the stack, the collections, the writing rules, and the list of things never to do. Skills under `.claude/skills` hold the reference material for each domain, such as the content schema, the SEO checklist, the JSON-LD patterns, and the visual style for simulations. Slash commands scaffold a new post, term, product, or recommendation, run the SEO check, and publish. When a new session starts, the agent reads those files first, so a post written in September follows the same rules as one written in April.

The human role is review. Every change lands as a diff, and prose gets read the way code gets read, line by line, before it merges. The writing rule that bans em dashes exists because the author never uses one, and the agent follows it because the rule is in the file.

## What would I change?

- JSON authoring without an agent is unpleasant. If the workflow ever needs a non-technical editor, the honest answer is a headless CMS like [Sanity](https://protrailblazer.com/recommendations/sanity/), which is what client projects use. For a single-author site, the repository is the better CMS.
- The Workers adapter has sharp edges in local development. One boolean in the assets config decides whether images and demo iframes load at all in the dev server, and the failure looks nothing like a config problem.
- The sims ship as hand-built HTML, one file each, embedded through an iframe. That keeps them portable and sandboxed, but it also means every mobile fix is a per-file retrofit rather than a shared component update.

## Frequently asked questions

### Why not Markdown or MDX for the posts?

Markdown is fine for one renderer. This site needs the same content to render as HTML, as a markdown twin for crawlers, and eventually in an app and a newsletter, and it needs glossary references and demo embeds to be data rather than custom syntax. A typed block array gives every renderer the same tree and lets Zod reject bad content at build time.

### Why Cloudflare Workers instead of Cloudflare Pages?

Workers serves static assets natively now and receives new platform features first, while Pages is where the older static hosting product lives. The site prerenders every page it can and uses the Worker only for API routes and redirects. The details, including the generated deploy config and the routing gotcha, are in the companion post on deploying Astro to Workers.

### Where do the interactive demos come from?

Each demo is a self-contained HTML file under the public directory, built to a shared visual style guide, with its metadata in a demos collection. A demo block in a post renders it in an iframe with a caption and a fullscreen toggle. The standalone page canonicals to the parent post so search engines see one page, not two.

### How do AI answer engines read the site?

Through three surfaces: a request-time llms.txt index, a full-text llms-full.txt, and a markdown twin for every post. JSON-LD covers BlogPosting, FAQPage, DefinedTerm, and Product schemas, and robots.txt allows the major AI crawlers explicitly.

### Is there a database anywhere?

No. Content is JSON in Git, the build validates it, and Cloudflare serves the output. Payment state lives with Polar, which is the Merchant of Record. The only runtime code is the Worker that handles checkout, the webhook, and redirects.

## Key takeaways

- The repository is the site. Every post, term, product, and recommendation is a JSON file validated by a Zod schema at build time, so bad content fails the build instead of shipping.
- Typed blocks beat Markdown when more than one renderer needs the content. The same tree produces the web page, a markdown twin, and the full-text file that answer engines read.
- Tailwind 4 in CSS-first mode means the design system is a single @theme block, and no JavaScript config to drift.
- Prerender everything you can and let the Worker handle only what has to run at request time. Deploys are automatic from the main branch through Workers Builds.
- An agent can maintain a site if the conventions live in the repository. CLAUDE.md, skills, and slash commands are what keep Claude Code consistent across months of sessions.
