# How Ghost Generates Pretty URLs and Manages URL Redirects

> Discover how Ghost generates pretty URLs using permalink templates and manages redirects with Express middleware, ensuring efficient SEO and user experience.

- Repository: [Ghost/Ghost](https://github.com/TryGhost/Ghost)
- Tags: internals
- Published: 2026-05-18

---

**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`](https://github.com/TryGhost/Ghost/blob/main/ghost/core/core/frontend/services/routing/collection-router.js), the router stores the pattern for later registration:

```javascript
// 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:

```javascript
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`](https://github.com/TryGhost/Ghost/blob/main/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`](https://github.com/TryGhost/Ghost/blob/main/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`](https://github.com/TryGhost/Ghost/blob/main/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`](https://github.com/TryGhost/Ghost/blob/main/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`](https://github.com/TryGhost/Ghost/blob/main/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`](https://github.com/TryGhost/Ghost/blob/main/ghost/core/core/server/web/shared/middleware/url-redirects.js) implements:

```javascript
_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)`:

```javascript
_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:

```javascript
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`](https://github.com/TryGhost/Ghost/blob/main/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:

```yaml

# 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`](https://github.com/TryGhost/Ghost/blob/main/config.production.json) triggers automatic HTTP to HTTPS redirects:

```json
{
  "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`](https://github.com/TryGhost/Ghost/blob/main/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:

```json
{
  "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`](https://github.com/TryGhost/Ghost/blob/main/url-redirects.js), lines 45-53).

## Summary

- **Ghost generates pretty URLs** through the `UrlService` pipeline, which processes permalink templates via the `UrlGenerator` and stores results in the `Urls` in-memory service.
- **Router registration** occurs in `CollectionRouter` constructors, where each router calls `urlService.onRouterAddedType()` with its specific permalink pattern and resource filter.
- **URL lookup** relies on `urlService.getUrlByResourceId()` for generating canonical links and `urlService.getResource()` for resolving incoming requests.
- **Redirects execute early** in the Express middleware stack via [`url-redirects.js`](https://github.com/TryGhost/Ghost/blob/main/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`](https://github.com/TryGhost/Ghost/blob/main/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`](https://github.com/TryGhost/Ghost/blob/main/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`](https://github.com/TryGhost/Ghost/blob/main/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`](https://github.com/TryGhost/Ghost/blob/main/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`](https://github.com/TryGhost/Ghost/blob/main/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.