Ghost's routes.yaml: Turning a Blog Engine into a Structured Headless CMS
Ghost's routes.yaml lets you define custom collections, taxonomies, and per-section feeds — turning a chronological blog engine into a structured headless CMS without plugins or build steps. Here's how to model case studies, docs, or podcasts with their own URLs and templates.
07 Sept 2026, 23:08 UTC

The problem: Ghost assumes you're building a blog
Out of the box, Ghost organizes everything chronologically. Posts flow into a single index, tagged content lives at /tag/slug/, and authors get /author/slug/. That works fine for a publication. It breaks down when you need a documentation site with versioned pages, a podcast with its own RSS feed, or a portfolio where case studies live at /work/project-name/ instead of /tag/case-study/project-name/.
Most teams reach for a separate CMS or a static-site generator at this point. But Ghost 3.0+ includes routes.yaml — a routing layer that decouples URL structure from content storage. You define collections, taxonomies, and channels in a single file, and Ghost serves them with the right templates and API endpoints. No build step, no plugin, no second system.
Collections: filter, order, and template independently
A collection is a named group of posts that shares a URL prefix, pagination, sort order, and Handlebars template. The default posts collection is just one example. You can create as many as you need.
Each collection accepts:
filter— Ghost's internal filter grammar (e.g.,tag:case-study,tag:blog+author:jane,page:true)order—published_at desc(default),title asc, or any indexed fieldpermalink— URL pattern like/work/{slug}/or/docs/v{version}/{slug}/template— the.hbsfile in your theme (e.g.,custom-case-studies.hbs)data— optional static context passed to the template
Because collections are independent, your blog index stays chronological while /work/ sorts case studies by client name and /docs/ orders by version number.
Taxonomies become first-class routes
Tags and authors aren't just filters anymore. In routes.yaml you can remap their URLs and assign dedicated templates:
taxonomies:
tag: /topics/{slug}/
author: /team/{slug}/
Now /topics/security/ renders with tag-security.hbs (falling back to tag.hbs then index.hbs) and receives a tag object in the data context. The same works for authors. This lets you build topic hubs or team pages without hardcoding slugs in your theme.
Channels: per-collection RSS, JSON Feed, and sitemaps
Attach a rss, json, or sitemap channel to any collection. A podcast collection at /episodes/ can emit /episodes/rss/ while the main blog keeps /rss/. The channel inherits the collection's filter and ordering, so subscribers get exactly what that section publishes.
collections:
episodes:
filter: 'tag:podcast'
permalink: /episodes/{slug}/
template: episode
channels:
rss: /episodes/rss/
json: /episodes/feed.json
Worked example: a case-studies section at /work/
Assume a Ghost 5.x instance with the default Casper theme. You want case studies filtered by the #case-study tag, served at /work/{slug}/, using a custom template.
- Create the template. In your theme folder, add
custom-case-studies.hbs. Start by copyingindex.hbsand adjusting the post-card partial or loop as needed. - Write routes.yaml. Place this at the root of your Ghost content folder (same level as
config.production.json):routes: collections: work: filter: 'tag:case-study' permalink: /work/{slug}/ template: custom-case-studies order: published_at desc data: section: "Case Studies" taxonomies: tag: /topics/{slug}/ channels: rss: /rss/ sitemap: /sitemap.xml - Validate and reload. Run
ghost doctor(requires CLI access) or upload the file via Admin → Labs → Routes. The UI will flag indentation errors before you restart. - Verify. Visit
/work/— you should see only posts tagged#case-studyrendered with your template. Check/ghost/api/content/posts/?filter=tag:case-study&limit=1to confirm the Content API returns the same filtered set.
If the template is missing, Ghost silently falls back to index.hbs. Add {{log "template: " @root.template}} in your custom template and watch the server logs to confirm resolution.
Trade-offs you'll hit
- YAML indentation is unforgiving. A two-space mistake breaks routing silently until restart. Always validate with
ghost doctoror the Labs UI before deploying. - Filter grammar is not SQL. You can combine tags and authors (
tag:news+author:john), but you cannot join on custom fields (e.g., "posts with tag X AND custom_field Y"). Workaround: create multiple collections or filter client-side. - Permalink changes break URLs. Changing
/work/{slug}/to/projects/{slug}/invalidates every existing link. Plan redirects in theredirectssection ofroutes.yamlor at your reverse proxy (nginx, Cloudflare). - No Admin API for routes. You cannot update
routes.yamlvia the REST API. Multi-instance deployments need shared storage or a deployment pipeline that writes the file and restarts Ghost.
Try it on a test instance today
Spin up a local Ghost (Docker: docker run -d -p 2368:2368 ghost:5), add a routes.yaml with one custom collection, and restart. Confirm the new URL serves your template and the Content API mirrors the filter. That ten-minute test tells you whether Ghost's routing layer replaces the separate CMS you were planning to introduce.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.