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/FRONTMATTER.md
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
# Scavold front matter reference
|
|
2
|
+
|
|
3
|
+
Front matter is YAML declared at the top of a Markdown file between `---` fences.
|
|
4
|
+
Scavold reads a set of well-known keys from every page's front matter at build time
|
|
5
|
+
to drive navigation, localisation, redirects, and URL aliasing. All keys are
|
|
6
|
+
optional unless noted otherwise.
|
|
7
|
+
|
|
8
|
+
```yaml
|
|
9
|
+
---
|
|
10
|
+
title: My page title
|
|
11
|
+
label: Short label
|
|
12
|
+
order: 10
|
|
13
|
+
locale: de
|
|
14
|
+
---
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## `title`
|
|
20
|
+
|
|
21
|
+
**Type:** `string`
|
|
22
|
+
|
|
23
|
+
The display title for the page. Used in menus and breadcrumbs when no `label` is
|
|
24
|
+
set, and exposed as `node.title` on the hierarchy node.
|
|
25
|
+
|
|
26
|
+
When absent, Scavold falls back to the first `#` heading found in the page
|
|
27
|
+
body, then to the path segment if no heading exists either.
|
|
28
|
+
|
|
29
|
+
```yaml
|
|
30
|
+
---
|
|
31
|
+
title: Datenschutzerklärung
|
|
32
|
+
---
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## `label`
|
|
38
|
+
|
|
39
|
+
**Type:** `string`
|
|
40
|
+
|
|
41
|
+
A short navigation label used in menus and breadcrumbs in place of `title`. Useful
|
|
42
|
+
when the full page title is too long for a navigation item.
|
|
43
|
+
|
|
44
|
+
When absent, menus fall back to `title`, then to the path segment.
|
|
45
|
+
|
|
46
|
+
```yaml
|
|
47
|
+
---
|
|
48
|
+
title: Allgemeine Geschäftsbedingungen
|
|
49
|
+
label: AGB
|
|
50
|
+
---
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## `order`
|
|
56
|
+
|
|
57
|
+
**Type:** `number`
|
|
58
|
+
|
|
59
|
+
Controls the position of this page among its siblings in the hierarchy. Pages are
|
|
60
|
+
sorted by `order` ascending; pages without an `order` value follow in filename order
|
|
61
|
+
after all ordered pages.
|
|
62
|
+
|
|
63
|
+
Using multiples of 10 leaves room to insert pages later without renumbering
|
|
64
|
+
existing ones.
|
|
65
|
+
|
|
66
|
+
```yaml
|
|
67
|
+
---
|
|
68
|
+
order: 20
|
|
69
|
+
---
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
A section with mixed ordered and unordered pages:
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
10 → kontakt.md
|
|
76
|
+
20 → leistungen/
|
|
77
|
+
(no order) → agb.md # follows after ordered pages, a–z
|
|
78
|
+
(no order) → impressum.md
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## `url`
|
|
84
|
+
|
|
85
|
+
**Type:** `string`
|
|
86
|
+
|
|
87
|
+
Overrides the public URL of the page. VitePress builds the page at this path
|
|
88
|
+
instead of the URL derived from the source file location. The source path never
|
|
89
|
+
appears in the browser's address bar.
|
|
90
|
+
|
|
91
|
+
The value is a root-relative path without a leading slash. The `.md` extension is
|
|
92
|
+
optional. Index-style paths (`de/impressum/`) are also accepted.
|
|
93
|
+
|
|
94
|
+
```yaml
|
|
95
|
+
---
|
|
96
|
+
url: de/impressum
|
|
97
|
+
---
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
For example, a page whose source lives under a `footer/` section would normally be
|
|
101
|
+
reachable at `/de/footer/impressum`. With the declaration above its URL becomes
|
|
102
|
+
`/de/impressum` — the `/footer/` segment is invisible to visitors.
|
|
103
|
+
|
|
104
|
+
Menu links and breadcrumbs produced by Scavold automatically use the alias URL.
|
|
105
|
+
|
|
106
|
+
**Conflict detection:** if two pages declare the same `url` value, the build aborts
|
|
107
|
+
with an error naming both conflicting files. Each alias must be unique across the
|
|
108
|
+
entire site.
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## `locale` / `lang`
|
|
113
|
+
|
|
114
|
+
**Type:** `string` (BCP 47 language tag, e.g. `de`, `en`, `fr-CH`)
|
|
115
|
+
|
|
116
|
+
Declares the language of this page. The value is inherited by all pages in the same
|
|
117
|
+
section — setting it once on a section's `index.md` is enough for the whole
|
|
118
|
+
subtree. Individual pages can override the inherited value.
|
|
119
|
+
|
|
120
|
+
`locale` is the preferred key. `lang` is accepted as an alias for backwards
|
|
121
|
+
compatibility; when both are present, `locale` takes precedence.
|
|
122
|
+
|
|
123
|
+
```yaml
|
|
124
|
+
---
|
|
125
|
+
locale: de
|
|
126
|
+
---
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Scavold uses the resolved locale to:
|
|
130
|
+
|
|
131
|
+
- drive the locale switcher (`ScavoldLocaleMenu`)
|
|
132
|
+
- select the correct branch in locale-conditional redirects
|
|
133
|
+
- resolve `{locale}` placeholders in `ScavoldMenu`'s `from-path` prop
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## `translations`
|
|
138
|
+
|
|
139
|
+
**Type:** `object` — map of BCP 47 locale code → relative page path
|
|
140
|
+
|
|
141
|
+
Declares counterpart pages in other locales. Used by `ScavoldLocaleMenu` to offer
|
|
142
|
+
direct links to the same content in a different language.
|
|
143
|
+
|
|
144
|
+
```yaml
|
|
145
|
+
---
|
|
146
|
+
translations:
|
|
147
|
+
en: en/about.md
|
|
148
|
+
fr: fr/a-propos.md
|
|
149
|
+
---
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Paths are relative to the project's pages folder. The `.md` extension is required.
|
|
153
|
+
|
|
154
|
+
**Automatic back-links:** when page A declares `translations.en: en/about.md`,
|
|
155
|
+
Scavold automatically adds the reverse entry (`translations.de: de/ueber-uns.md`)
|
|
156
|
+
to `en/about.md` at build time — provided that page has no explicit `translations`
|
|
157
|
+
entry for that locale already. This means you only need to declare the link on one
|
|
158
|
+
side.
|
|
159
|
+
|
|
160
|
+
If an explicit back-link on the target page points to a *different* page, Scavold
|
|
161
|
+
emits a warning and the explicit declaration takes precedence.
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## `hide`
|
|
166
|
+
|
|
167
|
+
**Type:** `boolean | "menu" | "breadcrumb"`
|
|
168
|
+
|
|
169
|
+
Controls whether this page appears in menus, breadcrumbs, or both.
|
|
170
|
+
|
|
171
|
+
| Value | Effect |
|
|
172
|
+
|---|---|
|
|
173
|
+
| `false` or absent | Visible everywhere (default) |
|
|
174
|
+
| `true` | Hidden in both menus and breadcrumbs |
|
|
175
|
+
| `"menu"` | Hidden in menus only |
|
|
176
|
+
| `"breadcrumb"` | Hidden in breadcrumbs only |
|
|
177
|
+
|
|
178
|
+
```yaml
|
|
179
|
+
---
|
|
180
|
+
hide: true
|
|
181
|
+
---
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
```yaml
|
|
185
|
+
---
|
|
186
|
+
hide: menu
|
|
187
|
+
---
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Pages hidden from menus are still reachable by direct URL and still appear as nodes
|
|
191
|
+
in the hierarchy tree — they are only excluded from rendered navigation.
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## `redirect`
|
|
196
|
+
|
|
197
|
+
**Type:** `string | object`
|
|
198
|
+
|
|
199
|
+
Declares a redirect from this page to another URL. Two forms are supported:
|
|
200
|
+
unconditional and locale-conditional.
|
|
201
|
+
|
|
202
|
+
### Unconditional redirect
|
|
203
|
+
|
|
204
|
+
A plain string value, or an object with only a `"*"` key, redirects all visitors
|
|
205
|
+
regardless of locale. The redirect is compiled into VitePress `rewrites` at build
|
|
206
|
+
time and handled server-side — the original URL is never served to the browser.
|
|
207
|
+
|
|
208
|
+
```yaml
|
|
209
|
+
---
|
|
210
|
+
redirect: other-page.md
|
|
211
|
+
---
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
```yaml
|
|
215
|
+
---
|
|
216
|
+
redirect:
|
|
217
|
+
"*": other-page.md
|
|
218
|
+
---
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Paths are resolved relative to the current page's location.
|
|
222
|
+
|
|
223
|
+
### Locale-conditional redirect
|
|
224
|
+
|
|
225
|
+
An object with locale keys routes visitors to different pages depending on the
|
|
226
|
+
language preferences reported by their browser (`navigator.languages`). This is
|
|
227
|
+
handled client-side on first render.
|
|
228
|
+
|
|
229
|
+
```yaml
|
|
230
|
+
---
|
|
231
|
+
redirect:
|
|
232
|
+
de: /de/
|
|
233
|
+
en: /en/
|
|
234
|
+
"*": /en/
|
|
235
|
+
---
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Scavold matches the browser's preferred languages against the map keys in order and
|
|
239
|
+
calls `location.replace()` on the first match, replacing the current history entry
|
|
240
|
+
so the back button never loops back to the redirect page. The `"*"` key serves as a
|
|
241
|
+
catch-all fallback.
|
|
242
|
+
|
|
243
|
+
A `<noscript>` `<meta http-equiv="refresh">` element is injected for the `"*"`
|
|
244
|
+
target, covering browsers with JavaScript disabled.
|
|
245
|
+
|
|
246
|
+
Themes using `<ScavoldLayout>` as their root wrapper get locale redirect handling
|
|
247
|
+
automatically. Themes that manage their own layout root can use the `useRedirect()`
|
|
248
|
+
composable and `<ScavoldLocaleRedirect>` component directly — see `COMPONENTS.md`.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 cepharum GmbH, Germany
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Scavold
|
|
2
|
+
|
|
3
|
+
Scavold is a VitePress theme framework — a scaffold for building custom VitePress
|
|
4
|
+
themes with Vue at the core. It handles the structural concerns common to every
|
|
5
|
+
content-driven website: page hierarchy, locale management, responsive images, and
|
|
6
|
+
custom containers for Markdown-driven structures on your pages such as galleries, grids etc.
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
bun add vitepress vue scavold
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Scavold is currently published as a pre-release, so install it explicitly while
|
|
13
|
+
`latest` does not exist yet:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
bun add vitepress vue scavold@next
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Responsive image generation uses [sharp](https://sharp.pixelplumbing.com/), which
|
|
20
|
+
installs a platform-specific native binary — build environments therefore need to
|
|
21
|
+
be able to fetch it (Node 20 or newer).
|
|
22
|
+
|
|
23
|
+
Full documentation, including a quick-start guide, component and front matter
|
|
24
|
+
references, and deployment instructions, is available at
|
|
25
|
+
|
|
26
|
+
**[https://scavold.io](https://scavold.io)**.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { useContainer, containerProps } from "../composables/useContainer.js";
|
|
3
|
+
|
|
4
|
+
const props = defineProps( containerProps );
|
|
5
|
+
const { classes, dataAttrs } = useContainer( props );
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
<template>
|
|
9
|
+
<article :class="classes" v-bind="dataAttrs">
|
|
10
|
+
<slot />
|
|
11
|
+
</article>
|
|
12
|
+
</template>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { useContainer, containerProps } from "../composables/useContainer.js";
|
|
3
|
+
|
|
4
|
+
const props = defineProps( containerProps );
|
|
5
|
+
const { classes, dataAttrs } = useContainer( props );
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
<template>
|
|
9
|
+
<aside :class="classes" v-bind="dataAttrs">
|
|
10
|
+
<slot />
|
|
11
|
+
</aside>
|
|
12
|
+
</template>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from "vue";
|
|
3
|
+
import { useHierarchy } from "../composables/hierarchy";
|
|
4
|
+
import { useScavoldI18n } from "../composables/useI18n.js";
|
|
5
|
+
import ScavoldMenuItems from "./ScavoldMenuItems.vue";
|
|
6
|
+
import type { Scavold } from "../index";
|
|
7
|
+
|
|
8
|
+
interface BreadcrumbProps {
|
|
9
|
+
|
|
10
|
+
/** Include the current page as the last crumb. Default: true. */
|
|
11
|
+
includeCurrent?: boolean;
|
|
12
|
+
|
|
13
|
+
/** Include the root node (typically "Home") as the first crumb. Default: false. */
|
|
14
|
+
includeRoot?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const props = withDefaults( defineProps<BreadcrumbProps>(), {
|
|
18
|
+
includeCurrent: true,
|
|
19
|
+
includeRoot: false,
|
|
20
|
+
} );
|
|
21
|
+
|
|
22
|
+
const { collectAncestors } = useHierarchy();
|
|
23
|
+
const { t } = useScavoldI18n();
|
|
24
|
+
|
|
25
|
+
const items = computed<Scavold.MenuItem[]>( () =>
|
|
26
|
+
collectAncestors( props.includeCurrent, props.includeRoot )
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const ariaLabel = t( "nav.breadcrumb" );
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<template>
|
|
33
|
+
<nav v-if="items.length" class="breadcrumb" :aria-label="ariaLabel">
|
|
34
|
+
<ScavoldMenuItems :items="items" />
|
|
35
|
+
</nav>
|
|
36
|
+
</template>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { useContainer, containerProps } from "../composables/useContainer.js";
|
|
3
|
+
|
|
4
|
+
const props = defineProps( containerProps );
|
|
5
|
+
const { rootTag, containerName, classes, dataAttrs } = useContainer( props );
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
<template>
|
|
9
|
+
<component
|
|
10
|
+
:is="rootTag"
|
|
11
|
+
:class="[containerName, classes]"
|
|
12
|
+
v-bind="dataAttrs"
|
|
13
|
+
>
|
|
14
|
+
<slot />
|
|
15
|
+
</component>
|
|
16
|
+
</template>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { useContainer, containerProps } from "../composables/useContainer.js";
|
|
3
|
+
|
|
4
|
+
const props = defineProps( containerProps );
|
|
5
|
+
const { classes, dataAttrs } = useContainer( props );
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
<template>
|
|
9
|
+
<footer :class="classes" v-bind="dataAttrs">
|
|
10
|
+
<slot />
|
|
11
|
+
</footer>
|
|
12
|
+
</template>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { useContainer, containerProps } from "../composables/useContainer.js";
|
|
3
|
+
|
|
4
|
+
const props = defineProps( containerProps );
|
|
5
|
+
const { classes, dataAttrs } = useContainer( props );
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
<template>
|
|
9
|
+
<header :class="classes" v-bind="dataAttrs">
|
|
10
|
+
<slot />
|
|
11
|
+
</header>
|
|
12
|
+
</template>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
defineProps( {
|
|
3
|
+
src: { type: String, required: true },
|
|
4
|
+
srcset: { type: String, default: "" },
|
|
5
|
+
webpSrcset: { type: String, default: "" },
|
|
6
|
+
sizes: { type: String, default: "100vw" },
|
|
7
|
+
alt: { type: String, default: "" },
|
|
8
|
+
} );
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<template>
|
|
12
|
+
<picture>
|
|
13
|
+
<source
|
|
14
|
+
v-if="webpSrcset"
|
|
15
|
+
type="image/webp"
|
|
16
|
+
:srcset="webpSrcset"
|
|
17
|
+
:sizes="sizes"
|
|
18
|
+
/>
|
|
19
|
+
<source
|
|
20
|
+
v-if="srcset"
|
|
21
|
+
:srcset="srcset"
|
|
22
|
+
:sizes="sizes"
|
|
23
|
+
/>
|
|
24
|
+
<img
|
|
25
|
+
:src="src"
|
|
26
|
+
:alt="alt"
|
|
27
|
+
:role="alt ? undefined : 'presentation'"
|
|
28
|
+
:sizes="sizes"
|
|
29
|
+
loading="lazy"
|
|
30
|
+
decoding="async"
|
|
31
|
+
/>
|
|
32
|
+
</picture>
|
|
33
|
+
</template>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { watchEffect } from "vue";
|
|
3
|
+
import { useL10n } from "@cepharum/vue3-i18n";
|
|
4
|
+
import { useRedirect } from "../composables/useRedirect.js";
|
|
5
|
+
import { useHierarchy } from "../composables/hierarchy";
|
|
6
|
+
import ScavoldLocaleRedirect from "./ScavoldLocaleRedirect.vue";
|
|
7
|
+
import ScavoldSimpleRedirect from "./ScavoldSimpleRedirect.vue";
|
|
8
|
+
|
|
9
|
+
const { isLocaleRedirect, isSimpleRedirect } = useRedirect();
|
|
10
|
+
const { currentLocale } = useHierarchy();
|
|
11
|
+
|
|
12
|
+
watchEffect( () => {
|
|
13
|
+
useL10n().setLocale( currentLocale.value );
|
|
14
|
+
} );
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<template>
|
|
18
|
+
<ScavoldLocaleRedirect v-if="isLocaleRedirect" />
|
|
19
|
+
<ScavoldSimpleRedirect v-else-if="isSimpleRedirect" />
|
|
20
|
+
<slot v-else />
|
|
21
|
+
</template>
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from "vue";
|
|
3
|
+
import { useHierarchy } from "../composables/hierarchy";
|
|
4
|
+
import { useScavoldI18n } from "../composables/useI18n.js";
|
|
5
|
+
import type { Scavold } from "../index";
|
|
6
|
+
|
|
7
|
+
interface LocaleMenuProps {
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* How to discover available locales.
|
|
11
|
+
*
|
|
12
|
+
* "auto" — try explicit, then inherited, then global; use first non-empty result (default).
|
|
13
|
+
* "explicit" — only locales listed in the current page's own `translations` frontmatter.
|
|
14
|
+
* "inherited" — explicit translations of current page plus any ancestor's `translations`.
|
|
15
|
+
* "global" — every locale present anywhere in the page hierarchy; link leads to the
|
|
16
|
+
* topmost page of that locale.
|
|
17
|
+
*/
|
|
18
|
+
detection?: Scavold.LocaleDetection;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* When true, include the current locale as a non-navigating item in the list.
|
|
22
|
+
* Useful when the locale switcher always shows the full set of locales.
|
|
23
|
+
* Default: false
|
|
24
|
+
*/
|
|
25
|
+
includeCurrent?: boolean;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* When true, render nothing if only one locale is available (or none after
|
|
29
|
+
* filtering). Default: true
|
|
30
|
+
*/
|
|
31
|
+
hideIfSingle?: boolean;
|
|
32
|
+
|
|
33
|
+
/** Accessible label for the nav landmark. */
|
|
34
|
+
label?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const props = withDefaults( defineProps<LocaleMenuProps>(), {
|
|
38
|
+
detection: "auto",
|
|
39
|
+
includeCurrent: false,
|
|
40
|
+
hideIfSingle: true,
|
|
41
|
+
label: undefined,
|
|
42
|
+
} );
|
|
43
|
+
|
|
44
|
+
const { collectLocaleLinks, currentLocale } = useHierarchy();
|
|
45
|
+
const { t } = useScavoldI18n();
|
|
46
|
+
|
|
47
|
+
const links = computed<Scavold.LocaleLink[]>( () =>
|
|
48
|
+
collectLocaleLinks( props.detection, props.includeCurrent )
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const visible = computed( () => {
|
|
52
|
+
if ( props.hideIfSingle && links.value.length <= 1 ) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return links.value.length > 0;
|
|
57
|
+
} );
|
|
58
|
+
|
|
59
|
+
const navLabel = computed( () => props.label ?? t( "nav.locale" ).value );
|
|
60
|
+
</script>
|
|
61
|
+
|
|
62
|
+
<template>
|
|
63
|
+
<nav v-if="visible" class="locale-nav" :aria-label="navLabel">
|
|
64
|
+
<ul>
|
|
65
|
+
<li
|
|
66
|
+
v-for="link of links"
|
|
67
|
+
:key="link.locale"
|
|
68
|
+
:class="{
|
|
69
|
+
current: link.current,
|
|
70
|
+
}"
|
|
71
|
+
:lang="link.locale"
|
|
72
|
+
>
|
|
73
|
+
<a
|
|
74
|
+
v-if="!link.current"
|
|
75
|
+
:href="link.href"
|
|
76
|
+
:hreflang="link.locale"
|
|
77
|
+
:aria-label="link.locale"
|
|
78
|
+
>{{ link.locale }}</a>
|
|
79
|
+
<span
|
|
80
|
+
v-else
|
|
81
|
+
:aria-current="'true'"
|
|
82
|
+
>{{ link.locale }}</span>
|
|
83
|
+
</li>
|
|
84
|
+
</ul>
|
|
85
|
+
</nav>
|
|
86
|
+
</template>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { useData } from "vitepress";
|
|
3
|
+
import { onMounted } from "vue";
|
|
4
|
+
import { isExternalUrl, servableRedirectTarget } from "../lib/redirectTarget.js";
|
|
5
|
+
|
|
6
|
+
const { frontmatter } = useData();
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Picks the best redirect target from the locale map in frontmatter.redirect.
|
|
10
|
+
* Tries each language in navigator.languages in order; falls back to "*".
|
|
11
|
+
*
|
|
12
|
+
* @param {Record<string,string>} map
|
|
13
|
+
* @returns {string|null}
|
|
14
|
+
*/
|
|
15
|
+
function pickTarget( map ) {
|
|
16
|
+
for ( const lang of ( navigator.languages ?? [] ) ) {
|
|
17
|
+
// exact match first (e.g. "de-AT" → "de-AT")
|
|
18
|
+
if ( map[lang] ) return map[lang];
|
|
19
|
+
// prefix match (e.g. "de-AT" → "de")
|
|
20
|
+
const prefix = lang.split( "-" )[0];
|
|
21
|
+
if ( map[prefix] ) return map[prefix];
|
|
22
|
+
}
|
|
23
|
+
return map["*"] ?? null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
onMounted( () => {
|
|
27
|
+
const map = frontmatter.value.redirect;
|
|
28
|
+
if ( !map || typeof map !== "object" ) return;
|
|
29
|
+
const target = pickTarget( map );
|
|
30
|
+
if ( !target ) return;
|
|
31
|
+
if ( isExternalUrl( target ) ) {
|
|
32
|
+
// External locale targets: open in a new tab. The locale is resolved at
|
|
33
|
+
// runtime so build-time <meta http-equiv="refresh"> is not an option here.
|
|
34
|
+
// The click interceptor in lib/index.js does not cover locale-redirect
|
|
35
|
+
// pages either (the map only contains simple-string redirects), so
|
|
36
|
+
// window.open() is the correct call. It is invoked from onMounted which
|
|
37
|
+
// is async relative to the nav click, but modern browsers propagate
|
|
38
|
+
// user-activation through the Promise chain long enough for this to work
|
|
39
|
+
// in practice; popup-blocked fallback is acceptable for a locale redirect.
|
|
40
|
+
window.open( target, "_blank", "noopener,noreferrer" );
|
|
41
|
+
} else {
|
|
42
|
+
location.replace( servableRedirectTarget( target ) );
|
|
43
|
+
}
|
|
44
|
+
} );
|
|
45
|
+
</script>
|
|
46
|
+
|
|
47
|
+
<template><!-- redirect handled in onMounted --></template>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { useContainer, containerProps } from "../composables/useContainer.js";
|
|
3
|
+
|
|
4
|
+
const props = defineProps( containerProps );
|
|
5
|
+
const { classes, dataAttrs } = useContainer( props );
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
<template>
|
|
9
|
+
<main :class="classes" v-bind="dataAttrs">
|
|
10
|
+
<slot />
|
|
11
|
+
</main>
|
|
12
|
+
</template>
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from "vue";
|
|
3
|
+
import { useHierarchy } from "../composables/hierarchy";
|
|
4
|
+
import ScavoldMenuItems from "./ScavoldMenuItems.vue";
|
|
5
|
+
import type { Scavold } from "../index";
|
|
6
|
+
|
|
7
|
+
interface MenuProps {
|
|
8
|
+
|
|
9
|
+
/** Selects the parent node by path, supporting a `{locale}` placeholder that is
|
|
10
|
+
* replaced with the current page's locale at runtime. Takes priority over
|
|
11
|
+
* `fromRoot` and `from` when set. Examples: `"de/footer"`, `"{locale}/footer"`. */
|
|
12
|
+
fromPath?: string;
|
|
13
|
+
|
|
14
|
+
/** Absolute depth from root. 1 = top-level pages, 2 = second level, etc.
|
|
15
|
+
* When set, `from` is ignored. Nothing rendered if current page has no ancestor at this depth.
|
|
16
|
+
* Ignored when fromPath is set. */
|
|
17
|
+
fromRoot?: number;
|
|
18
|
+
|
|
19
|
+
/** Depth relative to current page. 0 = siblings (default), 1 = children,
|
|
20
|
+
* -1 = aunt/uncle level, etc. Ignored when fromRoot or fromPath is set. */
|
|
21
|
+
from?: number;
|
|
22
|
+
|
|
23
|
+
/** Additional levels to descend. 0 = flat list (default), 1 = one level of children, -1 = unlimited. */
|
|
24
|
+
depth?: number;
|
|
25
|
+
|
|
26
|
+
/** Expand only the branch leading to the current page. Useful with depth > 0. */
|
|
27
|
+
activeOnly?: boolean;
|
|
28
|
+
|
|
29
|
+
/** Expand all nodes regardless of active branch. Enables sitemap-style rendering.
|
|
30
|
+
* activeOnly takes precedence when both are set. */
|
|
31
|
+
expand?: boolean;
|
|
32
|
+
|
|
33
|
+
/** Accessible label for the nav landmark. Required when multiple ScavoldMenu
|
|
34
|
+
* instances appear on the same page so screen readers can distinguish them. */
|
|
35
|
+
label?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const props = withDefaults( defineProps<MenuProps>(), {
|
|
39
|
+
fromPath: undefined,
|
|
40
|
+
fromRoot: undefined,
|
|
41
|
+
from: 0,
|
|
42
|
+
depth: 0,
|
|
43
|
+
activeOnly: false,
|
|
44
|
+
expand: false,
|
|
45
|
+
label: undefined,
|
|
46
|
+
} );
|
|
47
|
+
|
|
48
|
+
const { current, ancestorAtDepth, collectItems, resolveByPath } = useHierarchy();
|
|
49
|
+
|
|
50
|
+
const items = computed<Scavold.MenuItem[]>( () => {
|
|
51
|
+
let parent: Scavold.HierarchyNode | undefined;
|
|
52
|
+
|
|
53
|
+
if ( props.fromPath !== undefined ) {
|
|
54
|
+
parent = resolveByPath( props.fromPath );
|
|
55
|
+
} else if ( props.fromRoot !== undefined ) {
|
|
56
|
+
parent = ancestorAtDepth( current.value, props.fromRoot );
|
|
57
|
+
} else {
|
|
58
|
+
let iter: Scavold.HierarchyNode | undefined = current.value;
|
|
59
|
+
const steps = -props.from;
|
|
60
|
+
|
|
61
|
+
if ( steps > 0 ) {
|
|
62
|
+
for ( let i = 0; i < steps && iter?.parent; i++ ) {
|
|
63
|
+
iter = iter.parent;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
parent = iter?.parent;
|
|
67
|
+
} else if ( steps === 0 ) {
|
|
68
|
+
parent = iter?.parent;
|
|
69
|
+
} else {
|
|
70
|
+
parent = iter;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return collectItems( parent, props.depth, props.activeOnly, props.expand );
|
|
75
|
+
} );
|
|
76
|
+
</script>
|
|
77
|
+
|
|
78
|
+
<template>
|
|
79
|
+
<nav v-if="items.length" :aria-label="label || undefined">
|
|
80
|
+
<ScavoldMenuItems :items="items" />
|
|
81
|
+
</nav>
|
|
82
|
+
</template>
|