scavold 0.2.0-rc.1

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.
Files changed (42) hide show
  1. package/COMPONENTS.md +862 -0
  2. package/FRONTMATTER.md +248 -0
  3. package/LICENSE +21 -0
  4. package/README.md +26 -0
  5. package/components/ScavoldArticle.vue +12 -0
  6. package/components/ScavoldAside.vue +12 -0
  7. package/components/ScavoldBreadcrumb.vue +36 -0
  8. package/components/ScavoldContainer.vue +16 -0
  9. package/components/ScavoldFooter.vue +12 -0
  10. package/components/ScavoldHeader.vue +12 -0
  11. package/components/ScavoldImage.vue +33 -0
  12. package/components/ScavoldLayout.vue +21 -0
  13. package/components/ScavoldLocaleMenu.vue +86 -0
  14. package/components/ScavoldLocaleRedirect.vue +47 -0
  15. package/components/ScavoldMain.vue +12 -0
  16. package/components/ScavoldMenu.vue +82 -0
  17. package/components/ScavoldMenuItems.vue +45 -0
  18. package/components/ScavoldNav.vue +12 -0
  19. package/components/ScavoldSection.vue +12 -0
  20. package/components/ScavoldSimpleRedirect.vue +35 -0
  21. package/components/ScavoldVideo.vue +74 -0
  22. package/composables/hierarchy.ts +391 -0
  23. package/composables/useContainer.js +59 -0
  24. package/composables/useI18n.js +37 -0
  25. package/composables/useRedirect.js +20 -0
  26. package/composables/useVideo.js +89 -0
  27. package/index.d.ts +43 -0
  28. package/l10n/de.json +6 -0
  29. package/l10n/en.json +6 -0
  30. package/lib/config.d.ts +17 -0
  31. package/lib/config.js +396 -0
  32. package/lib/containers.js +128 -0
  33. package/lib/index.d.ts +9 -0
  34. package/lib/index.js +60 -0
  35. package/lib/markdown.js +22 -0
  36. package/lib/media.js +231 -0
  37. package/lib/pages.js +494 -0
  38. package/lib/parser.js +83 -0
  39. package/lib/redirectTarget.js +46 -0
  40. package/lib/sectionManifest.js +200 -0
  41. package/package.json +86 -0
  42. package/scripts/check-csp.js +68 -0
