@onderwijsin/directus-sluggernaut-bundle
v0.4.0
Published
Field-driven slug, permalink, and redirect tooling for Directus
Readme
@onderwijsin/directus-sluggernaut-bundle
Directus bundle for field-driven slugs, validated canonical paths, and optional redirect history. Sluggernaut keeps URL values stable as content changes while remaining independent of the frontend that serves them.
Purpose and bundle entries
| Entry | Type | Purpose |
| ------------------------- | -------------- | -------------------------------------------------------------------- |
| sluggernaut-slug | Interface | Derives and normalizes a slug from configured string source fields. |
| sluggernaut-permalink | Interface | Stores an absolute path, optionally derived from a Sluggernaut slug. |
| sluggernaut-link | Display | Displays, copies, and optionally opens a stored slug or path. |
| sluggernaut-hook | Hook | Derives fields and optionally maintains redirect lifecycle history. |
| sluggernaut-recalculate | Flow operation | Recalculates selected derived fields for an entire collection. |
When redirects are enabled, a canonical path change creates or rewrites an active managed 301.
Chains are flattened, and archive/delete transitions deactivate managed history. Explicit field
values and manually created redirects remain supported, but still pass server validation.
The package does not provide a redirect-serving endpoint, frontend router, SEO metadata, URL shortener, hosting, or role assignment. A separate application, endpoint, reverse proxy, or edge worker must serve records from the redirect collection.
Requirements and compatibility
- Directus
>=12.2.0 <13and Node.js>=24.10.0. - A trusted Directus runtime. The bundle is non-sandboxed and is not a general Marketplace package.
- A string source field for each generated slug and a string field for each Sluggernaut interface.
- A redirect collection when redirects are enabled and schema provisioning is disabled.
Installation
Install the published package in the Directus runtime and restart Directus:
pnpm add @onderwijsin/directus-sluggernaut-bundleThe package must be installed in the same runtime that loads API extensions. Installing it only in a Studio project or frontend application does not register the hook or operation.
Quick start
- Install the package and restart Directus.
- Create a collection, for example
articles, with a stringtitlefield. - Add a string field such as
slugand select theSluggernaut Sluginterface. - Set
Source fieldstotitleand keep the default options for a first setup. - Optionally add a string field such as
permalinkand selectSluggernaut Permalink. - Leave
Generate from slugenabled and selectslugas theSlug field. - Create an item with
title: "Hello World".
The server stores:
{
"title": "Hello World",
"slug": "hello-world",
"permalink": "/hello-world"
}The Studio inputs start locked. Unlock a field to edit it manually; server-side normalization and validation remain authoritative for every API, Flow, import, and Studio mutation.
Redirect consumer
Sluggernaut stores redirects; your application serves them. A minimal consumer should find an active record, honor its optional date window, and return the stored status code and destination:
const redirect = await directus.items('redirects').readByQuery({
filter: {
_and: [{ origin: { _eq: requestPath } }, { is_active: { _eq: true } }],
},
limit: 1,
})
// Apply start_date/end_date in the consumer's timezone policy, then:
// HTTP 301/302/307/308 + Location: redirect.destinationFor a direct API mutation, an exact manual redirect looks like this:
POST /items/redirects
Content-Type: application/json
{
"origin": "/old-news",
"destination": "/news/summer-news",
"type": 301,
"match": "exact",
"is_active": true
}Do not write Sluggernaut-owned provenance fields yourself. When the bundle provisions the collection, those fields are read-only and maintained by Sluggernaut.
Configuration
Set these variables in the Directus environment. Boolean and numeric values are parsed by the extension's configuration schema, so use the value formats supported by your Directus environment loader.
Sluggernaut settings
| Variable | Default | Description |
| -------------------------------------------------- | ----------: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| SLUGGERNAUT_ENABLED | true | Master switch for the hook and operation. When false, the operation returns zero counts and the hook registers no behavior. |
| SLUGGERNAUT_REDIRECTS_ENABLED | false | Enables redirect creation and archive/delete lifecycle handling. Slug and permalink derivation work independently of this setting. |
| SLUGGERNAUT_NORMALIZE_REDIRECTS | unset | Optional persistence policy: trailing-slash stores non-root paths with a trailing slash; no-trailing-slash stores them without one. Unset preserves the supplied trailing-slash form. Redirect validation always compares equivalent paths without a trailing slash. |
| SLUGGERNAUT_THROW_ON_PROCESSING_ERROR | true | Rejects source mutations when unexpected canonical or archive/unarchive redirect-processing failures occur. Set false to log and continue. |
| SLUGGERNAUT_REDIRECTS_COLLECTION | redirects | Collection used for managed redirects. Must be a valid Directus collection identifier. |
| SLUGGERNAUT_MAX_REDIRECT_GRAPH_DEPTH | 25 | Maximum exact-redirect graph expansion depth before a mutation is rejected. |
| SLUGGERNAUT_FIELDS_CACHE_TTL_MS | 60000 | Field metadata cache lifetime in milliseconds. Must be finite and greater than zero. |
| SLUGGERNAUT_SCHEMA_CHANGES_ENABLED | false | Allows Sluggernaut to create or reconcile its redirect collection schema at startup. |
| SLUGGERNAUT_SCHEMA_ABORT_ON_ERROR | true | Stops schema/policy startup processing when provisioning fails. |
| SLUGGERNAUT_MANAGE_REDIRECTS_POLICY_ENABLED | false | Enables the optional Can Manage Redirects policy definition. |
| SLUGGERNAUT_READ_ACTIVE_REDIRECTS_POLICY_ENABLED | false | Enables the optional Can Read Active Redirects policy definition. |
Schema changes and policy definitions are opt-in for this bundle. If schema changes remain disabled, create the configured redirect collection yourself before enabling redirects. If policy definitions are enabled, they are created but never assigned to a role.
Shared startup and lock settings
The hook also validates these shared settings. They matter when schema or policy setup runs in more than one Directus process.
| Variable | Default | Description |
| -------------------------------------------- | -------: | --------------------------------------------------------------------------------------------------------------- |
| DIRECTUS_EXTENSIONS_SCHEMA_CHANGES_ENABLED | true | Global master switch for extension-owned schema changes. Both this and the Sluggernaut switch must allow setup. |
| DIRECTUS_EXTENSIONS_DATA_SEED_ENABLED | true | Global switch for extension-owned policy/data seeding. |
| DIRECTUS_EXTENSIONS_LOCK_PROVIDER | unset | Startup lock provider: memory, redis, or fs. When unset, the synchronization store is used. |
| DIRECTUS_EXTENSIONS_LOCK_REDIS_URL | unset | Optional redis:// or rediss:// URL for the Redis lock provider. |
| DIRECTUS_EXTENSIONS_LOCK_FS_DIRECTORY | unset | Shared directory required when the lock provider is fs. |
| SYNCHRONIZATION_STORE | memory | Fallback startup coordination store: memory or redis. |
When using Redis, provide REDIS as a complete URL, or configure REDIS_ENABLED=true together with
REDIS_HOST, REDIS_PORT, REDIS_USERNAME, and REDIS_PASSWORD. A complete REDIS URL takes
precedence. For multiple Directus instances, use Redis or a shared filesystem lock.
Field metadata is cached for the configured TTL and invalidated when Directus fields are created, updated, or deleted. Use a shared cache backend such as Redis when multiple Directus instances must observe field changes consistently.
SLUGGERNAUT_ENABLED=true
SLUGGERNAUT_REDIRECTS_ENABLED=true
SLUGGERNAUT_SCHEMA_CHANGES_ENABLED=true
SLUGGERNAUT_MANAGE_REDIRECTS_POLICY_ENABLED=true
SLUGGERNAUT_READ_ACTIVE_REDIRECTS_POLICY_ENABLED=true
SLUGGERNAUT_REDIRECTS_COLLECTION=redirects
# Use shared coordination when more than one Directus process can start the extension.
DIRECTUS_EXTENSIONS_LOCK_PROVIDER=redis
REDIS=redis://redis:6379Slug interface
Add a string field and choose Sluggernaut Slug.
| Option | Default | Behavior |
| ------------------------------------- | ---------: | ---------------------------------------------------------------------------------------------------------- |
| sourceFields | required | One or more string fields from the same collection. Non-empty values are joined with spaces. |
| locale | en | Fixed locale choice used for case conversion. |
| lowercase | true | Lowercases the derived slug before separator normalization. |
| updateOnSourceChange | true | Re-derives the slug when a configured source field changes. |
| automaticRedirects | false | Allows this field to be selected as the canonical redirect source. |
| includeUnmanagedRedirectsInPlanning | true | Includes redirects not created by Sluggernaut in chain flattening, loop prevention, and conflict planning. |
| unmanagedRedirectConflictBehavior | override | On an included unmanaged conflict, either override it or block the canonical transition. |
The Studio locale option is a fixed dropdown. Supported values are nl, en, bg, de, es,
fr, pt, uk, vi, da, nb, it, and sv. Custom locale values are not offered by the
interface. The slug input uses a locale-specific generated-value placeholder when available.
For sourceFields: ["title", "category"], title: "Summer News", and category: "Sports":
summer-news-sportsNormalization trims empty input, removes combining marks, replaces runs of non-letter/non-digit
characters with -, collapses repeated separators, and removes leading/trailing separators. An
empty result is stored as null. Explicit slug values use the same normalization.
Permalink interface
Add a string field and choose Sluggernaut Permalink.
| Option | Default | Behavior |
| ------------------------------------- | ---------: | ---------------------------------------------------------------------------------------------------------- |
| generateFromSlug | true | Generates the path from a Sluggernaut slug field. Set false for an independent manual path. |
| slugField | unset | Sluggernaut slug field in the same collection. Required when generated. |
| updateOnSlugChange | false | Updates a generated permalink when its source slug changes. |
| prefix | unset | Optional path prefix such as /news. |
| validatePrefixOnManualInput | false | Rejects manual paths outside the configured prefix. |
| trailingSlash | false | Adds a trailing slash to generated non-root paths. |
| enforceTrailingSlashOnManualInput | false | Applies the trailing-slash policy to manual input. |
| automaticRedirects | false | Allows this field to be selected as the canonical redirect source. |
| includeUnmanagedRedirectsInPlanning | true | Includes redirects not created by Sluggernaut in chain flattening, loop prevention, and conflict planning. |
| unmanagedRedirectConflictBehavior | override | On an included unmanaged conflict, either override it or block the canonical transition. |
Example:
prefix: /news
slug: summer-news
generated permalink: /news/summer-newsPermalinks are paths, not full URLs. The server rejects schemes, hosts, protocol-relative paths,
query strings, fragments, whitespace, backslashes, control characters, and ./.. path segments.
Repeated slashes are normalized. Prefix and trailing-slash rules apply according to the options
above. When generateFromSlug is disabled, slug-derived options are hidden in the Directus field
editor; saved values are preserved if generation is enabled again later.
Mutation behavior
The hook handles items.create, items.update, and redirect-related delete/update actions:
- Slugs are derived before permalinks, so a permalink can use a slug created in the same mutation.
- Explicit values win for the mutation, then pass through server normalization.
- On updates, source values come from the payload when present and otherwise from the existing item.
- A permalink is unchanged when its slug changes unless
updateOnSlugChangeis enabled. - Bulk creates derive each item independently.
- An update requiring one existing item rejects ambiguous multi-item keys.
- Invalid interface configuration is logged as a warning and excluded; unrelated fields continue to work.
Invalid explicit values, missing source references, invalid paths, and ambiguous update keys fail at the mutation boundary. Validate imports and API payloads before retrying them.
Managed redirects
Redirects are disabled by default. To enable them:
- Set
SLUGGERNAUT_REDIRECTS_ENABLED=true. - Create the configured redirect collection, or enable both local and global schema changes.
- Set
automaticRedirects=trueon the canonical slug or permalink field.
Only one field supplies automatic redirects: the first valid permalink in Directus field order with automatic redirects enabled; otherwise the first valid slug. A later enabled field does not replace an earlier disabled permalink.
Sluggernaut creates managed 301 records with provenance:
| Field | Meaning |
| ----------------------------------------------------------------- | --------------------------------------------------------------- |
| origin | Previous canonical path. |
| destination | New canonical path. |
| type | 301 for managed records. |
| match | exact for automatically generated records. |
| specificity, matcher_signature | null for exact records; derived system metadata for patterns. |
| is_active | Whether your redirect consumer should serve the record. |
| start_date, end_date | Optional time window owned by the consumer. |
| managed_by | sluggernaut for managed records. |
| source_collection, source_item, source_field, source_type | Provenance for safe rewrites and lifecycle updates. |
| inactive_reason | archived or deleted for lifecycle deactivation. |
| user_created, date_created, user_updated, date_updated | Standard Directus audit fields. |
When Sluggernaut provisions this collection, the provenance and lifecycle fields (managed_by,
source_collection, source_item, source_field, source_type, inactive_reason, specificity,
and matcher_signature) are read-only and maintained by the bundle. The automatic history planner
currently includes exact-match records only. Redirect reads without an explicit sort default to
exact records first, then pattern specificity descending, then id ascending; an explicit sort is
preserved unchanged.
Canonical changes create or rewrite the latest redirect, flatten included redirect chains, and
deactivate included loops. By default, unmanaged redirects are included and the latest canonical
value overrides an unmanaged conflict. Set includeUnmanagedRedirectsInPlanning=false to ignore
unmanaged records, or set unmanagedRedirectConflictBehavior=block to preserve an included
unmanaged conflict and log a warning. Update-time redirect persistence is part of the item mutation
flow. Unexpected canonical or archive/unarchive processing failures are surfaced as
SLUGGERNAUT_REDIRECT_PROCESSING errors by default, so the source mutation does not silently
complete without its redirect side effect. Set SLUGGERNAUT_THROW_ON_PROCESSING_ERROR=false to
restore fail-open behavior; the failure is logged and the source mutation continues. Directus and
Sluggernaut validation/integrity errors remain propagated unchanged. Delete lifecycle failures are
logged after the source action.
Manual redirects
Manual redirects are created directly in the configured redirect collection and are not owned by Sluggernaut. They can be exact or pattern redirects. Manual writes still pass server-side normalization and integrity validation, including duplicate active exact origins, cycles, and pattern matcher conflicts.
Example exact manual redirect:
POST /items/redirects
Content-Type: application/json
{
"origin": "/old-news",
"destination": "/news/summer-news",
"type": 301,
"match": "exact",
"is_active": true
}Manual redirects are not rewritten by canonical history processing. A redirect consumer should
filter is_active=true, honor start_date/end_date, and return the stored type and
destination.
Pattern matching
Pattern redirects use match=pattern and are intended for manual redirect rules. Supported origin
syntax includes named parameters (:slug), optional parameters (:slug?), one wildcard (* or
*?), and simple parameter suffixes such as :name.pdf. Destinations may interpolate only captures
declared by the origin.
Pattern origins may contain at most 20 slash-separated segments. Sluggernaut removes non-root
trailing slashes and derives the read-only specificity and matcher_signature fields. Matching is
case-insensitive, so static segments and parameter suffixes are canonicalized to lowercase for
collision detection. Equivalent active patterns are rejected by their matcher signature, even when
parameter names, literal casing, or a non-root trailing slash differ. Query strings, fragments,
hosts, dot segments, and unsupported pattern syntax are rejected.
Example pattern redirect:
POST /items/redirects
Content-Type: application/json
{
"origin": "/legacy/:slug",
"destination": "/articles/:slug",
"type": 301,
"match": "pattern",
"is_active": true
}Link display
Select Sluggernaut Link as the display for a slug or permalink field. It shows the stored value
and provides copy. Configure host to enable Open:
host: https://www.example.com
stored value: /news/summer-news
opened URL: https://www.example.com/news/summer-newsThe host must be an http:// or https:// origin without credentials, a path, query, or fragment.
Invalid hosts disable Open; they do not invalidate the stored value.
Flow operation
Add Sluggernaut: Recalculate Fields to a Directus Flow:
{
"collection": "articles",
"fields": ["slug", "permalink"],
"createRedirects": true
}| Input | Required | Default | Description |
| ----------------- | -------- | -----------------: | ------------------------------------------------------------------------------------------------------------------ |
| collection | yes | — | Collection to scan. |
| fields | no | all derived fields | Exact slug/permalink field keys to recalculate. Unknown/non-derived keys are ignored. |
| createRedirects | no | true | Uses item-service updates when true and redirects are enabled; otherwise writes directly without redirect history. |
Existing flows using the previous fieldKeys option remain supported as a legacy alias; new flows
use fields.
The operation returns:
{
"processed": 125,
"updated": 119,
"skipped": 4,
"failed": 2
}Items are processed in pages of 100. Selecting only a slug does not implicitly recalculate a
dependent permalink. The operation requires administrator or internal system accountability and
rejects other callers with 403 Forbidden.
Permissions and security
The hook reads field and collection metadata with system accountability, but item updates read the existing item using the mutation's accountability. Recalculation is administrator-only because it can update every item in a collection.
Optional policies are:
Can Manage Redirects: create, read, update, and delete access to the configured redirect collection.Can Read Active Redirects: read access to active records within their optional date window. The field allowlist includesorigin,destination,type,match,specificity,start_date, andend_dateso consumers can serve the configured redirect status code.
Policies are definitions only. Assign them to roles yourself and review whether redirect provenance should be visible to each role.
Database constraints
Application-level validation cannot guarantee redirect integrity when concurrent requests, workers, imports, or other processes write to the same collection. If duplicate active redirect matches must be prevented in those scenarios, the invariant must also be enforced at database level.
Copy the
Sluggernaut redirect integrity migration template
into your project's Directus migrations/ directory and run it through the normal Directus
migration pipeline. Replace the template's redirects table name if
SLUGGERNAUT_REDIRECTS_COLLECTION uses a custom collection. The migration validates existing data
before creating the constraints and aborts when duplicates or incomplete pattern metadata require
repair.
Fresh installations
The migration targets an existing Directus collection. Directus runs custom migrations during bootstrap, before API extensions start, so do not make this migration available during the first bootstrap of a new project:
- Start Directus with
SLUGGERNAUT_SCHEMA_CHANGES_ENABLED=true, without the integrity migration in the activeMIGRATIONS_PATH. - Wait for Directus to start once so Sluggernaut can create the configured redirect collection.
- Add the integrity migration to the active
migrations/directory. - Restart Directus, or run
directus database migrate:latest, to apply the constraints.
If schema provisioning is disabled, apply the redirect collection schema first through your normal Directus schema/snapshot workflow. Existing installations whose redirect collection already exists can add the migration before their next migration run.
The template supports PostgreSQL, MySQL, and SQLite. It enforces active exact-origin uniqueness and active pattern-signature uniqueness. If another database provider is used, translate the migration to equivalent SQL supported by that provider before applying it. Sluggernaut does not run this migration during extension startup.
Direct items.create, single-item items.update, and multi-item items.update mutations to the
configured redirect collection validate exact redirects before persistence. Origins and internal
destinations are normalized, active exact redirects are checked for duplicate origins, self-loops,
and cycles, and only the relevant origin frontier is read. Pattern origins and destinations are
validated against the restricted grammar and receive derived matcher metadata. Bulk updates resolve
every target and validate the resulting mutation set before Directus writes. External structural
edits to a Sluggernaut-owned redirect clear its provenance; operational changes such as activation
dates preserve ownership. Redirect writes made by Sluggernaut's own history workflow preserve
provenance and local exact validation; the existing history planner remains authoritative for those
internal structural writes. Pattern redirects are always unmanaged/manual and do not participate in
exact graph validation. Direct redirect failures use Directus- compatible errors:
SLUGGERNAUT_VALIDATION with status 400 for invalid consumer input and SLUGGERNAUT_INTEGRITY
with status 409 for active redirect conflicts. An enabled but unavailable redirect collection
therefore rejects direct redirect mutations rather than silently skipping validation. Unexpected
configuration and internal failures use SLUGGERNAUT_CONFIGURATION or SLUGGERNAUT_INTERNAL with
status 500.
The bundle does not serve redirects. A web server, frontend, edge worker, or endpoint must query active records and issue the HTTP response.
The match value determines how an origin is interpreted: exact origins treat : and * as
literal path characters, while pattern origins reserve them for the documented pattern grammar.
Troubleshooting
Interfaces are missing
Confirm the package is installed in the Directus runtime, Directus was restarted, and the runtime
satisfies >=12.2.0 <13.
A permalink is ignored
Check that generateFromSlug references a Sluggernaut slug field in the same collection. Invalid
options and missing references are logged as warnings and excluded from derivation.
A changed title does not change the slug
Confirm the title is in sourceFields and updateOnSourceChange=true. An explicit slug payload
takes precedence for the mutation.
Redirects are not created
Check SLUGGERNAUT_REDIRECTS_ENABLED, the selected field's automaticRedirects, and the existence
of SLUGGERNAUT_REDIRECTS_COLLECTION. If schema changes are disabled, create the collection
manually or enable schema setup.
Startup provisioning does not run
Both SLUGGERNAUT_SCHEMA_CHANGES_ENABLED and DIRECTUS_EXTENSIONS_SCHEMA_CHANGES_ENABLED must be
true. Policy provisioning also requires DIRECTUS_EXTENSIONS_DATA_SEED_ENABLED=true and the
relevant policy flag.
Boundaries
- Directus runtime:
>=12.2.0 <13. - Node runtime:
>=24.10.0as declared by the package. - Non-sandboxed API hook and operation; deploy only in a trusted Directus runtime.
- Roles and policy assignments are never changed automatically.
- No redirect HTTP endpoint, router integration, frontend package, or external redirect service is installed.
Studio Docs
The bundle seeds the Sluggernaut articles from docs/slugs-and-permalinks.json and
docs/redirects.json when Studio Docs and data seeding are enabled. Set
SLUGGERNAUT_DOCS_SEED_ENABLED=false to opt out.
The bundled documentation is available in Dutch. To translate it, keep the seeding strategy on
versioning, start the extension with seeding enabled, edit and publish the translations on the
bundled articles, and reject incoming updates. Create fresh translations from the Dutch sources
whenever the documentation changes.
