Skip to main content
Whenever a page moves, gets renamed, is merged into another page, or is deleted outright, its old URL still needs to go somewhere. Get this wrong and either a real visitor lands on a 404, or - just as bad - a wildcard silently swallows a page that’s still supposed to be live. This page covers both sides: the 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.
See Content and redirect retention policy for default retention windows.

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.
Use a plain string for one old path. Once a file has two or more, switch to array format:
It’s the second-to-last frontmatter property, only followed by 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.
If you’ve worked in the old 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 to config/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):
Redirects default to permanent (308 - search engines transfer ranking, browsers cache it). Add "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:
Easy to miss since every other destination in this file is a bare path.

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:
Capture and reuse - source and destination both end in *, 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:
  1. 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.
  2. 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).
  3. 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.
  4. 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-update became 6.0-update), which would have silently sent that one visitor to a page that doesn’t exist.
  5. 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.)
Don’t compact when the sources don’t share a literal prefix at all - most “several old pages now land on one new page” groups are content consolidations with no structural relationship, and can’t be expressed as a wildcard regardless of how many sources point at that destination.
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.
Either way, this is fundamentally a “the needs of the many outweigh the needs of the few” call: a small number of visitors with an old bookmark lose exact precision so the array as a whole stays healthy and maintainable. State which justification applies in the commit message.
A full collapse is a live-routing decision, not a content-history one - it doesn’t mean the destination file’s own 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 in tools/ 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] groups redirects by 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 by source. Contributors should keep appending new redirects at the end (no need to insert in sorted position); run this periodically to restore order. --check for a dry-run/exit-code check, --diff to preview what would move without writing.
Neither is a commit hook - both are meant to be run by hand, periodically, as the array grows.

Verify

After editing config/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.