From JAMstack to the Edge
Built with Workers is more than a showcase—it's a demonstration of what a greenfield application looks like when designed specifically for Cloudflare Workers. The premise was simple: if the team had complete freedom to choose tools and architecture for a Workers-native site, what would that stack look like?
Unsurprisingly, the answer leans heavily on the JAMstack model: JavaScript, APIs, and markup delivered as static builds to the edge. But the deeper connection is philosophical. JAMstack's emphasis on reducing infrastructure mirrors the core motivation behind serverless on Workers. There are no databases to deploy, no servers to spin up, and no traditional CMS infrastructure to manage.
The result is an open-source codebase—now available on GitHub—that walks through the full lifecycle: modeling content, building statically, and deploying to the edge with Workers Sites.
The Stack in Practice
Three core pieces make up the architecture:
- Workers Sites handles deployment, pushing always-fresh static builds directly to Cloudflare's edge network.
- Gatsby.js provides the frontend foundation, bringing a mature set of defaults for building a modern web application.
- Sanity.io acts as the headless CMS, enabling full content modeling and site layout control without managing any backend infrastructure.
This combination mirrors the cross-functional nature of the Workers Developer Experience team—people who spend most of their days on docs and Wrangler (the CLI) but wanted to apply that experience to a real product. The guiding principle throughout was ease-of-use, the same priority that shapes the Workers platform itself.
The programming model for Workers is intentionally lightweight: just JavaScript (or Rust, C, and C++ via WASM) with full control over request ingress and egress. The same spirit applied to choosing tools for Built with Workers—every piece needed to enable building the entire application without adding operational overhead.
Why This Matters for Workers Developers
The open-source nature of the project is the key takeaway for developers exploring Workers. It offers an end-to-end reference: how the application is developed locally, how content is managed through Sanity, how Gatsby produces the static output, and how Workers Sites deploys it to the edge.
For those interested in JAMstack on Workers, this project is both a working example and a starting point. The codebase shows how the pieces fit together in a real, deployed application—not just a tutorial scenario.
The JAMstack Model, Revisited
JAMstack has become shorthand for building web applications from three familiar pieces: JavaScript, reusable APIs, and pre-built markup. It is not a new idea — personal sites generated from Markdown by tools like Jekyll have existed since 2013 — but the model has matured. Where early static sites hosted simple blogs, JAMstack today powers full applications, with the same deployment simplicity and performance benefits of static hosting.
What makes the model interesting is not the individual technologies but how they interact. JavaScript handles dynamic behavior on the client, APIs provide data services over HTTPS, and everything else — including the final HTML — is assembled ahead of time. That last point is a key shift from traditional server-rendered applications: by the time a site is deployed, it is a collection of plain HTML, CSS, and JavaScript files. No server is left waiting to interpolate data into a template for each incoming request.
JavaScript at the Edge
JavaScript is the language glue in JAMstack. On the client, frameworks like React and Vue take care of rendering and state. On the server side — or, in Cloudflare's case, at the edge — JavaScript handles any dynamic request and response logic. Workers support the Service Worker APIs directly, so a developer can intercept incoming requests and return responses entirely in JavaScript.
Workers Sites extends this model. Built on Workers KV as an edge storage layer, it lets developers deploy static assets with a single wrangler publish command. When a request arrives, a Workers script looks up the asset in Workers KV and returns it with no additional configuration. For JAMstack deployments, this turns the CDN into a runtime: the same edge infrastructure that serves static files also executes JavaScript next to them.
APIs as the Data Layer
Static site tooling works well for personal pages because those pages have minimal data requirements. Real applications need user data, analytics, and other dynamic content. The JAMstack answer is to expose all of that through HTTPS APIs, so the application can consume it without maintaining a persistent server connection.
Workers can serve both sides of that relationship. It can proxy requests to an origin without exposing that origin to clients, or it can persist and return data directly using Workers KV. This means an API endpoint can live entirely at the edge, returning JSON without any round trip to a central server. That flexibility shaped how Built with Workers itself was developed, as a hybrid between static asset serving and dynamic execution on the same platform.
Markup at Build Time
The most important conceptual step in JAMstack is the build phase. A Gatsby site, for example, connects to a headless CMS like Sanity.io over HTTPS, queries the content with GraphQL at build time, and uses that data to decide which pages to generate. The deployed application has no knowledge of the CMS: it is pure HTML, CSS, and JavaScript. The pattern works broadly — static generators like Hugo can pull data from APIs into local files the same way — but Gatsby treats it as a first-class workflow.
This build-time approach has a practical benefit for search-driven sites. Instead of building a dynamic API to serve thousands of variations of a page — say, "Senior React developer jobs in Europe" — a developer can define the markup once and iterate over the available data at build time, generating every combination as a static page. Each new job added to the data source enriches all the existing pages when the site is next built.
Why Deploy on Cloudflare Workers
The typical JAMstack deployment treats the CDN as a dumb bucket: a place to store files and serve them quickly. That is generally the least interesting part of the stack. Workers changes the equation by giving you a JavaScript runtime at every edge node, alongside the static assets. It turns the CDN into a low-latency compute layer.
Built with Workers takes advantage of that by implementing dynamic features — such as project bookmarking — that require per-user state. A single button element in the interface uses Workers, Workers KV, and the streaming HTML rewriter together to provide user-specific functionality on a statically generated page. It is an intentional design choice that blurs the boundary between classic static sites and fully dynamic applications.
Data: from CMS to GraphQL to static pages
With Gatsby selected, the next decision was where content would live. Built with Workers has two central data models: Projects (sites, apps, and APIs built on Workers) and Features (platform capabilities such as Workers KV or the streaming HTML rewriter). Those models had to be editable by non-technical team members, without requiring a code deploy for every tweak.
The conventional path would be a database plus a hand-rolled REST API, but that adds infrastructure and maintenance. The team chose Sanity.io, a headless CMS that ties data to no single presentation layer. Projects and features live as structured content in Sanity's dataset, and Gatsby pulls that data at build time to generate a page per project via createPage:
// gatsby-node.js
exports.createPages = async ({ graphql, actions }) => {
const { createPage } = actions;
const result = await graphql(`
{
allSanityProject {
edges {
node {
slug
}
}
}
}
`);
if (result.errors) {
throw result.errors;
}
const {
data: { allSanityProject }
} = result;
const projects = allSanityProject.edges.map(({ node }) => node);
projects.forEach((node, _index) => {
const path = `/built-with/projects/${node.slug}`;
createPage({
path,
component: require.resolve("./src/templates/project.js"),
context: { slug: node.slug }
});
});
};
Sanity also powers the homepage itself. The entire page is an instance of a "layout" data model, composed of "collection" entries that group projects. Because layouts and collections are content, not code, editors can reorder, add, or remove homepage sections from the Sanity studio. Changes go live a few minutes later, after the automated deploy pipeline finishes. The React code only needs to know how to render project titles, cards, and other primitives; the CMS dictates their arrangement.

