@zeropress/build-core 0.1.5 → 0.1.7

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.
Files changed (2) hide show
  1. package/package.json +3 -3
  2. package/src/api/build-site.js +196 -19
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeropress/build-core",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Shared ZeroPress build and partial-render core",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -33,7 +33,7 @@
33
33
  "node": ">=18.18.0"
34
34
  },
35
35
  "dependencies": {
36
- "@zeropress/preview-data-validator": "^0.3.1",
37
- "@zeropress/theme-validator": "^0.2.3"
36
+ "@zeropress/preview-data-validator": "^0.3.3",
37
+ "@zeropress/theme-validator": "^0.2.4"
38
38
  }
39
39
  }
@@ -192,7 +192,15 @@ async function renderRoute(state, templateName, route) {
192
192
  const currentUrl = normalizeRoutePath(route.path);
193
193
  let html = await state.engine.render(
194
194
  templateName,
195
- { ...route },
195
+ {
196
+ ...route,
197
+ meta: buildPageMeta(state.previewData.site, {
198
+ currentUrl,
199
+ title: state.previewData.site.title,
200
+ description: state.previewData.site.description,
201
+ ogType: 'website',
202
+ }),
203
+ },
196
204
  createRenderContext(state.previewData.site, currentUrl),
197
205
  );
198
206
  html = state.assetProcessor.updateAssetReferences(html, state.assetMap);
@@ -204,7 +212,18 @@ async function renderPost(state, post) {
204
212
  const currentUrl = `/posts/${encodeSlugSegment(post.slug)}/`;
205
213
  let html = await state.engine.render(
206
214
  'post',
207
- { post },
215
+ {
216
+ post,
217
+ meta: buildPageMeta(state.previewData.site, {
218
+ currentUrl,
219
+ title: buildDocumentTitle(post.title, state.previewData.site.title),
220
+ description: post.excerpt,
221
+ ogType: 'article',
222
+ image: post.featured_image,
223
+ publishedTime: post.published_at_iso,
224
+ modifiedTime: post.updated_at_iso,
225
+ }),
226
+ },
208
227
  createRenderContext(state.previewData.site, currentUrl),
209
228
  );
210
229
  html = state.assetProcessor.updateAssetReferences(html, state.assetMap);
@@ -223,16 +242,27 @@ async function renderPost(state, post) {
223
242
  }
224
243
 
225
244
  async function renderPage(state, page) {
245
+ const currentUrl = `/${encodeSlugSegment(page.slug)}/`;
226
246
  let html = await state.engine.render(
227
247
  'page',
228
- { page },
229
- createRenderContext(state.previewData.site, `/${encodeSlugSegment(page.slug)}/`),
248
+ {
249
+ page,
250
+ meta: buildPageMeta(state.previewData.site, {
251
+ currentUrl,
252
+ title: buildDocumentTitle(page.title, state.previewData.site.title),
253
+ description: page.excerpt,
254
+ ogType: 'website',
255
+ image: page.featured_image,
256
+ }),
257
+ },
258
+ createRenderContext(state.previewData.site, currentUrl),
230
259
  );
231
260
  html = state.assetProcessor.updateAssetReferences(html, state.assetMap);
232
261
  await writeOutput(state.writer, state.summaries, `${encodeSlugSegment(page.slug)}/index.html`, html, 'text/html');
233
262
  state.emitted.pages.push({
234
- url: `/${encodeSlugSegment(page.slug)}/`,
263
+ url: currentUrl,
235
264
  title: page.title,
265
+ description: page.excerpt,
236
266
  status: page.status,
237
267
  });
238
268
  }
@@ -244,7 +274,14 @@ async function maybeRenderNotFoundPage(state) {
244
274
 
245
275
  let html = await state.engine.render(
246
276
  '404',
247
- {},
277
+ {
278
+ meta: buildPageMeta(state.previewData.site, {
279
+ currentUrl: '/404.html',
280
+ title: state.previewData.site.title,
281
+ description: state.previewData.site.description,
282
+ ogType: 'website',
283
+ }),
284
+ },
248
285
  createRenderContext(state.previewData.site, '/404.html'),
249
286
  );
250
287
  html = state.assetProcessor.updateAssetReferences(html, state.assetMap);
@@ -269,23 +306,35 @@ function validateSelection(selection) {
269
306
  }
270
307
 
271
308
  function normalizePreviewData(previewData) {
309
+ const normalizedSite = {
310
+ ...previewData.site,
311
+ mediaBaseUrl: normalizeOptionalString(previewData.site.mediaBaseUrl),
312
+ postsPerPage: Number.isInteger(previewData.site.postsPerPage) && previewData.site.postsPerPage > 0
313
+ ? previewData.site.postsPerPage
314
+ : DEFAULT_POSTS_PER_PAGE,
315
+ dateFormat: normalizeNonEmptyString(previewData.site.dateFormat, DEFAULT_DATE_FORMAT),
316
+ timeFormat: typeof previewData.site.timeFormat === 'string' ? previewData.site.timeFormat : DEFAULT_TIME_FORMAT,
317
+ timezone: normalizeNonEmptyString(previewData.site.timezone, DEFAULT_TIMEZONE),
318
+ locale: normalizeLocale(previewData.site.locale || DEFAULT_LOCALE),
319
+ disallowComments: previewData.site.disallowComments === true,
320
+ };
321
+
272
322
  return {
273
323
  ...previewData,
274
- site: {
275
- ...previewData.site,
276
- postsPerPage: Number.isInteger(previewData.site.postsPerPage) && previewData.site.postsPerPage > 0
277
- ? previewData.site.postsPerPage
278
- : DEFAULT_POSTS_PER_PAGE,
279
- dateFormat: normalizeNonEmptyString(previewData.site.dateFormat, DEFAULT_DATE_FORMAT),
280
- timeFormat: typeof previewData.site.timeFormat === 'string' ? previewData.site.timeFormat : DEFAULT_TIME_FORMAT,
281
- timezone: normalizeNonEmptyString(previewData.site.timezone, DEFAULT_TIMEZONE),
282
- locale: normalizeLocale(previewData.site.locale || DEFAULT_LOCALE),
283
- disallowComments: previewData.site.disallowComments === true,
284
- },
324
+ site: normalizedSite,
285
325
  content: {
286
326
  ...previewData.content,
287
- posts: [...previewData.content.posts].sort((left, right) => toDate(right.published_at_iso).getTime() - toDate(left.published_at_iso).getTime()),
288
- pages: [...previewData.content.pages],
327
+ posts: previewData.content.posts
328
+ .map((post) => ({
329
+ ...post,
330
+ author_avatar: normalizeMediaField(post.author_avatar, normalizedSite.mediaBaseUrl),
331
+ featured_image: normalizeMediaField(post.featured_image, normalizedSite.mediaBaseUrl),
332
+ }))
333
+ .sort((left, right) => toDate(right.published_at_iso).getTime() - toDate(left.published_at_iso).getTime()),
334
+ pages: previewData.content.pages.map((page) => ({
335
+ ...page,
336
+ featured_image: normalizeMediaField(page.featured_image, normalizedSite.mediaBaseUrl),
337
+ })),
289
338
  categories: [...previewData.content.categories],
290
339
  tags: [...previewData.content.tags],
291
340
  },
@@ -697,6 +746,10 @@ function normalizeNonEmptyString(value, fallback) {
697
746
  return typeof value === 'string' && value.trim() ? value : fallback;
698
747
  }
699
748
 
749
+ function normalizeOptionalString(value) {
750
+ return typeof value === 'string' && value.trim() ? value.trim() : '';
751
+ }
752
+
700
753
  function normalizeLocale(value) {
701
754
  if (typeof value !== 'string' || !value.trim()) {
702
755
  return DEFAULT_LOCALE;
@@ -820,6 +873,129 @@ function createRenderContext(site, currentUrl) {
820
873
  };
821
874
  }
822
875
 
876
+ function buildPageMeta(site, options = {}) {
877
+ const resolvedTitle = normalizeNonEmptyString(options.title, site.title);
878
+ const resolvedDescription = normalizeOptionalString(options.description);
879
+ const canonicalUrl = resolveMetaCanonicalUrl(site, options.currentUrl);
880
+ const ogImage = resolveMetaImageUrl(options.image);
881
+ const ogType = normalizeNonEmptyString(options.ogType, 'website');
882
+ const publishedTime = normalizeOptionalString(options.publishedTime);
883
+ const modifiedTime = normalizeOptionalString(options.modifiedTime);
884
+
885
+ const meta = {
886
+ title: escapeHtml(resolvedTitle),
887
+ description: resolvedDescription ? escapeHtml(resolvedDescription) : '',
888
+ canonical_url: canonicalUrl ? escapeHtml(canonicalUrl) : '',
889
+ og_title: escapeHtml(resolvedTitle),
890
+ og_description: resolvedDescription ? escapeHtml(resolvedDescription) : '',
891
+ og_type: escapeHtml(ogType),
892
+ og_url: canonicalUrl ? escapeHtml(canonicalUrl) : '',
893
+ og_site_name: escapeHtml(site.title),
894
+ og_image: ogImage ? escapeHtml(ogImage) : '',
895
+ article_published_time: publishedTime ? escapeHtml(publishedTime) : '',
896
+ article_modified_time: modifiedTime ? escapeHtml(modifiedTime) : '',
897
+ };
898
+
899
+ return {
900
+ ...meta,
901
+ head_tags: buildMetaHeadTags(meta),
902
+ };
903
+ }
904
+
905
+ function buildDocumentTitle(contentTitle, siteTitle) {
906
+ const resolvedContentTitle = normalizeNonEmptyString(contentTitle, siteTitle);
907
+ const resolvedSiteTitle = normalizeNonEmptyString(siteTitle, resolvedContentTitle);
908
+ return `${resolvedContentTitle} - ${resolvedSiteTitle}`;
909
+ }
910
+
911
+ function buildMetaHeadTags(meta) {
912
+ const tags = [];
913
+
914
+ if (meta.description) {
915
+ tags.push(`<meta name="description" content="${meta.description}">`);
916
+ }
917
+ if (meta.canonical_url) {
918
+ tags.push(`<link rel="canonical" href="${meta.canonical_url}">`);
919
+ }
920
+
921
+ tags.push(`<meta property="og:title" content="${meta.og_title}">`);
922
+ if (meta.og_description) {
923
+ tags.push(`<meta property="og:description" content="${meta.og_description}">`);
924
+ }
925
+ tags.push(`<meta property="og:type" content="${meta.og_type}">`);
926
+ if (meta.og_url) {
927
+ tags.push(`<meta property="og:url" content="${meta.og_url}">`);
928
+ }
929
+ tags.push(`<meta property="og:site_name" content="${meta.og_site_name}">`);
930
+ if (meta.og_image) {
931
+ tags.push(`<meta property="og:image" content="${meta.og_image}">`);
932
+ }
933
+ if (meta.article_published_time) {
934
+ tags.push(`<meta property="article:published_time" content="${meta.article_published_time}">`);
935
+ }
936
+ if (meta.article_modified_time) {
937
+ tags.push(`<meta property="article:modified_time" content="${meta.article_modified_time}">`);
938
+ }
939
+
940
+ return tags.length ? `${tags.join('\n')}\n` : '';
941
+ }
942
+
943
+ function resolveMetaCanonicalUrl(site, currentUrl) {
944
+ if (!hasCanonicalSiteUrl(site.url) || !normalizeOptionalString(currentUrl)) {
945
+ return '';
946
+ }
947
+
948
+ return resolveSiteUrl(site.url, currentUrl);
949
+ }
950
+
951
+ function resolveMetaImageUrl(image) {
952
+ const normalizedImage = normalizeOptionalString(image);
953
+ if (!normalizedImage) {
954
+ return '';
955
+ }
956
+
957
+ if (isAbsoluteUrl(normalizedImage)) {
958
+ return normalizedImage;
959
+ }
960
+
961
+ return '';
962
+ }
963
+
964
+ function normalizeMediaField(value, mediaBaseUrl) {
965
+ if (value === undefined) {
966
+ return undefined;
967
+ }
968
+
969
+ const normalizedValue = normalizeOptionalString(value);
970
+ if (!normalizedValue) {
971
+ return '';
972
+ }
973
+
974
+ if (isAbsoluteUrl(normalizedValue)) {
975
+ return normalizedValue;
976
+ }
977
+
978
+ const normalizedBaseUrl = normalizeOptionalString(mediaBaseUrl);
979
+ if (!normalizedBaseUrl) {
980
+ return '';
981
+ }
982
+
983
+ try {
984
+ return decodeURI(new URL(normalizedValue, normalizedBaseUrl).toString());
985
+ } catch {
986
+ return '';
987
+ }
988
+ }
989
+
990
+ function isAbsoluteUrl(value) {
991
+ try {
992
+ new URL(value);
993
+ return true;
994
+ } catch {
995
+ return false;
996
+ }
997
+ }
998
+
823
999
  async function writeOutput(writer, summaries, path, content, contentType) {
824
1000
  const normalizedPath = normalizeOutputPath(path);
825
1001
  await writer.write({ path: normalizedPath, content, contentType });
@@ -947,6 +1123,7 @@ function buildMetaJson(site, emitted, generatedAt) {
947
1123
  ...emitted.pages.map((page) => ({
948
1124
  url: page.url,
949
1125
  title: page.title,
1126
+ description: page.description,
950
1127
  type: 'page',
951
1128
  metadata: { status: page.status },
952
1129
  })),