package/COMPONENTS.md ADDED
@@ -0,0 +1,862 @@
1
+ # Scavold component and composable reference
2
+
3
+ All components are registered globally by `enhanceApp()` and are available in any
4
+ Markdown page or Vue template without explicit imports. Composables must be imported
5
+ explicitly.
6
+
7
+ TypeScript-typed props give inline documentation in VS Code (Volar extension) and
8
+ JetBrains IDEs — hover a prop in a template to see its description. This file covers
9
+ the broader "when and why" context that hover docs cannot convey.
10
+
11
+ ---
12
+
13
+ ## Components
14
+
15
+ ---
16
+
17
+ ### `<ScavoldMenu>`
18
+
19
+ Renders a `<nav>` element containing a nested list of page links derived from the
20
+ site's page hierarchy. Nothing is rendered when the resolved item set is empty.
21
+
22
+ The component has three independent addressing modes — path-based, absolute (from
23
+ root), and relative (from current page) — that cover all common navigation patterns
24
+ without requiring knowledge of the current page's depth.
25
+
26
+ #### Props
27
+
28
+ | Prop | Type | Default | Description |
29
+ |---|---|---|---|
30
+ | `from-path` | `string` | — | Select the parent node by path. Supports `{locale}` (full BCP 47 tag, e.g. `de-CH`) and `{lang}` (primary language subtag only, e.g. `de`) placeholders, replaced with the current page's locale at runtime. Takes priority over `from-root` and `from`. Examples: `"de/footer"`, `"{lang}/footer"`, `"{locale}/footer"`. |
31
+ | `from-root` | `number` | — | List items at this absolute depth from root. `1` = top-level pages, `2` = second level, etc. When set, `from` is ignored. If the current page has no ancestor at this depth, nothing is rendered. Ignored when `from-path` is set. |
32
+ | `from` | `number` | `0` | List items relative to the current page. `0` = siblings, `1` = children, `-1` = aunt/uncle level (children of grandparent), etc. Ignored when `from-root` or `from-path` is set. |
33
+ | `depth` | `number` | `0` | Additional levels to descend below the starting level. `0` = flat list, `1` = one level of children, `-1` = unlimited. |
34
+ | `active-only` | `boolean` | `false` | When `true`, only expands children along the branch leading to the current page. Useful with `depth > 0` to show sub-items under the active section only. |
35
+ | `expand` | `boolean` | `false` | When `true`, expands children of all nodes regardless of the active branch. Enables sitemap-style rendering. `active-only` takes precedence when both are set. |
36
+ | `label` | `string` | — | Accessible label for the `<nav>` landmark (`aria-label`). Should be set whenever more than one `<ScavoldMenu>` appears on the same page so screen readers can distinguish them (e.g. `"Main navigation"`, `"Section navigation"`). |
37
+
38
+ #### Rendered markup
39
+
40
+ ```html
41
+ <nav>
42
+ <ul>
43
+ <li class="active"> <!-- .active when node is current or an ancestor -->
44
+ <a href="/section/">Section</a>
45
+ <ul> <!-- nested only when children are included -->
46
+ <li class="current">
47
+ <a href="/section/page/">Page</a>
48
+ </li>
49
+ </ul>
50
+ </li>
51
+ </ul>
52
+ </nav>
53
+ ```
54
+
55
+ The `.active` class is set on any item that is an ancestor of the current page or the
56
+ current page itself. The `.current` class is set only on the item that exactly matches
57
+ the current page.
58
+
59
+ #### Label resolution
60
+
61
+ Item labels are resolved in this order:
62
+
63
+ 1. `label` frontmatter — explicit navigation text, overrides everything
64
+ 2. `title` frontmatter — page title, used when no `label` is set
65
+ 3. First `#` heading in the page body — extracted at build time as a fallback when neither `label` nor `title` is declared in frontmatter
66
+ 4. The last path segment of the page file (e.g. `about` from `de/about.md`) — last resort
67
+
68
+ #### Hiding pages via front matter
69
+
70
+ A page can opt out of appearing in menus by setting `hide` in its front matter:
71
+
72
+ | Value | Effect |
73
+ |---|---|
74
+ | `false` / absent | Visible everywhere (default) |
75
+ | `true` | Hidden in menus **and** breadcrumbs |
76
+ | `"menu"` | Hidden in menus only |
77
+ | `"breadcrumb"` | Hidden in breadcrumbs only |
78
+
79
+ ```yaml
80
+ ---
81
+ hide: menu
82
+ ---
83
+ ```
84
+
85
+ #### Examples
86
+
87
+ ```html
88
+ <!-- top-level pages — typical site header nav -->
89
+ <ScavoldMenu :from-root="1" />
90
+
91
+ <!-- second level of the active top-level section only -->
92
+ <ScavoldMenu :from-root="2" active-only />
93
+
94
+ <!-- children of the current page -->
95
+ <ScavoldMenu :from="1" />
96
+
97
+ <!-- siblings of the current page -->
98
+ <ScavoldMenu />
99
+
100
+ <!-- full site hierarchy as a sitemap -->
101
+ <ScavoldMenu :from-root="1" :depth="-1" />
102
+
103
+ <!-- two levels starting from the active top-level section, active branch expanded -->
104
+ <ScavoldMenu :from-root="1" :depth="1" active-only />
105
+
106
+ <!-- fixed path — same footer nav regardless of locale -->
107
+ <ScavoldMenu from-path="de/footer" label="Footer-Navigation" />
108
+
109
+ <!-- locale-aware via primary language tag — works when paths use "de", "en", etc. -->
110
+ <ScavoldMenu from-path="{lang}/footer" label="Footer-Navigation" />
111
+
112
+ <!-- locale-aware via full BCP 47 tag — use when paths include region codes like "de-CH" -->
113
+ <ScavoldMenu from-path="{locale}/footer" label="Footer-Navigation" />
114
+ ```
115
+
116
+ ---
117
+
118
+ ### `<ScavoldBreadcrumb>`
119
+
120
+ Renders a `<nav class="breadcrumb">` element containing a flat list of links
121
+ representing the path from the root to the current page. Uses the same
122
+ `<ul>/<li>/<a>` structure as `<ScavoldMenu>` so both components can share CSS.
123
+
124
+ Each item in the trail carries `.active` (all ancestor items) and `.current` (the
125
+ last item, if the current page is included). This makes it straightforward to style
126
+ the current crumb differently in CSS.
127
+
128
+ #### Props
129
+
130
+ | Prop | Type | Default | Description |
131
+ |---|---|---|---|
132
+ | `include-current` | `boolean` | `true` | Include the current page as the last crumb |
133
+ | `include-root` | `boolean` | `false` | Include the root node (typically "Home") as the first crumb |
134
+
135
+ #### Rendered markup
136
+
137
+ ```html
138
+ <!-- on page de/leistungen.md with include-root omitted -->
139
+ <nav class="breadcrumb">
140
+ <ul>
141
+ <li class="active">
142
+ <a href="/de/">Deutsch</a> <!-- de/ folder node, label from de/index.md -->
143
+ </li>
144
+ <li class="active current">
145
+ <a href="/de/leistungen/">Leistungen</a>
146
+ </li>
147
+ </ul>
148
+ </nav>
149
+ ```
150
+
151
+ #### Examples
152
+
153
+ ```html
154
+ <!-- typical usage — section trail + current page -->
155
+ <ScavoldBreadcrumb />
156
+
157
+ <!-- trail without current page, e.g. when page heading serves as final crumb -->
158
+ <ScavoldBreadcrumb :include-current="false" />
159
+
160
+ <!-- full trail including home link -->
161
+ <ScavoldBreadcrumb include-root />
162
+ ```
163
+
164
+ ---
165
+
166
+ ### `<ScavoldLocaleMenu>`
167
+
168
+ Renders a `<nav class="locale-nav">` element containing a list of links to the
169
+ available translations of the current page. Nothing is rendered when the resolved
170
+ link set is empty, or (by default) when only a single locale is available.
171
+
172
+ The component has three locale detection modes covering common multi-language
173
+ site patterns without requiring knowledge of the site structure from the author.
174
+
175
+ #### Props
176
+
177
+ | Prop | Type | Default | Description |
178
+ |---|---|---|---|
179
+ | `detection` | `"auto" \| "explicit" \| "inherited" \| "global"` | `"auto"` | How to discover available locales. See Detection modes below. |
180
+ | `include-current` | `boolean` | `false` | Include the current locale as a non-navigating item in the list. Useful when the switcher always shows the full set. |
181
+ | `hide-if-single` | `boolean` | `true` | Render nothing when only one (or zero) locale links are available after filtering. |
182
+ | `label` | `string` | `"Language"` (i18n) | Accessible label for the `<nav>` landmark (`aria-label`). |
183
+
184
+ #### Detection modes
185
+
186
+ | Mode | Description |
187
+ |---|---|
188
+ | `"auto"` | Tries `explicit`, then `inherited`, then `global`; uses the first mode that yields at least one link. Good default for sites with mixed translation coverage. |
189
+ | `"explicit"` | Only locales declared in the **current page's own** `translations` frontmatter. Nothing is shown for pages that have no `translations` entry. |
190
+ | `"inherited"` | For each locale, follows the ancestor chain upward and links to the translation declared by the **nearest ancestor** (including the current page) that has a `translations` entry for that locale. A section `index.md` can therefore declare `translations` once and cover all its descendants that have no direct counterpart in the other locale — the switcher will link to the section rather than showing nothing. |
191
+ | `"global"` | Every locale present **anywhere in the hierarchy**. Link goes to the topmost page of that locale regardless of `translations` declarations. Use when no per-page `translations` are maintained at all. |
192
+
193
+ #### Rendered markup
194
+
195
+ ```html
196
+ <nav class="locale-nav" aria-label="Language">
197
+ <ul>
198
+ <li lang="de">
199
+ <span aria-current="true">de</span> <!-- current locale: span not link -->
200
+ </li>
201
+ <li lang="en">
202
+ <a href="/en/" hreflang="en" aria-label="en">en</a>
203
+ </li>
204
+ </ul>
205
+ </nav>
206
+ ```
207
+
208
+ The `.current` class is set on the `<li>` of the current locale. The current locale
209
+ renders as a `<span>` rather than an `<a>` since navigating to the current page makes
210
+ no sense.
211
+
212
+ #### `translations` frontmatter
213
+
214
+ Detection modes `"explicit"` and `"inherited"` rely on a `translations` map in page
215
+ front matter:
216
+
217
+ ```yaml
218
+ ---
219
+ locale: de
220
+ translations:
221
+ en: en/about_us.md
222
+ ---
223
+ ```
224
+
225
+ Paths are relative to `pages_folder`.
226
+
227
+ For `"inherited"` mode the common pattern is to declare `translations` on a section
228
+ `index.md` rather than on every individual page. Pages in that section that have no
229
+ own `translations` entry will fall back to the section link:
230
+
231
+ ```
232
+ pages/
233
+ de/
234
+ products/
235
+ index.md ← declares translations: { en: en/products/index.md }
236
+ widget-a.md ← no translations — inherited mode links to en/products/
237
+ widget-b.md ← no translations — inherited mode links to en/products/
238
+ widget-c.md ← own translations: { en: en/products/widget-c.md } — links directly
239
+ en/
240
+ products/
241
+ index.md
242
+ widget-c.md ← only widget-c has an English counterpart
243
+ ```
244
+
245
+ #### Examples
246
+
247
+ ```html
248
+ <!-- simplest: show other locales declared on this page or any ancestor -->
249
+ <ScavoldLocaleMenu />
250
+
251
+ <!-- always show all locales; link to topmost page in each locale -->
252
+ <ScavoldLocaleMenu detection="global" />
253
+
254
+ <!-- show all including current locale -->
255
+ <ScavoldLocaleMenu detection="global" include-current />
256
+
257
+ <!-- explicit only; keep visible even when only one locale is listed -->
258
+ <ScavoldLocaleMenu detection="explicit" :hide-if-single="false" />
259
+ ```
260
+
261
+ ---
262
+
263
+ ### `<ScavoldLayout>`
264
+
265
+ Base layout wrapper that intercepts pages with locale-conditional redirect front
266
+ matter and renders `<ScavoldLocaleRedirect>` in their place. All other pages render
267
+ the default slot.
268
+
269
+ Consuming themes wrap their own layout root element in this component so they
270
+ automatically inherit redirect handling without duplicating the detection logic:
271
+
272
+ ```vue
273
+ <template>
274
+ <ScavoldLayout>
275
+ <div class="site-shell">
276
+ <!-- header, main, footer … -->
277
+ </div>
278
+ </ScavoldLayout>
279
+ </template>
280
+ ```
281
+
282
+ Themes that need finer control can skip `<ScavoldLayout>` and compose the two
283
+ building blocks themselves using `useRedirect` and `<ScavoldLocaleRedirect>`:
284
+
285
+ ```vue
286
+ <script setup>
287
+ import { useRedirect } from "./scavold/composables/useRedirect.js";
288
+ import ScavoldLocaleRedirect from "./scavold/components/ScavoldLocaleRedirect.vue";
289
+
290
+ const { isLocaleRedirect } = useRedirect();
291
+ </script>
292
+
293
+ <template>
294
+ <ScavoldLocaleRedirect v-if="isLocaleRedirect" />
295
+ <div v-else class="site-shell"><!-- … --></div>
296
+ </template>
297
+ ```
298
+
299
+ ---
300
+
301
+ ### `<ScavoldLocaleRedirect>`
302
+
303
+ Handles locale-conditional redirect pages declared with an object-form `redirect`
304
+ in front matter:
305
+
306
+ ```yaml
307
+ ---
308
+ redirect:
309
+ de: /de/
310
+ en: /en/
311
+ "*": /en/
312
+ ---
313
+ ```
314
+
315
+ On mount the component walks `navigator.languages` in order, trying an exact match
316
+ then a language-prefix match against the redirect map keys, and calls
317
+ `location.replace()` on the first hit. This replaces the redirect page in history
318
+ so the back button never loops back to it.
319
+
320
+ A `<noscript>` `<meta http-equiv="refresh">` fallback targets the `"*"` catch-all
321
+ for browsers without JavaScript.
322
+
323
+ The component renders no visible content of its own. It is used automatically by
324
+ `<ScavoldLayout>` and is not normally placed directly in templates.
325
+
326
+ ---
327
+
328
+ ### `<ScavoldMenuItems>`
329
+
330
+ Internal recursive component used by `<ScavoldMenu>` to render `<ul>/<li>` trees.
331
+ Not intended for direct use. Documented here for theme developers who want to build
332
+ their own menu component using `useHierarchy`.
333
+
334
+ ---
335
+
336
+ ### `<ScavoldVideo>`
337
+
338
+ Renders a `<video>` element for a video embedded via the `:::video` container block.
339
+ Nothing is rendered when no `src` argument is provided.
340
+
341
+ #### Markdown syntax
342
+
343
+ ```markdown
344
+ ::: video src=./clip.mp4 poster=./thumb.jpg
345
+ Optional caption or fallback text for browsers that do not support the video element.
346
+ :::
347
+ ```
348
+
349
+ #### Arguments
350
+
351
+ | Argument | Type | Default | Description |
352
+ |---|---|---|---|
353
+ | `src` | `string` | required | URL of the video file |
354
+ | `poster` | `string` | — | URL of the poster image shown before playback |
355
+ | `autoplay` | flag | — | Play automatically on load. Forces `muted` (browser requirement) |
356
+ | `loop` | flag | — | Loop the video when it ends |
357
+ | `muted` | flag | — | Mute the audio track |
358
+ | `overlay` | flag | — | Render the video as a background and lay the block's body content on top of it (hero/background mode). See below. |
359
+ | `controls` | flag | — | Show native playback controls. Always shown in the default player; opt-in in `overlay` mode, which is chrome-free by default. |
360
+ | `preload` | `"none" \| "metadata" \| "auto"` | `"metadata"` | Browser preload hint |
361
+ | `label` | `string` | — | Accessible label (`aria-label`) announced by screen readers. Use when the surrounding context does not already describe the video. |
362
+
363
+ Boolean flags are written without a value:
364
+
365
+ ```markdown
366
+ ::: video src=./clip.mp4 autoplay loop
367
+ :::
368
+ ```
369
+
370
+ #### Rendered markup
371
+
372
+ ```html
373
+ <video src="..." poster="..." preload="metadata" controls playsinline>
374
+ <!-- slot content -->
375
+ </video>
376
+ ```
377
+
378
+ The `controls` and `playsinline` attributes are always present. `autoplay`, `loop`,
379
+ `muted`, and `poster` are only emitted when the corresponding argument is set.
380
+
381
+ #### Background mode (`overlay`)
382
+
383
+ With the `overlay` flag the video becomes a background and the block's body content
384
+ is layered on top of it — the classic hero pattern. The video is chrome-free by
385
+ default (add `controls` to bring the native controls back), and a typical hero
386
+ combines `overlay` with `autoplay` + `loop` (both imply/allow `muted`):
387
+
388
+ ```markdown
389
+ ::: video src=./hero.mp4 poster=./hero.jpg overlay autoplay loop
390
+ # Welcome
391
+ Content rendered on top of the looping background video.
392
+ :::
393
+ ```
394
+
395
+ ```html
396
+ <div class="scavold-video scavold-video--overlay">
397
+ <video class="scavold-video__media" src="..." poster="..." autoplay loop muted
398
+ preload="metadata" playsinline></video>
399
+ <div class="scavold-video__overlay">
400
+ <!-- slot content -->
401
+ </div>
402
+ </div>
403
+ ```
404
+
405
+ The component only ships **functional** layout CSS (the video and the content share
406
+ one CSS-grid cell, so the content defines the block height and the video covers the
407
+ area behind it via `object-fit: cover`). All visual styling — a darkening scrim,
408
+ text colour, alignment, a `min-height` for the hero — belongs to the consuming theme,
409
+ which can hook onto the `.scavold-video`, `.scavold-video__media`, and
410
+ `.scavold-video__overlay` classes:
411
+
412
+ ```css
413
+ /* theme CSS */
414
+ .scavold-video--overlay { min-height: 60vh; }
415
+ .scavold-video__overlay {
416
+ display: grid;
417
+ place-content: center;
418
+ padding: 2rem;
419
+ color: white;
420
+ background: rgba( 0, 0, 0, 0.4 ); /* scrim for legibility */
421
+ }
422
+ ```
423
+
424
+ Without the `overlay` flag the block behaves exactly as before: a plain inline
425
+ player whose body content is only fallback text for browsers that cannot play video.
426
+
427
+ #### Captions and accessibility
428
+
429
+ WCAG requires captions for prerecorded video with audio. Add a `<track>` element via
430
+ the slot:
431
+
432
+ ```markdown
433
+ ::: video src=./clip.mp4
434
+ <track kind="captions" src="./clip.vtt" srclang="de" label="Deutsch" default />
435
+ :::
436
+ ```
437
+
438
+ The `label` argument provides an `aria-label` on the `<video>` element for cases where
439
+ the surrounding page context does not already describe the video.
440
+
441
+ #### Replacing with a custom component
442
+
443
+ Register a component under the name `ScavoldVideo` before calling `scavoldEnhanceApp`,
444
+ or map the `video` container name to your component in `augmentConfig`:
445
+
446
+ ```js
447
+ export default defineConfig( await augmentConfig( config, {
448
+ containers: { video: "MyVideoPlayer" },
449
+ } ) );
450
+ ```
451
+
452
+ Custom components can reuse the resolution logic via `useVideo` and `videoProps`:
453
+
454
+ ```js
455
+ import { useVideo, videoProps } from "./scavold/composables/useVideo.js";
456
+
457
+ const props = defineProps( { ...videoProps } );
458
+ const { src, poster, autoplay, loop, muted, preload, overlay, controls } = useVideo( props );
459
+ ```
460
+
461
+ ---
462
+
463
+ ### `<ScavoldImage>`
464
+
465
+ Renders a responsive `<picture>` element for local images. Not intended for direct
466
+ use in templates — it is emitted automatically by the Markdown image renderer when a
467
+ local image path is encountered. Custom components that need to display a media-file
468
+ frontmatter field can use it directly.
469
+
470
+ #### Props
471
+
472
+ | Prop | Type | Default | Description |
473
+ |---|---|---|---|
474
+ | `src` | `string` | required | URL of the largest fallback image variant |
475
+ | `srcset` | `string` | `""` | `srcset` string for the original format (JPEG/PNG) |
476
+ | `webp-srcset` | `string` | `""` | `srcset` string for the WebP variants |
477
+ | `sizes` | `string` | `"100vw"` | CSS `sizes` attribute applied to all sources |
478
+ | `alt` | `string` | `""` | Alt text for the `<img>` element |
479
+
480
+ #### Rendered markup
481
+
482
+ ```html
483
+ <picture>
484
+ <source type="image/webp" srcset="..." sizes="..." />
485
+ <source srcset="..." sizes="..." />
486
+ <img src="..." alt="..." sizes="..." loading="lazy" decoding="async" />
487
+ </picture>
488
+ ```
489
+
490
+ ---
491
+
492
+ ### `<ScavoldContainer>`
493
+
494
+ Fallback component for Markdown container blocks whose name has no dedicated
495
+ registered component. Renders as the matching HTML sectioning element if the
496
+ container name is one (`section`, `aside`, etc.), otherwise as a `<div>`.
497
+
498
+ Not intended for direct use. Theme developers should instead create a dedicated
499
+ component for each container name they declare in `.cratly.config.yaml`.
500
+
501
+ See `useContainer` below for building custom container components.
502
+
503
+ ---
504
+
505
+ ### `<ScavoldSection>`, `<ScavoldAside>`, `<ScavoldArticle>`, `<ScavoldHeader>`, `<ScavoldFooter>`, `<ScavoldNav>`, `<ScavoldMain>`
506
+
507
+ Thin wrapper components for the seven HTML sectioning elements. Each renders its
508
+ corresponding element, forwarding `class` and `data-*` attributes from the container
509
+ arguments.
510
+
511
+ These are the default targets for their respective container names. A theme developer
512
+ can replace any of them by registering a component with the same name before calling
513
+ `scavoldEnhanceApp`, or by passing an explicit override to `augmentConfig`:
514
+
515
+ ```js
516
+ // .vitepress/config.js
517
+ export default defineConfig( await augmentConfig( config, {
518
+ containers: {
519
+ section: "MySiteSection",
520
+ }
521
+ } ) );
522
+ ```
523
+
524
+ ```js
525
+ // .vitepress/theme/index.js
526
+ async function enhanceApp( context ) {
527
+ app.component( "ScavoldSection", MySiteSection ); // registered before scavold
528
+ await scavoldEnhanceApp( context );
529
+ }
530
+ ```
531
+
532
+ ---
533
+
534
+ ## Composables
535
+
536
+ ---
537
+
538
+ ### `useHierarchy()`
539
+
540
+ ```js
541
+ import { useHierarchy } from "./scavold/composables/hierarchy";
542
+ ```
543
+
544
+ Provides access to the full page hierarchy tree and utilities for traversing it.
545
+ Must be called inside a component's `setup` function (or `<script setup>`).
546
+
547
+ #### Returns
548
+
549
+ | Name | Type | Description |
550
+ |---|---|---|
551
+ | `hierarchy` | `ComputedRef<HierarchyNode>` | Root node of the full page hierarchy tree |
552
+ | `current` | `ComputedRef<HierarchyNode \| undefined>` | Node for the currently viewed page |
553
+ | `ancestorAtDepth` | `(node, targetDepth) => HierarchyNode \| undefined` | Returns the ancestor of `node` at `targetDepth` levels below root. Used by `ScavoldMenu` for `from-root` mode. Returns `undefined` if `node` is not deep enough. |
554
+ | `isOnActivePath` | `(node) => boolean` | Returns `true` if `node` is the current page or one of its ancestors |
555
+ | `collectItems` | `(parent, depth, activeOnly, expand) => MenuItem[]` | Recursively collects menu items from `parent`'s children. Used internally by `ScavoldMenu`. |
556
+ | `collectAncestors` | `(includeCurrent, includeRoot) => MenuItem[]` | Returns the ancestor chain from root to the current page as a flat `MenuItem` array. Used internally by `ScavoldBreadcrumb`. |
557
+ | `currentLocale` | `ComputedRef<string>` | The resolved locale for the current page. Falls back to the browser locale when no page or ancestor declares one. |
558
+ | `collectLocaleLinks` | `(detection, includeCurrent?) => LocaleLink[]` | Returns available locale links for the current page. See `ScavoldLocaleMenu` for detection mode details. |
559
+ | `resolveByPath` | `(rawPath: string) => HierarchyNode \| undefined` | Resolves a path string to a hierarchy node. Supports `{locale}` (full BCP 47 tag) and `{lang}` (primary language subtag) placeholders. Tries the interpolated path as-is, then with `/index.md`, then with `.md` appended. |
560
+
561
+ #### `HierarchyNode` properties
562
+
563
+ | Property | Type | Description |
564
+ |---|---|---|
565
+ | `path` | `string` | Relative path of the Markdown source file |
566
+ | `isPage` | `boolean` | `true` for actual pages, `false` for intermediate folder nodes |
567
+ | `frontmatter` | `object` | Parsed front matter data |
568
+ | `frontmatter.hide` | `boolean \| "menu" \| "breadcrumb"` | Excludes this node from menus, breadcrumbs, or both. See [Hiding pages via front matter](#hiding-pages-via-front-matter). |
569
+ | `frontmatter.order` | `number?` | Sort position among siblings. Pages with a lower value appear first; pages without `order` follow in filename order. |
570
+ | `frontmatter.url` | `string?` | Alias output path. When set, VitePress builds the page at this URL instead of the path derived from the source file. Conflicts (two pages with the same alias) are a build error. |
571
+ | `title` | `string?` | Display title — from frontmatter `title` or the first `#` heading |
572
+ | `label` | `string?` | Navigation label — from frontmatter `label`; falls back to `title` in menus |
573
+ | `url` | `string?` | Normalised alias output path (e.g. `de/impressum.md`) when `frontmatter.url` is set. Used by `nodeHref` as the page's href. |
574
+ | `locale` | `string?` | Resolved locale (BCP 47), inherited from ancestors |
575
+ | `parent` | `HierarchyNode?` | Parent node — non-enumerable, not serialised to JSON |
576
+ | `subs` | `object?` | Child nodes keyed by path segment, sorted by `order` then filename |
577
+
578
+ #### `MenuItem` properties
579
+
580
+ | Property | Type | Description |
581
+ |---|---|---|
582
+ | `node` | `HierarchyNode` | The hierarchy node this item represents |
583
+ | `active` | `boolean` | `true` if this node is the current page or an ancestor of it |
584
+ | `current` | `boolean` | `true` if this node is exactly the current page |
585
+ | `children` | `MenuItem[]` | Nested items, populated according to `depth` / `activeOnly` / `expand` |
586
+
587
+ #### `LocaleLink` properties
588
+
589
+ | Property | Type | Description |
590
+ |---|---|---|
591
+ | `locale` | `string` | BCP 47 locale code |
592
+ | `href` | `string` | Root-relative URL for this locale |
593
+ | `current` | `boolean` | `true` if this locale matches the current page's locale |
594
+
595
+ #### Example — custom menu component
596
+
597
+ ```vue
598
+ <script setup>
599
+ import { useHierarchy } from "./scavold/composables/hierarchy";
600
+
601
+ const { current, collectItems } = useHierarchy();
602
+
603
+ // Children of current page, unlimited depth, active branch expanded
604
+ const items = computed( () =>
605
+ collectItems( current.value, -1, true, false )
606
+ );
607
+ </script>
608
+ ```
609
+
610
+ ---
611
+
612
+ ### `useVideo( props )`
613
+
614
+ ```js
615
+ import { useVideo, videoProps } from "./scavold/composables/useVideo.js";
616
+ ```
617
+
618
+ Composable for custom video components. Resolves the `:::video` container props into
619
+ clean, typed values ready for template binding. `autoplay` automatically forces `muted`
620
+ to satisfy browser autoplay policies.
621
+
622
+ #### `videoProps`
623
+
624
+ Spread into `defineProps` to declare all video-related props at once.
625
+
626
+ | Prop | Argument | Description |
627
+ |---|---|---|
628
+ | `dataSrc` | `src=…` | Video file URL |
629
+ | `dataPoster` | `poster=…` | Poster image URL |
630
+ | `dataAutoplay` | `autoplay` | Present when the `autoplay` flag is set |
631
+ | `dataLoop` | `loop` | Present when the `loop` flag is set |
632
+ | `dataMuted` | `muted` | Present when the `muted` flag is set |
633
+ | `dataOverlay` | `overlay` | Present when the `overlay` flag is set (background mode) |
634
+ | `dataControls` | `controls` | Present when the `controls` flag is set |
635
+ | `dataPreload` | `preload=…` | One of `"none"`, `"metadata"`, `"auto"` |
636
+
637
+ #### Returns
638
+
639
+ | Name | Type | Description |
640
+ |---|---|---|
641
+ | `src` | `ComputedRef<string>` | Video URL |
642
+ | `poster` | `ComputedRef<string>` | Poster URL, empty string if absent |
643
+ | `autoplay` | `ComputedRef<boolean>` | `true` when `autoplay` flag is set |
644
+ | `loop` | `ComputedRef<boolean>` | `true` when `loop` flag is set |
645
+ | `muted` | `ComputedRef<boolean>` | `true` when `muted` flag is set or `autoplay` is true |
646
+ | `overlay` | `ComputedRef<boolean>` | `true` when the `overlay` flag is set (render as background) |
647
+ | `controls` | `ComputedRef<boolean>` | `true` in the default player; in overlay mode only when the `controls` flag is set |
648
+ | `preload` | `ComputedRef<string>` | Resolved preload value, defaults to `"metadata"` |
649
+
650
+ ---
651
+
652
+ ### `useContainer( props )`
653
+
654
+ ```js
655
+ import { useContainer, containerProps } from "./scavold/composables/useContainer.js";
656
+ ```
657
+
658
+ Shared composable for custom container components. Parses the props emitted by the
659
+ Markdown container renderer and exposes derived values ready for template binding.
660
+
661
+ #### Usage
662
+
663
+ ```vue
664
+ <script setup>
665
+ import { useContainer, containerProps } from "./scavold/composables/useContainer.js";
666
+
667
+ const props = defineProps( {
668
+ ...containerProps,
669
+ background: String, // custom KV argument: background=/media/hero.jpg
670
+ } );
671
+
672
+ const { containerName, classes, dataAttrs } = useContainer( props );
673
+ </script>
674
+
675
+ <template>
676
+ <section
677
+ :class="[containerName, classes]"
678
+ :style="background ? `--bg: url(${background})` : ''"
679
+ v-bind="dataAttrs"
680
+ >
681
+ <slot />
682
+ </section>
683
+ </template>
684
+ ```
685
+
686
+ #### `containerProps`
687
+
688
+ The shared props definition every container component must declare. Spread it into
689
+ `defineProps` to avoid duplicating the base prop declarations.
690
+
691
+ | Prop | Type | Description |
692
+ |---|---|---|
693
+ | `class` | `string` | Space-separated boolean flags from the container opening line |
694
+ | `data-container` | `string` | The container name as written in Markdown |
695
+
696
+ All `key=value` pairs from the opening line are passed as additional props with a
697
+ `data-` prefix (e.g. `background=/img.jpg` → prop `data-background`). Declare them
698
+ explicitly in `defineProps` to use them.
699
+
700
+ #### Returns
701
+
702
+ | Name | Type | Description |
703
+ |---|---|---|
704
+ | `containerName` | `ComputedRef<string>` | The container name (value of `data-container`) |
705
+ | `rootTag` | `ComputedRef<string>` | `containerName` if it is a sectioning element, otherwise `"div"` |
706
+ | `classes` | `ComputedRef<string>` | The `class` prop value |
707
+ | `dataAttrs` | `ComputedRef<object>` | All `data-*` props as a plain object, suitable for `v-bind` |
708
+
709
+ #### Markdown syntax reminder
710
+
711
+ ```markdown
712
+ ::: hero dark centered background=/media/hero.jpg
713
+ Content here.
714
+ :::
715
+ ```
716
+
717
+ This emits `class="dark centered"`, `data-container="hero"`, and
718
+ `data-background="/media/hero.jpg"` onto the component.
719
+
720
+ ---
721
+
722
+ ### `useRedirect()`
723
+
724
+ ```js
725
+ import { useRedirect } from "./scavold/composables/useRedirect.js";
726
+ ```
727
+
728
+ Provides redirect-related state derived from the current page's front matter.
729
+ Use this when building a custom layout that needs to handle locale-conditional
730
+ redirects without wrapping in `<ScavoldLayout>`.
731
+
732
+ #### Returns
733
+
734
+ | Name | Type | Description |
735
+ |---|---|---|
736
+ | `isLocaleRedirect` | `ComputedRef<boolean>` | `true` when the current page's `redirect` front matter is an object (locale-conditional redirect), `false` otherwise |
737
+
738
+ #### Example
739
+
740
+ ```vue
741
+ <script setup>
742
+ import { useRedirect } from "./scavold/composables/useRedirect.js";
743
+ import ScavoldLocaleRedirect from "./scavold/components/ScavoldLocaleRedirect.vue";
744
+
745
+ const { isLocaleRedirect } = useRedirect();
746
+ </script>
747
+
748
+ <template>
749
+ <ScavoldLocaleRedirect v-if="isLocaleRedirect" />
750
+ <div v-else class="my-layout"><!-- … --></div>
751
+ </template>
752
+ ```
753
+
754
+ ---
755
+
756
+ ### `useScavoldI18n()` / `registerScavoldI18n()`
757
+
758
+ ```js
759
+ import { useScavoldI18n } from "./scavold/composables/useI18n.js";
760
+ ```
761
+
762
+ Scavold uses [@cepharum/vue3-i18n](https://www.npmjs.com/package/@cepharum/vue3-i18n) for all
763
+ framework-owned UI strings (e.g. the breadcrumb `aria-label`). Translations live in
764
+ the `@scavold` namespace so they never collide with a consuming theme's own keys.
765
+
766
+ `registerScavoldI18n()` is called automatically by `scavoldEnhanceApp` — theme
767
+ developers do not need to call it manually.
768
+
769
+ #### `useScavoldI18n()` — for custom Scavold components
770
+
771
+ Returns a `t()` helper that looks up keys in the `@scavold` namespace:
772
+
773
+ ```js
774
+ const { t } = useScavoldI18n();
775
+ const label = t("nav.breadcrumb"); // ComputedRef<string>, reactive to locale changes
776
+ ```
777
+
778
+ #### Built-in translation keys
779
+
780
+ | Key | Default (en) | Description |
781
+ |---|---|---|
782
+ | `@scavold.nav.breadcrumb` | `"Breadcrumb"` | `aria-label` on the breadcrumb `<nav>` |
783
+
784
+ #### Providing translations in your theme
785
+
786
+ Call `useL10n().setLoader()` in your `enhanceApp` as usual. To override a Scavold
787
+ key, include it under `@scavold.*` in your translation files:
788
+
789
+ ```js
790
+ // .vitepress/theme/index.js
791
+ import { useL10n } from "@cepharum/vue3-i18n";
792
+
793
+ async function enhanceApp( context ) {
794
+ await scavoldEnhanceApp( context );
795
+
796
+ useL10n().setLoader( locale =>
797
+ import( `./l10n/${locale}.json` )
798
+ );
799
+ }
800
+ ```
801
+
802
+ ```json
803
+ // .vitepress/theme/l10n/de.json
804
+ {
805
+ "@scavold": {
806
+ "nav": {
807
+ "breadcrumb": "Seitennavigation"
808
+ }
809
+ },
810
+ "MY_APP": {
811
+ "title": "Willkommen"
812
+ }
813
+ }
814
+ ```
815
+
816
+ The default loader runs last and wins over the `@scavold` namespace loader, so any
817
+ key placed under `@scavold` in the theme's own translation file silently overrides
818
+ the Scavold default.
819
+
820
+ ---
821
+
822
+ ## Design patterns
823
+
824
+ ---
825
+
826
+ ### Responsive images — let components own `sizes`, not content authors
827
+
828
+ The `sizes` attribute on `<ScavoldImage>` (and the global `image_sizes` default in
829
+ `.cratly.config.yaml`) is a **design concern**, not a content concern. It describes
830
+ how large an image is rendered in a specific layout context — something only the
831
+ theme developer knows. Content authors should never need to set or even see it.
832
+
833
+ **Do not** rely on the global `image_sizes` default to cover all cases. A value
834
+ tuned for prose (e.g. `(min-width: 40rem) 560px, calc(100vw - 3rem)`) will cause
835
+ the browser to fetch undersized variants for full-width hero images, and vice versa.
836
+
837
+ **Do** build layout-aware wrapper components that hardcode the correct `sizes` for
838
+ their context and pass it through to `<ScavoldImage>`:
839
+
840
+ ```vue
841
+ <!-- HeroImage.vue — always full-width -->
842
+ <ScavoldImage :src="src" :srcset="srcset" sizes="100vw" />
843
+
844
+ <!-- ThumbImage.vue — fixed grid cell -->
845
+ <ScavoldImage :src="src" :srcset="srcset" sizes="(min-width: 60rem) 320px, 50vw" />
846
+ ```
847
+
848
+ Content authors pick the component (`HeroImage`, `ThumbImage`, etc.) that matches
849
+ their intent. The `sizes` value is baked into the component and invisible to them.
850
+
851
+ **Why not measure at runtime?** A `ResizeObserver`-based approach that reads the
852
+ container's pixel size and sets `src` dynamically is accurate but has serious
853
+ downsides: it causes layout shift (no dimensions known during SSR, space cannot be
854
+ reserved), loses browser preload scanning (images load later), and produces a flash
855
+ of empty space on every navigation during hydration. The `sizes` + `srcset` approach
856
+ was specifically designed to avoid all of these.
857
+
858
+ The global `image_sizes` in `.cratly.config.yaml` is a fallback for images that have
859
+ no layout-aware component wrapping them. Set it to the most common case in the
860
+ theme (typically the prose column width) and override via dedicated components for
861
+ everything else.
862
+