@hutusi/amytis 1.15.0 → 1.16.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.
Files changed (87) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/CLAUDE.md +90 -219
  3. package/bun.lock +185 -547
  4. package/content/books/sample-book/index.mdx +3 -0
  5. package/content/posts/code-block-features-showcase.mdx +223 -0
  6. package/docs/ALERTS.md +112 -0
  7. package/docs/ARCHITECTURE.md +217 -5
  8. package/docs/CODE-BLOCKS.md +238 -0
  9. package/docs/CONTRIBUTING.md +25 -0
  10. package/docs/guides/README.md +11 -0
  11. package/docs/guides/importing-vuepress-books.md +178 -0
  12. package/eslint.config.mjs +18 -6
  13. package/package.json +42 -20
  14. package/scripts/generate-code-group-icons.ts +79 -0
  15. package/scripts/render-rst.py +207 -3
  16. package/scripts/sync-vuepress-book.ts +499 -0
  17. package/src/app/books/[slug]/{[chapter] → [...chapter]}/page.tsx +32 -10
  18. package/src/app/books/[slug]/page.tsx +67 -32
  19. package/src/app/globals.css +503 -123
  20. package/src/app/page.tsx +1 -1
  21. package/src/app/sitemap.ts +3 -3
  22. package/src/components/ArticleCopyCleaner.tsx +64 -0
  23. package/src/components/BookMobileNav.tsx +44 -50
  24. package/src/components/BookSidebar.tsx +0 -0
  25. package/src/components/CodeBlock.test.tsx +93 -8
  26. package/src/components/CodeBlock.tsx +39 -101
  27. package/src/components/CodeBlockToolbar.tsx +88 -0
  28. package/src/components/CodeGroup.tsx +81 -0
  29. package/src/components/CoverImage.tsx +1 -0
  30. package/src/components/ExternalLinkIcon.tsx +15 -0
  31. package/src/components/FeaturedStoriesSection.tsx +3 -3
  32. package/src/components/GithubAlert.tsx +97 -0
  33. package/src/components/MarkdownRenderer.test.tsx +14 -4
  34. package/src/components/MarkdownRenderer.tsx +144 -23
  35. package/src/components/Mermaid.tsx +32 -1
  36. package/src/components/PostList.tsx +1 -1
  37. package/src/components/PostNavigation.tsx +13 -2
  38. package/src/components/PostSidebar.tsx +13 -2
  39. package/src/components/RstRenderer.test.tsx +15 -15
  40. package/src/components/RstRenderer.tsx +37 -2
  41. package/src/components/Search.tsx +18 -4
  42. package/src/components/SeriesCatalog.tsx +1 -1
  43. package/src/components/ShareBar.tsx +5 -0
  44. package/src/components/TocPanel.tsx +10 -2
  45. package/src/i18n/translations.ts +2 -0
  46. package/src/layouts/BookLayout.tsx +35 -4
  47. package/src/layouts/PostLayout.tsx +5 -1
  48. package/src/lib/code-group-icons.test.ts +78 -0
  49. package/src/lib/code-group-icons.ts +148 -0
  50. package/src/lib/markdown.test.ts +56 -13
  51. package/src/lib/markdown.ts +203 -50
  52. package/src/lib/normalize-vuepress-math.ts +118 -0
  53. package/src/lib/rehype-fence-meta.ts +22 -0
  54. package/src/lib/remark-book-chapter-links.ts +106 -0
  55. package/src/lib/remark-code-group.ts +54 -0
  56. package/src/lib/remark-github-alerts.test.ts +83 -0
  57. package/src/lib/remark-github-alerts.ts +65 -0
  58. package/src/lib/remark-vuepress-containers.ts +130 -0
  59. package/src/lib/rst-renderer.ts +19 -7
  60. package/src/lib/rst.test.ts +212 -2
  61. package/src/lib/rst.ts +217 -13
  62. package/src/lib/shiki-rst.ts +185 -0
  63. package/src/lib/shiki.test.ts +153 -0
  64. package/src/lib/shiki.ts +292 -0
  65. package/src/lib/urls.ts +57 -0
  66. package/src/test-utils/render.ts +23 -0
  67. package/tests/fixtures/sync-vuepress-book/docs/.vuepress/config.js +43 -0
  68. package/tests/fixtures/sync-vuepress-book/docs/intro/welcome.md +7 -0
  69. package/tests/fixtures/sync-vuepress-book/docs/maths/linear/assets/diagram.png +1 -0
  70. package/tests/fixtures/sync-vuepress-book/docs/maths/linear/matrices.md +7 -0
  71. package/tests/fixtures/sync-vuepress-book/docs/maths/linear/vectors.md +9 -0
  72. package/tests/helpers/env.ts +19 -0
  73. package/tests/integration/book-chapter-links.test.ts +107 -0
  74. package/tests/integration/books-nested-toc.test.ts +176 -0
  75. package/tests/integration/books.test.ts +3 -2
  76. package/tests/integration/code-block-features.test.ts +188 -0
  77. package/tests/integration/code-group.test.ts +183 -0
  78. package/tests/integration/code-notation.test.ts +97 -0
  79. package/tests/integration/github-alerts.test.ts +82 -0
  80. package/tests/integration/markdown-external-links.test.ts +103 -0
  81. package/tests/integration/normalize-vuepress-math.test.ts +149 -0
  82. package/tests/integration/reading-time-headings.test.ts +8 -6
  83. package/tests/integration/series-draft.test.ts +6 -13
  84. package/tests/integration/sync-vuepress-book.test.ts +240 -0
  85. package/tests/integration/vuepress-containers.test.ts +107 -0
  86. package/tests/tooling/new-post.test.ts +1 -1
  87. package/tests/unit/static-params.test.ts +32 -19
