hd-garden 0.7.0 → 0.8.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/.agents/skills/garden-ui/SKILL.md +26 -19
- package/README.en.md +57 -0
- package/README.md +31 -46
- package/app/blog/[...path]/page.tsx +9 -1
- package/app/columns/[column]/[...path]/page.tsx +10 -12
- package/app/components/ArticlePager.tsx +32 -0
- package/app/components/Comments.tsx +2 -1
- package/app/components/FeedMore.tsx +70 -0
- package/app/components/GardenDocsNav.tsx +25 -20
- package/app/components/HandbookLangRedirect.tsx +30 -0
- package/app/components/HandbookLangSwitch.tsx +49 -0
- package/app/components/HandbookView.tsx +187 -0
- package/app/components/MarkdownArticle.tsx +11 -5
- package/app/components/MarkdownImage.tsx +6 -4
- package/app/components/SiteChrome.tsx +11 -2
- package/app/components/SiteFooter.tsx +5 -3
- package/app/components/SiteHeader.tsx +27 -23
- package/app/components/TableOfContents.tsx +15 -9
- package/app/components/ThoughtFeed.tsx +41 -0
- package/app/components/UiLang.tsx +18 -0
- package/app/components/Webmentions.tsx +3 -2
- package/app/components/garden/FilePreview.tsx +13 -11
- package/app/components/garden/GardenBlock.tsx +3 -1
- package/app/components/garden/GardenFile.tsx +9 -7
- package/app/components/garden/blocks.tsx +34 -24
- package/app/components/garden/chrome.tsx +20 -13
- package/app/garden/[...slug]/page.tsx +10 -130
- package/app/garden/en/[...slug]/page.tsx +28 -0
- package/app/garden/en/page.tsx +16 -0
- package/app/garden/page.tsx +7 -74
- package/app/garden-modules.d.ts +3 -0
- package/app/layout.tsx +2 -4
- package/app/lib/content.ts +12 -2
- package/app/lib/garden-docs-meta.ts +60 -5
- package/app/lib/handbook-path.ts +11 -0
- package/app/lib/i18n.ts +222 -0
- package/app/lib/page-alternates.ts +2 -2
- package/app/not-found.tsx +5 -3
- package/app/site.config.ts +1 -1
- package/app/styles/a2.css +101 -32
- package/app/styles/shell.css +6 -0
- package/app/thoughts/page/[n]/page.tsx +44 -0
- package/app/thoughts/page.tsx +2 -6
- package/bin/garden.mjs +4 -0
- package/docs/handbook/ai-writing.md +99 -0
- package/docs/handbook/components/card.md +2 -0
- package/docs/handbook/components/chart.md +2 -0
- package/docs/handbook/components/compare.md +10 -12
- package/docs/handbook/components/figure.md +2 -0
- package/docs/handbook/components/file.md +2 -0
- package/docs/handbook/components/html.md +2 -0
- package/docs/handbook/components/interactive.md +2 -0
- package/docs/handbook/components/stat.md +2 -0
- package/docs/handbook/components/timeline.md +2 -0
- package/docs/handbook/components.md +1 -1
- package/docs/handbook/config.md +2 -2
- package/docs/handbook/deploy.md +1 -1
- package/docs/handbook/design.md +3 -3
- package/docs/handbook/en/ai-writing.md +99 -0
- package/docs/handbook/en/components/card.md +46 -0
- package/docs/handbook/en/components/chart.md +54 -0
- package/docs/handbook/en/components/compare.md +38 -0
- package/docs/handbook/en/components/figure.md +72 -0
- package/docs/handbook/en/components/file/checklist.md +17 -0
- package/docs/handbook/en/components/file.md +34 -0
- package/docs/handbook/en/components/html.md +69 -0
- package/docs/handbook/en/components/interactive.md +67 -0
- package/docs/handbook/en/components/stat.md +57 -0
- package/docs/handbook/en/components/timeline.md +52 -0
- package/docs/handbook/en/components.md +30 -0
- package/docs/handbook/en/config.md +125 -0
- package/docs/handbook/en/deploy.md +273 -0
- package/docs/handbook/en/design.md +81 -0
- package/docs/handbook/en/index.md +192 -0
- package/docs/handbook/en/start.md +92 -0
- package/docs/handbook/en/workflow.md +81 -0
- package/docs/handbook/en/writing.md +156 -0
- package/docs/handbook/index.md +98 -3
- package/docs/handbook/writing.md +18 -10
- package/package.json +1 -1
- package/scripts/build-content.mjs +23 -3
- package/scripts/handbook-facts.mjs +34 -0
- package/scripts/html-lang.mjs +36 -0
- package/scripts/static-404.mjs +25 -0
- package/scripts/ui-cli.mjs +14 -8
- package/templates/site/site.config.yaml +2 -0
- package/ui/core/apply.mjs +22 -4
- package/ui/core/cli-locale.mjs +67 -0
- package/ui/core/fence.mjs +18 -3
- package/ui/core/fix-prompt.mjs +18 -15
- package/ui/core/garden-fence.mjs +64 -0
- package/ui/core/scan.mjs +19 -3
- package/ui/garden/adjacent.mjs +21 -0
- package/ui/garden/check.mjs +6 -0
- package/ui/garden/html-lang.mjs +16 -0
- package/ui/garden/paging.mjs +30 -0
- package/ui/preview/dist/garden-ui.css +9 -9
- package/ui/preview/dist/garden-ui.esm.js +169 -169
- package/ui/preview/dist/garden-ui.iife.js +169 -169
- package/ui/react/components/chart.tsx +7 -4
- package/ui/react/components/display.tsx +6 -3
- package/ui/react/components/inputs.tsx +6 -3
- package/ui/react/components/layout.tsx +6 -4
- package/ui/react/components/media.tsx +7 -4
- package/ui/react/components/sandbox.tsx +3 -1
- package/ui/react/index.tsx +11 -7
- package/ui/react/lang.tsx +85 -0
- package/ui/react/runtime.tsx +3 -1
- package/ui/react/ui.css +9 -9
- package/worker/index.ts +8 -1
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Checks & Shortcuts
|
|
3
|
+
navTitle: Checks & Shortcuts
|
|
4
|
+
description: Validate posts and diagnostic codes with npx garden check, install the Git pre-commit hook, and use editor snippets and Agent skills.
|
|
5
|
+
section: workflow
|
|
6
|
+
order: 40
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
To catch YAML indentation mistakes, missing required fields, or broken local file paths before publishing, Garden ships a CLI validator, a Git pre-commit hook, snippet templates, and bundled Agent skills.
|
|
10
|
+
|
|
11
|
+
## 1. Validation Commands
|
|
12
|
+
|
|
13
|
+
Three commands check your content at different scopes:
|
|
14
|
+
|
|
15
|
+
- **Check one or more posts (`npx garden check`)**:
|
|
16
|
+
```bash
|
|
17
|
+
npx garden check content/posts/my-post.md
|
|
18
|
+
# Or pass multiple Markdown files at once
|
|
19
|
+
npx garden check content/posts/*.md
|
|
20
|
+
```
|
|
21
|
+
- **Check interactive blocks and component syntax (`npx garden ui check`)**:
|
|
22
|
+
```bash
|
|
23
|
+
npx garden ui check content/posts/my-post.md
|
|
24
|
+
# Check staged Markdown files in Git
|
|
25
|
+
npx garden ui check --staged
|
|
26
|
+
# Check all Markdown files in the repository
|
|
27
|
+
npx garden ui check --all
|
|
28
|
+
```
|
|
29
|
+
- **Sync and build the entire site (`make check`)**:
|
|
30
|
+
```bash
|
|
31
|
+
make check
|
|
32
|
+
```
|
|
33
|
+
In the site `Makefile`, `make check` runs `npx garden sync && npx garden build` to execute full content synchronization, component validation, and the production build.
|
|
34
|
+
|
|
35
|
+
## 2. `npx garden check` Diagnostic Codes
|
|
36
|
+
|
|
37
|
+
`npx garden check` (backed by `scripts/check-post.mjs`, `ui/garden/check.mjs`, and `ui/garden/file-paths.mjs`) reports exact line numbers, diagnostic codes, and fix suggestions:
|
|
38
|
+
|
|
39
|
+
| Code | Severity | Cause and Fix |
|
|
40
|
+
| :--- | :--- | :--- |
|
|
41
|
+
| `missing-title` / `missing-description` | Error | Front Matter is missing a non-empty `title` or `description` |
|
|
42
|
+
| `unexpected-slug` | Error | Front Matter includes a manual `slug` field (remove `slug`; the filename is the slug) |
|
|
43
|
+
| `date-format` | Error | `date` is not a valid `YYYY-MM-DD`, `YYYY-MM-DD HH:mm:ss`, or ISO timestamp with timezone |
|
|
44
|
+
| `missing-cover` / `missing-asset` | Error | A local file referenced by `cover`, a Markdown image, or `garden file` does not exist |
|
|
45
|
+
| `heading-h1` / `heading-skip` | Error | The post body uses an `#` H1 heading or skips a heading level (such as jumping from `##` to `####`) |
|
|
46
|
+
| `reference-heading` | Error | The post body contains a references section heading (move references to the `references` Front Matter list) |
|
|
47
|
+
| `callout-type` | Error | `> [!TYPE]` uses an unsupported callout type |
|
|
48
|
+
| `title-split` | Warning | The title contains a split delimiter with an empty left or right side |
|
|
49
|
+
| `diff-orphan` | Error | A `diff` code fence is not immediately preceded by a complete code block |
|
|
50
|
+
| `legacy-fence` | Error | Uses a legacy fence name (such as `garden:stat`, `widget`, `embed`, or `html garden`); replace with `garden <name>` |
|
|
51
|
+
| `garden-idea` | Error | A non-draft post still contains an uncompiled `garden` idea fence |
|
|
52
|
+
| `garden-name` | Error | Unknown `garden` component name (must be `card`, `stat`, `compare`, `timeline`, `chart`, `figure`, `html`, `interactive`, or `file`) |
|
|
53
|
+
| `garden-yaml` | Error | Invalid YAML syntax, wrong top-level shape, or unsupported keys inside a `garden` fence |
|
|
54
|
+
| `garden-file` / `file-outside` | Error | `garden file` manually sets `resolved`, is missing a valid `src` / `files`, or points outside `content/` |
|
|
55
|
+
| `file-large` | Warning | A local file referenced by `garden file` exceeds `5 MB` |
|
|
56
|
+
| `compare-long` | Error | A single cell in `garden compare` exceeds roughly 60 characters |
|
|
57
|
+
| `interactive-trivial` / `steps-prose` | Error | `garden interactive` only computes a single multiplication, or `<Steps>` contains only prose without state changes |
|
|
58
|
+
|
|
59
|
+
## 3. Git Pre-commit Hook and Component Catalog
|
|
60
|
+
|
|
61
|
+
- **Install the pre-commit hook**: Run `npx garden hooks` to write `.git/hooks/pre-commit`, which runs `npx --no-install garden ui check --staged --quiet` on every `git commit`:
|
|
62
|
+
```bash
|
|
63
|
+
npx garden hooks
|
|
64
|
+
```
|
|
65
|
+
- **Generate the local component catalog**: Run `npx garden ui catalog` to write a static preview page of all built-in components to `.garden/ui-catalog.html`:
|
|
66
|
+
```bash
|
|
67
|
+
npx garden ui catalog
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## 4. Component Templates, Snippets, and Agent Skills
|
|
71
|
+
|
|
72
|
+
Garden provides three ways to insert or generate components quickly:
|
|
73
|
+
|
|
74
|
+
1. **Draft cheat sheet comment**: Running `make new SLUG=xxx TITLE="..."` appends an HTML comment to the bottom of the new `.md` file with examples for idea fences and all 9 components. HTML comments are hidden from the rendered page and can be deleted when finished.
|
|
75
|
+
2. **Snippet template files**: The `templates/` directory inside `hd-garden` includes two template files:
|
|
76
|
+
- `templates/garden-blocks.md`: Copyable Markdown templates for idea fences and all 9 built-in components.
|
|
77
|
+
- `templates/garden-snippets.plist`: macOS text replacements (drag into **System Settings → Keyboard → Text Replacements**) with triggers `;gidea`, `;gcard`, `;gstat`, `;gcompare`, `;gtimeline`, `;gchart`, `;gfigure`, `;ghtml`, `;ginteractive`, and `;gfile`.
|
|
78
|
+
3. **Install AI Agent skills (`npx garden skill`)**: Running `npx garden skill` in a site repository (also run automatically during `npx hd-garden init`) copies three skills into `.agents/skills/`:
|
|
79
|
+
- `garden-ui`: Compiles natural-language `garden` idea fences into concrete component blocks.
|
|
80
|
+
- `garden-interactive-chart`: Authors `interactive`, `chart`, `figure`, and `html` components from article data.
|
|
81
|
+
- `garden-post-review`: Reviews an entire post against layout and component rules.
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Writing Posts & Thoughts
|
|
3
|
+
navTitle: Writing Posts & Thoughts
|
|
4
|
+
description: Front Matter fields for posts and thoughts, code diff toggles, callouts, link cards, math formulas, and Mermaid diagrams.
|
|
5
|
+
section: writing-and-reading
|
|
6
|
+
order: 10
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
Garden organizes content into **posts** (`content/posts/`) and **thoughts** (`content/thoughts/`). Both are UTF-8 Markdown files whose filenames contain only lowercase letters, digits, and hyphens (`a-z`, `0-9`, `-`).
|
|
10
|
+
|
|
11
|
+
## 1. Post Front Matter (`content/posts/*.md`)
|
|
12
|
+
|
|
13
|
+
Create a post with `make new DIR=systems SLUG=wal-checkpoint TITLE="Understanding WAL | Checkpoints and Recovery"` or create a `.md` file directly:
|
|
14
|
+
|
|
15
|
+
```yaml
|
|
16
|
+
---
|
|
17
|
+
title: Understanding WAL | Checkpoints and Recovery
|
|
18
|
+
date: 2026-10-18
|
|
19
|
+
updated: 2026-10-19
|
|
20
|
+
description: How write-ahead logging preserves snapshot visibility under concurrent reads and writes, and how background checkpoints bound log growth.
|
|
21
|
+
tags:
|
|
22
|
+
- Database
|
|
23
|
+
- Storage
|
|
24
|
+
pinned: false
|
|
25
|
+
draft: false
|
|
26
|
+
cover: windowsill-herbs.svg
|
|
27
|
+
original: https://example.com/original-post
|
|
28
|
+
references:
|
|
29
|
+
- title: SQLite Write-Ahead Logging
|
|
30
|
+
url: https://www.sqlite.org/wal.html
|
|
31
|
+
- title: Database Internals
|
|
32
|
+
---
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
| Field | Required | Type | Description |
|
|
36
|
+
| :--- | :--- | :--- | :--- |
|
|
37
|
+
| `title` | Yes | String | Post title. Write ` \| ` (with spaces on both sides) to split the heading into two lines and scale the font size to fit |
|
|
38
|
+
| `date` | Yes | Date string | Publication date (such as `2026-10-18` or an ISO timestamp with timezone) |
|
|
39
|
+
| `description` | Yes | String | Summary shown in the article deck, archive lists, and search engine descriptions |
|
|
40
|
+
| `updated` | No | Date string | Last updated date, shown in the article meta bar and used for RSS sorting |
|
|
41
|
+
| `publishAt` | No | Date string | Scheduled publish time; included only when earlier than the build time (or when `CONTENT_INCLUDE_DRAFTS=1`) |
|
|
42
|
+
| `tags` | No | String array | Tags for the post; clicking a tag filters `/blog?t=<tag>` |
|
|
43
|
+
| `pinned` | No | Boolean | Set to `true` to feature the post in the pinned section on the home page |
|
|
44
|
+
| `draft` | No | Boolean | Set to `true` to mark as a draft and exclude from production builds |
|
|
45
|
+
| `cover` | No | Path string | Cover image path relative to `content/covers/` |
|
|
46
|
+
| `original` | No | Full URL | External canonical source link, rendered in the article meta bar |
|
|
47
|
+
| `references` | No | `{ title, url? }[]` | Reference list rendered at the bottom of the post with automatically extracted domain names |
|
|
48
|
+
|
|
49
|
+
At the bottom of every post page, between the Webmention section and the comments, sit the Previous and Next navigation cards. Posts link only to other posts ordered by `date` (where Previous is the older post), while column entries follow their order in `content/columns.yaml`; the oldest and newest entries show only one card.
|
|
50
|
+
|
|
51
|
+
> [!CAUTION] Caution
|
|
52
|
+
> Do not write a `slug` field in Front Matter (`npx garden check` reports an `unexpected-slug` error because the filename is already the slug). Posts do not use a `topic` field either.
|
|
53
|
+
|
|
54
|
+
## 2. Code Blocks and the Diff Toggle
|
|
55
|
+
|
|
56
|
+
Standard code blocks show their language label (such as `RUST`, `TS`, `PYTHON`, or `SHELL`) and a copy button in the top-right corner. To show how a snippet changed from an earlier version, place the updated code block first and follow it with a `garden` idea fence stating what changed in one sentence:
|
|
57
|
+
|
|
58
|
+
````markdown
|
|
59
|
+
```go
|
|
60
|
+
func openStore(path string) (*Store, error) {
|
|
61
|
+
return openWithMode(path, ModeWAL)
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
```garden
|
|
66
|
+
Compared with the previous version: open the store with ModeWAL instead of ModeJournal.
|
|
67
|
+
```
|
|
68
|
+
````
|
|
69
|
+
|
|
70
|
+
Following the `garden-ui` skill, the Agent runs `npx garden ui apply` to replace that idea fence with a `diff` fence right below the code block (see [Writing with AI](/garden/en/ai-writing)). The page combines the two adjacent blocks into a single code frame with tabs for the full code and the diff against the previous version, while the copy button always copies the new full code:
|
|
71
|
+
|
|
72
|
+
```go
|
|
73
|
+
func openStore(path string) (*Store, error) {
|
|
74
|
+
return openWithMode(path, ModeWAL)
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
```diff
|
|
79
|
+
func openStore(path string) (*Store, error) {
|
|
80
|
+
- return openWithMode(path, ModeJournal)
|
|
81
|
+
+ return openWithMode(path, ModeWAL)
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Hand-written format**: Without an Agent, you can write the `diff` code block directly below the complete code block (separated only by a blank line):
|
|
86
|
+
|
|
87
|
+
````markdown
|
|
88
|
+
```diff
|
|
89
|
+
func openStore(path string) (*Store, error) {
|
|
90
|
+
- return openWithMode(path, ModeJournal)
|
|
91
|
+
+ return openWithMode(path, ModeWAL)
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
````
|
|
95
|
+
|
|
96
|
+
> [!IMPORTANT] Important
|
|
97
|
+
> A `diff` fence must immediately follow a complete code block; a standalone `diff` fence with no preceding code block triggers a `diff-orphan` error in `npx garden check`.
|
|
98
|
+
|
|
99
|
+
## 3. CJK Bold, Highlighter Strokes, and Callouts
|
|
100
|
+
|
|
101
|
+
- **CJK-friendly bold**: `remark-cjk-friendly` is enabled so `**bold next to CJK punctuation**` parses cleanly without extra spaces around the asterisks.
|
|
102
|
+
- **Highlighter stroke**: Wrap text in `<u>key phrase</u>` or `<mark>key phrase</mark>` to apply a warm yellow highlighter band, like <u>this highlighted phrase</u>.
|
|
103
|
+
- **Callouts**: Supports `> [!NOTE]`, `> [!TIP]`, `> [!IMPORTANT]`, `> [!WARNING]`, and `> [!CAUTION]` (plus `INFO`, `SUCCESS`, `DANGER`, and `FAILURE`). Write a title on the same line after the marker to set a custom heading:
|
|
104
|
+
|
|
105
|
+
```markdown
|
|
106
|
+
> [!WARNING] Back Up the Data Directory Before Upgrading
|
|
107
|
+
> Changing the page size requires rebuilding the index files and cannot be rolled back in place.
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
> [!WARNING] Back Up the Data Directory Before Upgrading
|
|
111
|
+
> Changing the page size requires rebuilding the index files and cannot be rolled back in place.
|
|
112
|
+
|
|
113
|
+
## 4. Local Images and External Link Cards
|
|
114
|
+
|
|
115
|
+
- **Local images and sidecar folders**: For a post at `content/posts/my-post.md`, place its images inside `content/posts/my-post/` and reference them with relative paths such as `./my-post/diagram.svg`. At build time, Garden copies the file, records its pixel dimensions, and renders the `alt` text (unless it starts with default placeholders like `image` or `img`) as a `<figcaption>`.
|
|
116
|
+
- **Inline external links**: External links in body text automatically display the target site's favicon and keep the icon attached to the first word of the link so it never wraps alone.
|
|
117
|
+
- **Link cards**: Turn an external URL into a preview card with site name, title, and description in either of two ways:
|
|
118
|
+
1. Put the full URL on its own paragraph line (surrounded by blank lines): `https://spec.commonmark.org/`
|
|
119
|
+
2. Append `{card}` directly to an inline link (with no space): `See the [CommonMark Spec](https://spec.commonmark.org/){card}.`
|
|
120
|
+
You can also run `make embed URL="https://example.com"` to fetch Open Graph metadata for a URL.
|
|
121
|
+
|
|
122
|
+
## 5. Math Formulas and Mermaid Diagrams
|
|
123
|
+
|
|
124
|
+
- **LaTeX math**: Rendered at build and server-render time with KaTeX. Use `$L = \lambda W$` for inline math and `$$...$$` for display blocks. Multi-letter identifiers containing underscores (such as `group_num` or `max_batch`) are automatically wrapped as upright `\mathrm{group\_num}`, while single-letter subscripts like `W_q` or `H_{kv}` remain standard math subscripts.
|
|
125
|
+
- **Mermaid diagrams**: Use ```` ```mermaid ```` fences to draw flowcharts or sequence diagrams that adapt to light and dark themes:
|
|
126
|
+
|
|
127
|
+
```mermaid
|
|
128
|
+
graph LR
|
|
129
|
+
A[Markdown Source] --> B[garden sync]
|
|
130
|
+
B --> C[Search & Static Assets]
|
|
131
|
+
C --> D[Rendered Page]
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## 6. Writing Thoughts (`content/thoughts/*.md`)
|
|
135
|
+
|
|
136
|
+
Thoughts are short notes with optional image grids, audio/video files, or external link cards:
|
|
137
|
+
|
|
138
|
+
```yaml
|
|
139
|
+
---
|
|
140
|
+
title: Late-Night Debugging Notes
|
|
141
|
+
date: 2026-10-19 21:30
|
|
142
|
+
tags:
|
|
143
|
+
- Notes
|
|
144
|
+
media:
|
|
145
|
+
- type: image
|
|
146
|
+
src: ./rainy-night/terminal.png
|
|
147
|
+
alt: Benchmark output in the terminal
|
|
148
|
+
- type: link
|
|
149
|
+
src: https://sqlite.org/wal.html
|
|
150
|
+
title: SQLite Write-Ahead Logging
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
A short note written in standard Markdown.
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
The `media` array accepts four item types: `image` (multiple images automatically form a grid), `audio` (native `<audio>` player), `video` (native `<video>` player with optional `poster`), and `link` (rendered as an external link card supporting `src`, `title`, `description`, and `image`).
|
package/docs/handbook/index.md
CHANGED
|
@@ -5,11 +5,106 @@ section: overview
|
|
|
5
5
|
order: 0
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Garden 是一个 Claude 风格的博客框架,文章用 Markdown 写,组件用自然语言描述就行。
|
|
9
|
+
|
|
10
|
+
## 一个完整的例子
|
|
11
|
+
|
|
12
|
+
下面四步是同一篇稿子的真实过程,命令输出均来自实际运行结果。
|
|
13
|
+
|
|
14
|
+
### 1. 作者写一句想法
|
|
15
|
+
|
|
16
|
+
在想放交互的地方写一个不带名字的 `garden` 围栏,里面写清想让读者做什么、看到什么:
|
|
17
|
+
|
|
18
|
+
````markdown
|
|
19
|
+
## 写作节奏
|
|
20
|
+
|
|
21
|
+
```garden
|
|
22
|
+
让读者拖动每天写的字数和每周写几天,看写完一篇 8000 字的长文要几周。
|
|
23
|
+
```
|
|
24
|
+
````
|
|
25
|
+
|
|
26
|
+
### 2. Agent 扫描并写回组件
|
|
27
|
+
|
|
28
|
+
对 Agent 说「编译这篇:content/posts/writing-pace.md」。Agent 按站点里的 `garden-ui` skill 先运行 `npx garden ui scan`,拿到这一块的行号、原话和哈希:
|
|
29
|
+
|
|
30
|
+
```console
|
|
31
|
+
$ npx garden ui scan content/posts/writing-pace.md
|
|
32
|
+
{
|
|
33
|
+
"file": "content/posts/writing-pace.md",
|
|
34
|
+
"draft": false,
|
|
35
|
+
"blocks": [
|
|
36
|
+
{
|
|
37
|
+
"lang": "garden-idea",
|
|
38
|
+
"status": "uncompiled",
|
|
39
|
+
"startLine": 9,
|
|
40
|
+
"intentHash": "cb3a3569",
|
|
41
|
+
"intent": "让读者拖动每天写的字数和每周写几天,看写完一篇 8000 字的长文要几周。",
|
|
42
|
+
"section": "写作节奏"
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
接着 Agent 读取 `npx garden ui prompt` 里的组件目录,写出 `garden interactive`,再用 `npx garden ui apply` 按 `--intent-hash` 只替换这一块,围栏外的正文逐字节不变:
|
|
49
|
+
|
|
50
|
+
```console
|
|
51
|
+
$ npx garden ui apply content/posts/writing-pace.md --intent-hash cb3a3569 --file new.md
|
|
52
|
+
{"ok":true,"file":"content/posts/writing-pace.md","kind":"garden","line":9}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### 3. 提交前钩子检查并生成修复提示词
|
|
56
|
+
|
|
57
|
+
首次运行 `garden sync` 或 `garden dev` 时,Garden 会在 `.git/hooks/pre-commit` 写入 `exec npx --no-install garden ui check --staged --quiet`。如果作者在第 2 步之前直接提交,钩子会拦下提交,并打印一段可以直接粘贴给 AI 的修复提示词:
|
|
58
|
+
|
|
59
|
+
```console
|
|
60
|
+
$ git commit -m "写作节奏"
|
|
61
|
+
content/posts/writing-pace.md:9 - garden-idea 还有没编译的想法
|
|
62
|
+
|
|
63
|
+
──────────────────────── 发给 AI 的修复提示词 ────────────────────────
|
|
64
|
+
请处理下面 Garden 博文里列出的块:没编译的想法要编译,有错误的交互块要修好。
|
|
65
|
+
|
|
66
|
+
要求:
|
|
67
|
+
- 先运行 npx garden ui prompt,读组件目录和规则;仓库里有 garden-ui skill 时按它的流程做。
|
|
68
|
+
- 只改列出的块,用 npx garden ui apply <稿件> --id <id> 写回(没有 id 的想法用 --intent-hash),不要直接编辑稿件。
|
|
69
|
+
- 不改作者原话的意思。改完运行 npx garden ui check <稿件>,直到通过。
|
|
70
|
+
- 如果你不能访问这个仓库,就只回复每块修好后的完整围栏。
|
|
71
|
+
|
|
72
|
+
1. content/posts/writing-pace.md,第 9–11 行,intent-hash: cb3a3569
|
|
73
|
+
- garden-idea:这段想法还没编译,请按原话编译成 garden interactive 或其他 garden 组件(或判断后写回正文)。
|
|
74
|
+
(后面附当前围栏原文)
|
|
75
|
+
────────────────────────────────────────────────────────────
|
|
76
|
+
已写入 .garden/ui-fix-prompt.md。确实要先提交,可以用 git commit --no-verify。
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
这段提示词同时写入 `.garden/ui-fix-prompt.md`,有剪贴板命令时自动复制。把提示词贴给 Agent 完成转换后再提交,钩子静默通过(退出码 `0`)。
|
|
80
|
+
|
|
81
|
+
### 4. 页面渲染结果
|
|
82
|
+
|
|
83
|
+
第 2 步写回的 `garden interactive` 块在文章页面直接渲染为可拖动的交互控件:
|
|
84
|
+
|
|
85
|
+
```garden interactive #writing-pace
|
|
86
|
+
caption: 每天多写几百字,比每周多写一天更快。
|
|
87
|
+
intent: |
|
|
88
|
+
让读者拖动每天写的字数和每周写几天,看写完一篇 8000 字的长文要几周。
|
|
89
|
+
---
|
|
90
|
+
<State daily={500} days={3} />
|
|
91
|
+
<Let weekly={daily * days} />
|
|
92
|
+
<Let weeks={ceil(8000 / weekly)} />
|
|
93
|
+
|
|
94
|
+
<Slider bind="daily" label="每天写" min={200} max={2000} step={100} unit="字" />
|
|
95
|
+
<Slider bind="days" label="每周写" min={1} max={7} step={1} unit="天" />
|
|
96
|
+
|
|
97
|
+
<Stat label="写完 8000 字需要" value={weeks} unit="周" hint="不足一周按一周算" />
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
> [!IMPORTANT] 人和 Agent 用同一套工具
|
|
101
|
+
> Agent 找块用的 `npx garden ui scan`、写回用的 `npx garden ui apply`、自查用的 `npx garden ui check`,和作者提交时 pre-commit 钩子运行的检查是同一组命令。钩子拦截时打印的提示词,就是发给 Agent 的下一步指令。详见 [用 AI 写作](/garden/ai-writing)。
|
|
102
|
+
|
|
103
|
+
下面几节介绍不用 Agent 也会自动生效的版面与组件规则。
|
|
9
104
|
|
|
10
105
|
## 标题分行与 `740px` 正文列
|
|
11
106
|
|
|
12
|
-
|
|
107
|
+
标题里写一个「丨」,就在那里分成两行,字号按较长一行自动计算。正文保持在 `740px` 阅读列内,宽图会在两侧留白里对称外扩,不会盖住导航和目录。
|
|
13
108
|
|
|
14
109
|
下面这张透明底的线稿在深色模式下会自动垫上暖白底,不用另做一套配图:
|
|
15
110
|
|
|
@@ -27,7 +122,7 @@ order: 0
|
|
|
27
122
|
|
|
28
123
|
## 代码块与「对比上一版」切换
|
|
29
124
|
|
|
30
|
-
|
|
125
|
+
在改好的完整代码下面写一句想法说明和上一版比改了什么,Agent 会生成一段紧挨着的 `diff` 代码块;你也可以直接手写 `diff`。完整代码和 `diff` 紧挨着时,页面会把它们合成一个可切换「代码」和「对比上一版」的代码框,复制按钮始终复制新版完整代码。
|
|
31
126
|
|
|
32
127
|
```yaml
|
|
33
128
|
pages:
|
package/docs/handbook/writing.md
CHANGED
|
@@ -46,14 +46,14 @@ references:
|
|
|
46
46
|
| `original` | 否 | 完整链接 | 原文链接,设置后在标题下方元信息栏显示 `原文 ↗` |
|
|
47
47
|
| `references` | 否 | `{ title, url? }[]` | 文末参考资料列表,构建期自动提取域名并渲染在正文底部的「参考」卡片中 |
|
|
48
48
|
|
|
49
|
+
文章页底部,在「来自开放网络的回应」和评论之间,有「上一篇」「下一篇」两个按钮。长文只和长文排,按 `date` 先后,上一篇是更早的那篇;专栏文章按专栏里的顺序排;最早或最新的一篇只显示一侧。
|
|
50
|
+
|
|
49
51
|
> [!CAUTION]
|
|
50
52
|
> 不要在 Front Matter 里手写 `slug` 字段(`npx garden check` 会报 `unexpected-slug` 错误:`不要写 slug,文件名就是标识`)。文章也没有 `topic` 分类字段。
|
|
51
53
|
|
|
52
54
|
## 2. 代码块与「代码 / 对比上一版」切换
|
|
53
55
|
|
|
54
|
-
普通代码块在右上角带有语言名称(如 `RUST`、`TS`、`PYTHON`、`SHELL
|
|
55
|
-
|
|
56
|
-
如果你想展示某段代码修改前后的对比,**先放一段完整代码块,紧接着放一段 `diff` 代码块**。渲染器会自动把它们合并成一个带 **「代码」** 和 **「对比上一版」** 两个切换按钮的代码框:
|
|
56
|
+
普通代码块在右上角带有语言名称(如 `RUST`、`TS`、`PYTHON`、`SHELL`)和复制按钮。想展示一段代码和上一版相比改了什么,先放改好的完整代码,再紧跟着写一个 `garden` 想法块,用一句话写出改动内容:
|
|
57
57
|
|
|
58
58
|
````markdown
|
|
59
59
|
```go
|
|
@@ -62,15 +62,12 @@ func openStore(path string) (*Store, error) {
|
|
|
62
62
|
}
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
- return openWithMode(path, ModeJournal)
|
|
68
|
-
+ return openWithMode(path, ModeWAL)
|
|
69
|
-
}
|
|
65
|
+
```garden
|
|
66
|
+
和上一版比:打开存储时的模式从 ModeJournal 改成了 ModeWAL。
|
|
70
67
|
```
|
|
71
68
|
````
|
|
72
69
|
|
|
73
|
-
|
|
70
|
+
Agent 按 `garden-ui` skill 会通过 `npx garden ui apply` 把这段想法替换成紧挨着完整代码的 `diff` 围栏(详见 [用 AI 写作](/garden/ai-writing))。页面渲染时会把这两段合成一个可切换 **「代码」** 和 **「对比上一版」** 的代码框,右上角的复制按钮始终复制新版完整代码:
|
|
74
71
|
|
|
75
72
|
```go
|
|
76
73
|
func openStore(path string) (*Store, error) {
|
|
@@ -85,8 +82,19 @@ func openStore(path string) (*Store, error) {
|
|
|
85
82
|
}
|
|
86
83
|
```
|
|
87
84
|
|
|
85
|
+
**手写格式**:如果不使用 Agent,也可以直接在完整代码块正下方手写一段 `diff` 代码块(两段之间只隔空行):
|
|
86
|
+
|
|
87
|
+
````markdown
|
|
88
|
+
```diff
|
|
89
|
+
func openStore(path string) (*Store, error) {
|
|
90
|
+
- return openWithMode(path, ModeJournal)
|
|
91
|
+
+ return openWithMode(path, ModeWAL)
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
````
|
|
95
|
+
|
|
88
96
|
> [!IMPORTANT]
|
|
89
|
-
> `diff`
|
|
97
|
+
> `diff` 代码块必须紧跟在一段完整代码块后面;如果单独写一段 `diff` 而前面没有完整代码,`npx garden check` 会报 `diff-orphan` 错误。
|
|
90
98
|
|
|
91
99
|
## 3. 中文加粗、荧光笔高亮与提示框
|
|
92
100
|
|
package/package.json
CHANGED
|
@@ -716,9 +716,20 @@ const columns = readColumnReferences(posts).sort(
|
|
|
716
716
|
const thoughts = readMarkdownTree(path.join(contentRoot, "thoughts"), "thought").sort(
|
|
717
717
|
(a, b) => Date.parse(b.date) - Date.parse(a.date),
|
|
718
718
|
);
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
719
|
+
// docs/handbook/en/** mirrors the Chinese handbook page for page: the same
|
|
720
|
+
// relative path is the counterpart. Each entry carries its lang; English paths
|
|
721
|
+
// drop the en/ prefix and are served under /garden/en/.
|
|
722
|
+
const allHandbookDocs = readHandbookDocs().map((doc) =>
|
|
723
|
+
doc.path === "en" || doc.path.startsWith("en/")
|
|
724
|
+
? { ...doc, lang: "en", path: doc.path === "en" ? "index" : doc.path.slice(3) }
|
|
725
|
+
: { ...doc, lang: "zh" },
|
|
726
|
+
);
|
|
727
|
+
const zhHandbookDocs = allHandbookDocs.filter((doc) => doc.lang === "zh");
|
|
728
|
+
const enHandbookDocs = allHandbookDocs.filter((doc) => doc.lang === "en");
|
|
729
|
+
const gardenIndexDoc = zhHandbookDocs.find((doc) => doc.path === "index") ?? null;
|
|
730
|
+
const gardenDocs = zhHandbookDocs.filter((doc) => doc.path !== "index");
|
|
731
|
+
const gardenIndexDocEn = enHandbookDocs.find((doc) => doc.path === "index") ?? null;
|
|
732
|
+
const gardenDocsEn = enHandbookDocs.filter((doc) => doc.path !== "index");
|
|
722
733
|
|
|
723
734
|
// Intrinsic image sizes for width/height on <img>: local files are read now,
|
|
724
735
|
// remote ones come from image-sizes.json (written by scripts/image-sizes.mjs).
|
|
@@ -762,7 +773,12 @@ assertUnique(
|
|
|
762
773
|
"专栏文章顺序",
|
|
763
774
|
);
|
|
764
775
|
assertUnique(thoughts, (item) => item.slug, "随想 slug");
|
|
776
|
+
// /thoughts/page/<n>/ holds the static pages of the feed.
|
|
777
|
+
for (const thought of thoughts) {
|
|
778
|
+
if (thought.slug === "page") fail(thought.sourcePath ?? "content/thoughts/page.md", "随想不能叫 page:/thoughts/page/ 留给分页");
|
|
779
|
+
}
|
|
765
780
|
assertUnique(gardenDocs, (item) => item.path, "Garden 文档路径");
|
|
781
|
+
assertUnique(gardenDocsEn, (item) => item.path, "Garden English handbook path");
|
|
766
782
|
|
|
767
783
|
const parityDir = path.join(root, "parity", "articles");
|
|
768
784
|
const a2WordCounts = fs.existsSync(parityDir)
|
|
@@ -857,6 +873,8 @@ export const columns = ${JSON.stringify(columns, null, 2)} as ContentEntry[];
|
|
|
857
873
|
export const thoughts = ${JSON.stringify(thoughts, null, 2)} as ContentEntry[];
|
|
858
874
|
export const gardenDocs = ${JSON.stringify(gardenDocs, null, 2)} as GardenDocEntry[];
|
|
859
875
|
export const gardenIndexDoc = ${JSON.stringify(gardenIndexDoc, null, 2)} as GardenDocEntry | null;
|
|
876
|
+
export const gardenDocsEn = ${JSON.stringify(gardenDocsEn, null, 2)} as GardenDocEntry[];
|
|
877
|
+
export const gardenIndexDocEn = ${JSON.stringify(gardenIndexDocEn, null, 2)} as GardenDocEntry | null;
|
|
860
878
|
export const a2WordCounts = ${JSON.stringify(a2WordCounts, null, 2)} as Record<string, number>;
|
|
861
879
|
export const linkAssets = ${JSON.stringify(linkAssetsForRender(root, basePath), null, 2)} as LinkAssets;
|
|
862
880
|
export const imageSizes: Record<string, { width: number; height: number }> = ${JSON.stringify(imageSizes)};
|
|
@@ -949,6 +967,8 @@ writeRssFeed(
|
|
|
949
967
|
const sitemapEntries = [
|
|
950
968
|
"/", "/blog/", "/thoughts/", "/columns/", "/about/", "/garden/",
|
|
951
969
|
...gardenDocs.map((entry) => `/garden/${entry.path}/`),
|
|
970
|
+
...(gardenIndexDocEn ? ["/garden/en/"] : []),
|
|
971
|
+
...gardenDocsEn.map((entry) => `/garden/en/${entry.path}/`),
|
|
952
972
|
...posts.map((entry) => `/blog/${entry.path}/`),
|
|
953
973
|
...columns.map((entry) => `/columns/${entry.column}/${columnEntryPath(entry)}/`),
|
|
954
974
|
...thoughts.map((entry) => `/thoughts/${entry.slug}/`),
|
|
@@ -1055,6 +1055,39 @@ export function checkFileFacts(filePath, content, facts, root = ROOT) {
|
|
|
1055
1055
|
return issues;
|
|
1056
1056
|
}
|
|
1057
1057
|
|
|
1058
|
+
/**
|
|
1059
|
+
* English pages mirror the Chinese ones: docs/handbook/en/<path>.md must have
|
|
1060
|
+
* docs/handbook/<path>.md with the same section and order. Chinese pages without
|
|
1061
|
+
* an English twin are fine (not translated yet).
|
|
1062
|
+
*/
|
|
1063
|
+
export function checkHandbookPairs(root = ROOT) {
|
|
1064
|
+
const handbook = path.join(root, "docs/handbook");
|
|
1065
|
+
const enRoot = path.join(handbook, "en");
|
|
1066
|
+
const issues = [];
|
|
1067
|
+
const head = (text) => {
|
|
1068
|
+
const m = text.match(/^---\n([\s\S]*?)\n---/u);
|
|
1069
|
+
const field = (key) => m?.[1].match(new RegExp(`^${key}:\\s*(.+)$`, "mu"))?.[1].trim();
|
|
1070
|
+
return { section: field("section"), order: field("order") };
|
|
1071
|
+
};
|
|
1072
|
+
for (const file of walkFiles(enRoot, (f) => f.endsWith(".md"))) {
|
|
1073
|
+
const rel = path.relative(enRoot, file);
|
|
1074
|
+
const zhFile = path.join(handbook, rel);
|
|
1075
|
+
const relEn = path.relative(root, file).replace(/\\/g, "/");
|
|
1076
|
+
if (!fs.existsSync(zhFile)) {
|
|
1077
|
+
issues.push({ file: relEn, line: 1, kind: "handbook-pair", message: `no Chinese counterpart docs/handbook/${rel.replace(/\\/g, "/")}` });
|
|
1078
|
+
continue;
|
|
1079
|
+
}
|
|
1080
|
+
const en = head(fs.readFileSync(file, "utf8"));
|
|
1081
|
+
const zh = head(fs.readFileSync(zhFile, "utf8"));
|
|
1082
|
+
for (const key of ["section", "order"]) {
|
|
1083
|
+
if (en[key] !== zh[key]) {
|
|
1084
|
+
issues.push({ file: relEn, line: 1, kind: "handbook-pair", message: `${key} is ${en[key] ?? "missing"}, the Chinese page has ${zh[key] ?? "missing"}` });
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
return issues;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1058
1091
|
/**
|
|
1059
1092
|
* Scan all docs/handbook/ and app/garden/ files and return any fact-check failures.
|
|
1060
1093
|
*/
|
|
@@ -1069,6 +1102,7 @@ export function checkAllHandbookFacts(root = ROOT) {
|
|
|
1069
1102
|
const content = fs.readFileSync(file, "utf8");
|
|
1070
1103
|
issues.push(...checkFileFacts(file, content, facts, root));
|
|
1071
1104
|
}
|
|
1105
|
+
issues.push(...checkHandbookPairs(root));
|
|
1072
1106
|
const svgFiles = walkFiles(path.join(root, "docs/handbook"), (f) => f.endsWith(".svg"));
|
|
1073
1107
|
for (const svgFile of svgFiles) {
|
|
1074
1108
|
const svgContent = fs.readFileSync(svgFile, "utf8");
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/** Static export: English handbook pages get <html lang="en"> (ui/garden/html-lang.mjs). */
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
import { handbookHtmlLang, withHtmlLang } from "../ui/garden/html-lang.mjs";
|
|
7
|
+
|
|
8
|
+
/** Rewrites every .html under root whose path is an English handbook page; returns the relative paths. */
|
|
9
|
+
export function setHandbookHtmlLang(root) {
|
|
10
|
+
const changed = [];
|
|
11
|
+
const visit = (directory) => {
|
|
12
|
+
if (!fs.existsSync(directory)) return;
|
|
13
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
14
|
+
const absolute = path.join(directory, entry.name);
|
|
15
|
+
if (entry.isDirectory()) visit(absolute);
|
|
16
|
+
else if (entry.name.endsWith(".html")) {
|
|
17
|
+
const relative = path.relative(root, absolute).split(path.sep).join("/");
|
|
18
|
+
const lang = handbookHtmlLang(relative);
|
|
19
|
+
if (!lang || relative.includes("/media/") || relative.startsWith("media/")) continue;
|
|
20
|
+
const html = fs.readFileSync(absolute, "utf8");
|
|
21
|
+
const next = withHtmlLang(html, lang);
|
|
22
|
+
if (next !== html) {
|
|
23
|
+
fs.writeFileSync(absolute, next);
|
|
24
|
+
changed.push(relative);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
visit(root);
|
|
30
|
+
return changed.sort();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
34
|
+
const siteRoot = process.env.GARDEN_SITE ? path.resolve(process.env.GARDEN_SITE) : process.cwd();
|
|
35
|
+
setHandbookHtmlLang(path.join(siteRoot, "dist", "client"));
|
|
36
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Static export with a basePath: vinext writes 404.html at dist/client/404.html.
|
|
4
|
+
* Copy it into the basePath folder too, so a host that serves only that folder
|
|
5
|
+
* (or Paths under /basePath/…) still has a 404 page next to the site.
|
|
6
|
+
*/
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { pathToFileURL } from "node:url";
|
|
10
|
+
|
|
11
|
+
export function copyNotFoundIntoBasePath(clientRoot, basePath = "") {
|
|
12
|
+
const base = String(basePath ?? "").replace(/\/+$/u, "").replace(/^\/+/u, "");
|
|
13
|
+
if (!base) return null;
|
|
14
|
+
const source = path.join(clientRoot, "404.html");
|
|
15
|
+
const destination = path.join(clientRoot, base, "404.html");
|
|
16
|
+
if (!fs.existsSync(source) || !fs.existsSync(path.dirname(destination))) return null;
|
|
17
|
+
fs.copyFileSync(source, destination);
|
|
18
|
+
return path.relative(clientRoot, destination).split(path.sep).join("/");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
22
|
+
const siteRoot = process.env.GARDEN_SITE ? path.resolve(process.env.GARDEN_SITE) : process.cwd();
|
|
23
|
+
const basePath = (process.env.NEXT_PUBLIC_BASE_PATH ?? "").replace(/\/+$/u, "");
|
|
24
|
+
copyNotFoundIntoBasePath(path.join(siteRoot, "dist", "client"), basePath);
|
|
25
|
+
}
|