@zeropress/build-core 0.1.6 → 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 +181 -22
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeropress/build-core",
3
- "version": "0.1.6",
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
  }
@@ -194,7 +194,12 @@ async function renderRoute(state, templateName, route) {
194
194
  templateName,
195
195
  {
196
196
  ...route,
197
- meta: buildPageMeta(state.previewData.site),
197
+ meta: buildPageMeta(state.previewData.site, {
198
+ currentUrl,
199
+ title: state.previewData.site.title,
200
+ description: state.previewData.site.description,
201
+ ogType: 'website',
202
+ }),
198
203
  },
199
204
  createRenderContext(state.previewData.site, currentUrl),
200
205
  );
@@ -209,7 +214,15 @@ async function renderPost(state, post) {
209
214
  'post',
210
215
  {
211
216
  post,
212
- meta: buildPageMeta(state.previewData.site, post.excerpt),
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
+ }),
213
226
  },
214
227
  createRenderContext(state.previewData.site, currentUrl),
215
228
  );
@@ -229,19 +242,27 @@ async function renderPost(state, post) {
229
242
  }
230
243
 
231
244
  async function renderPage(state, page) {
245
+ const currentUrl = `/${encodeSlugSegment(page.slug)}/`;
232
246
  let html = await state.engine.render(
233
247
  'page',
234
248
  {
235
249
  page,
236
- meta: buildPageMeta(state.previewData.site),
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
+ }),
237
257
  },
238
- createRenderContext(state.previewData.site, `/${encodeSlugSegment(page.slug)}/`),
258
+ createRenderContext(state.previewData.site, currentUrl),
239
259
  );
240
260
  html = state.assetProcessor.updateAssetReferences(html, state.assetMap);
241
261
  await writeOutput(state.writer, state.summaries, `${encodeSlugSegment(page.slug)}/index.html`, html, 'text/html');
242
262
  state.emitted.pages.push({
243
- url: `/${encodeSlugSegment(page.slug)}/`,
263
+ url: currentUrl,
244
264
  title: page.title,
265
+ description: page.excerpt,
245
266
  status: page.status,
246
267
  });
247
268
  }
@@ -254,7 +275,12 @@ async function maybeRenderNotFoundPage(state) {
254
275
  let html = await state.engine.render(
255
276
  '404',
256
277
  {
257
- meta: buildPageMeta(state.previewData.site),
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
+ }),
258
284
  },
259
285
  createRenderContext(state.previewData.site, '/404.html'),
260
286
  );
@@ -280,23 +306,35 @@ function validateSelection(selection) {
280
306
  }
281
307
 
282
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
+
283
322
  return {
284
323
  ...previewData,
285
- site: {
286
- ...previewData.site,
287
- postsPerPage: Number.isInteger(previewData.site.postsPerPage) && previewData.site.postsPerPage > 0
288
- ? previewData.site.postsPerPage
289
- : DEFAULT_POSTS_PER_PAGE,
290
- dateFormat: normalizeNonEmptyString(previewData.site.dateFormat, DEFAULT_DATE_FORMAT),
291
- timeFormat: typeof previewData.site.timeFormat === 'string' ? previewData.site.timeFormat : DEFAULT_TIME_FORMAT,
292
- timezone: normalizeNonEmptyString(previewData.site.timezone, DEFAULT_TIMEZONE),
293
- locale: normalizeLocale(previewData.site.locale || DEFAULT_LOCALE),
294
- disallowComments: previewData.site.disallowComments === true,
295
- },
324
+ site: normalizedSite,
296
325
  content: {
297
326
  ...previewData.content,
298
- posts: [...previewData.content.posts].sort((left, right) => toDate(right.published_at_iso).getTime() - toDate(left.published_at_iso).getTime()),
299
- 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
+ })),
300
338
  categories: [...previewData.content.categories],
301
339
  tags: [...previewData.content.tags],
302
340
  },
@@ -708,6 +746,10 @@ function normalizeNonEmptyString(value, fallback) {
708
746
  return typeof value === 'string' && value.trim() ? value : fallback;
709
747
  }
710
748
 
749
+ function normalizeOptionalString(value) {
750
+ return typeof value === 'string' && value.trim() ? value.trim() : '';
751
+ }
752
+
711
753
  function normalizeLocale(value) {
712
754
  if (typeof value !== 'string' || !value.trim()) {
713
755
  return DEFAULT_LOCALE;
@@ -831,13 +873,129 @@ function createRenderContext(site, currentUrl) {
831
873
  };
832
874
  }
833
875
 
834
- function buildPageMeta(site, description = site.description) {
835
- const resolvedDescription = normalizeNonEmptyString(description, site.description);
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
+
836
899
  return {
837
- description: escapeHtml(resolvedDescription),
900
+ ...meta,
901
+ head_tags: buildMetaHeadTags(meta),
838
902
  };
839
903
  }
840
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
+
841
999
  async function writeOutput(writer, summaries, path, content, contentType) {
842
1000
  const normalizedPath = normalizeOutputPath(path);
843
1001
  await writer.write({ path: normalizedPath, content, contentType });
@@ -965,6 +1123,7 @@ function buildMetaJson(site, emitted, generatedAt) {
965
1123
  ...emitted.pages.map((page) => ({
966
1124
  url: page.url,
967
1125
  title: page.title,
1126
+ description: page.description,
968
1127
  type: 'page',
969
1128
  metadata: { status: page.status },
970
1129
  })),