The benefit of this arrangement is that content updates no longer require a pull request. A team member logs into Sanity, edits a project's description or creates a new project, and the next deploy reflects it.
Static files plus dynamic state
The Gatsby, Sanity, and Workers Sites setup works well, but it's not Workers-specific—any static host would do. The project also explored what the Workers platform uniquely enables for JAMstack sites: a JavaScript runtime right at the edge, between the static files and the client.
The streaming HTML rewriter, which Workers Sites pages pass through on their way to the client, was the vehicle for this experiment. The feature chosen for a first implementation was the "bookmark" button on each project page. When a user bookmarks a project, the Worker stores a JSON record in Workers KV. On a return visit, the Worker checks KV for that user's saved projects and embeds the result as "edge state" directly into the HTML being streamed:
// User-specific data stored in Workers KV, representing
// per-project bookmark information
{
"bytesized_scraper_bookmarked": false,
"web_scraper_bookmarked": true
}
The client-side React app detects that embedded state when it renders, toggling the bookmark icon accordingly. A useContext hook exposed the state to the components that needed it:
// workers-site/index.js
import { getAssetFromKV } from "@cloudflare/kv-asset-handler"
addEventListener("fetch", event => {
event.respondWith(handleEvent(event))
})
class EdgeStateEmbed {
constructor(state) {
this._state = state
}
element(element) {
const edgeStateElement = `
<script id='edge_state' type='application/json'>
${JSON.stringify(this._state)}
</script>
`
element.prepend(edgeStateElement, { html: true })
}
}
const hydrateEdgeState = async ({ state, response }) => {
const rewriter = new HTMLRewriter().on(
"body",
new EdgeStateEmbed(await state)
)
return rewriter.transform(await response)
}
async function handleEvent(event) {
return hydrateEdgeState({
response: getAssetFromKV(event, options),
// Get associated state for a request, based on the user and URL
state: transformBookmark(event.request),
})
}
This approach means static pages can carry per-user dynamic state without any client-side API round trip for that state, leveraging distributed key-value storage and a streaming HTML rewriter that adds no perceptible latency.
Deployment without a person in the loop
Continuous deployment fills the gap between content edits and what's live. Built with Workers uses the open-sourced wrangler-action to run builds and deploy from GitHub Actions whenever a change lands on master, and on a regular schedule. But the piece that makes the CMS workflow viable is "build-on-change": Sanity pings a deployed Worker webhook whenever content is published. That Worker fires a repository_event on GitHub Actions, triggering a fresh build and deploy:
// edge_state.js
import React from "react"
import { useSSR } from "../utils"
const parseDocumentState = () => {
const edgeStateElement = document.querySelector("#edge_state")
return edgeStateElement ? JSON.parse(edgeStateElement.innerText) : {}
}
export const EdgeStateContext = React.createContext([{}, () => {}])
export const EdgeStateProvider = ({ children }) => {
const { isBrowser } = useSSR()
if (!isBrowser) {
return <>{children}</>
}
const edgeState = parseDocumentState()
const [state, setState] = React.useState(edgeState)
const updateState = (newState, options = { immutable: true }) => options.immutable
? setState(Object.assign({}, state, newState))
: setState(newState)
return (
<EdgeStateContext.Provider value={[state, updateState]}>
{children}
</EdgeStateContext.Provider>
)
}
// Inside of a React component
const Bookmark = ({ bookmarked, project, setBookmarked, setLoaded }) => {
const [state, setState] = React.useContext(EdgeStateContext)
// `bookmarked` value is a simplification of actual code
return <BookmarkButton bookmarked={state[project.id]} />
}
The result is a deployment lifecycle that runs itself: scheduled builds, per-commit deploys, and immediate refreshes after CMS content edits. No team member needs deploy credentials or a terminal to control what's in production. From a code change or a content edit to the live site, the chain is fully automated.
const headers = {
Accept: 'application/vnd.github.everest-preview+json',
Authorization: 'Bearer $token',
}
const body = JSON.stringify({ event_type: 'repository_dispatch' })
const url = `https://api.github.com/repos/cloudflare/built-with-workers/dispatches`
const handleRequest = async evt => {
await fetch(url, { method: 'POST', headers, body })
return new Response('OK')
}
addEventListener('fetch', handleRequest)
The same primitives—Workers Sites for serving static files, Workers KV for state, the HTML rewriter for assembling responses—lay the groundwork for what looks like a platform-defining way to build "full-stack serverless" applications, as distinct from plain static hosting. The next step is pushing that model beyond bookmark state to deeper dynamic interactions within the site.



