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.
- package/COMPONENTS.md +862 -0
- package/FRONTMATTER.md +248 -0
- package/LICENSE +21 -0
- package/README.md +26 -0
- package/components/ScavoldArticle.vue +12 -0
- package/components/ScavoldAside.vue +12 -0
- package/components/ScavoldBreadcrumb.vue +36 -0
- package/components/ScavoldContainer.vue +16 -0
- package/components/ScavoldFooter.vue +12 -0
- package/components/ScavoldHeader.vue +12 -0
- package/components/ScavoldImage.vue +33 -0
- package/components/ScavoldLayout.vue +21 -0
- package/components/ScavoldLocaleMenu.vue +86 -0
- package/components/ScavoldLocaleRedirect.vue +47 -0
- package/components/ScavoldMain.vue +12 -0
- package/components/ScavoldMenu.vue +82 -0
- package/components/ScavoldMenuItems.vue +45 -0
- package/components/ScavoldNav.vue +12 -0
- package/components/ScavoldSection.vue +12 -0
- package/components/ScavoldSimpleRedirect.vue +35 -0
- package/components/ScavoldVideo.vue +74 -0
- package/composables/hierarchy.ts +391 -0
- package/composables/useContainer.js +59 -0
- package/composables/useI18n.js +37 -0
- package/composables/useRedirect.js +20 -0
- package/composables/useVideo.js +89 -0
- package/index.d.ts +43 -0
- package/l10n/de.json +6 -0
- package/l10n/en.json +6 -0
- package/lib/config.d.ts +17 -0
- package/lib/config.js +396 -0
- package/lib/containers.js +128 -0
- package/lib/index.d.ts +9 -0
- package/lib/index.js +60 -0
- package/lib/markdown.js +22 -0
- package/lib/media.js +231 -0
- package/lib/pages.js +494 -0
- package/lib/parser.js +83 -0
- package/lib/redirectTarget.js +46 -0
- package/lib/sectionManifest.js +200 -0
- package/package.json +86 -0
- 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
|
+
|