@zeropress/build-pages 0.6.1 → 0.6.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 CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  Build ZeroPress static output for modern hosting platforms.
7
7
 
8
- `@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`](https://github.com/zeropress-app/zeropress-build).
8
+ `@zeropress/build-pages` turns 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`](https://github.com/zeropress-app/zeropress-build).
9
9
 
10
10
  The generated output is plain static files that can be deployed to GitHub Pages, Cloudflare Pages, Netlify, Vercel, or any static hosting provider.
11
11
 
@@ -13,7 +13,9 @@ The generated output is plain static files that can be deployed to GitHub Pages,
13
13
 
14
14
  ```txt
15
15
  source directory
16
- Markdown pages + .zeropress/config.json + public files
16
+ Markdown pages + .zeropress/config.json
17
+ public directory
18
+ public files (defaults to source)
17
19
  |
18
20
  v
19
21
  @zeropress/build-pages
@@ -32,7 +34,7 @@ static output directory
32
34
  flowchart TD
33
35
  source["Source directory"] --> markdown["Markdown pages (*.md)"]
34
36
  source --> config[".zeropress/config.json"]
35
- source --> publicFiles["Public files<br/>images, CSS, JS, PDF, JSON, Markdown"]
37
+ publicRoot["Public directory<br/>defaults to source"] --> publicFiles["Public files<br/>images, CSS, JS, PDF, JSON, Markdown"]
36
38
 
37
39
  markdown --> buildPages["@zeropress/build-pages"]
38
40
  config --> buildPages
@@ -83,6 +85,7 @@ jobs:
83
85
  uses: zeropress-app/zeropress-build-pages@v0
84
86
  with:
85
87
  source: ./docs
88
+ public-dir: ./public
86
89
  destination: ./_site
87
90
  - name: Upload artifact
88
91
  uses: actions/upload-pages-artifact@v5
@@ -114,6 +117,7 @@ That is equivalent to:
114
117
  uses: zeropress-app/zeropress-build-pages@v0
115
118
  with:
116
119
  source: ./docs
120
+ public-dir: ./docs
117
121
  destination: ./_site
118
122
  theme: docs
119
123
  skip-untitled-markdown: false
@@ -128,6 +132,7 @@ Custom input example:
128
132
  uses: zeropress-app/zeropress-build-pages@v0
129
133
  with:
130
134
  source: ./documents
135
+ public-dir: ./public
131
136
  destination: ./_site
132
137
  theme-path: ./theme-docs
133
138
  config: ./documents/.zeropress/config.json
@@ -137,7 +142,8 @@ Custom input example:
137
142
 
138
143
  In the action inputs:
139
144
 
140
- - `source` is the directory that contains your Markdown pages, public files, and optional `.zeropress/config.json`. The default is `./docs`.
145
+ - `source` is the directory that contains your Markdown pages and optional `.zeropress/config.json`. The default is `./docs`.
146
+ - `public-dir` is the directory copied as public passthrough files. The default is `source`.
141
147
  - `destination` is the directory where the generated static site is written. The default is `./_site`.
142
148
  - `theme` is the bundled theme name. The default is `docs`.
143
149
  - `theme-path` is a custom local ZeroPress theme directory. It takes precedence over `theme`.
@@ -158,6 +164,7 @@ npx @zeropress/create-theme --name my-docs-theme --template docs
158
164
  ```yaml
159
165
  with:
160
166
  source: ./docs
167
+ public-dir: ./public
161
168
  destination: ./_site
162
169
  theme-path: ./my-docs-theme/theme
163
170
  ```
@@ -196,7 +203,8 @@ The CLI requires explicit input and output paths. The GitHub Action keeps safe d
196
203
 
197
204
  | Option | Default | Purpose |
198
205
  | --- | --- | --- |
199
- | `--source <dir>` | required | Dedicated source directory containing Markdown and public files |
206
+ | `--source <dir>` | required | Dedicated source directory containing Markdown and optional config |
207
+ | `--public-dir <dir>` | source | Public passthrough directory |
200
208
  | `--destination <dir>` | required | Output directory |
201
209
  | `--theme docs` | `docs` | Bundled docs theme |
202
210
  | `--theme-path <dir>` | none | Custom ZeroPress theme directory |
@@ -208,7 +216,7 @@ The CLI requires explicit input and output paths. The GitHub Action keeps safe d
208
216
 
209
217
  ## Source Tree
210
218
 
211
- The source directory is the folder that Build Pages reads. It is both the Markdown source root and the public passthrough root. GitHub Action usage defaults to `./docs`; CLI usage requires `--source`.
219
+ The source directory is the folder that Build Pages reads for Markdown pages and optional `.zeropress/config.json`. By default, the source directory is also the public passthrough root. Use `public-dir` when you want to keep Markdown source and public assets separate.
212
220
 
213
221
  Use a dedicated content directory such as `docs/` or `documents/`. Repository root source (`--source ./`) is not supported.
214
222
 
@@ -217,20 +225,25 @@ my-site/
217
225
  docs/ # source
218
226
  index.md
219
227
  guide.md
220
- assets/
221
- logo.png
222
228
  .zeropress/
223
229
  config.json
230
+ public/ # public-dir, optional
231
+ assets/
232
+ logo.png
233
+ favicon.svg
234
+ robots.txt
224
235
  _site/ # destination, generated
225
236
  ```
226
237
 
227
- Build Pages stages the source tree before calling [`@zeropress/build`](https://github.com/zeropress-app/zeropress-build). Generated ZeroPress output wins over staged public files.
238
+ Build Pages stages the public directory before calling [`@zeropress/build`](https://github.com/zeropress-app/zeropress-build). Generated ZeroPress output wins over staged public files.
239
+
240
+ Root-level public files named `favicon.ico`, `favicon.svg`, `favicon.png`, and `apple-touch-icon.png` are copied to the destination and auto-injected into generated HTML `<head>` output.
228
241
 
229
- Root-level source favicon files named `favicon.ico`, `favicon.svg`, `favicon.png`, and `apple-touch-icon.png` are copied to the destination and auto-injected into generated HTML `<head>` output.
242
+ A root-level public `sitemap.xsl` is copied to the destination. When ZeroPress generates `sitemap.xml`, it auto-discovers that file and adds an XML stylesheet processing instruction for `/sitemap.xsl`.
230
243
 
231
- A root-level source `sitemap.xsl` is copied to the destination. When ZeroPress generates `sitemap.xml`, it auto-discovers that file and adds an XML stylesheet processing instruction for `/sitemap.xsl`.
244
+ The source directory must not overlap the destination directory, the selected theme directory, or the internal `.zeropress/` working directory. An explicit public directory must be an existing dedicated directory and must not be a file, symlink, repository root, destination directory, selected theme directory, or internal `.zeropress/` working directory.
232
245
 
233
- The source directory must not overlap the destination directory, the selected theme directory, or the internal `.zeropress/` working directory.
246
+ If `public-dir` is inside `source`, Build Pages excludes that public subtree from Markdown page discovery.
234
247
 
235
248
  Ignored while staging and Markdown discovery:
236
249
 
@@ -334,6 +347,7 @@ See the public config reference at [zeropress.dev/build-pages-config](https://ze
334
347
  "description": "Project documentation",
335
348
  "url": "https://example.github.io/project",
336
349
  "expose_generator": true,
350
+ "search": true,
337
351
  "indexing": true,
338
352
  "footer": {
339
353
  "copyright_text": "Copyright 2026 Example Corp.",
@@ -383,7 +397,20 @@ The bundled docs theme shows `Published with ZeroPress.` by default. Set `site.f
383
397
 
384
398
  `site.expose_generator` controls the HTML generator meta tag. Missing or `true` emits `<meta name="generator" content="ZeroPress">`; set it to `false` for white-label sites.
385
399
 
386
- `site.indexing` controls only the generated fallback `robots.txt`. Missing or `true` allows indexing; `false` writes `User-agent: *` / `Disallow: /`. If the source directory contains `robots.txt`, that file is copied as-is and takes priority over `site.indexing`. ZeroPress does not append a `Sitemap` directive to a source-provided `robots.txt`; add `Sitemap: https://example.com/sitemap.xml` manually when needed.
400
+ `site.search` controls native ZeroPress search when the selected theme supports search UI. Missing or `true` enables native search for the bundled docs theme; `false` omits `/_zeropress/search.json`, `/_zeropress/search.js`, and `/_zeropress/search_pagefind.js` and hides the bundled search form.
401
+
402
+ The bundled docs theme marks post/page body content with `data-pagefind-body`. If you run Pagefind after the ZeroPress build, keep the theme UI pointed at `/_zeropress/search.js` and replace the native adapter:
403
+
404
+ ```bash
405
+ npx pagefind@latest \
406
+ --site ./_site \
407
+ --output-subdir _zeropress/pagefind
408
+
409
+ cp ./_site/_zeropress/search_pagefind.js ./_site/_zeropress/search.js
410
+ rm ./_site/_zeropress/search.json
411
+ ```
412
+
413
+ `site.indexing` controls only the generated fallback `robots.txt`. Missing or `true` allows indexing; `false` writes `User-agent: *` / `Disallow: /`. If the public directory contains `robots.txt`, that file is copied as-is and takes priority over `site.indexing`. ZeroPress does not append a `Sitemap` directive to a public `robots.txt`; add `Sitemap: https://example.com/sitemap.xml` manually when needed.
387
414
 
388
415
  Schemas:
389
416
 
@@ -405,13 +432,13 @@ Build Pages reads optional site config from `<source>/.zeropress/config.json`. S
405
432
 
406
433
  `preview-data.json` is an internal generated build input for the ZeroPress renderer. Most users do not need to edit or understand this file.
407
434
 
408
- `build-report.json` records discovered Markdown counts, skipped Markdown files, front page resolution, source Markdown copy policy, and custom HTML slots.
435
+ `build-report.json` records source/public roots, discovered Markdown counts, skipped Markdown files, front page resolution, source Markdown copy policy, and custom HTML slots.
409
436
 
410
437
  `public-assets/` is a temporary staged public root used before the final ZeroPress render.
411
438
 
412
439
  ## Destination Output
413
440
 
414
- The `destination` directory contains the deployable static site. It includes generated ZeroPress HTML, copied public files, and original Markdown files unless Markdown source copy is disabled or files are excluded by the public passthrough rules. A source `robots.txt` is copied as a site-owned policy file; otherwise ZeroPress writes a fallback `robots.txt` with a sitemap directive when `site.url` is available. Root-level source favicon files are copied and represented as generated HTML head links. A root-level source `sitemap.xsl` is copied and linked from generated `sitemap.xml`.
441
+ The `destination` directory contains the deployable static site. It includes generated ZeroPress HTML, copied public files, and original Markdown files unless Markdown source copy is disabled or files are excluded by the public passthrough rules. A public `robots.txt` is copied as a site-owned policy file; otherwise ZeroPress writes a fallback `robots.txt` with a sitemap directive when `site.url` is available. Root-level public favicon files are copied and represented as generated HTML head links. A root-level public `sitemap.xsl` is copied and linked from generated `sitemap.xml`.
415
442
 
416
443
  ## Demo
417
444
 
package/action.yml CHANGED
@@ -9,6 +9,9 @@ inputs:
9
9
  description: Dedicated source directory containing Markdown files and optional .zeropress/config.json. Repository root source is not supported.
10
10
  required: false
11
11
  default: ./docs
12
+ public-dir:
13
+ description: Public passthrough directory for site-owned assets. Defaults to source.
14
+ required: false
12
15
  destination:
13
16
  description: Output directory for the generated static site.
14
17
  required: false
package/dist/action.js CHANGED
@@ -52318,6 +52318,7 @@ function validateSite(site, path4, errors) {
52318
52318
  "media_delivery_mode",
52319
52319
  "favicon",
52320
52320
  "expose_generator",
52321
+ "search",
52321
52322
  "locale",
52322
52323
  "posts_per_page",
52323
52324
  "datetime_display",
@@ -52348,6 +52349,9 @@ function validateSite(site, path4, errors) {
52348
52349
  if (site.expose_generator !== void 0) {
52349
52350
  validateBoolean(site.expose_generator, `${path4}.expose_generator`, "INVALID_SITE_EXPOSE_GENERATOR", errors);
52350
52351
  }
52352
+ if (site.search !== void 0) {
52353
+ validateBoolean(site.search, `${path4}.search`, "INVALID_SITE_SEARCH", errors);
52354
+ }
52351
52355
  validateNonEmptyString(site.locale, `${path4}.locale`, "INVALID_SITE_LOCALE", errors);
52352
52356
  validateInteger(site.posts_per_page, `${path4}.posts_per_page`, "INVALID_SITE_POSTS_PER_PAGE", errors, { minimum: 1 });
52353
52357
  validateEnum(site.datetime_display, `${path4}.datetime_display`, "INVALID_SITE_DATETIME_DISPLAY", errors, PREVIEW_DATETIME_DISPLAY_MODES);
@@ -53010,7 +53014,7 @@ function isOptionalKey(path4, key) {
53010
53014
  return key === "head_end" || key === "body_end";
53011
53015
  }
53012
53016
  if (path4 === "site") {
53013
- return key === "media_delivery_mode" || key === "favicon" || key === "expose_generator" || key === "indexing" || key === "permalinks" || key === "front_page" || key === "post_index" || key === "footer" || key === "meta";
53017
+ return key === "media_delivery_mode" || key === "favicon" || key === "expose_generator" || key === "search" || key === "indexing" || key === "permalinks" || key === "front_page" || key === "post_index" || key === "footer" || key === "meta";
53014
53018
  }
53015
53019
  if (path4 === "site.favicon") {
53016
53020
  return key === "icon" || key === "svg" || key === "png" || key === "apple_touch_icon";
@@ -53272,7 +53276,7 @@ var MENU_SLOT_ID_MAX_LENGTH = 32;
53272
53276
  var MENU_SLOT_COUNT_MAX = 12;
53273
53277
  var MENU_SLOT_TITLE_MAX_LENGTH = 80;
53274
53278
  var MENU_SLOT_DESCRIPTION_MAX_LENGTH = 160;
53275
- var SUPPORTED_THEME_FEATURES = /* @__PURE__ */ new Set(["comments", "newsletter", "post_index"]);
53279
+ var SUPPORTED_THEME_FEATURES = /* @__PURE__ */ new Set(["comments", "newsletter", "post_index", "search"]);
53276
53280
  var THEME_MANIFEST_KEYS = /* @__PURE__ */ new Set([
53277
53281
  "$schema",
53278
53282
  "name",
@@ -60907,6 +60911,7 @@ var PERMALINK_OUTPUT_STYLES = /* @__PURE__ */ new Set(["directory", "html-extens
60907
60911
  var COMMENT_POLICY_OUTPUT_PATH = "_zeropress/comment-policy.json";
60908
60912
  var SEARCH_INDEX_OUTPUT_PATH = "_zeropress/search.json";
60909
60913
  var SEARCH_ADAPTER_OUTPUT_PATH = "_zeropress/search.js";
60914
+ var SEARCH_PAGEFIND_ADAPTER_OUTPUT_PATH = "_zeropress/search_pagefind.js";
60910
60915
  var OUTPUT_PATH_CONTROL_CHAR_PATTERN = /[\u0000-\u001F\u007F]/;
60911
60916
  var SAFE_MEDIA_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
60912
60917
  var SAFE_LINK_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:", "mailto:", "tel:"]);
@@ -60964,9 +60969,12 @@ async function buildSite(input2) {
60964
60969
  state.commentPolicyContent,
60965
60970
  "application/json"
60966
60971
  );
60967
- if (options2.generateSpecialFiles) {
60972
+ if (shouldGenerateSearchArtifacts(state, options2)) {
60968
60973
  await writeOutput(state.writer, state.summaries, SEARCH_INDEX_OUTPUT_PATH, buildSearchIndexJson(state), "application/json");
60969
60974
  await writeOutput(state.writer, state.summaries, SEARCH_ADAPTER_OUTPUT_PATH, buildSearchAdapterJs(), "application/javascript");
60975
+ await writeOutput(state.writer, state.summaries, SEARCH_PAGEFIND_ADAPTER_OUTPUT_PATH, buildSearchPagefindAdapterJs(), "application/javascript");
60976
+ }
60977
+ if (options2.generateSpecialFiles) {
60970
60978
  await maybeRenderNotFoundPage(state);
60971
60979
  if (hasCanonicalSiteUrl(state.previewData.site.url)) {
60972
60980
  await writeOutput(
@@ -61286,6 +61294,7 @@ function normalizePreviewData(previewData, options2 = {}) {
61286
61294
  locale: normalizeLocale(previewData.site.locale || DEFAULT_LOCALE),
61287
61295
  disallow_comments: previewData.site.disallow_comments === true,
61288
61296
  expose_generator: previewData.site.expose_generator !== false,
61297
+ search: previewData.site.search !== false,
61289
61298
  indexing: previewData.site.indexing !== false,
61290
61299
  permalinks: normalizePermalinks(previewData.site.permalinks),
61291
61300
  front_page: normalizeFrontPage(previewData.site.front_page),
@@ -61567,6 +61576,8 @@ function normalizePostIndex(post_index) {
61567
61576
  function createRenderData(previewData, themeMetadata = {}) {
61568
61577
  const themeSupportsComments = themeMetadata?.features?.comments === true;
61569
61578
  const themeSupportsPostIndex = themeMetadata?.features?.post_index !== false;
61579
+ const themeSupportsSearch = themeMetadata?.features?.search === true;
61580
+ previewData.site.search = previewData.site.search !== false && themeSupportsSearch;
61570
61581
  const authorsById = new Map(previewData.content.authors.map((author) => [author.id, author]));
61571
61582
  const categoriesBySlug = new Map(previewData.content.categories.map((category) => [category.slug, category]));
61572
61583
  const tagsBySlug = new Map(previewData.content.tags.map((tag) => [tag.slug, tag]));
@@ -62922,8 +62933,10 @@ function assertPlannedOutputPathsSafe(state) {
62922
62933
  ...state.assetOutputs.map((assetOutput) => assetOutput.path),
62923
62934
  COMMENT_POLICY_OUTPUT_PATH
62924
62935
  ];
62936
+ if (shouldGenerateSearchArtifacts(state, state.options)) {
62937
+ plannedPaths.push(SEARCH_INDEX_OUTPUT_PATH, SEARCH_ADAPTER_OUTPUT_PATH, SEARCH_PAGEFIND_ADAPTER_OUTPUT_PATH);
62938
+ }
62925
62939
  if (state.options.generateSpecialFiles) {
62926
- plannedPaths.push(SEARCH_INDEX_OUTPUT_PATH, SEARCH_ADAPTER_OUTPUT_PATH);
62927
62940
  plannedPaths.push("404.html");
62928
62941
  if (shouldGenerateRobotsTxt(state.options)) {
62929
62942
  plannedPaths.push("robots.txt");
@@ -63436,6 +63449,87 @@ function normalizeLimit(value) {
63436
63449
  }
63437
63450
  `;
63438
63451
  }
63452
+ function buildSearchPagefindAdapterJs() {
63453
+ return `let pagefindPromise;
63454
+
63455
+ export async function preload() {
63456
+ if (!pagefindPromise) {
63457
+ pagefindPromise = import(new URL('./pagefind/pagefind.js', import.meta.url).href).then(async (pagefind) => {
63458
+ if (typeof pagefind.options === 'function') {
63459
+ await pagefind.options({ baseUrl: '/' });
63460
+ }
63461
+ return pagefind;
63462
+ });
63463
+ }
63464
+
63465
+ return pagefindPromise;
63466
+ }
63467
+
63468
+ export async function search(query, options = {}) {
63469
+ const pagefind = await preload();
63470
+ const result = await pagefind.search(query, options);
63471
+ const limit = normalizeLimit(options.limit);
63472
+ if (!Array.isArray(result?.results)) {
63473
+ return result;
63474
+ }
63475
+
63476
+ const results = result.results.map(normalizeResult);
63477
+ return {
63478
+ ...result,
63479
+ results: limit ? results.slice(0, limit) : results,
63480
+ };
63481
+ }
63482
+
63483
+ function normalizeResult(result) {
63484
+ if (!result || typeof result.data !== 'function') {
63485
+ return result;
63486
+ }
63487
+
63488
+ return {
63489
+ ...result,
63490
+ data: async () => normalizeResultData(await result.data()),
63491
+ };
63492
+ }
63493
+
63494
+ function normalizeResultData(data) {
63495
+ if (!data || typeof data !== 'object') {
63496
+ return data;
63497
+ }
63498
+
63499
+ return {
63500
+ ...data,
63501
+ url: normalizeUrl(data.url),
63502
+ sub_results: Array.isArray(data.sub_results)
63503
+ ? data.sub_results.map((item) => ({ ...item, url: normalizeUrl(item.url) }))
63504
+ : data.sub_results,
63505
+ };
63506
+ }
63507
+
63508
+ function normalizeUrl(value) {
63509
+ const url = String(value || '');
63510
+ if (url.startsWith('/_zeropress/') && !url.startsWith('/_zeropress/pagefind/')) {
63511
+ return url.replace(/^\\/_zeropress/, '') || '/';
63512
+ }
63513
+ if (url.startsWith('_zeropress/') && !url.startsWith('_zeropress/pagefind/')) {
63514
+ return url.replace(/^_zeropress/, '') || '/';
63515
+ }
63516
+ return url;
63517
+ }
63518
+
63519
+ function normalizeLimit(value) {
63520
+ if (value === undefined || value === null) {
63521
+ return null;
63522
+ }
63523
+
63524
+ const limit = Number(value);
63525
+ if (!Number.isFinite(limit) || limit <= 0) {
63526
+ return null;
63527
+ }
63528
+
63529
+ return Math.floor(limit);
63530
+ }
63531
+ `;
63532
+ }
63439
63533
  function buildSitemapXml(site, emitted, generatedAt, stylesheetHref = "") {
63440
63534
  const entries = [
63441
63535
  ...emitted.frontPage && emitted.frontPage.includeInSitemap !== false ? [{
@@ -63521,6 +63615,9 @@ function buildRobotsTxt(site) {
63521
63615
  function shouldGenerateRobotsTxt(options2) {
63522
63616
  return options2.generateSpecialFiles && options2.generateRobotsTxt !== false;
63523
63617
  }
63618
+ function shouldGenerateSearchArtifacts(state, options2) {
63619
+ return options2.generateSpecialFiles && state.previewData.site.search === true;
63620
+ }
63524
63621
  function getContentType(assetPath) {
63525
63622
  const ext = assetPath.split(".").pop()?.toLowerCase();
63526
63623
  const contentTypes = {
@@ -64017,6 +64114,8 @@ async function runBuildPages(options2) {
64017
64114
  const cwd = path3.resolve(options2.cwd || process.cwd());
64018
64115
  const copyMarkdownSource = options2.copyMarkdownSource !== false;
64019
64116
  const sourceDir = path3.resolve(cwd, options2.source);
64117
+ const publicDirExplicit = hasExplicitPublicDir(options2);
64118
+ const publicDir = publicDirExplicit ? path3.resolve(cwd, options2.publicDir) : sourceDir;
64020
64119
  const destinationDir = path3.resolve(cwd, options2.destination);
64021
64120
  const generatedDir = path3.join(cwd, ".zeropress");
64022
64121
  const stagingDir = path3.join(cwd, STAGING_DIR);
@@ -64025,17 +64124,22 @@ async function runBuildPages(options2) {
64025
64124
  assertBuildPagesPathLayout({
64026
64125
  cwd,
64027
64126
  sourceDir,
64127
+ publicDir,
64128
+ publicDirExplicit,
64028
64129
  destinationDir,
64029
64130
  themeDir,
64030
64131
  generatedDir
64031
64132
  });
64032
64133
  await assertDirectory(sourceDir, "Source directory");
64134
+ await assertPublicDirectory(publicDir, publicDirExplicit);
64135
+ await assertDestinationPath(destinationDir);
64033
64136
  await fs3.rm(generatedDir, { recursive: true, force: true });
64034
64137
  await fs3.mkdir(generatedDir, { recursive: true });
64035
64138
  const env = {
64036
64139
  ...process.env,
64037
64140
  ZEROPRESS_BUILD_PAGES_SOURCE: sourceDir,
64038
- ZEROPRESS_PUBLIC_DIR: sourceDir,
64141
+ ZEROPRESS_BUILD_PAGES_PUBLIC_DIR: publicDir,
64142
+ ZEROPRESS_PUBLIC_DIR: publicDir,
64039
64143
  ZEROPRESS_SKIP_UNTITLED_MARKDOWN: String(Boolean(options2.skipUntitledMarkdown)),
64040
64144
  ZEROPRESS_COPY_MARKDOWN_SOURCE: String(copyMarkdownSource)
64041
64145
  };
@@ -64059,10 +64163,13 @@ async function runBuildPages(options2) {
64059
64163
  await fs3.rm(destinationDir, { recursive: true, force: true });
64060
64164
  await fs3.rm(stagingDir, { recursive: true, force: true });
64061
64165
  await fs3.mkdir(stagingDir, { recursive: true });
64062
- await copyPublicStaging(sourceDir, stagingDir, {
64166
+ await copyPublicStaging(publicDir, stagingDir, {
64063
64167
  excludePaths: [destinationDir, themeDir, generatedDir],
64064
64168
  copyMarkdownSource
64065
64169
  });
64170
+ if (copyMarkdownSource) {
64171
+ await copySourceMarkdownFiles(sourceDir, stagingDir, previewData);
64172
+ }
64066
64173
  const previousPublicDir = process.env.ZEROPRESS_PUBLIC_DIR;
64067
64174
  process.env.ZEROPRESS_PUBLIC_DIR = stagingDir;
64068
64175
  try {
@@ -64097,6 +64204,9 @@ function resolveThemeDir(cwd, options2) {
64097
64204
  }
64098
64205
  throw new Error(`Unknown bundled theme: ${options2.theme}`);
64099
64206
  }
64207
+ function hasExplicitPublicDir(options2) {
64208
+ return typeof options2.publicDir === "string" && Boolean(options2.publicDir.trim());
64209
+ }
64100
64210
  async function assertDirectory(dir, label) {
64101
64211
  let stat;
64102
64212
  try {
@@ -64111,17 +64221,78 @@ async function assertDirectory(dir, label) {
64111
64221
  throw new Error(`${label} is not a directory: ${dir}`);
64112
64222
  }
64113
64223
  }
64114
- function assertBuildPagesPathLayout({ cwd, sourceDir, destinationDir, themeDir, generatedDir }) {
64224
+ async function assertPublicDirectory(publicDir, explicit) {
64225
+ if (!explicit) {
64226
+ return;
64227
+ }
64228
+ let stat;
64229
+ try {
64230
+ stat = await fs3.lstat(publicDir);
64231
+ } catch (error) {
64232
+ if (error?.code === "ENOENT") {
64233
+ throw new Error(`Public directory not found: ${publicDir}`);
64234
+ }
64235
+ throw error;
64236
+ }
64237
+ if (stat.isSymbolicLink()) {
64238
+ throw new Error(`Public directory must not be a symbolic link: ${publicDir}`);
64239
+ }
64240
+ if (!stat.isDirectory()) {
64241
+ throw new Error(`Public path is not a directory: ${publicDir}`);
64242
+ }
64243
+ }
64244
+ async function assertDestinationPath(destinationDir) {
64245
+ let stat;
64246
+ try {
64247
+ stat = await fs3.lstat(destinationDir);
64248
+ } catch (error) {
64249
+ if (error?.code === "ENOENT") {
64250
+ return;
64251
+ }
64252
+ throw error;
64253
+ }
64254
+ if (!stat.isDirectory()) {
64255
+ throw new Error(`Destination path is not a directory: ${destinationDir}`);
64256
+ }
64257
+ }
64258
+ function assertBuildPagesPathLayout({
64259
+ cwd,
64260
+ sourceDir,
64261
+ publicDir,
64262
+ publicDirExplicit,
64263
+ destinationDir,
64264
+ themeDir,
64265
+ generatedDir
64266
+ }) {
64115
64267
  if (samePath(sourceDir, cwd)) {
64116
64268
  throw new Error(
64117
64269
  `Source directory must be a dedicated content directory, not the current working directory. Received: ${formatPath(cwd, sourceDir)}`
64118
64270
  );
64119
64271
  }
64272
+ if (publicDirExplicit && samePath(publicDir, cwd)) {
64273
+ throw new Error(
64274
+ `Public directory must be a dedicated asset directory, not the current working directory. Received: ${formatPath(cwd, publicDir)}`
64275
+ );
64276
+ }
64120
64277
  assertNoPathOverlap(cwd, "Source directory", sourceDir, "internal .zeropress working directory", generatedDir);
64121
64278
  assertNoPathOverlap(cwd, "Destination directory", destinationDir, "internal .zeropress working directory", generatedDir);
64122
64279
  assertNoPathOverlap(cwd, "Theme directory", themeDir, "internal .zeropress working directory", generatedDir);
64280
+ if (!samePath(publicDir, sourceDir)) {
64281
+ assertNoPathOverlap(cwd, "Public directory", publicDir, "internal .zeropress working directory", generatedDir);
64282
+ assertNoPathOverlap(cwd, "Public directory", publicDir, "destination directory", destinationDir);
64283
+ assertNoPathOverlap(cwd, "Public directory", publicDir, "theme directory", themeDir);
64284
+ }
64123
64285
  assertNoPathOverlap(cwd, "Source directory", sourceDir, "destination directory", destinationDir);
64124
64286
  assertNoPathOverlap(cwd, "Source directory", sourceDir, "theme directory", themeDir);
64287
+ assertSourceIsNotInsidePublicDirectory(cwd, sourceDir, publicDir);
64288
+ }
64289
+ function assertSourceIsNotInsidePublicDirectory(cwd, sourceDir, publicDir) {
64290
+ if (samePath(sourceDir, publicDir) || !isPathInside2(publicDir, sourceDir)) {
64291
+ return;
64292
+ }
64293
+ throw new Error(
64294
+ `Source directory must not be inside the public directory. Source directory: ${formatPath(cwd, sourceDir)}; Public directory: ${formatPath(cwd, publicDir)}`
64295
+ );
64125
64296
  }
64126
64297
  function assertNoPathOverlap(cwd, firstLabel, firstPath, secondLabel, secondPath) {
64127
64298
  if (!pathsOverlap2(firstPath, secondPath)) {
@@ -64157,6 +64328,52 @@ async function copyPublicStaging(sourceDir, targetDir, options2) {
64157
64328
  await fs3.copyFile(sourcePath, targetPath);
64158
64329
  }
64159
64330
  }
64331
+ async function copySourceMarkdownFiles(sourceDir, targetDir, previewData) {
64332
+ const markdownUrls = /* @__PURE__ */ new Set();
64333
+ for (const page of previewData?.content?.pages || []) {
64334
+ const sourceMarkdownUrl = page?.meta?.source_markdown_url;
64335
+ if (typeof sourceMarkdownUrl === "string" && sourceMarkdownUrl) {
64336
+ markdownUrls.add(sourceMarkdownUrl);
64337
+ }
64338
+ }
64339
+ for (const sourceMarkdownUrl of markdownUrls) {
64340
+ const relativePath = sourceMarkdownUrlToRelativePath(sourceMarkdownUrl);
64341
+ if (!relativePath) {
64342
+ continue;
64343
+ }
64344
+ const sourcePath = path3.join(sourceDir, relativePath);
64345
+ if (!isPathInside2(sourceDir, sourcePath)) {
64346
+ continue;
64347
+ }
64348
+ const targetPath = path3.join(targetDir, relativePath);
64349
+ await fs3.mkdir(path3.dirname(targetPath), { recursive: true });
64350
+ await fs3.copyFile(sourcePath, targetPath);
64351
+ }
64352
+ }
64353
+ function sourceMarkdownUrlToRelativePath(sourceMarkdownUrl) {
64354
+ if (!sourceMarkdownUrl.startsWith("/") || sourceMarkdownUrl.includes("?") || sourceMarkdownUrl.includes("#")) {
64355
+ return "";
64356
+ }
64357
+ const rawSegments = sourceMarkdownUrl.slice(1).split("/");
64358
+ const segments = [];
64359
+ for (const rawSegment of rawSegments) {
64360
+ if (!rawSegment) {
64361
+ return "";
64362
+ }
64363
+ let segment;
64364
+ try {
64365
+ segment = decodeURIComponent(rawSegment);
64366
+ } catch {
64367
+ return "";
64368
+ }
64369
+ if (!segment || segment === "." || segment === ".." || segment.includes("/") || segment.includes("\\")) {
64370
+ return "";
64371
+ }
64372
+ segments.push(segment);
64373
+ }
64374
+ const relativePath = segments.join("/");
64375
+ return relativePath.toLowerCase().endsWith(".md") ? relativePath : "";
64376
+ }
64160
64377
  function shouldIgnorePublicEntry2(name) {
64161
64378
  const basename = String(name || "");
64162
64379
  const lowerName = basename.toLowerCase();
@@ -64185,6 +64402,7 @@ function formatPath(cwd, targetPath) {
64185
64402
  // src/action.js
64186
64403
  var options = {
64187
64404
  source: input("source") || "./docs",
64405
+ publicDir: input("public-dir"),
64188
64406
  destination: input("destination") || "./_site",
64189
64407
  theme: input("theme") || "docs",
64190
64408
  themePath: input("theme-path"),
package/dist/prebuild.js CHANGED
@@ -3524,6 +3524,7 @@ import { fileURLToPath } from "node:url";
3524
3524
  var __dirname = path.dirname(fileURLToPath(import.meta.url));
3525
3525
  var rootDir = process.cwd();
3526
3526
  var sourceDir = resolveEnvPath(["ZEROPRESS_BUILD_PAGES_SOURCE"], "docs");
3527
+ var publicDir = resolveEnvPath(["ZEROPRESS_BUILD_PAGES_PUBLIC_DIR"], sourceDir);
3527
3528
  var defaultConfigPath = path.join(sourceDir, ".zeropress", "config.json");
3528
3529
  var configPath = resolveOptionalEnvPath(["ZEROPRESS_BUILD_PAGES_CONFIG"], defaultConfigPath);
3529
3530
  var outDir = path.join(rootDir, ".zeropress");
@@ -3540,6 +3541,7 @@ var FRONT_MATTER_DATA_MAX_DEPTH = 4;
3540
3541
  var FRONT_MATTER_DATA_MAX_KEYS = 64;
3541
3542
  var FRONT_MATTER_DATA_MAX_ARRAY_LENGTH = 256;
3542
3543
  var FRONT_MATTER_DISCOVERABILITY_VALUES = /* @__PURE__ */ new Set(["default", "noindex", "delist"]);
3544
+ var markdownDiscoverExcludeRoots = buildMarkdownDiscoverExcludeRoots();
3543
3545
  var PrebuildMarkdownError = class extends Error {
3544
3546
  constructor(sourcePath, reason, expected = "", code = "invalid_markdown") {
3545
3547
  super(reason);
@@ -3740,6 +3742,7 @@ function buildSiteData(config, frontPage) {
3740
3742
  },
3741
3743
  disallow_comments: true,
3742
3744
  expose_generator: configuredSite.expose_generator !== false,
3745
+ search: configuredSite.search !== false,
3743
3746
  indexing: configuredSite.indexing !== false
3744
3747
  };
3745
3748
  if (configuredSite.footer) {
@@ -3768,12 +3771,13 @@ function normalizeSiteConfig(value) {
3768
3771
  );
3769
3772
  }
3770
3773
  const configuredSite = isPlainObject(value) ? value : {};
3771
- assertKnownConfigKeys(configuredSite, ["title", "description", "url", "expose_generator", "indexing", "footer"], "site");
3774
+ assertKnownConfigKeys(configuredSite, ["title", "description", "url", "expose_generator", "search", "indexing", "footer"], "site");
3772
3775
  const site = {
3773
3776
  title: readConfigString(configuredSite.title, "Documentation"),
3774
3777
  description: readConfigString(configuredSite.description, "A documentation site."),
3775
3778
  url: readEnv("ZEROPRESS_SITE_URL", readConfigString(configuredSite.url, "")),
3776
3779
  expose_generator: readConfigBoolean(configuredSite.expose_generator, true, "site.expose_generator"),
3780
+ search: readConfigBoolean(configuredSite.search, true, "site.search"),
3777
3781
  indexing: readConfigBoolean(configuredSite.indexing, true, "site.indexing")
3778
3782
  };
3779
3783
  const footer = normalizeFooter(configuredSite.footer);
@@ -4184,6 +4188,7 @@ function buildPrebuildReport({
4184
4188
  return {
4185
4189
  generated_at: (/* @__PURE__ */ new Date()).toISOString(),
4186
4190
  source_dir: formatSourcePath(sourceDir),
4191
+ public_dir: formatSourcePath(publicDir),
4187
4192
  config_path: formatSourcePath(configPath),
4188
4193
  build_pages_config_path: formatSourcePath(buildPagesConfigPath),
4189
4194
  preview_data_path: formatSourcePath(previewDataPath),
@@ -4209,7 +4214,8 @@ function buildPrebuildReport({
4209
4214
  function printPrebuildSummary(report) {
4210
4215
  const lines = [
4211
4216
  "ZeroPress build report",
4212
- `- Public root: ${report.source_dir}`,
4217
+ `- Source root: ${report.source_dir}`,
4218
+ `- Public root: ${report.public_dir}`,
4213
4219
  `- Markdown discovered: ${report.markdown.discovered}`,
4214
4220
  `- Markdown pages generated: ${report.markdown.generated_pages}`,
4215
4221
  `- Markdown skipped: ${report.markdown.skipped}`,
@@ -4497,6 +4503,9 @@ async function listMarkdownFiles(dir) {
4497
4503
  continue;
4498
4504
  }
4499
4505
  const entryPath = path.join(dir, entry.name);
4506
+ if (isMarkdownDiscoverExcluded(entryPath)) {
4507
+ continue;
4508
+ }
4500
4509
  if (entry.isDirectory()) {
4501
4510
  files.push(...await listMarkdownFiles(entryPath));
4502
4511
  continue;
@@ -4507,6 +4516,18 @@ async function listMarkdownFiles(dir) {
4507
4516
  }
4508
4517
  return files.sort((left, right) => left.localeCompare(right));
4509
4518
  }
4519
+ function buildMarkdownDiscoverExcludeRoots() {
4520
+ if (samePath(sourceDir, publicDir) || !isPathInside(sourceDir, publicDir)) {
4521
+ return [];
4522
+ }
4523
+ return [publicDir];
4524
+ }
4525
+ function isMarkdownDiscoverExcluded(entryPath) {
4526
+ return markdownDiscoverExcludeRoots.some((excludeRoot) => samePath(entryPath, excludeRoot) || isPathInside(excludeRoot, entryPath));
4527
+ }
4528
+ function samePath(firstPath, secondPath) {
4529
+ return path.resolve(firstPath) === path.resolve(secondPath);
4530
+ }
4510
4531
  function shouldIgnoreMarkdownDiscoverEntry(name) {
4511
4532
  const basename = String(name || "");
4512
4533
  const lowerName = basename.toLowerCase();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeropress/build-pages",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
4
4
  "description": "ZeroPress Markdown build action and CLI for static hosting",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -40,7 +40,7 @@
40
40
  "node": ">=18.18.0"
41
41
  },
42
42
  "dependencies": {
43
- "@zeropress/build": "0.6.1",
43
+ "@zeropress/build": "0.6.2",
44
44
  "gray-matter": "4.0.3"
45
45
  },
46
46
  "devDependencies": {