1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
//! Pure value types describing a site to be rendered.
//!
//! Nothing here opens, resolves or allocates anything on disk. These are the
//! *description* a caller hands the renderer, which is why the same description
//! can be assembled by a CLI, by a server, or by an edge worker.
//!
//! Appearance types (colors, typography, favicon, theme) live in
//! [`crate::appearance`].
use ;
/// Options for publishing.
/// Which shell a page is wrapped in, from its frontmatter `layout:`.
/// A document that frames every page — a site's header or footer — as the
/// text of the file and the path it was read from.
///
/// The path is load-bearing twice over: its extension decides the grammar the
/// text is parsed in, and relative links in the text resolve against it.
///
/// Here rather than in [`crate::site`], which consumes it, because the caller
/// that *assembles* one has only read a file: `plates` builds a `FrameDoc` from
/// a vault without enabling `templating`, and the type it hands over cannot
/// live behind a feature it does not turn on.
/// A navigation link.
/// A processed file ready for publishing.
/// One heading of a rendered body, as the outline lists it.
/// One node of a site's **spanning outline**: the archive's own containment
/// hierarchy, materialized by whoever holds the workspace.
///
/// A vault's spine is configured, not spelled: prov's `spanning:` names the
/// relation whose links contain, and `contents:`/`part_of:` is one vault
/// dialect's spelling of it. This crate cannot read a workspace's configuration
/// — it reads nothing — so the layer that can walks the tree and hands the
/// result down as plain data. See [`SiteOptions::outline`](crate::site::SiteOptions::outline).
///
/// [`path`](Self::path) is the source path in the coordinates
/// [`SourceDoc::path`](crate::site::SourceDoc::path) is written in: rebased onto
/// the site's anchor, sanitized, carrying the body's own extension. That is what
/// lets a node be matched to the page it became without either side re-deriving
/// the other's naming rule.
///
/// A node naming a document this site does not publish is not an error and not a
/// nav entry — it is pruned, and its published descendants hoist to the nearest
/// ancestor that *is* published. Under explicit-only visibility that is the
/// ordinary shape, not the edge case.
/// One link between two documents, named by the relation that carries it.
///
/// A vault **declares its own relations** — `sequel`, `translation_of`,
/// `author`, whatever its configuration says — so the name is data, never
/// something this crate knows. Nothing here may hardcode a vocabulary: whatever
/// names arrive are the names a template can address.
///
/// [`relation`](Self::relation) is `None` for a link written in prose, which has
/// no name to be filed under. Those reach a template through `backlinks`, the
/// flat union, and nowhere else — a reserved key for them would collide with a
/// relation a vault is entitled to declare.
///
/// [`path`](Self::path) is the document at the far end, spelled as
/// [`SourceDoc::path`](crate::site::SourceDoc::path) spells it — the same
/// coordinates, so an edge can be matched to the page it names without either
/// side re-deriving the other's naming rule.
/// A node in the full site navigation tree.
/// Full site navigation context for a specific page.
/// Result of a publishing operation.
/// What a grouped arrangement sorts entries into groups by.
///
/// prov's own, not a mirror of it. This used to be a redeclaration — the crate
/// sits below the workspace layer and must stay portable to
/// `wasm32-unknown-unknown`, so it kept its own `DateGrain` and `Grouping` with
/// the spellings and prefix lengths copied across, on the reasoning that a site
/// grouped "by year" must cut dates the same way the app's lens does or the
/// published archive reads differently from the vault it came from.
///
/// Since prov 0.5 the grouping engine is `prov-views`, which reaches nothing
/// that can write and is already in this crate's dependency graph. So the way to
/// keep the two identical is to stop having two: the published site now groups
/// through the same [`Grouping::keys_of`] the vault does, and "identical" is a
/// fact rather than a promise two copies make to each other.
pub use ;
/// How a site is arranged — the render-side half of a site's `view:`.
/// Normalize a frontmatter `serve_at:` value into a path below the site root,
/// or `None` when it claims nothing this crate can serve.
///
/// The value is **site-root-absolute** and must start with `/`. That is what
/// makes it a claim on the site's own layout rather than on the directory the
/// document happens to sit in — and why, unlike a derived destination, it is
/// never rebased onto a site's anchor: it is already written in the
/// coordinates a rebasing would produce.
///
/// `/privacy` and `/privacy.html` are the same claim: a value that does not
/// already end in `.html` gains it, because what is being named is a page and a
/// page is an HTML file. Components are sanitized the way every other published
/// path is, and `.`/`..` are dropped rather than resolved — a destination is a
/// name *inside* the site, and there is nothing above the site root to reach.
/// Convert a canonical source path to its sanitized `.html` output filename.
///
/// Public because a caller that must know where a source's HTML lands *before*
/// rendering it has no other way to ask: `build_pages` applies this same rule
/// internally, and re-deriving it elsewhere is how the two drift apart. It is
/// also what `plates`'s collection calls, so a site's uploaded keys and its
/// rendered links come from one function rather than from two that agree.
///
/// Ordinarily the extension is swapped and nothing else moves:
/// `notes/post.md` publishes at `notes/post.html`, in any content format.
///
/// # A folder note publishes as its directory's index
///
/// A source whose file stem is the name of the directory holding it —
/// `page/page.md`, `2026/2026.dj`, `about/about.html` — is that directory's
/// own note, the same document an `about/index.md` would be. The two spellings
/// are interchangeable across note-taking tools, and only one of them used to
/// land on `about/index.html`; the other published at `about/about.html` and
/// left the directory with no index at all, so a reader who asked for
/// `about/` got nothing. Both now publish at `<dir>/index.html`.
///
/// `index.md` needs no case of its own here and never did: swapping its
/// extension already yields `index.html`. This is the same destination reached
/// by the other spelling, which is exactly why the two cannot both be used in
/// one directory — `page/page.md` and `page/index.md` side by side claim
/// `page/index.html` twice. Collection refuses that pair by name
/// (`plates`'s `DestinationClaimedTwice`); nothing is reported here, because
/// this function sees one path at a time and has no second one to name.
///
/// A file with no directory above it is nobody's folder note: `page.md` at the
/// site root publishes at `page.html`. The comparison is against the immediate
/// directory only, so `notes/page.md` is untouched.