scavold 0.2.0-rc.3 → 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 CHANGED
@@ -7,6 +7,58 @@ 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
+
10
62
  ## [0.2.0-rc.3] — 2026-08-09
11
63
 
12
64
  ### Added
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` | Hidden in menus **and** breadcrumbs |
76
- | `"menu"` | Hidden in menus only |
77
- | `"breadcrumb"` | Hidden in breadcrumbs only |
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
@@ -565,10 +701,14 @@ Must be called inside a component's `setup` function (or `<script setup>`).
565
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. |
566
702
  | `isOnActivePath` | `(node) => boolean` | Returns `true` if `node` is the current page or one of its ancestors |
567
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`. |
568
705
  | `collectAncestors` | `(includeCurrent, includeRoot) => MenuItem[]` | Returns the ancestor chain from root to the current page as a flat `MenuItem` array. Used internally by `ScavoldBreadcrumb`. |
569
706
  | `currentLocale` | `ComputedRef<string>` | The resolved locale for the current page. Falls back to the browser locale when no page or ancestor declares one. |
570
707
  | `collectLocaleLinks` | `(detection, includeCurrent?) => LocaleLink[]` | Returns available locale links for the current page. See `ScavoldLocaleMenu` for detection mode details. |
571
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. |
572
712
 
573
713
  #### `HierarchyNode` properties
574
714
 
@@ -577,13 +717,17 @@ Must be called inside a component's `setup` function (or `<script setup>`).
577
717
  | `path` | `string` | Relative path of the Markdown source file |
578
718
  | `isPage` | `boolean` | `true` for actual pages, `false` for intermediate folder nodes |
579
719
  | `frontmatter` | `object` | Parsed front matter data |
580
- | `frontmatter.hide` | `boolean \| "menu" \| "breadcrumb"` | Excludes this node from menus, breadcrumbs, or both. See [Hiding pages via front matter](#hiding-pages-via-front-matter). |
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). |
581
721
  | `frontmatter.order` | `number?` | Sort position among siblings. Pages with a lower value appear first; pages without `order` follow in filename order. |
582
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. |
583
723
  | `title` | `string?` | Display title — from frontmatter `title` or the first `#` heading |
584
724
  | `label` | `string?` | Navigation label — from frontmatter `label`; falls back to `title` in menus |
585
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. |
586
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` |
587
731
  | `parent` | `HierarchyNode?` | Parent node — non-enumerable, not serialised to JSON |
588
732
  | `subs` | `object?` | Child nodes keyed by path segment, sorted by `order` then filename |
589
733
 
@@ -661,6 +805,49 @@ Spread into `defineProps` to declare all video-related props at once.
661
805
 
662
806
  ---
663
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
+
664
851
  ### `useContainer( props )`
665
852
 
666
853
  ```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 whether this page appears in menus, breadcrumbs, or both.
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 in both menus and breadcrumbs |
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, currentLocale } = useHierarchy();
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 ( navigator.languages ?? [] ) ) {
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
- function label( node: Scavold.HierarchyNode ): string {
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="href( item.node )" :aria-current="item.current ? 'page' : undefined">{{ label( item.node ) }}</a>
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
+ >&lsaquo;</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
+ >&rsaquo;</button>
108
+ </nav>
109
+ </div>
110
+ </template>