@zeropress/build-pages 0.5.6 → 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/src/prebuild.js CHANGED
@@ -6,6 +6,7 @@ import matter from 'gray-matter';
6
6
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
7
  const rootDir = process.cwd();
8
8
  const sourceDir = resolveEnvPath(['ZEROPRESS_BUILD_PAGES_SOURCE'], 'docs');
9
+ const publicDir = resolveEnvPath(['ZEROPRESS_BUILD_PAGES_PUBLIC_DIR'], sourceDir);
9
10
  const defaultConfigPath = path.join(sourceDir, '.zeropress', 'config.json');
10
11
  const configPath = resolveOptionalEnvPath(['ZEROPRESS_BUILD_PAGES_CONFIG'], defaultConfigPath);
11
12
  const outDir = path.join(rootDir, '.zeropress');
@@ -16,7 +17,13 @@ const skipUntitledMarkdown = readBooleanEnv('ZEROPRESS_SKIP_UNTITLED_MARKDOWN');
16
17
  const copyMarkdownSource = readBooleanEnv('ZEROPRESS_COPY_MARKDOWN_SOURCE', true);
17
18
  const FRONT_PAGE_TYPES = new Set(['theme_index', 'markdown', 'html']);
18
19
  const BUILD_PAGES_CONFIG_SCHEMA_URL = 'https://zeropress.dev/schemas/zeropress-build-pages.config.v0.1.schema.json';
19
- const PREVIEW_DATA_SCHEMA_URL = 'https://zeropress.dev/schemas/preview-data.v0.5.schema.json';
20
+ const PREVIEW_DATA_SCHEMA_URL = 'https://zeropress.dev/schemas/preview-data.v0.6.schema.json';
21
+ const FRONT_MATTER_DATA_KEY_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*(?:-[a-zA-Z0-9_]+)*$/;
22
+ const FRONT_MATTER_DATA_MAX_DEPTH = 4;
23
+ const FRONT_MATTER_DATA_MAX_KEYS = 64;
24
+ const FRONT_MATTER_DATA_MAX_ARRAY_LENGTH = 256;
25
+ const FRONT_MATTER_DISCOVERABILITY_VALUES = new Set(['default', 'noindex', 'delist']);
26
+ const markdownDiscoverExcludeRoots = buildMarkdownDiscoverExcludeRoots();
20
27
 
