Set up with an AI agent
Let a coding agent such as Claude Code, Cursor or Codex add Alinea to your Next.js project. Paste the prompt below into your agent: it reads this guide as Markdown from /start.md, sets up Alinea step by step, then helps you model your content and create the first entries.
Works with Claude Code, Cursor, Codex and more
The rest of this page is written for the agent. You are adding Alinea, a git-based headless CMS, to the user's Next.js project. Content is stored as JSON files in the repository and the schema is TypeScript in cms.ts. Follow the steps in order, run the commands yourself, check each result before moving on and ask the user whenever a step needs a decision. The complete documentation is available as one file at /llms-full.txt, fetch it when you need an API that is not covered here. Once Alinea is installed, node_modules/alinea/docs/ holds the docs of the installed version as a Markdown file per page, start at index.md.
Check the project
Confirm this is a Next.js project that uses the App Router:
nextis listed in thedependenciesofpackage.jsonand there is anapp/orsrc/app/directory. Alinea's Next.js integration needs the App Router. If there is no project yet, ask the user whether to create one withnpx create-next-app@latest.Detect the package manager from the lockfile:
package-lock.json(npm),pnpm-lock.yaml(pnpm),yarn.lock(yarn),bun.lockorbun.lockb(bun). Use it for every command in this guide. The examples use npm: replacenpx alineawithpnpm alinea,yarn alineaorbun alinea, andnpm runwithpnpm,yarnorbun run.Check that
node --versionis 24 or higher and that the project uses React 19, the Alinea CLI refuses to run otherwise.If a
cms.ts,cms.tsx,src/cms.tsorsrc/cms.tsxfile already exists, Alinea is already set up: check steps 4 and 5, then continue at step 6.Suggest committing any pending changes first, so the user can review what you change.
Install Alinea
Install the package in the project root.
npm install alinea@previewWith other package managers:
pnpm add alinea@preview,yarn add alinea@previeworbun add alinea@preview.Run alinea init
npx alinea@preview initIn a Next.js project
alinea initcreates:cms.ts, insrc/if the project has asrcdirectory: an example schema and workspace,baseUrl,handlerUrl: '/api/cms'andadminPath: '/admin'.app/(alinea)/api/cms/route.ts, also undersrc/when it exists: the API route the dashboard talks to. It is only created whennextis listed independencies.content/pages/welcome.jsonandcontent/media/: a first entry and the media folder..gitignoreentries for/public/admin.htmland/public/admin/, wherealinea buildwrites the dashboard.AGENTS.md: a short Alinea section that points coding agents to the bundled docs and the MCP server. It is added to an existingAGENTS.mdonce, or creates the file.It prefixes the
devandbuildscripts inpackage.jsonwithalinea dev --andalinea build --.
Read the generated files. The route imports
@/cms: check thattsconfig.jsonmaps@/*to the folder that holdscms.ts(create-next-app sets this up), otherwise fix the import.Wire up Next.js
Wrap the existing Next.js config with
withAlineaand keep all of its options. It serves the dashboard at/adminand keeps the generated content package out of the server bundle.import {withAlinea} from 'alinea/next' import type {NextConfig} from 'next' const nextConfig: NextConfig = { // The existing options } export default withAlinea(nextConfig)In a CommonJS
next.config.jsuseconst {withAlinea} = require('alinea/next')andmodule.exports = withAlinea(nextConfig).Check that the API route exists, and create it if
alinea initdid not:import {cms} from '@/cms' import {createHandler} from 'alinea/next' // This handler responds to the API requests of the dashboard const handler = createHandler({cms}) export const GET = handler export const POST = handlerCheck the scripts in
package.json. Keep any flags the user had afternext devornext build.{ "scripts": { "dev": "alinea dev -- next dev", "build": "alinea build -- next build" } }alinea buildwrites the dashboard topublic/admin.htmlandpublic/admin/. Check that.gitignorelists/public/admin.htmland/public/admin/(alinea initadds them).Enable live previews
Set
preview: truein thecreateCMSoptions incms.ts(alinea initleaves it commented out). Then render the preview component in the root layout, add only this line to the existingapp/layout.tsx:import {cms} from '@/cms' export default function RootLayout({children}: {children: React.ReactNode}) { return ( <html lang="en"> <body> {children} <cms.previews widget /> </body> </html> ) }The dashboard now shows the page next to the editor and updates it while the user types. Remove the
widgetprop to hide the small preview toolbar on the page.baseUrl.productionincms.tsis read fromNEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL, which Vercel sets during the build. Ask the user where the site is deployed: if it is not on Vercel, ask for its production URL and set it asbaseUrl.production.Start the dev server
Start the dev script in the background and keep it running for the rest of this guide.
npm run devalinea devcompilescms.ts, starts the local CMS and then startsnext dev. Read the output:MCP server: http://localhost:4500/mcpis the MCP server for the next step. The port moves to 4501 and up when 4500 is taken, always use the printed url.- Alinea CMS: http://localhost:3000/adminis the dashboard. Ask the user to open it and have a look around.
If the output shows an error in
cms.ts, fix it before continuing. The config is recompiled whenever you save it.Connect to the Alinea MCP server
While
alinea devruns it also serves a Model Context Protocol server that reads the schema and creates, edits and publishes entries through the same write path as the dashboard. It fills in internal fields such as ids, ordering keys and rich text nodes, so use it for all content changes instead of writing JSON files. It only runs duringalinea devand only accepts requests from localhost.In Claude Code, add it with the url from the dev server output:
claude mcp add --transport http alinea http://localhost:4500/mcpFor other agents add an HTTP MCP server named
alineawith the same url, for example in a project.mcp.json(Cursor reads.cursor/mcp.json):{ "mcpServers": { "alinea": { "type": "http", "url": "http://localhost:4500/mcp" } } }Most agents only load MCP servers when a session starts. If the tools do not show up, ask the user to restart or reload the session, and to point you back to /start.md to continue at this step.
The tools are
describe_schema,find_entries,get_entry,create_entry,update_entry,publish_entry,delete_entry,move_entryandupload_file. Calldescribe_schemafirst. The MCP server page describes every tool, its parameters and the value formats it accepts. If the MCP server cannot be connected, edit the content JSON files following Working with AI agents instead.Model the content
Ask the user what the site needs before you write any schema: which pages and sections, which content repeats (posts, products, team members, events), what is shared across pages (navigation, footer, settings) and whether the site is translated. Propose a schema in plain words and wait for confirmation.
Then define the types in
cms.ts, or in separate files imported there:Config.document(label, {fields, contains})for entries that have their own page. Title, path (the url slug) and SEO metadata fields are added automatically,containslists the types allowed as children.Config.type(label, {fields})for blocks in lists or rich text, and for entries without a page of their own such as site settings.Fields such as
Field.text,Field.richText,Field.image,Field.link,Field.entry,Field.list,Field.select,Field.check,Field.dateandField.number, see Fields.Register every type in
schemaand list the types each root accepts incontains. Pages that must always exist, such as the homepage, can be seeded withConfig.pagein thechildrenof a root.
import {Config, Field} from 'alinea' import {createCMS} from 'alinea/next' export const Page = Config.document('Page', { fields: { intro: Field.text('Intro', {multiline: true}), body: Field.richText('Body') } }) export const Blog = Config.document('Blog', { contains: ['BlogPost'], fields: { intro: Field.text('Intro', {multiline: true}) } }) export const BlogPost = Config.document('Blog post', { fields: { publishDate: Field.date('Publish date'), image: Field.image('Image'), body: Field.richText('Body') } }) export const cms = createCMS({ schema: {Page, Blog, BlogPost}, workspaces: { main: Config.workspace('Main', { source: 'content', mediaDir: 'public/media', roots: { pages: Config.root('Pages', { contains: ['Page', 'Blog'] }), media: Config.media() } }) }, baseUrl: { development: 'http://localhost:3000', production: process.env.NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL ?? 'http://localhost:3000' }, preview: true, handlerUrl: '/api/cms', adminPath: '/admin' })The example
content/pages/welcome.jsonentry has typePage. If you remove or rename that type, delete the entry withdelete_entry. Savecms.ts, check the dev server output for errors and calldescribe_schemaagain to confirm the new types.Create the first entries
Create the starting content with the MCP tools: find the parent with
find_entries, then callcreate_entrywith the type, the parent and the field values. Rich text fields accept Markdown, link fields accept entry ids, andupload_fileadds images to the media library and returns the id for image fields. Entries are published by default. Write real starting content with the user rather than placeholder text, then ask them to review it in the dashboard. Every entry is a JSON file undercontent/.Render the content
Query content in server components with the
cmsinstance:cms.findreturns an array,cms.firstreturns the first match or null,cms.getreturns one entry and throws when nothing matches. An entry's url is built from the paths of its parents, a post with pathhellobelow a blog with pathblogis at/blog/hello.import {Query} from 'alinea' import Link from 'next/link' import {BlogPost, cms} from '@/cms' export default async function BlogPage() { const posts = await cms.find({ type: BlogPost, select: {id: Query.id, title: Query.title, url: Query.url} }) return ( <ul> {posts.map(post => ( <li key={post.id}> <Link href={post.url}>{post.title}</Link> </li> ))} </ul> ) }import {Query} from 'alinea' import {RichText} from 'alinea/ui' import {notFound} from 'next/navigation' import {BlogPost, cms} from '@/cms' interface PostPageProps { params: Promise<{slug: string}> } export default async function PostPage({params}: PostPageProps) { const {slug} = await params const post = await cms.first({type: BlogPost, url: `/blog/${slug}`}) if (!post) notFound() return ( <article> <h1>{post.title}</h1> <RichText doc={post.body} /> </article> ) } export async function generateStaticParams() { const paths = await cms.find({type: BlogPost, select: Query.path}) return paths.map(slug => ({slug})) }Open the pages in the browser and in the dashboard preview. Filtering, ordering, relations and child queries are described in Querying content.
Verify and hand over
Run a production build and fix every error it reports.
npm run buildThen give the user a short summary: the files you changed, how to open the dashboard (
npm run dev, then http://localhost:3000/admin) and what to do next. Publishing from a deployed site needs a backend for login, drafts and git commits, see Deploying.Further reading:
Docs of the installed version:
node_modules/alinea/docs/index.md