scavold 0.2.0-rc.4 → 0.2.0-rc.6
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 +112 -3
- package/COMPONENTS.md +60 -6
- package/FRONTMATTER.md +52 -0
- package/composables/hierarchy.ts +3 -3
- package/composables/useI18n.js +28 -3
- package/composables/usePageList.js +32 -35
- package/lib/config.js +203 -6
- package/lib/dateFormat.js +125 -0
- package/lib/feed.js +2 -15
- package/lib/href.d.ts +21 -0
- package/lib/href.js +62 -0
- package/lib/legacyUrls.js +179 -0
- package/lib/pageKeys.js +111 -0
- package/lib/pageList.js +38 -0
- package/lib/redirectTarget.js +6 -12
- package/lib/sectionManifest.js +14 -2
- package/lib/verifyBuild.js +324 -0
- package/package.json +4 -4
- package/scripts/check-fixture.js +72 -2
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,113 @@ changes may occur in any release.
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
+
## [0.2.0-rc.6] — 2026-09-13
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- `aliases` in a page's front matter names the former addresses it replaces, and
|
|
17
|
+
`retired_urls` in `.cratly.config.yaml` those that are not coming back. A site taking
|
|
18
|
+
over from an existing website declares them once and the build writes
|
|
19
|
+
`cratly-redirects.json` into its output: an address, where it went, and `301` or `410`.
|
|
20
|
+
The format names no server — only a server-side redirect passes a ranking on, and which
|
|
21
|
+
server that is cratly does not assume, so the deploy step translates the file for its
|
|
22
|
+
target. Keeping an address is still better than redirecting to it, which is what `url`
|
|
23
|
+
is for; the build refuses an alias that two pages claim, or that the site serves itself
|
|
24
|
+
and the redirect would therefore hide.
|
|
25
|
+
- Every build now ends by reading its own result. A page that the generator listed but
|
|
26
|
+
never wrote, or a script a built page names and the output does not contain, ends the
|
|
27
|
+
build with the file named — the two states a bundling fault leaves behind while the log
|
|
28
|
+
says "build complete". Links and images pointing at absent files are reported and left
|
|
29
|
+
to the site, since one may legitimately name a path the web server provides. Turn it
|
|
30
|
+
down with `verify: { content: false }` or off with `verify: false`. A reference is read
|
|
31
|
+
the way a browser reads it: a relative one against the address of the page holding it, so
|
|
32
|
+
a link that forgot its protocol — `[Website](./www.example.com)`, which the build turns
|
|
33
|
+
into a sibling page nobody wrote — is seen as well. Proven against the
|
|
34
|
+
case-collision that used to build green: the page missing from the output is the only
|
|
35
|
+
trace it leaves on a file system that ignores letter case, and that is now enough.
|
|
36
|
+
Everything found also goes to `.cratly/build-report.json`, written before the build is
|
|
37
|
+
ended rather than after, each entry naming the source file an author would open. A build
|
|
38
|
+
log is read by whoever has access to it; a file can be handed to whoever wrote the page.
|
|
39
|
+
- `ignoreDeadLinks` now defaults to `true`. VitePress ends a build on a dead link and does
|
|
40
|
+
so before writing anything, which leaves the reason in a job log that whoever wrote the
|
|
41
|
+
link often cannot read, and lets one typo stand between a finished page and its
|
|
42
|
+
publication. Scavold finds the same mistake in the finished output and reports it there
|
|
43
|
+
instead. Whether it should also stop a pipeline is a question whose answer differs per
|
|
44
|
+
branch, so it is left to CI: the site template fails a merge request over it and lets the
|
|
45
|
+
default branch deploy, since blocking there would answer a broken link with a stale site.
|
|
46
|
+
Ask for VitePress' abort back with `ignoreDeadLinks: false`.
|
|
47
|
+
- A site whose page paths collide in VitePress's eyes now learns that from Scavold, by
|
|
48
|
+
name, before the build starts. VitePress identifies a page by its path flattened into
|
|
49
|
+
a single name — slash becomes underscore, and the page-hash map lower-cases it — and
|
|
50
|
+
uses that name for the page's bundle entry, its server module and its client chunk.
|
|
51
|
+
Two pages meeting there overwrite each other: either the build dies at render time in
|
|
52
|
+
`pageChunk.imports`, naming no file, or it succeeds and ships two pages pointing at a
|
|
53
|
+
script that was never written. Scavold now reproduces that identity when it reads the
|
|
54
|
+
config and aborts with both file names, whether they collide through an underscore in
|
|
55
|
+
a file name (`de_kontakt.md` beside `de/kontakt.md`), through letter case alone, or
|
|
56
|
+
through a `url` alias landing on a path another page already owns as its file — the
|
|
57
|
+
case the previous alias check, which compared aliases only with each other, let pass.
|
|
58
|
+
- `date` and `datetime` join the type vocabulary of `frontmatter_fields` and of a
|
|
59
|
+
container's `props`. Scavold passes such a value through as the ISO 8601 text it is;
|
|
60
|
+
what changes is that the cratly editor now offers a date picker for it instead of a
|
|
61
|
+
plain text box. A `datetime` always carries an offset — a time of day without one is
|
|
62
|
+
a different moment on every machine that builds the site.
|
|
63
|
+
|
|
64
|
+
### Changed
|
|
65
|
+
|
|
66
|
+
- `@cepharum/vue3-i18n` moves to `2.0.0`, two majors on from the `0.5.4` a site would
|
|
67
|
+
have installed. Scavold uses `useL10n`, `setLocale` and `setLoader`, none of which
|
|
68
|
+
changed; what 2.0 breaks are the locale helpers now returning full BCP-47 tags, and
|
|
69
|
+
Scavold calls none of them. A site inherits the new version with its next install, and
|
|
70
|
+
gains what 2.0 adds along the way: regional locales overlaying a bare language, plural
|
|
71
|
+
and gender selection resolved through `Intl`, and `Intl`-backed formatters in
|
|
72
|
+
placeholders.
|
|
73
|
+
|
|
74
|
+
### Fixed
|
|
75
|
+
|
|
76
|
+
- `lib/href.js` ships the declarations it had been missing since it was split out, so
|
|
77
|
+
`typecheck` passes again — and the pipeline runs it now, which is why it could go
|
|
78
|
+
unnoticed at all. Nothing about the module changed; TypeScript simply had nothing to
|
|
79
|
+
read about it, and treated every href the hierarchy composes as `any`.
|
|
80
|
+
|
|
81
|
+
## [0.2.0-rc.5] — 2026-08-17
|
|
82
|
+
|
|
83
|
+
### Added
|
|
84
|
+
|
|
85
|
+
- `:::pagelist` dates its entries the way the site wants them: `time-style` adds the
|
|
86
|
+
time of day, `timezone` names the zone the timestamps are read in, and both accept
|
|
87
|
+
`iso` alongside the `Intl` styles, writing `2020-10-20 12:16` in every language. The
|
|
88
|
+
default is unchanged — the date alone, read in UTC, which is where a front matter
|
|
89
|
+
value carrying no time of day belongs.
|
|
90
|
+
- `formatTimestamp()` in `scavold/lib/dateFormat.js` renders timestamps the same way
|
|
91
|
+
for a theme's own components — a byline under a post's title, say. It is free of Vue
|
|
92
|
+
and VitePress and depends on its arguments alone, so pre-rendered markup and the
|
|
93
|
+
browser agree.
|
|
94
|
+
|
|
95
|
+
### Changed
|
|
96
|
+
|
|
97
|
+
- A `:::pagelist` that paginates reaches every page it found: `limit` now defaults to
|
|
98
|
+
`0` when `per-page` is set, rather than capping the selection at five entries and
|
|
99
|
+
leaving a pager to turn the pages of those five. A limit the author sets still wins.
|
|
100
|
+
- Every link a site generates — menus, breadcrumbs, page lists, the feed, redirect targets
|
|
101
|
+
— names the file the page is built into: `/blog/post.html`, `/blog/index.html`,
|
|
102
|
+
`/index.html`. Links used to leave the extension off while VitePress's own markdown
|
|
103
|
+
links carried it, so one site spoke two dialects and the shorter one only worked on
|
|
104
|
+
servers that go looking for the file. Naming it is answered on the first attempt, and
|
|
105
|
+
the sitemap now lists the same URLs. The address bar still shows `/` and `/blog/`:
|
|
106
|
+
VitePress's router shortens them while navigating.
|
|
107
|
+
|
|
108
|
+
### Fixed
|
|
109
|
+
|
|
110
|
+
- The site's own `index.md` is no longer linked as `/index`, a URL no server has to
|
|
111
|
+
answer. The rule for a page's URL existed in three copies and now lives in
|
|
112
|
+
`lib/href.js` alone.
|
|
113
|
+
- Scavold's built-in translations come from the bundle. They used to be fetched from a
|
|
114
|
+
URL that no build emits, so every page requested a file that was not there and the
|
|
115
|
+
interface silently fell back to English, whatever language the site is in.
|
|
116
|
+
|
|
117
|
+
## [0.2.0-rc.4] — 2026-08-15
|
|
118
|
+
|
|
12
119
|
### Added
|
|
13
120
|
|
|
14
121
|
- `metaChunk` is on by default, keeping VitePress's page-hash map out of the inline
|
|
@@ -143,6 +250,8 @@ First published release. Version `0.1.0` existed in-tree only.
|
|
|
143
250
|
- Only images are processed out of `media_folder`; other file types (video, documents)
|
|
144
251
|
are never copied into the build output and have to live in the static folder.
|
|
145
252
|
|
|
146
|
-
[0.2.0-rc.
|
|
147
|
-
[0.2.0-rc.
|
|
148
|
-
[0.2.0-rc.
|
|
253
|
+
[0.2.0-rc.5]: https://gitlab.com/cratly/scavold/-/compare/v0.2.0-rc.4...v0.2.0-rc.5
|
|
254
|
+
[0.2.0-rc.4]: https://gitlab.com/cratly/scavold/-/compare/v0.2.0-rc.3...v0.2.0-rc.4
|
|
255
|
+
[0.2.0-rc.3]: https://gitlab.com/cratly/scavold/-/compare/v0.2.0-rc.2...v0.2.0-rc.3
|
|
256
|
+
[0.2.0-rc.2]: https://gitlab.com/cratly/scavold/-/compare/v0.2.0-rc.1...v0.2.0-rc.2
|
|
257
|
+
[0.2.0-rc.1]: https://gitlab.com/cratly/scavold/-/tags/v0.2.0-rc.1
|
package/COMPONENTS.md
CHANGED
|
@@ -504,7 +504,7 @@ Which pages are listed:
|
|
|
504
504
|
| `from-root` | `number` | — | Absolute level from the site root: `1` = top-level pages, `2` = second level. Takes precedence over `from`. |
|
|
505
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
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. |
|
|
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
508
|
| `offset` | `number` | `0` | Entries to skip at the start, so a second block can continue where the first one ended. |
|
|
509
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
510
|
| `reverse` | flag | — | Reverse whichever order was selected. |
|
|
@@ -516,7 +516,9 @@ What each entry shows:
|
|
|
516
516
|
| `teaser` | flag | — | Show the introductory text of each listed page. |
|
|
517
517
|
| `images` | flag | — | Show the teaser image of each listed page. |
|
|
518
518
|
| `dates` | flag | — | Show the publication date of each listed page. |
|
|
519
|
-
| `date-style` | `"short" \| "medium" \| "long" \| "full"` | `"long"` | `Intl.DateTimeFormat`
|
|
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. |
|
|
520
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. |
|
|
521
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). |
|
|
522
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. |
|
|
@@ -706,7 +708,7 @@ Must be called inside a component's `setup` function (or `<script setup>`).
|
|
|
706
708
|
| `currentLocale` | `ComputedRef<string>` | The resolved locale for the current page. Falls back to the browser locale when no page or ancestor declares one. |
|
|
707
709
|
| `collectLocaleLinks` | `(detection, includeCurrent?) => LocaleLink[]` | Returns available locale links for the current page. See `ScavoldLocaleMenu` for detection mode details. |
|
|
708
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. |
|
|
709
|
-
| `nodeHref` | `(node) => string` | Root-relative link target of a node, preferring its alias URL. |
|
|
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`. |
|
|
710
712
|
| `nodeLabel` | `(node) => string` | Navigation text of a node: `label`, then `title`, then the path segment. For menus and breadcrumbs, where space is scarce. |
|
|
711
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. |
|
|
712
714
|
|
|
@@ -808,10 +810,34 @@ Spread into `defineProps` to declare all video-related props at once.
|
|
|
808
810
|
### `usePageList( props )`
|
|
809
811
|
|
|
810
812
|
```js
|
|
811
|
-
import { usePageList, pageListProps } from "
|
|
813
|
+
import { usePageList, pageListProps } from "scavold/composables/usePageList.js";
|
|
812
814
|
```
|
|
813
815
|
|
|
814
|
-
Composable for custom page-list components
|
|
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
|
|
815
841
|
into the entries to render, each carrying its link target, title, date, introductory
|
|
816
842
|
text and responsive teaser image data — see the
|
|
817
843
|
[arguments](#scavoldpagelist) for what each prop means.
|
|
@@ -833,7 +859,11 @@ argument arrives as `data…`, in the same way as `videoProps`: `dataFrom` for `
|
|
|
833
859
|
| `dates` | `ComputedRef<boolean>` | `true` when the `dates` flag is set |
|
|
834
860
|
| `imageSizes` | `ComputedRef<string>` | CSS `sizes` descriptor for the teaser images |
|
|
835
861
|
| `ariaLabel` | `ComputedRef<string \| undefined>` | Accessible label for the list, `undefined` when the author set none |
|
|
836
|
-
| `formatDate` | `(isoDate: string) => string` |
|
|
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 |
|
|
837
867
|
|
|
838
868
|
#### `PageListItem` properties
|
|
839
869
|
|
|
@@ -848,6 +878,30 @@ argument arrives as `data…`, in the same way as `videoProps`: `dataFrom` for `
|
|
|
848
878
|
|
|
849
879
|
---
|
|
850
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
|
+
|
|
851
905
|
### `useContainer( props )`
|
|
852
906
|
|
|
853
907
|
```js
|
package/FRONTMATTER.md
CHANGED
|
@@ -107,6 +107,20 @@ Menu links and breadcrumbs produced by Scavold automatically use the alias URL.
|
|
|
107
107
|
with an error naming both conflicting files. Each alias must be unique across the
|
|
108
108
|
entire site.
|
|
109
109
|
|
|
110
|
+
The same applies when an alias lands on a path another page already owns as its own
|
|
111
|
+
file — a `url: de/kontakt` next to a `pages/de/kontakt.md`. VitePress identifies a
|
|
112
|
+
page by its path flattened into one name, slash becoming underscore, and it uses that
|
|
113
|
+
name for the page's bundle entry, its server module and its client chunk. Two pages
|
|
114
|
+
meeting there overwrite each other's output; the alias does not win, it collides. So
|
|
115
|
+
does a page whose file name spells out a path another page reaches through folders
|
|
116
|
+
(`de_kontakt.md` beside `de/kontakt.md`), and a pair differing only in letter case.
|
|
117
|
+
Scavold checks all of these when it reads the config and names both files. Prefer `-`
|
|
118
|
+
over `_` as the word separator in page names and the class cannot arise.
|
|
119
|
+
|
|
120
|
+
To move a page and keep its URL, rename the file and declare the old URL as its
|
|
121
|
+
alias — the alias belongs to the page that carries the content, not to a second file
|
|
122
|
+
left behind at the old location.
|
|
123
|
+
|
|
110
124
|
---
|
|
111
125
|
|
|
112
126
|
## `locale` / `lang`
|
|
@@ -260,6 +274,44 @@ in the hierarchy tree — they are only excluded from rendered navigation.
|
|
|
260
274
|
|
|
261
275
|
---
|
|
262
276
|
|
|
277
|
+
## `aliases`
|
|
278
|
+
|
|
279
|
+
**Type:** `string | string[]`
|
|
280
|
+
|
|
281
|
+
Former addresses this page replaces. Declared when a cratly site takes over from an
|
|
282
|
+
existing website: every address a search engine already knows and the new site does not
|
|
283
|
+
answer is a result its owner loses.
|
|
284
|
+
|
|
285
|
+
```yaml
|
|
286
|
+
---
|
|
287
|
+
aliases:
|
|
288
|
+
- /kontakt.html
|
|
289
|
+
- kontakt.php
|
|
290
|
+
---
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
Values are written as they stood in the browser, with or without a leading slash. A
|
|
294
|
+
fragment is dropped — it never reaches a server. A query string is kept; whether it can
|
|
295
|
+
be matched is up to whoever serves the site.
|
|
296
|
+
|
|
297
|
+
Scavold collects them and writes `cratly-redirects.json` into the build output: an
|
|
298
|
+
address, where it went, and a status — `301` for an inherited address, `410` for one a
|
|
299
|
+
site declares as gone for good in
|
|
300
|
+
[`retired_urls`](./docs/reference/config.md#retired-urls). Turning that into server
|
|
301
|
+
configuration is the deploy step's job, because cratly does not assume who serves the
|
|
302
|
+
site. Nothing about the file is Scavold-specific: another adapter writes the same format.
|
|
303
|
+
|
|
304
|
+
**Keeping the address beats redirecting to it.** A permanent redirect passes a ranking on;
|
|
305
|
+
a page that simply answers at the old address never spends it. Where the legacy path can
|
|
306
|
+
be a file, [`url`](#url) is the better tool, and it works on every static server, whether
|
|
307
|
+
or not it can redirect at all.
|
|
308
|
+
|
|
309
|
+
**Conflict detection:** the build stops when two pages claim the same former address, and
|
|
310
|
+
when a former address is one the new site serves itself — the redirect would win over the
|
|
311
|
+
page, leaving it unreachable while every menu still links to it.
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
263
315
|
## `redirect`
|
|
264
316
|
|
|
265
317
|
**Type:** `string | object`
|
package/composables/hierarchy.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { useData } from "vitepress";
|
|
|
2
2
|
import { computed } from "vue";
|
|
3
3
|
import { useL10n } from "@cepharum/vue3-i18n";
|
|
4
4
|
import { hidesFrom } from "../lib/hide.js";
|
|
5
|
+
import { nodePath, pageHref } from "../lib/href.js";
|
|
5
6
|
import type { Scavold } from "../index";
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -262,8 +263,7 @@ export function useHierarchy() {
|
|
|
262
263
|
* Converts a hierarchy node path to a root-relative href.
|
|
263
264
|
* Prefers node.url (the alias output path) when declared.
|
|
264
265
|
*/
|
|
265
|
-
const nodeHref = ( node: Scavold.HierarchyNode ): string =>
|
|
266
|
-
"/" + ( node.url ?? node.path ).replace( /\.md$/, "" ).replace( /\/index$/, "/" );
|
|
266
|
+
const nodeHref = ( node: Scavold.HierarchyNode ): string => nodePath( node );
|
|
267
267
|
|
|
268
268
|
/**
|
|
269
269
|
* The last meaningful segment of a node's source path, used wherever a page
|
|
@@ -402,7 +402,7 @@ export function useHierarchy() {
|
|
|
402
402
|
if ( translations ) {
|
|
403
403
|
for ( const [ loc, path ] of Object.entries( translations ) ) {
|
|
404
404
|
if ( !links.has( loc ) ) {
|
|
405
|
-
links.set( loc,
|
|
405
|
+
links.set( loc, pageHref( String( path ) ) );
|
|
406
406
|
}
|
|
407
407
|
}
|
|
408
408
|
}
|
package/composables/useI18n.js
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
import { useL10n } from "@cepharum/vue3-i18n";
|
|
2
2
|
|
|
3
|
+
import de from "../l10n/de.json";
|
|
4
|
+
import en from "../l10n/en.json";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Scavold's own translations, keyed by language. They are part of the bundle on
|
|
8
|
+
* purpose: fetching them at runtime would have to name a URL, and a package that
|
|
9
|
+
* ships as source inside another site's build cannot know one. Each file is a few
|
|
10
|
+
* hundred bytes, and a site is free to override any key through its own loader.
|
|
11
|
+
*/
|
|
12
|
+
const TRANSLATIONS = { de, en };
|
|
13
|
+
|
|
14
|
+
/** Language served to anyone whose locale Scavold does not translate. */
|
|
15
|
+
const FALLBACK_LOCALE = "en";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Picks the translations for a locale, accepting regional tags: `de-AT` is served
|
|
19
|
+
* the German set rather than falling through to English.
|
|
20
|
+
*
|
|
21
|
+
* @param {string} locale locale tag as requested by the l10n context
|
|
22
|
+
* @returns {object} set of translations
|
|
23
|
+
*/
|
|
24
|
+
export function translationsFor( locale ) {
|
|
25
|
+
const language = String( locale ?? "" ).toLowerCase().split( /[-_]/ )[0];
|
|
26
|
+
|
|
27
|
+
return TRANSLATIONS[language] ?? TRANSLATIONS[FALLBACK_LOCALE];
|
|
28
|
+
}
|
|
29
|
+
|
|
3
30
|
/**
|
|
4
31
|
* Registers Scavold's built-in translations into the shared l10n context under
|
|
5
32
|
* the `@scavold` namespace. All keys are accessed as `@scavold.<key>`.
|
|
@@ -13,9 +40,7 @@ import { useL10n } from "@cepharum/vue3-i18n";
|
|
|
13
40
|
*/
|
|
14
41
|
export function registerScavoldI18n() {
|
|
15
42
|
useL10n().setNamespaceLoader( "@scavold", locale =>
|
|
16
|
-
|
|
17
|
-
import( "../l10n/en.json" )
|
|
18
|
-
)
|
|
43
|
+
Promise.resolve( translationsFor( locale ) )
|
|
19
44
|
);
|
|
20
45
|
}
|
|
21
46
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { computed, onMounted, ref } from "vue";
|
|
2
2
|
import { containerProps } from "./useContainer.js";
|
|
3
3
|
import { useHierarchy } from "./hierarchy";
|
|
4
|
+
import { formatTimestamp, TIMESTAMP_STYLES, DEFAULT_TIMEZONE } from "../lib/dateFormat.js";
|
|
5
|
+
import { asNumber, selectionLimit } from "../lib/pageList.js";
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
8
|
* Props definition for page-list container components. Spread into `defineProps`
|
|
@@ -49,9 +51,20 @@ export const pageListProps = {
|
|
|
49
51
|
/** Show the publication date of each listed page. */
|
|
50
52
|
dataDates: { type: String, default: null },
|
|
51
53
|
|
|
52
|
-
/**
|
|
54
|
+
/** Style of the dates: "short", "medium", "long", "full" — as Intl formats them
|
|
55
|
+
* in the page's locale — or "iso" for the locale-independent 2020-10-20. */
|
|
53
56
|
dataDateStyle: { type: String, default: null },
|
|
54
57
|
|
|
58
|
+
/** Style of the time of day, in the same values as `data-date-style`. Left out,
|
|
59
|
+
* entries show their date only. */
|
|
60
|
+
dataTimeStyle: { type: String, default: null },
|
|
61
|
+
|
|
62
|
+
/** IANA name of the zone the dates are read in, e.g. "Europe/Berlin". Defaults
|
|
63
|
+
* to UTC, the zone dates are normalised into at build time — which is what a
|
|
64
|
+
* date without a time of day should be shown in. A list that shows times wants
|
|
65
|
+
* the zone the site publishes from instead. */
|
|
66
|
+
dataTimezone: { type: String, default: null },
|
|
67
|
+
|
|
55
68
|
/** Heading level of the entry titles, 2–6. 0 renders a plain list of links. */
|
|
56
69
|
dataHeadingLevel: { type: String, default: null },
|
|
57
70
|
|
|
@@ -75,21 +88,8 @@ export const pageListProps = {
|
|
|
75
88
|
/** Orders a page list understands. */
|
|
76
89
|
const SORT_ORDERS = new Set( [ "order", "date", "date-asc", "title" ] );
|
|
77
90
|
|
|
78
|
-
/**
|
|
79
|
-
const DATE_STYLES = new Set(
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Reads a container argument that carries a number.
|
|
83
|
-
*
|
|
84
|
-
* @param {string|null} value raw attribute value
|
|
85
|
-
* @param {number} fallback value to use when absent or unusable
|
|
86
|
-
* @returns {number}
|
|
87
|
-
*/
|
|
88
|
-
function asNumber( value, fallback ) {
|
|
89
|
-
const parsed = Number.parseInt( value, 10 );
|
|
90
|
-
|
|
91
|
-
return Number.isNaN( parsed ) ? fallback : parsed;
|
|
92
|
-
}
|
|
91
|
+
/** Styles a page list renders its dates in. */
|
|
92
|
+
const DATE_STYLES = new Set( TIMESTAMP_STYLES );
|
|
93
93
|
|
|
94
94
|
/**
|
|
95
95
|
* Compares two nodes by date, newest first. Pages without a date sort last, in
|
|
@@ -130,7 +130,7 @@ export function usePageList( props ) {
|
|
|
130
130
|
const deep = computed( () => props.dataDeep != null );
|
|
131
131
|
const reverse = computed( () => props.dataReverse != null );
|
|
132
132
|
|
|
133
|
-
const limit = computed( () =>
|
|
133
|
+
const limit = computed( () => selectionLimit( props.dataLimit, props.dataPerPage ) );
|
|
134
134
|
const offset = computed( () => Math.max( 0, asNumber( props.dataOffset, 0 ) ) );
|
|
135
135
|
|
|
136
136
|
const sort = computed( () =>
|
|
@@ -141,6 +141,14 @@ export function usePageList( props ) {
|
|
|
141
141
|
( DATE_STYLES.has( props.dataDateStyle ) ? props.dataDateStyle : "long" )
|
|
142
142
|
);
|
|
143
143
|
|
|
144
|
+
// No time of day unless the list asks for one: most lists date an entry, they do
|
|
145
|
+
// not timestamp it.
|
|
146
|
+
const timeStyle = computed( () =>
|
|
147
|
+
( DATE_STYLES.has( props.dataTimeStyle ) ? props.dataTimeStyle : null )
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const timezone = computed( () => props.dataTimezone || DEFAULT_TIMEZONE );
|
|
151
|
+
|
|
144
152
|
// A plain list of links carries no headings; teaser cards do, and their level
|
|
145
153
|
// has to fit the document outline around them, which only the author knows.
|
|
146
154
|
const headingTag = computed( () => {
|
|
@@ -313,29 +321,18 @@ export function usePageList( props ) {
|
|
|
313
321
|
const pages = computed( () => Array.from( { length: pageCount.value }, ( _, index ) => index + 1 ) );
|
|
314
322
|
|
|
315
323
|
/**
|
|
316
|
-
*
|
|
317
|
-
* rendered in UTC, the zone they were normalised into at build time, so a
|
|
318
|
-
* date-only front matter value never shifts to its neighbouring day.
|
|
324
|
+
* Renders an entry's timestamp in the styles and zone the list was given.
|
|
319
325
|
*
|
|
320
326
|
* @param {string} isoDate ISO 8601 timestamp
|
|
321
327
|
* @returns {string}
|
|
322
328
|
*/
|
|
323
329
|
function formatDate( isoDate ) {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
try {
|
|
331
|
-
return new Intl.DateTimeFormat( currentLocale.value || undefined, {
|
|
332
|
-
dateStyle: dateStyle.value,
|
|
333
|
-
timeZone: "UTC",
|
|
334
|
-
} ).format( date );
|
|
335
|
-
} catch {
|
|
336
|
-
// An unusable locale tag must not take the page down with it.
|
|
337
|
-
return date.toISOString().slice( 0, 10 );
|
|
338
|
-
}
|
|
330
|
+
return formatTimestamp( isoDate, {
|
|
331
|
+
locale: currentLocale.value || undefined,
|
|
332
|
+
dateStyle: dateStyle.value,
|
|
333
|
+
timeStyle: timeStyle.value,
|
|
334
|
+
timeZone: timezone.value,
|
|
335
|
+
} );
|
|
339
336
|
}
|
|
340
337
|
|
|
341
338
|
return {
|