Restructuring Ghost URLs with routes.yaml: Collections, Permalinks, and Redirects
Ghost's routes.yaml lets you reorganize front-end URLs through collections, taxonomies, and permalinks without touching the API. Learn the ordering rules, a worked /blog/ and /portfolio/ split, and the verification steps that keep old links alive.
03 Oct 2026, 13:16 UTC

Most Ghost sites start with the default permalink /{slug}/. That works until you need a /blog/ prefix, a separate /portfolio/ section for client work, or cleaner tag archives. Rewriting core code is not the answer; Ghost exposes a front-end routing layer in routes.yaml that lets you remap URLs, collections, and taxonomy prefixes declaratively. The catch is that the file is order-sensitive, every custom collection needs a matching theme template, and changing permalinks without redirects will break inbound links.
This article walks through a concrete migration: splitting a mixed blog into /blog/ and /portfolio/ sections, reprefixing tag archives, and verifying the result before and after a restart.
What routes.yaml actually controls
routes.yaml is a small YAML file with three top-level keys: routes, collections, and taxonomies. It governs front-end URL structure, collection indexes, sitemap output, and RSS feeds. It does not change the Content API or Admin API endpoints, so external integrations that fetch posts by ID or slug through the API keep working. Only the human-facing URLs and feed locations move.
You download and upload the file through Ghost Admin under Settings > Labs. The exact menu label and any plan-level gating have shifted between Ghost versions, so confirm the location for your deployment before planning a migration.
Collections are a first-match waterfall
Collections are evaluated top to bottom. Each post lands in the first collection whose filter matches. A collection with no filter acts as a catch-all. This ordering is the single most important design decision in the file.
Filters use Ghost's NQL syntax. For example:
filter: tag:portfolio
filter: featured:true
Because a post appears in exactly one collection, overlapping filters can silently pull posts out of your main blog index. If a post matches both a tag:portfolio filter and a later featured:true filter, it will only appear in the portfolio collection. Readers may not notice until traffic to the main index drops.
Permalinks inside a collection support tokens such as {slug}, {id}, {year}, {primary_tag}, and {primary_author}. Moving from the default /{slug}/ to /blog/{slug}/ is a configuration change, not a code change.
Worked example: /blog/ and /portfolio/
Suppose you want all general writing under /blog/, client work tagged portfolio under /portfolio/, and tag archives reprefixed from /tag/ to /topic/. Your routes.yaml would look like this:
collections:
/portfolio/:
permalink: /portfolio/{slug}/
template: portfolio
filter: tag:portfolio
/blog/:
permalink: /blog/{slug}/
template: blog
/:
permalink: /{slug}/
template: index
taxonomies:
tag:
url: /topic/{slug}/
template: tag
author:
url: /author/{slug}/
template: author
Three things must line up for this to render:
- The
portfolioandblogtemplates must exist in your active theme (for exampleportfolio.hbsandblog.hbs). Deployroutes.yamland theme changes together, or the route will fail. - The portfolio collection is listed first so tagged posts are captured before the catch-all.
- The final collection has no filter, so it catches everything else.
After uploading the file, Ghost requires a restart for the new routes to take effect. On a typical self-hosted install using systemd, run this on the server with sudo:
sudo systemctl restart ghost_example-com
Replace ghost_example-com with your actual service name. If you use Ghost CLI, ghost restart from the install directory achieves the same result. Expected check: the service returns to active (running) without boot errors.
Redirects, verification, and rollback
Changing permalinks without redirects breaks inbound links and search equity. Add old-to-new mappings in redirects.json, uploaded through the same Labs area:
[
{
"from": "/^/(.*)/",
"to": "/blog/$1/",
"permanent": true
}
]
Be aware that redirects.json has practical size limits, so plan large migrations in batches rather than one massive file.
Before touching production, rehearse on a staging install running the same Ghost version and theme. Locally, validate the YAML syntax to catch indentation errors that could prevent Ghost from booting:
python3 -c "import yaml; yaml.safe_load(open('routes.yaml'))"
Run this in your working directory; no special permissions are required. If the command exits silently, the syntax is valid. A yaml.scanner.ScannerError means the file needs fixing before upload.
After restarting, verify from your local machine:
curl -I https://example.com/blog/my-post/
curl -I https://example.com/portfolio/case-study/
curl -I https://example.com/old-post/
Expected checks: the first two return 200 OK. The third should return 301 Moved Permanently with a Location header pointing to the new /blog/ URL. A 404 usually means a missing template, a misordered collection, or a typo in the permalink token.
Also confirm that /sitemap.xml and each collection's RSS feed list posts under the new URLs. Search engines and feed readers need time to re-crawl, so expect a lag between the switch and full index recovery.
Limits to plan around
- One collection per post. Overlapping NQL filters silently reassign posts. Audit your tags before adding filters.
- Template coupling. A custom collection without a matching
.hbsfile will not render. - Redirect scale.
redirects.jsonis not a substitute for a CDN or server-level rewrite when you are moving thousands of URLs. - Version drift. Admin menu paths and feature gating for routes.yaml have changed across Ghost releases. Verify against your version's documentation rather than assuming the Labs menu is identical.
Actionable checklist
- Download the current
routes.yamlfrom Settings > Labs and keep it as a rollback reference. - Add or update theme templates for every custom collection.
- Validate YAML locally, then upload
routes.yamlandredirects.jsontogether. - Restart Ghost and run the
curlchecks above. - Monitor 404 logs and sitemap coverage for at least one full crawl cycle.
If a change goes wrong, re-upload the original routes.yaml and redirects.json, restart again, and the previous URL structure returns. Because the operation changes live routing state, always keep that backup file within reach until the new URLs are confirmed stable.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.