Skip to content
New in 0.22: npm, pnpm, Yarn and Bun

Your CMS, inside your app

Define content in TypeScript. Get an admin, typed APIs and plugins, with no separate server to run.

npx create-easy-cms
The Easy CMS admin: editing a post

Everything a content team needs

Built in, not bolted on. No add-ons to buy, no second service to host.

Model your content

Fields and blocks

15 field types, groups, arrays and blocks for pages editors lay out themselves.

Rich text

A Tiptap editor with images from the media library, rendered to safe HTML.

Media library

Uploads with resized images, on disk or S3, Cloudflare R2 and MinIO.

Work as a team

Drafts and versions

Save drafts, keep the published page live, restore any earlier version.

Scheduling

Publish and unpublish at a set time, with a cron or the built-in timer.

Live preview

See unsaved changes on the real page, side by side with the form.

Ship it

Nuxt, Next.js or standalone

A Nuxt module, Next.js route handlers, or its own server for any frontend.

SQLite or Postgres

Start with a file, deploy on Postgres. Migrations are generated from the config.

Thai and English

The admin in both languages, and content in as many locales as you need.

Connect

API keys

Keys for scripts, other apps and AI assistants, limited to the collections they need.

Webhooks

Signed events on publish and change, retried for a day, to rebuild a static site.

Email

SMTP or your own adapter, queued and retried, for forms and your hooks.

From config to content in three steps

1

Describe your content

Collections and fields in one TypeScript file. Types are inferred from it.

ts
import { defineConfig } from '@easy-cms/core'
import { sqlite } from '@easy-cms/db-sqlite'

export default defineConfig({
  secret: process.env.EASY_CMS_SECRET,
  db: sqlite({ url: 'file:./cms.db' }),
  collections: [
    {
      slug: 'posts',
      drafts: true,
      fields: [
        { name: 'title', type: 'text', required: true },
        { name: 'slug', type: 'slug', from: 'title' },
        { name: 'body', type: 'richText' },
      ],
    },
  ],
})
2

Open the admin

At /admin, in your app. Editors get forms, drafts, history and preview.

Dashboard
3

Use it on your pages

Read typed documents with the Local API, or over REST from any frontend.

ts
export default defineEventHandler(async () => {
  const cms = await useEasyCMS()
  // Typed from the config: post.title is a string
  const { docs } = await cms.find('posts', { sort: '-createdAt' })
  return docs
})
tsx
import { getEasyCMS } from '@easy-cms/next'
import config from '@/easy-cms.config'

export default async function Home() {
  const cms = await getEasyCMS(config)
  const { docs } = await cms.find('posts', { sort: '-createdAt' })
  return docs.map((post) => <h2 key={post.id}>{post.title}</h2>)
}

Plugins

A plugin is a function over your config. It can add fields, REST endpoints and admin components.

SEO

@easy-cms/plugin-seo

Meta tags with length meters and a search preview, a sitemap, robots.txt, hreflang and structured data, and llms.txt for AI search.

Guide →

Form builder

@easy-cms/plugin-form-builder

Editors build forms in the admin. Submissions are checked, stored and emailed over SMTP, bots are kept out, and <easy-form> shows a form on any page.

Guide →

Redirects

@easy-cms/plugin-redirects

301s editors manage under Settings, and a redirect by itself when a published page gets a new slug.

Guide →

Nested pages

@easy-cms/plugin-nested-docs

Pages inside pages: a parent for each page, its full path and breadcrumbs kept right when a page above moves, and a tree in the admin.

Guide →

MCP

@easy-cms/plugin-mcp

Let AI assistants read and write your content through the Model Context Protocol, with an API key that limits what they can do.

Guide →
Write your own plugin →
The SEO plugin in the post editor
ts
import { seoPlugin } from '@easy-cms/plugin-seo'

plugins: [
  seoPlugin({
    collections: ['posts'],
    generateURL: ({ doc }) => `/posts/${doc.slug}`,
  }),
]
// Pages: seoMeta(post), sitemap(cms), robotsTxt(), llmsTxt(cms)
ts
import { formBuilderPlugin } from '@easy-cms/plugin-form-builder'
import { smtp } from '@easy-cms/email-smtp'

email: smtp({ from: 'My Site <no-reply@example.com>' }),
plugins: [formBuilderPlugin({ defaultTo: 'hello@example.com' })]
// Pages: <easy-form form="contact"></easy-form>
ts
import { redirectsPlugin } from '@easy-cms/plugin-redirects'

plugins: [
  redirectsPlugin({
    collections: ['posts'],
    url: ({ doc }) => `/posts/${doc.slug}`,
  }),
]
// Middleware: await resolveRedirect(cms, url)
ts
import { findByPath, nestedDocsPlugin } from '@easy-cms/plugin-nested-docs'

plugins: [nestedDocsPlugin({ collections: ['pages'] })]

// /about/team → the page, with its breadcrumbs
const page = await findByPath(cms, 'pages', '/about/team')
ts
import { mcpPlugin } from '@easy-cms/plugin-mcp'

apiKeys: true,
plugins: [mcpPlugin()]
// claude mcp add --transport http easy-cms https://example.com/api/cms/mcp \
//   --header "Authorization: Bearer ecms_…"

Why embedded?

SaaS headless CMSSeparate CMS serverEasy CMS
Where it runsTheir cloudA second app you hostInside your Nuxt or Next.js app
Content modelClicked together in a UICode or UITypeScript, in your repo and reviews
Your dataOn their serversYour databaseYour database: SQLite or Postgres
To deploy and pay forA subscription per seat or recordAnother server and databaseNothing extra
Types in your pagesGenerated from an APIGenerated or sharedInferred from the config

Try it in five minutes

Create a project, open /admin, write your first post.

npx create-easy-cms
Read the guide

Released under the MIT License.