Redirects
Janus manages HTTP redirects as first-class, versioned data. Every change to a redirect is appended to a per-path history; the latest active entry is what gets served.
Data model
A redirect record is a plain Clojure map.
The spec is ::janus.entities.specs.redirect/record.
| Key | Required | Description |
|---|---|---|
|
yes |
Keyword identifying the publishing origin (e.g. |
|
yes |
The path being redirected.
Must start with |
|
yes |
Absolute or relative URI to redirect to (e.g. |
|
yes |
HTTP status code: |
|
yes |
|
|
no |
ID of the user who made the change.
|
|
no |
|
Storage
Redirects are stored in the $$redirects PState, partitioned by :path.
{String ; path
{Keyword ; origin (:* for global)
[APersistentMap]}} ; chronological history of versions
Each entry in the history array conforms to ::redirect/record.
Versioned history and deduplication
Every write appends a new entry to the history array.
If the incoming record is functionally identical to the latest entry (same :target, :type, :active) it is silently dropped — only :user-id and :updated-at are excluded from the comparison.
;; Submitting the same redirect twice produces one history entry
(foreign-append! depot {:path "/old" :origin :zuz :target (uri "/new")
:type 301 :active true})
(foreign-append! depot {:path "/old" :origin :zuz :target (uri "/new")
:type 301 :active true})
;; history count = 1
;; Changing :type produces a second entry
(foreign-append! depot {:path "/old" :origin :zuz :target (uri "/new")
:type 302 :active true})
;; history count = 2
Global redirects
Use the reserved origin keyword :* to create a redirect that applies to every origin.
;; Applies to all origins
{:path "/press" :origin :* :target "https://press.example.com"
:type 301 :active true}
Resolution order: an origin-specific redirect always wins over a global one. If both exist for the same path, the origin-specific entry is returned. If only the global one exists, it is returned. If neither is active, resolution falls through to the entity layer.
Resolution in path→entity
path→entity is the core routing query.
It resolves a (origin, path) pair through several layers in order:
| Step | PState | Outcome |
|---|---|---|
1. Origin redirect |
|
Latest active entry for |
2. Global redirect |
|
Latest active entry for |
3. Path → entity |
|
Resolves path to an entity ID. Falls through on miss. |
4. Entity fetch |
|
Fetches the full denormalised entity.
Adds |
5. Automatic topic page |
|
For |
6. Not found |
— |
Returns |
HTTP API
The HTTP API exists primarily to back the redirects management application, which gives power users direct control over redirects within the system.
Endpoints are scoped under /redirect and /redirect-history.
List all redirects for an origin
GET /redirect/:origin
Returns the latest version of every redirect stored for the given origin.
Uses the origin→redirects query topology (scans all paths).
List all redirects for an origin with full history
GET /redirect-history/:origin
Returns every redirect stored for the given origin, with the complete version history per path.
Each entry in the response is a [path, [v1, v2, …]] tuple where the versions are ordered chronologically.
Uses the origin→redirects-with-history query topology (scans all paths).
Get a single redirect
GET /redirect/:origin/*path
Direct PState lookup for [path origin].
Returns the full history array or a 404 anomaly if the path is unknown.
Create or update a redirect
POST /redirect/:origin
Content-Type: application/json
{
"path": "/old-path",
"target": "https://www.example.com/new-path",
"type": 301,
"active": true,
"user-id": "[email protected]"
}
:origin is taken from the URL; :updated-at is injected server-side.
The record is validated against ::redirect/record before storage.
Request body fields:
| Field | Required | Description |
|---|---|---|
|
yes |
URI path starting with |
|
yes |
Absolute or relative URL. |
|
yes |
|
|
yes |
|
|
no |
Identifier of the acting user (for audit trail). |
Query topologies
| Name | Signature | Description |
|---|---|---|
|
|
Resolves one or multiple paths to their latest redirect for the given origin. Hashes on path; efficient for bulk lookups. |
|
|
Returns all latest redirects for an origin across all paths. Full scan — suitable for management UI, not for hot paths. |
|
Like |
Seeding
Redirects can be bulk-loaded from Excel/CSV sources via the seed tooling.
Each row is validated against ::redirect/record before being appended to the depot.
Invalid rows are logged and skipped; valid rows are appended with :append-ack.
See seed/src/io/forward_publishing/janus/seed/chm/redirects.clj for the CHM-specific seed implementation.