scavold 0.2.0-rc.2 → 0.2.0-rc.4
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/CHANGELOG.md +89 -0
- package/COMPONENTS.md +210 -4
- package/FRONTMATTER.md +71 -3
- package/README.md +1 -1
- package/components/ScavoldLocaleMenu.vue +1 -1
- package/components/ScavoldLocaleRedirect.vue +1 -1
- package/components/ScavoldMenuItems.vue +3 -19
- package/components/ScavoldPageList.vue +110 -0
- package/composables/hierarchy.ts +72 -21
- package/composables/useContainer.js +1 -1
- package/composables/useI18n.js +4 -3
- package/composables/usePageList.js +345 -0
- package/composables/useVideo.js +2 -2
- package/index.d.ts +42 -0
- package/l10n/de.json +8 -1
- package/l10n/en.json +8 -1
- package/lib/config.d.ts +1 -0
- package/lib/config.js +160 -47
- package/lib/containers.js +143 -12
- package/lib/excerpt.js +144 -0
- package/lib/feed.js +122 -0
- package/lib/hide.d.ts +13 -0
- package/lib/hide.js +37 -0
- package/lib/index.js +2 -3
- package/lib/media.js +97 -7
- package/lib/pages.js +113 -9
- package/lib/sectionManifest.js +98 -6
- package/package.json +6 -3
- package/scripts/check-csp.js +7 -1
- package/scripts/check-fixture.js +323 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,94 @@ All notable changes to Scavold are documented here. The format follows
|
|
|
7
7
|
While the version stays below `1.0.0` and carries a pre-release suffix, breaking
|
|
8
8
|
changes may occur in any release.
|
|
9
9
|
|
|
10
|
+
## [Unreleased]
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `metaChunk` is on by default, keeping VitePress's page-hash map out of the inline
|
|
15
|
+
scripts. Its content changes with every build, so under a content security policy that
|
|
16
|
+
names script hashes it has to be updated on the server for every deploy — and when it
|
|
17
|
+
is not, the map is blocked and the client side of the site stops working while the
|
|
18
|
+
pre-rendered HTML still shows. What stays inline are VitePress's own two snippets,
|
|
19
|
+
whose hashes hold still. `metaChunk: false` in the site's own config wins.
|
|
20
|
+
- An RSS feed, written at the end of the build when `augmentConfig` is given a `feed`
|
|
21
|
+
option: the pages of a section, newest first, with their excerpt as the entry text.
|
|
22
|
+
Announced are pages carrying a `date` — a feed is a chronology, and an entry a reader
|
|
23
|
+
cannot place in it is noise. Absolute links need a hostname, taken from
|
|
24
|
+
`sitemap.hostname` unless the feed names its own; without one the build says so and
|
|
25
|
+
writes nothing. Several feeds may be declared, one per language say. The feed is
|
|
26
|
+
compiled from the page hierarchy and is unrelated to `:::pagelist` — a page may appear
|
|
27
|
+
in three lists and in the feed, in the feed alone, or in neither.
|
|
28
|
+
- Front matter `hide` names surfaces: `"menu"`, `"breadcrumb"`, `"list"`, `"feed"`, a
|
|
29
|
+
list of several, or `true` for all of them. A page can now be kept out of the
|
|
30
|
+
navigation and the feed while staying in the lists.
|
|
31
|
+
- `:::pagelist` pages its entries in the browser with `per-page`: only the current page's
|
|
32
|
+
entries are rendered, the page shown is remembered as `?page=2` so returning from an
|
|
33
|
+
entry lands where the reader left off, and the pager is a labelled `<nav>` with a
|
|
34
|
+
status line and buttons.
|
|
35
|
+
- New built-in section `:::pagelist`, rendered by `<ScavoldPageList>`: links a
|
|
36
|
+
configurable number of the site's own pages — the latest posts of a blog, a section's
|
|
37
|
+
sub-pages, a list of downloads. Which pages are listed is addressed as in
|
|
38
|
+
`<ScavoldMenu>` (`from`, `from-root`, `from-path`, plus `deep` for pages filed in
|
|
39
|
+
sub-folders), and `limit`, `offset`, `sort` and `reverse` decide which of them appear
|
|
40
|
+
in what order. Entries are a plain list of links by default; the `teaser`, `images`,
|
|
41
|
+
`dates` and `heading-level` arguments turn them into teaser cards. Anything written
|
|
42
|
+
inside the block is rendered above the list. Each entry is a single link around all
|
|
43
|
+
of its parts, so a screen reader announces it once rather than three times.
|
|
44
|
+
- Pages carry teaser data on their hierarchy node, so a list shows content maintained
|
|
45
|
+
in the target page rather than repeated in the list: `date` (normalised to an ISO
|
|
46
|
+
timestamp and rendered in UTC, so a date-only value never shifts a day), `excerpt` —
|
|
47
|
+
from `excerpt` or `description` front matter, otherwise derived from the page's
|
|
48
|
+
opening prose and trimmed at a word boundary — and `image`, from `image` front matter
|
|
49
|
+
or the first image in the page body. Front matter images now go through the
|
|
50
|
+
responsive image pipeline too, which no pass reached before, as no Markdown syntax
|
|
51
|
+
reveals them.
|
|
52
|
+
- `.cratly.config.yaml` accepts `excerpt_length` (default `200`) to control how long
|
|
53
|
+
derived excerpts get; `0` switches the derivation off for sites that would rather not
|
|
54
|
+
carry it in every page's payload.
|
|
55
|
+
- Front matter `hide` accepts `"list"`, hiding a page from page lists only — a draft
|
|
56
|
+
post stays reachable by URL without being teased anywhere.
|
|
57
|
+
- `useHierarchy()` exposes `collectPages()`, `nodeHref()`, `nodeLabel()` and
|
|
58
|
+
`nodeTitle()`, the last two separating the short navigation text menus want from the
|
|
59
|
+
full title a teaser heading wants. `<ScavoldMenuItems>` uses them instead of its own
|
|
60
|
+
copies.
|
|
61
|
+
|
|
62
|
+
## [0.2.0-rc.3] — 2026-08-09
|
|
63
|
+
|
|
64
|
+
### Added
|
|
65
|
+
|
|
66
|
+
- Media that is not an image is published. Files an author uploads into the media folder
|
|
67
|
+
— PDFs, videos, archives — are copied into the static output keeping their path, and
|
|
68
|
+
references to them are rewritten to the URL the built site serves: Markdown links,
|
|
69
|
+
and container arguments declared as `media-file` such as a video's `src` or `poster`.
|
|
70
|
+
Previously only Markdown image syntax was handled, so a linked document or a video
|
|
71
|
+
resolved to a dead URL although the editor offered exactly that reference. Where a
|
|
72
|
+
single URL is needed instead of a srcset — a `poster`, a link to an image — the
|
|
73
|
+
largest generated variant is used.
|
|
74
|
+
|
|
75
|
+
### Fixed
|
|
76
|
+
|
|
77
|
+
- Flag arguments on a container reach components that declare them as props. Bare words
|
|
78
|
+
used to become the container's `class` only, while `useVideo()` reads its booleans
|
|
79
|
+
from `data-*` props — so the documented `::: video src=… overlay autoplay loop`
|
|
80
|
+
rendered a plain inline player with no background mode, no autoplay and no loop, and
|
|
81
|
+
failed silently. Flags are now emitted as an empty `data-<flag>` attribute in addition
|
|
82
|
+
to the class. A flag a component does not declare as a prop shows up in the markup as
|
|
83
|
+
an empty attribute (`<section class="highlight" data-highlight>`).
|
|
84
|
+
|
|
85
|
+
### Added
|
|
86
|
+
|
|
87
|
+
- Container blocks work with any name, declared or not: an undeclared name is rendered
|
|
88
|
+
by `ScavoldContainer` instead of leaving its fences in the page as literal text. To
|
|
89
|
+
keep a typo from silently becoming a `<div>`, the build reports each undeclared name
|
|
90
|
+
once; declaring it in `.cratly.config.yaml` silences the report and adds typed
|
|
91
|
+
properties for the editor.
|
|
92
|
+
|
|
93
|
+
### Fixed
|
|
94
|
+
|
|
95
|
+
- `registerContainers()` and the component reference described `ScavoldContainer` as a
|
|
96
|
+
catch-all fallback, which it was not — no rule was registered for unknown names.
|
|
97
|
+
|
|
10
98
|
## [0.2.0-rc.2] — 2026-08-06
|
|
11
99
|
|
|
12
100
|
### Fixed
|
|
@@ -55,5 +143,6 @@ First published release. Version `0.1.0` existed in-tree only.
|
|
|
55
143
|
- Only images are processed out of `media_folder`; other file types (video, documents)
|
|
56
144
|
are never copied into the build output and have to live in the static folder.
|
|
57
145
|
|
|
146
|
+
[0.2.0-rc.3]: https://gitlab.com/cepharum-foss/cratly/scavold/-/compare/v0.2.0-rc.2...v0.2.0-rc.3
|
|
58
147
|
[0.2.0-rc.2]: https://gitlab.com/cepharum-foss/cratly/scavold/-/compare/v0.2.0-rc.1...v0.2.0-rc.2
|
|
59
148
|
[0.2.0-rc.1]: https://gitlab.com/cepharum-foss/cratly/scavold/-/tags/v0.2.0-rc.1
|
package/COMPONENTS.md
CHANGED
|
@@ -72,9 +72,12 @@ A page can opt out of appearing in menus by setting `hide` in its front matter:
|
|
|
72
72
|
| Value | Effect |
|
|
73
73
|
|---|---|
|
|
74
74
|
| `false` / absent | Visible everywhere (default) |
|
|
75
|
-
| `true` |
|
|
76
|
-
| `"menu"` |
|
|
77
|
-
| `"breadcrumb"` |
|
|
75
|
+
| `true` | Kept off every surface: menus, breadcrumbs, page lists and the feed |
|
|
76
|
+
| `"menu"` | Kept out of menus only |
|
|
77
|
+
| `"breadcrumb"` | Kept out of breadcrumbs only |
|
|
78
|
+
| `"list"` | Kept out of page lists only |
|
|
79
|
+
| `"feed"` | Left out of the RSS feed only |
|
|
80
|
+
| `[ "menu", "feed" ]` | Kept off exactly the surfaces named |
|
|
78
81
|
|
|
79
82
|
```yaml
|
|
80
83
|
---
|
|
@@ -460,6 +463,139 @@ const { src, poster, autoplay, loop, muted, preload, overlay, controls } = useVi
|
|
|
460
463
|
|
|
461
464
|
---
|
|
462
465
|
|
|
466
|
+
### `<ScavoldPageList>`
|
|
467
|
+
|
|
468
|
+
Links a configurable number of the site's own pages — the latest posts of a blog, a
|
|
469
|
+
teaser row of a section's sub-pages, a list of downloads. Rendered for the
|
|
470
|
+
`:::pagelist` container block. Anything written inside the block (a heading, an
|
|
471
|
+
introduction) is rendered above the list; the list itself is left out when no page
|
|
472
|
+
matches.
|
|
473
|
+
|
|
474
|
+
Every entry can carry, besides its title, a date, an introductory text and a teaser
|
|
475
|
+
image. All three come from the **target** page, so authors maintain them where the
|
|
476
|
+
content lives instead of repeating them in the list.
|
|
477
|
+
|
|
478
|
+
#### Markdown syntax
|
|
479
|
+
|
|
480
|
+
A plain list of links, as a footer or sitemap-style block would use:
|
|
481
|
+
|
|
482
|
+
```markdown
|
|
483
|
+
::: pagelist from=1 limit=10
|
|
484
|
+
:::
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
Teaser cards for the five most recent posts:
|
|
488
|
+
|
|
489
|
+
```markdown
|
|
490
|
+
::: pagelist from=1 limit=5 sort=date teaser images dates heading-level=3 label="Recent posts"
|
|
491
|
+
## Recent posts
|
|
492
|
+
|
|
493
|
+
Everything we published lately.
|
|
494
|
+
:::
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
#### Arguments
|
|
498
|
+
|
|
499
|
+
Which pages are listed:
|
|
500
|
+
|
|
501
|
+
| Argument | Type | Default | Description |
|
|
502
|
+
|---|---|---|---|
|
|
503
|
+
| `from` | `number` | `1` | Level relative to the current page. `1` = its sub-pages, `0` = its siblings, `-1` = the aunt/uncle level. Same addressing as `<ScavoldMenu>`. |
|
|
504
|
+
| `from-root` | `number` | — | Absolute level from the site root: `1` = top-level pages, `2` = second level. Takes precedence over `from`. |
|
|
505
|
+
| `from-path` | `string` | — | Path of the page whose sub-pages are listed, regardless of where the block sits. Supports the `{locale}` and `{lang}` placeholders. Takes precedence over both other selectors. |
|
|
506
|
+
| `deep` | flag | — | List pages from every level below the selected one, flattened into one list — for posts filed in year or month folders. |
|
|
507
|
+
| `limit` | `number` | `5` | Maximum number of entries. `0` lists every page found. |
|
|
508
|
+
| `offset` | `number` | `0` | Entries to skip at the start, so a second block can continue where the first one ended. |
|
|
509
|
+
| `sort` | `"order" \| "date" \| "date-asc" \| "title"` | `"order"` | `order` follows the site structure (the `order` front matter and file names), `date` is newest first, `title` is alphabetical in the page's locale. |
|
|
510
|
+
| `reverse` | flag | — | Reverse whichever order was selected. |
|
|
511
|
+
|
|
512
|
+
What each entry shows:
|
|
513
|
+
|
|
514
|
+
| Argument | Type | Default | Description |
|
|
515
|
+
|---|---|---|---|
|
|
516
|
+
| `teaser` | flag | — | Show the introductory text of each listed page. |
|
|
517
|
+
| `images` | flag | — | Show the teaser image of each listed page. |
|
|
518
|
+
| `dates` | flag | — | Show the publication date of each listed page. |
|
|
519
|
+
| `date-style` | `"short" \| "medium" \| "long" \| "full"` | `"long"` | `Intl.DateTimeFormat` style used for the dates. |
|
|
520
|
+
| `heading-level` | `number` | `0` | Heading level of the entry titles. `0` keeps the entries a plain list of links; use `2`–`6` for teaser cards, one level below the heading above the list. |
|
|
521
|
+
| `image-sizes` | `string` | `"(min-width: 60rem) 30rem, 90vw"` | CSS `sizes` descriptor for the teaser images. Set it to match the column the list is shown in — see [Responsive images](#responsive-images-let-components-own-sizes-not-content-authors). |
|
|
522
|
+
| `label` | `string` | — | Accessible label (`aria-label`) for the list. Set it whenever more than one list appears on the same page so screen readers can tell them apart. |
|
|
523
|
+
| `per-page` | `number` | `0` | Entries per page. `0` shows all of them; anything higher adds a pager. |
|
|
524
|
+
| `param` | `string` | `"page"` | Query parameter the current page is remembered in. Only needs setting when one page carries several paginated lists. |
|
|
525
|
+
|
|
526
|
+
Pages without a date sort last in both date orders — an undated page is not the
|
|
527
|
+
oldest one, it is unplaced.
|
|
528
|
+
|
|
529
|
+
#### Where the entry data comes from
|
|
530
|
+
|
|
531
|
+
| Shown | Read from the target page |
|
|
532
|
+
|---|---|
|
|
533
|
+
| Title | `title` front matter, then `label`, then the first `#` heading, then the path segment. The full `title` wins here, unlike in menus, where the short `label` does. |
|
|
534
|
+
| Date | `date` front matter, normalised to an ISO timestamp. `date: 2026-06-21` is enough; it is rendered in UTC so a date-only value never shifts to the neighbouring day. |
|
|
535
|
+
| Text | `excerpt` front matter, then `description`, otherwise derived from the page's opening prose and trimmed at a word boundary — see [`excerpt_length`](./docs/reference/config.md#excerpt-length). |
|
|
536
|
+
| Image | `image` front matter, otherwise the first image in the page body. Both go through the responsive image pipeline. |
|
|
537
|
+
|
|
538
|
+
A page opts out of every list with `hide: list` (or `hide: true`) in its front
|
|
539
|
+
matter. The page holding the list is never an entry of it.
|
|
540
|
+
|
|
541
|
+
#### Rendered markup
|
|
542
|
+
|
|
543
|
+
```html
|
|
544
|
+
<div class="scavold-pagelist">
|
|
545
|
+
<!-- slot content: heading, introduction, … -->
|
|
546
|
+
<ul class="scavold-pagelist__items" aria-label="Recent posts">
|
|
547
|
+
<li class="scavold-pagelist__item">
|
|
548
|
+
<a class="scavold-pagelist__link" href="/blog/newest">
|
|
549
|
+
<picture class="scavold-pagelist__image">…</picture>
|
|
550
|
+
<h3 class="scavold-pagelist__title">Newest post</h3>
|
|
551
|
+
<time class="scavold-pagelist__date" datetime="2026-06-21T00:00:00.000Z">21 June 2026</time>
|
|
552
|
+
<p class="scavold-pagelist__excerpt">Opening prose of the post…</p>
|
|
553
|
+
</a>
|
|
554
|
+
</li>
|
|
555
|
+
</ul>
|
|
556
|
+
</div>
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
Each entry is a **single** link around all of its parts: image, title, date and text
|
|
560
|
+
address the same page, and separate links would make a screen reader announce every
|
|
561
|
+
entry three times over. The teaser image is therefore decorative (`alt=""`) — the
|
|
562
|
+
title inside the same link is the accessible name.
|
|
563
|
+
|
|
564
|
+
The component ships no CSS at all; the class names above are the hooks a theme styles.
|
|
565
|
+
|
|
566
|
+
#### Pagination
|
|
567
|
+
|
|
568
|
+
`per-page` turns the list into pages, and it happens entirely in the browser: every entry
|
|
569
|
+
is already part of the page's data, so turning a page is slicing an array — nothing is
|
|
570
|
+
fetched, and only the current page's entries are in the document, which keeps the images
|
|
571
|
+
of the others out of it.
|
|
572
|
+
|
|
573
|
+
```markdown
|
|
574
|
+
::: pagelist from=1 sort=date per-page=10 teaser images dates heading-level=3
|
|
575
|
+
## Archive
|
|
576
|
+
:::
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
The page shown is remembered in the address bar as `?page=2`, written with
|
|
580
|
+
`history.replaceState`: turning pages is not a step the back button should have to walk
|
|
581
|
+
through, while returning from an entry lands the reader where they left off. A page
|
|
582
|
+
carrying several paginated lists gives each one its own `param`.
|
|
583
|
+
|
|
584
|
+
What the reader gets is a `<nav>` labelled for screen readers, a status line saying which
|
|
585
|
+
page of how many is shown, and buttons — not links, because no navigation happens and a
|
|
586
|
+
link would promise a URL the built site does not serve. Every part carries a class and no
|
|
587
|
+
styling; the theme decides how a pager looks.
|
|
588
|
+
|
|
589
|
+
Two consequences worth knowing. The pre-rendered HTML always holds page 1, so arriving
|
|
590
|
+
through a shared `?page=3` link shows the first page for an instant before the browser
|
|
591
|
+
catches up — returning from an entry inside the site does not, that being client-side
|
|
592
|
+
navigation. And entries beyond the first page are not in the HTML, so a crawler that
|
|
593
|
+
executes no JavaScript will not see those links; the sitemap and the
|
|
594
|
+
[feed](./docs/reference/config.md#optionsfeed--an-rss-feed-for-the-site) are what makes
|
|
595
|
+
them discoverable.
|
|
596
|
+
|
|
597
|
+
---
|
|
598
|
+
|
|
463
599
|
### `<ScavoldImage>`
|
|
464
600
|
|
|
465
601
|
Renders a responsive `<picture>` element for local images. Not intended for direct
|
|
@@ -495,6 +631,18 @@ Fallback component for Markdown container blocks whose name has no dedicated
|
|
|
495
631
|
registered component. Renders as the matching HTML sectioning element if the
|
|
496
632
|
container name is one (`section`, `aside`, etc.), otherwise as a `<div>`.
|
|
497
633
|
|
|
634
|
+
Any container name works without being declared first — undeclared names are caught
|
|
635
|
+
by this component. Because a typo would otherwise become a silent `<div>`, the build
|
|
636
|
+
reports each undeclared name once:
|
|
637
|
+
|
|
638
|
+
```
|
|
639
|
+
[scavold] container ':::sectoin' is not declared in .cratly.config.yaml — rendering
|
|
640
|
+
it with ScavoldContainer. Declare it to silence this, or fix the name if it is a typo.
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
Declaring the container in `.cratly.config.yaml` silences the report and gives it
|
|
644
|
+
typed properties in the cratly editor.
|
|
645
|
+
|
|
498
646
|
Not intended for direct use. Theme developers should instead create a dedicated
|
|
499
647
|
component for each container name they declare in `.cratly.config.yaml`.
|
|
500
648
|
|
|
@@ -553,10 +701,14 @@ Must be called inside a component's `setup` function (or `<script setup>`).
|
|
|
553
701
|
| `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
702
|
| `isOnActivePath` | `(node) => boolean` | Returns `true` if `node` is the current page or one of its ancestors |
|
|
555
703
|
| `collectItems` | `(parent, depth, activeOnly, expand) => MenuItem[]` | Recursively collects menu items from `parent`'s children. Used internally by `ScavoldMenu`. |
|
|
704
|
+
| `collectPages` | `(parent, deep?) => HierarchyNode[]` | Collects the pages below `parent` as a flat array in hierarchy order, skipping pages hidden from lists. With `deep` it descends through every level. Used internally by `ScavoldPageList`. |
|
|
556
705
|
| `collectAncestors` | `(includeCurrent, includeRoot) => MenuItem[]` | Returns the ancestor chain from root to the current page as a flat `MenuItem` array. Used internally by `ScavoldBreadcrumb`. |
|
|
557
706
|
| `currentLocale` | `ComputedRef<string>` | The resolved locale for the current page. Falls back to the browser locale when no page or ancestor declares one. |
|
|
558
707
|
| `collectLocaleLinks` | `(detection, includeCurrent?) => LocaleLink[]` | Returns available locale links for the current page. See `ScavoldLocaleMenu` for detection mode details. |
|
|
559
708
|
| `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. |
|
|
709
|
+
| `nodeHref` | `(node) => string` | Root-relative link target of a node, preferring its alias URL. |
|
|
710
|
+
| `nodeLabel` | `(node) => string` | Navigation text of a node: `label`, then `title`, then the path segment. For menus and breadcrumbs, where space is scarce. |
|
|
711
|
+
| `nodeTitle` | `(node) => string` | Full text of a node: `title`, then `label`, then the path segment. For entries that stand on their own, such as a teaser heading. |
|
|
560
712
|
|
|
561
713
|
#### `HierarchyNode` properties
|
|
562
714
|
|
|
@@ -565,13 +717,17 @@ Must be called inside a component's `setup` function (or `<script setup>`).
|
|
|
565
717
|
| `path` | `string` | Relative path of the Markdown source file |
|
|
566
718
|
| `isPage` | `boolean` | `true` for actual pages, `false` for intermediate folder nodes |
|
|
567
719
|
| `frontmatter` | `object` | Parsed front matter data |
|
|
568
|
-
| `frontmatter.hide` | `boolean \|
|
|
720
|
+
| `frontmatter.hide` | `boolean \| string \| string[]` | Surfaces this node is kept off: `"menu"`, `"breadcrumb"`, `"list"`, `"feed"`, a list of them, or `true` for all. See [Hiding pages via front matter](#hiding-pages-via-front-matter). |
|
|
569
721
|
| `frontmatter.order` | `number?` | Sort position among siblings. Pages with a lower value appear first; pages without `order` follow in filename order. |
|
|
570
722
|
| `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
723
|
| `title` | `string?` | Display title — from frontmatter `title` or the first `#` heading |
|
|
572
724
|
| `label` | `string?` | Navigation label — from frontmatter `label`; falls back to `title` in menus |
|
|
573
725
|
| `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
726
|
| `locale` | `string?` | Resolved locale (BCP 47), inherited from ancestors |
|
|
727
|
+
| `excerpt` | `string?` | Introductory text — from frontmatter `excerpt` or `description`, else derived from the page's opening prose |
|
|
728
|
+
| `image` | `string?` | Lead image reference as the author wrote it — from frontmatter `image`, else the first image in the page |
|
|
729
|
+
| `imageData` | `{ src, srcset, webpSrcset }?` | Responsive variant data for `image`, resolved at build time. Carries no `sizes` value: that belongs to the component showing the image. |
|
|
730
|
+
| `date` | `string?` | Publication date as an ISO 8601 timestamp — from frontmatter `date` |
|
|
575
731
|
| `parent` | `HierarchyNode?` | Parent node — non-enumerable, not serialised to JSON |
|
|
576
732
|
| `subs` | `object?` | Child nodes keyed by path segment, sorted by `order` then filename |
|
|
577
733
|
|
|
@@ -649,6 +805,49 @@ Spread into `defineProps` to declare all video-related props at once.
|
|
|
649
805
|
|
|
650
806
|
---
|
|
651
807
|
|
|
808
|
+
### `usePageList( props )`
|
|
809
|
+
|
|
810
|
+
```js
|
|
811
|
+
import { usePageList, pageListProps } from "./scavold/composables/usePageList.js";
|
|
812
|
+
```
|
|
813
|
+
|
|
814
|
+
Composable for custom page-list components. Resolves the `:::pagelist` container props
|
|
815
|
+
into the entries to render, each carrying its link target, title, date, introductory
|
|
816
|
+
text and responsive teaser image data — see the
|
|
817
|
+
[arguments](#scavoldpagelist) for what each prop means.
|
|
818
|
+
|
|
819
|
+
#### `pageListProps`
|
|
820
|
+
|
|
821
|
+
Spread into `defineProps` to declare all page-list props at once. Each container
|
|
822
|
+
argument arrives as `data…`, in the same way as `videoProps`: `dataFrom` for `from=…`,
|
|
823
|
+
`dataHeadingLevel` for `heading-level=…`, `dataTeaser` for the `teaser` flag, and so on.
|
|
824
|
+
|
|
825
|
+
#### Returns
|
|
826
|
+
|
|
827
|
+
| Name | Type | Description |
|
|
828
|
+
|---|---|---|
|
|
829
|
+
| `items` | `ComputedRef<PageListItem[]>` | The selected, ordered and limited entries |
|
|
830
|
+
| `headingTag` | `ComputedRef<string \| null>` | `"h2"`…`"h6"` when a heading level was requested, `null` for a plain list of links |
|
|
831
|
+
| `teaser` | `ComputedRef<boolean>` | `true` when the `teaser` flag is set |
|
|
832
|
+
| `images` | `ComputedRef<boolean>` | `true` when the `images` flag is set |
|
|
833
|
+
| `dates` | `ComputedRef<boolean>` | `true` when the `dates` flag is set |
|
|
834
|
+
| `imageSizes` | `ComputedRef<string>` | CSS `sizes` descriptor for the teaser images |
|
|
835
|
+
| `ariaLabel` | `ComputedRef<string \| undefined>` | Accessible label for the list, `undefined` when the author set none |
|
|
836
|
+
| `formatDate` | `(isoDate: string) => string` | Formats a timestamp in the page's locale, in UTC |
|
|
837
|
+
|
|
838
|
+
#### `PageListItem` properties
|
|
839
|
+
|
|
840
|
+
| Property | Type | Description |
|
|
841
|
+
|---|---|---|
|
|
842
|
+
| `node` | `HierarchyNode` | The listed page's hierarchy node |
|
|
843
|
+
| `href` | `string` | Root-relative link target, alias URL included |
|
|
844
|
+
| `title` | `string` | Full title of the page (`title` wins over `label` here) |
|
|
845
|
+
| `date` | `string?` | ISO 8601 publication date, when the page declares one |
|
|
846
|
+
| `excerpt` | `string?` | Introductory text, when the page has or yields one |
|
|
847
|
+
| `image` | `{ src, srcset, webpSrcset }?` | Responsive variant data of the teaser image |
|
|
848
|
+
|
|
849
|
+
---
|
|
850
|
+
|
|
652
851
|
### `useContainer( props )`
|
|
653
852
|
|
|
654
853
|
```js
|
|
@@ -697,6 +896,13 @@ All `key=value` pairs from the opening line are passed as additional props with
|
|
|
697
896
|
`data-` prefix (e.g. `background=/img.jpg` → prop `data-background`). Declare them
|
|
698
897
|
explicitly in `defineProps` to use them.
|
|
699
898
|
|
|
899
|
+
Flags arrive **both ways**: as part of `class` and as an empty `data-<flag>` prop, so
|
|
900
|
+
a boolean container parameter can be declared as a prop (this is how `useVideo` reads
|
|
901
|
+
`overlay`, `autoplay` and `loop`) while purely presentational flags keep working as
|
|
902
|
+
class names. A flag a component does not declare falls through into the markup as an
|
|
903
|
+
empty attribute — `::: section highlight` renders `<section class="highlight"
|
|
904
|
+
data-highlight>`.
|
|
905
|
+
|
|
700
906
|
#### Returns
|
|
701
907
|
|
|
702
908
|
| Name | Type | Description |
|
package/FRONTMATTER.md
CHANGED
|
@@ -162,18 +162,86 @@ emits a warning and the explicit declaration takes precedence.
|
|
|
162
162
|
|
|
163
163
|
---
|
|
164
164
|
|
|
165
|
+
## `date`
|
|
166
|
+
|
|
167
|
+
**Type:** `string` (ISO 8601) — e.g. `2026-06-21` or `2026-06-21T14:30:00Z`
|
|
168
|
+
|
|
169
|
+
Publication date of the page. Read by [`<ScavoldPageList>`](./COMPONENTS.md#scavoldpagelist)
|
|
170
|
+
to order entries chronologically and to show a date per entry, and by the site's RSS feed,
|
|
171
|
+
which announces only pages that carry one.
|
|
172
|
+
|
|
173
|
+
```yaml
|
|
174
|
+
---
|
|
175
|
+
date: 2026-06-21
|
|
176
|
+
---
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
A date-only value is normalised to midnight UTC and rendered in UTC as well, so it
|
|
180
|
+
never shows up as the neighbouring day in another time zone. Pages without a date
|
|
181
|
+
sort last in both date orders.
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## `excerpt` / `description`
|
|
186
|
+
|
|
187
|
+
**Type:** `string`
|
|
188
|
+
|
|
189
|
+
Introductory text shown for this page wherever it is teased — in a
|
|
190
|
+
[`<ScavoldPageList>`](./COMPONENTS.md#scavoldpagelist) with the `teaser` flag, for
|
|
191
|
+
example.
|
|
192
|
+
|
|
193
|
+
```yaml
|
|
194
|
+
---
|
|
195
|
+
excerpt: Wie wir den Schulhof an einem Nachmittag umgestaltet haben.
|
|
196
|
+
---
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
When neither key is declared, Scavold derives the text from the page's opening prose
|
|
200
|
+
at build time: headings, images, code and container fences are dropped, the remaining
|
|
201
|
+
text is trimmed to [`excerpt_length`](./docs/reference/config.md#excerpt-length)
|
|
202
|
+
characters at a word boundary and marked with an ellipsis. Declaring `excerpt` is
|
|
203
|
+
therefore an override, not a prerequisite.
|
|
204
|
+
|
|
205
|
+
`description` is accepted as an alias, since sites often already maintain it for
|
|
206
|
+
search engines. `excerpt` wins when both are present.
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## `image`
|
|
211
|
+
|
|
212
|
+
**Type:** `string` — path relative to the media folder
|
|
213
|
+
|
|
214
|
+
Lead image of the page, used as its teaser image in a
|
|
215
|
+
[`<ScavoldPageList>`](./COMPONENTS.md#scavoldpagelist) with the `images` flag.
|
|
216
|
+
|
|
217
|
+
```yaml
|
|
218
|
+
---
|
|
219
|
+
image: /schulhof.jpg
|
|
220
|
+
---
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
The file goes through the responsive image pipeline like any image in a page body, so
|
|
224
|
+
a teaser gets the same `srcset` and WebP variants. When the key is absent, the first
|
|
225
|
+
image in the page body is used instead.
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
165
229
|
## `hide`
|
|
166
230
|
|
|
167
|
-
**Type:** `boolean | "menu" | "breadcrumb"`
|
|
231
|
+
**Type:** `boolean | "menu" | "breadcrumb" | "list" | "feed" | string[]`
|
|
168
232
|
|
|
169
|
-
Controls
|
|
233
|
+
Controls the surfaces this page appears on. Each value names one of them; `true` covers
|
|
234
|
+
all — which is what an unfinished post wants, reachable by URL and announced nowhere.
|
|
170
235
|
|
|
171
236
|
| Value | Effect |
|
|
172
237
|
|---|---|
|
|
173
238
|
| `false` or absent | Visible everywhere (default) |
|
|
174
|
-
| `true` | Hidden
|
|
239
|
+
| `true` | Hidden everywhere: menus, breadcrumbs, page lists and the feed |
|
|
175
240
|
| `"menu"` | Hidden in menus only |
|
|
176
241
|
| `"breadcrumb"` | Hidden in breadcrumbs only |
|
|
242
|
+
| `"list"` | Hidden in page lists only |
|
|
243
|
+
| `"feed"` | Left out of the RSS feed only |
|
|
244
|
+
| `[ "menu", "feed" ]` | Kept off exactly the surfaces named |
|
|
177
245
|
|
|
178
246
|
```yaml
|
|
179
247
|
---
|
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ Scavold is currently a **pre-release**: breaking changes may occur between relea
|
|
|
13
13
|
so pin the version you develop against rather than following the range blindly.
|
|
14
14
|
|
|
15
15
|
```sh
|
|
16
|
-
bun add vitepress vue scavold@^0.2.0-rc.
|
|
16
|
+
bun add vitepress vue scavold@^0.2.0-rc.3
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
Responsive image generation uses [sharp](https://sharp.pixelplumbing.com/), which
|
|
@@ -41,7 +41,7 @@ const props = withDefaults( defineProps<LocaleMenuProps>(), {
|
|
|
41
41
|
label: undefined,
|
|
42
42
|
} );
|
|
43
43
|
|
|
44
|
-
const { collectLocaleLinks
|
|
44
|
+
const { collectLocaleLinks } = useHierarchy();
|
|
45
45
|
const { t } = useScavoldI18n();
|
|
46
46
|
|
|
47
47
|
const links = computed<Scavold.LocaleLink[]>( () =>
|
|
@@ -13,7 +13,7 @@ const { frontmatter } = useData();
|
|
|
13
13
|
* @returns {string|null}
|
|
14
14
|
*/
|
|
15
15
|
function pickTarget( map ) {
|
|
16
|
-
for ( const lang of
|
|
16
|
+
for ( const lang of navigator.languages ?? [] ) {
|
|
17
17
|
// exact match first (e.g. "de-AT" → "de-AT")
|
|
18
18
|
if ( map[lang] ) return map[lang];
|
|
19
19
|
// prefix match (e.g. "de-AT" → "de")
|
|
@@ -1,28 +1,12 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { useHierarchy } from "../composables/hierarchy";
|
|
2
3
|
import type { Scavold } from "../index";
|
|
3
4
|
|
|
4
5
|
defineProps<{
|
|
5
6
|
items: Scavold.MenuItem[];
|
|
6
7
|
}>();
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
if ( node.label ) return node.label;
|
|
10
|
-
if ( node.title ) return node.title;
|
|
11
|
-
|
|
12
|
-
// Fall back to the last meaningful path segment (strip index.md, extension)
|
|
13
|
-
const segment = node.path
|
|
14
|
-
.replace( /\/index\.md$/, "" )
|
|
15
|
-
.replace( /\.md$/, "" )
|
|
16
|
-
.split( "/" )
|
|
17
|
-
.filter( Boolean )
|
|
18
|
-
.pop();
|
|
19
|
-
|
|
20
|
-
return segment ?? node.path;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function href( node: Scavold.HierarchyNode ): string {
|
|
24
|
-
return "/" + ( node.url ?? node.path ).replace( /\.md$/, "" ).replace( /\/index$/, "/" );
|
|
25
|
-
}
|
|
9
|
+
const { nodeLabel, nodeHref } = useHierarchy();
|
|
26
10
|
</script>
|
|
27
11
|
|
|
28
12
|
<template>
|
|
@@ -35,7 +19,7 @@ function href( node: Scavold.HierarchyNode ): string {
|
|
|
35
19
|
current: item.current,
|
|
36
20
|
}"
|
|
37
21
|
>
|
|
38
|
-
<a :href="
|
|
22
|
+
<a :href="nodeHref( item.node )" :aria-current="item.current ? 'page' : undefined">{{ nodeLabel( item.node ) }}</a>
|
|
39
23
|
<ScavoldMenuItems
|
|
40
24
|
v-if="item.children.length"
|
|
41
25
|
:items="item.children"
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed } from "vue";
|
|
3
|
+
import { useContainer } from "../composables/useContainer.js";
|
|
4
|
+
import { usePageList, pageListProps } from "../composables/usePageList.js";
|
|
5
|
+
import { useScavoldI18n } from "../composables/useI18n.js";
|
|
6
|
+
import ScavoldImage from "./ScavoldImage.vue";
|
|
7
|
+
|
|
8
|
+
const props = defineProps( pageListProps );
|
|
9
|
+
|
|
10
|
+
const { classes, dataAttrs } = useContainer( props );
|
|
11
|
+
const {
|
|
12
|
+
items, headingTag, teaser, images, dates, imageSizes, ariaLabel, formatDate,
|
|
13
|
+
page, pages, pageCount, goToPage,
|
|
14
|
+
} = usePageList( props );
|
|
15
|
+
const { t } = useScavoldI18n();
|
|
16
|
+
|
|
17
|
+
const pagerLabel = t( "nav.pages" );
|
|
18
|
+
const previousLabel = t( "pagelist.previous" );
|
|
19
|
+
const nextLabel = t( "pagelist.next" );
|
|
20
|
+
const status = t( "pagelist.status", computed( () => ( { current: page.value, total: pageCount.value } ) ) );
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<template>
|
|
24
|
+
<div class="scavold-pagelist" :class="classes" v-bind="dataAttrs">
|
|
25
|
+
<!-- whatever the author wrote inside the section: a heading, an intro, anything -->
|
|
26
|
+
<slot />
|
|
27
|
+
|
|
28
|
+
<ul
|
|
29
|
+
v-if="items.length"
|
|
30
|
+
class="scavold-pagelist__items"
|
|
31
|
+
:aria-label="ariaLabel"
|
|
32
|
+
>
|
|
33
|
+
<li
|
|
34
|
+
v-for="item of items"
|
|
35
|
+
:key="item.node.path"
|
|
36
|
+
class="scavold-pagelist__item"
|
|
37
|
+
>
|
|
38
|
+
<!-- One link per entry rather than one per element: image, title, date and text
|
|
39
|
+
all address the same page, and repeating that target would make a screen
|
|
40
|
+
reader announce every entry three times. -->
|
|
41
|
+
<a class="scavold-pagelist__link" :href="item.href">
|
|
42
|
+
<ScavoldImage
|
|
43
|
+
v-if="images && item.image"
|
|
44
|
+
class="scavold-pagelist__image"
|
|
45
|
+
:src="item.image.src"
|
|
46
|
+
:srcset="item.image.srcset"
|
|
47
|
+
:webp-srcset="item.image.webpSrcset"
|
|
48
|
+
:sizes="imageSizes"
|
|
49
|
+
alt=""
|
|
50
|
+
/>
|
|
51
|
+
|
|
52
|
+
<component
|
|
53
|
+
:is="headingTag"
|
|
54
|
+
v-if="headingTag"
|
|
55
|
+
class="scavold-pagelist__title"
|
|
56
|
+
>{{ item.title }}</component>
|
|
57
|
+
<span v-else class="scavold-pagelist__title">{{ item.title }}</span>
|
|
58
|
+
|
|
59
|
+
<time
|
|
60
|
+
v-if="dates && item.date"
|
|
61
|
+
class="scavold-pagelist__date"
|
|
62
|
+
:datetime="item.date"
|
|
63
|
+
>{{ formatDate( item.date ) }}</time>
|
|
64
|
+
|
|
65
|
+
<p
|
|
66
|
+
v-if="teaser && item.excerpt"
|
|
67
|
+
class="scavold-pagelist__excerpt"
|
|
68
|
+
>{{ item.excerpt }}</p>
|
|
69
|
+
</a>
|
|
70
|
+
</li>
|
|
71
|
+
</ul>
|
|
72
|
+
|
|
73
|
+
<!-- Turning a page swaps content in place, so the reader is told which page they are
|
|
74
|
+
on rather than being left to notice. Buttons, not links: no navigation happens,
|
|
75
|
+
and a link would promise a page the built site does not serve. -->
|
|
76
|
+
<nav
|
|
77
|
+
v-if="pageCount > 1"
|
|
78
|
+
class="scavold-pagelist__pager"
|
|
79
|
+
:aria-label="pagerLabel"
|
|
80
|
+
>
|
|
81
|
+
<p class="scavold-pagelist__status" role="status">{{ status }}</p>
|
|
82
|
+
|
|
83
|
+
<button
|
|
84
|
+
type="button"
|
|
85
|
+
class="scavold-pagelist__page scavold-pagelist__page--previous"
|
|
86
|
+
:disabled="page <= 1"
|
|
87
|
+
:aria-label="previousLabel"
|
|
88
|
+
@click="goToPage( page - 1 )"
|
|
89
|
+
>‹</button>
|
|
90
|
+
|
|
91
|
+
<button
|
|
92
|
+
v-for="number of pages"
|
|
93
|
+
:key="number"
|
|
94
|
+
type="button"
|
|
95
|
+
class="scavold-pagelist__page"
|
|
96
|
+
:class="{ current: number === page }"
|
|
97
|
+
:aria-current="number === page ? 'page' : undefined"
|
|
98
|
+
@click="goToPage( number )"
|
|
99
|
+
>{{ number }}</button>
|
|
100
|
+
|
|
101
|
+
<button
|
|
102
|
+
type="button"
|
|
103
|
+
class="scavold-pagelist__page scavold-pagelist__page--next"
|
|
104
|
+
:disabled="page >= pageCount"
|
|
105
|
+
:aria-label="nextLabel"
|
|
106
|
+
@click="goToPage( page + 1 )"
|
|
107
|
+
>›</button>
|
|
108
|
+
</nav>
|
|
109
|
+
</div>
|
|
110
|
+
</template>
|