@hutusi/amytis 1.16.0 → 1.17.0
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/.claude/rules/immersive-reading.md +21 -0
- package/.claude/rules/rst.md +13 -0
- package/CHANGELOG.md +16 -0
- package/CLAUDE.md +10 -11
- package/docs/ARCHITECTURE.md +81 -0
- package/docs/DIGITAL_GARDEN.md +1 -1
- package/docs/guides/importing-vuepress-books.md +95 -36
- package/package.json +1 -1
- package/scripts/sync-vuepress-book.ts +277 -66
- package/site.config.example.ts +3 -3
- package/site.config.ts +3 -3
- package/src/app/[slug]/layout.tsx +30 -0
- package/src/app/books/[slug]/layout.tsx +24 -0
- package/src/app/books/[slug]/page.tsx +18 -2
- package/src/app/globals.css +67 -0
- package/src/app/page.tsx +6 -0
- package/src/app/posts/layout.tsx +20 -0
- package/src/app/series/[slug]/page.tsx +33 -9
- package/src/components/BookReadingShell.tsx +145 -0
- package/src/components/BookSidebar.tsx +0 -0
- package/src/components/CuratedSeriesSection.tsx +28 -10
- package/src/components/FeaturedStoriesSection.tsx +41 -20
- package/src/components/Footer.tsx +1 -1
- package/src/components/ImmersiveReader.tsx +130 -0
- package/src/components/ImmersiveReaderTopBar.tsx +106 -0
- package/src/components/ImmersiveReadingFlagHandler.tsx +40 -0
- package/src/components/ImmersiveReadingPrefsPopover.tsx +249 -0
- package/src/components/ImmersiveReadingProvider.tsx +168 -0
- package/src/components/ImmersiveSeriesSidebar.tsx +143 -0
- package/src/components/ImmersiveToggleButton.tsx +45 -0
- package/src/components/MarkdownRenderer.tsx +31 -0
- package/src/components/Navbar.tsx +3 -1
- package/src/components/PostReadingShell.tsx +68 -0
- package/src/components/ReadingProgressBar.tsx +1 -1
- package/src/components/SelectedBooksSection.tsx +27 -8
- package/src/hooks/useActiveHeading.ts +35 -13
- package/src/hooks/useSidebarAutoScroll.ts +31 -7
- package/src/i18n/translations.ts +42 -0
- package/src/layouts/BookLayout.tsx +46 -89
- package/src/layouts/PostLayout.tsx +154 -115
- package/src/lib/immersive-reading-prefs.ts +104 -0
- package/src/lib/markdown.ts +18 -11
- package/src/lib/scroll-utils.ts +44 -6
- package/src/lib/shuffle.ts +15 -1
- package/src/lib/sort.ts +15 -0
- package/src/lib/urls.ts +5 -0
- package/tests/integration/book-index-cta.test.ts +87 -0
- package/tests/integration/series-index-cta.test.ts +88 -0
- package/tests/integration/sync-vuepress-book.test.ts +205 -2
- package/tests/unit/immersive-reading-prefs.test.ts +144 -0
- package/vercel.json +7 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "src/components/Immersive*.tsx"
|
|
4
|
+
- "src/components/PostReadingShell.tsx"
|
|
5
|
+
- "src/components/BookReadingShell.tsx"
|
|
6
|
+
- "src/components/Navbar.tsx"
|
|
7
|
+
- "src/components/Footer.tsx"
|
|
8
|
+
- "src/components/ReadingProgressBar.tsx"
|
|
9
|
+
- "src/lib/immersive-reading-prefs.ts"
|
|
10
|
+
- "src/app/books/**"
|
|
11
|
+
- "src/app/[slug]/**"
|
|
12
|
+
- "src/app/posts/**"
|
|
13
|
+
- "src/app/globals.css"
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Immersive reading gotchas
|
|
17
|
+
|
|
18
|
+
- **Chrome-hiding hooks.** `Navbar`, `Footer`, and `ReadingProgressBar` carry stable `data-site-nav` / `data-site-footer` / `data-reading-progress` attributes that the CSS rules in `globals.css` use to hide chrome when `html[data-immersive="true"]` is set. Don't strip these attributes during refactors — the fullscreen `ImmersiveReader` overlay in books and series posts depends on them as defense-in-depth even though the overlay also covers them. Reading-theme overrides (Light / Sepia / Dark) are scoped to `[data-reader-overlay]`, not `<html>`, so they compose with the site's light/dark theme without leaking outside the overlay; the overlay also adds Tailwind's `.dark` class when `readingTheme === 'dark'` so `dark:prose-invert` fires inside it even when the site is in light mode.
|
|
19
|
+
- **Provider is mounted at three layout boundaries**, one per content surface that supports the reader: `src/app/books/[slug]/layout.tsx` (chapter routes), `src/app/[slug]/layout.tsx` (series posts on autoPaths URLs, the default), and `src/app/posts/layout.tsx` (series posts on default-path URLs). Each layout mounts an independent provider instance — `enabled` state doesn't bleed between content types — but they all share the same localStorage key (`amytis-reader-prefs`), so a reader's font/theme/width prefs carry across books and series. Don't merge the three or move the provider to root layout: doing so would leak `enabled=true` to pages without a shell (PostReadingShell / BookReadingShell) to render the overlay, breaking the page.
|
|
20
|
+
- **`ImmersiveReadingFlagHandler` must stay in its own `<Suspense>` boundary in each of those three layouts**, as a sibling of `{children}` (not inside the provider), because its `useSearchParams` triggers a static-export bailout — wrapping the provider would drag the chapter/post page out of static prerender. The handler must **not** use a one-shot ref guard either: caught in PR-#93 review, the ref survives client-side navigation under the persistent layout, so a second `?immersive=1` click in the same tab silently no-ops. Rely on `router.replace` stripping the flag (which re-fires the effect with the flag gone) instead.
|
|
21
|
+
- **Prefs persistence quirks** (`src/components/ImmersiveReadingProvider.tsx` + `src/lib/immersive-reading-prefs.ts`): persist effect flips `hydratedRef` on its *first run* and returns — moving the flip into the hydration effect causes a default-clobbering race (the persist effect runs before React commits the stored values from the closure). Per-key defensive parsing on read is also load-bearing: a corrupt single value (schema drift, hand-edits) must fall back to default without discarding the whole blob. Both behaviours have unit/integration test coverage; don't regress them when refactoring the storage layer or the hydrate/persist effects.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "src/lib/rst*.ts"
|
|
4
|
+
- "src/lib/shiki-rst.ts"
|
|
5
|
+
- "src/components/RstRenderer.tsx"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# rST rendering gotchas
|
|
9
|
+
|
|
10
|
+
- **rST needs Python `docutils`.** Set `AMYTIS_RST_PYTHON=/path/to/python` if not on `$PATH`. Without it, falls back to a lower-fidelity built-in parser.
|
|
11
|
+
- **sanitize-html allowlist must keep `style` + `data-*` on `pre`/`code`/`span`/`div`.** Stripping any of these silently kills Shiki output (monochrome text in prod, looks fine locally because dev rST isn't sanitized). See `src/components/RstRenderer.tsx`.
|
|
12
|
+
- **Code-group tabs add `<input type="radio">` + `<label>` to the sanitize-html allowlist.** Keep the `transformTags` guard in `RstRenderer.tsx` that strips any `<input>` whose `type !== "radio"` — that's the defense against an rST author injecting password/file/etc. inputs through raw HTML.
|
|
13
|
+
- **Bump `RST_RENDERER_DISK_CACHE_VERSION` (`src/lib/rst-renderer.ts`) on highlighter-output changes.** Stale on-disk caches in `.cache/rst-renderer/` will serve old markup otherwise. Run `rm -rf .cache/rst-renderer` after pulling such a change.
|
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.17.0] - 2026-06-11
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Immersive Reading Mode**: A fullscreen, distraction-free reader for book chapters and series posts, launched from new "Immersive reading" CTAs on book and series index pages (deep-linkable via `?immersive=1`). Includes an `Aa` preferences popover (font size, Light / Sepia / Dark reading theme, column width) persisted in `localStorage` with a reset button, a reading progress bar under the top bar, and dedicated sidebars — the book sidebar with collapsible sections, plus a new series sidebar that inlines the active post's table of contents.
|
|
12
|
+
- **Homepage Section Ordering**: The curated homepage sections (`featured-posts`, `featured-series`, `featured-books`) accept an optional per-section `order` field — `shuffle`, `date-desc`, or `date-asc`.
|
|
13
|
+
- **VuePress Importer Upgrades**: `bun run sync-vuepress-book` understands VuePress 1.x sidebar shapes, gains `--skip-common` / `--skip` flags, supports narrow `chapters:`-only re-syncs, and falls back to `README.md` / `README.mdx` when resolving chapter ids.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- **Homepage shuffle** now reshuffles on every visit instead of rotating daily.
|
|
17
|
+
- **Vercel builds** are pinned via `vercel.json` and install with `--frozen-lockfile`.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
- **Immersive Reader Robustness**: Active-heading tracking and anchor scrolling now target the overlay's scroll container; the overlay is preserved when navigating across a collection; sidebar scroll position survives chapter/post clicks; the reading-theme override reliably flips the theme; the `Aa` popover stacks above code blocks; the chrome-hiding CSS is no longer stripped by Lightning CSS; and the first persist run no longer clobbers stored preferences.
|
|
21
|
+
- **Shuffle PRNG**: Small seeds are decorrelated with a splitmix32 finalizer and guarded against a zero state locking xorshift32; the featured-posts shuffle button is hidden when every slot is pinned.
|
|
22
|
+
- **Book Chapter Layout**: Chapter column width aligned with the site navbar and the article centered in its column; React warnings from VuePress-imported tags are silenced.
|
|
23
|
+
|
|
8
24
|
## [1.16.0] - 2026-06-01
|
|
9
25
|
|
|
10
26
|
### Added
|
package/CLAUDE.md
CHANGED
|
@@ -23,7 +23,7 @@ Content-creation scripts, test layout, validate pipeline → `docs/CONTRIBUTING.
|
|
|
23
23
|
|
|
24
24
|
Quick "where do routes live" lookup. Full reference: `docs/ARCHITECTURE.md`.
|
|
25
25
|
|
|
26
|
-
- Standard routes follow folder names under `src/app/`: `/posts`, `/series`, `/tags`, `/notes`, `/books`, `/authors`, `/archive`, `/graph`, `/flows/[year]/[month]/[day]`.
|
|
26
|
+
- Standard routes follow folder names under `src/app/`: `/posts`, `/series`, `/tags`, `/notes`, `/books`, `/authors`, `/archive`, `/graph`, `/flows/[year]/[month]/[day]`. Book chapters are a catch-all: `books/[slug]/[...chapter]` (supports nested chapter IDs like `maths/linear/intro`).
|
|
27
27
|
- **Top-level `[slug]` and `[slug]/[postSlug]`** resolve `redirectFrom` aliases and `series.customPaths` — highest-risk dynamic surface; touch with care.
|
|
28
28
|
- Feeds at `feed.xml` / `feed.atom` / `all.xml` / `all.atom` / `flows/feed.{xml,atom}`; sitemap at `sitemap.ts`; search index at `search.json`.
|
|
29
29
|
- Rendering pipeline lives in `src/lib/`: Shiki (`shiki.ts`, `shiki-rst.ts`), remark/rehype plugins (`remark-github-alerts`, `remark-wikilinks`, `remark-code-group`, `rehype-fence-meta`, `rehype-image-metadata`), redirects (`series-redirects.ts`), feeds/JSON-LD (`feed-utils.ts`, `json-ld.ts`).
|
|
@@ -32,7 +32,7 @@ Quick "where do routes live" lookup. Full reference: `docs/ARCHITECTURE.md`.
|
|
|
32
32
|
|
|
33
33
|
- **Strict build over silent runtime failure.** Static export means misconfiguration must fail at build time. Use `throw` in `generateStaticParams` and similar — never silent skips or `console.warn`. Precedent: `validateSeriesAutoPaths` throws on slug collisions; `redirectFrom` alias conflicts (reserved slug or duplicate) should also throw, not produce broken redirects.
|
|
34
34
|
- **Exception**: fence-language resolution in `src/lib/shiki.ts` deliberately degrades to `plaintext` + a deduped `console.warn` instead of throwing. Authors can't reliably predict Shiki's alias coverage, and "typo" vs "legitimate community alias" are indistinguishable from our side, so production deploys shouldn't fail on a single unhighlighted code block. Real typos still surface via the warn output in local/CI build logs.
|
|
35
|
-
- **Validate author input at build time; keep content portable.** Frontmatter via Zod
|
|
35
|
+
- **Validate author input at build time; keep content portable.** Frontmatter via Zod. Files on disk stay valid `.md` / `.mdx` / `.rst` — no Amytis-specific syntax that breaks other tools.
|
|
36
36
|
|
|
37
37
|
## Integration-point rules (always go through X)
|
|
38
38
|
|
|
@@ -47,30 +47,28 @@ Quick "where do routes live" lookup. Full reference: `docs/ARCHITECTURE.md`.
|
|
|
47
47
|
|
|
48
48
|
## Gotchas (things Claude will get wrong on first try)
|
|
49
49
|
|
|
50
|
+
Feature-local gotchas live in path-scoped rules under `.claude/rules/` and auto-load when you touch matching files: `rst.md` (docutils setup, sanitize-html allowlist, disk-cache version) and `immersive-reading.md` (chrome-hiding hooks, provider boundaries, prefs persistence).
|
|
51
|
+
|
|
50
52
|
- **`turbopackIgnore` on fs reads.** Any `fs.readFileSync()` path expression must be preceded by `/* turbopackIgnore: true */` (see `src/lib/markdown.ts`, `src/lib/rehype-image-metadata.ts`). Missing it causes incorrect bundling.
|
|
51
53
|
- **No AVIF for `coverImage`.** Upstream bug in `next-image-export-optimizer` emits `.webp` files but a `srcset` pointing at `.avif` → 404 in prod. Use `.jpg` / `.png` / `.webp`. See `docs/TROUBLESHOOTING.md`.
|
|
52
54
|
- **Unicode slugs.** Dynamic route pages call `safeDecodeParam()` and try decoded / raw / NFC / NFD variants — don't shortcut with bare `decodeURIComponent()` (it throws on malformed input). When touching dynamic routes, verify both ASCII and Unicode slugs.
|
|
53
55
|
- **`generateStaticParams` returns raw values.** Don't `encodeURIComponent` route params; Next.js handles encoding. Don't link to placeholder routes like `/posts/[slug]` — always link to concrete URLs.
|
|
54
56
|
- **Series format is locked.** A series index can be `index.md` / `.mdx` / `README.md` / `README.mdx` / `index.rst` / `README.rst` (first match wins). All child posts must match. Mixing formats is a build error.
|
|
55
|
-
- **rST needs Python `docutils`.** Set `AMYTIS_RST_PYTHON=/path/to/python` if not on `$PATH`. Without it, falls back to a lower-fidelity built-in parser.
|
|
56
57
|
- **Pagefind index.** `bun run build:dev` regenerates `public/pagefind/`; search returns stale results until you rerun it after content changes.
|
|
57
58
|
- **`trailingSlash: true` is load-bearing.** Lets co-located post assets (`posts/slug/images/`) coexist with `posts/slug/index.html`. Don't flip it in `next.config.ts`.
|
|
58
59
|
- **Shiki highlighter is a `globalThis` singleton.** Never instantiate it per render — `createHighlighter` loads Oniguruma WASM + grammars (~1–2 s). See `src/lib/shiki.ts`.
|
|
59
|
-
- **rST sanitize-html allowlist must keep `style` + `data-*` on `pre`/`code`/`span`/`div`.** Stripping any of these silently kills Shiki output (monochrome text in prod, looks fine locally because dev rST isn't sanitized). See `src/components/RstRenderer.tsx`.
|
|
60
|
-
- **Bump `RST_RENDERER_DISK_CACHE_VERSION` (`src/lib/rst-renderer.ts`) on highlighter-output changes.** Stale on-disk caches in `.cache/rst-renderer/` will serve old markup otherwise. Run `rm -rf .cache/rst-renderer` after pulling such a change.
|
|
61
60
|
- **Fence meta needs `rehype-fence-meta` BEFORE `rehype-raw`.** `mdast-util-to-hast` stores fence meta on `node.data.meta`, which `rehype-raw` drops during HTML round-trip. The plugin copies it to a real `data-meta` attribute first. Order matters in `MarkdownRenderer.tsx`.
|
|
62
|
-
- **Code-group tabs add `<input type="radio">` + `<label>` to the rST sanitize-html allowlist.** Keep the `transformTags` guard in `RstRenderer.tsx` that strips any `<input>` whose `type !== "radio"` — that's the defense against an rST author injecting password/file/etc. inputs through raw HTML.
|
|
63
61
|
- **GitHub alerts (`> [!NOTE]`, etc.) need the custom `remarkGithubAlerts` plugin.** `remark-gfm` v4 does NOT transform `[!TYPE]` blockquotes — they pass through as plain blockquotes with the literal marker visible. The custom plugin in `src/lib/remark-github-alerts.ts` is what detects them and routes to `<GithubAlert>`. If a future remark-gfm adds native alert support, that's a regression to watch for (covered by an integration test).
|
|
64
62
|
- **Single-line block math `$$ x $$` is silently inline.** `micromark-extension-math` (under `remark-math` v6) requires the `$$` markers on their own lines — single-line collapses to *inline* math (no `katex-display` wrapper, no centering, no display margin) and looks like a subtly under-styled formula. `src/lib/normalize-vuepress-math.ts` expands single-line `$$ x $$` to opener / body / closer before parsing, so authored content stays portable. If a chapter formula stops centering, suspect the normalizer's regex first.
|
|
65
63
|
|
|
66
64
|
## Development workflow
|
|
67
65
|
|
|
68
|
-
|
|
69
|
-
|
|
66
|
+
For a new feature or non-trivial change:
|
|
67
|
+
|
|
68
|
+
- **Branch.** Work on a dedicated `<type>/<topic>` branch off `main`, not directly on `main`.
|
|
69
|
+
- **Commit in focused slices.** One commit per logical slice (e.g. split a dead-code removal from the feature that replaces it). Keep `bun run lint` green at each commit so the branch stays bisectable. Conventional Commits: `feat | fix | refactor | perf | chore | docs | test | release`; subject under ~70 chars; body explains *why*; no `Co-Authored-By` trailers.
|
|
70
70
|
- **Tests + docs in the same change.** Update `docs/ARCHITECTURE.md` / `docs/CONTRIBUTING.md` / `docs/TROUBLESHOOTING.md` alongside seam/workflow/invariant changes — not as a follow-up.
|
|
71
|
-
- **
|
|
72
|
-
- **Branches:** `<type>/<kebab-slug>` matching commit prefixes.
|
|
73
|
-
- **No Claude attribution.** Do not add `Co-Authored-By: Claude ...` trailers to commit messages or the `🤖 Generated with [Claude Code]` footer to PR descriptions. The default templates in the harness include both — strip them.
|
|
71
|
+
- **Open a PR** into `main` when the branch is green. Do not add the `🤖 Generated with [Claude Code]` footer to PR descriptions. Pushing, and opening PRs are actions the user authorizes — don't push or open a PR unless asked.
|
|
74
72
|
|
|
75
73
|
## Verifying a change
|
|
76
74
|
|
|
@@ -97,6 +95,7 @@ When compressing history, preserve in priority order:
|
|
|
97
95
|
- `docs/ARCHITECTURE.md` — route map, content model, components, data layer, frontmatter schemas, full configuration reference
|
|
98
96
|
- `docs/CONTRIBUTING.md` — full command list, test layout, content-creation scripts
|
|
99
97
|
- `docs/TROUBLESHOOTING.md` — known issues (AVIF, dev-mode browser-extension CSP/SharedStorage noise)
|
|
98
|
+
- `docs/ALERTS.md` / `docs/CODE-BLOCKS.md` / `docs/DIGITAL_GARDEN.md` — content-feature references (alert callouts, code-block features, garden philosophy)
|
|
100
99
|
- `docs/deployment.md` — production deploy steps
|
|
101
100
|
- `docs/guides/` — task-oriented walkthroughs (e.g. `importing-vuepress-books.md`)
|
|
102
101
|
- `site.config.ts` — live config (read it directly; don't infer from this file)
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -305,6 +305,87 @@ happens automatically inside `BookLayout`), two extra plugins fire:
|
|
|
305
305
|
Mermaid diagrams in book chapters already work via the existing `Mermaid` component (any
|
|
306
306
|
\`\`\`mermaid fenced block, with or without a `compact` modifier after the language tag).
|
|
307
307
|
|
|
308
|
+
#### Immersive reading mode
|
|
309
|
+
|
|
310
|
+
Book chapters AND series posts support an "immersive reading" mode: a
|
|
311
|
+
fullscreen overlay with a top bar, a content-type-specific sidebar on the
|
|
312
|
+
left, and the article in a centred scrollable column. Entered two ways —
|
|
313
|
+
the toggle button in the article header (chapter header for books, post
|
|
314
|
+
header for series), or the secondary "Immersive reading" CTA on the book
|
|
315
|
+
index (`/books/<slug>`) or series index (`/series/<slug>`), which links to
|
|
316
|
+
the first chapter/post with `?immersive=1` appended.
|
|
317
|
+
|
|
318
|
+
**Generic reader, content-type-specific shells.** The overlay
|
|
319
|
+
(`src/components/ImmersiveReader.tsx`) and top bar
|
|
320
|
+
(`src/components/ImmersiveReaderTopBar.tsx`) are content-type-agnostic —
|
|
321
|
+
both take `rootHref` / `rootTitle` / `currentTitle` props plus a pre-rendered
|
|
322
|
+
`sidebar` ReactNode. Two shells consume the reader:
|
|
323
|
+
|
|
324
|
+
- `src/components/BookReadingShell.tsx` for chapters — passes
|
|
325
|
+
`<BookSidebar mode="fill" ...>` as the sidebar.
|
|
326
|
+
- `src/components/PostReadingShell.tsx` for series posts — passes
|
|
327
|
+
`<SeriesList mode="fill" ...>`. The toggle is gated on `post.series`;
|
|
328
|
+
non-series posts don't see a useless button.
|
|
329
|
+
|
|
330
|
+
**Layout boundaries.** The provider needs to survive client-side navigation
|
|
331
|
+
between sibling items within a content unit. Three layout files mount it:
|
|
332
|
+
|
|
333
|
+
- `src/app/books/[slug]/layout.tsx` — books reader (chapter-to-chapter).
|
|
334
|
+
- `src/app/[slug]/layout.tsx` — series posts on autoPaths URLs
|
|
335
|
+
(`/<series-slug>/<post>`), which is the default.
|
|
336
|
+
- `src/app/posts/layout.tsx` — series posts on default-path URLs
|
|
337
|
+
(`/posts/<slug>`), for sites with `series.autoPaths: false`.
|
|
338
|
+
|
|
339
|
+
Each layout file is identical: wraps `{children}` in
|
|
340
|
+
`<ImmersiveReadingProvider>` plus a `<Suspense>`-isolated
|
|
341
|
+
`<ImmersiveReadingFlagHandler />`. The three layouts each mount independent
|
|
342
|
+
provider instances — `enabled` state doesn't bleed between content types —
|
|
343
|
+
but localStorage prefs are shared (same `amytis-reader-prefs` key), so a
|
|
344
|
+
reader's font/theme/width choices carry across books and series.
|
|
345
|
+
|
|
346
|
+
**State + persistence.** `src/components/ImmersiveReadingProvider.tsx` holds
|
|
347
|
+
the context. Preferences (`fontSize`, `readingTheme`, `columnWidth`,
|
|
348
|
+
`sidebarOpen`) persist to `localStorage` under `amytis-reader-prefs` via the
|
|
349
|
+
helpers in `src/lib/immersive-reading-prefs.ts`; the read path is per-key
|
|
350
|
+
defensive so schema drift or hand-edited values fall back to their default
|
|
351
|
+
without discarding the whole blob. `enabled` and `prefsPanelOpen` are
|
|
352
|
+
deliberately **not** persisted — entering the reader is a per-visit intent,
|
|
353
|
+
not a preference.
|
|
354
|
+
|
|
355
|
+
**`?immersive=1` URL flag.** `src/components/ImmersiveReadingFlagHandler.tsx`
|
|
356
|
+
sits as a sibling of `{children}` inside each layout (wrapped in its own
|
|
357
|
+
`<Suspense>`), reads the query param via `useSearchParams`, calls
|
|
358
|
+
`provider.enter()`, then strips the flag via `router.replace`. The Suspense
|
|
359
|
+
boundary is load-bearing — `useSearchParams` triggers a static-export bailout,
|
|
360
|
+
so wrapping the provider instead would drag the chapter/post page out of
|
|
361
|
+
static prerender.
|
|
362
|
+
|
|
363
|
+
**Overlay anatomy** (`ImmersiveReader.tsx`, `position: fixed inset-0 z-40`):
|
|
364
|
+
|
|
365
|
+
- `ImmersiveReaderTopBar` — sidebar toggle, breadcrumb (book/chapter or
|
|
366
|
+
series/post), `Aa` button, exit (✕). The header is `relative z-30` so its
|
|
367
|
+
`backdrop-blur-md` stacking context paints above article-area code blocks.
|
|
368
|
+
- `ImmersiveReadingPrefsPopover` — anchored under the `Aa` button. Four
|
|
369
|
+
control groups with visual previews: font size (4 sizes, `A` letters at the
|
|
370
|
+
actual size), reading theme (Auto / Light / Sepia / Dark colour swatches —
|
|
371
|
+
Auto reads as a split light/dark gradient), column width (Narrow / Medium /
|
|
372
|
+
Wide / Full as stacked line-icons), and a "Reset to defaults" link at the
|
|
373
|
+
bottom (one-click, no confirmation). Dismisses on outside `pointerdown` or
|
|
374
|
+
ESC; ESC with the popover closed exits the reader.
|
|
375
|
+
- Sidebar in `mode="fill"` — either `BookSidebar` or `SeriesList`, without
|
|
376
|
+
their page-mode positioning classes. Auto-collapsed below `lg`
|
|
377
|
+
(one-directional: never auto-opens on resize to wide).
|
|
378
|
+
- Main scroll area — article centred at the column width the user picked
|
|
379
|
+
(`max-w-2xl` to `max-w-none`).
|
|
380
|
+
|
|
381
|
+
**CSS scoping.** `html[data-immersive]` hides site chrome via the three stable
|
|
382
|
+
hooks `data-site-nav` / `data-site-footer` / `data-reading-progress` —
|
|
383
|
+
defense-in-depth (the fixed overlay covers them anyway). Reading-theme
|
|
384
|
+
overrides are scoped to `[data-reader-overlay]` so they don't leak outside the
|
|
385
|
+
reader; when `readingTheme === 'dark'` the overlay also gets Tailwind's `.dark`
|
|
386
|
+
class so `dark:prose-invert` fires regardless of the underlying site theme.
|
|
387
|
+
Shiki code blocks deliberately keep their normal theme.
|
|
388
|
+
|
|
308
389
|
## Configuration Reference (`site.config.ts`)
|
|
309
390
|
|
|
310
391
|
| Field | Notes |
|
package/docs/DIGITAL_GARDEN.md
CHANGED
|
@@ -92,4 +92,4 @@ Related settings that shape the digital-garden experience live alongside that fl
|
|
|
92
92
|
|
|
93
93
|
- `flows.recentCount`: controls how many flow entries appear on the homepage.
|
|
94
94
|
- `pagination.flows` and `pagination.notes`: control listing page sizes.
|
|
95
|
-
- `homepage.sections`: lets you enable, disable, or reorder homepage sections such as recent flows.
|
|
95
|
+
- `homepage.sections`: lets you enable, disable, or reorder homepage sections such as recent flows. The three curated sections (`featured-posts`, `featured-series`, `featured-books`) also accept an `order` field — `'shuffle'` (default, daily-seeded permutation), `'date-desc'`, or `'date-asc'` — to choose how items inside the section are arranged.
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
# Importing a VuePress
|
|
1
|
+
# Importing a VuePress book
|
|
2
2
|
|
|
3
|
-
Amytis can host a VuePress
|
|
3
|
+
Amytis can host a VuePress book natively. `bun run sync-vuepress-book`
|
|
4
4
|
copies the upstream `docs/` tree into a slug under `content/books/`, derives
|
|
5
5
|
Amytis's nested-section book TOC from the VuePress sidebar config, and
|
|
6
6
|
preserves any user-controlled fields you've added to the book's `index.mdx`.
|
|
7
7
|
|
|
8
|
+
Both VuePress 2 and VuePress 1 sidebars are supported — the importer
|
|
9
|
+
auto-detects the shape per entry, so a single config can mix `{ text, link }`
|
|
10
|
+
(VP2) and `{ title, path }` (VP1) without configuration.
|
|
11
|
+
|
|
8
12
|
The importer is idempotent — re-running mirrors the current state of the
|
|
9
13
|
source (including upstream deletions). You should *not* edit chapter
|
|
10
14
|
markdown files in the dest: they get overwritten on every sync. Customize
|
|
@@ -14,8 +18,8 @@ the book's metadata in `index.mdx` (preserved) or extend the source repo.
|
|
|
14
18
|
|
|
15
19
|
## Prerequisites
|
|
16
20
|
|
|
17
|
-
- The source must be a VuePress 2
|
|
18
|
-
`.vuepress/` directory with a `config.js` or `config.mjs`.
|
|
21
|
+
- The source must be a VuePress project (1.x or 2.x) where the docs root
|
|
22
|
+
contains a `.vuepress/` directory with a `config.js` or `config.mjs`.
|
|
19
23
|
- `config.ts` is **not supported** — acorn (used to AST-extract the sidebar)
|
|
20
24
|
parses JS only. Compile to JS first (`tsc`, `bun build --no-bundle`, …) or
|
|
21
25
|
rename to `.mjs` if the config is pure ESM.
|
|
@@ -32,6 +36,9 @@ bun run sync-vuepress-book \
|
|
|
32
36
|
# Positional shorthand:
|
|
33
37
|
bun run sync-vuepress-book /path/to/your-book/docs content/books/your-book
|
|
34
38
|
|
|
39
|
+
# Skip extra files alongside the built-in defaults:
|
|
40
|
+
bun run sync-vuepress-book /path/to/docs content/books/foo --skip '*.bak,dist'
|
|
41
|
+
|
|
35
42
|
# Then rebuild + preview locally:
|
|
36
43
|
bun run build:dev
|
|
37
44
|
bun dev
|
|
@@ -44,8 +51,24 @@ The script prints a one-line summary on completion:
|
|
|
44
51
|
```
|
|
45
52
|
|
|
46
53
|
It will also warn about anomalies it noticed in the sidebar — empty section
|
|
47
|
-
placeholders,
|
|
48
|
-
|
|
54
|
+
placeholders, dropped meta-nav leaves (see [Conventions](#conventions)
|
|
55
|
+
below), files filtered out by skip rules, etc.
|
|
56
|
+
|
|
57
|
+
## Skip rules
|
|
58
|
+
|
|
59
|
+
A VuePress repo's docs root often carries non-content files (lockfiles,
|
|
60
|
+
package manifests, CI configs) that shouldn't land under
|
|
61
|
+
`content/books/<slug>/`. Two flags control filtering:
|
|
62
|
+
|
|
63
|
+
| Flag | Default | Effect |
|
|
64
|
+
| --- | --- | --- |
|
|
65
|
+
| `--skip-common` / `--no-skip-common` | on | Skip `package.json`, `package-lock.json`, `yarn.lock`, `pnpm-lock.yaml`, `bun.lock`, `bun.lockb`. |
|
|
66
|
+
| `--skip <pattern,pattern,…>` | empty | Skip files/dirs whose **basename** matches any of the supplied glob patterns. `*` and `?` are supported. Repeatable. Applied to both files and directories anywhere in the tree. |
|
|
67
|
+
|
|
68
|
+
Files filtered out by either rule are listed in the run summary. They're
|
|
69
|
+
also pruned from the dest on subsequent re-syncs (the mirror logic applies
|
|
70
|
+
to the same skip rules, so toggling `--no-skip-common` mid-stream brings
|
|
71
|
+
the lockfiles back).
|
|
49
72
|
|
|
50
73
|
## What the script does
|
|
51
74
|
|
|
@@ -55,9 +78,22 @@ leaves (see [Conventions](#conventions) below), etc.
|
|
|
55
78
|
literals to a plain JS structure. Unsupported AST shapes throw — silent
|
|
56
79
|
drops would produce a half-correct TOC.
|
|
57
80
|
3. Maps every VuePress sidebar item to one of:
|
|
58
|
-
- `{ title, id }` for a leaf
|
|
59
|
-
|
|
60
|
-
|
|
81
|
+
- `{ title, id }` for a leaf. Accepted shapes:
|
|
82
|
+
- VP2: `{ text, link }`
|
|
83
|
+
- VP1: `{ title, path }` (no `children`)
|
|
84
|
+
- VP1: a bare string path (e.g. `'/intro/about-me'`) — the title is
|
|
85
|
+
read from the source file's frontmatter `title`, falling back to the
|
|
86
|
+
first H1, then a slug-derived fallback.
|
|
87
|
+
- `{ section, items, collapsible? }` for a group. Accepted shapes:
|
|
88
|
+
- VP2: `{ text, children, collapsible? }`
|
|
89
|
+
- VP1: `{ title, children, collapsable? }` (VP1's `collapsable` is
|
|
90
|
+
aliased to `collapsible`)
|
|
91
|
+
- A group entry that carries **both** `path` (VP1) or `link` (VP2) AND
|
|
92
|
+
`children` (a "section + index page" — common in VP1 books where
|
|
93
|
+
`/foo/` is the section's README) has its index page **promoted as the
|
|
94
|
+
first chapter** of the section. The promoted chapter's title is read
|
|
95
|
+
from the README's frontmatter or first H1, not from the section title,
|
|
96
|
+
so the sidebar doesn't show the section name twice.
|
|
61
97
|
- Mixed/unknown shapes are warned and skipped.
|
|
62
98
|
4. Validates that every leaf's resolved chapter id has a real source file at
|
|
63
99
|
one of `<id>.md`, `<id>.mdx`, `<id>/README.md(x)`, or `<id>/index.md(x)`.
|
|
@@ -75,37 +111,59 @@ leaves (see [Conventions](#conventions) below), etc.
|
|
|
75
111
|
|
|
76
112
|
## Conventions
|
|
77
113
|
|
|
78
|
-
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
114
|
+
- Sidebar leaves whose id (case-insensitive, basename only) matches
|
|
115
|
+
`contents` or `SUMMARY` are dropped from the generated TOC. They're
|
|
116
|
+
VuePress/GitBook conventions for a hand-written table-of-contents page,
|
|
117
|
+
and Amytis's book landing page already renders one. The source files are
|
|
118
|
+
still copied so the dest layout matches upstream; they just aren't
|
|
119
|
+
reachable from the TOC.
|
|
120
|
+
- Sections with `collapsible: false` (or VP1's `collapsable: false`) on the
|
|
121
|
+
VuePress side keep that hint — the Amytis sidebar honors it (forces the
|
|
122
|
+
section open).
|
|
123
|
+
- A group whose VuePress entry has both `link`/`path` and `children` has
|
|
124
|
+
the index page promoted as the section's first chapter (see "Maps every
|
|
125
|
+
VuePress sidebar item …" above). Earlier versions dropped the link with
|
|
126
|
+
a warning; the current behavior is strictly more useful and matches the
|
|
127
|
+
upstream VuePress click-the-section-title UX.
|
|
128
|
+
- Path normalization is identical for VP1 and VP2: leading `/` stripped,
|
|
129
|
+
trailing `/` stripped (so `/guide/` resolves to id `guide` and the file
|
|
130
|
+
is found via the `guide/README.md` or `guide/index.md` candidate), and
|
|
131
|
+
any `.md`/`.mdx` suffix stripped. Both build-time validation and the
|
|
132
|
+
runtime chapter resolver accept `<id>.md`, `<id>.mdx`, `<id>/index.md`,
|
|
133
|
+
`<id>/index.mdx`, `<id>/README.md`, `<id>/README.mdx`.
|
|
87
134
|
|
|
88
135
|
## User-controlled fields in `index.mdx`
|
|
89
136
|
|
|
90
|
-
The script
|
|
91
|
-
|
|
137
|
+
The script's footprint on `index.mdx` is deliberately narrow:
|
|
138
|
+
|
|
139
|
+
- **First sync** (no `index.mdx` exists yet): a stub is created with
|
|
140
|
+
`title` (from the VuePress config), today's `date`, `draft: false`,
|
|
141
|
+
`featured: false`, and the parsed `chapters:`. Plus a one-line prose
|
|
142
|
+
body identifying the upstream source. These defaults exist solely so
|
|
143
|
+
the book is loadable by the runtime's Zod schema out of the box.
|
|
144
|
+
- **Every re-sync after that**: only `chapters:` is touched. Every other
|
|
145
|
+
frontmatter key, including ones you've added (`coverImage`, `excerpt`,
|
|
146
|
+
`authors`, `latex`, `showChapterExcerpt`, anything else) and any value
|
|
147
|
+
you've cleared (including intentionally-blank `date: ""`), is preserved
|
|
148
|
+
exactly. The prose body below the frontmatter is preserved too.
|
|
149
|
+
|
|
150
|
+
In other words: edit `index.mdx` once after the first sync, then never
|
|
151
|
+
worry about the script rewriting your choices.
|
|
152
|
+
|
|
153
|
+
The handful of fields that matter for book rendering:
|
|
92
154
|
|
|
93
|
-
| Field |
|
|
155
|
+
| Field | Notes |
|
|
94
156
|
| --- | --- |
|
|
95
|
-
| `title` |
|
|
96
|
-
| `excerpt` |
|
|
97
|
-
| `date` |
|
|
98
|
-
| `coverImage` |
|
|
99
|
-
| `featured` |
|
|
100
|
-
| `draft` |
|
|
101
|
-
| `authors` |
|
|
102
|
-
| `latex` |
|
|
103
|
-
| `showChapterExcerpt` |
|
|
104
|
-
| `chapters` | **Always rewritten** from the sidebar |
|
|
105
|
-
|
|
106
|
-
The prose body below the frontmatter is also preserved, so you can write a
|
|
107
|
-
custom landing-page introduction and re-running the script won't blow it
|
|
108
|
-
away.
|
|
157
|
+
| `title` | Required by the runtime — keep something here. |
|
|
158
|
+
| `excerpt` | Optional one-liner shown on book listings. |
|
|
159
|
+
| `date` | Optional. |
|
|
160
|
+
| `coverImage` | Optional. |
|
|
161
|
+
| `featured` | Show on the home page's featured strip. |
|
|
162
|
+
| `draft` | Hides the book from listings when `true`. |
|
|
163
|
+
| `authors` | Optional list. |
|
|
164
|
+
| `latex` | Set to `true` for math-heavy books to enable KaTeX globally for the book. |
|
|
165
|
+
| `showChapterExcerpt` | Defaults to `false`. Set to `true` if you want each chapter's `excerpt` rendered as a subtitle under the chapter title; most chapters open with their own lede paragraph so the default suppresses it. |
|
|
166
|
+
| `chapters` | **Always rewritten** from the sidebar. |
|
|
109
167
|
|
|
110
168
|
## What about VuePress-specific content?
|
|
111
169
|
|
|
@@ -175,4 +233,5 @@ output under `public/books/<slug>/` and the Pagefind search index.
|
|
|
175
233
|
pipeline plugins listed above.
|
|
176
234
|
- `tests/integration/sync-vuepress-book.test.ts` — covers AST extraction,
|
|
177
235
|
mirror semantics, folder-index links, TS-config rejection, dotfile
|
|
178
|
-
preservation,
|
|
236
|
+
preservation, the `contents` skip, and the VP1 sidebar shape (bare-string
|
|
237
|
+
children, `title`/`collapsable`, README promotion, `SUMMARY` drop).
|