How Ghost Generates Pretty URLs and Manages URL Redirects
Ghost creates human-readable URLs by processing permalink templates through the UrlService pipeline, while enforcing canonical SSL and host redirects via Express middleware before requests reach the router layer.
Ghost's architecture decouples URL generation from request handling to ensure consistent permalink structures across posts, pages, and custom collections. The TryGhost/Ghost repository implements this through a specialized UrlGenerator that transforms template patterns into concrete paths, paired with a redirect middleware layer that canonicalizes protocols and hostnames.
Pretty URL Generation Architecture
Ghost builds every frontend URL from a permalink pattern that you configure (for example, /:year/:slug/ or /:primary_tag/:slug/). This process operates through three coordinated services that run during application startup.
Permalink Registration in Collection Routers
The permalink pattern originates in your route settings and flows into the CollectionRouter constructor. In ghost/core/core/frontend/services/routing/collection-router.js, the router stores the pattern for later registration:
// collection-router.js
this.permalinks = { value: object.permalink };
(source: lines 29-31)
Each collection router (handling posts, tags, authors, or custom filters) then registers itself with the central UrlService by calling onRouterAddedType. This method accepts the router identifier, resource filter, resource type, and the permalink template:
urlService.onRouterAddedType(
identifier, // router id
filter, // NQL filter that selects resources
resourceType, // e.g., 'posts'
permalink // the pattern from route settings
);
(source: ghost/core/core/server/services/url/url-service.js, lines 98-106)
URL Generation and In-Memory Storage
For every registered router, the UrlService instantiates a UrlGenerator (located in ghost/core/core/server/services/url/url-generator.js). This generator queries the Resources service to fetch raw database rows and replaces permalink tokens—such as :slug, :year, :month, or :primary_tag—with actual values from each resource.
The generated mappings are stored in the Urls service (ghost/core/core/server/services/url/urls.js), which maintains an in-memory lookup table connecting canonical URL strings to their underlying resources.
URL Resolution Methods
Controllers and themes interact with this system through two primary methods on the UrlService façade:
getUrlByResourceId(id, {absolute: true})– Returns the fully qualified URL (including protocol and domain) for a given post, page, or tag ID.getResource(url)– Resolves an incoming request path back to its underlying resource object.
Permalink Validation
Before any URLs are generated, the route-settings validator (ghost/core/core/server/services/route-settings/validate.js) checks that permalink strings start and end with a forward slash and contain only supported tokens. Invalid patterns raise validation errors during Ghost boot, preventing runtime URL generation failures.
URL Redirect Handling and Canonicalization
Ghost applies admin host and SSL redirects before the request reaches the router stack. This early enforcement prevents duplicate content issues and ensures secure access.
Middleware Architecture
The redirect logic lives in ghost/core/core/server/web/shared/middleware/url-redirects.js and mounts as Express middleware. It handles four specific canonicalization scenarios:
| Redirect Type | Trigger Condition | Implementation |
|---|---|---|
| Admin Host | The admin.url config differs from the request host |
301 redirect to configured admin URL |
| Admin SSL | Admin URL uses HTTPS but request arrived via HTTP | 301 redirect to HTTPS version of admin URL |
| Frontend SSL | Site URL uses HTTPS but request is HTTP | 301 redirect to HTTPS on the requested host |
| Trailing Slash | Request path lacks trailing slash (and isn't a static asset) | Path amended to include / |
SSL and Host Enforcement Logic
The middleware uses private helper functions to construct redirect targets. For admin redirects, ghost/core/core/server/web/shared/middleware/url-redirects.js implements:
_private.getAdminRedirectUrl = ({requestedHost, requestedUrl, queryParameters, secure}) => {
const adminUrl = urlUtils.urlFor('admin', true);
const adminHost = url.parse(adminUrl).host;
if (adminHost !== requestedHost) {
return _private.redirectUrl({redirectTo: adminUrl, pathname: requestedUrl, query: queryParameters});
}
if (urlUtils.isSSL(adminUrl) && !secure) {
return _private.redirectUrl({redirectTo: adminUrl, pathname: requestedUrl, query: queryParameters});
}
};
(source: lines 33-66)
For the frontend, the logic checks the site URL from urlUtils.urlFor('home', true):
_private.getFrontendRedirectUrl = ({requestedHost, requestedUrl, queryParameters, secure}) => {
const siteUrl = urlUtils.urlFor('home', true);
if (urlUtils.isSSL(siteUrl) && !secure) {
return _private.redirectUrl({redirectTo: `https://${requestedHost}`, pathname: requestedUrl, query: queryParameters});
}
};
The frontendSSLRedirect and adminSSLAndHostRedirect middleware functions mount early in the Express stack, guaranteeing that every request undergoes canonicalization before routing.
Practical Code Examples
Retrieve a Canonical Post URL
To generate the absolute URL for a specific post within Ghost core or custom applications:
const urlService = require('@tryghost/url-service');
const postId = '5f7a8c1e2b3c4d5e6f7a8b9c';
const absoluteUrl = urlService.getUrlByResourceId(postId, {absolute: true});
console.log(absoluteUrl); // → https://myblog.com/awesome-post/
(uses UrlService.getUrlByResourceId from ghost/core/core/server/services/url/url-service.js, lines 31-38)
Configure a Custom Collection Permalink
Define a custom permalink structure in your routes configuration:
# routes.yaml
collections:
/podcast/:
permalink: '/podcast/:slug/'
filter: 'tag:podcast'
During startup, Ghost creates a CollectionRouter that registers this pattern with the UrlService. Generated URLs will follow the pattern https://myblog.com/podcast/episode-one/.
Force Site-Wide SSL
Configuration in config.production.json triggers automatic HTTP to HTTPS redirects:
{
"url": "https://example.com",
"admin": {
"url": "https://admin.example.com"
}
}
Because urlUtils.isSSL(siteUrl) returns true, the frontendSSLRedirect middleware redirects any http://example.com/ request to https://example.com/ (see url-redirects.js, lines 78-86).
Implement a Custom Admin Host
When the admin URL differs from the frontend, requests to the default host redirect automatically:
{
"url": "https://example.com",
"admin": {
"url": "https://admin.example.com"
}
}
A request to http://example.com/ghost/ triggers the admin-host redirect logic, issuing a 301 to https://admin.example.com/ghost/ (see url-redirects.js, lines 45-53).
Summary
- Ghost generates pretty URLs through the
UrlServicepipeline, which processes permalink templates via theUrlGeneratorand stores results in theUrlsin-memory service. - Router registration occurs in
CollectionRouterconstructors, where each router callsurlService.onRouterAddedType()with its specific permalink pattern and resource filter. - URL lookup relies on
urlService.getUrlByResourceId()for generating canonical links andurlService.getResource()for resolving incoming requests. - Redirects execute early in the Express middleware stack via
url-redirects.js, handling SSL enforcement, admin host canonicalization, and trailing-slash normalization before routing begins. - Validation occurs at boot time in
ghost/core/core/server/services/route-settings/validate.js, ensuring permalink templates contain only supported tokens and proper formatting.
Frequently Asked Questions
How does Ghost determine the URL structure for custom post types?
Ghost uses the CollectionRouter to define URL patterns for different resource filters. When you create a custom collection in routes.yaml with a permalink property (such as /portfolio/:slug/), the router registers this pattern with the UrlService via onRouterAddedType(). The UrlGenerator then processes each resource matching the collection's filter, replacing tokens like :slug or :year with actual database values to construct the final URL.
What happens when I change my permalink settings in Ghost?
Changing permalink patterns requires restarting Ghost because the UrlGenerator builds URLs during the boot sequence. The route-settings validator (validate.js) checks the new syntax for supported tokens and proper slash delimiters. After restart, the UrlService regenerates the in-memory URL map, and the url-redirects middleware ensures old URLs still resolve or redirect appropriately if you've implemented custom redirect rules separately.
Why does Ghost redirect my HTTP requests to HTTPS automatically?
Ghost checks the url configuration value in your JSON config against the incoming request protocol. If urlUtils.isSSL(siteUrl) returns true (indicating your configured URL uses HTTPS) but the request arrives via HTTP, the getFrontendRedirectUrl() function in url-redirects.js triggers a 301 redirect to the HTTPS version of the same path. This prevents duplicate content issues and enforces secure connections site-wide without manual server configuration.
Where does Ghost validate that my permalink format is correct?
Validation occurs in ghost/core/core/server/services/route-settings/validate.js during the route settings loading phase. The validator ensures permalink strings begin and end with forward slashes and contain only whitelisted tokens (:slug, :year, :month, :day, :primary_tag, etc.). Invalid patterns raise synchronous errors that prevent Ghost from completing its boot sequence, ensuring no malformed URLs enter the generation pipeline.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →