@riebeckite/plugin-permalink 0.0.12 → 0.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +965 -965
- package/README_ja.md +985 -985
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,965 +1,965 @@
|
|
|
1
|
-
# @riebeckite/plugin-permalink
|
|
2
|
-
|
|
3
|
-
Stable, configurable public URLs (permalinks) for Riebeckite content.
|
|
4
|
-
|
|
5
|
-
Instead of coupling public URLs directly to the filesystem layout, this plugin can build URLs from frontmatter IDs, deterministic path-derived IDs, or custom resolvers. Legacy URLs can also be registered as redirects in the same configuration.
|
|
6
|
-
|
|
7
|
-
[日本語](./README_ja.md)
|
|
8
|
-
|
|
9
|
-
## Basic usage
|
|
10
|
-
|
|
11
|
-
```ts
|
|
12
|
-
import { defineConfig } from "@riebeckite/core";
|
|
13
|
-
import { permalink } from "@riebeckite/plugin-permalink";
|
|
14
|
-
|
|
15
|
-
export default defineConfig({
|
|
16
|
-
plugins: [
|
|
17
|
-
permalink({
|
|
18
|
-
frontmatter: "id",
|
|
19
|
-
id: {
|
|
20
|
-
strategy: "frontmatter-or-hash",
|
|
21
|
-
length: 12,
|
|
22
|
-
},
|
|
23
|
-
path: {
|
|
24
|
-
mode: "flat",
|
|
25
|
-
prefix: "/n",
|
|
26
|
-
trailingSlash: false,
|
|
27
|
-
},
|
|
28
|
-
redirects: {
|
|
29
|
-
frontmatter: "redirect_from",
|
|
30
|
-
status: 308,
|
|
31
|
-
},
|
|
32
|
-
}),
|
|
33
|
-
],
|
|
34
|
-
});
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
Given the following content:
|
|
38
|
-
|
|
39
|
-
```md
|
|
40
|
-
---
|
|
41
|
-
id: hello-world
|
|
42
|
-
---
|
|
43
|
-
|
|
44
|
-
# Hello
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
with `path.mode: "flat"` and `path.prefix: "/n"`, the public URL becomes:
|
|
48
|
-
|
|
49
|
-
```text
|
|
50
|
-
/n/hello-world
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
With the default `frontmatter-or-hash` strategy, content without a frontmatter ID receives a deterministic ID derived from its file path.
|
|
54
|
-
|
|
55
|
-
This means an existing Obsidian vault does not need an `id` field added to every file.
|
|
56
|
-
|
|
57
|
-
---
|
|
58
|
-
|
|
59
|
-
## How it works
|
|
60
|
-
|
|
61
|
-
`permalink()` uses the build-time `resolveContentLocations` extension point to resolve the public location of each content entry.
|
|
62
|
-
|
|
63
|
-
Conceptually:
|
|
64
|
-
|
|
65
|
-
```text
|
|
66
|
-
Content
|
|
67
|
-
│
|
|
68
|
-
├─ frontmatter
|
|
69
|
-
├─ slug
|
|
70
|
-
└─ source path
|
|
71
|
-
│
|
|
72
|
-
▼
|
|
73
|
-
@riebeckite/plugin-permalink
|
|
74
|
-
│
|
|
75
|
-
├─ resolve ID
|
|
76
|
-
├─ resolve path
|
|
77
|
-
├─ normalize
|
|
78
|
-
├─ validate
|
|
79
|
-
└─ redirects
|
|
80
|
-
│
|
|
81
|
-
▼
|
|
82
|
-
ContentPublicLocation
|
|
83
|
-
│
|
|
84
|
-
├─ permalink
|
|
85
|
-
├─ redirects
|
|
86
|
-
└─ metadata
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
Core treats the resolved `permalink` as the canonical public URL.
|
|
90
|
-
|
|
91
|
-
The same canonical URL is then used by Wikilinks, backlinks, search, the content graph, SEO canonical URLs, sitemaps, RSS / Atom / JSON Feed, and other consumers.
|
|
92
|
-
|
|
93
|
-
Without this plugin, Riebeckite still resolves every URL through the Core default public location resolver, `resolveDefaultContentLocation`, which maps `index` to `/` and every other entry to `/{slug}`. That default is a first-class Core policy, not a fallback.
|
|
94
|
-
|
|
95
|
-
---
|
|
96
|
-
|
|
97
|
-
# Frontmatter
|
|
98
|
-
|
|
99
|
-
## ID
|
|
100
|
-
|
|
101
|
-
By default, the plugin reads the `id` field:
|
|
102
|
-
|
|
103
|
-
```md
|
|
104
|
-
---
|
|
105
|
-
id: hello-world
|
|
106
|
-
---
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
The field name is configurable:
|
|
110
|
-
|
|
111
|
-
```ts
|
|
112
|
-
permalink({
|
|
113
|
-
frontmatter: "uid",
|
|
114
|
-
});
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
```md
|
|
118
|
-
---
|
|
119
|
-
uid: hello-world
|
|
120
|
-
---
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
`frontmatter` refers to a top-level frontmatter field.
|
|
124
|
-
|
|
125
|
-
## Permalink override
|
|
126
|
-
|
|
127
|
-
A specific entry can override its public URL entirely:
|
|
128
|
-
|
|
129
|
-
```md
|
|
130
|
-
---
|
|
131
|
-
id: about-page
|
|
132
|
-
permalink: /about
|
|
133
|
-
---
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
The explicit permalink takes precedence over normal ID and path resolution.
|
|
137
|
-
|
|
138
|
-
The content ID and its public location remain separate concepts:
|
|
139
|
-
|
|
140
|
-
```text
|
|
141
|
-
ID
|
|
142
|
-
about-page
|
|
143
|
-
|
|
144
|
-
Canonical URL
|
|
145
|
-
/about
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
The field name can also be changed:
|
|
149
|
-
|
|
150
|
-
```ts
|
|
151
|
-
permalink({
|
|
152
|
-
override: {
|
|
153
|
-
frontmatter: "url",
|
|
154
|
-
},
|
|
155
|
-
});
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
## Redirects
|
|
159
|
-
|
|
160
|
-
Legacy URLs can be declared in frontmatter:
|
|
161
|
-
|
|
162
|
-
```md
|
|
163
|
-
---
|
|
164
|
-
id: hello-world
|
|
165
|
-
redirect_from:
|
|
166
|
-
- /posts/hello
|
|
167
|
-
- /blog/2024/hello-world
|
|
168
|
-
---
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
If the canonical URL is:
|
|
172
|
-
|
|
173
|
-
```text
|
|
174
|
-
/n/hello-world
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
the redirects resolve to it:
|
|
178
|
-
|
|
179
|
-
```text
|
|
180
|
-
/posts/hello
|
|
181
|
-
↓ 308
|
|
182
|
-
/n/hello-world
|
|
183
|
-
|
|
184
|
-
/blog/2024/hello-world
|
|
185
|
-
↓ 308
|
|
186
|
-
/n/hello-world
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
Configure the field and status with:
|
|
190
|
-
|
|
191
|
-
```ts
|
|
192
|
-
permalink({
|
|
193
|
-
redirects: {
|
|
194
|
-
frontmatter: "redirect_from",
|
|
195
|
-
status: 308,
|
|
196
|
-
},
|
|
197
|
-
});
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
`redirect_from` accepts either a string or an array of strings.
|
|
201
|
-
|
|
202
|
-
---
|
|
203
|
-
|
|
204
|
-
# Options
|
|
205
|
-
|
|
206
|
-
| Option | Type | Default | Description |
|
|
207
|
-
| --- | --- | --- | --- |
|
|
208
|
-
| `frontmatter` | `string` | `"id"` | Top-level frontmatter field used as the ID |
|
|
209
|
-
| `id.strategy` | `"frontmatter" \| "hash" \| "frontmatter-or-hash"` | `"frontmatter-or-hash"` | How IDs are resolved |
|
|
210
|
-
| `id.length` | `number` | `12` | Length of path-derived hash IDs (6–43) |
|
|
211
|
-
| `path.mode` | `"flat" \| "preserve" \| "append"` | `"flat"` | How the resolved ID is placed into the public URL |
|
|
212
|
-
| `path.prefix` | `string` | `"/n"` | URL prefix. `"/"` or an empty string means no prefix |
|
|
213
|
-
| `path.trailingSlash` | `boolean` | `false` | Whether canonical URLs end with `/` |
|
|
214
|
-
| `index.collapse` | `boolean` | `true` | Whether the `index` segment is collapsed |
|
|
215
|
-
| `override.frontmatter` | `string` | `"permalink"` | Frontmatter field containing a complete permalink override |
|
|
216
|
-
| `redirects.frontmatter` | `string` | `"redirect_from"` | Frontmatter field containing legacy URLs |
|
|
217
|
-
| `redirects.status` | `301 \| 302 \| 307 \| 308` | `308` | Redirect HTTP status |
|
|
218
|
-
| `resolveId` | `(content) => string` | — | Fully customize ID resolution |
|
|
219
|
-
| `resolvePath` | `({ content, id }) => string` | — | Fully customize public path generation |
|
|
220
|
-
|
|
221
|
-
---
|
|
222
|
-
|
|
223
|
-
# ID strategies
|
|
224
|
-
|
|
225
|
-
## `frontmatter`
|
|
226
|
-
|
|
227
|
-
Uses the configured frontmatter field as the ID.
|
|
228
|
-
|
|
229
|
-
```ts
|
|
230
|
-
permalink({
|
|
231
|
-
frontmatter: "id",
|
|
232
|
-
id: {
|
|
233
|
-
strategy: "frontmatter",
|
|
234
|
-
},
|
|
235
|
-
});
|
|
236
|
-
```
|
|
237
|
-
|
|
238
|
-
Every content entry must provide the field. A missing ID causes a build error.
|
|
239
|
-
|
|
240
|
-
This is useful when URL identity should be explicitly controlled:
|
|
241
|
-
|
|
242
|
-
```md
|
|
243
|
-
---
|
|
244
|
-
id: article-123
|
|
245
|
-
---
|
|
246
|
-
```
|
|
247
|
-
|
|
248
|
-
With `flat` mode, renaming or moving the source file does not change the ID or public URL.
|
|
249
|
-
|
|
250
|
-
---
|
|
251
|
-
|
|
252
|
-
## `hash`
|
|
253
|
-
|
|
254
|
-
Ignores the frontmatter ID and derives the ID from the source file path.
|
|
255
|
-
|
|
256
|
-
```ts
|
|
257
|
-
permalink({
|
|
258
|
-
id: {
|
|
259
|
-
strategy: "hash",
|
|
260
|
-
length: 12,
|
|
261
|
-
},
|
|
262
|
-
});
|
|
263
|
-
```
|
|
264
|
-
|
|
265
|
-
The input path is normalized by:
|
|
266
|
-
|
|
267
|
-
- replacing `\` with `/`
|
|
268
|
-
- stripping leading `/`
|
|
269
|
-
- applying Unicode NFC normalization
|
|
270
|
-
|
|
271
|
-
The normalized path is hashed using SHA-256, encoded as base64url, and truncated to `id.length`.
|
|
272
|
-
|
|
273
|
-
```text
|
|
274
|
-
notes/flutter/riverpod.md
|
|
275
|
-
│
|
|
276
|
-
▼
|
|
277
|
-
normalized path
|
|
278
|
-
│
|
|
279
|
-
▼
|
|
280
|
-
SHA-256
|
|
281
|
-
│
|
|
282
|
-
▼
|
|
283
|
-
base64url
|
|
284
|
-
│
|
|
285
|
-
▼
|
|
286
|
-
K7m3Qp8d...
|
|
287
|
-
```
|
|
288
|
-
|
|
289
|
-
The same path produces the same ID across operating systems and build environments.
|
|
290
|
-
|
|
291
|
-
Because the source path is the input, renaming or moving the file changes the ID.
|
|
292
|
-
|
|
293
|
-
---
|
|
294
|
-
|
|
295
|
-
## `frontmatter-or-hash`
|
|
296
|
-
|
|
297
|
-
This is the default strategy.
|
|
298
|
-
|
|
299
|
-
```ts
|
|
300
|
-
permalink({
|
|
301
|
-
frontmatter: "id",
|
|
302
|
-
id: {
|
|
303
|
-
strategy: "frontmatter-or-hash",
|
|
304
|
-
},
|
|
305
|
-
});
|
|
306
|
-
```
|
|
307
|
-
|
|
308
|
-
Resolution works as follows:
|
|
309
|
-
|
|
310
|
-
```text
|
|
311
|
-
frontmatter ID exists
|
|
312
|
-
↓
|
|
313
|
-
use frontmatter ID
|
|
314
|
-
|
|
315
|
-
frontmatter ID missing
|
|
316
|
-
↓
|
|
317
|
-
derive ID from path
|
|
318
|
-
```
|
|
319
|
-
|
|
320
|
-
This is useful for existing Obsidian vaults: content works without modification, while important entries can opt into explicit stable IDs.
|
|
321
|
-
|
|
322
|
-
---
|
|
323
|
-
|
|
324
|
-
# ID metadata
|
|
325
|
-
|
|
326
|
-
The source of a resolved ID is exposed through metadata.
|
|
327
|
-
|
|
328
|
-
| `metadata.idSource` | Meaning |
|
|
329
|
-
| --- | --- |
|
|
330
|
-
| `frontmatter` | Taken from the configured frontmatter field |
|
|
331
|
-
| `derived` | Derived from the source path hash |
|
|
332
|
-
| `custom` | Returned by `resolveId` |
|
|
333
|
-
|
|
334
|
-
A manual `permalink` override changes the URL without removing identity. When the frontmatter ID is present it is still recorded (`metadata.idSource` is `frontmatter`); metadata is omitted only when no explicit ID exists. Overrides never produce an implicit `/{id}` URL.
|
|
335
|
-
|
|
336
|
-
---
|
|
337
|
-
|
|
338
|
-
# URL path modes
|
|
339
|
-
|
|
340
|
-
ID resolution and URL composition are separate concerns.
|
|
341
|
-
|
|
342
|
-
Assume:
|
|
343
|
-
|
|
344
|
-
```text
|
|
345
|
-
Source:
|
|
346
|
-
notes/flutter/hello.md
|
|
347
|
-
|
|
348
|
-
ID:
|
|
349
|
-
hello-world
|
|
350
|
-
```
|
|
351
|
-
|
|
352
|
-
## `flat`
|
|
353
|
-
|
|
354
|
-
```ts
|
|
355
|
-
path: {
|
|
356
|
-
mode: "flat",
|
|
357
|
-
prefix: "/n",
|
|
358
|
-
}
|
|
359
|
-
```
|
|
360
|
-
|
|
361
|
-
Result:
|
|
362
|
-
|
|
363
|
-
```text
|
|
364
|
-
/n/hello-world
|
|
365
|
-
```
|
|
366
|
-
|
|
367
|
-
The filesystem directory structure is not exposed in the public URL.
|
|
368
|
-
|
|
369
|
-
This mode is useful for opaque, location-independent URLs.
|
|
370
|
-
|
|
371
|
-
---
|
|
372
|
-
|
|
373
|
-
## `preserve`
|
|
374
|
-
|
|
375
|
-
Preserves the source directory structure while placing the ID into the resulting URL.
|
|
376
|
-
|
|
377
|
-
```ts
|
|
378
|
-
path: {
|
|
379
|
-
mode: "preserve",
|
|
380
|
-
prefix: "/n",
|
|
381
|
-
}
|
|
382
|
-
```
|
|
383
|
-
|
|
384
|
-
Example:
|
|
385
|
-
|
|
386
|
-
```text
|
|
387
|
-
notes/flutter/hello.md
|
|
388
|
-
|
|
389
|
-
↓
|
|
390
|
-
|
|
391
|
-
/n/notes/flutter/hello-world
|
|
392
|
-
```
|
|
393
|
-
|
|
394
|
-
---
|
|
395
|
-
|
|
396
|
-
## `append`
|
|
397
|
-
|
|
398
|
-
`append` is also available:
|
|
399
|
-
|
|
400
|
-
```ts
|
|
401
|
-
path: {
|
|
402
|
-
mode: "append",
|
|
403
|
-
}
|
|
404
|
-
```
|
|
405
|
-
|
|
406
|
-
The current implementation returns the same path as `preserve`.
|
|
407
|
-
|
|
408
|
-
The mode exists separately so it can represent different composition semantics in the future. Do not rely on `append` having behavior distinct from `preserve` in the current version.
|
|
409
|
-
|
|
410
|
-
---
|
|
411
|
-
|
|
412
|
-
# Index files
|
|
413
|
-
|
|
414
|
-
`index.collapse` controls how `index.md` is represented.
|
|
415
|
-
|
|
416
|
-
```ts
|
|
417
|
-
index: {
|
|
418
|
-
collapse: true,
|
|
419
|
-
}
|
|
420
|
-
```
|
|
421
|
-
|
|
422
|
-
For:
|
|
423
|
-
|
|
424
|
-
```text
|
|
425
|
-
notes/flutter/index.md
|
|
426
|
-
```
|
|
427
|
-
|
|
428
|
-
with ID `hello-world`:
|
|
429
|
-
|
|
430
|
-
| Configuration | URL |
|
|
431
|
-
| --- | --- |
|
|
432
|
-
| `flat` | `/n/hello-world` |
|
|
433
|
-
| `preserve` + collapse | `/n/notes/flutter/hello-world` |
|
|
434
|
-
| `preserve` + no collapse | `/n/notes/flutter/index/hello-world` |
|
|
435
|
-
|
|
436
|
-
`flat` does not use the filesystem directory structure, so index collapsing does not affect it.
|
|
437
|
-
|
|
438
|
-
---
|
|
439
|
-
|
|
440
|
-
# Advanced: Custom resolvers
|
|
441
|
-
|
|
442
|
-
For URL schemes that cannot be expressed with the built-in strategies, `resolveId` and `resolvePath` provide escape hatches.
|
|
443
|
-
|
|
444
|
-
Prefer the built-in options when they are sufficient. Custom resolvers are intended for project-specific identity and URL schemes.
|
|
445
|
-
|
|
446
|
-
## `resolveId`
|
|
447
|
-
|
|
448
|
-
`resolveId` lets you compute the content ID yourself.
|
|
449
|
-
|
|
450
|
-
```ts
|
|
451
|
-
permalink({
|
|
452
|
-
resolveId(content) {
|
|
453
|
-
return `post-${content.slug}`;
|
|
454
|
-
},
|
|
455
|
-
|
|
456
|
-
path: {
|
|
457
|
-
mode: "flat",
|
|
458
|
-
prefix: "/articles",
|
|
459
|
-
},
|
|
460
|
-
});
|
|
461
|
-
```
|
|
462
|
-
|
|
463
|
-
Conceptually:
|
|
464
|
-
|
|
465
|
-
```text
|
|
466
|
-
Content
|
|
467
|
-
↓
|
|
468
|
-
resolveId(content)
|
|
469
|
-
↓
|
|
470
|
-
ID
|
|
471
|
-
↓
|
|
472
|
-
built-in path resolver
|
|
473
|
-
↓
|
|
474
|
-
canonical URL
|
|
475
|
-
```
|
|
476
|
-
|
|
477
|
-
Values returned by `resolveId` still pass through the same ID validation as built-in IDs.
|
|
478
|
-
|
|
479
|
-
A custom resolver therefore does not bypass the plugin's normal validation.
|
|
480
|
-
|
|
481
|
-
### Example: derive an ID from custom frontmatter
|
|
482
|
-
|
|
483
|
-
Suppose your content uses:
|
|
484
|
-
|
|
485
|
-
```md
|
|
486
|
-
---
|
|
487
|
-
category: flutter
|
|
488
|
-
serial: 42
|
|
489
|
-
---
|
|
490
|
-
```
|
|
491
|
-
|
|
492
|
-
You can define a project-specific ID:
|
|
493
|
-
|
|
494
|
-
```ts
|
|
495
|
-
permalink({
|
|
496
|
-
resolveId(content) {
|
|
497
|
-
const category = content.frontmatter.category;
|
|
498
|
-
const serial = content.frontmatter.serial;
|
|
499
|
-
|
|
500
|
-
if (typeof category !== "string") {
|
|
501
|
-
throw new Error("category is required");
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
if (typeof serial !== "number") {
|
|
505
|
-
throw new Error("serial is required");
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
return `${category}-${serial}`;
|
|
509
|
-
},
|
|
510
|
-
|
|
511
|
-
path: {
|
|
512
|
-
mode: "flat",
|
|
513
|
-
prefix: "/articles",
|
|
514
|
-
},
|
|
515
|
-
});
|
|
516
|
-
```
|
|
517
|
-
|
|
518
|
-
Result:
|
|
519
|
-
|
|
520
|
-
```text
|
|
521
|
-
/articles/flutter-42
|
|
522
|
-
```
|
|
523
|
-
|
|
524
|
-
Validation of project-specific frontmatter values inside a custom resolver is the resolver's responsibility.
|
|
525
|
-
|
|
526
|
-
---
|
|
527
|
-
|
|
528
|
-
# Advanced: `resolvePath`
|
|
529
|
-
|
|
530
|
-
`resolvePath` gives full control over how a resolved ID becomes a public URL.
|
|
531
|
-
|
|
532
|
-
```ts
|
|
533
|
-
permalink({
|
|
534
|
-
frontmatter: "id",
|
|
535
|
-
|
|
536
|
-
id: {
|
|
537
|
-
strategy: "frontmatter-or-hash",
|
|
538
|
-
},
|
|
539
|
-
|
|
540
|
-
resolvePath({ content, id }) {
|
|
541
|
-
return `/articles/${id}`;
|
|
542
|
-
},
|
|
543
|
-
});
|
|
544
|
-
```
|
|
545
|
-
|
|
546
|
-
Result:
|
|
547
|
-
|
|
548
|
-
```text
|
|
549
|
-
/articles/hello-world
|
|
550
|
-
```
|
|
551
|
-
|
|
552
|
-
The returned value must be a site-local absolute path.
|
|
553
|
-
|
|
554
|
-
```text
|
|
555
|
-
/articles/hello valid
|
|
556
|
-
/articles/hello/ valid
|
|
557
|
-
articles/hello invalid
|
|
558
|
-
https://example.com invalid
|
|
559
|
-
```
|
|
560
|
-
|
|
561
|
-
The result still passes through the plugin's normal URL normalization, validation, and collision detection.
|
|
562
|
-
|
|
563
|
-
---
|
|
564
|
-
|
|
565
|
-
# Combining `resolveId` and `resolvePath`
|
|
566
|
-
|
|
567
|
-
Both resolvers can be used together when both identity and URL structure are project-specific.
|
|
568
|
-
|
|
569
|
-
For example:
|
|
570
|
-
|
|
571
|
-
```md
|
|
572
|
-
---
|
|
573
|
-
published: 2026-09-28
|
|
574
|
-
article_id: riebeckite-permalink
|
|
575
|
-
---
|
|
576
|
-
```
|
|
577
|
-
|
|
578
|
-
```ts
|
|
579
|
-
permalink({
|
|
580
|
-
resolveId(content) {
|
|
581
|
-
const value = content.frontmatter.article_id;
|
|
582
|
-
|
|
583
|
-
if (typeof value !== "string") {
|
|
584
|
-
throw new Error("article_id is required");
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
return value;
|
|
588
|
-
},
|
|
589
|
-
|
|
590
|
-
resolvePath({ content, id }) {
|
|
591
|
-
const published = content.frontmatter.published;
|
|
592
|
-
|
|
593
|
-
if (typeof published !== "string") {
|
|
594
|
-
throw new Error("published is required");
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
const year = published.slice(0, 4);
|
|
598
|
-
|
|
599
|
-
return `/articles/${year}/${id}`;
|
|
600
|
-
},
|
|
601
|
-
});
|
|
602
|
-
```
|
|
603
|
-
|
|
604
|
-
Result:
|
|
605
|
-
|
|
606
|
-
```text
|
|
607
|
-
/articles/2026/riebeckite-permalink
|
|
608
|
-
```
|
|
609
|
-
|
|
610
|
-
Riebeckite still treats only the final resolved URL as the canonical public location.
|
|
611
|
-
|
|
612
|
-
---
|
|
613
|
-
|
|
614
|
-
# Choosing between built-in and custom resolution
|
|
615
|
-
|
|
616
|
-
A useful rule is:
|
|
617
|
-
|
|
618
|
-
```text
|
|
619
|
-
Built-in strategies are sufficient
|
|
620
|
-
↓
|
|
621
|
-
Use normal options
|
|
622
|
-
|
|
623
|
-
Only ID generation is special
|
|
624
|
-
↓
|
|
625
|
-
Use resolveId
|
|
626
|
-
|
|
627
|
-
Only URL structure is special
|
|
628
|
-
↓
|
|
629
|
-
Use resolvePath
|
|
630
|
-
|
|
631
|
-
Both are project-specific
|
|
632
|
-
↓
|
|
633
|
-
Use resolveId + resolvePath
|
|
634
|
-
```
|
|
635
|
-
|
|
636
|
-
For example, creating `/n/{id}` does not require a custom resolver:
|
|
637
|
-
|
|
638
|
-
```ts
|
|
639
|
-
permalink({
|
|
640
|
-
id: {
|
|
641
|
-
strategy: "frontmatter-or-hash",
|
|
642
|
-
},
|
|
643
|
-
|
|
644
|
-
path: {
|
|
645
|
-
mode: "flat",
|
|
646
|
-
prefix: "/n",
|
|
647
|
-
},
|
|
648
|
-
});
|
|
649
|
-
```
|
|
650
|
-
|
|
651
|
-
This is preferable because the intent is clearer and the configuration remains declarative.
|
|
652
|
-
|
|
653
|
-
---
|
|
654
|
-
|
|
655
|
-
# Guarantees with custom resolvers
|
|
656
|
-
|
|
657
|
-
Using a custom resolver does not bypass the rest of the Permalink Plugin pipeline.
|
|
658
|
-
|
|
659
|
-
The following behavior is still preserved:
|
|
660
|
-
|
|
661
|
-
- ID validation
|
|
662
|
-
- URL normalization
|
|
663
|
-
- URL validation
|
|
664
|
-
- ID collision detection
|
|
665
|
-
- canonical URL collision detection
|
|
666
|
-
- redirect collision detection
|
|
667
|
-
- trailing slash handling
|
|
668
|
-
- canonical public location registration in Core
|
|
669
|
-
|
|
670
|
-
Custom resolvers change how the ID or path is produced, not how the resulting public location is validated and registered.
|
|
671
|
-
|
|
672
|
-
---
|
|
673
|
-
|
|
674
|
-
# Manual permalink precedence
|
|
675
|
-
|
|
676
|
-
A manual permalink override takes precedence over normal ID and path resolution.
|
|
677
|
-
|
|
678
|
-
For example:
|
|
679
|
-
|
|
680
|
-
```md
|
|
681
|
-
---
|
|
682
|
-
id: abc
|
|
683
|
-
permalink: /about
|
|
684
|
-
---
|
|
685
|
-
```
|
|
686
|
-
|
|
687
|
-
is treated conceptually as:
|
|
688
|
-
|
|
689
|
-
```text
|
|
690
|
-
ID candidate
|
|
691
|
-
abc
|
|
692
|
-
|
|
693
|
-
Canonical URL
|
|
694
|
-
/about
|
|
695
|
-
```
|
|
696
|
-
|
|
697
|
-
If `resolvePath` is also configured, the manual permalink override still wins.
|
|
698
|
-
|
|
699
|
-
This makes it possible to use a general URL strategy while giving a few special pages fixed URLs.
|
|
700
|
-
|
|
701
|
-
---
|
|
702
|
-
|
|
703
|
-
# Stateless builds
|
|
704
|
-
|
|
705
|
-
The Permalink Plugin does not maintain a persistent ID registry.
|
|
706
|
-
|
|
707
|
-
It does not create or require:
|
|
708
|
-
|
|
709
|
-
```text
|
|
710
|
-
.riebeckite/content-ids.json
|
|
711
|
-
state.json
|
|
712
|
-
SQLite database
|
|
713
|
-
KV database
|
|
714
|
-
```
|
|
715
|
-
|
|
716
|
-
Public locations are derived at build time from:
|
|
717
|
-
|
|
718
|
-
```text
|
|
719
|
-
Plugin configuration
|
|
720
|
-
+
|
|
721
|
-
source content
|
|
722
|
-
```
|
|
723
|
-
|
|
724
|
-
This makes the same configuration suitable for local builds, CI, and Cloudflare Workers deployments without additional identity state.
|
|
725
|
-
|
|
726
|
-
With the `hash` strategy, the source path is part of the identity input. Renaming or moving a file therefore changes its derived ID.
|
|
727
|
-
|
|
728
|
-
Use explicit frontmatter IDs for content whose URL must survive source-file moves.
|
|
729
|
-
|
|
730
|
-
---
|
|
731
|
-
|
|
732
|
-
# Rename and move behavior
|
|
733
|
-
|
|
734
|
-
URL stability depends on both the ID strategy and path mode.
|
|
735
|
-
|
|
736
|
-
| ID strategy | Path mode | Rename | Move |
|
|
737
|
-
| --- | --- | --- | --- |
|
|
738
|
-
| frontmatter | flat | Preserved | Preserved |
|
|
739
|
-
| frontmatter | preserve | May change | Changes |
|
|
740
|
-
| frontmatter | append | May change | Changes |
|
|
741
|
-
| hash | flat | Changes | Changes |
|
|
742
|
-
| hash | preserve | Changes | Changes |
|
|
743
|
-
| hash | append | Changes | Changes |
|
|
744
|
-
|
|
745
|
-
For explicitly managed permanent URLs:
|
|
746
|
-
|
|
747
|
-
```ts
|
|
748
|
-
permalink({
|
|
749
|
-
frontmatter: "id",
|
|
750
|
-
|
|
751
|
-
id: {
|
|
752
|
-
strategy: "frontmatter",
|
|
753
|
-
},
|
|
754
|
-
|
|
755
|
-
path: {
|
|
756
|
-
mode: "flat",
|
|
757
|
-
},
|
|
758
|
-
});
|
|
759
|
-
```
|
|
760
|
-
|
|
761
|
-
For existing vaults where adding IDs everywhere is undesirable:
|
|
762
|
-
|
|
763
|
-
```ts
|
|
764
|
-
id: {
|
|
765
|
-
strategy: "frontmatter-or-hash",
|
|
766
|
-
}
|
|
767
|
-
```
|
|
768
|
-
|
|
769
|
-
is usually more convenient.
|
|
770
|
-
|
|
771
|
-
---
|
|
772
|
-
|
|
773
|
-
# Validation
|
|
774
|
-
|
|
775
|
-
## IDs
|
|
776
|
-
|
|
777
|
-
An ID must represent a single URL path segment.
|
|
778
|
-
|
|
779
|
-
The following are rejected:
|
|
780
|
-
|
|
781
|
-
- empty values
|
|
782
|
-
- `/`
|
|
783
|
-
- `#`
|
|
784
|
-
- `?`
|
|
785
|
-
- whitespace
|
|
786
|
-
- malformed percent-encoding
|
|
787
|
-
|
|
788
|
-
Values returned by `resolveId` are subject to the same rules.
|
|
789
|
-
|
|
790
|
-
## Permalinks
|
|
791
|
-
|
|
792
|
-
Permalinks and redirects must be site-local absolute paths.
|
|
793
|
-
|
|
794
|
-
The following are rejected:
|
|
795
|
-
|
|
796
|
-
- relative paths
|
|
797
|
-
- query strings
|
|
798
|
-
- fragments
|
|
799
|
-
- `\`
|
|
800
|
-
- invalid `//`
|
|
801
|
-
- external URLs
|
|
802
|
-
|
|
803
|
-
Values returned by `resolvePath` are subject to the same rules.
|
|
804
|
-
|
|
805
|
-
---
|
|
806
|
-
|
|
807
|
-
# Collision detection
|
|
808
|
-
|
|
809
|
-
The plugin detects conflicting public locations during the build.
|
|
810
|
-
|
|
811
|
-
This includes:
|
|
812
|
-
|
|
813
|
-
- ID ↔ ID
|
|
814
|
-
- canonical URL ↔ canonical URL
|
|
815
|
-
- canonical URL ↔ redirect
|
|
816
|
-
- redirect ↔ redirect
|
|
817
|
-
|
|
818
|
-
For example:
|
|
819
|
-
|
|
820
|
-
```text
|
|
821
|
-
a.md
|
|
822
|
-
→ /about
|
|
823
|
-
|
|
824
|
-
b.md
|
|
825
|
-
→ /about
|
|
826
|
-
```
|
|
827
|
-
|
|
828
|
-
fails the build.
|
|
829
|
-
|
|
830
|
-
The following also fails:
|
|
831
|
-
|
|
832
|
-
```text
|
|
833
|
-
a.md canonical
|
|
834
|
-
→ /about
|
|
835
|
-
|
|
836
|
-
b.md redirect
|
|
837
|
-
→ /about
|
|
838
|
-
```
|
|
839
|
-
|
|
840
|
-
The plugin does not silently append suffixes to resolve collisions.
|
|
841
|
-
|
|
842
|
-
This prevents public URLs from changing based on build or content ordering.
|
|
843
|
-
|
|
844
|
-
---
|
|
845
|
-
|
|
846
|
-
# Inspecting resolved URLs
|
|
847
|
-
|
|
848
|
-
Resolved values can be inspected through Riebeckite's existing content inspection command:
|
|
849
|
-
|
|
850
|
-
```sh
|
|
851
|
-
riebeckite inspect content --list
|
|
852
|
-
```
|
|
853
|
-
|
|
854
|
-
When the Permalink Plugin is enabled, the output can expose the resolved ID, ID source, and permalink for each entry.
|
|
855
|
-
|
|
856
|
-
For example:
|
|
857
|
-
|
|
858
|
-
```text
|
|
859
|
-
PATH ID ID SOURCE PERMALINK
|
|
860
|
-
notes/a.md K7m3Qp8d... derived /n/K7m3Qp8d...
|
|
861
|
-
notes/about.md about frontmatter /about
|
|
862
|
-
```
|
|
863
|
-
|
|
864
|
-
The exact output format may vary between CLI versions.
|
|
865
|
-
|
|
866
|
-
---
|
|
867
|
-
|
|
868
|
-
# Exports
|
|
869
|
-
|
|
870
|
-
## Functions
|
|
871
|
-
|
|
872
|
-
- `permalink(options?)`
|
|
873
|
-
- `permalinkPlugin(options?)`
|
|
874
|
-
|
|
875
|
-
Both create the Permalink Plugin.
|
|
876
|
-
|
|
877
|
-
## Types
|
|
878
|
-
|
|
879
|
-
- `PermalinkOptions`
|
|
880
|
-
- `PermalinkIdStrategy`
|
|
881
|
-
- `PermalinkPathMode`
|
|
882
|
-
- `RedirectStatus`
|
|
883
|
-
|
|
884
|
-
Use the exported types when building type-safe project-specific resolver configuration.
|
|
885
|
-
|
|
886
|
-
---
|
|
887
|
-
|
|
888
|
-
# Configuration examples
|
|
889
|
-
|
|
890
|
-
## Existing Obsidian vault
|
|
891
|
-
|
|
892
|
-
Hide the filesystem layout without requiring frontmatter changes across the vault:
|
|
893
|
-
|
|
894
|
-
```ts
|
|
895
|
-
permalink({
|
|
896
|
-
frontmatter: "id",
|
|
897
|
-
|
|
898
|
-
id: {
|
|
899
|
-
strategy: "frontmatter-or-hash",
|
|
900
|
-
length: 12,
|
|
901
|
-
},
|
|
902
|
-
|
|
903
|
-
path: {
|
|
904
|
-
mode: "flat",
|
|
905
|
-
prefix: "/n",
|
|
906
|
-
},
|
|
907
|
-
});
|
|
908
|
-
```
|
|
909
|
-
|
|
910
|
-
## Explicit permanent IDs
|
|
911
|
-
|
|
912
|
-
Require every entry to define its identity explicitly:
|
|
913
|
-
|
|
914
|
-
```ts
|
|
915
|
-
permalink({
|
|
916
|
-
frontmatter: "id",
|
|
917
|
-
|
|
918
|
-
id: {
|
|
919
|
-
strategy: "frontmatter",
|
|
920
|
-
},
|
|
921
|
-
|
|
922
|
-
path: {
|
|
923
|
-
mode: "flat",
|
|
924
|
-
prefix: "/n",
|
|
925
|
-
},
|
|
926
|
-
});
|
|
927
|
-
```
|
|
928
|
-
|
|
929
|
-
## Preserve directory structure
|
|
930
|
-
|
|
931
|
-
```ts
|
|
932
|
-
permalink({
|
|
933
|
-
id: {
|
|
934
|
-
strategy: "frontmatter-or-hash",
|
|
935
|
-
},
|
|
936
|
-
|
|
937
|
-
path: {
|
|
938
|
-
mode: "preserve",
|
|
939
|
-
prefix: "",
|
|
940
|
-
},
|
|
941
|
-
});
|
|
942
|
-
```
|
|
943
|
-
|
|
944
|
-
## Fully custom URL scheme
|
|
945
|
-
|
|
946
|
-
```ts
|
|
947
|
-
permalink({
|
|
948
|
-
resolveId(content) {
|
|
949
|
-
// Project-specific identity.
|
|
950
|
-
return "...";
|
|
951
|
-
},
|
|
952
|
-
|
|
953
|
-
resolvePath({ content, id }) {
|
|
954
|
-
// Project-specific public URL.
|
|
955
|
-
return `/articles/${id}`;
|
|
956
|
-
},
|
|
957
|
-
});
|
|
958
|
-
```
|
|
959
|
-
|
|
960
|
-
---
|
|
961
|
-
|
|
962
|
-
# See also
|
|
963
|
-
|
|
964
|
-
- [Plugin guide](../../../docs/en/plugin-
|
|
965
|
-
- [Content system](../../../docs/en/content-system.md)
|
|
1
|
+
# @riebeckite/plugin-permalink
|
|
2
|
+
|
|
3
|
+
Stable, configurable public URLs (permalinks) for Riebeckite content.
|
|
4
|
+
|
|
5
|
+
Instead of coupling public URLs directly to the filesystem layout, this plugin can build URLs from frontmatter IDs, deterministic path-derived IDs, or custom resolvers. Legacy URLs can also be registered as redirects in the same configuration.
|
|
6
|
+
|
|
7
|
+
[日本語](./README_ja.md)
|
|
8
|
+
|
|
9
|
+
## Basic usage
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { defineConfig } from "@riebeckite/core";
|
|
13
|
+
import { permalink } from "@riebeckite/plugin-permalink";
|
|
14
|
+
|
|
15
|
+
export default defineConfig({
|
|
16
|
+
plugins: [
|
|
17
|
+
permalink({
|
|
18
|
+
frontmatter: "id",
|
|
19
|
+
id: {
|
|
20
|
+
strategy: "frontmatter-or-hash",
|
|
21
|
+
length: 12,
|
|
22
|
+
},
|
|
23
|
+
path: {
|
|
24
|
+
mode: "flat",
|
|
25
|
+
prefix: "/n",
|
|
26
|
+
trailingSlash: false,
|
|
27
|
+
},
|
|
28
|
+
redirects: {
|
|
29
|
+
frontmatter: "redirect_from",
|
|
30
|
+
status: 308,
|
|
31
|
+
},
|
|
32
|
+
}),
|
|
33
|
+
],
|
|
34
|
+
});
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Given the following content:
|
|
38
|
+
|
|
39
|
+
```md
|
|
40
|
+
---
|
|
41
|
+
id: hello-world
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
# Hello
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
with `path.mode: "flat"` and `path.prefix: "/n"`, the public URL becomes:
|
|
48
|
+
|
|
49
|
+
```text
|
|
50
|
+
/n/hello-world
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
With the default `frontmatter-or-hash` strategy, content without a frontmatter ID receives a deterministic ID derived from its file path.
|
|
54
|
+
|
|
55
|
+
This means an existing Obsidian vault does not need an `id` field added to every file.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## How it works
|
|
60
|
+
|
|
61
|
+
`permalink()` uses the build-time `resolveContentLocations` extension point to resolve the public location of each content entry.
|
|
62
|
+
|
|
63
|
+
Conceptually:
|
|
64
|
+
|
|
65
|
+
```text
|
|
66
|
+
Content
|
|
67
|
+
│
|
|
68
|
+
├─ frontmatter
|
|
69
|
+
├─ slug
|
|
70
|
+
└─ source path
|
|
71
|
+
│
|
|
72
|
+
▼
|
|
73
|
+
@riebeckite/plugin-permalink
|
|
74
|
+
│
|
|
75
|
+
├─ resolve ID
|
|
76
|
+
├─ resolve path
|
|
77
|
+
├─ normalize
|
|
78
|
+
├─ validate
|
|
79
|
+
└─ redirects
|
|
80
|
+
│
|
|
81
|
+
▼
|
|
82
|
+
ContentPublicLocation
|
|
83
|
+
│
|
|
84
|
+
├─ permalink
|
|
85
|
+
├─ redirects
|
|
86
|
+
└─ metadata
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Core treats the resolved `permalink` as the canonical public URL.
|
|
90
|
+
|
|
91
|
+
The same canonical URL is then used by Wikilinks, backlinks, search, the content graph, SEO canonical URLs, sitemaps, RSS / Atom / JSON Feed, and other consumers.
|
|
92
|
+
|
|
93
|
+
Without this plugin, Riebeckite still resolves every URL through the Core default public location resolver, `resolveDefaultContentLocation`, which maps `index` to `/` and every other entry to `/{slug}`. That default is a first-class Core policy, not a fallback.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
# Frontmatter
|
|
98
|
+
|
|
99
|
+
## ID
|
|
100
|
+
|
|
101
|
+
By default, the plugin reads the `id` field:
|
|
102
|
+
|
|
103
|
+
```md
|
|
104
|
+
---
|
|
105
|
+
id: hello-world
|
|
106
|
+
---
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The field name is configurable:
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
permalink({
|
|
113
|
+
frontmatter: "uid",
|
|
114
|
+
});
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
```md
|
|
118
|
+
---
|
|
119
|
+
uid: hello-world
|
|
120
|
+
---
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`frontmatter` refers to a top-level frontmatter field.
|
|
124
|
+
|
|
125
|
+
## Permalink override
|
|
126
|
+
|
|
127
|
+
A specific entry can override its public URL entirely:
|
|
128
|
+
|
|
129
|
+
```md
|
|
130
|
+
---
|
|
131
|
+
id: about-page
|
|
132
|
+
permalink: /about
|
|
133
|
+
---
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The explicit permalink takes precedence over normal ID and path resolution.
|
|
137
|
+
|
|
138
|
+
The content ID and its public location remain separate concepts:
|
|
139
|
+
|
|
140
|
+
```text
|
|
141
|
+
ID
|
|
142
|
+
about-page
|
|
143
|
+
|
|
144
|
+
Canonical URL
|
|
145
|
+
/about
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
The field name can also be changed:
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
permalink({
|
|
152
|
+
override: {
|
|
153
|
+
frontmatter: "url",
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Redirects
|
|
159
|
+
|
|
160
|
+
Legacy URLs can be declared in frontmatter:
|
|
161
|
+
|
|
162
|
+
```md
|
|
163
|
+
---
|
|
164
|
+
id: hello-world
|
|
165
|
+
redirect_from:
|
|
166
|
+
- /posts/hello
|
|
167
|
+
- /blog/2024/hello-world
|
|
168
|
+
---
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
If the canonical URL is:
|
|
172
|
+
|
|
173
|
+
```text
|
|
174
|
+
/n/hello-world
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
the redirects resolve to it:
|
|
178
|
+
|
|
179
|
+
```text
|
|
180
|
+
/posts/hello
|
|
181
|
+
↓ 308
|
|
182
|
+
/n/hello-world
|
|
183
|
+
|
|
184
|
+
/blog/2024/hello-world
|
|
185
|
+
↓ 308
|
|
186
|
+
/n/hello-world
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Configure the field and status with:
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
permalink({
|
|
193
|
+
redirects: {
|
|
194
|
+
frontmatter: "redirect_from",
|
|
195
|
+
status: 308,
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
`redirect_from` accepts either a string or an array of strings.
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
# Options
|
|
205
|
+
|
|
206
|
+
| Option | Type | Default | Description |
|
|
207
|
+
| --- | --- | --- | --- |
|
|
208
|
+
| `frontmatter` | `string` | `"id"` | Top-level frontmatter field used as the ID |
|
|
209
|
+
| `id.strategy` | `"frontmatter" \| "hash" \| "frontmatter-or-hash"` | `"frontmatter-or-hash"` | How IDs are resolved |
|
|
210
|
+
| `id.length` | `number` | `12` | Length of path-derived hash IDs (6–43) |
|
|
211
|
+
| `path.mode` | `"flat" \| "preserve" \| "append"` | `"flat"` | How the resolved ID is placed into the public URL |
|
|
212
|
+
| `path.prefix` | `string` | `"/n"` | URL prefix. `"/"` or an empty string means no prefix |
|
|
213
|
+
| `path.trailingSlash` | `boolean` | `false` | Whether canonical URLs end with `/` |
|
|
214
|
+
| `index.collapse` | `boolean` | `true` | Whether the `index` segment is collapsed |
|
|
215
|
+
| `override.frontmatter` | `string` | `"permalink"` | Frontmatter field containing a complete permalink override |
|
|
216
|
+
| `redirects.frontmatter` | `string` | `"redirect_from"` | Frontmatter field containing legacy URLs |
|
|
217
|
+
| `redirects.status` | `301 \| 302 \| 307 \| 308` | `308` | Redirect HTTP status |
|
|
218
|
+
| `resolveId` | `(content) => string` | — | Fully customize ID resolution |
|
|
219
|
+
| `resolvePath` | `({ content, id }) => string` | — | Fully customize public path generation |
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
# ID strategies
|
|
224
|
+
|
|
225
|
+
## `frontmatter`
|
|
226
|
+
|
|
227
|
+
Uses the configured frontmatter field as the ID.
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
permalink({
|
|
231
|
+
frontmatter: "id",
|
|
232
|
+
id: {
|
|
233
|
+
strategy: "frontmatter",
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Every content entry must provide the field. A missing ID causes a build error.
|
|
239
|
+
|
|
240
|
+
This is useful when URL identity should be explicitly controlled:
|
|
241
|
+
|
|
242
|
+
```md
|
|
243
|
+
---
|
|
244
|
+
id: article-123
|
|
245
|
+
---
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
With `flat` mode, renaming or moving the source file does not change the ID or public URL.
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## `hash`
|
|
253
|
+
|
|
254
|
+
Ignores the frontmatter ID and derives the ID from the source file path.
|
|
255
|
+
|
|
256
|
+
```ts
|
|
257
|
+
permalink({
|
|
258
|
+
id: {
|
|
259
|
+
strategy: "hash",
|
|
260
|
+
length: 12,
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
The input path is normalized by:
|
|
266
|
+
|
|
267
|
+
- replacing `\` with `/`
|
|
268
|
+
- stripping leading `/`
|
|
269
|
+
- applying Unicode NFC normalization
|
|
270
|
+
|
|
271
|
+
The normalized path is hashed using SHA-256, encoded as base64url, and truncated to `id.length`.
|
|
272
|
+
|
|
273
|
+
```text
|
|
274
|
+
notes/flutter/riverpod.md
|
|
275
|
+
│
|
|
276
|
+
▼
|
|
277
|
+
normalized path
|
|
278
|
+
│
|
|
279
|
+
▼
|
|
280
|
+
SHA-256
|
|
281
|
+
│
|
|
282
|
+
▼
|
|
283
|
+
base64url
|
|
284
|
+
│
|
|
285
|
+
▼
|
|
286
|
+
K7m3Qp8d...
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
The same path produces the same ID across operating systems and build environments.
|
|
290
|
+
|
|
291
|
+
Because the source path is the input, renaming or moving the file changes the ID.
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
## `frontmatter-or-hash`
|
|
296
|
+
|
|
297
|
+
This is the default strategy.
|
|
298
|
+
|
|
299
|
+
```ts
|
|
300
|
+
permalink({
|
|
301
|
+
frontmatter: "id",
|
|
302
|
+
id: {
|
|
303
|
+
strategy: "frontmatter-or-hash",
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
Resolution works as follows:
|
|
309
|
+
|
|
310
|
+
```text
|
|
311
|
+
frontmatter ID exists
|
|
312
|
+
↓
|
|
313
|
+
use frontmatter ID
|
|
314
|
+
|
|
315
|
+
frontmatter ID missing
|
|
316
|
+
↓
|
|
317
|
+
derive ID from path
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
This is useful for existing Obsidian vaults: content works without modification, while important entries can opt into explicit stable IDs.
|
|
321
|
+
|
|
322
|
+
---
|
|
323
|
+
|
|
324
|
+
# ID metadata
|
|
325
|
+
|
|
326
|
+
The source of a resolved ID is exposed through metadata.
|
|
327
|
+
|
|
328
|
+
| `metadata.idSource` | Meaning |
|
|
329
|
+
| --- | --- |
|
|
330
|
+
| `frontmatter` | Taken from the configured frontmatter field |
|
|
331
|
+
| `derived` | Derived from the source path hash |
|
|
332
|
+
| `custom` | Returned by `resolveId` |
|
|
333
|
+
|
|
334
|
+
A manual `permalink` override changes the URL without removing identity. When the frontmatter ID is present it is still recorded (`metadata.idSource` is `frontmatter`); metadata is omitted only when no explicit ID exists. Overrides never produce an implicit `/{id}` URL.
|
|
335
|
+
|
|
336
|
+
---
|
|
337
|
+
|
|
338
|
+
# URL path modes
|
|
339
|
+
|
|
340
|
+
ID resolution and URL composition are separate concerns.
|
|
341
|
+
|
|
342
|
+
Assume:
|
|
343
|
+
|
|
344
|
+
```text
|
|
345
|
+
Source:
|
|
346
|
+
notes/flutter/hello.md
|
|
347
|
+
|
|
348
|
+
ID:
|
|
349
|
+
hello-world
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
## `flat`
|
|
353
|
+
|
|
354
|
+
```ts
|
|
355
|
+
path: {
|
|
356
|
+
mode: "flat",
|
|
357
|
+
prefix: "/n",
|
|
358
|
+
}
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
Result:
|
|
362
|
+
|
|
363
|
+
```text
|
|
364
|
+
/n/hello-world
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
The filesystem directory structure is not exposed in the public URL.
|
|
368
|
+
|
|
369
|
+
This mode is useful for opaque, location-independent URLs.
|
|
370
|
+
|
|
371
|
+
---
|
|
372
|
+
|
|
373
|
+
## `preserve`
|
|
374
|
+
|
|
375
|
+
Preserves the source directory structure while placing the ID into the resulting URL.
|
|
376
|
+
|
|
377
|
+
```ts
|
|
378
|
+
path: {
|
|
379
|
+
mode: "preserve",
|
|
380
|
+
prefix: "/n",
|
|
381
|
+
}
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
Example:
|
|
385
|
+
|
|
386
|
+
```text
|
|
387
|
+
notes/flutter/hello.md
|
|
388
|
+
|
|
389
|
+
↓
|
|
390
|
+
|
|
391
|
+
/n/notes/flutter/hello-world
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
---
|
|
395
|
+
|
|
396
|
+
## `append`
|
|
397
|
+
|
|
398
|
+
`append` is also available:
|
|
399
|
+
|
|
400
|
+
```ts
|
|
401
|
+
path: {
|
|
402
|
+
mode: "append",
|
|
403
|
+
}
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
The current implementation returns the same path as `preserve`.
|
|
407
|
+
|
|
408
|
+
The mode exists separately so it can represent different composition semantics in the future. Do not rely on `append` having behavior distinct from `preserve` in the current version.
|
|
409
|
+
|
|
410
|
+
---
|
|
411
|
+
|
|
412
|
+
# Index files
|
|
413
|
+
|
|
414
|
+
`index.collapse` controls how `index.md` is represented.
|
|
415
|
+
|
|
416
|
+
```ts
|
|
417
|
+
index: {
|
|
418
|
+
collapse: true,
|
|
419
|
+
}
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
For:
|
|
423
|
+
|
|
424
|
+
```text
|
|
425
|
+
notes/flutter/index.md
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
with ID `hello-world`:
|
|
429
|
+
|
|
430
|
+
| Configuration | URL |
|
|
431
|
+
| --- | --- |
|
|
432
|
+
| `flat` | `/n/hello-world` |
|
|
433
|
+
| `preserve` + collapse | `/n/notes/flutter/hello-world` |
|
|
434
|
+
| `preserve` + no collapse | `/n/notes/flutter/index/hello-world` |
|
|
435
|
+
|
|
436
|
+
`flat` does not use the filesystem directory structure, so index collapsing does not affect it.
|
|
437
|
+
|
|
438
|
+
---
|
|
439
|
+
|
|
440
|
+
# Advanced: Custom resolvers
|
|
441
|
+
|
|
442
|
+
For URL schemes that cannot be expressed with the built-in strategies, `resolveId` and `resolvePath` provide escape hatches.
|
|
443
|
+
|
|
444
|
+
Prefer the built-in options when they are sufficient. Custom resolvers are intended for project-specific identity and URL schemes.
|
|
445
|
+
|
|
446
|
+
## `resolveId`
|
|
447
|
+
|
|
448
|
+
`resolveId` lets you compute the content ID yourself.
|
|
449
|
+
|
|
450
|
+
```ts
|
|
451
|
+
permalink({
|
|
452
|
+
resolveId(content) {
|
|
453
|
+
return `post-${content.slug}`;
|
|
454
|
+
},
|
|
455
|
+
|
|
456
|
+
path: {
|
|
457
|
+
mode: "flat",
|
|
458
|
+
prefix: "/articles",
|
|
459
|
+
},
|
|
460
|
+
});
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
Conceptually:
|
|
464
|
+
|
|
465
|
+
```text
|
|
466
|
+
Content
|
|
467
|
+
↓
|
|
468
|
+
resolveId(content)
|
|
469
|
+
↓
|
|
470
|
+
ID
|
|
471
|
+
↓
|
|
472
|
+
built-in path resolver
|
|
473
|
+
↓
|
|
474
|
+
canonical URL
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
Values returned by `resolveId` still pass through the same ID validation as built-in IDs.
|
|
478
|
+
|
|
479
|
+
A custom resolver therefore does not bypass the plugin's normal validation.
|
|
480
|
+
|
|
481
|
+
### Example: derive an ID from custom frontmatter
|
|
482
|
+
|
|
483
|
+
Suppose your content uses:
|
|
484
|
+
|
|
485
|
+
```md
|
|
486
|
+
---
|
|
487
|
+
category: flutter
|
|
488
|
+
serial: 42
|
|
489
|
+
---
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
You can define a project-specific ID:
|
|
493
|
+
|
|
494
|
+
```ts
|
|
495
|
+
permalink({
|
|
496
|
+
resolveId(content) {
|
|
497
|
+
const category = content.frontmatter.category;
|
|
498
|
+
const serial = content.frontmatter.serial;
|
|
499
|
+
|
|
500
|
+
if (typeof category !== "string") {
|
|
501
|
+
throw new Error("category is required");
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (typeof serial !== "number") {
|
|
505
|
+
throw new Error("serial is required");
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return `${category}-${serial}`;
|
|
509
|
+
},
|
|
510
|
+
|
|
511
|
+
path: {
|
|
512
|
+
mode: "flat",
|
|
513
|
+
prefix: "/articles",
|
|
514
|
+
},
|
|
515
|
+
});
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
Result:
|
|
519
|
+
|
|
520
|
+
```text
|
|
521
|
+
/articles/flutter-42
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
Validation of project-specific frontmatter values inside a custom resolver is the resolver's responsibility.
|
|
525
|
+
|
|
526
|
+
---
|
|
527
|
+
|
|
528
|
+
# Advanced: `resolvePath`
|
|
529
|
+
|
|
530
|
+
`resolvePath` gives full control over how a resolved ID becomes a public URL.
|
|
531
|
+
|
|
532
|
+
```ts
|
|
533
|
+
permalink({
|
|
534
|
+
frontmatter: "id",
|
|
535
|
+
|
|
536
|
+
id: {
|
|
537
|
+
strategy: "frontmatter-or-hash",
|
|
538
|
+
},
|
|
539
|
+
|
|
540
|
+
resolvePath({ content, id }) {
|
|
541
|
+
return `/articles/${id}`;
|
|
542
|
+
},
|
|
543
|
+
});
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
Result:
|
|
547
|
+
|
|
548
|
+
```text
|
|
549
|
+
/articles/hello-world
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
The returned value must be a site-local absolute path.
|
|
553
|
+
|
|
554
|
+
```text
|
|
555
|
+
/articles/hello valid
|
|
556
|
+
/articles/hello/ valid
|
|
557
|
+
articles/hello invalid
|
|
558
|
+
https://example.com invalid
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
The result still passes through the plugin's normal URL normalization, validation, and collision detection.
|
|
562
|
+
|
|
563
|
+
---
|
|
564
|
+
|
|
565
|
+
# Combining `resolveId` and `resolvePath`
|
|
566
|
+
|
|
567
|
+
Both resolvers can be used together when both identity and URL structure are project-specific.
|
|
568
|
+
|
|
569
|
+
For example:
|
|
570
|
+
|
|
571
|
+
```md
|
|
572
|
+
---
|
|
573
|
+
published: 2026-09-28
|
|
574
|
+
article_id: riebeckite-permalink
|
|
575
|
+
---
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
```ts
|
|
579
|
+
permalink({
|
|
580
|
+
resolveId(content) {
|
|
581
|
+
const value = content.frontmatter.article_id;
|
|
582
|
+
|
|
583
|
+
if (typeof value !== "string") {
|
|
584
|
+
throw new Error("article_id is required");
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
return value;
|
|
588
|
+
},
|
|
589
|
+
|
|
590
|
+
resolvePath({ content, id }) {
|
|
591
|
+
const published = content.frontmatter.published;
|
|
592
|
+
|
|
593
|
+
if (typeof published !== "string") {
|
|
594
|
+
throw new Error("published is required");
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const year = published.slice(0, 4);
|
|
598
|
+
|
|
599
|
+
return `/articles/${year}/${id}`;
|
|
600
|
+
},
|
|
601
|
+
});
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
Result:
|
|
605
|
+
|
|
606
|
+
```text
|
|
607
|
+
/articles/2026/riebeckite-permalink
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
Riebeckite still treats only the final resolved URL as the canonical public location.
|
|
611
|
+
|
|
612
|
+
---
|
|
613
|
+
|
|
614
|
+
# Choosing between built-in and custom resolution
|
|
615
|
+
|
|
616
|
+
A useful rule is:
|
|
617
|
+
|
|
618
|
+
```text
|
|
619
|
+
Built-in strategies are sufficient
|
|
620
|
+
↓
|
|
621
|
+
Use normal options
|
|
622
|
+
|
|
623
|
+
Only ID generation is special
|
|
624
|
+
↓
|
|
625
|
+
Use resolveId
|
|
626
|
+
|
|
627
|
+
Only URL structure is special
|
|
628
|
+
↓
|
|
629
|
+
Use resolvePath
|
|
630
|
+
|
|
631
|
+
Both are project-specific
|
|
632
|
+
↓
|
|
633
|
+
Use resolveId + resolvePath
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
For example, creating `/n/{id}` does not require a custom resolver:
|
|
637
|
+
|
|
638
|
+
```ts
|
|
639
|
+
permalink({
|
|
640
|
+
id: {
|
|
641
|
+
strategy: "frontmatter-or-hash",
|
|
642
|
+
},
|
|
643
|
+
|
|
644
|
+
path: {
|
|
645
|
+
mode: "flat",
|
|
646
|
+
prefix: "/n",
|
|
647
|
+
},
|
|
648
|
+
});
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
This is preferable because the intent is clearer and the configuration remains declarative.
|
|
652
|
+
|
|
653
|
+
---
|
|
654
|
+
|
|
655
|
+
# Guarantees with custom resolvers
|
|
656
|
+
|
|
657
|
+
Using a custom resolver does not bypass the rest of the Permalink Plugin pipeline.
|
|
658
|
+
|
|
659
|
+
The following behavior is still preserved:
|
|
660
|
+
|
|
661
|
+
- ID validation
|
|
662
|
+
- URL normalization
|
|
663
|
+
- URL validation
|
|
664
|
+
- ID collision detection
|
|
665
|
+
- canonical URL collision detection
|
|
666
|
+
- redirect collision detection
|
|
667
|
+
- trailing slash handling
|
|
668
|
+
- canonical public location registration in Core
|
|
669
|
+
|
|
670
|
+
Custom resolvers change how the ID or path is produced, not how the resulting public location is validated and registered.
|
|
671
|
+
|
|
672
|
+
---
|
|
673
|
+
|
|
674
|
+
# Manual permalink precedence
|
|
675
|
+
|
|
676
|
+
A manual permalink override takes precedence over normal ID and path resolution.
|
|
677
|
+
|
|
678
|
+
For example:
|
|
679
|
+
|
|
680
|
+
```md
|
|
681
|
+
---
|
|
682
|
+
id: abc
|
|
683
|
+
permalink: /about
|
|
684
|
+
---
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
is treated conceptually as:
|
|
688
|
+
|
|
689
|
+
```text
|
|
690
|
+
ID candidate
|
|
691
|
+
abc
|
|
692
|
+
|
|
693
|
+
Canonical URL
|
|
694
|
+
/about
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
If `resolvePath` is also configured, the manual permalink override still wins.
|
|
698
|
+
|
|
699
|
+
This makes it possible to use a general URL strategy while giving a few special pages fixed URLs.
|
|
700
|
+
|
|
701
|
+
---
|
|
702
|
+
|
|
703
|
+
# Stateless builds
|
|
704
|
+
|
|
705
|
+
The Permalink Plugin does not maintain a persistent ID registry.
|
|
706
|
+
|
|
707
|
+
It does not create or require:
|
|
708
|
+
|
|
709
|
+
```text
|
|
710
|
+
.riebeckite/content-ids.json
|
|
711
|
+
state.json
|
|
712
|
+
SQLite database
|
|
713
|
+
KV database
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
Public locations are derived at build time from:
|
|
717
|
+
|
|
718
|
+
```text
|
|
719
|
+
Plugin configuration
|
|
720
|
+
+
|
|
721
|
+
source content
|
|
722
|
+
```
|
|
723
|
+
|
|
724
|
+
This makes the same configuration suitable for local builds, CI, and Cloudflare Workers deployments without additional identity state.
|
|
725
|
+
|
|
726
|
+
With the `hash` strategy, the source path is part of the identity input. Renaming or moving a file therefore changes its derived ID.
|
|
727
|
+
|
|
728
|
+
Use explicit frontmatter IDs for content whose URL must survive source-file moves.
|
|
729
|
+
|
|
730
|
+
---
|
|
731
|
+
|
|
732
|
+
# Rename and move behavior
|
|
733
|
+
|
|
734
|
+
URL stability depends on both the ID strategy and path mode.
|
|
735
|
+
|
|
736
|
+
| ID strategy | Path mode | Rename | Move |
|
|
737
|
+
| --- | --- | --- | --- |
|
|
738
|
+
| frontmatter | flat | Preserved | Preserved |
|
|
739
|
+
| frontmatter | preserve | May change | Changes |
|
|
740
|
+
| frontmatter | append | May change | Changes |
|
|
741
|
+
| hash | flat | Changes | Changes |
|
|
742
|
+
| hash | preserve | Changes | Changes |
|
|
743
|
+
| hash | append | Changes | Changes |
|
|
744
|
+
|
|
745
|
+
For explicitly managed permanent URLs:
|
|
746
|
+
|
|
747
|
+
```ts
|
|
748
|
+
permalink({
|
|
749
|
+
frontmatter: "id",
|
|
750
|
+
|
|
751
|
+
id: {
|
|
752
|
+
strategy: "frontmatter",
|
|
753
|
+
},
|
|
754
|
+
|
|
755
|
+
path: {
|
|
756
|
+
mode: "flat",
|
|
757
|
+
},
|
|
758
|
+
});
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
For existing vaults where adding IDs everywhere is undesirable:
|
|
762
|
+
|
|
763
|
+
```ts
|
|
764
|
+
id: {
|
|
765
|
+
strategy: "frontmatter-or-hash",
|
|
766
|
+
}
|
|
767
|
+
```
|
|
768
|
+
|
|
769
|
+
is usually more convenient.
|
|
770
|
+
|
|
771
|
+
---
|
|
772
|
+
|
|
773
|
+
# Validation
|
|
774
|
+
|
|
775
|
+
## IDs
|
|
776
|
+
|
|
777
|
+
An ID must represent a single URL path segment.
|
|
778
|
+
|
|
779
|
+
The following are rejected:
|
|
780
|
+
|
|
781
|
+
- empty values
|
|
782
|
+
- `/`
|
|
783
|
+
- `#`
|
|
784
|
+
- `?`
|
|
785
|
+
- whitespace
|
|
786
|
+
- malformed percent-encoding
|
|
787
|
+
|
|
788
|
+
Values returned by `resolveId` are subject to the same rules.
|
|
789
|
+
|
|
790
|
+
## Permalinks
|
|
791
|
+
|
|
792
|
+
Permalinks and redirects must be site-local absolute paths.
|
|
793
|
+
|
|
794
|
+
The following are rejected:
|
|
795
|
+
|
|
796
|
+
- relative paths
|
|
797
|
+
- query strings
|
|
798
|
+
- fragments
|
|
799
|
+
- `\`
|
|
800
|
+
- invalid `//`
|
|
801
|
+
- external URLs
|
|
802
|
+
|
|
803
|
+
Values returned by `resolvePath` are subject to the same rules.
|
|
804
|
+
|
|
805
|
+
---
|
|
806
|
+
|
|
807
|
+
# Collision detection
|
|
808
|
+
|
|
809
|
+
The plugin detects conflicting public locations during the build.
|
|
810
|
+
|
|
811
|
+
This includes:
|
|
812
|
+
|
|
813
|
+
- ID ↔ ID
|
|
814
|
+
- canonical URL ↔ canonical URL
|
|
815
|
+
- canonical URL ↔ redirect
|
|
816
|
+
- redirect ↔ redirect
|
|
817
|
+
|
|
818
|
+
For example:
|
|
819
|
+
|
|
820
|
+
```text
|
|
821
|
+
a.md
|
|
822
|
+
→ /about
|
|
823
|
+
|
|
824
|
+
b.md
|
|
825
|
+
→ /about
|
|
826
|
+
```
|
|
827
|
+
|
|
828
|
+
fails the build.
|
|
829
|
+
|
|
830
|
+
The following also fails:
|
|
831
|
+
|
|
832
|
+
```text
|
|
833
|
+
a.md canonical
|
|
834
|
+
→ /about
|
|
835
|
+
|
|
836
|
+
b.md redirect
|
|
837
|
+
→ /about
|
|
838
|
+
```
|
|
839
|
+
|
|
840
|
+
The plugin does not silently append suffixes to resolve collisions.
|
|
841
|
+
|
|
842
|
+
This prevents public URLs from changing based on build or content ordering.
|
|
843
|
+
|
|
844
|
+
---
|
|
845
|
+
|
|
846
|
+
# Inspecting resolved URLs
|
|
847
|
+
|
|
848
|
+
Resolved values can be inspected through Riebeckite's existing content inspection command:
|
|
849
|
+
|
|
850
|
+
```sh
|
|
851
|
+
riebeckite inspect content --list
|
|
852
|
+
```
|
|
853
|
+
|
|
854
|
+
When the Permalink Plugin is enabled, the output can expose the resolved ID, ID source, and permalink for each entry.
|
|
855
|
+
|
|
856
|
+
For example:
|
|
857
|
+
|
|
858
|
+
```text
|
|
859
|
+
PATH ID ID SOURCE PERMALINK
|
|
860
|
+
notes/a.md K7m3Qp8d... derived /n/K7m3Qp8d...
|
|
861
|
+
notes/about.md about frontmatter /about
|
|
862
|
+
```
|
|
863
|
+
|
|
864
|
+
The exact output format may vary between CLI versions.
|
|
865
|
+
|
|
866
|
+
---
|
|
867
|
+
|
|
868
|
+
# Exports
|
|
869
|
+
|
|
870
|
+
## Functions
|
|
871
|
+
|
|
872
|
+
- `permalink(options?)`
|
|
873
|
+
- `permalinkPlugin(options?)`
|
|
874
|
+
|
|
875
|
+
Both create the Permalink Plugin.
|
|
876
|
+
|
|
877
|
+
## Types
|
|
878
|
+
|
|
879
|
+
- `PermalinkOptions`
|
|
880
|
+
- `PermalinkIdStrategy`
|
|
881
|
+
- `PermalinkPathMode`
|
|
882
|
+
- `RedirectStatus`
|
|
883
|
+
|
|
884
|
+
Use the exported types when building type-safe project-specific resolver configuration.
|
|
885
|
+
|
|
886
|
+
---
|
|
887
|
+
|
|
888
|
+
# Configuration examples
|
|
889
|
+
|
|
890
|
+
## Existing Obsidian vault
|
|
891
|
+
|
|
892
|
+
Hide the filesystem layout without requiring frontmatter changes across the vault:
|
|
893
|
+
|
|
894
|
+
```ts
|
|
895
|
+
permalink({
|
|
896
|
+
frontmatter: "id",
|
|
897
|
+
|
|
898
|
+
id: {
|
|
899
|
+
strategy: "frontmatter-or-hash",
|
|
900
|
+
length: 12,
|
|
901
|
+
},
|
|
902
|
+
|
|
903
|
+
path: {
|
|
904
|
+
mode: "flat",
|
|
905
|
+
prefix: "/n",
|
|
906
|
+
},
|
|
907
|
+
});
|
|
908
|
+
```
|
|
909
|
+
|
|
910
|
+
## Explicit permanent IDs
|
|
911
|
+
|
|
912
|
+
Require every entry to define its identity explicitly:
|
|
913
|
+
|
|
914
|
+
```ts
|
|
915
|
+
permalink({
|
|
916
|
+
frontmatter: "id",
|
|
917
|
+
|
|
918
|
+
id: {
|
|
919
|
+
strategy: "frontmatter",
|
|
920
|
+
},
|
|
921
|
+
|
|
922
|
+
path: {
|
|
923
|
+
mode: "flat",
|
|
924
|
+
prefix: "/n",
|
|
925
|
+
},
|
|
926
|
+
});
|
|
927
|
+
```
|
|
928
|
+
|
|
929
|
+
## Preserve directory structure
|
|
930
|
+
|
|
931
|
+
```ts
|
|
932
|
+
permalink({
|
|
933
|
+
id: {
|
|
934
|
+
strategy: "frontmatter-or-hash",
|
|
935
|
+
},
|
|
936
|
+
|
|
937
|
+
path: {
|
|
938
|
+
mode: "preserve",
|
|
939
|
+
prefix: "",
|
|
940
|
+
},
|
|
941
|
+
});
|
|
942
|
+
```
|
|
943
|
+
|
|
944
|
+
## Fully custom URL scheme
|
|
945
|
+
|
|
946
|
+
```ts
|
|
947
|
+
permalink({
|
|
948
|
+
resolveId(content) {
|
|
949
|
+
// Project-specific identity.
|
|
950
|
+
return "...";
|
|
951
|
+
},
|
|
952
|
+
|
|
953
|
+
resolvePath({ content, id }) {
|
|
954
|
+
// Project-specific public URL.
|
|
955
|
+
return `/articles/${id}`;
|
|
956
|
+
},
|
|
957
|
+
});
|
|
958
|
+
```
|
|
959
|
+
|
|
960
|
+
---
|
|
961
|
+
|
|
962
|
+
# See also
|
|
963
|
+
|
|
964
|
+
- [Plugin guide](../../../docs/en/reference/plugin-api.md)
|
|
965
|
+
- [Content system](../../../docs/en/framework/content-system.md)
|