> ## Documentation Index
> Fetch the complete documentation index at: https://docs.superoffice.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Redirects and redirect_from

> What redirect_from is for, how it becomes a live redirect in config/redirects.json, and when it's safe to compact a group of redirects into a wildcard.

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`.

<Note>
  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][3]. 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][4].
</Note>

See [Content and redirect retention policy][4] 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.

```yaml theme={null}
redirect_from: /da/admin/import/learn/create-import-file
```

Use a plain string for one old path. Once a file has two or more, switch to array format:

```yaml theme={null}
redirect_from:
  - /da/admin/import/learn/create-import-file
  - /da/admin/import/learn/create-import-file-legacy
```

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][3]).

<Note>
  **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.
</Note>

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.

| Situation                                                                                             | Redirect entry                                                                                                                                                                        |
| ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Renamed or moved a file                                                                               | old path -> new file's path                                                                                                                                                           |
| Deleted a file, content lives elsewhere on this site                                                  | old path -> the page that now covers it                                                                                                                                               |
| Deleted a file, no replacement on this site                                                           | old path -> the most relevant external page (for example, the [EOL archive][1]) instead of leaving a dead link                                                                        |
| Merged file A into file B                                                                             | A's old path -> B. If the merged content got its own section, give that heading an explicit `<a id="...">` - anchor text varies by language, so never rely on the auto-generated one. |
| Restructured a whole folder, filenames unchanged                                                      | See [wildcards][2] below - this is the one case a single rule can replace many entries                                                                                                |
| First file in a new folder isn't `index`                                                              | `/old/section/index` -> the new file, so the folder's own URL still resolves                                                                                                          |
| A folder's `index.md` got flattened into a same-named file one level up (`foo/index.md` -> `foo.mdx`) | **Don't** add `/section/foo/index` -> `/section/foo` - see the danger callout below, this exact shape self-redirects live                                                             |

## How this becomes a live redirect

`config/redirects.json` is what Mintlify's hosting layer reads (via `docs.json`'s `$ref`):

```json theme={null}
{
  "source": "/da/admin/import/learn/create-import-file",
  "destination": "/da/admin/import/create-spreadsheet"
}
```

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.

<Danger>
  **Never add a redirect whose `source` is `<page>/index` (or `<page>/`) when the `destination` is `<page>` itself.** Confirmed live via [#242][5]: 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.
</Danger>

<h3 id="external-destinations-need-html">
  External destinations need `.html`
</h3>

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:

```json theme={null}
{
  "source": "/da/onsite/win-client/learn/backup-routines",
  "destination": "https://help.superoffice.com/docs/10.3/da/onsite/win-client/learn/backup-routines.html"
}
```

Easy to miss since every other destination in this file is a bare path.

<h2 id="wildcards">
  Wildcards
</h2>

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:

```json theme={null}
{
  "source": "/da/reports/learn/*",
  "destination": "/release-notes/eol/reports"
}
```

**Capture and reuse** - source and destination both end in `*`, for a clean folder rename where filenames carried over unchanged:

```json theme={null}
{
  "source": "/release-notes/8.5/pocket-crm/*",
  "destination": "/release-notes/pocket-crm/*"
}
```

`/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.

<Danger>
  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`.
</Danger>

## 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.

<Danger>
  **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.
</Danger>

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.

[1]: #external-destinations-need-html

[2]: #wildcards

[3]: ./configure-mintlify

[4]: ./retention-policy

[5]: https://github.com/SuperOfficeDocs/docs/issues/242
