@zeropress/build-pages 0.5.1

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/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@zeropress/build-pages",
3
+ "version": "0.5.1",
4
+ "description": "ZeroPress Markdown build action and CLI for static hosting",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "bin": {
8
+ "zeropress-build-pages": "bin/zeropress-build-pages.js"
9
+ },
10
+ "files": [
11
+ "action.yml",
12
+ "bin",
13
+ "dist",
14
+ "schemas",
15
+ "src",
16
+ "themes"
17
+ ],
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/zeropress-app/zeropress-build-pages.git"
21
+ },
22
+ "bugs": {
23
+ "url": "https://github.com/zeropress-app/zeropress-build-pages/issues"
24
+ },
25
+ "homepage": "https://github.com/zeropress-app/zeropress-build-pages#readme",
26
+ "keywords": [
27
+ "zeropress",
28
+ "github-pages",
29
+ "cloudflare-pages",
30
+ "netlify",
31
+ "vercel",
32
+ "markdown",
33
+ "static-site-generator"
34
+ ],
35
+ "scripts": {
36
+ "test": "node --test",
37
+ "build:action": "node scripts/build-action.mjs"
38
+ },
39
+ "engines": {
40
+ "node": ">=18.18.0"
41
+ },
42
+ "dependencies": {
43
+ "@zeropress/build": "0.5.1"
44
+ },
45
+ "devDependencies": {
46
+ "esbuild": "0.28.0"
47
+ }
48
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://zeropress.dev/schemas/zeropress-build-pages.config.schema.json",
4
+ "$ref": "https://zeropress.dev/schemas/zeropress-build-pages.config.v0.1.schema.json"
5
+ }
@@ -0,0 +1,365 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://zeropress.dev/schemas/zeropress-build-pages.config.v0.1.schema.json",
4
+ "title": "ZeroPress Build Pages Config v0.1",
5
+ "description": "Optional source-directory configuration for @zeropress/build-pages workflows.",
6
+ "markdownDescription": "Optional source-directory configuration for `@zeropress/build-pages` workflows.",
7
+ "type": "object",
8
+ "additionalProperties": false,
9
+ "properties": {
10
+ "$schema": {
11
+ "type": "string",
12
+ "description": "Optional JSON Schema URI or path used by editors and tooling.",
13
+ "markdownDescription": "Optional JSON Schema URI or path used by editors and tooling."
14
+ },
15
+ "version": {
16
+ "type": "string",
17
+ "const": "0.1",
18
+ "description": "ZeroPress Build Pages config format version.",
19
+ "markdownDescription": "ZeroPress Build Pages config format version."
20
+ },
21
+ "site": {
22
+ "$ref": "#/$defs/site"
23
+ },
24
+ "front_page": {
25
+ "$ref": "#/$defs/frontPage"
26
+ },
27
+ "custom_html": {
28
+ "$ref": "#/$defs/customHtml"
29
+ },
30
+ "menus": {
31
+ "$ref": "#/$defs/menus"
32
+ }
33
+ },
34
+ "$defs": {
35
+ "site": {
36
+ "type": "object",
37
+ "additionalProperties": false,
38
+ "description": "Site metadata and output policy defaults used while generating preview-data.",
39
+ "markdownDescription": "Site metadata and output policy defaults used while generating preview-data.",
40
+ "properties": {
41
+ "title": {
42
+ "type": "string",
43
+ "minLength": 1
44
+ },
45
+ "description": {
46
+ "type": "string"
47
+ },
48
+ "url": {
49
+ "type": "string",
50
+ "description": "Canonical site URL. Use an empty string or omit this field for local builds without canonical output.",
51
+ "markdownDescription": "Canonical site URL. Use an empty string or omit this field for local builds without canonical output."
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
+ "footer": {
80
+ "$ref": "#/$defs/siteFooter"
81
+ },
82
+ "disallowComments": {
83
+ "type": "boolean"
84
+ }
85
+ }
86
+ },
87
+ "siteFooter": {
88
+ "type": "object",
89
+ "additionalProperties": false,
90
+ "description": "Optional footer display data copied into generated preview-data.",
91
+ "markdownDescription": "Optional footer display data copied into generated preview-data.",
92
+ "properties": {
93
+ "copyright_text": {
94
+ "type": "string",
95
+ "minLength": 1,
96
+ "description": "Footer copyright or legal text. ZeroPress does not add a copyright symbol automatically.",
97
+ "markdownDescription": "Footer copyright or legal text. ZeroPress does not add a copyright symbol automatically."
98
+ },
99
+ "attribution": {
100
+ "$ref": "#/$defs/siteFooterAttribution"
101
+ }
102
+ }
103
+ },
104
+ "siteFooterAttribution": {
105
+ "type": "object",
106
+ "additionalProperties": false,
107
+ "description": "Optional ZeroPress attribution display policy for themes.",
108
+ "markdownDescription": "Optional ZeroPress attribution display policy for themes.",
109
+ "properties": {
110
+ "enabled": {
111
+ "type": "boolean",
112
+ "description": "When false, bundled themes hide the Published with ZeroPress attribution. Missing or true means attribution may be shown.",
113
+ "markdownDescription": "When `false`, bundled themes hide the Published with ZeroPress attribution. Missing or `true` means attribution may be shown."
114
+ }
115
+ }
116
+ },
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
+ "frontPage": {
149
+ "type": "object",
150
+ "additionalProperties": false,
151
+ "required": ["type"],
152
+ "description": "Source file or theme template used for the site root.",
153
+ "markdownDescription": "Source file or theme template used for the site root.",
154
+ "properties": {
155
+ "type": {
156
+ "type": "string",
157
+ "enum": ["theme_index", "markdown", "html"],
158
+ "default": "theme_index",
159
+ "description": "Front page source mode. theme_index uses theme/index.html. markdown and html read a source file.",
160
+ "markdownDescription": "Front page source mode. `theme_index` uses `theme/index.html`. `markdown` and `html` read a source file."
161
+ },
162
+ "file": {
163
+ "$ref": "#/$defs/sourceFilePath",
164
+ "description": "Optional source-root relative file path. Defaults to index.md for markdown and .zeropress/index.html for html.",
165
+ "markdownDescription": "Optional source-root relative file path. Defaults to `index.md` for `markdown` and `.zeropress/index.html` for `html`."
166
+ },
167
+ "layout": {
168
+ "type": "boolean",
169
+ "default": true,
170
+ "description": "Only applies to html front pages. true renders the file as page body content through the ZeroPress layout; false treats it as a full standalone HTML document.",
171
+ "markdownDescription": "Only applies to `html` front pages. `true` renders the file as page body content through the ZeroPress layout; `false` treats it as a full standalone HTML document."
172
+ }
173
+ },
174
+ "allOf": [
175
+ {
176
+ "if": {
177
+ "properties": {
178
+ "type": { "const": "theme_index" }
179
+ },
180
+ "required": ["type"]
181
+ },
182
+ "then": {
183
+ "not": {
184
+ "anyOf": [
185
+ { "required": ["file"] },
186
+ { "required": ["layout"] }
187
+ ]
188
+ }
189
+ }
190
+ },
191
+ {
192
+ "if": {
193
+ "properties": {
194
+ "type": { "const": "markdown" }
195
+ },
196
+ "required": ["type"]
197
+ },
198
+ "then": {
199
+ "not": {
200
+ "required": ["layout"]
201
+ },
202
+ "properties": {
203
+ "file": {
204
+ "type": "string",
205
+ "pattern": "\\.md$"
206
+ }
207
+ }
208
+ }
209
+ },
210
+ {
211
+ "if": {
212
+ "properties": {
213
+ "type": { "const": "html" }
214
+ },
215
+ "required": ["type"]
216
+ },
217
+ "then": {
218
+ "properties": {
219
+ "file": {
220
+ "type": "string",
221
+ "pattern": "^\\.zeropress/.+\\.html$"
222
+ }
223
+ }
224
+ }
225
+ }
226
+ ]
227
+ },
228
+ "sourceFilePath": {
229
+ "type": "string",
230
+ "minLength": 1,
231
+ "pattern": "^(?!/)(?!.*(?:^|/)\\.\\.(?:/|$))(?!.*[?#]).+$",
232
+ "description": "Source-root relative path. Leading slash, parent-directory traversal, query strings, and hash fragments are not allowed.",
233
+ "markdownDescription": "Source-root relative path. Leading slash, parent-directory traversal, query strings, and hash fragments are not allowed."
234
+ },
235
+ "customHtml": {
236
+ "type": "object",
237
+ "additionalProperties": false,
238
+ "minProperties": 1,
239
+ "description": "Trusted HTML snippets copied into preview-data custom_html slots.",
240
+ "markdownDescription": "Trusted HTML snippets copied into preview-data `custom_html` slots.",
241
+ "properties": {
242
+ "head_end": {
243
+ "$ref": "#/$defs/customHtmlSlot",
244
+ "description": "HTML file injected before </head> by ZeroPress build.",
245
+ "markdownDescription": "HTML file injected before `</head>` by ZeroPress build."
246
+ },
247
+ "body_end": {
248
+ "$ref": "#/$defs/customHtmlSlot",
249
+ "description": "HTML file injected before </body> by ZeroPress build.",
250
+ "markdownDescription": "HTML file injected before `</body>` by ZeroPress build."
251
+ }
252
+ }
253
+ },
254
+ "customHtmlSlot": {
255
+ "type": "object",
256
+ "additionalProperties": false,
257
+ "required": ["file"],
258
+ "properties": {
259
+ "file": {
260
+ "allOf": [
261
+ { "$ref": "#/$defs/sourceFilePath" },
262
+ {
263
+ "type": "string",
264
+ "pattern": "^\\.zeropress/.+\\.html$"
265
+ }
266
+ ],
267
+ "description": "Prebuild-only HTML file inside .zeropress/.",
268
+ "markdownDescription": "Prebuild-only HTML file inside `.zeropress/`."
269
+ }
270
+ }
271
+ },
272
+ "menus": {
273
+ "type": "object",
274
+ "description": "Menu definitions copied into generated preview-data.",
275
+ "markdownDescription": "Menu definitions copied into generated preview-data.",
276
+ "propertyNames": {
277
+ "type": "string",
278
+ "pattern": "^[a-z][a-z0-9_-]{0,63}$"
279
+ },
280
+ "additionalProperties": {
281
+ "$ref": "#/$defs/menu"
282
+ }
283
+ },
284
+ "menu": {
285
+ "type": "object",
286
+ "additionalProperties": false,
287
+ "required": ["items"],
288
+ "properties": {
289
+ "name": {
290
+ "type": "string",
291
+ "minLength": 1
292
+ },
293
+ "items": {
294
+ "type": "array",
295
+ "items": {
296
+ "$ref": "#/$defs/menuItem"
297
+ }
298
+ }
299
+ }
300
+ },
301
+ "menuItem": {
302
+ "type": "object",
303
+ "additionalProperties": false,
304
+ "required": ["title", "url"],
305
+ "properties": {
306
+ "title": {
307
+ "type": "string",
308
+ "minLength": 1
309
+ },
310
+ "url": {
311
+ "type": "string",
312
+ "minLength": 1
313
+ },
314
+ "type": {
315
+ "type": "string",
316
+ "enum": ["custom", "page", "post", "category"],
317
+ "default": "custom"
318
+ },
319
+ "target": {
320
+ "type": "string",
321
+ "enum": ["_self", "_blank"],
322
+ "default": "_self"
323
+ },
324
+ "children": {
325
+ "type": "array",
326
+ "items": {
327
+ "$ref": "#/$defs/menuItem"
328
+ },
329
+ "default": []
330
+ }
331
+ }
332
+ }
333
+ },
334
+ "examples": [
335
+ {
336
+ "$schema": "../schemas/zeropress-build-pages.config.v0.1.schema.json",
337
+ "version": "0.1",
338
+ "site": {
339
+ "title": "ZeroPress Public Docs",
340
+ "description": "Public documentation.",
341
+ "url": "https://zeropress.dev"
342
+ },
343
+ "front_page": {
344
+ "type": "markdown"
345
+ },
346
+ "custom_html": {
347
+ "head_end": {
348
+ "file": ".zeropress/head-end.html"
349
+ },
350
+ "body_end": {
351
+ "file": ".zeropress/body-end.html"
352
+ }
353
+ },
354
+ "menus": {
355
+ "primary": {
356
+ "name": "Primary Menu",
357
+ "items": [
358
+ { "title": "Home", "url": "/" },
359
+ { "title": "Docs", "url": "/docs/" }
360
+ ]
361
+ }
362
+ }
363
+ }
364
+ ]
365
+ }
package/src/action.js ADDED
@@ -0,0 +1,32 @@
1
+ import { runBuildPages } from './index.js';
2
+
3
+ const options = {
4
+ source: input('source') || './',
5
+ destination: input('destination') || './_site',
6
+ theme: input('theme') || 'docs',
7
+ themePath: input('theme-path'),
8
+ config: input('config'),
9
+ siteUrl: input('site-url'),
10
+ skipUntitledMarkdown: booleanInput('skip-untitled-markdown', false),
11
+ checkLinks: booleanInput('check-links', true),
12
+ };
13
+
14
+ try {
15
+ await runBuildPages(options);
16
+ } catch (error) {
17
+ const message = error instanceof Error ? error.message : String(error);
18
+ console.error(message);
19
+ process.exitCode = 1;
20
+ }
21
+
22
+ function input(name) {
23
+ return process.env[`INPUT_${name.toUpperCase().replace(/-/g, '_')}`]?.trim() || '';
24
+ }
25
+
26
+ function booleanInput(name, fallback) {
27
+ const value = input(name);
28
+ if (!value) {
29
+ return fallback;
30
+ }
31
+ return value.toLowerCase() === 'true';
32
+ }
@@ -0,0 +1,94 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ export async function checkInternalLinks(siteDir) {
5
+ const htmlFiles = await listFiles(siteDir, (filePath) => filePath.endsWith('.html'));
6
+ const brokenLinks = [];
7
+
8
+ for (const filePath of htmlFiles) {
9
+ const html = await fs.readFile(filePath, 'utf8');
10
+ const links = extractInternalLinks(html);
11
+
12
+ for (const link of links) {
13
+ if (!(await linkExists(siteDir, link))) {
14
+ brokenLinks.push(`${path.relative(siteDir, filePath)} -> ${link}`);
15
+ }
16
+ }
17
+ }
18
+
19
+ return {
20
+ htmlFiles,
21
+ brokenLinks,
22
+ };
23
+ }
24
+
25
+ async function listFiles(dir, predicate) {
26
+ const entries = await fs.readdir(dir, { withFileTypes: true });
27
+ const files = [];
28
+
29
+ for (const entry of entries) {
30
+ const filePath = path.join(dir, entry.name);
31
+ if (entry.isDirectory()) {
32
+ files.push(...await listFiles(filePath, predicate));
33
+ continue;
34
+ }
35
+
36
+ if (entry.isFile() && predicate(filePath)) {
37
+ files.push(filePath);
38
+ }
39
+ }
40
+
41
+ return files;
42
+ }
43
+
44
+ function extractInternalLinks(html) {
45
+ const links = [];
46
+ const pattern = /\b(?:href|src)="([^"]+)"/g;
47
+ let match;
48
+
49
+ while ((match = pattern.exec(html)) !== null) {
50
+ const raw = match[1].trim();
51
+ if (
52
+ !raw
53
+ || raw.startsWith('#')
54
+ || /^[a-z][a-z0-9+.-]*:/i.test(raw)
55
+ || raw.startsWith('//')
56
+ ) {
57
+ continue;
58
+ }
59
+
60
+ links.push(stripHashAndQuery(raw));
61
+ }
62
+
63
+ return links.filter(Boolean);
64
+ }
65
+
66
+ function stripHashAndQuery(link) {
67
+ return link.split('#')[0].split('?')[0];
68
+ }
69
+
70
+ async function linkExists(siteDir, link) {
71
+ const relativePath = decodeURIComponent(link.replace(/^\/+/, ''));
72
+ const candidates = link.endsWith('/')
73
+ ? [path.join(siteDir, relativePath, 'index.html')]
74
+ : [
75
+ path.join(siteDir, relativePath),
76
+ path.join(siteDir, `${relativePath}.html`),
77
+ path.join(siteDir, relativePath, 'index.html'),
78
+ ];
79
+
80
+ for (const candidate of candidates) {
81
+ try {
82
+ const stat = await fs.stat(candidate);
83
+ if (stat.isFile()) {
84
+ return true;
85
+ }
86
+ } catch (error) {
87
+ if (error?.code !== 'ENOENT') {
88
+ throw error;
89
+ }
90
+ }
91
+ }
92
+
93
+ return false;
94
+ }