The Architecture Behind Instant Publishing: WollyCMS + Astro SSR on Cloudflare Workers
A deep dive into how WollyCMS APIs, Astro SSR on Cloudflare Workers, and structured content blocks create a publishing workflow that is instant, flexible, and surprisingly AI-friendly.
You are reading this blog post because of the architecture I am about to describe. It was drafted by an AI assistant, pushed into the CMS through an API call, and rendered on a Cloudflare Worker at the moment you requested this page. No build step. No deploy queue. No static site generator chugging through templates. Just an API, a Worker, and HTML in your browser.
This post is a deep dive into how WollyCMS, Astro, and Cloudflare Workers fit together to make that possible — and why this stack is unusually well-suited for working with AI.
The problem with traditional publishing
Most CMS-to-frontend setups fall into one of two camps, and both have tradeoffs that get painful at scale.
Monolithic CMS platforms like WordPress couple your content, your rendering, and your frontend into one deployable unit. You get instant publishing, but you give up frontend flexibility. Want to use a modern component framework? You are fighting the system.
Headless CMS with static site generation (Contentful + Next.js, Sanity + Gatsby, etc.) gives you frontend freedom, but publishing means triggering a build. Edit a typo, wait for a rebuild. Publish a blog post, wait for a rebuild. The build queue becomes a bottleneck, and your content team learns to dread the phrase "deploying changes."
We wanted a third option: the frontend flexibility of a headless CMS with the instant publishing of a monolith. That is what WollyCMS + Astro SSR on Cloudflare Workers gives us.
The stack at a glance
Three layers, each deployed as a Cloudflare Worker:
WollyCMS — a Hono-based headless CMS with D1 (SQLite) for content storage and R2 for media. Exposes a public REST API for content and an authenticated admin API for management. The admin UI is a SvelteKit SPA.
Astro frontend — an Astro site running in SSR mode (output: server) on the @astrojs/cloudflare adapter. Every page is rendered at request time by fetching content from the CMS API.
Cloudflare Workers — the runtime for both. Workers run at the edge, close to users, with sub-100ms cold starts. Combined with Cloudflare's HTTP cache, you get the performance of static files with the freshness of dynamic rendering.
This site — wollycms.com — runs on this exact stack. So do several other production sites we manage, each with their own CMS instance and Astro frontend, all deployed from the same WollyCMS codebase.
How data flows: from CMS to rendered page
Let us trace what happens when you load this page. This is the core of the architecture, and where the design decisions pay off.
Step 1: Content lives as structured data in D1
WollyCMS stores content in a three-table pattern: pages, blocks, and page_blocks (a join table). A page has a content type that defines its fields and named regions. Regions are slots — hero, content, sidebar — that hold an ordered list of typed blocks. Each block has its own fields defined by its block type schema.
This blog post, for example, is a page of type "blog_post" with fields for author, excerpt, and published date. It has a single "content" region containing a rich_text block. That block's body field holds the text you are reading right now as TipTap JSON — a structured document format, not raw HTML.
Step 2: The content API serves resolved pages
When the Astro frontend needs a page, it calls the WollyCMS content API:
GET /api/content/pages/blog/architecture-instant-publishingThe API does not just return the raw page row. It resolves the full page structure: joins blocks through page_blocks, groups them by region, sorts by position, and merges any field overrides for shared blocks. The response is a complete, render-ready document:
{
"data": {
"title": "The Architecture Behind Instant Publishing",
"type": "blog_post",
"fields": { "author": "Chad", "published_date": "2026-03-24" },
"regions": {
"content": [
{
"block_type": "rich_text",
"fields": { "body": { "type": "doc", "content": [...] } }
}
]
}
}
}This is a public, unauthenticated endpoint. No API key needed for reading content. The CMS also sends ETag and Cache-Control headers, so Cloudflare's edge cache can serve repeated requests without hitting D1 again.
Step 3: Astro fetches and renders at request time
Here is where SSR earns its keep. The Astro frontend runs a catch-all route that handles every page on the site:
// src/pages/[...slug].astro
const wolly = getWolly();
const page = await wolly.pages.getBySlug(slug);
const regions = page.regions;At request time — not build time — the Worker fetches the page from the CMS, gets the block data organized by region, and passes it to the BlockRenderer component. The WollyClient is a thin typed wrapper around fetch. No GraphQL, no SDK with a learning curve. Just REST endpoints and TypeScript types.
Step 4: BlockRenderer maps blocks to Astro components
This is the part that makes the architecture feel effortless. Every CMS block type has a corresponding Astro component. The BlockRenderer maps them automatically:
<BlockRenderer
blocks={regions.content}
region="content"
components={{
rich_text: RichText,
hero: Hero,
accordion: Accordion,
cta_button: CTAButton,
// ... every block type maps to a component
}}
/>The BlockRenderer iterates through the blocks in a region, looks up the component by block type slug, and renders it with the block's fields as props. If you add a new block type in the CMS, you write one new Astro component and add it to the map. That is it.
For this blog post, the content region has one rich_text block. The RichText component receives the TipTap JSON body, runs it through a recursive renderer that converts nodes to HTML (headings, paragraphs, lists, code blocks, images, links), and outputs it with Tailwind's typography plugin for styling.
Step 5: HTML arrives in your browser
The Worker sends back fully rendered HTML. No client-side JavaScript needed to display the content. No hydration step. No loading spinners. The browser gets a complete document on the first response, which is exactly what search engines and screen readers want to see.
Total time from request to response: typically 100-300ms, including the CMS API call. On cache hit (Cloudflare edge serving a previously-rendered response), it is even faster.
Why SSR on Workers, not static generation
This is the question we get most often. Static site generation is the default recommendation in the Astro ecosystem, and for good reason — it is fast and cheap. But for a CMS-driven site, SSR on Workers is strictly better. Here is why.
Instant publishing. When you save a page in the CMS admin, it is live immediately. No build trigger, no webhook, no waiting. The next request to the Astro Worker fetches the updated content from the CMS and renders it. For a marketing team or a solo developer managing multiple sites, this is transformative.
No build infrastructure. Static generation requires a build step that knows about every page on your site. That means build servers, CI minutes, and increasingly long build times as your content grows. With SSR, the "build" is compiling Astro once. Content changes never require a redeploy.
Cache does the heavy lifting. WollyCMS sends Cache-Control headers with a 60-second public TTL and stale-while-revalidate. Cloudflare's edge cache respects these headers, so most requests never hit the Worker at all. You get static-file performance without static-file constraints. When content changes, the cache naturally refreshes within a minute.
Dynamic capabilities when you need them. SSR means you can do things that static sites cannot: personalized content, A/B testing, authentication, real-time data from external APIs. You might not need these on day one, but the architecture supports them without a rewrite.
The block and region content model
If you have used Drupal, the block/region model will feel familiar. If you have not, here is why it matters.
Most headless CMS platforms give you a flat content model: a page is a bag of fields. Maybe there is a "body" field with a WYSIWYG editor, or a "components" JSON field with nested data. But the structure is implicit. Your frontend has to interpret and validate the shape of that data.
WollyCMS makes structure explicit. A content type defines named regions. Each region accepts specific block types. Each block type has a typed field schema. The result is a content model that mirrors your frontend component architecture:
Content Type: "Landing Page"
├── Region: hero
│ └── Block: hero (fields: heading, subheading, background_image, cta_text, cta_url)
├── Region: content
│ ├── Block: rich_text (fields: body)
│ ├── Block: image (fields: src, alt, caption)
│ └── Block: accordion (fields: items[])
├── Region: features
│ └── Block: feature_grid (fields: features[])
└── Region: bottom
└── Block: cta_button (fields: text, url, style)Content editors see these regions in the admin UI and can add, remove, and reorder blocks within them. Developers define what is possible — which block types are allowed in which regions — and editors work within that structure. It is constrained enough to prevent layout chaos, flexible enough to handle real content needs.
Blocks can also be shared across pages. A "site-wide CTA" block can be created once in the block library and placed on multiple pages. Update it once, and every page that uses it reflects the change.
Why this architecture loves AI
Here is where it gets personal. I manage multiple sites on this stack, and I work with Claude Code — an AI coding assistant — for nearly everything. This architecture turns out to be remarkably AI-friendly, and not by accident.
Structured APIs are easy to reason about. The WollyCMS admin API is REST with predictable endpoints and JSON payloads. An AI assistant can create pages, add blocks, update content, and manage menus through API calls. There is no browser automation, no fragile screen-scraping. The API is the interface.
Content is data, not markup. Because WollyCMS stores rich text as TipTap JSON rather than raw HTML, an AI can construct well-structured content programmatically. Each paragraph, heading, list, and code block is a typed node in a JSON tree. The AI does not need to generate valid HTML with proper nesting — it generates a data structure, and the frontend handles rendering.
The block model is composable. An AI assistant can look at the available block types and their field schemas, then compose a page by choosing the right blocks for each region. It is like giving the AI a set of building blocks with clear interfaces — much easier to work with than an open-ended page builder.
The whole workflow is scriptable. Create a page, add blocks, set fields, publish — it is all API calls. This blog post was literally created by an AI assistant making HTTP requests to the WollyCMS admin API. The assistant drafted the TipTap JSON content, created the page as a draft, and populated the rich text block. I reviewed it, made edits, and published. The CMS never knew or cared whether the content came from a human in the admin UI or an AI through the API.
This is a meaningful shift in how content management works. The traditional workflow — log in to admin, click through forms, paste from a Google Doc, fix formatting, hit publish — becomes: describe what you want, review the draft, publish. The CMS API becomes a tool the AI uses, just like it uses a code editor or a terminal.
Multi-site from one codebase
One more thing that makes this architecture practical at scale: WollyCMS uses a matrix deploy. One push to the main branch deploys the CMS to all site instances simultaneously. Each instance has its own D1 database, R2 bucket, and configuration — but they all run the same CMS code.
We currently run four CMS instances (this site, a community college, an online catalog, and a tech publication) from the same codebase. A bug fix or feature addition benefits all sites at once. Each Astro frontend is independent — different designs, different block components, different content models — but they all talk to WollyCMS the same way.
The frontends also share the @wollycms/astro integration package, which provides the WollyClient, BlockRenderer, rich text helpers, and TypeScript types. When the CMS API evolves, we update the package once and propagate it to each frontend.
The numbers
Some concrete performance characteristics from production:
Time to first byte: 100-300ms for uncached requests (CMS API call + Astro render). Under 50ms on cache hit.
Content publish latency: Zero. Save in admin, content is live on next request (within cache TTL of 60 seconds).
Cold start: Sub-100ms for the Astro Worker. Cloudflare Workers have minimal cold start overhead compared to traditional serverless.
Build time: Only when deploying code changes to the Astro frontend. Content changes never require a build.
Infrastructure cost: Cloudflare Workers free tier covers most small-to-medium sites. D1 and R2 have generous free tiers. You can run a complete CMS + frontend for effectively nothing.
Try it yourself
WollyCMS is open source under the MIT license. You can spin up a local instance in about a minute:
npx create-wolly my-site
cd my-site
npm run devThat gives you the CMS with an admin UI, a SQLite database, and a local dev server. Pair it with an Astro frontend using the @wollycms/astro package, deploy both to Cloudflare Workers, and you have the full stack described in this post.
The architecture is simple by design. A REST API that serves structured content. An Astro frontend that renders it at the edge. Typed blocks that map to components. No magic, no abstraction layers hiding complexity, no vendor lock-in. Just fast, flexible publishing that works as well for an AI assistant as it does for a human editor.