Back to Blog

Implementing a CMS for My Blog with Decap

How I added a lightweight CMS to my Next.js and MDX blog without changing the way the blog is deployed.

July 7, 2026
6 min read

With more time on my hands recently, I had the chance to make improvements to this blog and write new content again. That was great, but it also reminded me of one annoying part of running a custom blog: publishing a new article was more cumbersome than I wanted it to be.

The blog itself was already in a much better place than when I first built it. Posts were written in MDX, stored in the repository, and rendered by Next.js. That setup gave me a lot of control. I could write Markdown, use React components inside posts, keep everything versioned in Git, and deploy the site as a normal web app.

But the writing flow still felt too developer-heavy.

Every time I wanted to publish something, I had to create or edit an MDX file, make sure the frontmatter was correct, commit the change, and push it to the main branch. This is fine when I am already in coding mode, but it is not ideal when I simply want to write. A blog should make it easy to capture thoughts while they are still fresh.

So I started looking for a simple CMS solution.

What I Wanted

I did not want to rebuild the blog around a traditional CMS. I also did not want to add a database just for blog posts. The current architecture already worked well:

  • Posts live in content/posts
  • Images live under public/blog
  • Metadata is stored as frontmatter
  • Next.js reads the files and renders the blog
  • Git remains the source of truth

What I wanted was a nicer editing layer on top of that system.

The ideal CMS would let me open an admin page, create a post, fill out the title, date, tags, and body, then save the result back into the repository as an MDX file. It should not require me to operate a separate backend service or migrate my content to someone else's format.

That is how I ended up choosing Decap CMS.

Why Decap

Decap CMS is a Git-based CMS. Instead of storing content in its own database, it writes files back to a Git repository. That was the main reason it fit this blog so well.

My blog was already built around files, so Decap did not need to replace the content model. It only needed to understand where posts are stored and which fields each post should have.

In this blog, the Decap admin UI lives at:

Text
/admin

The CMS itself is loaded from public/admin/index.html. I use Decap's manual initialization mode so the configuration can live directly in that page instead of relying only on a separate config file.

The important part of the setup looks like this:

JavaScript
CMS.init({
  config: {
    local_backend: true,
    backend: {
      name: "github",
      repo: "daniel2231/personal-blog",
      branch: "main",
      base_url: window.location.origin,
      auth_endpoint: "api/auth",
    },
    publish_mode: "editorial_workflow",
    media_folder: "public/blog/uploads",
    public_folder: "/blog/uploads",
  },
});

This tells Decap to use GitHub as the backend, write changes to the main branch, and upload images to public/blog/uploads. The editorial_workflow option also gives me a better publishing flow than immediately committing everything as a finished post.

Mapping Decap to My Blog Posts

The next step was describing my blog post format to Decap.

Each article in this blog is an MDX file with frontmatter. A normal post has fields like title, excerpt, date, readTime, tags, and language. Decap needs to know about those fields so it can show the right form controls in the admin UI.

The collection configuration points Decap at the same folder my blog already uses:

Yaml
collections:
  - name: posts
    label: Blog Posts
    folder: content/posts
    create: true
    extension: mdx
    format: frontmatter
    slug: "{{slug}}"

From there, the fields are mapped one by one. Text inputs are used for the title and read time, a datetime widget is used for the publish date, a list widget is used for tags, and a select widget is used for the post language.

For example, this is how the language field is defined:

Yaml
- label: Language
  name: language
  widget: select
  options:
    - label: Korean
      value: ko
    - label: English
      value: en
  default: ko

This matters because I sometimes write in English and sometimes in Korean. Instead of remembering the exact frontmatter value every time, I can just choose the language from a dropdown.

The final field is the body:

Yaml
- label: Body
  name: body
  widget: markdown

That body becomes the MDX content below the frontmatter. The blog can keep rendering posts the same way it always did, because the output is still just an .mdx file in content/posts.

Authentication

One part that required a little extra work was authentication.

Decap needs permission to write to the GitHub repository. For that, I added a small OAuth flow inside the Next.js app. The /api/auth route redirects the user to GitHub's authorization page, and /api/auth/callback exchanges the returned code for an access token.

The callback then sends the token back to the Decap admin window using the message format Decap expects:

Text
authorization:github:success:{...}

This is not a full content backend. It does not store posts, users, sessions, or drafts in a database. It is just a bridge that lets Decap authenticate with GitHub from inside my own site.

I also added a separate layer of protection around the admin route. In production, the middleware protects /admin with Basic Auth:

Text
ADMIN_USERNAME
ADMIN_PASSWORD

So the admin page is not publicly open, and GitHub OAuth still controls whether the CMS can actually write to the repository.

Local Editing

For local development, Decap has a local backend mode. This is useful because I do not want to test every CMS change by going through the production GitHub flow.

The local workflow is:

Bash
npm run dev
npm run cms:local

Then I can open:

Text
http://localhost:3001/admin

In local mode, Decap can write directly to the local files. That makes it much easier to test the fields, create sample posts, and check whether the generated MDX matches what the blog expects.

What This Improved

Before this, writing a post meant opening the project, creating a file, copying frontmatter from another post, editing metadata manually, and pushing the result. Now I can use a browser-based editor while keeping the same file-based architecture.

The nice part is that the blog did not need to become more complicated. I still get:

  • MDX posts
  • Git history
  • Static-friendly content
  • Image uploads inside the repository
  • No separate database
  • No separate CMS hosting

Decap is basically an editing interface for the system I already had.

Final Thoughts

This was a good reminder that not every tooling improvement needs to be a big architecture change. Sometimes the best upgrade is adding a thin layer that removes friction from an existing workflow.

Although I'm not a developer anymore, this was a fun experience for me, reminding me of the time when I used to use code to solve technical problems.

Back to Blog