Color mode

Rename

Reduces broken URLs after notes are renamed or moved by turning detected renames into permanent redirects on the existing Riebeckite redirect machinery.

日本語

Basic usage

ts
import { defineConfig } from "@riebeckite/core";
import { renamePlugin } from "@riebeckite/plugin-rename";
 
export default defineConfig({
  plugins: [
    renamePlugin({
      status: 308,
      onUnexpectedRemoval: "warning",
    }),
  ],
});

When a note moves, the plugin compares the current routes with the route lock saved during the previous build. If it can identify the same note, it records a redirect from the old permalink to the new one in manifest.redirects, which Core already consumes for redirect pages and deploy files.

How it works

text
current entries (published only)
        │
        ▼
buildRouteLock ──► current RouteLock
        │                    │
        │                    ▼
        └──────────► diffRoutes(previous, current)
                             │
              ┌──────────────┴───────────────┐
              ▼                              ▼
        rename redirects                diagnostics
        (merged + chain-collapsed)      (ambiguous / removed)
              │
              ▼
     manifest.redirects  ◄── never overwrites an existing key
              │
              ▼
     context.cache "routes.lock"

Detection precedence is strict and never fuzzy:

  1. Explicit identity — the entry's frontmatter id matches a lock route's id.
  2. Exact content hash — sha256(entry.html) matches exactly one new route.
  3. Otherwise the route is treated as removed.

Multiple matching candidates (or none) produce no redirect. Ambiguous matches emit a rename-ambiguous diagnostic; unmatched removals emit a diagnostic whose severity follows onUnexpectedRemoval.

State

The plugin never writes files directly. The only cross-build state is the route lock, stored through context.cache under the key routes.lock. The cache is created under the resolved plugin cache directory (.riebeckite/cache in the standard HonoX setup) and is gitignored, so the lock is machine-local and is regenerated from scratch when missing or corrupt.

Because the lock is machine-local, the first build on a fresh machine has no history and cannot detect a rename that happened before the lock existed.

Options

Option Type Default Description
enabled boolean true Enables rename detection
status 301 | 302 | 307 | 308 308 HTTP status for new rename redirects
onUnexpectedRemoval "info" | "warning" | "error" "warning" Severity when a route disappears without rename evidence

Rules and guarantees

  • Only entries passing isPublished are considered. Private or unpublished notes never enter the lock and never produce redirects.
  • Existing manifest.redirects keys are never overwritten, so Permalink's redirect_from always wins.
  • Permanent redirect chains are collapsed transitively (A → B, B → C becomes A → C).
  • The lock is deterministic: route keys and redirects are sorted, with no timestamps or randomness.
  • Redirects are replayed on every build, so the CLI build and the SSG build reproduce the same redirects.

Exports

Functions:

  • renamePlugin(options?) / rename(options?)
  • diffRoutes(previous, current, options?)
  • buildRouteLock(entries, isPublished)
  • collapseRedirects(rules)
  • applyRouteRedirects(manifest, rules)
  • parseRouteLock(value), emptyRouteLock(), hashContent(html)

Types:

  • RenameOptions
  • RouteLock, RouteLockRoute, RouteLockRedirect, RedirectRule
  • DiffRoutesOptions, DiffRoutesResult, RenameDiagnostic

Limitations

  • Git-based rename detection is not implemented. It would require filesystem or repository access, which plugin code intentionally avoids. Rename detection relies on frontmatter id and on exact content hashes only.
  • A rename in which both the id is absent and the HTML body changes cannot be detected and is reported as an unexpected removal.

See also

History

