@zeropress/build-core 0.5.1 → 0.5.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
@@ -54,7 +54,7 @@ import {
54
54
  - generating special files such as:
55
55
  - `sitemap.xml`
56
56
  - `feed.xml`
57
- - `robots.txt`
57
+ - fallback `robots.txt`
58
58
  - writing outputs through a pluggable writer
59
59
 
60
60
  It does not:
@@ -119,7 +119,9 @@ Notes:
119
119
  - `previewData` must already satisfy the canonical preview-data contract
120
120
  - `themePackage` must already be a validated in-memory theme package
121
121
  - `sitemap.xml` and `feed.xml` are emitted only when `site.url` is a non-empty canonical URL
122
- - `robots.txt` is still emitted when `generateSpecialFiles` is enabled
122
+ - fallback `robots.txt` is emitted when `generateSpecialFiles` is enabled and `generateRobotsTxt` is not `false`
123
+ - fallback `robots.txt` uses `site.indexing`; `false` emits `Disallow: /`, while missing or `true` emits `Allow: /`
124
+ - callers that disable fallback robots because a public `robots.txt` exists should copy that file as-is; sitemap directives in custom robots files are caller/user responsibility
123
125
 
124
126
  ### `buildSiteFromThemeDir(input)`
125
127
 
@@ -175,6 +177,8 @@ Theme validation is enforced through:
175
177
 
176
178
  `buildSiteFromThemeDir()` is the convenience entry point that loads a theme directory and converts it into the required in-memory `themePackage`.
177
179
 
180
+ JavaScript theme assets are emitted as provided. Build-core may hash output filenames, but it does not rewrite or minify JavaScript content.
181
+
178
182
  As of `preview-data v0.4`, the payload no longer carries `routes` arrays or raw HTML fragments such as `categories_html`.
179
183
 
180
184
  Build-core now derives:
@@ -219,6 +223,7 @@ Supported options:
219
223
 
220
224
  - `assetHashing`
221
225
  - `generateSpecialFiles`
226
+ - `generateRobotsTxt`
222
227
  - `writeManifest`
223
228
 
224
229
  These options apply to both full builds and partial renders where relevant.
@@ -227,6 +232,7 @@ Defaults:
227
232
 
228
233
  - `assetHashing: true`
229
234
  - `generateSpecialFiles: true`
235
+ - `generateRobotsTxt: true`
230
236
  - `writeManifest: false`
231
237
 
232
238
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeropress/build-core",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "description": "Shared ZeroPress build core",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -33,9 +33,9 @@
33
33
  "node": ">=18.18.0"
34
34
  },
