scavold 0.2.0-rc.1 → 0.2.0-rc.11

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.
Files changed (44) hide show
  1. package/CHANGELOG.md +348 -0
  2. package/COMPONENTS.md +484 -62
  3. package/FRONTMATTER.md +123 -3
  4. package/README.md +9 -4
  5. package/SECURITY.md +38 -0
  6. package/components/ScavoldCover.vue +162 -0
  7. package/components/ScavoldImage.vue +5 -1
  8. package/components/ScavoldLocaleMenu.vue +1 -1
  9. package/components/ScavoldLocaleRedirect.vue +1 -1
  10. package/components/ScavoldMenuItems.vue +3 -19
  11. package/components/ScavoldPageList.vue +110 -0
  12. package/components/ScavoldVideo.vue +12 -53
  13. package/composables/hierarchy.ts +75 -24
  14. package/composables/useAutoplay.js +128 -0
  15. package/composables/useContainer.js +1 -1
  16. package/composables/useCover.js +111 -0
  17. package/composables/useI18n.js +32 -6
  18. package/composables/usePageList.js +342 -0
  19. package/composables/useVideo.js +12 -22
  20. package/index.d.ts +45 -0
  21. package/l10n/de.json +12 -1
  22. package/l10n/en.json +12 -1
  23. package/lib/config.d.ts +1 -0
  24. package/lib/config.js +425 -55
  25. package/lib/containers.js +189 -12
  26. package/lib/dateFormat.js +125 -0
  27. package/lib/excerpt.js +144 -0
  28. package/lib/feed.js +109 -0
  29. package/lib/hide.d.ts +13 -0
  30. package/lib/hide.js +37 -0
  31. package/lib/href.d.ts +21 -0
  32. package/lib/href.js +62 -0
  33. package/lib/index.js +2 -3
  34. package/lib/legacyUrls.js +179 -0
  35. package/lib/media.js +125 -14
  36. package/lib/pageKeys.js +111 -0
  37. package/lib/pageList.js +38 -0
  38. package/lib/pages.js +113 -9
  39. package/lib/redirectTarget.js +6 -12
  40. package/lib/sectionManifest.js +144 -16
  41. package/lib/verifyBuild.js +324 -0
  42. package/package.json +17 -9
  43. package/scripts/check-csp.js +7 -1
  44. package/scripts/check-fixture.js +507 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,348 @@
