Alinea is an open source, Git-based headless CMS for Next.js. You define your content model in TypeScript, editors work in a dashboard that ships with your app, and every entry is stored as a JSON file in your repository.
Docs · Demo · Alinea Cloud · Changelog
- Content in git: every entry is a JSON file in your repository. Review, branch and roll back content like code.
- Typed, no codegen: query types come straight from your schema.
- No database server for content: content is indexed in SQLite and bundled with your site, with full text search in the dashboard.
- Live previews: add one component to your layout and pages preview drafts as editors type, rendered by your server components.
- Instant publishing: every publish is a git commit, and deployed sites pick up new content without waiting for a rebuild.
- An accessible dashboard: built on React Aria Components, with dark mode, entry history, roles and permissions, and per-field translations.
- Self-host or Cloud: run the backend on your own database, or let Alinea Cloud handle sign-in and publishing.
Alinea requires Node.js 24 or higher, React 19 and the Next.js App Router. In a Next.js project:
npm install alinea@preview
npx alinea@preview initalinea init creates cms.ts with your schema and settings, the API route
the dashboard talks to (app/(alinea)/api/cms/route.ts), a first entry in
content/pages, and rewrites your dev and build scripts to run through
Alinea. Then wrap your Next.js config:
// next.config.ts
import {withAlinea} from 'alinea/next'
export default withAlinea({
// Your Next.js options
})Start the dev server with npm run dev and open the dashboard at
http://localhost:3000/admin.
Types and fields are plain TypeScript in cms.ts. Every document gets a
title and a path.
// cms.ts
import {Config, Field} from 'alinea'
import {createCMS} from 'alinea/next'
export const BlogPost = Config.document('Blog post', {
fields: {
publishDate: Field.date('Publish date'),
cover: Field.image('Cover image'),
body: Field.richText('Body')
}
})
export const Blog = Config.document('Blog', {
contains: [BlogPost]
})
export const cms = createCMS({
schema: {Blog, BlogPost},
workspaces: {
main: Config.workspace('My site', {
source: 'content',
mediaDir: 'public/media',
roots: {
pages: Config.root('Pages', {contains: [Blog]}),
media: Config.media()
}
})
},
baseUrl: {
development: 'http://localhost:3000',
production: 'https://example.com'
},
handlerUrl: '/api/cms',
adminPath: '/admin'
})Query content in server components. Results are typed from your schema, and
select returns exactly the shape you ask for, including related entries.
// app/blog/page.tsx
import {Blog, BlogPost, cms} from '@/cms'
import {Query} from 'alinea'
export default async function BlogPage() {
const blog = await cms.get({
type: Blog,
select: {
title: Query.title,
posts: Query.children({
type: BlogPost,
select: {title: Query.title, url: Query.url, date: BlogPost.publishDate},
orderBy: {desc: BlogPost.publishDate}
})
}
})
return (
<main>
<h1>{blog.title}</h1>
{blog.posts.map(post => (
<a key={post.url} href={post.url}>
{post.title}
</a>
))}
</main>
)
}Queries → · Live previews → · Deploy →
The npm package includes the documentation of its version as Markdown in
node_modules/alinea/docs/ (start at index.md), and the website lists every
page in /llms.txt. alinea init adds a
marked section to your AGENTS.md that points agents to both, and registers an
MCP server in .mcp.json. While alinea dev runs, agents use it to read your
schema and create, edit and publish entries through the same save path as the
dashboard.
{
"mcpServers": {
"alinea": {"command": "npx", "args": ["alinea", "mcp"]}
}
}See the upgrade guide and the changelog.
Have a question or an idea? Found a bug? Read how to contribute.