35
35
  "dependencies": {
36
- "@zeropress/slug-policy": "^0.1.0",
37
- "@zeropress/preview-data-validator": "0.5.1",
38
- "@zeropress/theme-validator": "0.5.2",
36
+ "@zeropress/slug-policy": "0.1.0",
37
+ "@zeropress/preview-data-validator": "0.5.2",
38
+ "@zeropress/theme-validator": "0.5.3",
39
39
  "highlight.js": "^11.11.1",
40
40
  "markdown-it": "^14.1.0",
41
41
  "markdown-it-anchor": "^9.2.0"
@@ -9,6 +9,7 @@ import { ZeroPressEngine } from '../render/zeropress-engine.js';
9
9
  const DEFAULT_OPTIONS = {
10
10
  assetHashing: true,
11
11
  generateSpecialFiles: true,
12
+ generateRobotsTxt: true,
12
13
  writeManifest: false,
13
14
  };
14
15
 
@@ -37,6 +38,9 @@ const COMMENT_POLICY_OUTPUT_PATH = '_zeropress/comment-policy.json';
37
38
  const OUTPUT_PATH_CONTROL_CHAR_PATTERN = /[\u0000-\u001F\u007F]/;
38
39
  const SAFE_MEDIA_PROTOCOLS = new Set(['http:', 'https:']);
39
40
  const SAFE_LINK_PROTOCOLS = new Set(['http:', 'https:', 'mailto:', 'tel:']);
41
+ const MEDIA_DELIVERY_MODES = new Set(['none', 'media_domain']);
42
+ const RESPONSIVE_IMAGE_WIDTHS = [320, 480, 768, 1024, 1280, 1600, 1920];
43
+ const RESPONSIVE_IMAGE_EXTENSIONS = new Set(['jpg', 'jpeg', 'png', 'webp', 'avif']);
40
44
  export async function buildSite(input) {
41
45
  const options = { ...DEFAULT_OPTIONS, ...(input.options || {}) };
42
46
  const state = await createBuildState(input, options);
@@ -94,7 +98,9 @@ export async function buildSite(input) {
94
98
  await writeOutput(state.writer, state.summaries, 'sitemap.xml', buildSitemapXml(state.previewData.site, state.emitted, state.generatedAt), 'application/xml');
95
99
  await writeOutput(state.writer, state.summaries, 'feed.xml', buildFeedXml(state.previewData.site, state.emitted, state.generatedAt), 'application/rss+xml');
96
100
  }
97
- await writeOutput(state.writer, state.summaries, 'robots.txt', buildRobotsTxt(state.previewData.site), 'text/plain');
101
+ if (shouldGenerateRobotsTxt(options)) {
102
+ await writeOutput(state.writer, state.summaries, 'robots.txt', buildRobotsTxt(state.previewData.site), 'text/plain');
103
+ }
98
104
  }
99
105
 
100
106
  return finalizeBuildResult(state.writer, state.summaries, options);
@@ -111,7 +117,7 @@ async function createBuildState(input, options) {
111
117
  const engine = new ZeroPressEngine();
112
118
  const assetProcessor = new AssetProcessor();
113
119
  const summaries = [];
114
- const previewData = normalizePreviewData(input.previewData);
120
+ const previewData = normalizePreviewData(input.previewData, options);
115
121
  const renderData = createRenderData(previewData, themePackage.metadata);
116
122
 
117
123
  engine.initialize(themePackage);
@@ -137,6 +143,7 @@ async function createBuildState(input, options) {
137
143
  assetMap,
138
144
  customCssHref: customCssAsset ? `/${customCssAsset.path}` : '',
139
145
  customHtml: previewData.custom_html,
146
+ favicon: previewData.site.favicon,
140
147
  commentPolicyContent: buildCommentPolicyManifest(renderData.posts),
141
148
  options,
142
149
  generatedAt: new Date(),
@@ -197,6 +204,7 @@ async function renderRoute(state, templateName, route) {
197
204
  {
198
205
  menus: state.previewData.menus,
199
206
  widgets: state.widgets,
207
+ collections: state.renderData.collections,
200
208
  taxonomies: state.renderData.taxonomies,
201
209
  ...route,
202
210
  route: routeContext,
@@ -243,6 +251,7 @@ async function renderFrontPage(state, route) {
243
251
  {
244
252
  menus: state.previewData.menus,
245
253
  widgets: state.widgets,
254
+ collections: state.renderData.collections,
246
255
  taxonomies: state.renderData.taxonomies,
247
256
  page,
248
257
  route: routeContext,
@@ -273,6 +282,7 @@ async function renderFrontPage(state, route) {
273
282
  {
274
283
  menus: state.previewData.menus,
275
284
  widgets: state.widgets,
285
+ collections: state.renderData.collections,
276
286
  taxonomies: state.renderData.taxonomies,
277
287
  ...route,
278
288
  route: routeContext,
@@ -303,6 +313,7 @@ async function renderPost(state, post) {
303
313
  {
304
314
  menus: state.previewData.menus,
305
315
  widgets: state.widgets,
316
+ collections: state.renderData.collections,
306
317
  taxonomies: state.renderData.taxonomies,
307
318
  post,
308
319
  route: buildRouteContext('post', currentUrl),
@@ -340,6 +351,7 @@ async function renderPage(state, page) {
340
351
  {
341
352
  menus: state.previewData.menus,
342
353
  widgets: state.widgets,
354
+ collections: state.renderData.collections,
343
355
  taxonomies: state.renderData.taxonomies,
344
356
  page,
345
357
  route: buildRouteContext('page', currentUrl),
@@ -376,6 +388,7 @@ async function maybeRenderNotFoundPage(state) {
376
388
  {
377
389
  menus: state.previewData.menus,
378
390
  widgets: state.widgets,
391
+ collections: state.renderData.collections,
379
392
  taxonomies: state.renderData.taxonomies,
380
393
  route: buildRouteContext('not_found', '/404.html'),
381
394
  meta: buildPageMeta(state.previewData.site, {
@@ -392,10 +405,14 @@ async function maybeRenderNotFoundPage(state) {
392
405
  await writeOutput(state.writer, state.summaries, '404.html', html, 'text/html');
393
406
  }
394
407
 
395
- function normalizePreviewData(previewData) {
408
+ function normalizePreviewData(previewData, options = {}) {
396
409
  const normalizedSite = {
397
410
  ...previewData.site,
398
411
  mediaBaseUrl: normalizeOptionalString(previewData.site.mediaBaseUrl),
412
+ mediaDeliveryMode: MEDIA_DELIVERY_MODES.has(previewData.site.mediaDeliveryMode)
413
+ ? previewData.site.mediaDeliveryMode
414
+ : 'none',
415
+ favicon: normalizeSiteFavicon(previewData.site.favicon || options.favicon),
399
416
  postsPerPage: Number.isInteger(previewData.site.postsPerPage) && previewData.site.postsPerPage > 0
400
417
  ? previewData.site.postsPerPage
401
418
  : DEFAULT_POSTS_PER_PAGE,
@@ -404,42 +421,195 @@ function normalizePreviewData(previewData) {
404
421
  timezone: normalizeNonEmptyString(previewData.site.timezone, DEFAULT_TIMEZONE),
405
422
  locale: normalizeLocale(previewData.site.locale || DEFAULT_LOCALE),
406
423
  disallowComments: previewData.site.disallowComments === true,
424
+ indexing: previewData.site.indexing !== false,
407
425
  permalinks: normalizePermalinks(previewData.site.permalinks),
408
426
  front_page: normalizeFrontPage(previewData.site.front_page),
409
427
  post_index: normalizePostIndex(previewData.site.post_index),
410
428
  footer: normalizeSiteFooter(previewData.site.footer),
411
429
  };
430
+ const media = normalizeContentMedia(previewData.content.media, normalizedSite);
431
+ const mediaRegistry = buildMediaRegistry(media);
412
432
 
413
433
  return {
414
434
  ...previewData,
415
435
  site: normalizedSite,
436
+ menus: normalizeRecordMap(previewData.menus),
437
+ collections: normalizeCollections(previewData.collections),
416
438
  widgets: normalizeWidgetAreas(previewData.widgets, normalizedSite.mediaBaseUrl),
417
439
  custom_css: normalizeCustomCss(previewData.custom_css),
418
440
  custom_html: normalizeCustomHtml(previewData.custom_html),
419
441
  content: {
420
442
  ...previewData.content,
421
- authors: previewData.content.authors.map((author) => ({
422
- ...author,
423
- avatar: normalizeMediaField(author.avatar, normalizedSite.mediaBaseUrl),
424
- })),
443
+ authors: previewData.content.authors.map((author) => {
444
+ const avatar = normalizeMediaField(author.avatar, normalizedSite.mediaBaseUrl);
445
+ const avatarMedia = deriveManagedMedia(avatar, mediaRegistry, normalizedSite);
446
+ return {
447
+ ...author,
448
+ avatar,
449
+ ...(avatarMedia ? { avatar_media: avatarMedia } : {}),
450
+ };
451
+ }),
425
452
  posts: previewData.content.posts
426
- .map((post) => ({
427
- ...post,
428
- published_at_iso: normalizeIsoTimestamp(post.published_at_iso),
429
- updated_at_iso: normalizeIsoTimestamp(post.updated_at_iso),
430
- featured_image: normalizeMediaField(post.featured_image, normalizedSite.mediaBaseUrl),
431
- }))
453
+ .map((post) => {
454
+ const featuredImage = normalizeMediaField(post.featured_image, normalizedSite.mediaBaseUrl);
455
+ const featuredMedia = deriveManagedMedia(featuredImage, mediaRegistry, normalizedSite);
456
+ return {
457
+ ...post,
458
+ published_at_iso: normalizeIsoTimestamp(post.published_at_iso),
459
+ updated_at_iso: normalizeIsoTimestamp(post.updated_at_iso),
460
+ featured_image: featuredImage,
461
+ ...(featuredMedia ? { featured_media: featuredMedia } : {}),
462
+ };
463
+ })
432
464
  .sort((left, right) => toDate(right.published_at_iso).getTime() - toDate(left.published_at_iso).getTime()),
433
- pages: previewData.content.pages.map((page) => ({
434
- ...page,
435
- featured_image: normalizeMediaField(page.featured_image, normalizedSite.mediaBaseUrl),
436
- })),
465
+ pages: previewData.content.pages.map((page) => {
466
+ const featuredImage = normalizeMediaField(page.featured_image, normalizedSite.mediaBaseUrl);
467
+ const featuredMedia = deriveManagedMedia(featuredImage, mediaRegistry, normalizedSite);
468
+ return {
469
+ ...page,
470
+ featured_image: featuredImage,
471
+ ...(featuredMedia ? { featured_media: featuredMedia } : {}),
472
+ };
473
+ }),
437
474
  categories: [...previewData.content.categories],
438
475
  tags: [...previewData.content.tags],
476
+ media,
439
477
  },
440
478
  };
441
479
  }
442
480
 
481
+ function normalizeRecordMap(value) {
482
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
483
+ return {};
484
+ }
485
+
486
+ return { ...value };
487
+ }
488
+
489
+ function normalizeContentMedia(mediaItems, site) {
490
+ if (!Array.isArray(mediaItems)) {
491
+ return [];
492
+ }
493
+
494
+ return mediaItems
495
+ .map((item) => {
496
+ if (!item || typeof item !== 'object') {
497
+ return null;
498
+ }
499
+ const src = normalizeMediaField(item.src, site.mediaBaseUrl);
500
+ const width = Number.isInteger(item.width) && item.width > 0 ? item.width : 0;
501
+ const height = Number.isInteger(item.height) && item.height > 0 ? item.height : 0;
502
+ if (!src || !width || !height) {
503
+ return null;
504
+ }
505
+ return {
506
+ src,
507
+ width,
508
+ height,
509
+ alt: typeof item.alt === 'string' ? item.alt : '',
510
+ };
511
+ })
512
+ .filter(Boolean);
513
+ }
514
+
515
+ function buildMediaRegistry(mediaItems) {
516
+ const registry = new Map();
517
+ for (const item of mediaItems) {
518
+ if (!registry.has(item.src)) {
519
+ registry.set(item.src, item);
520
+ }
521
+ }
522
+ return registry;
523
+ }
524
+
525
+ function deriveManagedMedia(src, mediaRegistry, site) {
526
+ const normalizedSrc = normalizeOptionalString(src);
527
+ if (!normalizedSrc) {
528
+ return null;
529
+ }
530
+ const media = mediaRegistry.get(normalizedSrc);
531
+ if (!media) {
532
+ return null;
533
+ }
534
+ return {
535
+ ...media,
536
+ srcset: buildResponsiveImageSrcset(media, site),
537
+ };
538
+ }
539
+
540
+ function buildResponsiveImageSrcset(media, site) {
541
+ if (site.mediaDeliveryMode !== 'media_domain') {
542
+ return '';
543
+ }
544
+
545
+ const mediaBaseUrl = normalizeOptionalString(site.mediaBaseUrl);
546
+ if (!mediaBaseUrl || !isUrlUnderMediaBase(media.src, mediaBaseUrl) || !isResponsiveRasterImage(media.src)) {
547
+ return '';
548
+ }
549
+
550
+ const widths = RESPONSIVE_IMAGE_WIDTHS.filter((width) => width <= media.width);
551
+ if (!widths.includes(media.width)) {
552
+ widths.push(media.width);
553
+ }
554
+
555
+ return widths
556
+ .filter((width, index, values) => width > 0 && values.indexOf(width) === index)
557
+ .map((width) => `${buildResponsiveImageVariantUrl(media.src, width)} ${width}w`)
558
+ .join(', ');
559
+ }
560
+
561
+ function buildResponsiveImageVariantUrl(src, width) {
562
+ try {
563
+ const url = new URL(src);
564
+ url.searchParams.set('w', String(width));
565
+ url.searchParams.set('fit', 'scale-down');
566
+ url.searchParams.set('format', 'auto');
567
+ return decodeURI(url.toString());
568
+ } catch {
569
+ return src;
570
+ }
571
+ }
572
+
573
+ function isUrlUnderMediaBase(src, mediaBaseUrl) {
574
+ try {
575
+ const sourceUrl = new URL(src);
576
+ const baseUrl = new URL(mediaBaseUrl);
577
+ const basePath = baseUrl.pathname.endsWith('/') ? baseUrl.pathname : `${baseUrl.pathname}/`;
578
+ return sourceUrl.origin === baseUrl.origin && sourceUrl.pathname.startsWith(basePath);
579
+ } catch {
580
+ return false;
581
+ }
582
+ }
583
+
584
+ function isResponsiveRasterImage(src) {
585
+ try {
586
+ const url = new URL(src);
587
+ const lastSegment = url.pathname.split('/').pop() || '';
588
+ const extension = lastSegment.includes('.') ? lastSegment.split('.').pop().toLowerCase() : '';
589
+ return RESPONSIVE_IMAGE_EXTENSIONS.has(extension);
590
+ } catch {
591
+ return false;
592
+ }
593
+ }
594
+
595
+ function normalizeCollections(collections) {
596
+ if (!collections || typeof collections !== 'object' || Array.isArray(collections)) {
597
+ return {};
598
+ }
599
+
600
+ return Object.fromEntries(
601
+ Object.entries(collections).map(([collectionId, collection]) => [
602
+ collectionId,
603
+ {
604
+ ...collection,
605
+ title: normalizeOptionalString(collection?.title),
606
+ description: normalizeOptionalString(collection?.description),
607
+ items: Array.isArray(collection?.items) ? collection.items.map((item) => ({ ...item })) : [],
608
+ },
609
+ ]),
610
+ );
611
+ }
612
+
443
613
  function normalizeWidgetAreas(widgetAreas, mediaBaseUrl) {
444
614
  if (!widgetAreas || typeof widgetAreas !== 'object') {
445
615
  return {};
@@ -516,6 +686,22 @@ function normalizeCustomHtml(customHtml) {
516
686
  };
517
687
  }
518
688
 
689
+ function normalizeSiteFavicon(favicon) {
690
+ if (!favicon || typeof favicon !== 'object') {
691
+ return undefined;
692
+ }
693
+
694
+ const normalized = {};
695
+ for (const key of ['icon', 'svg', 'png', 'apple_touch_icon']) {
696
+ const value = normalizeOptionalString(favicon[key]);
697
+ if (value) {
698
+ normalized[key] = value;
699
+ }
700
+ }
701
+
702
+ return Object.keys(normalized).length ? normalized : undefined;
703
+ }
704
+
519
705
  function normalizePermalinks(permalinks) {
520
706
  const source = permalinks && typeof permalinks === 'object' ? permalinks : {};
521
707
  const outputStyle = typeof source.output_style === 'string' && PERMALINK_OUTPUT_STYLES.has(source.output_style)
@@ -590,6 +776,7 @@ function createRenderData(previewData, themeMetadata = {}) {
590
776
  }));
591
777
  const pages = previewData.content.pages.map((page) => preparePage(page, previewData.site));
592
778
  const postBySlug = new Map(posts.map((post) => [post.slug, post]));
779
+ const pageBySlug = new Map(pages.map((page) => [page.slug, page]));
593
780
  const frontPage = previewData.site.front_page;
594
781
  const postIndex = previewData.site.post_index;
595
782
  const effectivePostIndexEnabled = postIndex.enabled !== false && themeSupportsPostIndex;
@@ -610,6 +797,7 @@ function createRenderData(previewData, themeMetadata = {}) {
610
797
  posts,
611
798
  pages: preparedPages,
612
799
  postBySlug,
800
+ collections: resolveCollections(previewData.collections, postBySlug, pageBySlug, frontPage),
613
801
  taxonomies: buildGlobalTaxonomies(previewData, categoryCountBySlug, tagCountBySlug),
614
802
  frontPageRoute,
615
803
  indexRoutes: buildPostIndexRoutes({
@@ -668,6 +856,69 @@ function buildGlobalTaxonomies(previewData, categoryCountBySlug, tagCountBySlug)
668
856
  };
669
857
  }
670
858
 
859
+ function resolveCollections(collections, postBySlug, pageBySlug, frontPage) {
860
+ if (!collections || typeof collections !== 'object') {
861
+ return {};
862
+ }
863
+
864
+ return Object.fromEntries(
865
+ Object.entries(collections).map(([collectionId, collection]) => [
866
+ collectionId,
867
+ {
868
+ id: collectionId,
869
+ title: normalizeOptionalString(collection?.title),
870
+ description: normalizeOptionalString(collection?.description),
871
+ items: resolveCollectionItems(collectionId, collection?.items, postBySlug, pageBySlug, frontPage),
872
+ },
873
+ ]),
874
+ );
875
+ }
876
+
877
+ function resolveCollectionItems(collectionId, items, postBySlug, pageBySlug, frontPage) {
878
+ if (!Array.isArray(items)) {
879
+ return [];
880
+ }
881
+
882
+ return items.map((item, index) => resolveCollectionItem(collectionId, item, index, postBySlug, pageBySlug, frontPage));
883
+ }
884
+
885
+ function resolveCollectionItem(collectionId, item, index, postBySlug, pageBySlug, frontPage) {
886
+ if (item?.type === 'post') {
887
+ const post = postBySlug.get(item.slug);
888
+ if (!post) {
889
+ throw new Error(`Invalid collection "${collectionId}": item ${index + 1} references missing post slug "${item.slug}".`);
890
+ }
891
+ return {
892
+ type: 'post',
893
+ meta: post.meta,
894
+ ...buildStructuredPostSummary(post),
895
+ };
896
+ }
897
+
898
+ if (item?.type === 'page') {
899
+ const page = pageBySlug.get(item.slug);
900
+ if (!page) {
901
+ throw new Error(`Invalid collection "${collectionId}": item ${index + 1} references missing page slug "${item.slug}".`);
902
+ }
903
+ return buildCollectionPageSummary(page, frontPage);
904
+ }
905
+
906
+ throw new Error(`Invalid collection "${collectionId}": item ${index + 1} has unsupported type "${item?.type}".`);
907
+ }
908
+
909
+ function buildCollectionPageSummary(page, frontPage) {
910
+ return {
911
+ type: 'page',
912
+ title: page.title,
913
+ slug: page.slug,
914
+ url: frontPage?.type === 'page' && frontPage.page_slug === page.slug ? '/' : page.url,
915
+ excerpt: page.excerpt || '',
916
+ featured_image: page.featured_image || '',
917
+ ...(page.featured_media ? { featured_media: { ...page.featured_media } } : {}),
918
+ meta: page.meta,
919
+ };
920
+ }
921
+
671
922
  function buildFrontPageRoute(frontPage, pages, effectivePostIndexEnabled, postIndexBasePath) {
672
923
  if (frontPage.type === 'theme_index') {
673
924
  if (effectivePostIndexEnabled && postIndexBasePath === '/') {
@@ -1031,6 +1282,7 @@ function preparePost(post, site, authorsById, categoriesBySlug, tagsBySlug, them
1031
1282
  updated_at_iso: post.updated_at_iso,
1032
1283
  author_id: post.author_id,
1033
1284
  featured_image: post.featured_image,
1285
+ ...(post.featured_media ? { featured_media: { ...post.featured_media } } : {}),
1034
1286
  meta: post.meta,
1035
1287
  status: post.status,
1036
1288
  allow_comments: post.allow_comments,
@@ -1040,6 +1292,7 @@ function preparePost(post, site, authorsById, categoriesBySlug, tagsBySlug, them
1040
1292
  id: post.author_id,
1041
1293
  display_name: normalizeNonEmptyString(author?.display_name, post.author_id),
1042
1294
  avatar: author?.avatar || '',
1295
+ ...(author?.avatar_media ? { avatar_media: { ...author.avatar_media } } : {}),
1043
1296
  },
1044
1297
  categories,
1045
1298
  tags,
@@ -1281,9 +1534,11 @@ function buildStructuredPostSummary(post) {
1281
1534
  published_at_iso: post.published_at_iso,
1282
1535
  reading_time: post.reading_time,
1283
1536
  featured_image: post.featured_image,
1537
+ ...(post.featured_media ? { featured_media: { ...post.featured_media } } : {}),
1284
1538
  author: {
1285
1539
  display_name: post.author?.display_name || '',
1286
1540
  avatar: post.author?.avatar || '',
1541
+ ...(post.author?.avatar_media ? { avatar_media: { ...post.author.avatar_media } } : {}),
1287
1542
  },
1288
1543
  categories: Array.isArray(post.categories) ? post.categories.map((category) => ({ ...category })) : [],
1289
1544
  tags: Array.isArray(post.tags) ? post.tags.map((tag) => ({ ...tag })) : [],
@@ -1533,7 +1788,8 @@ async function normalizeAndValidateThemePackage(themePackage) {
1533
1788
  ...(themePackage.metadata.features ? { features: themePackage.metadata.features } : {}),
1534
1789
  ...(themePackage.metadata.menuSlots ? { menuSlots: themePackage.metadata.menuSlots } : {}),
1535
1790
  ...(themePackage.metadata.widgetAreas ? { widgetAreas: themePackage.metadata.widgetAreas } : {}),
1536
- settings: themePackage.metadata.settings || {},
1791
+ ...(themePackage.metadata.siteMeta ? { siteMeta: themePackage.metadata.siteMeta } : {}),
1792
+ ...(themePackage.metadata.collectionSlots ? { collectionSlots: themePackage.metadata.collectionSlots } : {}),
1537
1793
  }));
1538
1794
 
1539
1795
  for (const [templateName, templateContent] of themePackage.templates.entries()) {
@@ -1569,7 +1825,6 @@ function normalizeThemePackageMetadata(sourceMetadata, manifest) {
1569
1825
  return {
1570
1826
  ...manifest,
1571
1827
  ...(sourceMetadata?.thumbnail ? { thumbnail: sourceMetadata.thumbnail } : {}),
1572
- settings: sourceMetadata?.settings || {},
1573
1828
  };
1574
1829
  }
1575
1830
 
@@ -1743,7 +1998,7 @@ function normalizeMediaField(value, mediaBaseUrl) {
1743
1998
 
1744
1999
  const normalizedBaseUrl = normalizeOptionalString(mediaBaseUrl);
1745
2000
  if (!normalizedBaseUrl) {
1746
- return '';
2001
+ return normalizedValue;
1747
2002
  }
1748
2003
 
1749
2004
  try {
@@ -1979,7 +2234,10 @@ function assertPlannedOutputPathsSafe(state) {
1979
2234
  ];
1980
2235
 
1981
2236
  if (state.options.generateSpecialFiles) {
1982
- plannedPaths.push('404.html', 'robots.txt');
2237
+ plannedPaths.push('404.html');
2238
+ if (shouldGenerateRobotsTxt(state.options)) {
2239
+ plannedPaths.push('robots.txt');
2240
+ }
1983
2241
  if (hasCanonicalSiteUrl(state.previewData.site.url)) {
1984
2242
  plannedPaths.push('sitemap.xml', 'feed.xml');
1985
2243
  }
@@ -2079,11 +2337,43 @@ function sha256(content) {
2079
2337
  }
2080
2338
 
2081
2339
  function injectSiteCustomizations(html, state) {
2082
- let next = injectCustomCssAssetLink(html, state.customCssHref);
2340
+ let next = injectFaviconLinks(html, state.favicon);
2341
+ next = injectCustomCssAssetLink(next, state.customCssHref);
2083
2342
  next = injectCustomHtml(next, state.customHtml);
2084
2343
  return next;
2085
2344
  }
2086
2345
 
2346
+ function injectFaviconLinks(html, favicon) {
2347
+ const links = buildFaviconLinks(favicon);
2348
+ if (!links) {
2349
+ return html;
2350
+ }
2351
+
2352
+ return html.replace('</head>', `${links}\n</head>`);
2353
+ }
2354
+
2355
+ function buildFaviconLinks(favicon) {
2356
+ if (!favicon || typeof favicon !== 'object') {
2357
+ return '';
2358
+ }
2359
+
2360
+ const lines = [];
2361
+ if (normalizeOptionalString(favicon.icon)) {
2362
+ lines.push(` <link rel="icon" href="${escapeHtml(favicon.icon)}" sizes="any">`);
2363
+ }
2364
+ if (normalizeOptionalString(favicon.svg)) {
2365
+ lines.push(` <link rel="icon" href="${escapeHtml(favicon.svg)}" type="image/svg+xml">`);
2366
+ }
2367
+ if (normalizeOptionalString(favicon.png)) {
2368
+ lines.push(` <link rel="icon" href="${escapeHtml(favicon.png)}" type="image/png">`);
2369
+ }
2370
+ if (normalizeOptionalString(favicon.apple_touch_icon)) {
2371
+ lines.push(` <link rel="apple-touch-icon" href="${escapeHtml(favicon.apple_touch_icon)}">`);
2372
+ }
2373
+
2374
+ return lines.join('\n');
2375
+ }
2376
+
2087
2377
  function injectCustomCssAssetLink(html, href) {
2088
2378
  if (!normalizeOptionalString(href)) {
2089
2379
  return html;
@@ -2165,13 +2455,23 @@ function buildFeedXml(site, emitted, generatedAt) {
2165
2455
  }
2166
2456
 
2167
2457
  function buildRobotsTxt(site) {
2168
- const lines = ['User-agent: *', 'Allow: /'];
2458
+ const lines = ['User-agent: *'];
2459
+ if (site.indexing === false) {
2460
+ lines.push('Disallow: /');
2461
+ return `${lines.join('\n')}\n`;
2462
+ }
2463
+
2464
+ lines.push('Allow: /');
2169
2465
  if (site.url) {
2170
2466
  lines.push('', `Sitemap: ${resolveSiteUrl(site.url, '/sitemap.xml')}`);
2171
2467
  }
2172
2468
  return `${lines.join('\n')}\n`;
2173
2469
  }
2174
2470
 
2471
+ function shouldGenerateRobotsTxt(options) {
2472
+ return options.generateSpecialFiles && options.generateRobotsTxt !== false;
2473
+ }
2474
+
2175
2475
  function getContentType(assetPath) {
2176
2476
  const ext = assetPath.split('.').pop()?.toLowerCase();
2177
2477
  const contentTypes = {
@@ -11,12 +11,7 @@ export class AssetProcessor {
11
11
  }
12
12
 
13
13
  async processJavaScript(js) {
14
- return js
15
- .replace(/\/\/(?![^\r\n]*https?:)[^\r\n]*/g, '')
16
- .replace(/\/\*[\s\S]*?\*\//g, '')
17
- .replace(/\s+/g, ' ')
18
- .replace(/\s*([{}();,=+\-*/<>!&|])\s*/g, '$1')
19
- .trim();
14
+ return js;
20
15
  }
21
16
 
22
17
  generateAssetHash(content) {
package/src/index.d.ts CHANGED
@@ -4,7 +4,6 @@ import type { ThemeManifest } from '@zeropress/theme-validator';
4
4
  export interface ThemePackage {
5
5
  metadata: ThemeManifest & {
6
6
  thumbnail?: string;
7
- settings?: Record<string, unknown>;
8
7
  };
9
8
  templates: Map<string, string>;
10
9
  partials: Map<string, string>;
@@ -23,7 +22,14 @@ export interface BuildWriter {
23
22
 
24
23
  export interface BuildOptions {
25
24
  assetHashing?: boolean;
25
+ favicon?: {
26
+ icon?: string;
27
+ svg?: string;
28
+ png?: string;
29
+ apple_touch_icon?: string;
30
+ };
26
31
  generateSpecialFiles?: boolean;
32
+ generateRobotsTxt?: boolean;
27
33
  writeManifest?: boolean;
28
34
  }
29
35
 
@@ -51,7 +51,6 @@ export async function loadThemePackageFromDir(themeDir) {
51
51
  metadata: {
52
52
  ...manifest,
53
53
  thumbnail: themeJson.thumbnail,
54
- settings: themeJson.settings || {},
55
54
  },
56
55
  templates,
57
56
  partials,