21
28
  class PrebuildMarkdownError extends Error {
22
29
  constructor(sourcePath, reason, expected = '', code = 'invalid_markdown') {
@@ -99,6 +106,8 @@ async function main() {
99
106
  ...frontMatter.meta,
100
107
  ...(copyMarkdownSource ? { source_markdown_url: buildSourceMarkdownUrl(sourcePath) } : {}),
101
108
  },
109
+ ...(frontMatter.data !== undefined ? { data: frontMatter.data } : {}),
110
+ ...(frontMatter.discoverability !== 'default' ? { discoverability: frontMatter.discoverability } : {}),
102
111
  content: rewriteMarkdownLinks(bodyMarkdown, sourcePath, routeBySourcePath),
103
112
  document_type: 'markdown',
104
113
  excerpt: frontMatter.description || extractExcerpt(bodyMarkdown, title),
@@ -115,7 +124,7 @@ async function main() {
115
124
 
116
125
  const previewData = {
117
126
  $schema: PREVIEW_DATA_SCHEMA_URL,
118
- version: '0.5',
127
+ version: '0.6',
119
128
  generator: 'zeropress-build-pages',
120
129
  generated_at: new Date().toISOString(),
121
130
  site,
@@ -222,18 +231,21 @@ function buildSiteData(config, frontPage) {
222
231
  title: configuredSite.title,
223
232
  description: configuredSite.description,
224
233
  url: configuredSite.url,
225
- mediaBaseUrl: '',
234
+ media_base_url: '',
226
235
  locale: 'en-US',
227
- postsPerPage: 10,
228
- dateFormat: 'YYYY-MM-DD',
229
- timeFormat: 'HH:mm',
236
+ posts_per_page: 10,
237
+ datetime_display: 'static',
238
+ date_style: 'medium',
239
+ time_style: 'none',
230
240
  timezone: 'UTC',
231
241
  permalinks: defaultPermalinks(),
232
242
  front_page: frontPage,
233
243
  post_index: {
234
244
  enabled: false,
235
245
  },
236
- disallowComments: true,
246
+ disallow_comments: true,
247
+ expose_generator: configuredSite.expose_generator !== false,
248
+ search: configuredSite.search !== false,
237
249
  indexing: configuredSite.indexing !== false,
238
250
  };
239
251
 
@@ -269,11 +281,13 @@ function normalizeSiteConfig(value) {
269
281
  }
270
282
 
271
283
  const configuredSite = isPlainObject(value) ? value : {};
272
- assertKnownConfigKeys(configuredSite, ['title', 'description', 'url', 'indexing', 'footer'], 'site');
284
+ assertKnownConfigKeys(configuredSite, ['title', 'description', 'url', 'expose_generator', 'search', 'indexing', 'footer'], 'site');
273
285
  const site = {
274
286
  title: readConfigString(configuredSite.title, 'Documentation'),
275
287
  description: readConfigString(configuredSite.description, 'A documentation site.'),
276
288
  url: readEnv('ZEROPRESS_SITE_URL', readConfigString(configuredSite.url, '')),
289
+ expose_generator: readConfigBoolean(configuredSite.expose_generator, true, 'site.expose_generator'),
290
+ search: readConfigBoolean(configuredSite.search, true, 'site.search'),
277
291
  indexing: readConfigBoolean(configuredSite.indexing, true, 'site.indexing'),
278
292
  };
279
293
 
@@ -300,19 +314,11 @@ function normalizeFooter(value) {
300
314
  footer.copyright_text = copyrightText;
301
315
  }
302
316
 
303
- if (value.attribution !== undefined && !isPlainObject(value.attribution)) {
304
- throw new PrebuildConfigError('site.footer.attribution must be an object.');
305
- }
306
- if (isPlainObject(value.attribution)) {
307
- assertKnownConfigKeys(value.attribution, ['enabled'], 'site.footer.attribution');
308
- if (value.attribution.enabled !== undefined && typeof value.attribution.enabled !== 'boolean') {
309
- throw new PrebuildConfigError('site.footer.attribution.enabled must be a boolean when provided.');
317
+ if (value.attribution !== undefined) {
318
+ if (typeof value.attribution !== 'boolean') {
319
+ throw new PrebuildConfigError('site.footer.attribution must be a boolean when provided.');
310
320
  }
311
- }
312
- if (isPlainObject(value.attribution) && typeof value.attribution.enabled === 'boolean') {
313
- footer.attribution = {
314
- enabled: value.attribution.enabled,
315
- };
321
+ footer.attribution = value.attribution;
316
322
  }
317
323
 
318
324
  return Object.keys(footer).length ? footer : undefined;
@@ -712,12 +718,29 @@ function normalizeMenuItem(item, pathLabel) {
712
718
  url,
713
719
  type: readConfigString(item.type, 'custom'),
714
720
  target: readConfigString(item.target, '_self'),
721
+ ...(item.meta !== undefined ? { meta: normalizeMenuItemMeta(item.meta, `${pathLabel}.meta`) } : {}),
715
722
  children: Array.isArray(item.children)
716
723
  ? item.children.map((child, index) => normalizeMenuItem(child, `${pathLabel}.children[${index}]`))
717
724
  : [],
718
725
  };
719
726
  }
720
727
 
728
+ function normalizeMenuItemMeta(value, pathLabel) {
729
+ if (!isPlainObject(value)) {
730
+ throw new PrebuildConfigError(`${pathLabel} must be an object when provided.`);
731
+ }
732
+
733
+ const meta = {};
734
+ for (const [key, metaValue] of Object.entries(value)) {
735
+ if (!isPreviewMetaValue(metaValue)) {
736
+ throw new PrebuildConfigError(`${pathLabel}.${key} must be a string, number, boolean, or null.`);
737
+ }
738
+ meta[key] = metaValue;
739
+ }
740
+
741
+ return meta;
742
+ }
743
+
721
744
  function defaultMenus() {
722
745
  return {
723
746
  primary: {
@@ -741,6 +764,7 @@ function buildPrebuildReport({
741
764
  return {
742
765
  generated_at: new Date().toISOString(),
743
766
  source_dir: formatSourcePath(sourceDir),
767
+ public_dir: formatSourcePath(publicDir),
744
768
  config_path: formatSourcePath(configPath),
745
769
  build_pages_config_path: formatSourcePath(buildPagesConfigPath),
746
770
  preview_data_path: formatSourcePath(previewDataPath),
@@ -767,7 +791,8 @@ function buildPrebuildReport({
767
791
  function printPrebuildSummary(report) {
768
792
  const lines = [
769
793
  'ZeroPress build report',
770
- `- Public root: ${report.source_dir}`,
794
+ `- Source root: ${report.source_dir}`,
795
+ `- Public root: ${report.public_dir}`,
771
796
  `- Markdown discovered: ${report.markdown.discovered}`,
772
797
  `- Markdown pages generated: ${report.markdown.generated_pages}`,
773
798
  `- Markdown skipped: ${report.markdown.skipped}`,
@@ -844,7 +869,9 @@ function normalizePublishedFrontMatter(frontMatter, sourcePath) {
844
869
  title: normalizeFrontMatterTitle(frontMatter.title, sourcePath),
845
870
  description: normalizeFrontMatterDescription(frontMatter.description, sourcePath),
846
871
  path: normalizeFrontMatterRoutePath(frontMatter.path, sourcePath),
872
+ discoverability: normalizeFrontMatterDiscoverability(frontMatter.discoverability, sourcePath),
847
873
  meta: normalizeFrontMatterMeta(frontMatter.meta, sourcePath),
874
+ data: normalizeFrontMatterData(frontMatter.data, sourcePath),
848
875
  };
849
876
  }
850
877
 
@@ -900,13 +927,28 @@ function normalizeFrontMatterRoutePath(value, sourcePath) {
900
927
  throw new PrebuildMarkdownError(
901
928
  sourcePath,
902
929
  'front matter path must be a safe generated route path.',
903
- ' path: guides/install\n path: spec/preview-data-v0.5',
930
+ ' path: guides/install\n path: spec/preview-data-v0.6',
904
931
  );
905
932
  }
906
933
 
907
934
  return routePath;
908
935
  }
909
936
 
937
+ function normalizeFrontMatterDiscoverability(value, sourcePath) {
938
+ if (value === undefined) {
939
+ return 'default';
940
+ }
941
+ if (typeof value === 'string' && FRONT_MATTER_DISCOVERABILITY_VALUES.has(value)) {
942
+ return value;
943
+ }
944
+
945
+ throw new PrebuildMarkdownError(
946
+ sourcePath,
947
+ `front matter discoverability must be one of: ${Array.from(FRONT_MATTER_DISCOVERABILITY_VALUES).join(', ')}.`,
948
+ ' discoverability: default\n discoverability: noindex\n discoverability: delist',
949
+ );
950
+ }
951
+
910
952
  function isSafeRoutePathSegment(segment) {
911
953
  return (
912
954
  /^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/.test(segment)
@@ -943,11 +985,102 @@ function isPreviewMetaValue(value) {
943
985
  return (
944
986
  value === null
945
987
  || typeof value === 'string'
946
- || typeof value === 'number'
988
+ || (typeof value === 'number' && Number.isFinite(value))
947
989
  || typeof value === 'boolean'
948
990
  );
949
991
  }
950
992
 
993
+ function normalizeFrontMatterData(value, sourcePath) {
994
+ if (value === undefined) {
995
+ return undefined;
996
+ }
997
+ if (!isPlainObject(value)) {
998
+ throw new PrebuildMarkdownError(
999
+ sourcePath,
1000
+ 'front matter data must be an object when provided.',
1001
+ );
1002
+ }
1003
+
1004
+ validateFrontMatterDataObject(value, sourcePath, 'data', 0);
1005
+ return value;
1006
+ }
1007
+
1008
+ function validateFrontMatterDataValue(value, sourcePath, pathLabel, depth) {
1009
+ if (value === null || typeof value === 'string' || typeof value === 'boolean') {
1010
+ return;
1011
+ }
1012
+ if (typeof value === 'number') {
1013
+ if (!Number.isFinite(value)) {
1014
+ throw new PrebuildMarkdownError(
1015
+ sourcePath,
1016
+ `front matter ${pathLabel} must be a finite number.`,
1017
+ );
1018
+ }
1019
+ return;
1020
+ }
1021
+ if (Array.isArray(value)) {
1022
+ validateFrontMatterDataArray(value, sourcePath, pathLabel, depth);
1023
+ return;
1024
+ }
1025
+ if (isPlainObject(value)) {
1026
+ validateFrontMatterDataObject(value, sourcePath, pathLabel, depth);
1027
+ return;
1028
+ }
1029
+
1030
+ throw new PrebuildMarkdownError(
1031
+ sourcePath,
1032
+ `front matter ${pathLabel} must be JSON-safe structured data.`,
1033
+ );
1034
+ }
1035
+
1036
+ function validateFrontMatterDataObject(object, sourcePath, pathLabel, depth) {
1037
+ if (depth > FRONT_MATTER_DATA_MAX_DEPTH) {
1038
+ throw new PrebuildMarkdownError(
1039
+ sourcePath,
1040
+ `front matter ${pathLabel} nesting must not exceed ${FRONT_MATTER_DATA_MAX_DEPTH} container levels.`,
1041
+ );
1042
+ }
1043
+
1044
+ const entries = Object.entries(object);
1045
+ if (entries.length > FRONT_MATTER_DATA_MAX_KEYS) {
1046
+ throw new PrebuildMarkdownError(
1047
+ sourcePath,
1048
+ `front matter ${pathLabel} must not contain more than ${FRONT_MATTER_DATA_MAX_KEYS} keys.`,
1049
+ );
1050
+ }
1051
+
1052
+ for (const [key, dataValue] of entries) {
1053
+ const childLabel = `${pathLabel}.${key}`;
1054
+ if (!FRONT_MATTER_DATA_KEY_PATTERN.test(key)) {
1055
+ throw new PrebuildMarkdownError(
1056
+ sourcePath,
1057
+ `front matter ${childLabel} uses an invalid key.`,
1058
+ );
1059
+ }
1060
+ validateFrontMatterDataValue(dataValue, sourcePath, childLabel, depth + 1);
1061
+ }
1062
+ }
1063
+
1064
+ function validateFrontMatterDataArray(array, sourcePath, pathLabel, depth) {
1065
+ if (depth > FRONT_MATTER_DATA_MAX_DEPTH) {
1066
+ throw new PrebuildMarkdownError(
1067
+ sourcePath,
1068
+ `front matter ${pathLabel} nesting must not exceed ${FRONT_MATTER_DATA_MAX_DEPTH} container levels.`,
1069
+ );
1070
+ }
1071
+
1072
+ if (array.length > FRONT_MATTER_DATA_MAX_ARRAY_LENGTH) {
1073
+ throw new PrebuildMarkdownError(
1074
+ sourcePath,
1075
+ `front matter ${pathLabel} must not contain more than ${FRONT_MATTER_DATA_MAX_ARRAY_LENGTH} items.`,
1076
+ );
1077
+ }
1078
+
1079
+ array.forEach((dataValue, index) => {
1080
+ validateFrontMatterDataValue(dataValue, sourcePath, `${pathLabel}[${index}]`, depth + 1);
1081
+ });
1082
+ }
1083
+
951
1084
  function formatFrontMatterValue(value) {
952
1085
  if (typeof value === 'string') {
953
1086
  return `"${value}"`;
@@ -1007,6 +1140,10 @@ async function listMarkdownFiles(dir) {
1007
1140
  }
1008
1141
 
1009
1142
  const entryPath = path.join(dir, entry.name);
1143
+ if (isMarkdownDiscoverExcluded(entryPath)) {
1144
+ continue;
1145
+ }
1146
+
1010
1147
  if (entry.isDirectory()) {
1011
1148
  files.push(...await listMarkdownFiles(entryPath));
1012
1149
  continue;
@@ -1020,6 +1157,24 @@ async function listMarkdownFiles(dir) {
1020
1157
  return files.sort((left, right) => left.localeCompare(right));
1021
1158
  }
1022
1159
 
1160
+ function buildMarkdownDiscoverExcludeRoots() {
1161
+ if (samePath(sourceDir, publicDir) || !isPathInside(sourceDir, publicDir)) {
1162
+ return [];
1163
+ }
1164
+
1165
+ return [publicDir];
1166
+ }
1167
+
1168
+ function isMarkdownDiscoverExcluded(entryPath) {
1169
+ return markdownDiscoverExcludeRoots.some((excludeRoot) => (
1170
+ samePath(entryPath, excludeRoot) || isPathInside(excludeRoot, entryPath)
1171
+ ));
1172
+ }
1173
+
1174
+ function samePath(firstPath, secondPath) {
1175
+ return path.resolve(firstPath) === path.resolve(secondPath);
1176
+ }
1177
+
1023
1178
  function shouldIgnoreMarkdownDiscoverEntry(name) {
1024
1179
  const basename = String(name || '');
1025
1180
  const lowerName = basename.toLowerCase();
@@ -12,6 +12,15 @@
12
12
  --prose-ink: #344054;
13
13
  --pre-bg: #101828;
14
14
  --pre-ink: #f8fafc;
15
+ --syntax-comment: #98a2b3;
16
+ --syntax-keyword: #f472b6;
17
+ --syntax-title: #93c5fd;
18
+ --syntax-string: #86efac;
19
+ --syntax-number: #fbbf24;
20
+ --syntax-attr: #67e8f9;
21
+ --syntax-built-in: #c4b5fd;
22
+ --syntax-deletion: #fca5a5;
23
+ --syntax-addition: #bbf7d0;
15
24
  --button-ink: #ffffff;
16
25
  --alert-note: #2563eb;
17
26
  --alert-tip: #059669;
@@ -99,12 +108,31 @@ a {
99
108
  gap: 1.5rem;
100
109
  }
101
110
 
111
+ .visually-hidden {
112
+ position: absolute;
113
+ width: 1px;
114
+ height: 1px;
115
+ padding: 0;
116
+ overflow: hidden;
117
+ clip: rect(0, 0, 0, 0);
118
+ white-space: nowrap;
119
+ border: 0;
120
+ }
121
+
102
122
  .brand {
103
123
  color: var(--ink);
104
124
  font-weight: 750;
105
125
  text-decoration: none;
106
126
  }
107
127
 
128
+ .site-header__actions {
129
+ display: flex;
130
+ align-items: center;
131
+ justify-content: flex-end;
132
+ gap: 1rem;
133
+ min-width: 0;
134
+ }
135
+
108
136
  .site-nav ul {
109
137
  list-style: none;
110
138
  margin: 0;
@@ -129,6 +157,109 @@ a {
129
157
  color: var(--accent);
130
158
  }
131
159
 
160
+ .site-search {
161
+ position: relative;
162
+ width: min(22rem, 34vw);
163
+ }
164
+
165
+ .site-search__form {
166
+ display: flex;
167
+ align-items: center;
168
+ gap: 0.4rem;
169
+ }
170
+
171
+ .site-search__input {
172
+ min-width: 0;
173
+ width: 100%;
174
+ height: 2.35rem;
175
+ padding: 0 0.75rem;
176
+ border: 1px solid var(--line);
177
+ border-radius: 6px;
178
+ background: var(--surface);
179
+ color: var(--ink);
180
+ font: inherit;
181
+ font-size: 0.92rem;
182
+ }
183
+
184
+ .site-search__input:focus {
185
+ border-color: var(--accent);
186
+ outline: 2px solid color-mix(in srgb, var(--accent) 22%, transparent);
187
+ outline-offset: 1px;
188
+ }
189
+
190
+ .site-search__button {
191
+ height: 2.35rem;
192
+ padding: 0 0.75rem;
193
+ border: 0;
194
+ border-radius: 6px;
195
+ background: var(--accent);
196
+ color: var(--button-ink);
197
+ font: inherit;
198
+ font-size: 0.88rem;
199
+ font-weight: 750;
200
+ cursor: pointer;
201
+ }
202
+
203
+ .site-search__panel {
204
+ position: absolute;
205
+ top: calc(100% + 0.55rem);
206
+ right: 0;
207
+ width: min(31rem, calc(100vw - 2rem));
208
+ max-height: min(31rem, calc(100vh - 6rem));
209
+ overflow: auto;
210
+ padding: 0.85rem;
211
+ border: 1px solid var(--line);
212
+ border-radius: var(--radius);
213
+ background: var(--surface);
214
+ box-shadow: 0 20px 45px rgb(15 23 42 / 18%);
215
+ }
216
+
217
+ .site-search__status {
218
+ margin: 0 0 0.75rem;
219
+ color: var(--muted);
220
+ font-size: 0.86rem;
221
+ }
222
+
223
+ .site-search__results {
224
+ list-style: none;
225
+ margin: 0;
226
+ padding: 0;
227
+ }
228
+
229
+ .site-search__results li + li {
230
+ margin-top: 0.65rem;
231
+ padding-top: 0.65rem;
232
+ border-top: 1px solid var(--line);
233
+ }
234
+
235
+ .site-search__results a {
236
+ display: inline-block;
237
+ color: var(--ink);
238
+ font-weight: 750;
239
+ text-decoration: none;
240
+ }
241
+
242
+ .site-search__results a:hover {
243
+ color: var(--accent);
244
+ text-decoration: underline;
245
+ }
246
+
247
+ .site-search__results span {
248
+ display: block;
249
+ margin-top: 0.1rem;
250
+ color: var(--accent);
251
+ font-size: 0.75rem;
252
+ font-weight: 750;
253
+ text-transform: uppercase;
254
+ }
255
+
256
+ .site-search__results p {
257
+ margin: 0.25rem 0 0;
258
+ color: var(--muted);
259
+ font-size: 0.88rem;
260
+ line-height: 1.45;
261
+ }
262
+
132
263
  .hero {
133
264
  background: var(--surface);
134
265
  border-bottom: 1px solid var(--line);
@@ -372,6 +503,71 @@ h1 {
372
503
  padding: 0;
373
504
  }
374
505
 
506
+ .prose pre code.hljs {
507
+ color: var(--pre-ink);
508
+ }
509
+
510
+ .prose .hljs-comment,
511
+ .prose .hljs-quote {
512
+ color: var(--syntax-comment);
513
+ font-style: italic;
514
+ }
515
+
516
+ .prose .hljs-keyword,
517
+ .prose .hljs-selector-tag,
518
+ .prose .hljs-subst {
519
+ color: var(--syntax-keyword);
520
+ }
521
+
522
+ .prose .hljs-title,
523
+ .prose .hljs-section,
524
+ .prose .hljs-name,
525
+ .prose .hljs-selector-id,
526
+ .prose .hljs-selector-class {
527
+ color: var(--syntax-title);
528
+ }
529
+
530
+ .prose .hljs-string,
531
+ .prose .hljs-regexp,
532
+ .prose .hljs-symbol,
533
+ .prose .hljs-bullet {
534
+ color: var(--syntax-string);
535
+ }
536
+
537
+ .prose .hljs-number,
538
+ .prose .hljs-literal {
539
+ color: var(--syntax-number);
540
+ }
541
+
542
+ .prose .hljs-attr,
543
+ .prose .hljs-attribute,
544
+ .prose .hljs-variable,
545
+ .prose .hljs-template-variable {
546
+ color: var(--syntax-attr);
547
+ }
548
+
549
+ .prose .hljs-built_in,
550
+ .prose .hljs-type,
551
+ .prose .hljs-class .hljs-title {
552
+ color: var(--syntax-built-in);
553
+ }
554
+
555
+ .prose .hljs-deletion {
556
+ color: var(--syntax-deletion);
557
+ }
558
+
559
+ .prose .hljs-addition {
560
+ color: var(--syntax-addition);
561
+ }
562
+
563
+ .prose .hljs-emphasis {
564
+ font-style: italic;
565
+ }
566
+
567
+ .prose .hljs-strong {
568
+ font-weight: 800;
569
+ }
570
+
375
571
  .prose table {
376
572
  display: block;
377
573
  width: 100%;
@@ -544,10 +740,26 @@ h1, h2, h3, h4, h5, h6 {
544
740
  padding: 1rem 0;
545
741
  }
546
742
 
743
+ .site-header__actions {
744
+ width: 100%;
745
+ align-items: stretch;
746
+ flex-direction: column;
747
+ }
748
+
547
749
  .site-nav ul {
548
750
  justify-content: flex-start;
549
751
  }
550
752
 
753
+ .site-search {
754
+ width: 100%;
755
+ }
756
+
757
+ .site-search__panel {
758
+ left: 0;
759
+ right: auto;
760
+ width: 100%;
761
+ }
762
+
551
763
  .docs-grid,
552
764
  .npm-links,
553
765
  .doc-layout,
@@ -0,0 +1,121 @@
1
+ const searchRoot = document.querySelector('[data-site-search]');
2
+
3
+ if (searchRoot) {
4
+ const form = searchRoot.querySelector('[data-site-search-form]');
5
+ const input = searchRoot.querySelector('[data-site-search-input]');
6
+ const panel = searchRoot.querySelector('[data-site-search-panel]');
7
+ const status = searchRoot.querySelector('[data-site-search-status]');
8
+ const resultsList = searchRoot.querySelector('[data-site-search-results]');
9
+ let searchApiPromise;
10
+ let debounceTimer;
11
+ let requestId = 0;
12
+
13
+ const loadSearchApi = () => {
14
+ searchApiPromise ||= import('/_zeropress/search.js');
15
+ return searchApiPromise;
16
+ };
17
+
18
+ const clearResults = () => {
19
+ resultsList.replaceChildren();
20
+ };
21
+
22
+ const showPanel = () => {
23
+ panel.hidden = false;
24
+ };
25
+
26
+ const hidePanel = () => {
27
+ panel.hidden = true;
28
+ };
29
+
30
+ const setStatus = (message) => {
31
+ status.textContent = message;
32
+ };
33
+
34
+ const renderResults = async (items) => {
35
+ clearResults();
36
+ if (!items.length) {
37
+ setStatus('No results found.');
38
+ return;
39
+ }
40
+
41
+ setStatus(`${items.length} result${items.length === 1 ? '' : 's'} found.`);
42
+ const fragment = document.createDocumentFragment();
43
+
44
+ for (const item of items) {
45
+ const data = await item.data();
46
+ const title = data.meta?.title || data.url;
47
+ const excerpt = data.plain_excerpt || data.excerpt || '';
48
+ const listItem = document.createElement('li');
49
+ const link = document.createElement('a');
50
+ const meta = document.createElement('span');
51
+ const summary = document.createElement('p');
52
+
53
+ link.href = data.url;
54
+ link.textContent = title;
55
+ meta.textContent = data.meta?.type || 'page';
56
+ summary.textContent = excerpt;
57
+
58
+ listItem.append(link, meta, summary);
59
+ fragment.append(listItem);
60
+ }
61
+
62
+ resultsList.append(fragment);
63
+ };
64
+
65
+ const runSearch = async () => {
66
+ const query = input.value.trim();
67
+ const currentRequest = ++requestId;
68
+ showPanel();
69
+
70
+ if (query.length < 2) {
71
+ clearResults();
72
+ setStatus('Type at least two characters.');
73
+ return;
74
+ }
75
+
76
+ setStatus('Searching...');
77
+ clearResults();
78
+
79
+ try {
80
+ const searchApi = await loadSearchApi();
81
+ const response = await searchApi.search(query, { limit: 8 });
82
+ if (currentRequest === requestId) {
83
+ await renderResults(response.results || []);
84
+ }
85
+ } catch {
86
+ if (currentRequest === requestId) {
87
+ clearResults();
88
+ setStatus('Search is not available for this build.');
89
+ }
90
+ }
91
+ };
92
+
93
+ input.addEventListener('input', () => {
94
+ window.clearTimeout(debounceTimer);
95
+ debounceTimer = window.setTimeout(runSearch, 160);
96
+ });
97
+
98
+ input.addEventListener('focus', () => {
99
+ if (input.value.trim()) {
100
+ showPanel();
101
+ }
102
+ });
103
+
104
+ form.addEventListener('submit', (event) => {
105
+ event.preventDefault();
106
+ runSearch();
107
+ });
108
+
109
+ document.addEventListener('keydown', (event) => {
110
+ if (event.key === 'Escape') {
111
+ hidePanel();
112
+ input.blur();
113
+ }
114
+ });
115
+
116
+ document.addEventListener('click', (event) => {
117
+ if (!searchRoot.contains(event.target)) {
118
+ hidePanel();
119
+ }
120
+ });
121
+ }