1 changesCollapseExpand
1 + <!-- Generated from packages/plugins/rename/README.md. Do not edit this page directly; edit the package README and run `pnpm docs:sync`. -->
2 +
3 + # Rename
4 +
5 + Reduces broken URLs after notes are renamed or moved by turning detected
6 + renames into permanent redirects on the existing Riebeckite redirect
7 + machinery.
8 +
9 + [日本語](./rename.md)
10 +
11 + ## Basic usage
12 +
13 + ```ts
14 + import { defineConfig } from "@riebeckite/core";
15 + import { renamePlugin } from "@riebeckite/plugin-rename";
16 +
17 + export default defineConfig({
18 + plugins: [
19 + renamePlugin({
20 + status: 308,
21 + onUnexpectedRemoval: "warning",
22 + }),
23 + ],
24 + });
25 + ```
26 +
27 + When a note moves, the plugin compares the current routes with the route lock
28 + saved during the previous build. If it can identify the same note, it records a
29 + redirect from the old permalink to the new one in `manifest.redirects`, which
30 + Core already consumes for redirect pages and deploy files.
31 +
32 + ## How it works
33 +
34 + ```text
35 + current entries (published only)
36 + │
37 + ▼
38 + buildRouteLock ──► current RouteLock
39 + │ │
40 + │ ▼
41 + └──────────► diffRoutes(previous, current)
42 + │
43 + ┌──────────────┴───────────────┐
44 + ▼ ▼
45 + rename redirects diagnostics
46 + (merged + chain-collapsed) (ambiguous / removed)
47 + │
48 + ▼
49 + manifest.redirects ◄── never overwrites an existing key
50 + │
51 + ▼
52 + context.cache "routes.lock"
53 + ```
54 +
55 + Detection precedence is strict and never fuzzy:
56 +
57 + 1. **Explicit identity** — the entry's frontmatter `id` matches a lock route's
58 + `id`.
59 + 2. **Exact content hash** — `sha256(entry.html)` matches exactly one new route.
60 + 3. Otherwise the route is treated as removed.
61 +
62 + Multiple matching candidates (or none) produce no redirect. Ambiguous matches
63 + emit a `rename-ambiguous` diagnostic; unmatched removals emit a diagnostic whose
64 + severity follows `onUnexpectedRemoval`.
65 +
66 + ## State
67 +
68 + The plugin never writes files directly. The only cross-build state is the route
69 + lock, stored through `context.cache` under the key `routes.lock`. The cache is
70 + created under the resolved plugin cache directory (`.riebeckite/cache` in the
71 + standard HonoX setup) and is gitignored, so the lock is machine-local and is
72 + regenerated from scratch when missing or corrupt.
73 +
74 + Because the lock is machine-local, the first build on a fresh machine has no
75 + history and cannot detect a rename that happened before the lock existed.
76 +
77 + ## Options
78 +
79 + | Option | Type | Default | Description |
80 + | --- | --- | --- | --- |
81 + | `enabled` | `boolean` | `true` | Enables rename detection |
82 + | `status` | `301 \| 302 \| 307 \| 308` | `308` | HTTP status for new rename redirects |
83 + | `onUnexpectedRemoval` | `"info" \| "warning" \| "error"` | `"warning"` | Severity when a route disappears without rename evidence |
84 +
85 + ## Rules and guarantees
86 +
87 + - Only entries passing `isPublished` are considered. Private or unpublished
88 + notes never enter the lock and never produce redirects.
89 + - Existing `manifest.redirects` keys are never overwritten, so Permalink's
90 + `redirect_from` always wins.
91 + - Permanent redirect chains are collapsed transitively (`A → B`, `B → C`
92 + becomes `A → C`).
93 + - The lock is deterministic: route keys and redirects are sorted, with no
94 + timestamps or randomness.
95 + - Redirects are replayed on every build, so the CLI build and the SSG build
96 + reproduce the same redirects.
97 +
98 + ## Exports
99 +
100 + Functions:
101 +
102 + - `renamePlugin(options?)` / `rename(options?)`
103 + - `diffRoutes(previous, current, options?)`
104 + - `buildRouteLock(entries, isPublished)`
105 + - `collapseRedirects(rules)`
106 + - `applyRouteRedirects(manifest, rules)`
107 + - `parseRouteLock(value)`, `emptyRouteLock()`, `hashContent(html)`
108 +
109 + Types:
110 +
111 + - `RenameOptions`
112 + - `RouteLock`, `RouteLockRoute`, `RouteLockRedirect`, `RedirectRule`
113 + - `DiffRoutesOptions`, `DiffRoutesResult`, `RenameDiagnostic`
114 +
115 + ## Limitations
116 +
117 + - Git-based rename detection is not implemented. It would require filesystem or
118 + repository access, which plugin code intentionally avoids. Rename detection
119 + relies on frontmatter `id` and on exact content hashes only.
120 + - A rename in which both the `id` is absent and the HTML body changes cannot be
121 + detected and is reported as an unexpected removal.
122 +
123 + ## See also
124 +
125 + - [Permalink plugin](./permalink.en.md) — stable URLs and `redirect_from`
126 + - [Plugin guide](../reference/plugin-api.en.md)
127 +