@@ -6,6 +6,9 @@ coverImage: "/images/flowers.jpg"
6
6
  featured: true
7
7
  draft: false
8
8
  authors: ["Amytis"]
9
+ # Render each chapter's excerpt as a subtitle under its title.
10
+ # Default is false; enabled here so the template demonstrates the feature.
11
+ showChapterExcerpt: true
9
12
  chapters:
10
13
  - part: "Part I: Getting Started"
11
14
  chapters:
@@ -0,0 +1,223 @@
1
+ ---
2
+ title: "Code Block Features Showcase"
3
+ date: "2026-05-25"
4
+ excerpt: "Walk through the advanced code-block features: line numbers, line highlighting, title bars, diff colors, word-wrap toggle, and plaintext fallback."
5
+ category: "Showcase"
6
+ tags: ["test", "code", "shiki"]
7
+ authors: ["Amytis Team"]
8
+ toc: true
9
+ ---
10
+
11
+ The default Shiki pipeline applies build-time syntax highlighting with a
12
+ dual `github-light` / `github-dark` theme. The features below are opt-in
13
+ via fence metadata.
14
+
15
+ ## Title bar
16
+
17
+ ```ts title="src/app.ts"
18
+ export const greet = (name: string) => `Hello, ${name}!`;
19
+ ```
20
+
21
+ ## Line numbers
22
+
23
+ ```python linenos
24
+ def fib(n):
25
+ if n < 2:
26
+ return n
27
+ return fib(n - 1) + fib(n - 2)
28
+ ```
29
+
30
+ ## Highlighted lines
31
+
32
+ ```rust {2,4-6}
33
+ fn main() {
34
+ let x = compute();
35
+ println!("{x}");
36
+ if x > 10 {
37
+ warn();
38
+ recover();
39
+ }
40
+ }
41
+ ```
42
+
43
+ ## Title + line numbers + highlights combined
44
+
45
+ ```tsx title="components/CodeBlock.tsx" linenos {3,7-9}
46
+ import { highlightToHast } from '@/lib/shiki';
47
+ import { toHtml } from 'hast-util-to-html';
48
+ import CodeBlockToolbar from './CodeBlockToolbar';
49
+
50
+ export default async function CodeBlock({ language, children, title }) {
51
+ const hast = await highlightToHast(children, language, { title });
52
+ const html = toHtml(hast);
53
+ return (
54
+ <div className="cb-root">
55
+ {title && <span className="cb-title">{title}</span>}
56
+ <CodeBlockToolbar code={children} />
57
+ <div dangerouslySetInnerHTML={{ __html: html }} />
58
+ </div>
59
+ );
60
+ }
61
+ ```
62
+
63
+ ## Diff with red/green backgrounds
64
+
65
+ ```diff
66
+ -export const VERSION = "1.0";
67
+ +export const VERSION = "2.0";
68
+ export const NAME = "amytis";
69
+ -export const STAGE = "alpha";
70
+ +export const STAGE = "beta";
71
+ ```
72
+
73
+ ## Word-wrap toggle
74
+
75
+ Long lines in code blocks overflow horizontally by default — the block grows
76
+ a scrollbar at the bottom. Click the **Wrap** button in any block's header
77
+ to soft-wrap long lines onto multiple visual lines instead; click again to
78
+ restore horizontal scrolling.
79
+
80
+ ```bash
81
+ curl -X POST "https://api.example.com/v1/items?fields=id,name,description,createdAt,updatedAt&sort=createdAt:desc&limit=100&offset=0&filter[status]=active&filter[category]=blog&include=author,tags" -H "Authorization: Bearer YOUR_API_TOKEN_HERE" -H "Content-Type: application/json" -d '{"name":"example","description":"a sufficiently long single line to demonstrate the wrap toggle"}'
82
+ ```
83
+
84
+ Try the **Wrap** button in the header above ↑ to see the long line collapse
85
+ into multiple soft-wrapped lines.
86
+
87
+ ## Mermaid still works (regression check)
88
+
89
+ ```mermaid
90
+ graph LR
91
+ A[Markdown] --> B[remark]
92
+ B --> C[rehype + Shiki]
93
+ C --> D[Static HTML]
94
+ ```
95
+
96
+ ## Tabbed code groups
97
+
98
+ Group adjacent fences into a single tabbed widget by wrapping them in a
99
+ `:::code-group` container directive. Tab names come from the `[label]`
100
+ token after the language. The mechanism is pure CSS (radio inputs + sibling
101
+ selectors) — zero JavaScript, zero hydration cost.
102
+
103
+ When a tab label matches a known package manager, language, or common
104
+ config filename, an icon appears automatically before the label. Resolution
105
+ is handled by `resolveCodeGroupIcon` in `src/lib/code-group-icons.ts`.
106
+
107
+ :::code-group
108
+ ```bash [npm]
109
+ npm install amytis
110
+ ```
111
+ ```bash [yarn]
112
+ yarn add amytis
113
+ ```
114
+ ```bash [pnpm]
115
+ pnpm add amytis
116
+ ```
117
+ ```bash [bun]
118
+ bun add amytis
119
+ ```
120
+ :::
121
+
122
+ Filename labels also resolve — e.g. `tsconfig.json`, `vite.config.ts`, or
123
+ `Dockerfile`:
124
+
125
+ :::code-group
126
+ ```json [tsconfig.json]
127
+ { "compilerOptions": { "target": "es2022", "module": "esnext" } }
128
+ ```
129
+ ```ts [vite.config.ts]
130
+ import { defineConfig } from 'vite';
131
+ export default defineConfig({ server: { port: 5173 } });
132
+ ```
133
+ ```dockerfile [Dockerfile]
134
+ FROM node:20-alpine
135
+ WORKDIR /app
136
+ COPY . .
137
+ RUN npm ci && npm run build
138
+ ```
139
+ :::
140
+
141
+ Tabs work across languages too — handy for showing the same algorithm in
142
+ multiple implementations:
143
+
144
+ :::code-group
145
+ ```ts [TypeScript]
146
+ export function greet(name: string): string {
147
+ return `Hello, ${name}!`;
148
+ }
149
+ ```
150
+ ```python [Python]
151
+ def greet(name: str) -> str:
152
+ return f"Hello, {name}!"
153
+ ```
154
+ ```rust [Rust]
155
+ fn greet(name: &str) -> String {
156
+ format!("Hello, {name}!")
157
+ }
158
+ ```
159
+ :::
160
+
161
+ ## Notation comments
162
+
163
+ Annotate individual lines with `// [!code …]` comments. Six markers are
164
+ supported — focus, diff, highlight, error, warning — using the language's
165
+ native comment style (`//` in C-family, `#` in Python, `--` in SQL, etc.):
166
+
167
+ ```ts
168
+ function login(user: string) { // [!code focus]
169
+ const token = oldApi.auth(user) // [!code --]
170
+ const token = newApi.auth({ user }) // [!code ++]
171
+ validate(token) // [!code highlight]
172
+ throwIfExpired(token) // [!code error]
173
+ if (!token.refreshable) warn() // [!code warning]
174
+ return token
175
+ }
176
+ ```
177
+
178
+ `// [!code focus]` dims the rest of the block so the focused subset stands
179
+ out; hover the block to reveal the dimmed lines. `// [!code error]` /
180
+ `[!code warning]` tint the line with a colored left-border (red / amber).
181
+ `[!code ++]` / `[!code --]` color individual lines without needing a
182
+ `diff`-language fence.
183
+
184
+ Notation comments work in **any** language — Python:
185
+
186
+ ```python
187
+ def fib(n):
188
+ if n < 2: return n # [!code focus]
189
+ return fib(n-1) + fib(n-2)
190
+ ```
191
+
192
+ ## GitHub-flavored alerts
193
+
194
+ Add a `[!TYPE]` marker on the first line of a blockquote to render it as a
195
+ GitHub-style callout. Five types are supported, each with its own color,
196
+ icon, and label:
197
+
198
+ > [!NOTE]
199
+ > Highlights information that users should take into account, even when skimming.
200
+
201
+ > [!TIP]
202
+ > Optional information to help a user be more successful.
203
+
204
+ > [!IMPORTANT]
205
+ > Crucial information necessary for users to succeed.
206
+
207
+ > [!WARNING]
208
+ > Critical content demanding immediate user attention due to potential risks.
209
+
210
+ > [!CAUTION]
211
+ > Negative potential consequences of an action.
212
+
213
+ rST equivalents (`.. note::` / `.. tip::` / `.. important::` / `.. warning::`
214
+ / `.. caution::`) render with matching colors via docutils' admonition
215
+ directives — same visual treatment across both pipelines.
216
+
217
+ ## Explicit plaintext
218
+
219
+ ```plaintext
220
+ This block opts out of syntax highlighting entirely. Use `plaintext` (or its
221
+ aliases `text`, `txt`, `plain`) for prose-like blocks where token coloring
222
+ would be noisy or misleading. Unknown language names will fail the build.
223
+ ```
package/docs/ALERTS.md ADDED
@@ -0,0 +1,112 @@
1
+ # Alerts (GitHub-flavored callouts)
2
+
3
+ Amytis renders the five GitHub-flavored alert types as styled callouts.
4
+ Both Markdown / MDX (`> [!TYPE]` blockquote markers) and rST (the built-in
5
+ `.. note::` / `.. tip::` etc. directives) produce visually consistent
6
+ output — same border color, same icon-less title bar for rST (docutils
7
+ supplies the title), same per-type accent palette.
8
+
9
+ ## Markdown / MDX
10
+
11
+ Start a blockquote with `[!TYPE]` on its own first line:
12
+
13
+ ```markdown
14
+ > [!NOTE]
15
+ > Highlights information that users should take into account, even when skimming.
16
+
17
+ > [!TIP]
18
+ > Optional information to help a user be more successful.
19
+
20
+ > [!IMPORTANT]
21
+ > Crucial information necessary for users to succeed.
22
+
23
+ > [!WARNING]
24
+ > Critical content demanding immediate user attention due to potential risks.
25
+
26
+ > [!CAUTION]
27
+ > Negative potential consequences of an action.
28
+ ```
29
+
30
+ The marker is **case-insensitive** (`[!note]` works too). Body content
31
+ keeps full Markdown — paragraphs, lists, links, inline code, even other
32
+ code blocks.
33
+
34
+ A blockquote without a recognized marker stays as a plain blockquote.
35
+ An unknown type like `[!UNKNOWN]` also passes through unchanged.
36
+
37
+ ## reStructuredText
38
+
39
+ rST uses docutils' built-in admonition directives. All five GitHub types
40
+ have a docutils equivalent, plus a few aliases:
41
+
42
+ ```rst
43
+ .. note::
44
+
45
+ Highlights information that users should take into account.
46
+
47
+ .. tip::
48
+
49
+ Optional information to help a user be more successful.
50
+
51
+ .. hint::
52
+
53
+ Also styled as a tip.
54
+
55
+ .. important::
56
+
57
+ Crucial information necessary for users to succeed.
58
+
59
+ .. warning::
60
+
61
+ Critical content demanding immediate user attention.
62
+
63
+ .. attention::
64
+
65
+ Also styled as a warning.
66
+
67
+ .. caution::
68
+
69
+ Negative potential consequences of an action.
70
+
71
+ .. danger::
72
+
73
+ Also styled as a caution.
74
+ ```
75
+
76
+ The CSS rules in `src/app/globals.css` apply the same `--alert-accent`
77
+ color variable to docutils' `.admonition-note` / `.admonition-tip` /
78
+ `.admonition-hint` / `.admonition-important` / `.admonition-warning` /
79
+ `.admonition-attention` / `.admonition-caution` / `.admonition-danger`
80
+ classes, so `> [!NOTE]` in MDX and `.. note::` in rST land at the same
81
+ visual output.
82
+
83
+ ## Visual style
84
+
85
+ - Per-type accent color drives the left border, the title bar text, and
86
+ a tinted background (8% accent over the page background).
87
+ - Dark mode uses brighter accent variants matching GitHub Primer's
88
+ dark-tier alert colors.
89
+ - MDX alerts use an inline SVG icon; rST admonitions skip the icon (docutils
90
+ doesn't emit one) but keep the colored title.
91
+
92
+ ## How it works
93
+
94
+ - **MDX**: a small remark plugin at `src/lib/remark-github-alerts.ts`
95
+ detects the `[!TYPE]` marker, strips it from the blockquote, and routes
96
+ the node through a `<GithubAlert>` React server component
97
+ (`src/components/GithubAlert.tsx`). `remark-gfm` v4 does NOT include
98
+ the alert transform — it passes blockquotes through with the marker
99
+ intact — so the plugin is what makes this work.
100
+ - **rST**: docutils' built-in admonition directives produce the
101
+ `<aside class="admonition admonition-<type>">` markup. The shared CSS
102
+ in `globals.css` matches both pipelines.
103
+
104
+ ## Gotchas
105
+
106
+ - Don't rely on a blank line between the marker and body — `> [!NOTE]\n> body`
107
+ works; `> [!NOTE]\n>\n> body` also works.
108
+ - The marker must be `[!TYPE]` *exactly* (square brackets, exclamation,
109
+ type). VitePress-style colon variants like `:::tip` aren't recognized.
110
+ - Custom alert types beyond the five GitHub ones aren't supported. If you
111
+ need one, extend the regex in `remark-github-alerts.ts` and add a CSS
112
+ rule.
@@ -94,11 +94,45 @@ src/app/
94
94
 
95
95
  ## Key Components
96
96
 
97
- - Layout/navigation: `Navbar`, `Footer`, `Hero`, `FlowHubTabs`
98
- - Content renderers: `MarkdownRenderer`, `CodeBlock`, `Mermaid`
99
- - Post surfaces: `PostLayout`, `PostSidebar`, `PostCard`, `RelatedPosts`, `ShareBar`
100
- - Notes/flows discovery: `NoteSidebar`, `FlowContent`, `FlowCalendarSidebar`, `TagContentTabs`
101
- - Search/discovery: `Search`, `Pagination`, `KnowledgeGraph`
97
+ Layout & navigation:
98
+
99
+ - `Navbar`, `Footer`, `Hero` (configurable homepage hero with collapsible intro)
100
+ - `LanguageSwitch` (i18n language selector), `ThemeToggle` (light/dark mode)
101
+ - `FlowHubTabs`
102
+
103
+ Content renderers:
104
+
105
+ - `MarkdownRenderer` — MDX with GFM, KaTeX math, build-time syntax highlighting via Shiki, Mermaid
106
+ - `CodeBlock` (async server component, calls Shiki) and `CodeBlockToolbar` (client: copy + word-wrap toggle), `Mermaid`
107
+ - `CoverImage` — optimized image component with WebP support
108
+
109
+ Post & series surfaces:
110
+
111
+ - `PostLayout` / `SimpleLayout` — post page layouts with TOC, series sidebar, external links, comments
112
+ - `PostList` — card-based post listing with thumbnails, metadata, excerpts, tags
113
+ - `PostCard`, `PostSidebar`, `RelatedPosts`, `ShareBar`
114
+ - `SeriesCatalog` — timeline-style listing with numbered entries and progress indicator
115
+ - `SeriesSidebar` — series navigation sidebar with progress bar and color-coded states
116
+ - `SeriesList` — mobile-optimized series navigation matching sidebar design
117
+ - `TableOfContents` — sticky TOC with scroll tracking, reading progress, back-to-top
118
+ - `HorizontalScroll` — scrollable container with navigation arrows for featured content
119
+
120
+ Notes & flows:
121
+
122
+ - `NoteSidebar`, `TagContentTabs`
123
+ - `FlowContent` — client wrapper for flow pages with tag filtering state
124
+ - `FlowCalendarSidebar` — calendar sidebar with date navigation, browse panel, clickable tag filters
125
+ - `FlowTimelineEntry` — individual flow entry in timeline list
126
+
127
+ Search & discovery:
128
+
129
+ - `Search` — full-text search (Cmd/Ctrl+K) powered by Pagefind; type filter tabs (All/Post/Flow/Book), recent searches, keyboard navigation, debounced input, focus trap, ARIA, search syntax (`"phrase"`, `-exclude`)
130
+ - `Pagination`, `KnowledgeGraph`
131
+
132
+ Integrations:
133
+
134
+ - `Comments` — Giscus or Disqus (theme-aware)
135
+ - `Analytics` — Umami, Plausible, or Google Analytics
102
136
 
103
137
  ## Data Layer Highlights (`src/lib/markdown.ts`)
104
138
 
@@ -108,6 +142,15 @@ src/app/
108
142
  - Notes: `getAllNotes`, `getNoteBySlug`, `getNotesByTag`
109
143
  - Discovery: `buildSlugRegistry`, `getBacklinks`, `getAllTags`, `getAllAuthors`
110
144
 
145
+ ## Code Block Highlighting
146
+
147
+ - Highlighter: **Shiki** (build-time, dual `github-light` / `github-dark` theme via CSS variables). See `docs/CODE-BLOCKS.md` for author-facing fence/directive metadata.
148
+ - Singleton lives at `src/lib/shiki.ts` (`getHighlighter()`, cached on `globalThis` for HMR). It exposes `highlightToHast(code, language, opts)` and `parseFenceMeta(meta)`.
149
+ - Transformers: `transformerMetaHighlight` from `@shikijs/transformers` plus three custom transformers in `src/lib/shiki.ts` for the line-numbers data attribute, the title data attribute, and raw-`diff` line backgrounds.
150
+ - MDX/Markdown path: `MarkdownRenderer.tsx` → `rehype-fence-meta` (copies `node.data.meta` to a real `data-meta` HTML attribute so it survives `react-markdown`'s prop filtering and the `rehypeRaw` round-trip) → `CodeBlock` (async server component) → Shiki → inline HTML. Mermaid blocks are short-circuited before `CodeBlock` is reached.
151
+ - rST path: `scripts/render-rst.py` rewrites every `literal_block` into a `<pre data-amytis-code …>` marker carrying option attributes (`data-language`, `data-line-numbers`, `data-highlight-lines`, `data-title`); `src/lib/shiki-rst.ts` walks the rendered HTML inside `RstRenderer` (async server component) and replaces each marker with Shiki output. The fallback rST parser routes through `rstToMarkdown` and lands in the MDX path — single highlighter, single source of truth.
152
+ - Cache: `RST_RENDERER_DISK_CACHE_VERSION` in `src/lib/rst-renderer.ts` must be bumped whenever the docutils output shape or Shiki theme changes, otherwise stale cached entries in `.cache/rst-renderer/` keep rendering with the old markup.
153
+
111
154
  ## rST Notes
112
155
 
113
156
  - Full-fidelity rST rendering depends on a Python environment with `docutils` (and ideally `pygments`) available.
@@ -127,3 +170,172 @@ src/app/
127
170
  5. Pagefind indexing:
128
171
  - Production: `pagefind --site out` (writes to `out/pagefind`)
129
172
  - Dev build: `pagefind --site out --output-path public/pagefind`
173
+
174
+ ## Content Frontmatter
175
+
176
+ Validated by Zod in `src/lib/markdown.ts` — invalid frontmatter throws at build time.
177
+
178
+ ### Posts
179
+
180
+ ```yaml
181
+ ---
182
+ title: "Post Title"
183
+ subtitle: "Optional subtitle line" # Rendered below the title in italic
184
+ date: "2026-01-01"
185
+ excerpt: "Optional summary (auto-generated if omitted)"
186
+ category: "Category Name"
187
+ tags: ["tag1", "tag2"]
188
+ authors: ["Author Name"]
189
+ series: "series-slug" # Link to a series
190
+ draft: true # Hidden in production
191
+ featured: true # Show in featured section
192
+ pinned: true # Always shown in featured section; hero = most recent pinned
193
+ coverImage: "./images/cover.jpg" # Local path, external URL, or text placeholder
194
+ latex: true # Enable KaTeX math
195
+ toc: false # Hide table of contents
196
+ layout: "simple" # Use simple layout (default: "post")
197
+ externalLinks: # Links to external discussions
198
+ - name: "Hacker News"
199
+ url: "https://news.ycombinator.com/item?id=12345"
200
+ - name: "V2EX"
201
+ url: "https://v2ex.com/t/123456"
202
+ redirectFrom: # Old URLs to redirect to this post (prefix changes only)
203
+ - /posts/my-old-slug
204
+ - /old-series/my-old-slug
205
+ ---
206
+ ```
207
+
208
+ ### Series (`content/series/[slug]/index.mdx`)
209
+
210
+ ```yaml
211
+ ---
212
+ title: "Series Title"
213
+ excerpt: "Series description"
214
+ date: "2026-01-01"
215
+ coverImage: "./images/cover.jpg"
216
+ featured: true # Show in featured series
217
+ draft: true # Hidden in production (default: false)
218
+ sort: "date-asc" # 'date-asc' | 'date-desc' | 'manual'
219
+ posts: ["post-1", "post-2"] # Manual post ordering (optional)
220
+ ---
221
+ ```
222
+
223
+ ### Books (`content/books/[slug]/index.mdx`)
224
+
225
+ A book's `chapters:` array accepts three item shapes (mix freely):
226
+
227
+ ```yaml
228
+ ---
229
+ title: "Book Title"
230
+ excerpt: "Book description"
231
+ date: "2026-01-01"
232
+ coverImage: "text:DG" # Image path or text placeholder
233
+ featured: true
234
+ draft: false
235
+ authors: ["Author Name"]
236
+ chapters:
237
+ # 1. Bare chapter ref — top-level chapter with no grouping
238
+ - title: "Standalone Chapter"
239
+ id: "standalone"
240
+
241
+ # 2. Legacy "part" — single-level grouping
242
+ - part: "Part I: Getting Started"
243
+ chapters:
244
+ - title: "Chapter Title"
245
+ id: "chapter-file" # Maps to chapter-file.mdx or chapter-file/index.mdx
246
+
247
+ # 3. "section" — recursive grouping with arbitrary nesting depth (≥ 2 layers)
248
+ - section: "机器学习数学基础"
249
+ collapsible: true # Optional UI hint for the sidebar
250
+ items:
251
+ - section: "线性代数"
252
+ items:
253
+ - title: "引言:机器学习的语言"
254
+ id: "maths/linear/introduction" # Slash-separated id → nested folder on disk
255
+ - title: "向量基础"
256
+ id: "maths/linear/vectors"
257
+ - section: "微积分"
258
+ items:
259
+ - title: "引言:变化与累积"
260
+ id: "maths/calculus/introduction"
261
+ ---
262
+ ```
263
+
264
+ Chapter `id` values may contain `/` to map to nested folders. For id `maths/linear/introduction`,
265
+ the loader resolves to the first existing file under `<bookDir>/maths/linear/introduction.{md,mdx}`
266
+ or `<bookDir>/maths/linear/introduction/index.{md,mdx}`. Path traversal (`..`) is rejected.
267
+
268
+ Per the strict-build invariant, `getBookData` throws if any chapter id in the TOC has no
269
+ matching file on disk — silent skips are not allowed.
270
+
271
+ #### Book-level `latex: true`
272
+
273
+ Book frontmatter accepts an optional `latex: true` flag that enables KaTeX rendering for
274
+ every chapter in the book without having to annotate each chapter file. Chapter-level
275
+ `latex: true` still works and takes precedence. Math-heavy books (e.g. ML textbooks) should
276
+ set the book-level flag rather than copy it onto every chapter.
277
+
278
+ #### Book-level `showChapterExcerpt`
279
+
280
+ Book frontmatter accepts an optional `showChapterExcerpt` flag (default `false`)
281
+ controlling whether the chapter-page header renders the chapter's `excerpt` underneath
282
+ the title. The default suppresses it because the common case is a chapter that opens
283
+ with its own lede paragraph, and an excerpt line above it just duplicates that text.
284
+ Set it to `true` on books where the excerpt is a distinct subtitle the author actually
285
+ wants the reader to see. The excerpt is still used in metadata (OpenGraph, JSON-LD,
286
+ search) regardless of this flag.
287
+
288
+ #### Book-specific markdown extensions
289
+
290
+ When `MarkdownRenderer` renders a book chapter (i.e. `bookContext` prop is set, which
291
+ happens automatically inside `BookLayout`), two extra plugins fire:
292
+
293
+ - **`remark-vuepress-containers`** — converts VuePress fenced containers
294
+ (`:::note`, `:::tip`, `:::important`, `:::warning`, `:::danger`, `:::info`) into the
295
+ same `<github-alert>` hast element that `remark-github-alerts` produces. Custom titles
296
+ (`:::tip 智慧的疆界`) are preserved via `data-alert-title`. A small string-level
297
+ preprocessor (`normalizeVuepressContainerSyntax`) normalizes `::: name [label]` →
298
+ `:::name[label]` so `remark-directive` (which only parses the space-less form) sees the
299
+ containers.
300
+ - **`remark-book-chapter-links`** — rewrites relative `.md` / `.mdx` links to other
301
+ chapters into canonical `/books/<slug>/<chapter-id>[#fragment]` URLs. Resolution uses
302
+ the chapter's `sourcePath` (exposed by `getBookChapter`). Broken links (target chapter
303
+ id not in the TOC, or target outside the book directory) throw at build time.
304
+
305
+ Mermaid diagrams in book chapters already work via the existing `Mermaid` component (any
306
+ \`\`\`mermaid fenced block, with or without a `compact` modifier after the language tag).
307
+
308
+ ## Configuration Reference (`site.config.ts`)
309
+
310
+ | Field | Notes |
311
+ | --- | --- |
312
+ | `nav` | Navigation links with weights |
313
+ | `social` | GitHub, Twitter, email links for the footer |
314
+ | `series.navbar` | Series slugs to show in the navbar dropdown |
315
+ | `series.customPaths` | Per-series URL prefix, e.g. `{ 'weeklies': 'weeklies' }` → `/weeklies/[slug]` |
316
+ | `pagination.posts`, `pagination.series` | Items per page |
317
+ | `themeColor` | `'default' \| 'blue' \| 'rose' \| 'amber'` |
318
+ | `hero` | Homepage hero title and subtitle |
319
+ | `i18n` | Default locale and supported locales |
320
+ | `featured.series` | Scrollable series: `scrollThreshold` (default 2), `maxItems` (default 6) |
321
+ | `featured.stories` | Scrollable stories: `scrollThreshold` (default 1), `maxItems` (default 5) |
322
+ | `analytics.providers` | Enabled providers, e.g. `['umami', 'google']`; `[]` disables |
323
+ | `comments.provider` | `'giscus' \| 'disqus' \| null` |
324
+ | `feed.format` | `'rss' \| 'atom' \| 'both'` |
325
+ | `feed.content` | `'full' \| 'excerpt'` |
326
+ | `feed.maxItems` | Max feed items (`0` = unlimited) |
327
+ | `footer.bottomLinks` | Custom footer links (ICP, cookie policy); `text` accepts plain string or `{ en, zh }` |
328
+ | `posts.basePath` | URL prefix for all posts (default `'posts'`) |
329
+ | `posts.authors.default` | Fallback authors when a post has none in frontmatter |
330
+ | `posts.authors.showInHeader` | Show author byline below post title (default `true`) |
331
+ | `posts.authors.showAuthorCard` | Show author card at end of post (default `true`) |
332
+ | `posts.excludeFromListing` | Series slugs whose posts are hidden from `/posts` listings |
333
+ | `authors` | Per-author profiles: `bio`, `avatar`, `social[]` |
334
+
335
+ ### Config sync
336
+
337
+ `site.config.ts` (this repo, i18n object form) and `site.config.example.ts`
338
+ (shipped via `create-amytis`, plain strings, single-locale, optional features
339
+ default disabled) must stay in sync. Any schema change to one must be mirrored
340
+ in the other. Locale-aware fields use `{ en, zh }` in `site.config.ts` and
341
+ plain strings in the example.