How to Add a CMS to Jekyll with Decap CMS (Formerly Netlify CMS)
Add a visual content editor to your Jekyll site using Decap CMS β set up the admin panel, configure collections, and let non-developers publish posts without touching code.
Jekyll is code-first β posts are Markdown files, publishing means a Git commit. Thatβs great for developers but a barrier for non-technical collaborators. Decap CMS (the open-source successor to Netlify CMS) adds a visual editing interface to any Jekyll site, backed by your Git repository. No database, no separate server.
What Is Decap CMS?
Decap CMS is an open-source, Git-based content management system. It adds an /admin page to your site with a visual editor. When a content editor saves a post, Decap CMS commits the Markdown file to your GitHub repository β the same way you would from the command line, but through a web interface.
Key points:
- Runs entirely in the browser β no server needed
- Saves content as Markdown files in your repository
- Authenticates via GitHub, GitLab, or Bitbucket OAuth
- Free and open-source (MIT licence)
- Works with GitHub Pages, Netlify, and Cloudflare Pages
Step 1: Create the Admin Folder
Create an admin/ folder in your Jekyll site root with two files:
admin/index.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="robots" content="noindex">
<title>Content Manager</title>
</head>
<body>
<script src="https://unpkg.com/decap-cms@^3.0.0/dist/decap-cms.js"></script>
</body>
</html>
admin/config.yml:
This is the main configuration file β it defines your content collections and fields.
backend:
name: github
repo: yourusername/your-repo-name
branch: main
media_folder: "assets/images/uploads"
public_folder: "/assets/images/uploads"
collections:
- name: "posts"
label: "Blog Posts"
folder: "_posts"
create: true
slug: "{{year}}-{{month}}-{{day}}-{{slug}}"
fields:
- { label: "Title", name: "title", widget: "string" }
- { label: "Description", name: "description", widget: "string" }
- { label: "Date", name: "date", widget: "datetime" }
- { label: "Cover Image", name: "image", widget: "image", required: false }
- { label: "Author", name: "author", widget: "string", required: false }
- { label: "Category", name: "category", widget: "string", required: false }
- label: "Tags"
name: "tags"
widget: "list"
field: { label: "Tag", name: "tag", widget: "string" }
- { label: "Featured", name: "featured", widget: "boolean", default: false }
- { label: "Body", name: "body", widget: "markdown" }
Step 2: Set Up Authentication
Decap CMS needs a way to authenticate with GitHub. Choose one:
Option A: Netlify Identity (Easiest, Netlify only)
If your site is on Netlify:
- Enable Netlify Identity in your siteβs Netlify dashboard β Identity β Enable
- Under Registration preferences, set to Invite only
- Under External providers, enable GitHub
- Add the Netlify Identity widget to your site:
In admin/index.html, add before </head>:
<script src="https://identity.netlify.com/v1/netlify-identity-widget.js"></script>
Also add to your _layouts/default.html before </body>:
<script>
if (window.netlifyIdentity) {
window.netlifyIdentity.on("init", user => {
if (!user) {
window.netlifyIdentity.on("login", () => {
document.location.href = "/admin/";
});
}
});
}
</script>
<script src="https://identity.netlify.com/v1/netlify-identity-widget.js"></script>
- Invite users in Netlify β Identity β Invite users
Option B: GitHub OAuth App (Works on any host)
- Go to GitHub β Settings β Developer settings β OAuth Apps β New OAuth App
- Set:
- Application name: Your Site CMS
- Homepage URL:
https://yourdomain.com - Authorization callback URL:
https://api.netlify.com/auth/done
- Copy the Client ID and Client Secret
- In Netlify Dashboard β Site settings β Access control β OAuth β Install provider β GitHub β paste Client ID and Secret
Update admin/config.yml:
backend:
name: github
repo: yourusername/your-repo-name
branch: main
base_url: https://api.netlify.com
auth_endpoint: auth
Step 3: Configure Your Collections
A collection maps to a folder of content files. Configure one for each content type you want editors to manage.
Posts Collection
- name: "posts"
label: "Blog Posts"
folder: "_posts"
create: true
slug: "{{year}}-{{month}}-{{day}}-{{slug}}"
preview_path: "blog/{{slug}}"
fields:
- { label: "Layout", name: "layout", widget: "hidden", default: "post" }
- { label: "Title", name: "title", widget: "string" }
- { label: "Description", name: "description", widget: "string", hint: "150-160 characters for SEO" }
- { label: "Publish Date", name: "date", widget: "datetime" }
- { label: "Cover Image", name: "image", widget: "image", required: false }
- label: "Category"
name: "category"
widget: "select"
options: ["Tutorial", "Blog", "Themes", "SEO", "Comparison"]
- label: "Tags"
name: "tags"
widget: "list"
- { label: "Featured Post", name: "featured", widget: "boolean", default: false }
- { label: "Body", name: "body", widget: "markdown" }
Pages Collection
- name: "pages"
label: "Pages"
files:
- label: "About"
name: "about"
file: "_pages/about.md"
fields:
- { label: "Title", name: "title", widget: "string" }
- { label: "Body", name: "body", widget: "markdown" }
- label: "Contact"
name: "contact"
file: "_pages/contact.md"
fields:
- { label: "Title", name: "title", widget: "string" }
- { label: "Body", name: "body", widget: "markdown" }
Themes Collection (for JekyllHub)
- name: "themes"
label: "Themes"
folder: "_themes"
create: true
slug: "{{slug}}"
fields:
- { label: "Title", name: "title", widget: "string" }
- { label: "Description", name: "description", widget: "text" }
- { label: "Demo URL", name: "demo_url", widget: "string" }
- { label: "GitHub URL", name: "github_url", widget: "string" }
- { label: "Stars", name: "stars", widget: "number" }
- label: "Price Type"
name: "price_type"
widget: "select"
options: ["free", "premium"]
- { label: "Card Image", name: "card_image", widget: "image" }
- { label: "Body", name: "body", widget: "markdown" }
Step 4: Add admin/ to Your Build
Make sure the admin/ folder is included in your Jekyll build. By default it is β Jekyll copies non-underscored folders to _site/.
If you have a .gitignore that excludes admin/, remove that exclusion.
Verify by running bundle exec jekyll build and checking that _site/admin/index.html exists.
Step 5: Exclude admin from Sitemap
You donβt want the admin panel showing up in your sitemap or search results:
# admin/index.html front matter (add front matter to the file)
---
sitemap: false
---
Or in _config.yml:
exclude:
- admin/config.yml
Using the CMS
Once set up, your editors visit https://yourdomain.com/admin/ and log in with GitHub. They see a dashboard with all your configured collections.
Creating a post: Click the collection, click New Post, fill in the fields, write in the Markdown editor, click Publish. Decap CMS commits the file to your repository, triggering a rebuild.
Editing existing content: Browse the collection, click a file, edit, save.
Media uploads: Images drag-and-drop into the Markdown editor and are saved to your assets/images/uploads/ folder.
Editorial Workflow (Optional)
Enable draft/review workflow so posts go through approval before publishing:
# admin/config.yml
publish_mode: editorial_workflow
This adds a Kanban-style board with Drafts, In Review, and Ready columns. Content stays as a branch until approved, then merges to main.
Alternatives to Decap CMS
| Tool | Price | Best for |
|---|---|---|
| Decap CMS | Free | Git-based, self-hosted |
| CloudCannon | $45+/month | Most polished Jekyll CMS |
| Forestry.io | Free (limited) | Simple visual editing |
| TinaCMS | Free (limited) | Real-time inline editing |
| Contentful | Free (limited) | API-first, enterprise |
For most Jekyll sites, Decap CMS hits the right balance of features and cost (free). CloudCannon is worth it if youβre managing content for clients or a team.
Browse Jekyll themes on JekyllHub β all themes work with Decap CMS as the admin panel.
Setting up your content model in Decap CMS
The most important configuration decision in Decap CMS is your content model β how you structure the fields available to editors when creating or editing content. A poorly designed content model creates friction for editors and leads to inconsistent post data; a well-designed one makes publishing fast and keeps your front matter fields consistently populated.
For a blog post, a practical base content model includes title (string), date (datetime), description (string, for SEO meta), tags (list), cover image (image), and body (markdown). Beyond this core, add fields specific to your siteβs needs: a featured boolean to surface selected posts on the homepage, an author string or relation if you have multiple authors, or a canonical_url string for posts republished from elsewhere.
Use Decapβs required: true property sparingly β only on fields that are genuinely required for the page to render correctly. Over-requiring fields creates publishing friction and causes editors to fill in placeholder values just to save a draft. It is better to handle missing optional fields gracefully in your templates with Liquid conditionals than to enforce completeness through the CMS.
The Git workflow under the hood
Understanding what Decap CMS actually does helps you maintain it correctly. Every save triggers a Git commit to your repository β either directly to the branch you configured (typically main or gh-pages) or to a draft branch in the GitHub PR workflow if you enabled the editorial workflow. Decap CMS is a Git client with a content-aware UI, not a database.
This Git-based architecture has practical consequences. Your content history is your Git log β you can see every change, who made it, and when, with full diff support. Rolling back a content mistake is a standard git revert. Migrating to a different CMS is a matter of exporting your Markdown files, which are already on disk in your repository. There is no proprietary format, no data lock-in, and no export button to find in a settings menu.
The trade-off is that Git operations add latency β saving a post involves a network request to the GitHub API, and changes appear on your live site only after a full Jekyll build completes. For most editorial workflows this is acceptable; a five-to-ten minute delay between saving and publishing is fine for blog content. For sites requiring real-time content updates, a database-backed CMS is more appropriate.
When to upgrade beyond Decap CMS
Decap CMS is the right tool for solo creators and small teams with straightforward content needs. Consider alternatives when your requirements exceed what Decap handles well.
CloudCannon is worth evaluating when you have non-technical clients or editors who need guided onboarding, visual page editing (drag-and-drop rather than Markdown), and the ability to manage content on multiple Jekyll sites from a single account. CloudCannonβs pricing reflects these capabilities β it is not free β but for an agency managing client sites it pays for itself quickly in reduced support time.
Contentful or Sanity are worth considering when your content is structured data consumed across multiple channels (website, mobile app, third-party integrations) rather than primarily long-form Markdown. These headless CMSs decouple your content from Jekyll entirely, building the site by fetching content from an API at build time. The setup is more complex, the architecture is more powerful, and the ongoing cost is higher β a fair trade for content-heavy multi-channel products but overkill for a blog or portfolio.
Decap CMS, set up correctly with the right content model and your preferred hosting, is a genuinely excellent tool. Its combination of zero cost, Git-based storage, and a capable Markdown editor makes it the default recommendation for any Jekyll site that needs a CMS without a database.
Customising the Decap CMS editorial interface
Decap CMSβs editorial interface is driven entirely by the config.yml file β every collection, field, widget, and editorial option is configured there, not in a GUI. This configuration-as-code approach means your CMS setup is version-controlled alongside your content, which is a significant maintenance advantage over CMSs that store their configuration in a database.
Field widgets are the building blocks of the editorial interface. The string widget renders a single-line text input; text renders a multi-line textarea; markdown renders the Markdown editor with toolbar; image renders a file picker connected to your media folder; select renders a dropdown from a predefined list of options; boolean renders a toggle switch; datetime renders a date-time picker. Each field can have required: true, default:, hint: (help text shown below the field), and pattern: (a regex for validation) properties.
The list widget allows repeated fields β a list of tags, a list of related links, or a list of team members each with a name, photo, and bio. Configure the list widget with a fields: array to define the shape of each list item. This is the most powerful Decap widget for structured content: a projects collection where each project has multiple team members, each with their own name and role, is straightforward to configure with nested list widgets.
Custom preview templates allow you to render a live preview of content as it would appear on your site. Without a custom preview, Decap renders a generic preview. With a preview template (a JavaScript function registered with CMS.registerPreviewTemplate), editors see a close approximation of the final rendered page as they type, which improves editorial confidence and reduces publish-check-edit cycles.
Managing media with Decap CMS
Decap CMS handles media uploads by committing image files directly to your Git repository β by default to a uploads/ or assets/images/ directory you specify in config.yml. Every image a content editor uploads becomes a file in your repository, browseable via the Decap media library and servable from your siteβs CDN or hosting platform.
This Git-based media storage has storage implications. Git repositories with large binary files become slow to clone and fetch. The practical limit for comfortable Git performance is around 1-5GB of binary content; beyond that, Git LFS (Large File Storage) is the standard solution. Configure your repository to use Git LFS for image formats (*.jpg, *.png, *.webp) before your media library grows large, rather than after, because migrating existing files to LFS is a more disruptive operation.
An alternative to Git-stored media is Cloudinary, Uploadcare, or another media service integrated via Decapβs external media library support. Instead of committing images to Git, the media widget uploads images to the external service and stores the resulting URL in the front matter field. The editorial experience is similar, but images are served from a professional media CDN with automatic resizing and format conversion β a meaningful performance benefit over serving images from Git-backed hosting.
Configure your media folder and public folder carefully in config.yml. The media_folder is the path where Decap writes uploaded files in your repository (e.g., assets/images/posts); the public_folder is the URL path used to reference those files in content (e.g., /assets/images/posts). These must match your Jekyll siteβs asset structure, or uploaded image links will resolve to 404s.