This guide is the map for the whole series. It walks through every step from an empty folder to a live site that your marketing team edits without you, and explains the decisions at each step. It works with any major AI coding agent: Claude Code, OpenAI Codex and Cursor first, Gemini CLI and GitHub Copilot in agent mode too. The Astro tutorial and the Next.js tutorial then build the same site, file by file.
The example site throughout is Northwind Studio, a small company website with a homepage, a blog, a team page and a contact form.
Why are people leaving Webflow and Framer for AI coding agents?
People leave visual builders for AI coding agents because an agent removes the main reason they used a builder: not wanting to write and maintain the code by hand. With Claude Code, Codex or Cursor you describe a page, the agent writes the components, and you own a normal repository with no per-seat editor pricing, no platform-specific interactions, and no ceiling on what the site can do.
Webflow and Framer are good products. Their visual canvas is fast, designers can work without a developer, and the hosting, forms and CMS come in one box. Those are real strengths, and for a team of designers who never want to touch code they remain a sensible choice.
The trade-off is that the site lives inside the platform. Webflow’s own documentation states that CMS content is not included when you export a site’s code: collection lists render their empty state on an exported site. A developer who wants a custom integration, a different framework or a specific hosting setup hits the edge of the builder sooner or later.
Coding agents change the economics of that trade. Writing an Astro or Next.js site by hand is days of developer work. An agent that reads your repository, runs your build and fixes its own errors turns most of that work into prompting and reviewing.
What breaks after you switch to a site built with an AI agent?
The thing that breaks is content editing. A site generated by an AI coding agent typically has every headline, paragraph and image path written directly into components, so the only way to change a word is to edit code, which means a developer, or a prompt to the agent, followed by a redeploy.
It shows up in three ways within the first month:
- Copy is hardcoded. The hero headline sits in
src/components/Hero.astro, the team bios in a TypeScript array, the blog posts in Markdown files. Nothing is wrong with the code; it just has no editing surface. - Marketers are locked out. The people who used to fix typos in the Webflow Editor now have to file a request. They have no access to the repository, and you do not want them editing JSX.
- Every typo costs a prompt and a deploy. Changing “Get a quote” to “Request a quote” means opening the agent, prompting it, reviewing a diff, committing and waiting for the build. That is fine once. It is not fine twelve times a week.
The fix is not to stop using the agent. It is to separate the two things that a visual builder bundled together: the code that renders the site, and the content that the team edits. The agent owns the first, a headless CMS owns the second.
What does the architecture of an AI-built website with a CMS look like?
The architecture has three parts with one job each: a Git repository holds the code your agent writes, Diggama holds the content your team edits, and Cloudflare builds and hosts the site. The site reads content from Diggama’s REST API at build time, and Diggama triggers a rebuild whenever content changes.
flowchart TD dev["Developer + AI agent"] -->|git push| repo["Git repository"] dev -.->|"MCP (dev only)"| cms["Diggama"] team["Marketing team"] -->|"edit, publish"| cms repo -->|push starts a build| cf["Cloudflare Workers Builds"] cms -->|"Workflow: webhook"| cf cf -->|"REST API (build)"| cms cf -->|wrangler deploy| site["Live site"] site -->|"POST contact-submissions"| cms
Who owns what, and how changes reach production:
| Layer | Lives in | Changed by | Reaches production by |
|---|---|---|---|
| Layout, components, styles | Git repository | Developer with an AI coding agent | git push, then Cloudflare build |
| Copy, posts, images, team | Diggama | Editors in the dashboard | Publish, then webhook, then Cloudflare build |
| Content model (blueprints) | Diggama | Developer | Dashboard change, then matching code change |
| Form submissions | Diggama (read-only blueprint) | Site visitors | Server-side POST at request time, with a create-only token |
Two choices in this design are deliberate.
Content is fetched at build time, not in the browser. The output is static HTML, so pages load fast, the API token never reaches a visitor, and an API outage cannot take the site down. The cost is a rebuild after each publish, which step 5 automates.
The agent talks to the CMS over MCP during development only. The production site uses plain HTTP requests with a read-only token. MCP is how the agent discovers the content model and seeds test content while it writes the code.
Which framework should you ask your agent to use?
Pick Astro for a content site that is mostly pages and posts, and Next.js if the site will grow into an application with logged-in areas or heavy interactivity. Both work well with AI coding agents because they are widely used, well documented and have first-class Cloudflare deployment paths.
| Framework | Best for | Rendering for CMS content | Cloudflare deployment | Tutorial in this series |
|---|---|---|---|---|
| Astro | Marketing sites, blogs, docs | Static by default, server routes on demand | Static output needs no adapter; @astrojs/cloudflare for server routes | Yes |
| Next.js | Sites that become apps | Static generation, server components, ISR | Adapter required: @opennextjs/cloudflare or vinext | Yes |
| Nuxt | Vue teams | Static generation or server rendering | Nitro’s Cloudflare presets | Not written yet |
| SvelteKit | Svelte teams, small bundles | Prerendering or server rendering | @sveltejs/adapter-cloudflare | Not written yet |
| Plain HTML | One-page sites | A small Node script that fetches and writes HTML | Static assets only | Not written yet |
A few facts behind the table. Astro’s documentation says that a static Astro site needs no adapter, and that the current Cloudflare adapter targets Workers and no longer supports Cloudflare Pages. For Next.js, the OpenNext adapter supports all of Next.js 16 and the latest minors of 14 and 15, while Cloudflare’s own Next.js guide now recommends vinext, a Vite plugin that reimplements the Next.js API and is still in beta.
Whatever you pick, do not fetch CMS content from the browser with a token in client-side JavaScript. Anyone can read that token from the page source. Keep tokens in the build and in server code.
Step 1: How do you model the content before the agent writes any code?
Model the content first, because the content model is the contract between the code and the editors. In Diggama the model is a set of blueprints: each blueprint is a content type with typed fields, and its field keys become the keys of the attributes object the API returns.
Blueprints are created under Configuration › Blueprints in the dashboard. Diggama has three blueprint types: a resource (many records, like posts), a singular resource (exactly one record, like the homepage), and a read-only resource (records created through the API but not editable in the dashboard, like form submissions).
Here is the Northwind Studio model. Every tutorial in the series uses these exact blueprint and field keys.
| Blueprint | Type | Field key | Field type | Notes |
|---|---|---|---|---|
homepage | Singular resource | hero-title | Text | Main headline |
hero-subtitle | Text | One sentence under the headline | ||
hero-image | Image | Returned as an absolute URL | ||
intro | Rich text | Returned as an HTML string | ||
posts | Resource | title | Text | Required |
slug | Slug | Required; the URL segment, e.g. our-design-process | ||
excerpt | Text | Used on the blog index and in meta tags | ||
cover | Image | Absolute URL | ||
content | Rich text | HTML string | ||
team-members | Resource | name | Text | Required |
role | Text | |||
photo | Image | Absolute URL | ||
bio | Rich text | HTML string | ||
contact-submissions | Read-only resource | name | Text | Written by the form endpoint |
email | Validated by Diggama as an email | |||
message | Text |
Posts have no date field. Publication is handled by Diggama itself: every record carries a system published_at value that is null for a draft and a date once published, so you sort and filter on published_at instead of inventing your own. Only standard blueprints have that cycle: the singular homepage and the read-only submissions are never published, and a view token sees them whatever their published_at.
Field keys are derived from field names
When you add a field in the dashboard, Diggama derives its key from the name: “Hero title” becomes hero-title, with a hyphen, and the key cannot be changed after the field exists. Name fields with that in mind. In TypeScript, hyphenated keys are read with bracket notation: home.attributes['hero-title'].
Let the agent propose the model, then create it yourself
If you are migrating an existing site, a coding agent is good at turning pages into a content model. Ask for a proposal, review it, then create the blueprints in the dashboard. You can also sketch one with the content model generator.
Read every page in src/pages and every component in src/components. List each piece of text, image and repeated item that a non-developer would want to change. Group them into Diggama blueprints and output one Markdown table per blueprint with: field name, field type, required or not, and an example value from the current site.
Use only these Diggama field types: text, rich-text, slug, email, number, boolean, date, datetime, image, file, link, enum, multi-enum, reference, references.
Use a singular resource for one-off pages like the homepage and a read-only resource for anything visitors submit. Do not change any code yet.Create the tokens
Tokens are created under Configuration › API Tokens (Developer role or higher), and each one is scoped per blueprint and per ability. The value is shown once, so copy it straight into your password manager. Create one token per job so that a leaked token can do as little as possible.
| Token | Abilities | Blueprints | Used by |
|---|---|---|---|
| Build | view | homepage, posts, team-members | Cloudflare build, production |
| Preview | preview | homepage, posts, team-members | Preview build that shows drafts |
| Form | create | contact-submissions | Form endpoint at runtime |
| Agent (dev) | preview, optionally create and update | homepage, posts, team-members | MCP server in your coding agent, created in step 2 |
The number of API keys depends on the plan: one on Starter, three on Growth, five on Enterprise (see pricing). On Starter, remember that a token’s abilities are set per blueprint, so a single token can hold view on the three content blueprints and create on contact-submissions. Use it for the build and the form and keep it server-side. A token created on the API Tokens page works over MCP too, so the agent can share it, at the cost of seeing published records only. Split it into dedicated tokens when you upgrade. We have not confirmed whether an MCP connection counts toward a plan’s API-key limit, so check with Diggama before you plan around it.
Step 2: How do you set up your AI coding agent for a CMS-backed website?
Set up your agent with two things before the first prompt: an AGENTS.md file that states where content comes from, and the Diggama MCP server so the agent can read the real blueprints. Without both, any agent will hardcode copy, because that is the shortest path to a page that renders.
Install the agent you use. The rest of this guide is the same for all of them.
| Agent | Install | Start | Notes |
|---|---|---|---|
| Claude Code | curl -fsSL https://claude.ai/install.sh | bash (setup) | claude | Needs a paid Claude plan or a Console account; the free claude.ai plan does not include it |
| OpenAI Codex | npm install -g @openai/codex (CLI docs) | codex, then Sign in with ChatGPT | CLI, IDE extension and ChatGPT desktop app share one config |
| Cursor | The editor, or the CLI: curl https://cursor.com/install -fsS | bash (install) | agent in the terminal | Editor and CLI share one mcp.json |
| Gemini CLI | npm install -g @google/gemini-cli (README) | gemini | Free tier with a Google account |
| GitHub Copilot | VS Code with Copilot, in agent mode | Chat panel | Agent mode and MCP are listed on every plan, including Free (plans) |
Write one AGENTS.md that forbids hardcoded content
AGENTS.md at the repository root is the one instructions file most agents read without any setup. Write the content rules there once, rather than keeping a CLAUDE.md, a GEMINI.md and a Cursor rule in sync by hand.
| Agent | Reads AGENTS.md? | What to do |
|---|---|---|
| Codex | Yes, from the Git root down to the working directory (docs) | Nothing. Keep it short: Codex reads at most 32 KiB of combined instructions by default |
| Cursor | Yes, at the root and in subdirectories (rules) | Nothing. .cursor/rules stays available for file-specific rules |
| GitHub Copilot (VS Code) | Yes, chat.useAgentsMdFile is on by default (instructions) | Nothing |
| Claude Code | Yes since v2.1.277, but only when there is no CLAUDE.md or CLAUDE.local.md in the folder or above it (memory) | If a CLAUDE.md exists, put @AGENTS.md on its first line |
| Gemini CLI | No, it reads GEMINI.md (docs) | Add "context": { "fileName": ["AGENTS.md", "GEMINI.md"] } to .gemini/settings.json |
Here is the file for Northwind Studio. It names the MCP server diggama, which is the name every config below uses.
# Northwind Studio website
Company website. Code lives in this repo; all content lives in Diggama (headless CMS).
## Commands
- `npm run dev`: local dev server
- `npm run build`: production build (fetches content from Diggama)
## Content rules (non-negotiable)
- Never hardcode user-facing copy, images or lists in components. Every headline,
paragraph, image, post and team member comes from Diggama.
- All Diggama calls go through `src/lib/diggama.ts`. Pages never call fetch() on
the API directly.
- Content is fetched at build time. Never send a Diggama token to the browser.
- Allowed hardcoded strings: navigation labels, form labels, legal footer.
- If a page needs content that has no blueprint field yet, stop and tell me
which field to add. Do not invent a fallback string.
## Diggama
- API: https://api.diggama.com/v2, header `Authorization: Bearer $DIGGAMA_TOKEN`
- Blueprints: `homepage` (singular), `posts`, `team-members`,
`contact-submissions` (read-only, written by the form endpoint only)
- Field keys use hyphens: read them as `attributes['hero-title']`.
- Images are absolute URLs; rich text fields are HTML strings.
- Use the `diggama` MCP server to check field keys before writing code against
a blueprint. Do not guess keys. MCP returns flattened records; the REST API
nests fields under `attributes`. Site code uses the REST shape.
- MCP writes are allowed only for draft test content, and only after showing me
a dry run.
## Environment variables
- `DIGGAMA_TOKEN`: view token, build time only
- `DIGGAMA_FORM_TOKEN`: create-only token on contact-submissions, server runtime only
- `DIGGAMA_MCP_TOKEN`: the agent's own token, in my shell only, never in the repo
If the project already has a CLAUDE.md, for example one your framework generated, make Claude Code read both files with an import at the top. Per Anthropic’s memory documentation, Claude reads the imported file first, then anything you add below the import.
@AGENTS.md
A longer, annotated version of the rules is in the AGENTS.md template for websites.
Connect the Diggama MCP server
Diggama runs a remote MCP server at https://api.diggama.com/mcp that authenticates with a project token sent as a bearer header, with no OAuth (MCP docs). The quickest way to get a token for it is Configuration › Connect to AI in the dashboard: pick Customise per content type, tick preview on homepage, posts and team-members, name the connection after your agent, and copy the token, which is shown once. Then put it in your shell profile, not in the repository:
export DIGGAMA_MCP_TOKEN="your-dev-token"
Why preview rather than view: without it, the agent only sees published records, so it cannot read the drafts it seeds in step 3 or the posts editors are still writing. The preset called Read only has view alone. Avoid Full access for day-to-day coding: it includes permanent deletion.
Every agent below can send that header. Only the place the config lives and the way it reads the token differ.
| Agent | Where the server is configured | How the token is read | Check it with |
|---|---|---|---|
| Claude Code | claude mcp add, or a project .mcp.json | Written into the config, or ${DIGGAMA_MCP_TOKEN} in .mcp.json | /mcp or claude mcp list |
| Codex | codex mcp add, stored in ~/.codex/config.toml | bearer_token_env_var | /mcp or codex mcp list |
| Cursor | .cursor/mcp.json (project) or ~/.cursor/mcp.json | ${env:DIGGAMA_MCP_TOKEN} | agent mcp list |
| Gemini CLI | .gemini/settings.json (project) or ~/.gemini/settings.json | ${DIGGAMA_MCP_TOKEN} | /mcp list or gemini mcp list |
| GitHub Copilot | .vscode/mcp.json | A password prompt, stored by VS Code | MCP: List Servers |
Claude Code. The user scope stores the server in ~/.claude.json in your home directory, so nothing is committed (MCP docs). Your shell expands $DIGGAMA_MCP_TOKEN when you run the command, so it is the token value that gets stored there. For a team, a committed .mcp.json with "Authorization": "Bearer ${DIGGAMA_MCP_TOKEN}" works too: Claude Code expands the variable, and asks each person to approve the server in an interactive session before using it.
claude mcp add --transport http diggama https://api.diggama.com/mcp \
--scope user \
--header "Authorization: Bearer $DIGGAMA_MCP_TOKEN"
Codex. The flag names an environment variable whose value is sent as the bearer token, so the token itself never lands in the config (CLI reference). The CLI, the IDE extension and the ChatGPT desktop app share this configuration. Codex cloud tasks have no documented MCP support, so run this part locally.
codex mcp add diggama --url https://api.diggama.com/mcp --bearer-token-env-var DIGGAMA_MCP_TOKEN
The same server written by hand (Codex MCP docs):
[mcp_servers.diggama]
url = "https://api.diggama.com/mcp"
bearer_token_env_var = "DIGGAMA_MCP_TOKEN"
Cursor. The editor and the agent CLI read the same file, and ${env:NAME} is expanded in headers (Cursor MCP docs), so the project file is safe to commit.
{
"mcpServers": {
"diggama": {
"url": "https://api.diggama.com/mcp",
"headers": { "Authorization": "Bearer ${env:DIGGAMA_MCP_TOKEN}" }
}
}
}
Gemini CLI. Use httpUrl, not url: in Gemini CLI, url means the older SSE transport (MCP docs). The same file also tells it to read AGENTS.md. If you turn Folder Trust on, an untrusted folder ignores this file and connects no MCP server.
{
"context": { "fileName": ["AGENTS.md", "GEMINI.md"] },
"mcpServers": {
"diggama": {
"httpUrl": "https://api.diggama.com/mcp",
"headers": { "Authorization": "Bearer ${DIGGAMA_MCP_TOKEN}" }
}
}
}
GitHub Copilot in VS Code. VS Code asks for the token once and stores it, so the file holds no secret (MCP configuration). VS Code’s documentation now describes .vscode/mcp.json as deprecated and kept for compatibility, and prefers a portable .mcp.json for new servers; the inputs prompt shown here is the documented way to keep the token out of the file.
{
"inputs": [
{ "type": "promptString", "id": "diggama-token", "description": "Diggama API token", "password": true }
],
"servers": {
"diggama": {
"type": "http",
"url": "https://api.diggama.com/mcp",
"headers": { "Authorization": "Bearer ${input:diggama-token}" }
}
}
}
Leave tool approval on while the agent builds the site. Cursor and Gemini CLI ask before each MCP tool call by default, VS Code keeps global auto-approval off by default (approvals), and Codex prompts only for tools that are not marked read-only when you add default_tools_approval_mode = "writes" under [mcp_servers.diggama] (Codex MCP docs).
If you change the token’s abilities or add a blueprint later, edit the token on the API Tokens page and reconnect the agent: clients cache the tool list, and a preset only covers the blueprints that existed when you created it. The chat apps are a different case. ChatGPT on the web cannot present a custom API key or bearer token to an MCP server, only a token from an OAuth flow (OpenAI auth docs), and Diggama does not offer OAuth. In claude.ai, custom request headers are a beta that only some organizations have, set by an Owner (Claude connectors). The MCP guide covers every tool, the write safeguards and those chat-app setups.
One trap to know before the agent writes code: MCP and REST return records in different shapes. Over MCP, a record is flattened (id, published, published_at, created_at, updated_at, then one key per field). Over REST, the fields sit under attributes. The agent should read field keys from MCP but write code against the REST shape.
Step 3: How do you get your AI agent to build the site?
Have your agent build the site in small, verifiable passes: first the data layer, then one page at a time, each pass ending with a build that succeeds against real content. Large “build my whole website” prompts work, but they produce more hardcoded fallbacks and more code to review at once.
Pass 1: the data layer
The single most important file is the fetch helper. Every page goes through it, so there is exactly one place that knows the API URL, the token, pagination and the publication rules.
Use the diggama MCP server to describe the blueprints homepage, posts and team-members. Then create src/lib/diggama.ts that:
- exports TypeScript types for each blueprint's attributes, using the exact field keys from the MCP output (the REST API, unlike MCP, nests them under "attributes")
- exports createDiggama(token) returning homepage(), posts(), post(slug) and teamMembers()
- reads the singular homepage from data[0] of GET /v2/resources/homepage
- requests filter[published]=true on list endpoints and pages through all results with per_page=100
- throws an error with the status code and URL when the API does not answer 200
Do not create any page yet. Show me the file.What you should get back looks like this. It is framework-agnostic: Astro passes import.meta.env.DIGGAMA_TOKEN, Next.js passes process.env.DIGGAMA_TOKEN.
// One place for every call to Diggama. Pages import the functions below,
// never fetch() the API themselves.
const API = 'https://api.diggama.com/v2';
export type DiggamaResource<A> = {
id: string;
type: 'resource';
published_at: string | null;
created_at: string;
updated_at: string;
attributes: A;
};
type ListResponse<A> = {
data: DiggamaResource<A>[];
meta: { current_page: number; last_page: number; per_page: number; total: number };
};
// Attribute keys are the field keys Diggama derived from the field names.
// Image fields come back as absolute URLs, rich text as HTML strings.
export type Homepage = {
'hero-title': string | null;
'hero-subtitle': string | null;
'hero-image': string | null;
intro: string | null;
};
export type Post = {
title: string;
slug: string;
excerpt: string | null;
cover: string | null;
content: string | null;
};
export type TeamMember = {
name: string;
role: string | null;
photo: string | null;
bio: string | null;
};
export function createDiggama(token: string | undefined, options: { drafts?: boolean } = {}) {
if (!token) throw new Error('Diggama token is missing: set DIGGAMA_TOKEN');
async function request<T>(path: string, params: Record<string, string>): Promise<T> {
const url = new URL(`${API}/${path}`);
for (const [key, value] of Object.entries(params)) url.searchParams.set(key, value);
const res = await fetch(url, {
headers: { Accept: 'application/json', Authorization: `Bearer ${token}` },
});
if (!res.ok) {
const body = await res.text();
throw new Error(`Diggama ${res.status} on ${url.pathname}${url.search}: ${body.slice(0, 300)}`);
}
return (await res.json()) as T;
}
// Every page of a list, 100 records at a time (the API maximum).
// Unless drafts are wanted, only records whose published_at has passed:
// the API compares it to the current time, to the second, so a post
// scheduled for later today stays out of the build.
async function list<A>(blueprint: string, params: Record<string, string> = {}) {
const records: DiggamaResource<A>[] = [];
for (let page = 1; ; page++) {
const query: Record<string, string> = { ...params, per_page: '100', page: String(page) };
if (!options.drafts) query['filter[published]'] = 'true';
const json = await request<ListResponse<A>>(`resources/${blueprint}`, query);
records.push(...json.data);
if (page >= json.meta.last_page) break;
}
return records;
}
// A singular blueprint holds exactly one record, which the API always
// serves: it has no draft state, so it is read without the published
// filter. V2 returns it as a one-item list.
async function single<A>(blueprint: string) {
const json = await request<ListResponse<A>>(`resources/${blueprint}`, { per_page: '1' });
const record = json.data[0];
if (!record) throw new Error(`Diggama: "${blueprint}" has no record yet`);
return record;
}
return {
homepage: () => single<Homepage>('homepage'),
posts: () => list<Post>('posts', { sort: '-published_at' }),
post: async (slug: string) => (await list<Post>('posts', { 'filter[slug][eq]': slug }))[0] ?? null,
teamMembers: () => list<TeamMember>('team-members', { sort: 'name' }),
};
}
Three details in that file are worth keeping whatever your agent produces:
- It asks for published records explicitly with
filter[published]=true(publishing docs). Aviewtoken already hides drafts and scheduled posts, but the filter means that a build run by mistake with apreviewtoken still ships only published posts. The filter comparespublished_atto the current time, to the second, so a post scheduled for 18:00 today stays out of a build that runs at 9:00. A preview build passes{ drafts: true }and gets everything. - It reads the homepage without that filter. A singular resource has no publication cycle, so the API serves it to a
viewtoken whatever itspublished_at, and filtering it would only risk an empty result. - It reads every page. Lists return 25 records by default and at most 100 per request (pagination). A blog that quietly stops at 25 posts is a classic bug in generated code.
Pass 2: the pages
With the helper in place, build one page per prompt and check each against real content.
Build the homepage from the homepage blueprint, using createDiggama from src/lib/diggama.ts with the DIGGAMA_TOKEN environment variable. Render hero-title as the h1, hero-subtitle under it, hero-image as the hero image, and intro as HTML below the hero. The API returns the image as a bare URL with no dimensions, so set width and height attributes from the design's aspect ratio and use object-fit: cover. No hardcoded copy except the navigation. Then run npm run build and fix any error until it passes.Build the blog: an index page listing posts() with cover, title, excerpt and the published_at date, and one static page per post at /blog/[slug]/ generated from posts() at build time. Render content as HTML. Add a meta description from excerpt. Run npm run build and confirm one HTML file per published post in the output.Build the team page from teamMembers(): photo, name, role and bio for each member. Then search every file in src/ for user-facing strings that are not navigation labels, form labels or the legal footer, and list them with file and line. Do not fix them yet.That last prompt is the habit that keeps the site editable. Run it after every feature.
Pass 3: the contact form
The form is the only part that runs at request time. A server endpoint receives the submission, validates it, and creates a record in the contact-submissions blueprint with the create-only token, using POST /v2/resources/contact-submissions (create resources). The token never leaves the server.
Add a contact form on /contact/ with name, email and message, and a server endpoint that receives it. The endpoint validates that all three fields are present, that email looks like an email and that message is under 5000 characters, and silently drops any request that fills a hidden honeypot field. Then it POSTs {"attributes": {"name": ..., "email": ..., "message": ...}} as JSON to https://api.diggama.com/v2/resources/contact-submissions with "Authorization: Bearer" and the DIGGAMA_FORM_TOKEN environment variable, read at runtime on the server. Return a JSON success, a 400 with field errors, or a 502 if Diggama answers anything other than 201. Keep every other page static.Validate in your endpoint rather than relying only on the CMS to reject bad input: you control the error messages, and spam never reaches your project. Diggama answers 201 Created on success and 422 VALIDATION_ERROR if a value does not match its field type or a required field is missing (errors). To have the team notified, add a Diggama Workflow with an Event type condition (Resource Created), a Resource type condition (contact-submissions) and the Send email action; the body can insert {{name}}, {{email}} and {{message}}.
Seeding content through MCP
Before editors fill the CMS, the agent can create test content so the pages have something to render. Give the agent’s token create on posts for this, and remove it afterwards.
Using the diggama MCP server, call describe_blueprint on posts, then create three draft posts about Northwind Studio's design process, each with a title, a slug derived from the title, an excerpt, and content of about 300 words as HTML paragraphs. Leave cover empty. Show me a dry run of the first one before writing anything. Leave all three unpublished.Records created over MCP start as drafts, and Diggama’s MCP write tools accept a dry_run flag that validates the payload and shows the result without writing. There is no upload tool, so image fields only accept the URL of a file already in Diggama: leave covers to the editors. Every real MCP write also fires your workflows, so a seeding run with the rebuild workflow from step 5 active will trigger builds. Writes over MCP are capped at 60 a minute per project.
Step 4: How do you deploy the site to Cloudflare?
Deploy by connecting the repository to a Cloudflare Worker with Workers Builds: every push to the production branch runs your build command, then npx wrangler deploy. The Diggama token is a build variable, because the content is fetched during the build; the form token is a runtime secret, because the form endpoint needs it on each request.
This series deploys to Workers rather than Cloudflare Pages, for two reasons: Astro’s Cloudflare adapter no longer supports Pages, and deploy hooks, which step 5 relies on, came to Workers Builds in April 2026.
The sequence, which the framework tutorials walk through with every file and command:
- Have your agent add the framework’s Cloudflare setup (an adapter if you use server routes, and a
wrangler.jsonc). The Workernamein that file must match the Worker name in the dashboard, or the build fails. - Push the repository to GitHub or GitLab.
- In the Cloudflare dashboard, open Workers & Pages, create a Worker from your repository, and set the build command to
npm run build. The deploy command defaults tonpx wrangler deploy. - Under the build settings, add
DIGGAMA_TOKENas a build variable. Cloudflare’s documentation is explicit that build variables are not available at runtime, which is what you want for this token. - Add
DIGGAMA_FORM_TOKENas a runtime secret under Settings › Variables & Secrets, or from your terminal:
npx wrangler secret put DIGGAMA_FORM_TOKEN
- Push a commit, watch the build log, and open the
workers.devURL. Add your domain once the content checks out.
Step 5: How do you rebuild the site when content is published?
Connect Diggama to Cloudflare with a deploy hook: Cloudflare gives you a URL that starts a build when it receives a POST, and a Diggama Workflow with the Send webhook action calls that URL whenever a record changes. Editors click Publish, and the site is up to date as soon as that build finishes.
Create the deploy hook in Cloudflare
In your Worker, go to Settings › Builds › Deploy Hooks, give the hook a name, select the production branch, and copy the URL. According to Cloudflare’s deploy hooks documentation:
- the hook needs no authorization header, so the URL itself is the secret; keep it out of your repository
- if the hook fires again while a build is still queued or initializing, no second build is created
- hooks are rate limited to 10 builds per minute per Worker and 100 per minute per account
You can test it from a terminal before wiring Diggama:
curl -X POST "https://api.cloudflare.com/client/v4/workers/builds/deploy_hooks/YOUR_HOOK_ID"
Create the Workflow in Diggama
In the Diggama dashboard, open Configuration › Workflows and create a Workflow with the Event trigger type, one Resource type condition set to posts, and one action, Send webhook, with the deploy hook URL as the Destination URL. Add no Event type condition, so creates, updates and deletes all rebuild. Repeat for homepage and team-members.
One Workflow per blueprint is not an oversight. A Workflow’s conditions must all pass, so a single Workflow cannot match several blueprints. And a Workflow with no condition would also fire on every contact form submission, rebuilding the site each time a visitor writes to you.
What the webhook does, precisely (workflows docs):
- It fires on three events: resource created, resource updated and resource deleted. Publishing and unpublishing count as updates.
- The request is a
POSTwith the body{"event": "resource_updated"}(or the matching event name) and nothing else. Cloudflare ignores the body, which is fine: the build fetches everything again. - Saving a draft also counts as an update and triggers a build. The build only includes published records, so nothing leaks, it just costs a build. Background autosaves in the editor do not fire workflows.
- When a record scheduled for a future date reaches its publication time, Diggama fires an update event within about five minutes, so scheduled posts go live without anyone touching the dashboard.
- Records created by a JSON import fire no event. After an import, trigger one build yourself with the
curlcommand above. - Any
2xxanswer counts as success. Diggama tries twice, with a 10-second timeout. If builds do not start, open the Workflow’s Execution history: a failed run shows the error the hook returned.
If a burst of edits arrives while a build is queued, Cloudflare’s deduplication absorbs it. If the build is already running, the next hook call queues one more build, so the last edit is always included.
Step 6: How do you hand the CMS over to the marketing team?
Hand over by giving each person a role that matches what they do, and by showing them the one loop they need: edit, preview, publish, wait for the build. Diggama has three roles, and each includes the permissions of the one before it.
| Role | Can do | Give it to |
|---|---|---|
| Editor | Create, edit, publish and delete content, upload images | Marketers, writers |
| Developer | Everything an Editor can, plus blueprints, workflows, API tokens and Connect to AI | You, and anyone who changes the code |
| Administrator | Everything a Developer can, plus members and project settings | The site owner |
Add people from Configuration › Users. Each person must already have a Diggama account: members are added by the email of an existing account and no invitation email is sent, so ask them to sign up first.
Editors cannot change blueprints. That protects the contract: a marketer cannot rename a field and break a page that reads it.
Drafts and previews
Posts and team members have a draft state. A draft is invisible to the production build, so editors can save work in progress at any time. For a full-page preview before publishing, run a second build:
- a second Worker (or a non-production branch build) whose
DIGGAMA_TOKENis the preview token - a build flag that makes the site call
createDiggama(token, { drafts: true }) - a second deploy hook, called by the same Workflows, so the preview rebuilds on every save
Put the preview site behind access control, for example Cloudflare Access, since it shows unpublished content.
The homepage is a singular resource, with no draft or publish switch in the dashboard: every saved change goes live with the next build. Tell editors that explicitly.
A one-page handover note
Write the team a short note with: the dashboard URL, which blueprint drives which page, how long a rebuild takes on your site (time it once and write the real number down), the preview URL, and who to call when a page needs a field that does not exist yet. That last item is the only time they need a developer.
What does this stack cost to run?
The stack has three bills: a plan for the AI coding agent of whoever writes the code, a Diggama plan for the content, and Cloudflare for hosting and builds.
| Service | What you pay for | Starting point |
|---|---|---|
| Claude Code | The developer’s Claude subscription or Console usage | Pro at $20/month (Anthropic pricing) |
| OpenAI Codex | A ChatGPT plan or API usage | Included in every ChatGPT plan, from Free; Plus at $20/month (Codex pricing) |
| Cursor | A Cursor plan | Hobby is free with limited agent requests; Individual from $20/month lists MCP (Cursor pricing) |
| Gemini CLI / GitHub Copilot | A Google account / a Copilot plan | Gemini CLI has a free tier of 1,000 requests a day (quotas); Copilot Pro is $10/month (plans) |
| Diggama | Content, users, blueprints, API keys | Starter at 29.90 EUR/month: 3 users, 5 custom blueprints, 1 API key |
| Cloudflare Workers | Hosting, builds, the form endpoint | A free plan exists; requests to static assets are free and unlimited (Workers pricing) |
Diggama’s Growth plan (89 EUR/month) raises the limits to 6 users, 10 blueprints and 3 API keys, enough for dedicated build, form and preview tokens. Enterprise (249 EUR/month) adds 2FA and priority support with 12 users, 25 blueprints and 5 API keys. MCP, the AI Assistant and Workflows are included on every plan, and Diggama offers a three-month free trial with no credit card. There is no free plan. Details on the pricing page; for a full budget including design and development time, try the website cost calculator.
Launch checklist
Before you announce the site, check each line:
- Every blueprint in the model exists in Diggama with the exact field keys the code uses
-
AGENTS.mdstates the content rules, every agent on the team reads it, and the last hardcoded-string audit came back clean - All API calls go through
src/lib/diggama.ts; no token appears in client-side code or in the repository - The production build uses a
viewtoken; the form uses acreate-only token oncontact-submissions -
DIGGAMA_TOKENis a build variable;DIGGAMA_FORM_TOKENis a runtime secret - List requests page through all results (
per_page=100and a loop untilmeta.last_page), so the blog does not stop at 25 posts - One Workflow per content blueprint calls the deploy hook; a test publish triggers a build
- A test form submission appears in
contact-submissions, and the team gets the notification email - Editors have the Editor role, and have published one change themselves end to end
- The agent’s MCP token has lost any
create,updateordeleteit was given for seeding - The preview site, if any, is behind access control
- Images have width and height, pages have titles and meta descriptions; run the website grader on the live URL
What are the most common mistakes?
Most failures in AI-built websites with a CMS come from the boundary between code and content, not from the code itself. These are the ones we see most often, whichever agent wrote the code.
Letting the agent add fallback copy. A component that renders home.attributes['hero-title'] ?? 'Design that works' looks safe, but the fallback ships the day the field is empty and nobody knows where it came from. Forbid fallbacks in AGENTS.md and fail the build instead.
Guessing field keys. Agents will happily write heroTitle or hero_title when the key is hero-title, or read post.title (the flattened MCP shape) instead of post.attributes.title. Without types, the page renders nothing and no error tells you why. Make the agent read the blueprint over MCP before it writes code against it.
Instructions the agent never reads. Rules in a CLAUDE.md do nothing for Codex, and an AGENTS.md does nothing for Gemini CLI until you add it to context.fileName. Ask the agent to quote the content rules back at the start of a session if you are not sure.
Fetching from the browser. A token in client-side JavaScript is public. Even a read-only token there means anyone can query your project and spend your API quota.
Using one all-powerful token everywhere. The build needs view. The form needs create on one blueprint. The agent needs preview and, briefly, create. Three narrow tokens are safer than one broad one.
Forgetting pagination. Lists return 25 records by default. A blog index built from one request stops at 25 posts.
One Workflow without a resource type condition. Every contact form submission then rebuilds the whole site.
Giving MCP delete “just in case”. Deletion in Diggama is permanent, with no undo. Grant delete to the agent’s token only for a specific clean-up, then remove it.
Changing a blueprint without changing the code. Removing a field in the dashboard breaks the pages that read it at the next build. Treat blueprint changes like a database migration: change the model and the code together, and have the agent update the types from the MCP output.
Where do you go from here?
Pick your framework and follow the full build, with every file shown:
- Build the Northwind Studio site with Astro, an AI coding agent and Diggama
- Build the Northwind Studio site with Next.js, an AI coding agent and Diggama
If you are coming from a visual builder, migrating from Webflow to an AI-built site covers moving the content and keeping your URLs, and Webflow vs Framer vs AI coding agents helps decide whether the switch is worth it for your team. For the reasoning behind this whole setup, read why AI-built websites need a CMS.
Still choosing a CMS? The best headless CMS in 2026 compares ten platforms on hosting, pricing and how well AI agents can work with them, and the web and CMS trends for 2027 covers where this way of building sites is heading.
To try the content side now, create a Diggama project and build the four blueprints from step 1; the API introduction has the first request.