redirect_from property you add to a content file, and how that turns into a live redirect through config/redirects.json.
This page is redirect-specific. For how
config/redirects.json fits into docs.json’s broader modular $ref split (navigation, redirects, and where each piece lives), see Configuring Mintlify. For retention windows and lifecycle decisions (when to keep 1:1 redirects, when to collapse to wildcard redirects, and when to retire content pages), see Content and redirect retention policy.Why redirect_from exists
redirect_from lives in the frontmatter of the file that now owns a piece of content, and lists every old URL that used to point to it.
language.
During the Mintlify migration, redirect_from declarations across the repo are what generated the redirects array in the first place - that’s why the two line up so closely throughout this repo’s history. That generation step is migration-time tooling, though, not an ongoing one. The array itself originally lived inline in docs.json. It now lives in config/redirects.json, which docs.json reaches via a $ref (see Configuring Mintlify).
Going forward,
config/redirects.json is the actual source of truth - redirect_from is informative only. Adding it to a file’s frontmatter does not by itself create a working redirect. To make a redirect happen, add the entry to config/redirects.json directly (see below).Still add redirect_from too - it costs little, documents the page’s history for whoever reads the frontmatter later, and means nothing needs backfilling if a script ever gets built to keep the two in sync. Just don’t rely on it alone.superoffice-docs (DocFx) repo, you’ll remember a second mechanism there: a redirect_url stub file left behind at the old path. This repo doesn’t use that.
Common scenarios
Every row below means: add the entry toconfig/redirects.json, and add the matching redirect_from to the content file too.
How this becomes a live redirect
config/redirects.json is what Mintlify’s hosting layer reads (via docs.json’s $ref):
"permanent": false for a temporary (307) one. Sources can’t include an anchor or query string. Destinations can include an anchor.
A trailing / on a source is equivalent to /index - /release-notes/8.5/pocket-crm/ and /release-notes/8.5/pocket-crm/index are the same rule. Don’t add both. It’s dead weight.
Never add a redirect whose
source is <page>/index (or <page>/) when the destination is <page> itself. Confirmed live via #242: Mintlify’s redirect matcher treats a trailing /index (and, per the equivalence above, a trailing /) as interchangeable with the bare parent path when evaluating sources - not only when serving ordinary requests. If the destination is that same bare path, the platform ends up applying the rule to itself, and the live page 308-redirects to itself forever. config/redirects.json looks completely fine in this case - there’s no textual loop for process-redirects.ps1’s own loop check to catch, since source and destination are literally different strings.This shape shows up specifically when a folder’s index.md gets flattened into a same-named file one level up (foo/index.md -> foo.mdx) and someone adds redirect_from: /section/foo/index to the new flat file, expecting the old folder URL to keep resolving. The resulting entry - "source": "/section/foo/index", "destination": "/section/foo" - is exactly the shape that loops. We hit this for real on 6 pages across all 6 languages (31 redirect entries) before it was caught.Confirmed empirically, not just theorized: removing the rule entirely does not make Mintlify fall back to serving the flat page at the old /index URL - it 404s instead. A 404 is still strictly better than an infinite loop, but it means this specific old URL can’t be preserved with a same-shape redirect. If it truly must resolve, point it somewhere that isn’t its own destination’s bare path (an anchor on the page, for example) instead of the flattened file’s root.This is a hosting-layer quirk, not a build-time one - mint dev won’t reproduce it. Any newly added /index-suffixed or trailing-slash source needs a live check against a real deployment (a PR preview is enough) before merging, not just a local mint dev spot-check.External destinations need .html
A redirect to our own site uses a plain path (/da/admin/import/create-spreadsheet). A redirect to the legacy help archive needs the full URL including .html to actually resolve:
Wildcards
A* in the source matches one or more path segments, letting one rule stand in for many literal entries. Two shapes, both used in our own config/redirects.json:
Many sources, one fixed destination - for content that got consolidated, not just moved:
*, for a clean folder rename where filenames carried over unchanged:
/release-notes/8.5/pocket-crm/9.0.4-update redirects to /release-notes/pocket-crm/9.0.4-update - same trailing segment, different folder.
Whether
* also matches a zero-length value (so a bare folder path matches too, not just paths with something after it) isn’t documented anywhere by Mintlify - it’s only inferred from :slug*’s resemblance to standard path-to-regexp convention, where * conventionally means “zero or more.” We’ve relied on this once (merging .../install and .../install-mac into one .../install* rule) as an accepted, deliberate risk, not a confirmed fact. If you’re relying on it too, spot-check the bare-path case in mint dev.When to compact a group into a wildcard
Multiple redirects sharing a destination are tempting to collapse, but a wildcard is a routing rule, not a label - get the scope wrong and it silently hijacks something it shouldn’t. Compact only when all of these hold:- The sources share a real literal prefix - not just the same destination. Two paths that happen to end up on the same page but look nothing alike can’t be expressed as a wildcard at all, no matter how many of them there are.
- Nothing else in the whole array shares that prefix but points elsewhere. Check the full file, not just the group you’re looking at - a prefix that looks like it belongs to one topic can quietly fan out to several unrelated destinations (we found a real one: a single folder-level prefix that, unchecked, would have fanned out to 9 different pages).
- The prefix doesn’t shadow a page that’s live today. Check the actual file tree, not just the redirects array. A wildcard matches on URL shape alone - if real content sits under that path, the rule redirects requests to it too.
-
If using the capture-and-reuse form, confirm the filename genuinely carried over unchanged. Don’t assume it from the folder move alone - verify against the real files. We found one case where a prefix got dropped during a move (
sofo-6.0-updatebecame6.0-update), which would have silently sent that one visitor to a page that doesn’t exist. - Check whether the content was actually merged into one page, or just relocated. If several old pages’ content now lives as sections on one consolidated page, a many-to-one collapse is correct. If each old page has its own matching new page, use the capture-and-reuse form instead - unless the group has any exceptions (see the warning below), in which case capture-and-reuse is unsafe and you’re choosing between leaving the group uncompacted or a full collapse. (We found a real bug of exactly this shape - a set of dated pages that should each redirect to their own matching page were all funneling to one index instead.)
A wildcard with even one exception sharing its prefix doesn’t just risk the wrong page winning - Mintlify silently drops the wildcard rule entirely. Confirmed directly (curl against a live
mint dev instance, not inferred): if any other redirect’s source shares a wildcard’s exact prefix, the wildcard never makes it into the compiled config. The literal exception still works. Every URL the wildcard was supposed to cover 404s instead. This isn’t a precedence question - the wildcard just isn’t there.This means: a majority-pattern group with a handful of exceptions (say, 90% of sources rename cleanly, 10% don’t) cannot be compacted with capture-and-reuse plus literal exceptions, no matter how tempting the high match rate looks. Your real options, in order of preference:- Check whether a chained redirect avoids the problem entirely first. The wildcard will mechanically send an exception’s old URL wherever its transform says, even though that’s the wrong final destination. If that mechanical (wrong) intermediate path has no real content and no existing entry, add one plain literal redirect there, forwarding to the exception’s true destination. The exception’s new source (the intermediate path) no longer shares the wildcard’s prefix, so the drop-bug never triggers - visitors get one extra hop, but land on the exact same correct page, with zero precision lost. Verify the intermediate path is genuinely unused (filesystem + array check) and re-run the circular-chain check afterward - this introduces a real 2-hop chain, not a loop, but confirm that, don’t assume it.
- Leave the whole group uncompacted (fallback, safest choice when chaining isn’t available - preserves every page’s precision).
- A full many-to-one collapse of the entire group (majority-fits entries and exceptions alike) to one fixed destination - safe (zero exceptions left, since nothing stays literal), but it sacrifices the precision of the entries that did have their own exact match. Only take this trade when it’s justified (see below) and chaining genuinely doesn’t apply - don’t reach for it just because the array is big.
When a full collapse is justified
A full collapse trades real precision for array health, so it needs a real reason, not just convenience:-
The content is demonstrably being phased out, evidenced by something already public on the destination page itself - a
<Warning>/<Danger>block, a page titled “(LEGACY)”, version-gated deprecation language. Never justify this by non-public/internal-only knowledge, even if you have it - if it isn’t visible on the page, it isn’t usable as justification here. -
The content moved as part of a folder-level site restructuring (for example, into
integrations/) - landing on the new folder’s index is an acceptable trade when the destination is genuinely the right general topic area for the whole group, not a broad, unrelated hub.
redirect_from list is wrong. A page can correctly document redirect_from: /old/specific/path (that genuinely was its old URL) even while config/redirects.json deliberately routes that same old path to a different, more generic destination for array-health reasons. Don’t edit a file’s redirect_from to “match” a full-collapse decision - it’s still telling the truth about the page’s own history.
Maintenance tooling
Two scripts intools/ support ongoing redirect hygiene - reusable, not one-off. Both take config/redirects.json as their positional argument:
-
tools/compact-redirects.py config/redirects.json---report [--min-size N] [--depth D]groupsredirectsby source-prefix depth and ranks candidate wildcard clusters by match % against a majority transform, flagging live-content collisions.--apply <prefix1,prefix2,...>mechanically compacts named, gate-clear clusters. It refuses (unless--force) to apply any cluster with exceptions, per the confirmed drop-bug above - review its report output yourself before choosing which clusters to apply, and how (leave uncompacted versus full collapse). -
tools/sort-redirects.py config/redirects.json- stable-sorts the array alphabetically bysource. Contributors should keep appending new redirects at the end (no need to insert in sorted position); run this periodically to restore order.--checkfor a dry-run/exit-code check,--diffto preview what would move without writing.
Verify
After editingconfig/redirects.json, spot-check in mint dev: visit the old URL and confirm it lands where you expect, including any edge case you’re relying on (a bare folder path, an external .html link, a capture-and-reuse filename). For a /index-suffixed or trailing-slash source specifically, mint dev isn’t enough - see the danger callout above. Check it against a live deployment (a PR preview) too.