What is your developer-dependent CMS actually costing you? Calculate your costs and get a full report
What is your developer-dependent CMS actually costing you? Calculate your costs and get a full report
Vibe Coding
Use Claude Code, Cursor or Copilot with the Agility Next.js starter, an AGENTS.md, and the Agility CMS and Knowledgebase MCP servers to model, build, preview and ship new components.
You can build and extend an Agility CMS website with an AI coding tool and get code you would actually ship: real components, wired to real content models, cached correctly and previewable by your editors.
This article is part of the Vibe Coding with Agility section.
The trick is the same one that works for building Agility apps with AI coding tools: give the agent the right context. For a website that means three things:
AGENTS.md that tells the agent the Agility-specific conventions it can't discover by reading your code.This guide uses Claude Code in its examples, but the approach works the same in Cursor, GitHub Copilot, Codex, Windsurf, or any tool that reads a project instruction file and supports MCP.
The flagship starting point is the Agility Next.js Starter, a Next.js 16 App Router site built on React Server Components. Before you hand it to an agent, it helps to know the moving parts (the full tour is in How the Next.js Starter Works):
app/[...slug]/page.tsx, resolves every URL against the Agility sitemap. Editors decide which pages exist.components/agility-pages/, which render named zones with <ContentZone>.components/agility-components/, looked up by name through a registry.lib/cms/ (getContentItem, getContentList, getSitemapFlat, getSitemapNested) that tags the request with a Next.js fetch cache tag and a 60-second revalidate. App-specific shaping lives in lib/cms-content/./api/revalidate, which calls revalidateTag() for the tags that changed.draftMode(). In draft mode, and always under npm run dev, the site requests content with the preview API key, so editors see their latest saved drafts.The starter is also already set up for AI tools. It ships its own AGENTS.md, a .cursorrules file, a docs/ folder written for AI assistants, and a .vscode/mcp.json that connects VS Code to the Agility CMS MCP server. You'll build on all of that.
Clone the starter and install dependencies:
git clone https://github.com/agility/agilitycms-nextjs-starter my-site
cd my-site
npm install
cp .env.local.example .env.local
Fill in .env.local with your instance details. You'll find the GUID and API keys in Agility under Settings → API Keys (see API Key Management):
AGILITY_GUID=
AGILITY_API_FETCH_KEY=
AGILITY_API_PREVIEW_KEY=
AGILITY_SECURITY_KEY=
AGILITY_LOCALES=en-us
AGILITY_SITEMAP=website
Run npm run dev and confirm the site loads at http://localhost:3000 before you bring in an agent. If the starter doesn't run cleanly, the agent will spend its first session debugging your environment instead of building.
Every major AI coding tool reads a project instruction file at the start of a session. AGENTS.md is the open standard most of them support. Claude Code reads CLAUDE.md, so either symlink one to the other (ln -s AGENTS.md CLAUDE.md) or keep a short CLAUDE.md that points at AGENTS.md.
Start from the AGENTS.md that ships with the starter. It already covers the project layout, the three-tier data pattern (component, then lib/cms-content/, then lib/cms/, then the SDK), the rule that every new component must be registered in components/agility-components/index.ts, how to add a component, domain helper or page template, how to test preview, the cache tags, and how to use the Agility CMS MCP server to check models before writing code. Read it, keep it, and correct anything that no longer matches your project as it evolves.
Then add what's specific to how you work with agents. The value of an instruction file comes from things the agent can't figure out on its own. It can read package.json and see you use Next.js. It can't know that your cache tags are a contract with a webhook, that a list call returns only 10 items unless you ask for more, or which publishing level your team has chosen for it: manual, via approval, or autonomous.
Here are the sections we recommend adding:
## Working with the Agility MCP
- save_content_items saves to Staging. Nothing is live until it's published.
- Publishing: [manual | via approval | autonomous]. Keep the one your team chose:
- manual: never publish. Save to Staging and list what I should review.
- via approval: save, then request approval (manage_content_workflow / manage_page_workflow).
- autonomous: publish (publish_content / publish_page) once your checks pass, and list what you published.
- Never unpublish or delete content, pages, or media without asking me.
- Before creating a model, list existing ones (get_content_models,
get_component_models) and reuse when one fits. After creating one, read it
back (get_content_model_details / get_component_model_details) and use the
real field names.
- For Agility API or SDK questions, search the docs with the Agility
Knowledgebase MCP (search_docs, fetch_doc) before guessing.
## List limits
- getContentList (Fetch SDK) returns 10 items by default and at most 250 per
request. Always pass take explicitly, and page with skip until you've read
totalCount.
## Reference names
- When writing to Agility (saving content, linked-content fields), use
container and model reference names in the exact case get_containers and
get_content_models return. Reads return reference names lowercased.
- The component registry in components/agility-components/index.ts matches
the component model's reference name case-insensitively.
- Field values arrive with a lowercase first letter (a field named Title is
fields.title).
## Cache tags are a contract with /api/revalidate
- Every read goes through lib/cms/. Each wrapper sets
agilitySDK.config.fetchConfig = { next: { tags: [...], revalidate: 60 } }.
A new wrapper must do the same.
- Tags used by the wrappers:
agility-content-{contentID}-{locale} (getContentItem)
agility-content-{referenceName}-{locale} (getContentList)
agility-sitemap-flat-{locale} (getSitemapFlat)
agility-sitemap-nested-{locale} (getSitemapNested)
- app/api/revalidate/route.ts handles Published events only. For content it
revalidates the contentID and referenceName tags; for pages it revalidates
agility-page-{pageID}-{locale} and both sitemap tags.
- Tags are case-sensitive strings. The referenceName in a list tag must match
the referenceName the webhook sends, character for character. If you change
a tag format, change it in the wrapper and in the webhook route together.
Write these additions yourself, from what you know about the project. A generated instruction file tends to restate what the agent could already read in the code, which costs tokens without adding context.
The two servers do different jobs, and you want both.
| Server | URL | Auth | What the agent uses it for |
|---|---|---|---|
| Agility CMS MCP | https://mcp.agilitycms.com/api/mcp | OAuth (your Agility login) | Reading and creating content models, component models, containers, content, and pages in your instance |
| Agility Knowledgebase MCP | https://docs.agilitycms.com/docs/api/mcp | None (read-only, public docs) | Searching and reading the Agility documentation (search_docs, fetch_doc) |
Add both from your project folder:
claude mcp add --transport http "Agility-CMS" https://mcp.agilitycms.com/api/mcp
claude mcp add agility-knowledgebase --transport http https://docs.agilitycms.com/docs/api/mcp
Or commit a .mcp.json in the project root for the Knowledgebase server so everyone on the team gets it:
{
"mcpServers": {
"agility-knowledgebase": {
"url": "https://docs.agilitycms.com/docs/api/mcp"
}
}
}
The first time the agent calls the Agility CMS MCP, a browser window opens so you can sign in to Agility. The server acts with your permissions, so it can only see and change what your account can.
.vscode/mcp.json already points at the server. For other editors, use the one-click install buttons at mcp.agilitycms.com/instructions or follow the per-client steps in Agility CMS MCP Server..cursor/mcp.json with the same url block shown above. Setup for other clients is in Agility Knowledgebase MCP Server.Check that both are connected before you start (in Claude Code, run /mcp). A quick sanity prompt: "List the component models in my Agility instance, then search the Agility docs for how ContentZone works." If the agent answers both from tool calls, you're ready.
This is where the setup pays off. Ask for one complete feature: the model in Agility, the React component, and the registration, in a single focused session.
Here's an example prompt for a testimonials block:
Add a "Testimonials" component to the site.
1. Check the Agility docs (Knowledgebase MCP) for component model and
linked content guidance.
2. Check my instance for existing models. Reuse a Testimonial content model
if one exists; otherwise create one with: Quote (multi-line text),
Name (text), Role (text), Photo (image).
3. Create a "Testimonials" component model with a Heading (text) and a
linked content list of Testimonial items.
4. Build components/agility-components/Testimonials.tsx as a Server
Component following the starter's patterns, using AgilityPic for the
photo, and register it.
5. Add 3 sample testimonials to the container. Do not publish anything.
A good run looks like this:
search_docs to ground itself, then get_content_models and get_component_models to see what already exists.save_content_model and save_component_model, then reads them back with get_component_model_details to confirm the field names.contentLinkDepth: 0, so a component receives only module.contentid and fetches its own fields through getContentItem (the same way the starter's Heading component does). The linked list is then read through getContentList, which returns the SDK's list response with an items array:// components/agility-components/Testimonials.tsx
import { UnloadedModuleProps, AgilityPic, ImageField, ContentItem } from "@agility/nextjs"
import { getContentItem } from "lib/cms/getContentItem"
import { getContentList } from "lib/cms/getContentList"
interface ITestimonial {
quote: string
name: string
role?: string
photo?: ImageField
}
interface ITestimonials {
heading: string
testimonials: { referencename: string }
}
export default async function Testimonials({ module, languageCode }: UnloadedModuleProps) {
// The component's own fields (tagged agility-content-{contentID}-{locale}).
const { fields, contentID } = await getContentItem<ITestimonials>({
contentID: module.contentid,
languageCode,
})
// The linked list (tagged agility-content-{referenceName}-{locale}), with an explicit take.
const list = await getContentList({
referenceName: fields.testimonials.referencename,
languageCode,
take: 50,
})
const items = list.items as ContentItem<ITestimonial>[]
return (
<section className="mx-auto max-w-5xl px-8 py-12" data-agility-component={contentID}>
<h2 className="text-3xl font-semibold dark:text-white">{fields.heading}</h2>
<ul className="mt-8 grid grid-cols-1 gap-6 md:grid-cols-3">
{items.map((t) => (
<li key={t.contentID} className="rounded-lg border p-6">
<blockquote>{t.fields.quote}</blockquote>
<div className="mt-4 flex items-center gap-3">
{t.fields.photo && (
<AgilityPic image={t.fields.photo} fallbackWidth={64} className="h-12 w-12 rounded-full" />
)}
<div>
<p className="font-medium">{t.fields.name}</p>
{t.fields.role && <p className="text-sm opacity-70">{t.fields.role}</p>}
</div>
</div>
</li>
))}
</ul>
</section>
)
}
<ContentZone> can find it. The registry's getModule matches name against the component model's reference name, case-insensitively:// components/agility-components/index.ts
import Testimonials from "./Testimonials"
const allModules = [
// ...existing components
{ name: "Testimonials", module: Testimonials },
]
save_content_items. They land in Staging.Treat the snippet above as the shape to expect, not code to paste. The exact field names depend on the model the agent actually created, which is exactly why the agent reads the model back and checks the real response.
Review the diff before you move on. The usual misses are a registry name that doesn't match the model's reference name, a direct SDK call that skips the lib/cms/ wrappers (and so has no cache tag), and a list call without an explicit take.
npm run dev. In development the starter always uses the preview API key, so your staged component and sample items render without publishing anything.app/api/preview route validates the preview key, turns on draft mode, and redirects to the page, which then renders with preview content. app/api/preview/exit turns draft mode off.npm run build and fix anything that fails. The build type-checks the project and prerenders every page in the sitemap, so a component that breaks on real content fails here instead of in production.POST https://your-site.com/api/revalidate. The starter's route acts on publish events only. It doesn't check a signature, so to make sure only Agility can call it, see Verifying Signed Webhooks.publish_content and publish_page once its checks pass. Publish tools aren't annotated as destructive; unpublish and delete are. Whether anyone is asked to confirm a publish depends on your client's settings and the level you chose.The starter's fetch-tag model is the simplest place to start. If you later want long-lived caches with publish-only invalidation, Caching with Next.js and Agility covers moving to Next.js Cache Components ("use cache") as an upgrade path.
One feature per session. Clear the context between features (/clear in Claude Code, a fresh chat in Cursor or Copilot). Each session starts from your codebase and AGENTS.md, nothing else.
Make the agent read before it writes. Asking it to list existing models first avoids near-duplicate models that editors then have to choose between.
Ground it in the docs. When the agent is unsure about an SDK method or an API parameter, the Knowledgebase MCP gives it the documented answer. Ask it to cite the article it used.
Pick a publishing level per content type. Everything the agent saves through save_content_items lands in Staging first. Decide per content type whether a person reviews and publishes, an approver signs off, or the agent publishes after its checks pass. If you like, start with review while you build confidence, then move repeatable work to autonomous publishing.
Keep the instruction files in step. The starter ships both AGENTS.md and .cursorrules. If you change a convention in one, change it in the other, or point one at the other.
Make your site readable by agents too. Once the site is live, see Making Your Agility-Powered Site Readable by AI for llms.txt, Markdown twins and structured data.