1
+ # Changelog
2
+
3
+ All notable changes to Scavold are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), versions follow
5
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ While the version stays below `1.0.0` and carries a pre-release suffix, breaking
8
+ changes may occur in any release.
9
+
10
+ ## [Unreleased]
11
+
12
+ ## [0.2.0-rc.11] — 2026-10-01
13
+
14
+ ### Added
15
+
16
+ - A container opening a page carries an empty `data-leading` attribute, and the page
17
+ data name it as `leadingContainer` — known before the content renders, so a layout
18
+ can arrange its own parts around a cover at the top of a page, e.g. move its
19
+ breadcrumb below it. Both are information only; nothing changes for a theme that
20
+ does not ask.
21
+
22
+ ## [0.2.0-rc.10] — 2026-10-01
23
+
24
+ ### Added
25
+
26
+ - `:::cover` shows an image or a video across the full width and height of the
27
+ window, cropped to fit, with the block's content laid on top. An image comes with
28
+ all its variants and a `sizes` value that accounts for the cropping; a video plays
29
+ muted in a loop while at least half of it is on screen, stays still for visitors
30
+ asking for reduced motion, and always has a button to pause it. The editor offers it
31
+ as a section type.
32
+ - A container argument declared as `media-file` that names an image hands its
33
+ component the variants and dimensions as well, as `data-<arg>-srcset`,
34
+ `data-<arg>-webp-srcset`, `data-<arg>-width` and `data-<arg>-height`.
35
+ - `<ScavoldImage>` takes `loading` and `fetchpriority`, for an image at the top of a
36
+ page.
37
+ - `useAutoplay()` brings the same autoplay behaviour to a site's own video component.
38
+
39
+ ### Changed
40
+
41
+ - A `:::video` with `autoplay` plays only while at least half of it is on screen, and
42
+ pauses when scrolled away — unless the visitor turned the sound up. It stays paused
43
+ for visitors whose system asks for reduced motion, and once the visitor paused it.
44
+ The `autoplay` attribute is no longer part of the markup: it started the video at
45
+ once, wherever it was on the page and before reduced motion could be checked.
46
+
47
+ ### Removed
48
+
49
+ - The `overlay` and `controls` arguments of `:::video`. Use `:::cover` for a video
50
+ with content on top: the background mode had no way to pause a looping video, which
51
+ WCAG 2.2.2 requires, and ignored reduced motion. A `:::video` now always is a player
52
+ with its controls; a block still carrying `overlay` renders as one, with its content
53
+ as fallback text. `useVideo()` no longer returns `overlay` and `controls`, and the
54
+ `.scavold-video--overlay`, `.scavold-video__media` and `.scavold-video__overlay`
55
+ classes are gone.
56
+
57
+ ## [0.2.0-rc.9] — 2026-10-01
58
+
59
+ ### Fixed
60
+
61
+ - An image wider than the largest of `image_widths` named that width twice in its
62
+ `srcset`, and had its widest variant written twice at the same time.
63
+
64
+ ## [0.2.0-rc.8] — 2026-10-01
65
+
66
+ ### Added
67
+
68
+ - `SECURITY.md` says how to report a vulnerability and which versions receive fixes,
69
+ beside the [cratly security policy](https://cratly.io/security) it belongs to.
70
+ Every release's tag pipeline keeps a CycloneDX SBOM of what a site installs along
71
+ with scavold, and checks those packages for known vulnerabilities; `bun run sbom`
72
+ and `bun run audit:runtime` do the same locally.
73
+
74
+ ### Fixed
75
+
76
+ - A photo whose camera stored it turned, saying in its EXIF data how to show it, came
77
+ out upside down or lying on its side on the site, and a portrait got the variant
78
+ widths of a landscape. The variants are now turned the way the photo says, and
79
+ measured by the width it is shown at. Every image gets new variant names once, so
80
+ variants kept between builds are written again rather than served as they were.
81
+
82
+ ### Security
83
+
84
+ - `sharp` 0.35, whose bundled libvips and libheif fix vulnerabilities reported against
85
+ 0.34 (GHSA-f88m-g3jw-g9cj, GHSA-rgj7-g3m4-5g8c). It needs Node 20.9 or newer, and so
86
+ does scavold now.
87
+
88
+ ## [0.2.0-rc.7] — 2026-09-29
89
+
90
+ ### Fixed
91
+
92
+ - A link to a media file whose extension VitePress does not know, such as `.diff`,
93
+ pointed at the file name with `.html` appended and therefore at nothing. It now names
94
+ the file as published.
95
+
96
+ ## [0.2.0-rc.6] — 2026-09-13
97
+
98
+ ### Added
99
+
100
+ - `aliases` in a page's front matter names the former addresses it replaces, and
101
+ `retired_urls` in `.cratly.config.yaml` those that are not coming back. A site taking
102
+ over from an existing website declares them once and the build writes
103
+ `cratly-redirects.json` into its output: an address, where it went, and `301` or `410`.
104
+ The format names no server — only a server-side redirect passes a ranking on, and which
105
+ server that is cratly does not assume, so the deploy step translates the file for its
106
+ target. Keeping an address is still better than redirecting to it, which is what `url`
107
+ is for; the build refuses an alias that two pages claim, or that the site serves itself
108
+ and the redirect would therefore hide.
109
+ - Every build now ends by reading its own result. A page that the generator listed but
110
+ never wrote, or a script a built page names and the output does not contain, ends the
111
+ build with the file named — the two states a bundling fault leaves behind while the log
112
+ says "build complete". Links and images pointing at absent files are reported and left
113
+ to the site, since one may legitimately name a path the web server provides. Turn it
114
+ down with `verify: { content: false }` or off with `verify: false`. A reference is read
115
+ the way a browser reads it: a relative one against the address of the page holding it, so
116
+ a link that forgot its protocol — `[Website](./www.example.com)`, which the build turns
117
+ into a sibling page nobody wrote — is seen as well. Proven against the
118
+ case-collision that used to build green: the page missing from the output is the only
119
+ trace it leaves on a file system that ignores letter case, and that is now enough.
120
+ Everything found also goes to `.cratly/build-report.json`, written before the build is
121
+ ended rather than after, each entry naming the source file an author would open. A build
122
+ log is read by whoever has access to it; a file can be handed to whoever wrote the page.
123
+ - `ignoreDeadLinks` now defaults to `true`. VitePress ends a build on a dead link and does
124
+ so before writing anything, which leaves the reason in a job log that whoever wrote the
125
+ link often cannot read, and lets one typo stand between a finished page and its
126
+ publication. Scavold finds the same mistake in the finished output and reports it there
127
+ instead. Whether it should also stop a pipeline is a question whose answer differs per
128
+ branch, so it is left to CI: the site template fails a merge request over it and lets the
129
+ default branch deploy, since blocking there would answer a broken link with a stale site.
130
+ Ask for VitePress' abort back with `ignoreDeadLinks: false`.
131
+ - A site whose page paths collide in VitePress's eyes now learns that from Scavold, by
132
+ name, before the build starts. VitePress identifies a page by its path flattened into
133
+ a single name — slash becomes underscore, and the page-hash map lower-cases it — and
134
+ uses that name for the page's bundle entry, its server module and its client chunk.
135
+ Two pages meeting there overwrite each other: either the build dies at render time in
136
+ `pageChunk.imports`, naming no file, or it succeeds and ships two pages pointing at a
137
+ script that was never written. Scavold now reproduces that identity when it reads the
138
+ config and aborts with both file names, whether they collide through an underscore in
139
+ a file name (`de_kontakt.md` beside `de/kontakt.md`), through letter case alone, or
140
+ through a `url` alias landing on a path another page already owns as its file — the
141
+ case the previous alias check, which compared aliases only with each other, let pass.
142
+ - `date` and `datetime` join the type vocabulary of `frontmatter_fields` and of a
143
+ container's `props`. Scavold passes such a value through as the ISO 8601 text it is;
144
+ what changes is that the cratly editor now offers a date picker for it instead of a
145
+ plain text box. A `datetime` always carries an offset — a time of day without one is
146
+ a different moment on every machine that builds the site.
147
+
148
+ ### Changed
149
+
150
+ - `@cepharum/vue3-i18n` moves to `2.0.0`, two majors on from the `0.5.4` a site would
151
+ have installed. Scavold uses `useL10n`, `setLocale` and `setLoader`, none of which
152
+ changed; what 2.0 breaks are the locale helpers now returning full BCP-47 tags, and
153
+ Scavold calls none of them. A site inherits the new version with its next install, and
154
+ gains what 2.0 adds along the way: regional locales overlaying a bare language, plural
155
+ and gender selection resolved through `Intl`, and `Intl`-backed formatters in
156
+ placeholders.
157
+
158
+ ### Fixed
159
+
160
+ - `lib/href.js` ships the declarations it had been missing since it was split out, so
161
+ `typecheck` passes again — and the pipeline runs it now, which is why it could go
162
+ unnoticed at all. Nothing about the module changed; TypeScript simply had nothing to
163
+ read about it, and treated every href the hierarchy composes as `any`.
164
+
165
+ ## [0.2.0-rc.5] — 2026-08-17
166
+
167
+ ### Added
168
+
169
+ - `:::pagelist` dates its entries the way the site wants them: `time-style` adds the
170
+ time of day, `timezone` names the zone the timestamps are read in, and both accept
171
+ `iso` alongside the `Intl` styles, writing `2020-10-20 12:16` in every language. The
172
+ default is unchanged — the date alone, read in UTC, which is where a front matter
173
+ value carrying no time of day belongs.
174
+ - `formatTimestamp()` in `scavold/lib/dateFormat.js` renders timestamps the same way
175
+ for a theme's own components — a byline under a post's title, say. It is free of Vue
176
+ and VitePress and depends on its arguments alone, so pre-rendered markup and the
177
+ browser agree.
178
+
179
+ ### Changed
180
+
181
+ - A `:::pagelist` that paginates reaches every page it found: `limit` now defaults to
182
+ `0` when `per-page` is set, rather than capping the selection at five entries and
183
+ leaving a pager to turn the pages of those five. A limit the author sets still wins.
184
+ - Every link a site generates — menus, breadcrumbs, page lists, the feed, redirect targets
185
+ — names the file the page is built into: `/blog/post.html`, `/blog/index.html`,
186
+ `/index.html`. Links used to leave the extension off while VitePress's own markdown
187
+ links carried it, so one site spoke two dialects and the shorter one only worked on
188
+ servers that go looking for the file. Naming it is answered on the first attempt, and
189
+ the sitemap now lists the same URLs. The address bar still shows `/` and `/blog/`:
190
+ VitePress's router shortens them while navigating.
191
+
192
+ ### Fixed
193
+
194
+ - The site's own `index.md` is no longer linked as `/index`, a URL no server has to
195
+ answer. The rule for a page's URL existed in three copies and now lives in
196
+ `lib/href.js` alone.
197
+ - Scavold's built-in translations come from the bundle. They used to be fetched from a
198
+ URL that no build emits, so every page requested a file that was not there and the
199
+ interface silently fell back to English, whatever language the site is in.
200
+
201
+ ## [0.2.0-rc.4] — 2026-08-15
202
+
203
+ ### Added
204
+
205
+ - `metaChunk` is on by default, keeping VitePress's page-hash map out of the inline
206
+ scripts. Its content changes with every build, so under a content security policy that
207
+ names script hashes it has to be updated on the server for every deploy — and when it
208
+ is not, the map is blocked and the client side of the site stops working while the
209
+ pre-rendered HTML still shows. What stays inline are VitePress's own two snippets,
210
+ whose hashes hold still. `metaChunk: false` in the site's own config wins.
211
+ - An RSS feed, written at the end of the build when `augmentConfig` is given a `feed`
212
+ option: the pages of a section, newest first, with their excerpt as the entry text.
213
+ Announced are pages carrying a `date` — a feed is a chronology, and an entry a reader
214
+ cannot place in it is noise. Absolute links need a hostname, taken from
215
+ `sitemap.hostname` unless the feed names its own; without one the build says so and
216
+ writes nothing. Several feeds may be declared, one per language say. The feed is
217
+ compiled from the page hierarchy and is unrelated to `:::pagelist` — a page may appear
218
+ in three lists and in the feed, in the feed alone, or in neither.
219
+ - Front matter `hide` names surfaces: `"menu"`, `"breadcrumb"`, `"list"`, `"feed"`, a
220
+ list of several, or `true` for all of them. A page can now be kept out of the
221
+ navigation and the feed while staying in the lists.
222
+ - `:::pagelist` pages its entries in the browser with `per-page`: only the current page's
223
+ entries are rendered, the page shown is remembered as `?page=2` so returning from an
224
+ entry lands where the reader left off, and the pager is a labelled `<nav>` with a
225
+ status line and buttons.
226
+ - New built-in section `:::pagelist`, rendered by `<ScavoldPageList>`: links a
227
+ configurable number of the site's own pages — the latest posts of a blog, a section's
228
+ sub-pages, a list of downloads. Which pages are listed is addressed as in
229
+ `<ScavoldMenu>` (`from`, `from-root`, `from-path`, plus `deep` for pages filed in
230
+ sub-folders), and `limit`, `offset`, `sort` and `reverse` decide which of them appear
231
+ in what order. Entries are a plain list of links by default; the `teaser`, `images`,
232
+ `dates` and `heading-level` arguments turn them into teaser cards. Anything written
233
+ inside the block is rendered above the list. Each entry is a single link around all
234
+ of its parts, so a screen reader announces it once rather than three times.
235
+ - Pages carry teaser data on their hierarchy node, so a list shows content maintained
236
+ in the target page rather than repeated in the list: `date` (normalised to an ISO
237
+ timestamp and rendered in UTC, so a date-only value never shifts a day), `excerpt` —
238
+ from `excerpt` or `description` front matter, otherwise derived from the page's
239
+ opening prose and trimmed at a word boundary — and `image`, from `image` front matter
240
+ or the first image in the page body. Front matter images now go through the
241
+ responsive image pipeline too, which no pass reached before, as no Markdown syntax
242
+ reveals them.
243
+ - `.cratly.config.yaml` accepts `excerpt_length` (default `200`) to control how long
244
+ derived excerpts get; `0` switches the derivation off for sites that would rather not
245
+ carry it in every page's payload.
246
+ - Front matter `hide` accepts `"list"`, hiding a page from page lists only — a draft
247
+ post stays reachable by URL without being teased anywhere.
248
+ - `useHierarchy()` exposes `collectPages()`, `nodeHref()`, `nodeLabel()` and
249
+ `nodeTitle()`, the last two separating the short navigation text menus want from the
250
+ full title a teaser heading wants. `<ScavoldMenuItems>` uses them instead of its own
251
+ copies.
252
+
253
+ ## [0.2.0-rc.3] — 2026-08-09
254
+
255
+ ### Added
256
+
257
+ - Media that is not an image is published. Files an author uploads into the media folder
258
+ — PDFs, videos, archives — are copied into the static output keeping their path, and
259
+ references to them are rewritten to the URL the built site serves: Markdown links,
260
+ and container arguments declared as `media-file` such as a video's `src` or `poster`.
261
+ Previously only Markdown image syntax was handled, so a linked document or a video
262
+ resolved to a dead URL although the editor offered exactly that reference. Where a
263
+ single URL is needed instead of a srcset — a `poster`, a link to an image — the
264
+ largest generated variant is used.
265
+
266
+ ### Fixed
267
+
268
+ - Flag arguments on a container reach components that declare them as props. Bare words
269
+ used to become the container's `class` only, while `useVideo()` reads its booleans
270
+ from `data-*` props — so the documented `::: video src=… overlay autoplay loop`
271
+ rendered a plain inline player with no background mode, no autoplay and no loop, and
272
+ failed silently. Flags are now emitted as an empty `data-<flag>` attribute in addition
273
+ to the class. A flag a component does not declare as a prop shows up in the markup as
274
+ an empty attribute (`<section class="highlight" data-highlight>`).
275
+
276
+ ### Added
277
+
278
+ - Container blocks work with any name, declared or not: an undeclared name is rendered
279
+ by `ScavoldContainer` instead of leaving its fences in the page as literal text. To
280
+ keep a typo from silently becoming a `<div>`, the build reports each undeclared name
281
+ once; declaring it in `.cratly.config.yaml` silences the report and adds typed
282
+ properties for the editor.
283
+
284
+ ### Fixed
285
+
286
+ - `registerContainers()` and the component reference described `ScavoldContainer` as a
287
+ catch-all fallback, which it was not — no rule was registered for unknown names.
288
+
289
+ ## [0.2.0-rc.2] — 2026-08-06
290
+
291
+ ### Fixed
292
+
293
+ - `media_folder` from `.cratly.config.yaml` is honoured again. `augmentConfig()` read
294
+ every other folder declaration but this one, so image resolution fell back to the
295
+ pages folder: any image stored in the editor-managed media folder was reported as
296
+ missing and failed the build. No site could use an image from `media/`.
297
+
298
+ ### Changed
299
+
300
+ - The folder precedence rules moved into an exported, pure `resolveFolders()`, so an
301
+ explicit `srcDir` / `mediaDir` in the VitePress config beating the YAML declaration
302
+ is covered by unit tests instead of only by a running site.
303
+
304
+ ## [0.2.0-rc.1] — 2026-08-05
305
+
306
+ First published release. Version `0.1.0` existed in-tree only.
307
+
308
+ ### Added
309
+
310
+ - **Section-type manifest.** The build emits `.cratly/sections.json`, describing the
311
+ typed properties each container accepts, so the cratly editor can render matching
312
+ controls. Container declarations in `.cratly.config.yaml` support `props` (with
313
+ `flags` and `kv` as shorthands) and are merged with Scavold's built-in sections.
314
+ - **Background videos.** The `video` container accepts `overlay` to place the video
315
+ behind the block's content, plus `controls` to bring the native controls back in
316
+ that mode.
317
+ - Type declarations for both entry points (`scavold`, `scavold/config`), a `typecheck`
318
+ script, and the MIT licence text the package always claimed to carry.
319
+
320
+ ### Fixed
321
+
322
+ - `Scavold.UserConfig` in the shipped declarations was a self-referential type alias
323
+ (TS2456) and now aliases VitePress's `UserConfig`.
324
+
325
+ ### Known issues
326
+
327
+ - Flag arguments on the `video` container have no effect: bare words become the
328
+ container's `class`, while `useVideo()` reads its booleans from `data-*` props, which
329
+ only `key=value` arguments produce. Use `overlay=1 autoplay=1 loop=1` until this is
330
+ resolved.
331
+ - Container names that are neither built in nor declared in `.cratly.config.yaml` are
332
+ not parsed as containers at all, despite `ScavoldContainer` being documented as a
333
+ catch-all fallback.
334
+ - Only images are processed out of `media_folder`; other file types (video, documents)
335
+ are never copied into the build output and have to live in the static folder.
336
+
337
+ [Unreleased]: https://gitlab.com/cratly/scavold/-/compare/v0.2.0-rc.11...main
338
+ [0.2.0-rc.11]: https://gitlab.com/cratly/scavold/-/compare/v0.2.0-rc.10...v0.2.0-rc.11
339
+ [0.2.0-rc.10]: https://gitlab.com/cratly/scavold/-/compare/v0.2.0-rc.9...v0.2.0-rc.10
340
+ [0.2.0-rc.9]: https://gitlab.com/cratly/scavold/-/compare/v0.2.0-rc.8...v0.2.0-rc.9
341
+ [0.2.0-rc.8]: https://gitlab.com/cratly/scavold/-/compare/v0.2.0-rc.7...v0.2.0-rc.8
342
+ [0.2.0-rc.7]: https://gitlab.com/cratly/scavold/-/compare/v0.2.0-rc.6...v0.2.0-rc.7
343
+ [0.2.0-rc.6]: https://gitlab.com/cratly/scavold/-/compare/v0.2.0-rc.5...v0.2.0-rc.6
344
+ [0.2.0-rc.5]: https://gitlab.com/cratly/scavold/-/compare/v0.2.0-rc.4...v0.2.0-rc.5
345
+ [0.2.0-rc.4]: https://gitlab.com/cratly/scavold/-/compare/v0.2.0-rc.3...v0.2.0-rc.4
346
+ [0.2.0-rc.3]: https://gitlab.com/cratly/scavold/-/compare/v0.2.0-rc.2...v0.2.0-rc.3
347
+ [0.2.0-rc.2]: https://gitlab.com/cratly/scavold/-/compare/v0.2.0-rc.1...v0.2.0-rc.2
348
+ [0.2.0-rc.1]: https://gitlab.com/cratly/scavold/-/tags/v0.2.0-rc.1