scavold 0.2.0-rc.3 → 0.2.0-rc.5
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 +245 -4
- package/FRONTMATTER.md +71 -3
- 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 +75 -24
- package/composables/useContainer.js +1 -1
- package/composables/useI18n.js +32 -6
- package/composables/usePageList.js +342 -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 +131 -26
- package/lib/containers.js +4 -3
- package/lib/dateFormat.js +125 -0
- package/lib/excerpt.js +144 -0
- package/lib/feed.js +109 -0
- package/lib/hide.d.ts +13 -0
- package/lib/hide.js +37 -0
- package/lib/href.js +62 -0
- package/lib/index.js +2 -3
- package/lib/media.js +2 -2
- package/lib/pageList.js +38 -0
- package/lib/pages.js +113 -9
- package/lib/redirectTarget.js +6 -12
- package/lib/sectionManifest.js +107 -3
- package/package.json +6 -3
- package/scripts/check-csp.js +7 -1
- package/scripts/check-fixture.js +366 -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
|
+
- `:::pagelist` dates its entries the way the site wants them: `time-style` adds the
|
|
15
|
+
time of day, `timezone` names the zone the timestamps are read in, and both accept
|
|
16
|
+
`iso` alongside the `Intl` styles, writing `2020-10-20 12:16` in every language. The
|
|
17
|
+
default is unchanged — the date alone, read in UTC, which is where a front matter
|
|
18
|
+
value carrying no time of day belongs.
|
|
19
|
+
- `formatTimestamp()` in `scavold/lib/dateFormat.js` renders timestamps the same way
|
|
20
|
+
for a theme's own components — a byline under a post's title, say. It is free of Vue
|
|
21
|
+
and VitePress and depends on its arguments alone, so pre-rendered markup and the
|
|
22
|
+
browser agree.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- A `:::pagelist` that paginates reaches every page it found: `limit` now defaults to
|
|
27
|
+
`0` when `per-page` is set, rather than capping the selection at five entries and
|
|
28
|
+
leaving a pager to turn the pages of those five. A limit the author sets still wins.
|
|
29
|
+
- Every link a site generates — menus, breadcrumbs, page lists, the feed, redirect targets
|
|
30
|
+
— names the file the page is built into: `/blog/post.html`, `/blog/index.html`,
|
|
31
|
+
`/index.html`. Links used to leave the extension off while VitePress's own markdown
|
|
32
|
+
links carried it, so one site spoke two dialects and the shorter one only worked on
|
|
33
|
+
servers that go looking for the file. Naming it is answered on the first attempt, and
|
|
34
|
+
the sitemap now lists the same URLs. The address bar still shows `/` and `/blog/`:
|
|
35
|
+
VitePress's router shortens them while navigating.
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
|
|
39
|
+
- The site's own `index.md` is no longer linked as `/index`, a URL no server has to
|
|
40
|
+
answer. The rule for a page's URL existed in three copies and now lives in
|
|
41
|
+
`lib/href.js` alone.
|
|
42
|
+
- Scavold's built-in translations come from the bundle. They used to be fetched from a
|
|
43
|
+
URL that no build emits, so every page requested a file that was not there and the
|
|
44
|
+
interface silently fell back to English, whatever language the site is in.
|
|
45
|
+
|
|
46
|
+
## [0.2.0-rc.4] — 2026-08-15
|
|
47
|
+
|
|
48
|
+
### Added
|
|
49
|
+
|
|
50
|
+
- `metaChunk` is on by default, keeping VitePress's page-hash map out of the inline
|
|
51
|
+
scripts. Its content changes with every build, so under a content security policy that
|
|
52
|
+
names script hashes it has to be updated on the server for every deploy — and when it
|
|
53
|
+
is not, the map is blocked and the client side of the site stops working while the
|
|
54
|
+
pre-rendered HTML still shows. What stays inline are VitePress's own two snippets,
|
|
55
|
+
whose hashes hold still. `metaChunk: false` in the site's own config wins.
|
|
56
|
+
- An RSS feed, written at the end of the build when `augmentConfig` is given a `feed`
|
|
57
|
+
option: the pages of a section, newest first, with their excerpt as the entry text.
|
|
58
|
+
Announced are pages carrying a `date` — a feed is a chronology, and an entry a reader
|
|
59
|
+
cannot place in it is noise. Absolute links need a hostname, taken from
|
|
60
|
+
`sitemap.hostname` unless the feed names its own; without one the build says so and
|
|
61
|
+
writes nothing. Several feeds may be declared, one per language say. The feed is
|
|
62
|
+
compiled from the page hierarchy and is unrelated to `:::pagelist` — a page may appear
|
|
63
|
+
in three lists and in the feed, in the feed alone, or in neither.
|
|
64
|
+
- Front matter `hide` names surfaces: `"menu"`, `"breadcrumb"`, `"list"`, `"feed"`, a
|
|
65
|
+
list of several, or `true` for all of them. A page can now be kept out of the
|
|
66
|
+
navigation and the feed while staying in the lists.
|
|
67
|
+
- `:::pagelist` pages its entries in the browser with `per-page`: only the current page's
|
|
68
|
+
entries are rendered, the page shown is remembered as `?page=2` so returning from an
|
|
69
|
+
entry lands where the reader left off, and the pager is a labelled `<nav>` with a
|
|
70
|
+
status line and buttons.
|
|
71
|
+
- New built-in section `:::pagelist`, rendered by `<ScavoldPageList>`: links a
|
|
72
|
+
configurable number of the site's own pages — the latest posts of a blog, a section's
|
|
73
|
+
sub-pages, a list of downloads. Which pages are listed is addressed as in
|
|
74
|
+
`<ScavoldMenu>` (`from`, `from-root`, `from-path`, plus `deep` for pages filed in
|
|
75
|
+
sub-folders), and `limit`, `offset`, `sort` and `reverse` decide which of them appear
|
|
76
|
+
in what order. Entries are a plain list of links by default; the `teaser`, `images`,
|
|
77
|
+
`dates` and `heading-level` arguments turn them into teaser cards. Anything written
|
|
78
|
+
inside the block is rendered above the list. Each entry is a single link around all
|
|
79
|
+
of its parts, so a screen reader announces it once rather than three times.
|
|
80
|
+
- Pages carry teaser data on their hierarchy node, so a list shows content maintained
|
|
81
|
+
in the target page rather than repeated in the list: `date` (normalised to an ISO
|
|
82
|
+
timestamp and rendered in UTC, so a date-only value never shifts a day), `excerpt` —
|
|
83
|
+
from `excerpt` or `description` front matter, otherwise derived from the page's
|
|
84
|
+
opening prose and trimmed at a word boundary — and `image`, from `image` front matter
|
|
85
|
+
or the first image in the page body. Front matter images now go through the
|
|
86
|
+
responsive image pipeline too, which no pass reached before, as no Markdown syntax
|
|
87
|
+
reveals them.
|
|
88
|
+
- `.cratly.config.yaml` accepts `excerpt_length` (default `200`) to control how long
|
|
89
|
+
derived excerpts get; `0` switches the derivation off for sites that would rather not
|
|
90
|
+
carry it in every page's payload.
|
|
91
|
+
- Front matter `hide` accepts `"list"`, hiding a page from page lists only — a draft
|
|
92
|
+
post stays reachable by URL without being teased anywhere.
|
|
93
|
+
- `useHierarchy()` exposes `collectPages()`, `nodeHref()`, `nodeLabel()` and
|
|
94
|
+
`nodeTitle()`, the last two separating the short navigation text menus want from the
|
|
95
|
+
full title a teaser heading wants. `<ScavoldMenuItems>` uses them instead of its own
|
|
96
|
+
copies.
|
|
97
|
+
|
|
10
98
|
## [0.2.0-rc.3] — 2026-08-09
|
|
11
99
|
|
|
12
100
|
### Added
|
|
@@ -91,6 +179,7 @@ First published release. Version `0.1.0` existed in-tree only.
|
|
|
91
179
|
- Only images are processed out of `media_folder`; other file types (video, documents)
|
|
92
180
|
are never copied into the build output and have to live in the static folder.
|
|
93
181
|
|
|
182
|
+
[0.2.0-rc.4]: https://gitlab.com/cepharum-foss/cratly/scavold/-/compare/v0.2.0-rc.3...v0.2.0-rc.4
|
|
94
183
|
[0.2.0-rc.3]: https://gitlab.com/cepharum-foss/cratly/scavold/-/compare/v0.2.0-rc.2...v0.2.0-rc.3
|
|
95
184
|
[0.2.0-rc.2]: https://gitlab.com/cepharum-foss/cratly/scavold/-/compare/v0.2.0-rc.1...v0.2.0-rc.2
|
|
96
185
|
[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,141 @@ 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`, or `0` with `per-page` | Maximum number of entries. `0` lists every page found. A paginated list reaches every page it found unless a limit says otherwise. |
|
|
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" \| "iso"` | `"long"` | Style of the dates. The `Intl.DateTimeFormat` styles follow the page's language; `iso` writes `2020-10-20` in every language. |
|
|
520
|
+
| `time-style` | `"short" \| "medium" \| "long" \| "full" \| "iso"` | — | Style of the time of day. Left out, entries show their date only. `iso` writes `12:16`. |
|
|
521
|
+
| `timezone` | `string` | `"UTC"` | IANA name of the zone the dates are read in, e.g. `Europe/Berlin`. A date carrying no time of day belongs in UTC, the zone the build normalises it into; a list that shows times wants the zone the site publishes from. |
|
|
522
|
+
| `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. |
|
|
523
|
+
| `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). |
|
|
524
|
+
| `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. |
|
|
525
|
+
| `per-page` | `number` | `0` | Entries per page. `0` shows all of them; anything higher adds a pager. |
|
|
526
|
+
| `param` | `string` | `"page"` | Query parameter the current page is remembered in. Only needs setting when one page carries several paginated lists. |
|
|
527
|
+
|
|
528
|
+
Pages without a date sort last in both date orders — an undated page is not the
|
|
529
|
+
oldest one, it is unplaced.
|
|
530
|
+
|
|
531
|
+
#### Where the entry data comes from
|
|
532
|
+
|
|
533
|
+
| Shown | Read from the target page |
|
|
534
|
+
|---|---|
|
|
535
|
+
| 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. |
|
|
536
|
+
| 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. |
|
|
537
|
+
| 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). |
|
|
538
|
+
| Image | `image` front matter, otherwise the first image in the page body. Both go through the responsive image pipeline. |
|
|
539
|
+
|
|
540
|
+
A page opts out of every list with `hide: list` (or `hide: true`) in its front
|
|
541
|
+
matter. The page holding the list is never an entry of it.
|
|
542
|
+
|
|
543
|
+
#### Rendered markup
|
|
544
|
+
|
|
545
|
+
```html
|
|
546
|
+
<div class="scavold-pagelist">
|
|
547
|
+
<!-- slot content: heading, introduction, … -->
|
|
548
|
+
<ul class="scavold-pagelist__items" aria-label="Recent posts">
|
|
549
|
+
<li class="scavold-pagelist__item">
|
|
550
|
+
<a class="scavold-pagelist__link" href="/blog/newest">
|
|
551
|
+
<picture class="scavold-pagelist__image">…</picture>
|
|
552
|
+
<h3 class="scavold-pagelist__title">Newest post</h3>
|
|
553
|
+
<time class="scavold-pagelist__date" datetime="2026-06-21T00:00:00.000Z">21 June 2026</time>
|
|
554
|
+
<p class="scavold-pagelist__excerpt">Opening prose of the post…</p>
|
|
555
|
+
</a>
|
|
556
|
+
</li>
|
|
557
|
+
</ul>
|
|
558
|
+
</div>
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
Each entry is a **single** link around all of its parts: image, title, date and text
|
|
562
|
+
address the same page, and separate links would make a screen reader announce every
|
|
563
|
+
entry three times over. The teaser image is therefore decorative (`alt=""`) — the
|
|
564
|
+
title inside the same link is the accessible name.
|
|
565
|
+
|
|
566
|
+
The component ships no CSS at all; the class names above are the hooks a theme styles.
|
|
567
|
+
|
|
568
|
+
#### Pagination
|
|
569
|
+
|
|
570
|
+
`per-page` turns the list into pages, and it happens entirely in the browser: every entry
|
|
571
|
+
is already part of the page's data, so turning a page is slicing an array — nothing is
|
|
572
|
+
fetched, and only the current page's entries are in the document, which keeps the images
|
|
573
|
+
of the others out of it.
|
|
574
|
+
|
|
575
|
+
```markdown
|
|
576
|
+
::: pagelist from=1 sort=date per-page=10 teaser images dates heading-level=3
|
|
577
|
+
## Archive
|
|
578
|
+
:::
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
The page shown is remembered in the address bar as `?page=2`, written with
|
|
582
|
+
`history.replaceState`: turning pages is not a step the back button should have to walk
|
|
583
|
+
through, while returning from an entry lands the reader where they left off. A page
|
|
584
|
+
carrying several paginated lists gives each one its own `param`.
|
|
585
|
+
|
|
586
|
+
What the reader gets is a `<nav>` labelled for screen readers, a status line saying which
|
|
587
|
+
page of how many is shown, and buttons — not links, because no navigation happens and a
|
|
588
|
+
link would promise a URL the built site does not serve. Every part carries a class and no
|
|
589
|
+
styling; the theme decides how a pager looks.
|
|
590
|
+
|
|
591
|
+
Two consequences worth knowing. The pre-rendered HTML always holds page 1, so arriving
|
|
592
|
+
through a shared `?page=3` link shows the first page for an instant before the browser
|
|
593
|
+
catches up — returning from an entry inside the site does not, that being client-side
|
|
594
|
+
navigation. And entries beyond the first page are not in the HTML, so a crawler that
|
|
595
|
+
executes no JavaScript will not see those links; the sitemap and the
|
|
596
|
+
[feed](./docs/reference/config.md#optionsfeed--an-rss-feed-for-the-site) are what makes
|
|
597
|
+
them discoverable.
|
|
598
|
+
|
|
599
|
+
---
|
|
600
|
+
|
|
463
601
|
### `<ScavoldImage>`
|
|
464
602
|
|
|
465
603
|
Renders a responsive `<picture>` element for local images. Not intended for direct
|
|
@@ -565,10 +703,14 @@ Must be called inside a component's `setup` function (or `<script setup>`).
|
|
|
565
703
|
| `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. |
|
|
566
704
|
| `isOnActivePath` | `(node) => boolean` | Returns `true` if `node` is the current page or one of its ancestors |
|
|
567
705
|
| `collectItems` | `(parent, depth, activeOnly, expand) => MenuItem[]` | Recursively collects menu items from `parent`'s children. Used internally by `ScavoldMenu`. |
|
|
706
|
+
| `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`. |
|
|
568
707
|
| `collectAncestors` | `(includeCurrent, includeRoot) => MenuItem[]` | Returns the ancestor chain from root to the current page as a flat `MenuItem` array. Used internally by `ScavoldBreadcrumb`. |
|
|
569
708
|
| `currentLocale` | `ComputedRef<string>` | The resolved locale for the current page. Falls back to the browser locale when no page or ancestor declares one. |
|
|
570
709
|
| `collectLocaleLinks` | `(detection, includeCurrent?) => LocaleLink[]` | Returns available locale links for the current page. See `ScavoldLocaleMenu` for detection mode details. |
|
|
571
710
|
| `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. |
|
|
711
|
+
| `nodeHref` | `(node) => string` | Root-relative link target of a node, preferring its alias URL. Every page is linked as the file it is built into: `/blog/post.html`, a folder's index page as `/blog/index.html`, the site's own as `/index.html`. |
|
|
712
|
+
| `nodeLabel` | `(node) => string` | Navigation text of a node: `label`, then `title`, then the path segment. For menus and breadcrumbs, where space is scarce. |
|
|
713
|
+
| `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. |
|
|
572
714
|
|
|
573
715
|
#### `HierarchyNode` properties
|
|
574
716
|
|
|
@@ -577,13 +719,17 @@ Must be called inside a component's `setup` function (or `<script setup>`).
|
|
|
577
719
|
| `path` | `string` | Relative path of the Markdown source file |
|
|
578
720
|
| `isPage` | `boolean` | `true` for actual pages, `false` for intermediate folder nodes |
|
|
579
721
|
| `frontmatter` | `object` | Parsed front matter data |
|
|
580
|
-
| `frontmatter.hide` | `boolean \|
|
|
722
|
+
| `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). |
|
|
581
723
|
| `frontmatter.order` | `number?` | Sort position among siblings. Pages with a lower value appear first; pages without `order` follow in filename order. |
|
|
582
724
|
| `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. |
|
|
583
725
|
| `title` | `string?` | Display title — from frontmatter `title` or the first `#` heading |
|
|
584
726
|
| `label` | `string?` | Navigation label — from frontmatter `label`; falls back to `title` in menus |
|
|
585
727
|
| `url` | `string?` | Normalised alias output path (e.g. `de/impressum.md`) when `frontmatter.url` is set. Used by `nodeHref` as the page's href. |
|
|
586
728
|
| `locale` | `string?` | Resolved locale (BCP 47), inherited from ancestors |
|
|
729
|
+
| `excerpt` | `string?` | Introductory text — from frontmatter `excerpt` or `description`, else derived from the page's opening prose |
|
|
730
|
+
| `image` | `string?` | Lead image reference as the author wrote it — from frontmatter `image`, else the first image in the page |
|
|
731
|
+
| `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. |
|
|
732
|
+
| `date` | `string?` | Publication date as an ISO 8601 timestamp — from frontmatter `date` |
|
|
587
733
|
| `parent` | `HierarchyNode?` | Parent node — non-enumerable, not serialised to JSON |
|
|
588
734
|
| `subs` | `object?` | Child nodes keyed by path segment, sorted by `order` then filename |
|
|
589
735
|
|
|
@@ -661,6 +807,101 @@ Spread into `defineProps` to declare all video-related props at once.
|
|
|
661
807
|
|
|
662
808
|
---
|
|
663
809
|
|
|
810
|
+
### `usePageList( props )`
|
|
811
|
+
|
|
812
|
+
```js
|
|
813
|
+
import { usePageList, pageListProps } from "scavold/composables/usePageList.js";
|
|
814
|
+
```
|
|
815
|
+
|
|
816
|
+
Composable for custom page-list components — the way a theme renders lists its own
|
|
817
|
+
way instead of restyling Scavold's markup. Write a component, declare
|
|
818
|
+
`pageListProps`, call the composable and render whatever the design asks for; map a
|
|
819
|
+
container name to it in the site's configuration:
|
|
820
|
+
|
|
821
|
+
```yaml
|
|
822
|
+
# .cratly.config.yaml
|
|
823
|
+
containers:
|
|
824
|
+
postlist:
|
|
825
|
+
label: "Posts, our way"
|
|
826
|
+
component: SitePostList
|
|
827
|
+
```
|
|
828
|
+
|
|
829
|
+
```js
|
|
830
|
+
// .vitepress/theme/index.js — register it after Scavold's own components
|
|
831
|
+
async function enhanceApp( context ) {
|
|
832
|
+
await scavoldEnhanceApp( context );
|
|
833
|
+
|
|
834
|
+
context.app.component( "SitePostList", SitePostList );
|
|
835
|
+
}
|
|
836
|
+
```
|
|
837
|
+
|
|
838
|
+
Selection, ordering, paging and timestamp rendering stay Scavold's; only the markup
|
|
839
|
+
is the theme's. Registering a component under a name Scavold already uses
|
|
840
|
+
(`ScavoldPageList`) replaces that one everywhere instead. Resolves the `:::pagelist` container props
|
|
841
|
+
into the entries to render, each carrying its link target, title, date, introductory
|
|
842
|
+
text and responsive teaser image data — see the
|
|
843
|
+
[arguments](#scavoldpagelist) for what each prop means.
|
|
844
|
+
|
|
845
|
+
#### `pageListProps`
|
|
846
|
+
|
|
847
|
+
Spread into `defineProps` to declare all page-list props at once. Each container
|
|
848
|
+
argument arrives as `data…`, in the same way as `videoProps`: `dataFrom` for `from=…`,
|
|
849
|
+
`dataHeadingLevel` for `heading-level=…`, `dataTeaser` for the `teaser` flag, and so on.
|
|
850
|
+
|
|
851
|
+
#### Returns
|
|
852
|
+
|
|
853
|
+
| Name | Type | Description |
|
|
854
|
+
|---|---|---|
|
|
855
|
+
| `items` | `ComputedRef<PageListItem[]>` | The selected, ordered and limited entries |
|
|
856
|
+
| `headingTag` | `ComputedRef<string \| null>` | `"h2"`…`"h6"` when a heading level was requested, `null` for a plain list of links |
|
|
857
|
+
| `teaser` | `ComputedRef<boolean>` | `true` when the `teaser` flag is set |
|
|
858
|
+
| `images` | `ComputedRef<boolean>` | `true` when the `images` flag is set |
|
|
859
|
+
| `dates` | `ComputedRef<boolean>` | `true` when the `dates` flag is set |
|
|
860
|
+
| `imageSizes` | `ComputedRef<string>` | CSS `sizes` descriptor for the teaser images |
|
|
861
|
+
| `ariaLabel` | `ComputedRef<string \| undefined>` | Accessible label for the list, `undefined` when the author set none |
|
|
862
|
+
| `formatDate` | `(isoDate: string) => string` | Renders a timestamp in the styles and zone the list was given |
|
|
863
|
+
| `page` | `Ref<number>` | Page currently shown, `1` before the browser reads the query |
|
|
864
|
+
| `pages` | `ComputedRef<number[]>` | Page numbers to offer, `[ 1 ]` for an unpaginated list |
|
|
865
|
+
| `pageCount` | `ComputedRef<number>` | Number of pages the selection divides into |
|
|
866
|
+
| `goToPage` | `(page: number) => void` | Turns to a page and remembers it in the URL |
|
|
867
|
+
|
|
868
|
+
#### `PageListItem` properties
|
|
869
|
+
|
|
870
|
+
| Property | Type | Description |
|
|
871
|
+
|---|---|---|
|
|
872
|
+
| `node` | `HierarchyNode` | The listed page's hierarchy node |
|
|
873
|
+
| `href` | `string` | Root-relative link target, alias URL included |
|
|
874
|
+
| `title` | `string` | Full title of the page (`title` wins over `label` here) |
|
|
875
|
+
| `date` | `string?` | ISO 8601 publication date, when the page declares one |
|
|
876
|
+
| `excerpt` | `string?` | Introductory text, when the page has or yields one |
|
|
877
|
+
| `image` | `{ src, srcset, webpSrcset }?` | Responsive variant data of the teaser image |
|
|
878
|
+
|
|
879
|
+
---
|
|
880
|
+
|
|
881
|
+
### `formatTimestamp( timestamp, options )`
|
|
882
|
+
|
|
883
|
+
```js
|
|
884
|
+
import { formatTimestamp } from "scavold/lib/dateFormat.js";
|
|
885
|
+
```
|
|
886
|
+
|
|
887
|
+
Renders a timestamp the way page lists do, for a theme's own components — a byline
|
|
888
|
+
under a post's title, for instance. Free of Vue and VitePress, so build-time code can
|
|
889
|
+
use it too.
|
|
890
|
+
|
|
891
|
+
| Option | Type | Default | Description |
|
|
892
|
+
|---|---|---|---|
|
|
893
|
+
| `locale` | `string` | the runtime's | Locale to phrase the result in |
|
|
894
|
+
| `dateStyle` | `"short" \| "medium" \| "long" \| "full" \| "iso"` | `"long"` | Style of the date; `iso` writes `2020-10-20` in every language |
|
|
895
|
+
| `timeStyle` | same values | — | Style of the time of day; omitted, only the date is rendered |
|
|
896
|
+
| `timeZone` | `string` | `"UTC"` | IANA name of the zone the timestamp is read in |
|
|
897
|
+
|
|
898
|
+
The result depends on the arguments alone, never on the machine it runs on, so
|
|
899
|
+
pre-rendered markup and the browser agree — a timestamp that changes on hydration
|
|
900
|
+
would tear the markup apart. A timestamp that cannot be read renders as an empty
|
|
901
|
+
string rather than as `Invalid Date`.
|
|
902
|
+
|
|
903
|
+
---
|
|
904
|
+
|
|
664
905
|
### `useContainer( props )`
|
|
665
906
|
|
|
666
907
|
```js
|
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
|
---
|
|
@@ -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>
|