@zeropress/build-pages 0.5.1 → 0.5.2
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/README.md +156 -27
- package/action.yml +1 -1
- package/dist/action.js +2 -2
- package/package.json +3 -2
- package/schemas/zeropress-build-pages.config.v0.1.schema.json +2 -62
- package/src/action.js +1 -1
- package/src/index.js +15 -13
- package/src/prebuild.js +338 -53
package/README.md
CHANGED
|
@@ -2,53 +2,149 @@
|
|
|
2
2
|
|
|
3
3
|
Build ZeroPress static output for modern hosting platforms.
|
|
4
4
|
|
|
5
|
-
`@zeropress/build-pages` turns a directory of Markdown files and public assets into static ZeroPress
|
|
5
|
+
`@zeropress/build-pages` turns a directory of Markdown files and public assets into a static ZeroPress site. It discovers Markdown pages, prepares the site data, stages public files, and runs `@zeropress/build`.
|
|
6
6
|
|
|
7
7
|
The generated output is plain static files that can be deployed to GitHub Pages, Cloudflare Pages, Netlify, Vercel, or any static hosting provider.
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## Build Flow
|
|
10
|
+
|
|
11
|
+
```txt
|
|
12
|
+
source directory
|
|
13
|
+
Markdown pages + .zeropress/config.json + public files
|
|
14
|
+
|
|
|
15
|
+
v
|
|
16
|
+
@zeropress/build-pages
|
|
17
|
+
generates .zeropress/preview-data.json
|
|
18
|
+
stages public files
|
|
19
|
+
|
|
|
20
|
+
v
|
|
21
|
+
@zeropress/build + ZeroPress theme
|
|
22
|
+
|
|
|
23
|
+
v
|
|
24
|
+
static output directory
|
|
25
|
+
HTML pages + assets + copied public files
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```mermaid
|
|
29
|
+
flowchart TD
|
|
30
|
+
source["Source directory"] --> markdown["Markdown pages (*.md)"]
|
|
31
|
+
source --> config[".zeropress/config.json"]
|
|
32
|
+
source --> publicFiles["Public files<br/>images, CSS, JS, PDF, JSON, Markdown"]
|
|
33
|
+
|
|
34
|
+
markdown --> buildPages["@zeropress/build-pages"]
|
|
35
|
+
config --> buildPages
|
|
36
|
+
publicFiles --> buildPages
|
|
37
|
+
|
|
38
|
+
buildPages --> previewData[".zeropress/preview-data.json<br/>internal generated build input"]
|
|
39
|
+
buildPages --> stagedPublic["Staged public files"]
|
|
40
|
+
|
|
41
|
+
previewData --> build["@zeropress/build"]
|
|
42
|
+
stagedPublic --> build
|
|
43
|
+
theme["ZeroPress theme"] --> build
|
|
44
|
+
|
|
45
|
+
build --> output["Static output directory"]
|
|
46
|
+
output --> html["HTML pages"]
|
|
47
|
+
output --> assets["Theme assets"]
|
|
48
|
+
output --> copied["Copied public files"]
|
|
49
|
+
output --> special["sitemap.xml / robots.txt / feed.xml"]
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
### GitHub Action
|
|
55
|
+
|
|
56
|
+
A basic Pages deployment workflow with the `zeropress-build-pages` action looks like this.
|
|
10
57
|
|
|
11
58
|
```yaml
|
|
59
|
+
name: Build and Deploy Docs to GitHub Pages
|
|
60
|
+
on:
|
|
61
|
+
push:
|
|
62
|
+
branches: ["main"]
|
|
63
|
+
workflow_dispatch:
|
|
64
|
+
permissions:
|
|
65
|
+
contents: read
|
|
66
|
+
pages: write
|
|
67
|
+
id-token: write
|
|
68
|
+
concurrency:
|
|
69
|
+
group: "pages"
|
|
70
|
+
cancel-in-progress: false
|
|
12
71
|
jobs:
|
|
13
72
|
build:
|
|
14
73
|
runs-on: ubuntu-latest
|
|
15
74
|
steps:
|
|
16
|
-
-
|
|
17
|
-
|
|
75
|
+
- name: Checkout
|
|
76
|
+
uses: actions/checkout@v6
|
|
77
|
+
- name: Setup Pages
|
|
78
|
+
uses: actions/configure-pages@v6
|
|
18
79
|
- name: Build ZeroPress Pages
|
|
19
80
|
uses: zeropress-app/zeropress-build-pages@v0
|
|
20
81
|
with:
|
|
21
|
-
source: ./
|
|
82
|
+
source: ./documents
|
|
22
83
|
destination: ./_site
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
84
|
+
- name: Upload artifact
|
|
85
|
+
uses: actions/upload-pages-artifact@v5
|
|
86
|
+
deploy:
|
|
87
|
+
runs-on: ubuntu-latest
|
|
88
|
+
needs: build
|
|
89
|
+
environment:
|
|
90
|
+
name: github-pages
|
|
91
|
+
url: ${{ steps.deployment.outputs.page_url }}
|
|
92
|
+
steps:
|
|
93
|
+
- name: Deploy to GitHub Pages
|
|
94
|
+
id: deployment
|
|
95
|
+
uses: actions/deploy-pages@v5
|
|
27
96
|
```
|
|
28
97
|
|
|
29
|
-
The action builds the static files only. Uploading and deploying are handled by your hosting provider's deployment action or CLI.
|
|
98
|
+
The action `zeropress-build-pages` builds the static files only. Uploading and deploying are handled by your hosting provider's deployment action or CLI.
|
|
99
|
+
|
|
100
|
+
In the action inputs:
|
|
101
|
+
|
|
102
|
+
- `source` is the directory that contains your Markdown pages, public files, and optional `.zeropress/config.json`. The default is `./docs`.
|
|
103
|
+
- `destination` is the directory where the generated static site is written. The default is `./_site`.
|
|
30
104
|
|
|
31
105
|
For GitHub Pages, the generated `destination` directory can be passed to `actions/upload-pages-artifact`. For Cloudflare Pages, Netlify, Vercel, or another static host, pass the same `destination` directory to that provider's deploy step.
|
|
32
106
|
|
|
33
|
-
|
|
107
|
+
### npx
|
|
108
|
+
|
|
109
|
+
Use `npx` when you want to run Build Pages without adding it to your project dependencies.
|
|
34
110
|
|
|
35
111
|
```bash
|
|
36
|
-
npx @zeropress/build-pages --source ./
|
|
112
|
+
npx @zeropress/build-pages --source ./documents --destination ./_site
|
|
37
113
|
```
|
|
38
114
|
|
|
39
|
-
|
|
115
|
+
### package.json script
|
|
116
|
+
|
|
117
|
+
Use a package script when your project already has a Node.js toolchain.
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
npm install --save-dev @zeropress/build-pages
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
```json
|
|
124
|
+
{
|
|
125
|
+
"scripts": {
|
|
126
|
+
"build": "zeropress-build-pages --source ./documents --destination ./_site"
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
npm run build
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## CLI Options
|
|
136
|
+
|
|
137
|
+
The CLI requires explicit input and output paths. The GitHub Action keeps safe defaults for workflow convenience.
|
|
40
138
|
|
|
41
139
|
| Option | Default | Purpose |
|
|
42
140
|
| --- | --- | --- |
|
|
43
|
-
| `--source <dir>` |
|
|
44
|
-
| `--destination <dir>` |
|
|
45
|
-
| `--out <dir>` | `_site` | Alias for `--destination` |
|
|
141
|
+
| `--source <dir>` | required | Source directory containing Markdown and public files |
|
|
142
|
+
| `--destination <dir>` | required | Output directory |
|
|
46
143
|
| `--theme docs` | `docs` | Bundled docs theme |
|
|
47
144
|
| `--theme-path <dir>` | none | Custom ZeroPress theme directory |
|
|
48
145
|
| `--config <path>` | `<source>/.zeropress/config.json` | Build Pages config |
|
|
49
146
|
| `--site-url <url>` | config `site.url` | Canonical URL override |
|
|
50
|
-
| `--skip-untitled-markdown` | `false` | Skip Markdown without
|
|
51
|
-
| `--check-links` | `true` | Warn about broken internal links |
|
|
147
|
+
| `--skip-untitled-markdown` | `false` | Skip Markdown without a page title |
|
|
52
148
|
| `--no-check-links` | false | Skip link checking |
|
|
53
149
|
|
|
54
150
|
Equivalent environment variables:
|
|
@@ -66,7 +162,7 @@ CLI options take precedence over environment variables.
|
|
|
66
162
|
|
|
67
163
|
## Source Tree
|
|
68
164
|
|
|
69
|
-
The source directory is both the Markdown source root and the public passthrough root.
|
|
165
|
+
The source directory is both the Markdown source root and the public passthrough root. GitHub Action usage defaults to `./docs`; CLI usage requires `--source` or `ZEROPRESS_PUBLIC_DIR`.
|
|
70
166
|
|
|
71
167
|
```txt
|
|
72
168
|
docs/
|
|
@@ -77,7 +173,7 @@ docs/
|
|
|
77
173
|
config.json
|
|
78
174
|
```
|
|
79
175
|
|
|
80
|
-
Build Pages stages the source tree before calling `@zeropress/build`, so `--source ./` and `--destination ./_site` are supported. Generated ZeroPress output wins over staged public files.
|
|
176
|
+
Build Pages stages the source tree before calling `@zeropress/build`, so `--source ./` and `--destination ./_site` are supported when you intentionally want to build from the repository root. Generated ZeroPress output wins over staged public files.
|
|
81
177
|
|
|
82
178
|
Ignored while staging and Markdown discovery:
|
|
83
179
|
|
|
@@ -95,24 +191,54 @@ Additional Markdown discovery ignores:
|
|
|
95
191
|
- path segments ending with `~`
|
|
96
192
|
- `vendor`
|
|
97
193
|
|
|
98
|
-
## Markdown
|
|
194
|
+
## Markdown Pages
|
|
99
195
|
|
|
100
196
|
- `*.md` files are discovered recursively.
|
|
101
|
-
- Each Markdown page needs an ATX H1 (`# Title`)
|
|
102
|
-
-
|
|
197
|
+
- Each Markdown page needs a page title. Build Pages uses front matter `title`, then an ATX H1 (`# Title`), then a Setext H1 (`Title` + `====`).
|
|
198
|
+
- If no title can be found, the build fails unless `--skip-untitled-markdown` is used.
|
|
199
|
+
- `--skip-untitled-markdown` skips those Markdown files. It does not create untitled pages.
|
|
103
200
|
- Root `index.md` becomes the front page when no config is present.
|
|
104
201
|
- Nested `index.md` maps to a directory route, such as `cli/index.md` -> `/cli/`.
|
|
105
202
|
- Other Markdown files map to extensionless routes, such as `cli/tool.md` -> `/cli/tool`.
|
|
106
203
|
- Markdown links to other discovered `.md` files are rewritten to generated public URLs.
|
|
107
204
|
- Original Markdown files remain available as public passthrough files.
|
|
108
205
|
|
|
206
|
+
Optional YAML front matter is supported at the top of Markdown files:
|
|
207
|
+
|
|
208
|
+
```md
|
|
209
|
+
---
|
|
210
|
+
title: Install ZeroPress
|
|
211
|
+
description: Build a static docs site from Markdown.
|
|
212
|
+
path: guides/install
|
|
213
|
+
status: published
|
|
214
|
+
meta:
|
|
215
|
+
source: docs
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
Body content...
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
All supported front matter fields are optional. When `status` is omitted, the page is treated as `published`.
|
|
222
|
+
|
|
223
|
+
Supported front matter fields:
|
|
224
|
+
|
|
225
|
+
| Field | Purpose |
|
|
226
|
+
| --- | --- |
|
|
227
|
+
| `title` | Page title. Takes priority over Markdown H1. |
|
|
228
|
+
| `description` | Page excerpt and description. |
|
|
229
|
+
| `path` | Generated route path, such as `guides/install` for `/guides/install`. |
|
|
230
|
+
| `status` | `published` includes the page. `draft` skips the page. Other values warn and skip. |
|
|
231
|
+
| `meta` | Optional scalar/null metadata copied to the generated page. |
|
|
232
|
+
|
|
233
|
+
Unknown front matter fields are ignored to make migration from existing Markdown sites easier.
|
|
234
|
+
|
|
109
235
|
## Config
|
|
110
236
|
|
|
111
237
|
Build Pages reads `<source>/.zeropress/config.json` when present. Missing config falls back to defaults.
|
|
112
238
|
|
|
113
239
|
```json
|
|
114
240
|
{
|
|
115
|
-
"$schema": "
|
|
241
|
+
"$schema": "https://zeropress.dev/schemas/zeropress-build-pages.config.v0.1.schema.json",
|
|
116
242
|
"version": "0.1",
|
|
117
243
|
"site": {
|
|
118
244
|
"title": "My Docs",
|
|
@@ -168,12 +294,15 @@ Build Pages writes:
|
|
|
168
294
|
|
|
169
295
|
```txt
|
|
170
296
|
.zeropress/
|
|
297
|
+
build-pages-config.json
|
|
171
298
|
preview-data.json
|
|
172
|
-
|
|
173
|
-
|
|
299
|
+
build-report.json
|
|
300
|
+
public-assets/
|
|
174
301
|
```
|
|
175
302
|
|
|
176
|
-
`
|
|
303
|
+
`build-pages-config.json` is the resolved user-facing Build Pages config used for the current run. It combines `.zeropress/config.json`, defaults, and CLI/env overrides where applicable.
|
|
304
|
+
|
|
305
|
+
`preview-data.json` is an internal generated build input for the ZeroPress renderer. Most users do not need to edit or understand this file.
|
|
177
306
|
|
|
178
307
|
## Development
|
|
179
308
|
|
package/action.yml
CHANGED
|
@@ -8,7 +8,7 @@ inputs:
|
|
|
8
8
|
source:
|
|
9
9
|
description: Source directory containing Markdown files and optional .zeropress/config.json.
|
|
10
10
|
required: false
|
|
11
|
-
default: ./
|
|
11
|
+
default: ./docs
|
|
12
12
|
destination:
|
|
13
13
|
description: Output directory for the generated static site.
|
|
14
14
|
required: false
|
package/dist/action.js
CHANGED
|
@@ -62139,7 +62139,7 @@ var __dirname = path3.dirname(fileURLToPath(import.meta.url));
|
|
|
62139
62139
|
var packageDir = path3.resolve(__dirname, "..");
|
|
62140
62140
|
var prebuildScript = path3.join(packageDir, "src", "prebuild.js");
|
|
62141
62141
|
var PREVIEW_DATA_PATH = ".zeropress/preview-data.json";
|
|
62142
|
-
var STAGING_DIR = ".zeropress/
|
|
62142
|
+
var STAGING_DIR = ".zeropress/public-assets";
|
|
62143
62143
|
var DEFAULT_THEME = "docs";
|
|
62144
62144
|
async function runBuildPages(options2) {
|
|
62145
62145
|
const cwd = path3.resolve(options2.cwd || process.cwd());
|
|
@@ -62276,7 +62276,7 @@ function formatPath(cwd, targetPath) {
|
|
|
62276
62276
|
|
|
62277
62277
|
// src/action.js
|
|
62278
62278
|
var options = {
|
|
62279
|
-
source: input("source") || "./",
|
|
62279
|
+
source: input("source") || "./docs",
|
|
62280
62280
|
destination: input("destination") || "./_site",
|
|
62281
62281
|
theme: input("theme") || "docs",
|
|
62282
62282
|
themePath: input("theme-path"),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zeropress/build-pages",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "ZeroPress Markdown build action and CLI for static hosting",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -40,7 +40,8 @@
|
|
|
40
40
|
"node": ">=18.18.0"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@zeropress/build": "0.5.1"
|
|
43
|
+
"@zeropress/build": "0.5.1",
|
|
44
|
+
"gray-matter": "4.0.3"
|
|
44
45
|
},
|
|
45
46
|
"devDependencies": {
|
|
46
47
|
"esbuild": "0.28.0"
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
"site": {
|
|
36
36
|
"type": "object",
|
|
37
37
|
"additionalProperties": false,
|
|
38
|
-
"description": "
|
|
39
|
-
"markdownDescription": "
|
|
38
|
+
"description": "User-facing site metadata used by Build Pages.",
|
|
39
|
+
"markdownDescription": "User-facing site metadata used by Build Pages.",
|
|
40
40
|
"properties": {
|
|
41
41
|
"title": {
|
|
42
42
|
"type": "string",
|
|
@@ -50,37 +50,8 @@
|
|
|
50
50
|
"description": "Canonical site URL. Use an empty string or omit this field for local builds without canonical output.",
|
|
51
51
|
"markdownDescription": "Canonical site URL. Use an empty string or omit this field for local builds without canonical output."
|
|
52
52
|
},
|
|
53
|
-
"mediaBaseUrl": {
|
|
54
|
-
"type": "string"
|
|
55
|
-
},
|
|
56
|
-
"locale": {
|
|
57
|
-
"type": "string",
|
|
58
|
-
"minLength": 1
|
|
59
|
-
},
|
|
60
|
-
"postsPerPage": {
|
|
61
|
-
"type": "integer",
|
|
62
|
-
"minimum": 1
|
|
63
|
-
},
|
|
64
|
-
"dateFormat": {
|
|
65
|
-
"type": "string",
|
|
66
|
-
"minLength": 1
|
|
67
|
-
},
|
|
68
|
-
"timeFormat": {
|
|
69
|
-
"type": "string",
|
|
70
|
-
"minLength": 1
|
|
71
|
-
},
|
|
72
|
-
"timezone": {
|
|
73
|
-
"type": "string",
|
|
74
|
-
"minLength": 1
|
|
75
|
-
},
|
|
76
|
-
"permalinks": {
|
|
77
|
-
"$ref": "#/$defs/permalinks"
|
|
78
|
-
},
|
|
79
53
|
"footer": {
|
|
80
54
|
"$ref": "#/$defs/siteFooter"
|
|
81
|
-
},
|
|
82
|
-
"disallowComments": {
|
|
83
|
-
"type": "boolean"
|
|
84
55
|
}
|
|
85
56
|
}
|
|
86
57
|
},
|
|
@@ -114,37 +85,6 @@
|
|
|
114
85
|
}
|
|
115
86
|
}
|
|
116
87
|
},
|
|
117
|
-
"permalinks": {
|
|
118
|
-
"type": "object",
|
|
119
|
-
"additionalProperties": false,
|
|
120
|
-
"description": "Preview-data permalink defaults emitted by Build Pages.",
|
|
121
|
-
"markdownDescription": "Preview-data permalink defaults emitted by Build Pages.",
|
|
122
|
-
"properties": {
|
|
123
|
-
"output_style": {
|
|
124
|
-
"type": "string",
|
|
125
|
-
"enum": ["directory", "html-extension"]
|
|
126
|
-
},
|
|
127
|
-
"posts": {
|
|
128
|
-
"$ref": "#/$defs/permalinkPattern"
|
|
129
|
-
},
|
|
130
|
-
"pages": {
|
|
131
|
-
"$ref": "#/$defs/permalinkPattern"
|
|
132
|
-
},
|
|
133
|
-
"categories": {
|
|
134
|
-
"$ref": "#/$defs/permalinkPattern"
|
|
135
|
-
},
|
|
136
|
-
"tags": {
|
|
137
|
-
"$ref": "#/$defs/permalinkPattern"
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
},
|
|
141
|
-
"permalinkPattern": {
|
|
142
|
-
"type": "string",
|
|
143
|
-
"minLength": 1,
|
|
144
|
-
"pattern": "^/",
|
|
145
|
-
"description": "Absolute URL path pattern passed through to preview-data site.permalinks.",
|
|
146
|
-
"markdownDescription": "Absolute URL path pattern passed through to preview-data `site.permalinks`."
|
|
147
|
-
},
|
|
148
88
|
"frontPage": {
|
|
149
89
|
"type": "object",
|
|
150
90
|
"additionalProperties": false,
|
package/src/action.js
CHANGED
package/src/index.js
CHANGED
|
@@ -9,7 +9,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
9
9
|
const packageDir = path.resolve(__dirname, '..');
|
|
10
10
|
const prebuildScript = path.join(packageDir, 'src', 'prebuild.js');
|
|
11
11
|
const PREVIEW_DATA_PATH = '.zeropress/preview-data.json';
|
|
12
|
-
const STAGING_DIR = '.zeropress/
|
|
12
|
+
const STAGING_DIR = '.zeropress/public-assets';
|
|
13
13
|
const DEFAULT_THEME = 'docs';
|
|
14
14
|
|
|
15
15
|
export async function runCli(argv = process.argv.slice(2)) {
|
|
@@ -114,10 +114,6 @@ export function parseArgs(argv, env = process.env) {
|
|
|
114
114
|
flags.skipUntitledMarkdown = true;
|
|
115
115
|
continue;
|
|
116
116
|
}
|
|
117
|
-
if (arg === '--check-links') {
|
|
118
|
-
flags.checkLinks = true;
|
|
119
|
-
continue;
|
|
120
|
-
}
|
|
121
117
|
if (arg === '--no-check-links') {
|
|
122
118
|
flags.checkLinks = false;
|
|
123
119
|
continue;
|
|
@@ -126,7 +122,6 @@ export function parseArgs(argv, env = process.env) {
|
|
|
126
122
|
const valueOptions = new Set([
|
|
127
123
|
'--source',
|
|
128
124
|
'--destination',
|
|
129
|
-
'--out',
|
|
130
125
|
'--theme',
|
|
131
126
|
'--theme-path',
|
|
132
127
|
'--config',
|
|
@@ -145,9 +140,18 @@ export function parseArgs(argv, env = process.env) {
|
|
|
145
140
|
throw new Error(`Invalid arguments: unknown option ${arg}`);
|
|
146
141
|
}
|
|
147
142
|
|
|
143
|
+
const source = flags.source || env.ZEROPRESS_PUBLIC_DIR || '';
|
|
144
|
+
const destination = flags.destination || env.ZEROPRESS_OUT_DIR || '';
|
|
145
|
+
if (!source) {
|
|
146
|
+
throw new Error('Invalid arguments: --source <dir> is required. You can also set ZEROPRESS_PUBLIC_DIR.');
|
|
147
|
+
}
|
|
148
|
+
if (!destination) {
|
|
149
|
+
throw new Error('Invalid arguments: --destination <dir> is required. You can also set ZEROPRESS_OUT_DIR.');
|
|
150
|
+
}
|
|
151
|
+
|
|
148
152
|
return {
|
|
149
|
-
source
|
|
150
|
-
destination
|
|
153
|
+
source,
|
|
154
|
+
destination,
|
|
151
155
|
theme: flags.theme || DEFAULT_THEME,
|
|
152
156
|
themePath: flags['theme-path'] || env.ZEROPRESS_THEME_DIR || '',
|
|
153
157
|
config: flags.config || env.ZEROPRESS_BUILD_PAGES_CONFIG || '',
|
|
@@ -164,15 +168,13 @@ Usage:
|
|
|
164
168
|
zeropress-build-pages [options]
|
|
165
169
|
|
|
166
170
|
Options:
|
|
167
|
-
--source <dir> Source directory (
|
|
168
|
-
--destination <dir> Output directory (
|
|
169
|
-
--out <dir> Alias for --destination
|
|
171
|
+
--source <dir> Source directory (required, or ZEROPRESS_PUBLIC_DIR)
|
|
172
|
+
--destination <dir> Output directory (required, or ZEROPRESS_OUT_DIR)
|
|
170
173
|
--theme docs Bundled theme name (default: docs)
|
|
171
174
|
--theme-path <dir> Custom ZeroPress theme directory
|
|
172
175
|
--config <path> Config file (default: <source>/.zeropress/config.json)
|
|
173
176
|
--site-url <url> Canonical site URL override
|
|
174
|
-
--skip-untitled-markdown Skip Markdown files without
|
|
175
|
-
--check-links Warn about broken internal links (default)
|
|
177
|
+
--skip-untitled-markdown Skip Markdown files without a page title
|
|
176
178
|
--no-check-links Skip internal link checking
|
|
177
179
|
--help, -h Show help
|
|
178
180
|
--version, -v Show version`);
|
package/src/prebuild.js
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import matter from 'gray-matter';
|
|
4
5
|
|
|
5
6
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
7
|
const rootDir = process.cwd();
|
|
7
|
-
const sourceDir = resolveEnvPath(['ZEROPRESS_BUILD_PAGES_SOURCE', 'ZEROPRESS_PUBLIC_DIR'], '
|
|
8
|
+
const sourceDir = resolveEnvPath(['ZEROPRESS_BUILD_PAGES_SOURCE', 'ZEROPRESS_PUBLIC_DIR'], 'docs');
|
|
8
9
|
const defaultConfigPath = path.join(sourceDir, '.zeropress', 'config.json');
|
|
9
10
|
const configPath = resolveOptionalEnvPath(['ZEROPRESS_BUILD_PAGES_CONFIG'], defaultConfigPath);
|
|
10
11
|
const outDir = path.join(rootDir, '.zeropress');
|
|
12
|
+
const buildPagesConfigPath = path.join(outDir, 'build-pages-config.json');
|
|
11
13
|
const previewDataPath = path.join(outDir, 'preview-data.json');
|
|
12
|
-
const
|
|
14
|
+
const buildReportPath = path.join(outDir, 'build-report.json');
|
|
13
15
|
const skipUntitledMarkdown = readBooleanEnv('ZEROPRESS_SKIP_UNTITLED_MARKDOWN');
|
|
14
16
|
const FRONT_PAGE_TYPES = new Set(['theme_index', 'markdown', 'html']);
|
|
17
|
+
const BUILD_PAGES_CONFIG_SCHEMA_URL = 'https://zeropress.dev/schemas/zeropress-build-pages.config.v0.1.schema.json';
|
|
18
|
+
const PREVIEW_DATA_SCHEMA_URL = 'https://zeropress.dev/schemas/preview-data.v0.5.schema.json';
|
|
15
19
|
|
|
16
20
|
class PrebuildMarkdownError extends Error {
|
|
17
21
|
constructor(sourcePath, reason, expected = '', code = 'invalid_markdown') {
|
|
@@ -41,23 +45,43 @@ async function main() {
|
|
|
41
45
|
normalizeFrontPageConfig(config.front_page),
|
|
42
46
|
config.front_page,
|
|
43
47
|
);
|
|
48
|
+
const menus = normalizeMenus(config.menus);
|
|
49
|
+
const customHtmlConfig = normalizeCustomHtmlConfig(config.custom_html);
|
|
50
|
+
const resolvedConfig = buildResolvedConfig(config, {
|
|
51
|
+
frontPageConfig,
|
|
52
|
+
menus,
|
|
53
|
+
customHtmlConfig,
|
|
54
|
+
});
|
|
44
55
|
const sourceFiles = await listMarkdownFiles(sourceDir);
|
|
45
56
|
const skippedMarkdown = [];
|
|
46
57
|
const pageInputs = [];
|
|
47
58
|
|
|
48
59
|
for (const sourcePath of sourceFiles) {
|
|
49
60
|
const rawMarkdown = await fs.readFile(sourcePath, 'utf8');
|
|
50
|
-
const
|
|
61
|
+
const parsedMarkdown = parseMarkdownSource(rawMarkdown, sourcePath);
|
|
62
|
+
const frontMatterStatus = readFrontMatterStatus(parsedMarkdown.frontMatter.status, sourcePath);
|
|
63
|
+
if (frontMatterStatus !== 'published') {
|
|
64
|
+
recordSkippedMarkdown(skippedMarkdown, sourcePath, frontMatterStatus.reason);
|
|
65
|
+
if (frontMatterStatus.warning) {
|
|
66
|
+
console.warn(formatSkippedMarkdownWarning(sourcePath, frontMatterStatus.reason, frontMatterStatus.expected));
|
|
67
|
+
}
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const frontMatter = normalizePublishedFrontMatter(parsedMarkdown.frontMatter, sourcePath);
|
|
72
|
+
const title = extractTitleOrSkip(parsedMarkdown.bodyMarkdown, sourcePath, skippedMarkdown, frontMatter.title);
|
|
51
73
|
if (!title) {
|
|
52
74
|
continue;
|
|
53
75
|
}
|
|
54
76
|
|
|
55
77
|
pageInputs.push({
|
|
56
78
|
sourcePath,
|
|
57
|
-
|
|
79
|
+
bodyMarkdown: parsedMarkdown.bodyMarkdown,
|
|
80
|
+
frontMatter,
|
|
58
81
|
title,
|
|
59
82
|
route: buildPageRoute(sourcePath, {
|
|
60
83
|
allowRootIndex: shouldAllowRootMarkdownIndex(frontPageConfig),
|
|
84
|
+
routePath: frontMatter.path,
|
|
61
85
|
}),
|
|
62
86
|
});
|
|
63
87
|
}
|
|
@@ -66,29 +90,30 @@ async function main() {
|
|
|
66
90
|
pageInputs.map(({ sourcePath, route }) => [sourcePath, route]),
|
|
67
91
|
);
|
|
68
92
|
|
|
69
|
-
const pages = pageInputs.map(({ sourcePath,
|
|
93
|
+
const pages = pageInputs.map(({ sourcePath, bodyMarkdown, frontMatter, title, route }) => ({
|
|
70
94
|
title,
|
|
71
95
|
slug: route.slug,
|
|
72
96
|
path: route.path,
|
|
73
97
|
meta: {
|
|
98
|
+
...frontMatter.meta,
|
|
74
99
|
source_markdown_url: buildSourceMarkdownUrl(sourcePath),
|
|
75
100
|
},
|
|
76
|
-
content: rewriteMarkdownLinks(
|
|
101
|
+
content: rewriteMarkdownLinks(bodyMarkdown, sourcePath, routeBySourcePath),
|
|
77
102
|
document_type: 'markdown',
|
|
78
|
-
excerpt: extractExcerpt(
|
|
103
|
+
excerpt: frontMatter.description || extractExcerpt(bodyMarkdown, title),
|
|
79
104
|
status: 'published',
|
|
80
105
|
}));
|
|
81
106
|
|
|
82
|
-
const frontPageResult = await buildFrontPageData(frontPageConfig, pageInputs,
|
|
107
|
+
const frontPageResult = await buildFrontPageData(frontPageConfig, pageInputs, resolvedConfig);
|
|
83
108
|
if (frontPageResult.page) {
|
|
84
109
|
pages.push(frontPageResult.page);
|
|
85
110
|
}
|
|
86
111
|
|
|
87
|
-
const site = buildSiteData(
|
|
88
|
-
const
|
|
89
|
-
const customHtml = await buildCustomHtmlData(config.custom_html);
|
|
112
|
+
const site = buildSiteData(resolvedConfig, frontPageResult.frontPage);
|
|
113
|
+
const customHtml = await buildCustomHtmlData(customHtmlConfig);
|
|
90
114
|
|
|
91
115
|
const previewData = {
|
|
116
|
+
$schema: PREVIEW_DATA_SCHEMA_URL,
|
|
92
117
|
version: '0.5',
|
|
93
118
|
generator: 'zeropress-build-pages',
|
|
94
119
|
generated_at: new Date().toISOString(),
|
|
@@ -108,6 +133,7 @@ async function main() {
|
|
|
108
133
|
}
|
|
109
134
|
|
|
110
135
|
await fs.mkdir(outDir, { recursive: true });
|
|
136
|
+
await fs.writeFile(buildPagesConfigPath, `${JSON.stringify(resolvedConfig, null, 2)}\n`, 'utf8');
|
|
111
137
|
await fs.writeFile(previewDataPath, `${JSON.stringify(previewData, null, 2)}\n`, 'utf8');
|
|
112
138
|
|
|
113
139
|
const report = buildPrebuildReport({
|
|
@@ -119,7 +145,7 @@ async function main() {
|
|
|
119
145
|
frontPage: frontPageResult.frontPage,
|
|
120
146
|
customHtml,
|
|
121
147
|
});
|
|
122
|
-
await fs.writeFile(
|
|
148
|
+
await fs.writeFile(buildReportPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
|
|
123
149
|
|
|
124
150
|
console.log(`Wrote ${path.relative(rootDir, previewDataPath)} with ${pages.length} pages`);
|
|
125
151
|
printPrebuildSummary(report);
|
|
@@ -189,27 +215,66 @@ async function loadPrebuildConfig() {
|
|
|
189
215
|
}
|
|
190
216
|
|
|
191
217
|
function buildSiteData(config, frontPage) {
|
|
192
|
-
const configuredSite = isPlainObject(config.site) ? config.site :
|
|
193
|
-
const footer = normalizeFooter(configuredSite.footer);
|
|
218
|
+
const configuredSite = isPlainObject(config.site) ? config.site : normalizeSiteConfig(undefined);
|
|
194
219
|
|
|
195
220
|
const site = {
|
|
196
|
-
title:
|
|
197
|
-
description:
|
|
198
|
-
url:
|
|
199
|
-
mediaBaseUrl:
|
|
200
|
-
locale:
|
|
201
|
-
postsPerPage:
|
|
202
|
-
dateFormat:
|
|
203
|
-
timeFormat:
|
|
204
|
-
timezone:
|
|
205
|
-
permalinks:
|
|
221
|
+
title: configuredSite.title,
|
|
222
|
+
description: configuredSite.description,
|
|
223
|
+
url: configuredSite.url,
|
|
224
|
+
mediaBaseUrl: '',
|
|
225
|
+
locale: 'en-US',
|
|
226
|
+
postsPerPage: 10,
|
|
227
|
+
dateFormat: 'YYYY-MM-DD',
|
|
228
|
+
timeFormat: 'HH:mm',
|
|
229
|
+
timezone: 'UTC',
|
|
230
|
+
permalinks: defaultPermalinks(),
|
|
206
231
|
front_page: frontPage,
|
|
207
232
|
post_index: {
|
|
208
233
|
enabled: false,
|
|
209
234
|
},
|
|
210
|
-
disallowComments:
|
|
235
|
+
disallowComments: true,
|
|
211
236
|
};
|
|
212
237
|
|
|
238
|
+
if (configuredSite.footer) {
|
|
239
|
+
site.footer = configuredSite.footer;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return site;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function buildResolvedConfig(config, { frontPageConfig, menus, customHtmlConfig }) {
|
|
246
|
+
const resolvedConfig = {
|
|
247
|
+
$schema: BUILD_PAGES_CONFIG_SCHEMA_URL,
|
|
248
|
+
version: '0.1',
|
|
249
|
+
site: normalizeSiteConfig(config.site),
|
|
250
|
+
front_page: frontPageConfig,
|
|
251
|
+
menus,
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
if (customHtmlConfig) {
|
|
255
|
+
resolvedConfig.custom_html = customHtmlConfig;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return resolvedConfig;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function normalizeSiteConfig(value) {
|
|
262
|
+
if (value !== undefined && !isPlainObject(value)) {
|
|
263
|
+
throw new PrebuildConfigError(
|
|
264
|
+
'site must be an object.',
|
|
265
|
+
' "site": { "title": "My Docs", "description": "Project documentation" }',
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const configuredSite = isPlainObject(value) ? value : {};
|
|
270
|
+
assertKnownConfigKeys(configuredSite, ['title', 'description', 'url', 'footer'], 'site');
|
|
271
|
+
const site = {
|
|
272
|
+
title: readConfigString(configuredSite.title, 'Documentation'),
|
|
273
|
+
description: readConfigString(configuredSite.description, 'A documentation site.'),
|
|
274
|
+
url: readEnv('ZEROPRESS_SITE_URL', readConfigString(configuredSite.url, '')),
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const footer = normalizeFooter(configuredSite.footer);
|
|
213
278
|
if (footer) {
|
|
214
279
|
site.footer = footer;
|
|
215
280
|
}
|
|
@@ -218,9 +283,13 @@ function buildSiteData(config, frontPage) {
|
|
|
218
283
|
}
|
|
219
284
|
|
|
220
285
|
function normalizeFooter(value) {
|
|
221
|
-
if (
|
|
286
|
+
if (value === undefined) {
|
|
222
287
|
return undefined;
|
|
223
288
|
}
|
|
289
|
+
if (!isPlainObject(value)) {
|
|
290
|
+
throw new PrebuildConfigError('site.footer must be an object.');
|
|
291
|
+
}
|
|
292
|
+
assertKnownConfigKeys(value, ['copyright_text', 'attribution'], 'site.footer');
|
|
224
293
|
|
|
225
294
|
const footer = {};
|
|
226
295
|
const copyrightText = readConfigString(value.copyright_text, '');
|
|
@@ -228,6 +297,15 @@ function normalizeFooter(value) {
|
|
|
228
297
|
footer.copyright_text = copyrightText;
|
|
229
298
|
}
|
|
230
299
|
|
|
300
|
+
if (value.attribution !== undefined && !isPlainObject(value.attribution)) {
|
|
301
|
+
throw new PrebuildConfigError('site.footer.attribution must be an object.');
|
|
302
|
+
}
|
|
303
|
+
if (isPlainObject(value.attribution)) {
|
|
304
|
+
assertKnownConfigKeys(value.attribution, ['enabled'], 'site.footer.attribution');
|
|
305
|
+
if (value.attribution.enabled !== undefined && typeof value.attribution.enabled !== 'boolean') {
|
|
306
|
+
throw new PrebuildConfigError('site.footer.attribution.enabled must be a boolean when provided.');
|
|
307
|
+
}
|
|
308
|
+
}
|
|
231
309
|
if (isPlainObject(value.attribution) && typeof value.attribution.enabled === 'boolean') {
|
|
232
310
|
footer.attribution = {
|
|
233
311
|
enabled: value.attribution.enabled,
|
|
@@ -390,7 +468,7 @@ function isZeropressHtmlFile(filePath) {
|
|
|
390
468
|
return filePath.startsWith('.zeropress/') && filePath.toLowerCase().endsWith('.html');
|
|
391
469
|
}
|
|
392
470
|
|
|
393
|
-
|
|
471
|
+
function normalizeCustomHtmlConfig(value) {
|
|
394
472
|
if (value === undefined) {
|
|
395
473
|
return undefined;
|
|
396
474
|
}
|
|
@@ -408,18 +486,18 @@ async function buildCustomHtmlData(value) {
|
|
|
408
486
|
);
|
|
409
487
|
}
|
|
410
488
|
|
|
411
|
-
const
|
|
489
|
+
const customHtmlConfig = {};
|
|
412
490
|
if (value.head_end !== undefined) {
|
|
413
|
-
|
|
491
|
+
customHtmlConfig.head_end = normalizeCustomHtmlSlotConfig(value.head_end, 'custom_html.head_end');
|
|
414
492
|
}
|
|
415
493
|
if (value.body_end !== undefined) {
|
|
416
|
-
|
|
494
|
+
customHtmlConfig.body_end = normalizeCustomHtmlSlotConfig(value.body_end, 'custom_html.body_end');
|
|
417
495
|
}
|
|
418
496
|
|
|
419
|
-
return
|
|
497
|
+
return customHtmlConfig;
|
|
420
498
|
}
|
|
421
499
|
|
|
422
|
-
|
|
500
|
+
function normalizeCustomHtmlSlotConfig(value, pathLabel) {
|
|
423
501
|
if (!isPlainObject(value)) {
|
|
424
502
|
throw new PrebuildConfigError(`${pathLabel} must be an object.`);
|
|
425
503
|
}
|
|
@@ -439,7 +517,29 @@ async function buildCustomHtmlSlotData(value, pathLabel) {
|
|
|
439
517
|
);
|
|
440
518
|
}
|
|
441
519
|
|
|
442
|
-
|
|
520
|
+
return {
|
|
521
|
+
file,
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
async function buildCustomHtmlData(config) {
|
|
526
|
+
if (!config) {
|
|
527
|
+
return undefined;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const customHtml = {};
|
|
531
|
+
if (config.head_end) {
|
|
532
|
+
customHtml.head_end = await buildCustomHtmlSlotData(config.head_end, 'custom_html.head_end');
|
|
533
|
+
}
|
|
534
|
+
if (config.body_end) {
|
|
535
|
+
customHtml.body_end = await buildCustomHtmlSlotData(config.body_end, 'custom_html.body_end');
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
return customHtml;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
async function buildCustomHtmlSlotData(slotConfig, pathLabel) {
|
|
542
|
+
const sourcePath = resolveConfiguredSourceFile(slotConfig.file, '.html', `${pathLabel}.file`);
|
|
443
543
|
return {
|
|
444
544
|
content: await readRequiredSourceFile(sourcePath, `${pathLabel}.file`),
|
|
445
545
|
};
|
|
@@ -549,13 +649,13 @@ function isPathInside(parentPath, childPath) {
|
|
|
549
649
|
return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath));
|
|
550
650
|
}
|
|
551
651
|
|
|
552
|
-
function
|
|
652
|
+
function defaultPermalinks() {
|
|
553
653
|
return {
|
|
554
|
-
output_style:
|
|
555
|
-
posts:
|
|
556
|
-
pages:
|
|
557
|
-
categories:
|
|
558
|
-
tags:
|
|
654
|
+
output_style: 'html-extension',
|
|
655
|
+
posts: '/posts/:slug/',
|
|
656
|
+
pages: '/:slug/',
|
|
657
|
+
categories: '/categories/:slug/',
|
|
658
|
+
tags: '/tags/:slug/',
|
|
559
659
|
};
|
|
560
660
|
}
|
|
561
661
|
|
|
@@ -629,8 +729,9 @@ function buildPrebuildReport({
|
|
|
629
729
|
generated_at: new Date().toISOString(),
|
|
630
730
|
source_dir: formatSourcePath(sourceDir),
|
|
631
731
|
config_path: formatSourcePath(configPath),
|
|
732
|
+
build_pages_config_path: formatSourcePath(buildPagesConfigPath),
|
|
632
733
|
preview_data_path: formatSourcePath(previewDataPath),
|
|
633
|
-
report_path: formatSourcePath(
|
|
734
|
+
report_path: formatSourcePath(buildReportPath),
|
|
634
735
|
skip_untitled_markdown: skipUntitledMarkdown,
|
|
635
736
|
markdown: {
|
|
636
737
|
discovered: sourceFiles.length,
|
|
@@ -651,7 +752,7 @@ function buildPrebuildReport({
|
|
|
651
752
|
|
|
652
753
|
function printPrebuildSummary(report) {
|
|
653
754
|
const lines = [
|
|
654
|
-
'ZeroPress
|
|
755
|
+
'ZeroPress build report',
|
|
655
756
|
`- Public root: ${report.source_dir}`,
|
|
656
757
|
`- Markdown discovered: ${report.markdown.discovered}`,
|
|
657
758
|
`- Markdown pages generated: ${report.markdown.generated_pages}`,
|
|
@@ -659,6 +760,7 @@ function printPrebuildSummary(report) {
|
|
|
659
760
|
`- Total preview pages: ${report.pages.total}`,
|
|
660
761
|
`- Front page: ${formatFrontPageSummary(report.front_page)}`,
|
|
661
762
|
`- Custom HTML slots: ${report.custom_html.length ? report.custom_html.join(', ') : 'none'}`,
|
|
763
|
+
`- Resolved config: ${report.build_pages_config_path}`,
|
|
662
764
|
`- Report: ${report.report_path}`,
|
|
663
765
|
];
|
|
664
766
|
|
|
@@ -680,7 +782,171 @@ function formatFrontPageSummary(frontPageReport) {
|
|
|
680
782
|
return `html ${config.file} -> / (${previewData.page_slug})`;
|
|
681
783
|
}
|
|
682
784
|
|
|
683
|
-
function
|
|
785
|
+
function parseMarkdownSource(rawMarkdown, sourcePath) {
|
|
786
|
+
try {
|
|
787
|
+
const parsed = matter(rawMarkdown);
|
|
788
|
+
if (!isPlainObject(parsed.data)) {
|
|
789
|
+
throw new PrebuildMarkdownError(
|
|
790
|
+
sourcePath,
|
|
791
|
+
'front matter must be a YAML object.',
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
return {
|
|
796
|
+
bodyMarkdown: parsed.content,
|
|
797
|
+
frontMatter: parsed.data,
|
|
798
|
+
};
|
|
799
|
+
} catch (error) {
|
|
800
|
+
if (error instanceof PrebuildMarkdownError) {
|
|
801
|
+
throw error;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
throw new PrebuildMarkdownError(
|
|
805
|
+
sourcePath,
|
|
806
|
+
`invalid YAML front matter: ${error instanceof Error ? error.message : String(error)}`,
|
|
807
|
+
);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
function readFrontMatterStatus(value, sourcePath) {
|
|
812
|
+
if (value === undefined || value === 'published') {
|
|
813
|
+
return 'published';
|
|
814
|
+
}
|
|
815
|
+
if (value === 'draft') {
|
|
816
|
+
return {
|
|
817
|
+
reason: 'front matter status is "draft".',
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
return {
|
|
822
|
+
reason: `unsupported front matter status ${formatFrontMatterValue(value)}.`,
|
|
823
|
+
expected: 'Expected status: published or draft.',
|
|
824
|
+
warning: true,
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function normalizePublishedFrontMatter(frontMatter, sourcePath) {
|
|
829
|
+
return {
|
|
830
|
+
title: normalizeFrontMatterTitle(frontMatter.title, sourcePath),
|
|
831
|
+
description: normalizeFrontMatterDescription(frontMatter.description, sourcePath),
|
|
832
|
+
path: normalizeFrontMatterRoutePath(frontMatter.path, sourcePath),
|
|
833
|
+
meta: normalizeFrontMatterMeta(frontMatter.meta, sourcePath),
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function normalizeFrontMatterTitle(value, sourcePath) {
|
|
838
|
+
if (value === undefined) {
|
|
839
|
+
return '';
|
|
840
|
+
}
|
|
841
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
842
|
+
throw new PrebuildMarkdownError(
|
|
843
|
+
sourcePath,
|
|
844
|
+
'front matter title must be a non-empty string when provided.',
|
|
845
|
+
);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
return value.trim();
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
function normalizeFrontMatterDescription(value, sourcePath) {
|
|
852
|
+
if (value === undefined) {
|
|
853
|
+
return '';
|
|
854
|
+
}
|
|
855
|
+
if (typeof value !== 'string') {
|
|
856
|
+
throw new PrebuildMarkdownError(
|
|
857
|
+
sourcePath,
|
|
858
|
+
'front matter description must be a string when provided.',
|
|
859
|
+
);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
return value.trim();
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function normalizeFrontMatterRoutePath(value, sourcePath) {
|
|
866
|
+
if (value === undefined) {
|
|
867
|
+
return '';
|
|
868
|
+
}
|
|
869
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
870
|
+
throw new PrebuildMarkdownError(
|
|
871
|
+
sourcePath,
|
|
872
|
+
'front matter path must be a non-empty string when provided.',
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
const routePath = value.trim();
|
|
877
|
+
const segments = routePath.split('/');
|
|
878
|
+
if (
|
|
879
|
+
routePath.startsWith('/')
|
|
880
|
+
|| routePath.endsWith('/')
|
|
881
|
+
|| routePath.includes('\\')
|
|
882
|
+
|| routePath.includes('?')
|
|
883
|
+
|| routePath.includes('#')
|
|
884
|
+
|| segments.some((segment) => !isSafeRoutePathSegment(segment))
|
|
885
|
+
) {
|
|
886
|
+
throw new PrebuildMarkdownError(
|
|
887
|
+
sourcePath,
|
|
888
|
+
'front matter path must be a safe generated route path.',
|
|
889
|
+
' path: guides/install\n path: spec/preview-data-v0.5',
|
|
890
|
+
);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
return routePath;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
function isSafeRoutePathSegment(segment) {
|
|
897
|
+
return (
|
|
898
|
+
/^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/.test(segment)
|
|
899
|
+
&& !segment.includes('..')
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
function normalizeFrontMatterMeta(value, sourcePath) {
|
|
904
|
+
if (value === undefined) {
|
|
905
|
+
return {};
|
|
906
|
+
}
|
|
907
|
+
if (!isPlainObject(value)) {
|
|
908
|
+
throw new PrebuildMarkdownError(
|
|
909
|
+
sourcePath,
|
|
910
|
+
'front matter meta must be an object when provided.',
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
const meta = {};
|
|
915
|
+
for (const [key, metaValue] of Object.entries(value)) {
|
|
916
|
+
if (!isPreviewMetaValue(metaValue)) {
|
|
917
|
+
throw new PrebuildMarkdownError(
|
|
918
|
+
sourcePath,
|
|
919
|
+
`front matter meta.${key} must be a string, number, boolean, or null.`,
|
|
920
|
+
);
|
|
921
|
+
}
|
|
922
|
+
meta[key] = metaValue;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
return meta;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
function isPreviewMetaValue(value) {
|
|
929
|
+
return (
|
|
930
|
+
value === null
|
|
931
|
+
|| typeof value === 'string'
|
|
932
|
+
|| typeof value === 'number'
|
|
933
|
+
|| typeof value === 'boolean'
|
|
934
|
+
);
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
function formatFrontMatterValue(value) {
|
|
938
|
+
if (typeof value === 'string') {
|
|
939
|
+
return `"${value}"`;
|
|
940
|
+
}
|
|
941
|
+
const serialized = JSON.stringify(value);
|
|
942
|
+
return serialized === undefined ? String(value) : serialized;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
function extractTitleOrSkip(markdown, sourcePath, skippedMarkdown, frontMatterTitle = '') {
|
|
946
|
+
if (frontMatterTitle) {
|
|
947
|
+
return frontMatterTitle;
|
|
948
|
+
}
|
|
949
|
+
|
|
684
950
|
try {
|
|
685
951
|
return extractTitle(markdown, sourcePath);
|
|
686
952
|
} catch (error) {
|
|
@@ -689,11 +955,8 @@ function extractTitleOrSkip(markdown, sourcePath, skippedMarkdown) {
|
|
|
689
955
|
&& error instanceof PrebuildMarkdownError
|
|
690
956
|
&& error.code === 'untitled_markdown'
|
|
691
957
|
) {
|
|
692
|
-
console.warn(
|
|
693
|
-
skippedMarkdown.
|
|
694
|
-
file: formatSourcePath(error.sourcePath),
|
|
695
|
-
reason: error.reason,
|
|
696
|
-
});
|
|
958
|
+
console.warn(formatSkippedMarkdownWarning(error.sourcePath, error.reason, '', 'Skipped untitled Markdown'));
|
|
959
|
+
recordSkippedMarkdown(skippedMarkdown, error.sourcePath, error.reason);
|
|
697
960
|
return '';
|
|
698
961
|
}
|
|
699
962
|
|
|
@@ -701,12 +964,23 @@ function extractTitleOrSkip(markdown, sourcePath, skippedMarkdown) {
|
|
|
701
964
|
}
|
|
702
965
|
}
|
|
703
966
|
|
|
704
|
-
function
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
967
|
+
function recordSkippedMarkdown(skippedMarkdown, sourcePath, reason) {
|
|
968
|
+
skippedMarkdown.push({
|
|
969
|
+
file: formatSourcePath(sourcePath),
|
|
970
|
+
reason,
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
function formatSkippedMarkdownWarning(sourcePath, reason, expected = '', label = 'Skipped Markdown') {
|
|
975
|
+
const lines = [
|
|
976
|
+
`[zeropress-build-pages] ${label}: ${formatSourcePath(sourcePath)}`,
|
|
977
|
+
`Reason: ${reason}`,
|
|
978
|
+
];
|
|
979
|
+
if (expected) {
|
|
980
|
+
lines.push(expected);
|
|
981
|
+
}
|
|
982
|
+
lines.push('This file was not added to preview-data pages.');
|
|
983
|
+
return lines.join('\n');
|
|
710
984
|
}
|
|
711
985
|
|
|
712
986
|
async function listMarkdownFiles(dir) {
|
|
@@ -780,6 +1054,17 @@ function buildHtmlPageRoute(sourcePath, options = {}) {
|
|
|
780
1054
|
}
|
|
781
1055
|
|
|
782
1056
|
function buildRoutePath(relativeSourcePath, sourcePath, options = {}) {
|
|
1057
|
+
if (options.routePath) {
|
|
1058
|
+
if (options.routePath === 'index' && !options.allowRootIndex) {
|
|
1059
|
+
throw new PrebuildMarkdownError(
|
|
1060
|
+
sourcePath,
|
|
1061
|
+
'front matter path "index" is reserved for the front page.',
|
|
1062
|
+
' path: docs/index\n path: guide',
|
|
1063
|
+
);
|
|
1064
|
+
}
|
|
1065
|
+
return options.routePath;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
783
1068
|
const extensionPattern = options.extensionPattern || /\.md$/i;
|
|
784
1069
|
const withoutExtension = relativeSourcePath.replace(extensionPattern, '').toLowerCase();
|
|
785
1070
|
const segments = withoutExtension
|