Skip to content

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.

Read https://v2.alineacms.com/start.md and help me add Alinea to my Next.js project

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.

  1. Check the project

    • Confirm this is a Next.js project that uses the App Router: next is listed in the dependencies of package.json and there is an app/ or src/app/ directory. Alinea's Next.js integration needs the App Router. If there is no project yet, ask the user whether to create one with npx create-next-app@latest.

    • Detect the package manager from the lockfile: package-lock.json (npm), pnpm-lock.yaml (pnpm), yarn.lock (yarn), bun.lock or bun.lockb (bun). Use it for every command in this guide. The examples use npm: replace npx alinea with pnpm alinea, yarn alinea or bun alinea, and npm run with pnpm, yarn or bun run.

    • Check that node --version is 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.ts or src/cms.tsx file 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.

  2. Install Alinea

    Install the package in the project root.

    Terminal
    npm install alinea@preview

    With other package managers: pnpm add alinea@preview, yarn add alinea@preview or bun add alinea@preview.

  3. Run alinea init

    Terminal
    npx alinea@preview init

    In a Next.js project alinea init creates:

    • cms.ts, in src/ if the project has a src directory: an example schema and workspace, baseUrl, handlerUrl: '/api/cms' and adminPath: '/admin'.

    • app/(alinea)/api/cms/route.ts, also under src/ when it exists: the API route the dashboard talks to. It is only created when next is listed in dependencies.

    • content/pages/welcome.json and content/media/: a first entry and the media folder.

    • .gitignore entries for /public/admin.html and /public/admin/, where alinea build writes 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 existing AGENTS.md once, or creates the file.

    • It prefixes the dev and build scripts in package.json with alinea dev -- and alinea build --.

    Read the generated files. The route imports @/cms: check that tsconfig.json maps @/* to the folder that holds cms.ts (create-next-app sets this up), otherwise fix the import.

  4. Wire up Next.js

    Wrap the existing Next.js config with withAlinea and keep all of its options. It serves the dashboard at /admin and keeps the generated content package out of the server bundle.

    next.config.ts
    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.js use const {withAlinea} = require('alinea/next') and module.exports = withAlinea(nextConfig).

    Check that the API route exists, and create it if alinea init did not:

    app/(alinea)/api/cms/route.ts
    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 = handler

    Check the scripts in package.json. Keep any flags the user had after next dev or next build.

    {
      "scripts": {
        "dev": "alinea dev -- next dev",
        "build": "alinea build -- next build"
      }
    }

    alinea build writes the dashboard to public/admin.html and public/admin/. Check that .gitignore lists /public/admin.html and /public/admin/ (alinea init adds them).

  5. Enable live previews

    Set preview: true in the createCMS options in cms.ts (alinea init leaves it commented out). Then render the preview component in the root layout, add only this line to the existing app/layout.tsx:

    app/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 widget prop to hide the small preview toolbar on the page. baseUrl.production in cms.ts is read from NEXT_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 as baseUrl.production.

  6. Start the dev server

    Start the dev script in the background and keep it running for the rest of this guide.

    Terminal
    npm run dev

    alinea dev compiles cms.ts, starts the local CMS and then starts next dev. Read the output:

    • MCP server: http://localhost:4500/mcp is 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/admin is 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.

  7. Connect to the Alinea MCP server

    While alinea dev runs 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 during alinea dev and only accepts requests from localhost.

    In Claude Code, add it with the url from the dev server output:

    Terminal
    claude mcp add --transport http alinea http://localhost:4500/mcp

    For other agents add an HTTP MCP server named alinea with 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_entry and upload_file. Call describe_schema first. 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.

  8. 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, contains lists 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.date and Field.number, see Fields.

    • Register every type in schema and list the types each root accepts in contains. Pages that must always exist, such as the homepage, can be seeded with Config.page in the children of a root.

    cms.ts
    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.json entry has type Page. If you remove or rename that type, delete the entry with delete_entry. Save cms.ts, check the dev server output for errors and call describe_schema again to confirm the new types.

  9. Create the first entries

    Create the starting content with the MCP tools: find the parent with find_entries, then call create_entry with the type, the parent and the field values. Rich text fields accept Markdown, link fields accept entry ids, and upload_file adds 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 under content/.

  10. Render the content

    Query content in server components with the cms instance: cms.find returns an array, cms.first returns the first match or null, cms.get returns one entry and throws when nothing matches. An entry's url is built from the paths of its parents, a post with path hello below a blog with path blog is at /blog/hello.

    app/blog/page.tsx
    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>
      )
    }
    app/blog/[slug]/page.tsx
    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.

  11. Verify and hand over

    Run a production build and fix every error it reports.

    Terminal
    npm run build

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