@zeropress/build-core 0.6.1 → 0.6.3

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
@@ -14,9 +14,9 @@ This package is the canonical rendering core for preview-data and theme packages
14
14
  Public contract references:
15
15
 
16
16
  - [Preview Data v0.6 Spec](https://zeropress.dev/spec/preview-data-v0.6.html)
17
- - [Preview Data v0.6 Schema](https://zeropress.dev/schemas/preview-data.v0.6.schema.json)
17
+ - [Preview Data v0.6 Schema](https://schemas.zeropress.dev/preview-data/v0.6/schema.json)
18
18
  - [Theme Runtime v0.6 Spec](https://zeropress.dev/spec/theme-runtime-v0.6.html)
19
- - [Theme Runtime v0.6 Schema](https://zeropress.dev/schemas/theme.v0.6.runtime.schema.json)
19
+ - [Theme Runtime v0.6 Schema](https://schemas.zeropress.dev/theme-runtime/v0.6/schema.json)
20
20
 
21
21
  It accepts canonical preview-data plus a validated theme package and produces static HTML artifacts through a writer interface.
22
22
 
@@ -56,9 +56,9 @@ import {
56
56
  - `sitemap.xml`
57
57
  - `feed.xml`
58
58
  - fallback `robots.txt`
59
- - `/_zeropress/comment-policy.json`
60
- - `/_zeropress/search.json`
61
- - `/_zeropress/search.js`
59
+ - `/_zeropress/search.json` when native search is enabled
60
+ - `/_zeropress/search.js` when native search is enabled
61
+ - `/_zeropress/search_pagefind.js` when native search is enabled
62
62
  - writing outputs through a pluggable writer
63
63
 
64
64
  It does not:
@@ -191,10 +191,6 @@ Build-core derives:
191
191
  - `reading_time`
192
192
  - `comments_enabled`
193
193
 
194
- It also emits a comments allowlist artifact:
195
-
196
- - `/_zeropress/comment-policy.json`
197
-
198
194
  Comment availability is derived from preview-data policy:
199
195
 
200
196
  - `site.disallow_comments`
@@ -212,6 +208,9 @@ The canonical `preview-data v0.6` site contract uses:
212
208
  - `site.disallow_comments`
213
209
  - `site.indexing`
214
210
  - `site.expose_generator`
211
+ - `site.search`
212
+
213
+ Native search artifacts are emitted only when preview-data does not set `site.search: false` and the active theme declares `features.search: true`. `search_pagefind.js` is a Pagefind adapter that can replace `search.js` after a post-build Pagefind step.
215
214
 
216
215
  Optional route templates behave as rendering capabilities, not guaranteed outputs:
217
216
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeropress/build-core",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "description": "Shared ZeroPress build core",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -34,9 +34,9 @@
34
34
  },
35
35
  "dependencies": {
36
36
  "@zeropress/slug-policy": "0.1.0",
37
- "@zeropress/preview-data-validator": "0.6.1",
38
- "@zeropress/theme-validator": "0.6.1",
39
- "highlight.js": "^11.11.1",
37
+ "@zeropress/preview-data-validator": "0.6.3",
38
+ "@zeropress/theme-validator": "0.6.3",
39
+ "highlight.js": "11.11.1",
40
40
  "markdown-it": "^14.1.0",
41
41
  "markdown-it-anchor": "^9.2.0"
42
42
  }
@@ -37,9 +37,9 @@ const DEFAULT_POST_INDEX = Object.freeze({
37
37
  paginate: true,
38
38
  });
39
39
  const PERMALINK_OUTPUT_STYLES = new Set(['directory', 'html-extension']);
40
- const COMMENT_POLICY_OUTPUT_PATH = '_zeropress/comment-policy.json';
41
40
  const SEARCH_INDEX_OUTPUT_PATH = '_zeropress/search.json';
42
41
  const SEARCH_ADAPTER_OUTPUT_PATH = '_zeropress/search.js';
42
+ const SEARCH_PAGEFIND_ADAPTER_OUTPUT_PATH = '_zeropress/search_pagefind.js';
43
43
  const OUTPUT_PATH_CONTROL_CHAR_PATTERN = /[\u0000-\u001F\u007F]/;
44
44
  const SAFE_MEDIA_PROTOCOLS = new Set(['http:', 'https:']);
45
45
  const SAFE_LINK_PROTOCOLS = new Set(['http:', 'https:', 'mailto:', 'tel:']);
@@ -99,17 +99,13 @@ export async function buildSite(input) {
99
99
  await writeOutput(state.writer, state.summaries, assetOutput.path, assetOutput.content, assetOutput.contentType);
100
100
  }
101
101
 
102
- await writeOutput(
103
- state.writer,
104
- state.summaries,
105
- COMMENT_POLICY_OUTPUT_PATH,
106
- state.commentPolicyContent,
107
- 'application/json',
108
- );
109
-
110
- if (options.generateSpecialFiles) {
102
+ if (shouldGenerateSearchArtifacts(state, options)) {
111
103
  await writeOutput(state.writer, state.summaries, SEARCH_INDEX_OUTPUT_PATH, buildSearchIndexJson(state), 'application/json');
112
104
  await writeOutput(state.writer, state.summaries, SEARCH_ADAPTER_OUTPUT_PATH, buildSearchAdapterJs(), 'application/javascript');
105
+ await writeOutput(state.writer, state.summaries, SEARCH_PAGEFIND_ADAPTER_OUTPUT_PATH, buildSearchPagefindAdapterJs(), 'application/javascript');
106
+ }
107
+
108
+ if (options.generateSpecialFiles) {
113
109
  await maybeRenderNotFoundPage(state);
114
110
  if (hasCanonicalSiteUrl(state.previewData.site.url)) {
115
111
  await writeOutput(
@@ -168,7 +164,6 @@ async function createBuildState(input, options) {
168
164
  customHtml: previewData.custom_html,
169
165
  favicon: previewData.site.favicon,
170
166
  exposeGenerator: previewData.site.expose_generator !== false,
171
- commentPolicyContent: buildCommentPolicyManifest(renderData.posts),
172
167
  options,
173
168
  generatedAt: new Date(),
174
169
  emitted: {
@@ -234,7 +229,9 @@ async function renderRoute(state, templateName, route) {
234
229
  route: routeContext,
235
230
  meta: buildPageMeta(state.previewData.site, {
236
231
  currentUrl,
237
- title: state.previewData.site.title,
232
+ title: route.is_front_page === true
233
+ ? buildFrontPageTitle(state.previewData.site)
234
+ : state.previewData.site.title,
238
235
  description: state.previewData.site.description,
239
236
  ogType: 'website',
240
237
  }),
@@ -281,7 +278,7 @@ async function renderFrontPage(state, route) {
281
278
  route: routeContext,
282
279
  meta: buildPageMeta(state.previewData.site, {
283
280
  currentUrl,
284
- title: buildDocumentTitle(page.title, state.previewData.site.title),
281
+ title: buildFrontPageTitle(state.previewData.site),
285
282
  description: page.excerpt,
286
283
  ogType: 'website',
287
284
  image: page.featured_image,
@@ -314,7 +311,7 @@ async function renderFrontPage(state, route) {
314
311
  route: routeContext,
315
312
  meta: buildPageMeta(state.previewData.site, {
316
313
  currentUrl,
317
- title: state.previewData.site.title,
314
+ title: buildFrontPageTitle(state.previewData.site),
318
315
  description: state.previewData.site.description,
319
316
  ogType: 'website',
320
317
  }),
@@ -436,13 +433,17 @@ async function maybeRenderNotFoundPage(state) {
436
433
  }
437
434
 
438
435
  function normalizePreviewData(previewData, options = {}) {
436
+ const media_base_url = normalizeOptionalString(previewData.site.media_base_url);
439
437
  const normalizedSite = {
440
438
  ...previewData.site,
441
- media_base_url: normalizeOptionalString(previewData.site.media_base_url),
439
+ media_base_url,
442
440
  media_delivery_mode: MEDIA_DELIVERY_MODES.has(previewData.site.media_delivery_mode)
443
441
  ? previewData.site.media_delivery_mode
444
442
  : 'none',
445
- favicon: normalizeSiteFavicon(previewData.site.favicon || options.favicon),
443
+ favicon: previewData.site.favicon
444
+ ? normalizeSiteFavicon(previewData.site.favicon, media_base_url)
445
+ : normalizeSiteFavicon(options.favicon, ''),
446
+ logo: normalizeSiteLogo(previewData.site.logo, media_base_url),
446
447
  posts_per_page: Number.isInteger(previewData.site.posts_per_page) && previewData.site.posts_per_page > 0
447
448
  ? previewData.site.posts_per_page
448
449
  : DEFAULT_POSTS_PER_PAGE,
@@ -459,6 +460,7 @@ function normalizePreviewData(previewData, options = {}) {
459
460
  locale: normalizeLocale(previewData.site.locale || DEFAULT_LOCALE),
460
461
  disallow_comments: previewData.site.disallow_comments === true,
461
462
  expose_generator: previewData.site.expose_generator !== false,
463
+ search: previewData.site.search !== false,
462
464
  indexing: previewData.site.indexing !== false,
463
465
  permalinks: normalizePermalinks(previewData.site.permalinks),
464
466
  front_page: normalizeFrontPage(previewData.site.front_page),
@@ -732,7 +734,7 @@ function normalizeCustomHtml(customHtml) {
732
734
  };
733
735
  }
734
736
 
735
- function normalizeSiteFavicon(favicon) {
737
+ function normalizeSiteFavicon(favicon, media_base_url) {
736
738
  if (!favicon || typeof favicon !== 'object') {
737
739
  return undefined;
738
740
  }
@@ -741,13 +743,30 @@ function normalizeSiteFavicon(favicon) {
741
743
  for (const key of ['icon', 'svg', 'png', 'apple_touch_icon']) {
742
744
  const value = normalizeOptionalString(favicon[key]);
743
745
  if (value) {
744
- normalized[key] = value;
746
+ normalized[key] = normalizeMediaField(value, media_base_url);
745
747
  }
746
748
  }
747
749
 
748
750
  return Object.keys(normalized).length ? normalized : undefined;
749
751
  }
750
752
 
753
+ function normalizeSiteLogo(logo, media_base_url) {
754
+ if (!logo || typeof logo !== 'object') {
755
+ return undefined;
756
+ }
757
+
758
+ const src = normalizeMediaField(logo.src, media_base_url);
759
+ if (!src) {
760
+ return undefined;
761
+ }
762
+
763
+ const alt = normalizeOptionalString(logo.alt);
764
+ return {
765
+ src,
766
+ ...(alt ? { alt } : {}),
767
+ };
768
+ }
769
+
751
770
  function normalizePermalinks(permalinks) {
752
771
  const source = permalinks && typeof permalinks === 'object' ? permalinks : {};
753
772
  const outputStyle = typeof source.output_style === 'string' && PERMALINK_OUTPUT_STYLES.has(source.output_style)
@@ -794,6 +813,8 @@ function normalizePostIndex(post_index) {
794
813
  function createRenderData(previewData, themeMetadata = {}) {
795
814
  const themeSupportsComments = themeMetadata?.features?.comments === true;
796
815
  const themeSupportsPostIndex = themeMetadata?.features?.post_index !== false;
816
+ const themeSupportsSearch = themeMetadata?.features?.search === true;
817
+ previewData.site.search = previewData.site.search !== false && themeSupportsSearch;
797
818
  const authorsById = new Map(previewData.content.authors.map((author) => [author.id, author]));
798
819
  const categoriesBySlug = new Map(previewData.content.categories.map((category) => [category.slug, category]));
799
820
  const tagsBySlug = new Map(previewData.content.tags.map((tag) => [tag.slug, tag]));
@@ -1426,18 +1447,6 @@ function preparePost(post, site, authorsById, categoriesBySlug, tagsBySlug, them
1426
1447
  };
1427
1448
  }
1428
1449
 
1429
- function buildCommentPolicyManifest(posts) {
1430
- const commentablePosts = posts
1431
- .filter((post) => post.status === 'published' && post.comments_enabled === true)
1432
- .map((post) => post.public_id)
1433
- .filter((value) => Number.isInteger(value) && value > 0);
1434
-
1435
- return JSON.stringify({
1436
- version: 1,
1437
- commentable_posts: commentablePosts,
1438
- }, null, 2);
1439
- }
1440
-
1441
1450
  function buildTaxonomyRoutes(options) {
1442
1451
  const routes = [];
1443
1452
 
@@ -2086,6 +2095,12 @@ function buildDocumentTitle(contentTitle, siteTitle) {
2086
2095
  return `${resolvedContentTitle} - ${resolvedSiteTitle}`;
2087
2096
  }
2088
2097
 
2098
+ function buildFrontPageTitle(site) {
2099
+ const resolvedSiteTitle = normalizeNonEmptyString(site.title, '');
2100
+ const resolvedDescription = normalizeOptionalString(site.description);
2101
+ return resolvedDescription ? `${resolvedSiteTitle} - ${resolvedDescription}` : resolvedSiteTitle;
2102
+ }
2103
+
2089
2104
  function buildMetaHeadTags(meta) {
2090
2105
  const tags = [];
2091
2106
 
@@ -2390,11 +2405,13 @@ function assertPlannedOutputPathsSafe(state) {
2390
2405
  const plannedPaths = [
2391
2406
  ...routeEntries.map((entry) => entry.outputPath),
2392
2407
  ...state.assetOutputs.map((assetOutput) => assetOutput.path),
2393
- COMMENT_POLICY_OUTPUT_PATH,
2394
2408
  ];
2395
2409
 
2410
+ if (shouldGenerateSearchArtifacts(state, state.options)) {
2411
+ plannedPaths.push(SEARCH_INDEX_OUTPUT_PATH, SEARCH_ADAPTER_OUTPUT_PATH, SEARCH_PAGEFIND_ADAPTER_OUTPUT_PATH);
2412
+ }
2413
+
2396
2414
  if (state.options.generateSpecialFiles) {
2397
- plannedPaths.push(SEARCH_INDEX_OUTPUT_PATH, SEARCH_ADAPTER_OUTPUT_PATH);
2398
2415
  plannedPaths.push('404.html');
2399
2416
  if (shouldGenerateRobotsTxt(state.options)) {
2400
2417
  plannedPaths.push('robots.txt');
@@ -2965,6 +2982,88 @@ function normalizeLimit(value) {
2965
2982
  `;
2966
2983
  }
2967
2984
 
2985
+ function buildSearchPagefindAdapterJs() {
2986
+ return `let pagefindPromise;
2987
+
2988
+ export async function preload() {
2989
+ if (!pagefindPromise) {
2990
+ pagefindPromise = import(new URL('./pagefind/pagefind.js', import.meta.url).href).then(async (pagefind) => {
2991
+ if (typeof pagefind.options === 'function') {
2992
+ await pagefind.options({ baseUrl: '/' });
2993
+ }
2994
+ return pagefind;
2995
+ });
2996
+ }
2997
+
2998
+ return pagefindPromise;
2999
+ }
3000
+
3001
+ export async function search(query, options = {}) {
3002
+ const pagefind = await preload();
3003
+ const result = await pagefind.search(query, options);
3004
+ const limit = normalizeLimit(options.limit);
3005
+ if (!Array.isArray(result?.results)) {
3006
+ return result;
3007
+ }
3008
+
3009
+ const results = result.results.map(normalizeResult);
3010
+ return {
3011
+ ...result,
3012
+ results: limit ? results.slice(0, limit) : results,
3013
+ };
3014
+ }
3015
+
3016
+ function normalizeResult(result) {
3017
+ if (!result || typeof result.data !== 'function') {
3018
+ return result;
3019
+ }
3020
+
3021
+ return {
3022
+ ...result,
3023
+ data: async () => normalizeResultData(await result.data()),
3024
+ };
3025
+ }
3026
+
3027
+ function normalizeResultData(data) {
3028
+ if (!data || typeof data !== 'object') {
3029
+ return data;
3030
+ }
3031
+
3032
+ return {
3033
+ ...data,
3034
+ url: normalizeUrl(data.url),
3035
+ sub_results: Array.isArray(data.sub_results)
3036
+ ? data.sub_results.map((item) => ({ ...item, url: normalizeUrl(item.url) }))
3037
+ : data.sub_results,
3038
+ };
3039
+ }
3040
+
3041
+ function normalizeUrl(value) {
3042
+ const url = String(value || '');
3043
+ if (url.startsWith('/_zeropress/') && !url.startsWith('/_zeropress/pagefind/')) {
3044
+ return url.replace(/^\\/_zeropress/, '') || '/';
3045
+ }
3046
+ if (url.startsWith('_zeropress/') && !url.startsWith('_zeropress/pagefind/')) {
3047
+ return url.replace(/^_zeropress/, '') || '/';
3048
+ }
3049
+ return url;
3050
+ }
3051
+
3052
+ function normalizeLimit(value) {
3053
+ if (value === undefined || value === null) {
3054
+ return null;
3055
+ }
3056
+
3057
+ const limit = Number(value);
3058
+ if (!Number.isFinite(limit) || limit <= 0) {
3059
+ return null;
3060
+ }
3061
+
3062
+ return Math.floor(limit);
3063
+ }
3064
+ `;
3065
+ }
3066
+
2968
3067
  function buildSitemapXml(site, emitted, generatedAt, stylesheetHref = '') {
2969
3068
  const entries = [
2970
3069
  ...(emitted.frontPage && emitted.frontPage.includeInSitemap !== false
@@ -3045,6 +3144,10 @@ function shouldGenerateRobotsTxt(options) {
3045
3144
  return options.generateSpecialFiles && options.generateRobotsTxt !== false;
3046
3145
  }
3047
3146
 
3147
+ function shouldGenerateSearchArtifacts(state, options) {
3148
+ return options.generateSpecialFiles && state.previewData.site.search === true;
3149
+ }
3150
+
3048
3151
  function getContentType(assetPath) {
3049
3152
  const ext = assetPath.split('.').pop()?.toLowerCase();
3050
3153
  const contentTypes = {