# Build a website with Claude Code, Codex or Cursor your team edits

> To build a website with an AI coding agent such as Claude Code, Codex or Cursor and keep it editable, let the agent write the code but keep every headline, image and post in a headless CMS that the site reads through a REST API at build time. Host the build on Cloudflare and have the CMS call a deploy hook on every change, so editors publish without a prompt or a developer. Give the agent one AGENTS.md file with the content rules and connect it to the CMS over MCP, so it codes against the real content model instead of guessing.

Source: https://diggama.com/guides/build-a-website-with-ai/
Last updated: 2026-10-07
Tested with: TypeScript 7.0, Node.js 22

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](/guides/build-a-website-with-ai/astro/) and the [Next.js tutorial](/guides/build-a-website-with-ai/nextjs/) 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](https://help.webflow.com/hc/en-us/articles/33961386739347): 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.

```mermaid
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](/guides/build-a-website-with-ai/astro/) |
| Next.js | Sites that become apps | Static generation, server components, ISR | Adapter required: `@opennextjs/cloudflare` or vinext | [Yes](/guides/build-a-website-with-ai/nextjs/) |
| 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](https://docs.astro.build/en/guides/integrations-guide/cloudflare/), and that the current Cloudflare adapter targets Workers and no longer supports Cloudflare Pages. For Next.js, the [OpenNext adapter](https://opennext.js.org/cloudflare) supports all of Next.js 16 and the latest minors of 14 and 15, while Cloudflare's own [Next.js guide](https://developers.cloudflare.com/workers/framework-guides/web-apps/nextjs/) 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](https://docs.diggama.com/blueprints): 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` | 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](https://docs.diggama.com/publishing), 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](/tools/content-model-generator/).

```prompt
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](https://docs.diggama.com/authentication). 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](/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](https://docs.diggama.com/mcp#managing-connections), 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](https://code.claude.com/docs/en/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](https://learn.chatgpt.com/docs/cli)) | `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](https://cursor.com/docs/cli/installation)) | `agent` in the terminal | Editor and CLI share one `mcp.json` |
| Gemini CLI | `npm install -g @google/gemini-cli` ([README](https://github.com/google-gemini/gemini-cli)) | `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](https://github.com/features/copilot/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](https://learn.chatgpt.com/docs/agent-configuration/agents-md)) | Nothing. Keep it short: Codex reads at most 32 KiB of combined instructions by default |
| Cursor | Yes, at the root and in subdirectories ([rules](https://cursor.com/docs/context/rules)) | Nothing. `.cursor/rules` stays available for file-specific rules |
| GitHub Copilot (VS Code) | Yes, `chat.useAgentsMdFile` is on by default ([instructions](https://code.visualstudio.com/docs/copilot/customization/custom-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](https://code.claude.com/docs/en/memory#agents-md)) | If a `CLAUDE.md` exists, put `@AGENTS.md` on its first line |
| Gemini CLI | No, it reads `GEMINI.md` ([docs](https://github.com/google-gemini/gemini-cli/blob/main/docs/cli/gemini-md.md)) | 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.

```markdown title="AGENTS.md"
# 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](https://code.claude.com/docs/en/memory#share-one-file-with-other-coding-tools), Claude reads the imported file first, then anything you add below the import.

```markdown title="CLAUDE.md"
@AGENTS.md
```

A longer, annotated version of the rules is in the [AGENTS.md template for websites](/guides/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](https://docs.diggama.com/mcp)). 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:

```bash
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](https://code.claude.com/docs/en/mcp)). 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.

```bash
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](https://learn.chatgpt.com/docs/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.

```bash
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](https://learn.chatgpt.com/docs/extend/mcp)):

```toml title="~/.codex/config.toml"
[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](https://cursor.com/docs/context/mcp)), so the project file is safe to commit.

```json title=".cursor/mcp.json"
{
  "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](https://github.com/google-gemini/gemini-cli/blob/main/docs/tools/mcp-server.md)). 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.

```json title=".gemini/settings.json"
{
  "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](https://code.visualstudio.com/docs/copilot/customization/mcp-servers)). 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.

```json title=".vscode/mcp.json"
{
  "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](https://github.com/microsoft/vscode-docs/blob/main/docs/agents/run/approvals.md)), 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](https://learn.chatgpt.com/docs/extend/mcp)).

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](https://developers.openai.com/plugins/build/auth)), 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](https://claude.com/docs/connectors/custom/add-unlisted)). The [MCP guide](/guides/headless-cms-mcp-server/) 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.

```prompt
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`.

```ts title="src/lib/diggama.ts"
// 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](https://docs.diggama.com/publishing)). A `view` token already hides drafts and scheduled posts, but the filter means that a build run by mistake with a `preview` token still ships only published posts. The filter compares `published_at` to 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 `view` token whatever its `published_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](https://docs.diggama.com/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.

```prompt
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.
```

```prompt
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.
```

```prompt
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](https://docs.diggama.com/create-resources)). The token never leaves the server.

```prompt
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](https://docs.diggama.com/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.

```prompt
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](https://developers.cloudflare.com/changelog/2026-04-01-deploy-hooks).

The sequence, which the framework tutorials walk through with every file and command:

1. Have your agent add the framework's Cloudflare setup (an adapter if you use server routes, and a `wrangler.jsonc`). The Worker `name` in that file must match the Worker name in the dashboard, or the build fails.
2. Push the repository to GitHub or GitLab.
3. 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 to](https://developers.cloudflare.com/workers/ci-cd/builds/configuration/) `npx wrangler deploy`.
4. Under the build settings, add `DIGGAMA_TOKEN` as a **build variable**. Cloudflare's documentation is explicit that build variables are not available at runtime, which is what you want for this token.
5. Add `DIGGAMA_FORM_TOKEN` as a runtime secret under **Settings › Variables & Secrets**, or from your terminal:

```bash
npx wrangler secret put DIGGAMA_FORM_TOKEN
```

6. Push a commit, watch the build log, and open the `workers.dev` URL. 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](https://developers.cloudflare.com/workers/ci-cd/builds/deploy-hooks/):

- 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:

```bash
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](https://docs.diggama.com/workflows)):

- It fires on three events: resource created, resource updated and resource deleted. Publishing and unpublishing count as updates.
- The request is a `POST` with 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](https://docs.diggama.com/json-import) fire no event. After an import, trigger one build yourself with the `curl` command above.
- Any `2xx` answer 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](https://manage.diggama.com/register) 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_TOKEN` is 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](https://claude.com/pricing)) |
| OpenAI Codex | A ChatGPT plan or API usage | Included in every ChatGPT plan, from Free; Plus at $20/month ([Codex pricing](https://learn.chatgpt.com/docs/pricing)) |
| Cursor | A Cursor plan | Hobby is free with limited agent requests; Individual from $20/month lists MCP ([Cursor pricing](https://cursor.com/pricing)) |
| Gemini CLI / GitHub Copilot | A Google account / a Copilot plan | Gemini CLI has a free tier of 1,000 requests a day ([quotas](https://github.com/google-gemini/gemini-cli/blob/main/docs/resources/quota-and-pricing.md)); Copilot Pro is $10/month ([plans](https://github.com/features/copilot/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](https://developers.cloudflare.com/workers/platform/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](/pricing/); for a full budget including design and development time, try the [website cost calculator](/tools/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.md` states 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 `view` token; the form uses a `create`-only token on `contact-submissions`
- [ ] `DIGGAMA_TOKEN` is a build variable; `DIGGAMA_FORM_TOKEN` is a runtime secret
- [ ] List requests page through all results (`per_page=100` and a loop until `meta.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`, `update` or `delete` it 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](/tools/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](/guides/build-a-website-with-ai/astro/)
- [Build the Northwind Studio site with Next.js, an AI coding agent and Diggama](/guides/build-a-website-with-ai/nextjs/)

If you are coming from a visual builder, [migrating from Webflow to an AI-built site](/guides/migrate-from-webflow/) covers moving the content and keeping your URLs, and [Webflow vs Framer vs AI coding agents](/guides/webflow-vs-framer-vs-ai-coding/) 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](/guides/why-ai-built-websites-need-a-cms/).

Still choosing a CMS? [The best headless CMS in 2026](/guides/best-headless-cms-2026/) compares ten platforms on hosting, pricing and how well AI agents can work with them, and [the web and CMS trends for 2027](/guides/web-and-cms-trends-2027/) covers where this way of building sites is heading.

To try the content side now, [create a Diggama project](https://manage.diggama.com/register) and build the four blueprints from step 1; the [API introduction](https://docs.diggama.com/introduction) has the first request.

## FAQ

### Can an AI coding agent build a complete website on its own?

Claude Code, Codex, Cursor and similar agents can scaffold the project, write every component and page, wire the data layer and run the build, all from prompts. What they cannot do is decide what your content model should be or keep the site editable for people who will never open a terminal. You still review the diff, own the repository and decide where content lives.

### Which AI coding agent should I use: Claude Code, Codex or Cursor?

Any of them works for this setup, and so do Gemini CLI and GitHub Copilot in agent mode. All of them can read an AGENTS.md file and connect to a remote MCP server with a bearer token. Only the install, the plan and the config file differ, so pick the one your team already pays for or prefers to review code in.

### Do I need to know how to code to build a website with AI?

You need to be comfortable with Git and with reading code well enough to review what the agent changes. You do not need to write the components yourself. The people who edit the site afterwards need no technical skill at all if the content lives in a CMS instead of in the code.

### Why not keep the content in Markdown files in the repository?

Markdown in the repo works when only developers edit the site. As soon as a marketer needs to fix a headline or publish a post, every change becomes a Git commit, which means a developer or an agent prompt for each typo. A headless CMS gives non-developers a form, drafts, roles and a publish button, while the code stays in Git.

### Can the agent read and write the content in the CMS directly?

Yes, if the CMS exposes an MCP server. Diggama's MCP server lets any MCP-capable agent list blueprints, read records and, when the token allows it, create, update or publish them. The tools the agent sees depend on the token's abilities, so a view-only token gives it no way to write at all.

### Does the live site update as soon as someone clicks Publish?

Not instantly. Publishing in Diggama fires a workflow that calls the Cloudflare deploy hook, which queues a new build; the change is live when that build finishes. Records scheduled for a future date trigger a rebuild within about five minutes of their publication time.

### Do I have to host on Cloudflare?

No. The architecture works on any host that can run a build and expose a deploy hook URL, including Vercel and Netlify. This series uses Cloudflare Workers because Workers Builds offers deploy hooks with built-in deduplication and the same platform runs the server endpoint for the contact form.
