@zeropress/build-core 0.5.0 → 0.5.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeropress/build-core",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Shared ZeroPress build core",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -34,8 +34,8 @@
34
34
  },
35
35
  "dependencies": {
36
36
  "@zeropress/slug-policy": "^0.1.0",
37
- "@zeropress/preview-data-validator": "0.5.0",
38
- "@zeropress/theme-validator": "0.5.1",
37
+ "@zeropress/preview-data-validator": "0.5.1",
38
+ "@zeropress/theme-validator": "0.5.2",
39
39
  "highlight.js": "^11.11.1",
40
40
  "markdown-it": "^14.1.0",
41
41
  "markdown-it-anchor": "^9.2.0"
@@ -24,6 +24,14 @@ const DEFAULT_PERMALINKS = Object.freeze({
24
24
  categories: '/categories/:slug/',
25
25
  tags: '/tags/:slug/',
26
26
  });
27
+ const DEFAULT_FRONT_PAGE = Object.freeze({
28
+ type: 'theme_index',
29
+ });
30
+ const DEFAULT_POST_INDEX = Object.freeze({
31
+ enabled: true,
32
+ path: '/',
33
+ paginate: true,
34
+ });
27
35
  const PERMALINK_OUTPUT_STYLES = new Set(['directory', 'html-extension']);
28
36
  const COMMENT_POLICY_OUTPUT_PATH = '_zeropress/comment-policy.json';
29
37
  const OUTPUT_PATH_CONTROL_CHAR_PATTERN = /[\u0000-\u001F\u007F]/;
@@ -34,6 +42,10 @@ export async function buildSite(input) {
34
42
  const state = await createBuildState(input, options);
35
43
  assertPlannedOutputPathsSafe(state);
36
44
 
45
+ if (state.renderData.frontPageRoute) {
46
+ await renderFrontPage(state, state.renderData.frontPageRoute);
47
+ }
48
+
37
49
  for (const route of state.renderData.indexRoutes) {
38
50
  await renderRoute(state, 'index', route);
39
51
  }
@@ -129,6 +141,7 @@ async function createBuildState(input, options) {
129
141
  options,
130
142
  generatedAt: new Date(),
131
143
  emitted: {
144
+ frontPage: null,
132
145
  indexRoutes: [],
133
146
  archiveRoutes: [],
134
147
  categoryRoutes: [],
@@ -175,6 +188,10 @@ async function finalizeBuildResult(writer, summaries, options) {
175
188
 
176
189
  async function renderRoute(state, templateName, route) {
177
190
  const currentUrl = routePathToPublicUrl(route.path, state.previewData.site.permalinks.output_style);
191
+ const routeContext = buildRouteContext(route.route_type || templateName, currentUrl, {
192
+ isFrontPage: route.is_front_page === true,
193
+ isPostIndex: route.is_post_index === true,
194
+ });
178
195
  let html = await state.engine.render(
179
196
  templateName,
180
197
  {
@@ -182,6 +199,7 @@ async function renderRoute(state, templateName, route) {
182
199
  widgets: state.widgets,
183
200
  taxonomies: state.renderData.taxonomies,
184
201
  ...route,
202
+ route: routeContext,
185
203
  meta: buildPageMeta(state.previewData.site, {
186
204
  currentUrl,
187
205
  title: state.previewData.site.title,
@@ -197,6 +215,87 @@ async function renderRoute(state, templateName, route) {
197
215
  recordRouteEmission(state, templateName, route, currentUrl);
198
216
  }
199
217
 
218
+ async function renderFrontPage(state, route) {
219
+ const currentUrl = '/';
220
+ const routeContext = buildRouteContext('front_page', currentUrl, {
221
+ isFrontPage: true,
222
+ isPostIndex: false,
223
+ });
224
+
225
+ if (route.front_page_type === 'standalone_html') {
226
+ await writeOutput(state.writer, state.summaries, 'index.html', route.html, 'text/html');
227
+ state.emitted.frontPage = {
228
+ url: currentUrl,
229
+ title: state.previewData.site.title,
230
+ description: state.previewData.site.description,
231
+ includeInFeed: false,
232
+ };
233
+ return;
234
+ }
235
+
236
+ if (route.front_page_type === 'page') {
237
+ const page = {
238
+ ...route.page,
239
+ url: currentUrl,
240
+ };
241
+ let html = await state.engine.render(
242
+ 'page',
243
+ {
244
+ menus: state.previewData.menus,
245
+ widgets: state.widgets,
246
+ taxonomies: state.renderData.taxonomies,
247
+ page,
248
+ route: routeContext,
249
+ meta: buildPageMeta(state.previewData.site, {
250
+ currentUrl,
251
+ title: buildDocumentTitle(page.title, state.previewData.site.title),
252
+ description: page.excerpt,
253
+ ogType: 'website',
254
+ image: page.featured_image,
255
+ }),
256
+ },
257
+ createRenderContext(state.previewData.site, currentUrl),
258
+ );
259
+ html = state.assetProcessor.updateAssetReferences(html, state.assetMap);
260
+ html = injectSiteCustomizations(html, state);
261
+ await writeOutput(state.writer, state.summaries, 'index.html', html, 'text/html');
262
+ state.emitted.frontPage = {
263
+ url: currentUrl,
264
+ title: page.title,
265
+ description: page.excerpt,
266
+ includeInFeed: false,
267
+ };
268
+ return;
269
+ }
270
+
271
+ let html = await state.engine.render(
272
+ 'index',
273
+ {
274
+ menus: state.previewData.menus,
275
+ widgets: state.widgets,
276
+ taxonomies: state.renderData.taxonomies,
277
+ ...route,
278
+ route: routeContext,
279
+ meta: buildPageMeta(state.previewData.site, {
280
+ currentUrl,
281
+ title: state.previewData.site.title,
282
+ description: state.previewData.site.description,
283
+ ogType: 'website',
284
+ }),
285
+ },
286
+ createRenderContext(state.previewData.site, currentUrl),
287
+ );
288
+ html = state.assetProcessor.updateAssetReferences(html, state.assetMap);
289
+ html = injectSiteCustomizations(html, state);
290
+ await writeOutput(state.writer, state.summaries, 'index.html', html, 'text/html');
291
+ state.emitted.frontPage = {
292
+ url: currentUrl,
293
+ title: state.previewData.site.title,
294
+ description: state.previewData.site.description,
295
+ includeInFeed: false,
296
+ };
297
+ }
298
+
200
299
  async function renderPost(state, post) {
201
300
  const currentUrl = post.url;
202
301
  let html = await state.engine.render(
@@ -206,6 +305,7 @@ async function renderPost(state, post) {
206
305
  widgets: state.widgets,
207
306
  taxonomies: state.renderData.taxonomies,
208
307
  post,
308
+ route: buildRouteContext('post', currentUrl),
209
309
  meta: buildPageMeta(state.previewData.site, {
210
310
  currentUrl,
211
311
  title: buildDocumentTitle(post.title, state.previewData.site.title),
@@ -233,6 +333,8 @@ async function renderPost(state, post) {
233
333
 
234
334
  async function renderPage(state, page) {
235
335
  const currentUrl = page.url;
336
+ const outputPath = pageToOutputPath(page, state.previewData.site.permalinks.output_style);
337
+ const canonicalUrl = normalizeOptionalString(page.canonical_url) || currentUrl;
236
338
  let html = await state.engine.render(
237
339
  'page',
238
340
  {
@@ -240,8 +342,10 @@ async function renderPage(state, page) {
240
342
  widgets: state.widgets,
241
343
  taxonomies: state.renderData.taxonomies,
242
344
  page,
345
+ route: buildRouteContext('page', currentUrl),
243
346
  meta: buildPageMeta(state.previewData.site, {
244
347
  currentUrl,
348
+ canonicalUrl,
245
349
  title: buildDocumentTitle(page.title, state.previewData.site.title),
246
350
  description: page.excerpt,
247
351
  ogType: 'website',
@@ -252,12 +356,13 @@ async function renderPage(state, page) {
252
356
  );
253
357
  html = state.assetProcessor.updateAssetReferences(html, state.assetMap);
254
358
  html = injectSiteCustomizations(html, state);
255
- await writeOutput(state.writer, state.summaries, routePathToOutputPath(page.url, state.previewData.site.permalinks.output_style), html, 'text/html');
359
+ await writeOutput(state.writer, state.summaries, outputPath, html, 'text/html');
256
360
  state.emitted.pages.push({
257
361
  url: currentUrl,
258
362
  title: page.title,
259
363
  description: page.excerpt,
260
364
  status: page.status,
365
+ includeInSitemap: page.omit_from_sitemap !== true,
261
366
  });
262
367
  }
263
368
 
@@ -272,6 +377,7 @@ async function maybeRenderNotFoundPage(state) {
272
377
  menus: state.previewData.menus,
273
378
  widgets: state.widgets,
274
379
  taxonomies: state.renderData.taxonomies,
380
+ route: buildRouteContext('not_found', '/404.html'),
275
381
  meta: buildPageMeta(state.previewData.site, {
276
382
  currentUrl: '/404.html',
277
383
  title: state.previewData.site.title,
@@ -299,6 +405,9 @@ function normalizePreviewData(previewData) {
299
405
  locale: normalizeLocale(previewData.site.locale || DEFAULT_LOCALE),
300
406
  disallowComments: previewData.site.disallowComments === true,
301
407
  permalinks: normalizePermalinks(previewData.site.permalinks),
408
+ front_page: normalizeFrontPage(previewData.site.front_page),
409
+ post_index: normalizePostIndex(previewData.site.post_index),
410
+ footer: normalizeSiteFooter(previewData.site.footer),
302
411
  };
303
412
 
304
413
  return {
@@ -350,6 +459,22 @@ function normalizeWidgetAreas(widgetAreas, mediaBaseUrl) {
350
459
  );
351
460
  }
352
461
 
462
+ function normalizeSiteFooter(footer) {
463
+ const source = footer && typeof footer === 'object' && !Array.isArray(footer) ? footer : {};
464
+ const attribution = source.attribution && typeof source.attribution === 'object' && !Array.isArray(source.attribution)
465
+ ? source.attribution
466
+ : {};
467
+
468
+ return {
469
+ ...source,
470
+ copyright_text: normalizeOptionalString(source.copyright_text),
471
+ attribution: {
472
+ ...attribution,
473
+ enabled: attribution.enabled !== false,
474
+ },
475
+ };
476
+ }
477
+
353
478
  function normalizeWidgetItem(item, mediaBaseUrl) {
354
479
  const normalizedItem = {
355
480
  ...item,
@@ -406,8 +531,37 @@ function normalizePermalinks(permalinks) {
406
531
  };
407
532
  }
408
533
 
534
+ function normalizeFrontPage(frontPage) {
535
+ if (!frontPage || typeof frontPage !== 'object') {
536
+ return { ...DEFAULT_FRONT_PAGE };
537
+ }
538
+
539
+ const type = ['theme_index', 'page', 'standalone_html'].includes(frontPage.type)
540
+ ? frontPage.type
541
+ : DEFAULT_FRONT_PAGE.type;
542
+
543
+ return {
544
+ type,
545
+ ...(type === 'page' ? { page_slug: normalizeOptionalString(frontPage.page_slug) } : {}),
546
+ ...(type === 'standalone_html' ? { html: normalizeOptionalRawString(frontPage.html) } : {}),
547
+ };
548
+ }
549
+
550
+ function normalizePostIndex(postIndex) {
551
+ if (!postIndex || typeof postIndex !== 'object') {
552
+ return { ...DEFAULT_POST_INDEX };
553
+ }
554
+
555
+ return {
556
+ enabled: postIndex.enabled !== false,
557
+ path: normalizeNonEmptyString(postIndex.path, DEFAULT_POST_INDEX.path),
558
+ paginate: postIndex.paginate !== false,
559
+ };
560
+ }
561
+
409
562
  function createRenderData(previewData, themeMetadata = {}) {
410
563
  const themeSupportsComments = themeMetadata?.features?.comments === true;
564
+ const themeSupportsPostIndex = themeMetadata?.features?.postIndex !== false;
411
565
  const authorsById = new Map(previewData.content.authors.map((author) => [author.id, author]));
412
566
  const categoriesBySlug = new Map(previewData.content.categories.map((category) => [category.slug, category]));
413
567
  const tagsBySlug = new Map(previewData.content.tags.map((tag) => [tag.slug, tag]));
@@ -436,24 +590,38 @@ function createRenderData(previewData, themeMetadata = {}) {
436
590
  }));
437
591
  const pages = previewData.content.pages.map((page) => preparePage(page, previewData.site));
438
592
  const postBySlug = new Map(posts.map((post) => [post.slug, post]));
593
+ const frontPage = previewData.site.front_page;
594
+ const postIndex = previewData.site.post_index;
595
+ const effectivePostIndexEnabled = postIndex.enabled !== false && themeSupportsPostIndex;
596
+ const effectivePostIndexPaginate = effectivePostIndexEnabled && postIndex.paginate !== false;
597
+ const postIndexBasePath = normalizeRoutePath(postIndex.path || DEFAULT_POST_INDEX.path);
598
+
599
+ if (frontPage.type !== 'theme_index' && effectivePostIndexEnabled && postIndexBasePath === '/') {
600
+ throw new Error('Invalid front page configuration: site.front_page occupies "/" so site.post_index.path must not be "/". Set site.post_index.path to a non-root path or disable site.post_index.');
601
+ }
602
+
603
+ const frontPageRoute = buildFrontPageRoute(frontPage, pages, effectivePostIndexEnabled, postIndexBasePath);
604
+ const pageFrontPageSlug = frontPage.type === 'page' ? frontPage.page_slug : '';
605
+ const preparedPages = pageFrontPageSlug
606
+ ? pages.filter((page) => page.slug !== pageFrontPageSlug)
607
+ : pages;
439
608
 
440
609
  return {
441
610
  posts,
442
- pages,
611
+ pages: preparedPages,
443
612
  postBySlug,
444
613
  taxonomies: buildGlobalTaxonomies(previewData, categoryCountBySlug, tagCountBySlug),
445
- indexRoutes: buildPaginatedCollection({
614
+ frontPageRoute,
615
+ indexRoutes: buildPostIndexRoutes({
616
+ enabled: effectivePostIndexEnabled,
617
+ paginate: effectivePostIndexPaginate,
446
618
  items: previewData.content.posts,
447
619
  postsPerPage: previewData.site.postsPerPage,
448
- basePath: '/',
620
+ basePath: postIndexBasePath,
449
621
  outputStyle: previewData.site.permalinks.output_style,
450
- }).map((entry) => ({
451
- path: entry.path,
452
- page: entry.page,
453
- totalPages: entry.totalPages,
454
- posts: buildStructuredPostCollection(entry.items, postBySlug),
455
- pagination: buildStructuredPagination(entry.paginationData),
456
- })),
622
+ postBySlug,
623
+ frontPage,
624
+ }),
457
625
  archiveRoutes: buildPaginatedCollection({
458
626
  items: previewData.content.posts,
459
627
  postsPerPage: previewData.site.postsPerPage,
@@ -500,6 +668,84 @@ function buildGlobalTaxonomies(previewData, categoryCountBySlug, tagCountBySlug)
500
668
  };
501
669
  }
502
670
 
671
+ function buildFrontPageRoute(frontPage, pages, effectivePostIndexEnabled, postIndexBasePath) {
672
+ if (frontPage.type === 'theme_index') {
673
+ if (effectivePostIndexEnabled && postIndexBasePath === '/') {
674
+ return null;
675
+ }
676
+
677
+ return {
678
+ path: '/',
679
+ front_page_type: 'theme_index',
680
+ posts: buildStructuredPostCollection([], new Map()),
681
+ pagination: buildStructuredPagination(buildDisabledPaginationData(0)),
682
+ };
683
+ }
684
+
685
+ if (frontPage.type === 'page') {
686
+ const page = pages.find((entry) => entry.slug === frontPage.page_slug);
687
+ if (!page) {
688
+ throw new Error(`Invalid front page configuration: site.front_page.page_slug "${frontPage.page_slug}" does not match a page.`);
689
+ }
690
+
691
+ return {
692
+ path: '/',
693
+ front_page_type: 'page',
694
+ page,
695
+ };
696
+ }
697
+
698
+ if (frontPage.type === 'standalone_html') {
699
+ if (!normalizeOptionalRawString(frontPage.html)) {
700
+ throw new Error('Invalid front page configuration: site.front_page.html is required for standalone_html.');
701
+ }
702
+
703
+ return {
704
+ path: '/',
705
+ front_page_type: 'standalone_html',
706
+ html: frontPage.html,
707
+ };
708
+ }
709
+
710
+ return null;
711
+ }
712
+
713
+ function buildPostIndexRoutes(options) {
714
+ if (!options.enabled) {
715
+ return [];
716
+ }
717
+
718
+ if (!options.paginate) {
719
+ const items = options.items.slice(0, options.postsPerPage);
720
+ return [{
721
+ path: options.basePath,
722
+ route_type: 'post_index',
723
+ is_front_page: options.basePath === '/' && options.frontPage.type === 'theme_index',
724
+ is_post_index: true,
725
+ page: 1,
726
+ totalPages: 1,
727
+ posts: buildStructuredPostCollection(items, options.postBySlug),
728
+ pagination: buildStructuredPagination(buildDisabledPaginationData(options.items.length)),
729
+ }];
730
+ }
731
+
732
+ return buildPaginatedCollection({
733
+ items: options.items,
734
+ postsPerPage: options.postsPerPage,
735
+ basePath: options.basePath,
736
+ outputStyle: options.outputStyle,
737
+ }).map((entry) => ({
738
+ path: entry.path,
739
+ route_type: 'post_index',
740
+ is_front_page: entry.path === '/' && options.frontPage.type === 'theme_index',
741
+ is_post_index: true,
742
+ page: entry.page,
743
+ totalPages: entry.totalPages,
744
+ posts: buildStructuredPostCollection(entry.items, options.postBySlug),
745
+ pagination: buildStructuredPagination(entry.paginationData),
746
+ }));
747
+ }
748
+
503
749
  function buildGlobalTaxonomyItems(site, permalinkKind, items, countBySlug) {
504
750
  return items.map((item) => ({
505
751
  name: item.name,
@@ -904,6 +1150,7 @@ function buildPaginationData(currentPage, totalPages, totalPosts, basePath, outp
904
1150
  const buildPageUrl = (page) => routePathToPublicUrl(buildPaginatedPath(basePath, page), outputStyle);
905
1151
 
906
1152
  return {
1153
+ enabled: true,
907
1154
  currentPage,
908
1155
  totalPages,
909
1156
  totalPosts,
@@ -922,8 +1169,23 @@ function buildPaginationData(currentPage, totalPages, totalPosts, basePath, outp
922
1169
  };
923
1170
  }
924
1171
 
1172
+ function buildDisabledPaginationData(totalPosts) {
1173
+ return {
1174
+ enabled: false,
1175
+ currentPage: 1,
1176
+ totalPages: 1,
1177
+ totalPosts,
1178
+ hasNext: false,
1179
+ hasPrev: false,
1180
+ nextUrl: undefined,
1181
+ prevUrl: undefined,
1182
+ pages: [],
1183
+ };
1184
+ }
1185
+
925
1186
  function buildStructuredPagination(paginationData) {
926
1187
  return {
1188
+ enabled: paginationData.enabled !== false,
927
1189
  current_page: paginationData.currentPage,
928
1190
  total_pages: paginationData.totalPages,
929
1191
  total_items: paginationData.totalPosts,
@@ -1367,10 +1629,20 @@ function createRenderContext(site, currentUrl) {
1367
1629
  };
1368
1630
  }
1369
1631
 
1632
+ function buildRouteContext(type, url, options = {}) {
1633
+ return {
1634
+ type,
1635
+ is_front_page: options.isFrontPage === true,
1636
+ is_post_index: options.isPostIndex === true,
1637
+ path: url,
1638
+ url,
1639
+ };
1640
+ }
1641
+
1370
1642
  function buildPageMeta(site, options = {}) {
1371
1643
  const resolvedTitle = normalizeNonEmptyString(options.title, site.title);
1372
1644
  const resolvedDescription = normalizeOptionalString(options.description);
1373
- const canonicalUrl = resolveMetaCanonicalUrl(site, options.currentUrl);
1645
+ const canonicalUrl = resolveMetaCanonicalUrl(site, options.canonicalUrl || options.currentUrl);
1374
1646
  const ogImage = resolveMetaImageUrl(options.image);
1375
1647
  const ogType = normalizeNonEmptyString(options.ogType, 'website');
1376
1648
  const publishedTime = normalizeOptionalString(options.publishedTime);
@@ -1550,6 +1822,22 @@ function routePathToPublicUrl(routePath, outputStyle = DEFAULT_PERMALINKS.output
1550
1822
  return normalizedPath;
1551
1823
  }
1552
1824
 
1825
+ function pagePathToPublicUrl(routePath, outputStyle = DEFAULT_PERMALINKS.output_style) {
1826
+ const normalizedPath = normalizeRoutePath(routePath);
1827
+ if (outputStyle !== 'html-extension') {
1828
+ return routePathToPublicUrl(normalizedPath, outputStyle);
1829
+ }
1830
+
1831
+ const withoutTrailingSlash = normalizedPath.replace(/\/$/, '');
1832
+ if (withoutTrailingSlash === '/index') {
1833
+ return '/';
1834
+ }
1835
+ if (withoutTrailingSlash.endsWith('/index')) {
1836
+ return `${withoutTrailingSlash.slice(0, -'/index'.length)}/`;
1837
+ }
1838
+ return withoutTrailingSlash;
1839
+ }
1840
+
1553
1841
  function normalizeRoutePath(routePath) {
1554
1842
  if (!routePath || routePath === '/') {
1555
1843
  return '/';
@@ -1560,7 +1848,7 @@ function normalizeRoutePath(routePath) {
1560
1848
 
1561
1849
  function resolvePagePermalink(site, page) {
1562
1850
  if (normalizeOptionalString(page.path)) {
1563
- return buildRouteInfo(page.path, site.permalinks.output_style);
1851
+ return buildRouteInfo(page.path, site.permalinks.output_style, { pagePath: true });
1564
1852
  }
1565
1853
  return resolvePermalink(site, 'pages', page);
1566
1854
  }
@@ -1570,15 +1858,21 @@ function resolvePermalink(site, kind, item) {
1570
1858
  return buildRouteInfo(applyPermalinkPattern(pattern, kind, item, site), site.permalinks.output_style);
1571
1859
  }
1572
1860
 
1573
- function buildRouteInfo(routePath, outputStyle) {
1861
+ function buildRouteInfo(routePath, outputStyle, options = {}) {
1574
1862
  const path = normalizeRoutePath(routePath);
1575
1863
  return {
1576
1864
  path,
1577
- url: routePathToPublicUrl(path, outputStyle),
1865
+ url: options.pagePath ? pagePathToPublicUrl(path, outputStyle) : routePathToPublicUrl(path, outputStyle),
1578
1866
  outputPath: routePathToOutputPath(path, outputStyle),
1579
1867
  };
1580
1868
  }
1581
1869
 
1870
+ function pageToOutputPath(page, outputStyle) {
1871
+ return normalizeOptionalString(page.path)
1872
+ ? routePathToOutputPath(page.path, outputStyle)
1873
+ : routePathToOutputPath(page.url, outputStyle);
1874
+ }
1875
+
1582
1876
  function applyPermalinkPattern(pattern, kind, item, site) {
1583
1877
  const tokenValues = buildPermalinkTokenValues(kind, item, site);
1584
1878
  const body = String(pattern || '').replace(/^\/+|\/+$/g, '');
@@ -1635,7 +1929,7 @@ function assertPlannedOutputPathsSafe(state) {
1635
1929
  }
1636
1930
 
1637
1931
  for (const page of state.renderData.pages) {
1638
- assertSafeSlugDerivedOutputPath(page.slug, routePathToOutputPath(page.url, outputStyle));
1932
+ assertSafeSlugDerivedOutputPath(page.slug, pageToOutputPath(page, outputStyle));
1639
1933
  }
1640
1934
 
1641
1935
  for (const category of state.previewData.content.categories) {
@@ -1647,6 +1941,10 @@ function assertPlannedOutputPathsSafe(state) {
1647
1941
  }
1648
1942
 
1649
1943
  const routeEntries = [
1944
+ ...(state.renderData.frontPageRoute ? [{
1945
+ url: '/',
1946
+ outputPath: 'index.html',
1947
+ }] : []),
1650
1948
  ...state.renderData.indexRoutes.map((route) => ({
1651
1949
  url: routePathToPublicUrl(route.path, outputStyle),
1652
1950
  outputPath: routePathToOutputPath(route.path, outputStyle),
@@ -1669,7 +1967,7 @@ function assertPlannedOutputPathsSafe(state) {
1669
1967
  })),
1670
1968
  ...state.renderData.pages.map((page) => ({
1671
1969
  url: page.url,
1672
- outputPath: routePathToOutputPath(page.url, outputStyle),
1970
+ outputPath: pageToOutputPath(page, outputStyle),
1673
1971
  })),
1674
1972
  ];
1675
1973
  assertUniqueRoutes(routeEntries);
@@ -1811,12 +2109,19 @@ function injectCustomHtml(html, customHtml) {
1811
2109
 
1812
2110
  function buildSitemapXml(site, emitted, generatedAt) {
1813
2111
  const entries = [
2112
+ ...(emitted.frontPage
2113
+ ? [{
2114
+ url: emitted.frontPage.url,
2115
+ changefreq: 'daily',
2116
+ priority: 1.0,
2117
+ }]
2118
+ : []),
1814
2119
  ...emitted.indexRoutes
1815
- .filter((route) => route.url === '/')
2120
+ .filter((route) => route.page === 1)
1816
2121
  .map((route) => ({
1817
2122
  url: route.url,
1818
2123
  changefreq: 'daily',
1819
- priority: 1.0,
2124
+ priority: route.url === '/' ? 1.0 : 0.7,
1820
2125
  })),
1821
2126
  ...emitted.posts.map((post) => ({
1822
2127
  url: post.url,
@@ -1824,11 +2129,13 @@ function buildSitemapXml(site, emitted, generatedAt) {
1824
2129
  changefreq: 'weekly',
1825
2130
  priority: 0.8,
1826
2131
  })),
1827
- ...emitted.pages.map((page) => ({
1828
- url: page.url,
1829
- changefreq: 'monthly',
1830
- priority: 0.7,
1831
- })),
2132
+ ...emitted.pages
2133
+ .filter((page) => page.includeInSitemap !== false)
2134
+ .map((page) => ({
2135
+ url: page.url,
2136
+ changefreq: 'monthly',
2137
+ priority: 0.7,
2138
+ })),
1832
2139
  ];
1833
2140
 
1834
2141
  const body = entries.map((entry) => {
@@ -3,6 +3,13 @@ import hljs from 'highlight.js';
3
3
  import anchor from 'markdown-it-anchor';
4
4
 
5
5
  const TOC_LEVELS = new Set([2, 3, 4]);
6
+ const ALERT_TYPES = new Map([
7
+ ['NOTE', { className: 'note', title: 'Note' }],
8
+ ['TIP', { className: 'tip', title: 'Tip' }],
9
+ ['IMPORTANT', { className: 'important', title: 'Important' }],
10
+ ['WARNING', { className: 'warning', title: 'Warning' }],
11
+ ['CAUTION', { className: 'caution', title: 'Caution' }],
12
+ ]);
6
13
 
7
14
  export function renderDocumentContent(content, documentType = 'markdown') {
8
15
  return renderDocument(content, documentType).html;
@@ -62,6 +69,8 @@ function createMarkdownRenderer(toc) {
62
69
  },
63
70
  });
64
71
 
72
+ markdown.use(markdownTaskLists);
73
+ markdown.use(markdownAlerts);
65
74
  markdown.use(anchor, {
66
75
  slugify: slugify,
67
76
  callback(token, { slug, title }) {
@@ -82,6 +91,189 @@ function createMarkdownRenderer(toc) {
82
91
  return markdown;
83
92
  }
84
93
 
94
+ function markdownTaskLists(markdown) {
95
+ markdown.core.ruler.after('inline', 'zeropress_task_lists', (state) => {
96
+ const listStack = [];
97
+ const listItemStack = [];
98
+
99
+ for (let index = 0; index < state.tokens.length; index += 1) {
100
+ const token = state.tokens[index];
101
+
102
+ if (token.type === 'bullet_list_open' || token.type === 'ordered_list_open') {
103
+ listStack.push(index);
104
+ continue;
105
+ }
106
+
107
+ if (token.type === 'bullet_list_close' || token.type === 'ordered_list_close') {
108
+ listStack.pop();
109
+ continue;
110
+ }
111
+
112
+ if (token.type === 'list_item_open') {
113
+ listItemStack.push({
114
+ index,
115
+ listIndex: listStack[listStack.length - 1],
116
+ });
117
+ continue;
118
+ }
119
+
120
+ if (token.type === 'list_item_close') {
121
+ listItemStack.pop();
122
+ continue;
123
+ }
124
+
125
+ if (token.type !== 'inline' || listItemStack.length === 0) {
126
+ continue;
127
+ }
128
+
129
+ const markerMatch = token.content.match(/^\s*\[([ xX])\](?:\s+|$)/);
130
+ if (!markerMatch) {
131
+ continue;
132
+ }
133
+
134
+ const listItem = listItemStack[listItemStack.length - 1];
135
+ const listToken = state.tokens[listItem.listIndex];
136
+ const listItemToken = state.tokens[listItem.index];
137
+ if (!listToken || !listItemToken) {
138
+ continue;
139
+ }
140
+
141
+ addTokenClass(listToken, 'contains-task-list');
142
+ addTokenClass(listItemToken, 'task-list-item');
143
+
144
+ const checkboxToken = new state.Token('html_inline', '', 0);
145
+ const checked = markerMatch[1].toLowerCase() === 'x' ? ' checked' : '';
146
+ checkboxToken.content = `<input class="task-list-item-checkbox" type="checkbox"${checked} disabled> `;
147
+
148
+ const strippedContent = token.content.slice(markerMatch[0].length);
149
+ token.content = strippedContent;
150
+ token.children = [];
151
+ state.md.inline.parse(strippedContent, state.md, state.env, token.children);
152
+ token.children.unshift(checkboxToken);
153
+ }
154
+ });
155
+ }
156
+
157
+ function markdownAlerts(markdown) {
158
+ markdown.core.ruler.after('inline', 'zeropress_alerts', (state) => {
159
+ for (let index = 0; index < state.tokens.length; index += 1) {
160
+ const token = state.tokens[index];
161
+ if (token.type !== 'blockquote_open') {
162
+ continue;
163
+ }
164
+
165
+ const closeIndex = findMatchingBlockquoteClose(state.tokens, index);
166
+ if (closeIndex === -1) {
167
+ continue;
168
+ }
169
+
170
+ const firstParagraph = findFirstParagraph(state.tokens, index + 1, closeIndex);
171
+ if (!firstParagraph) {
172
+ continue;
173
+ }
174
+
175
+ const inlineToken = state.tokens[firstParagraph.inlineIndex];
176
+ const alert = stripAlertMarker(inlineToken, state);
177
+ if (!alert) {
178
+ continue;
179
+ }
180
+
181
+ token.tag = 'aside';
182
+ token.attrSet('class', `zp-alert zp-alert--${alert.className}`);
183
+ token.attrSet('role', 'note');
184
+ state.tokens[closeIndex].tag = 'aside';
185
+
186
+ if (inlineToken.content.trim() === '') {
187
+ state.tokens.splice(firstParagraph.openIndex, 3);
188
+ }
189
+
190
+ state.tokens.splice(index + 1, 0, ...createAlertTitleTokens(state, alert.title));
191
+ index += 3;
192
+ }
193
+ });
194
+ }
195
+
196
+ function findMatchingBlockquoteClose(tokens, openIndex) {
197
+ let depth = 0;
198
+
199
+ for (let index = openIndex; index < tokens.length; index += 1) {
200
+ if (tokens[index].type === 'blockquote_open') {
201
+ depth += 1;
202
+ continue;
203
+ }
204
+
205
+ if (tokens[index].type === 'blockquote_close') {
206
+ depth -= 1;
207
+ if (depth === 0) {
208
+ return index;
209
+ }
210
+ }
211
+ }
212
+
213
+ return -1;
214
+ }
215
+
216
+ function findFirstParagraph(tokens, startIndex, endIndex) {
217
+ if (
218
+ startIndex + 2 < endIndex
219
+ && tokens[startIndex].type === 'paragraph_open'
220
+ && tokens[startIndex + 1].type === 'inline'
221
+ && tokens[startIndex + 2].type === 'paragraph_close'
222
+ ) {
223
+ return {
224
+ openIndex: startIndex,
225
+ inlineIndex: startIndex + 1,
226
+ closeIndex: startIndex + 2,
227
+ };
228
+ }
229
+
230
+ return null;
231
+ }
232
+
233
+ function stripAlertMarker(inlineToken, state) {
234
+ const markerMatch = inlineToken.content.match(/^\s*\[!([A-Za-z]+)\][ \t]*(?:\n)?/);
235
+ if (!markerMatch) {
236
+ return null;
237
+ }
238
+
239
+ const alertType = ALERT_TYPES.get(markerMatch[1].toUpperCase());
240
+ if (!alertType) {
241
+ return null;
242
+ }
243
+
244
+ const strippedContent = inlineToken.content.slice(markerMatch[0].length);
245
+ inlineToken.content = strippedContent;
246
+ inlineToken.children = [];
247
+ state.md.inline.parse(strippedContent, state.md, state.env, inlineToken.children);
248
+
249
+ return alertType;
250
+ }
251
+
252
+ function createAlertTitleTokens(state, title) {
253
+ const openToken = new state.Token('paragraph_open', 'p', 1);
254
+ openToken.attrSet('class', 'zp-alert__title');
255
+
256
+ const inlineToken = new state.Token('inline', '', 0);
257
+ inlineToken.content = title;
258
+ const textToken = new state.Token('text', '', 0);
259
+ textToken.content = title;
260
+ inlineToken.children = [textToken];
261
+
262
+ const closeToken = new state.Token('paragraph_close', 'p', -1);
263
+
264
+ return [openToken, inlineToken, closeToken];
265
+ }
266
+
267
+ function addTokenClass(token, className) {
268
+ const existingValue = token.attrGet('class') || '';
269
+ const existingClasses = existingValue.split(/\s+/).filter(Boolean);
270
+ if (existingClasses.includes(className)) {
271
+ return;
272
+ }
273
+
274
+ token.attrJoin('class', className);
275
+ }
276
+
85
277
  function normalizeDocumentType(value) {
86
278
  return value === 'plaintext' || value === 'html' ? value : 'markdown';
87
279
  }
@@ -113,16 +305,18 @@ function sanitizeHtml(html) {
113
305
  'strong', 'em', 'u', 's', 'code', 'pre',
114
306
  'a', 'img',
115
307
  'ul', 'ol', 'li',
116
- 'blockquote',
308
+ 'blockquote', 'aside',
117
309
  'table', 'thead', 'tbody', 'tr', 'th', 'td',
118
310
  'div', 'span', 'nav',
119
- 'iframe',
311
+ 'iframe', 'input',
120
312
  ]);
121
313
 
122
314
  const allowedAttributes = {
123
315
  a: new Set(['href', 'title', 'class', 'id']),
316
+ aside: new Set(['role', 'class', 'id']),
124
317
  img: new Set(['src', 'alt', 'title', 'class', 'id', 'width', 'height']),
125
318
  iframe: new Set(['src', 'width', 'height', 'frameborder', 'allowfullscreen', 'class']),
319
+ input: new Set(['type', 'checked', 'disabled', 'class', 'id']),
126
320
  '*': new Set(['class', 'id']),
127
321
  };
128
322
 
@@ -163,11 +357,15 @@ function sanitizeHtml(html) {
163
357
  continue;
164
358
  }
165
359
 
360
+ if (normalizedTag === 'input' && attributeName === 'type' && attributeValue !== 'checkbox') {
361
+ continue;
362
+ }
363
+
166
364
  filteredAttributes.push(`${attributeName}="${attributeValue}"`);
167
365
  }
168
366
  }
169
367
 
170
- const isSelfClosing = match.endsWith('/>') || normalizedTag === 'br' || normalizedTag === 'hr' || normalizedTag === 'img';
368
+ const isSelfClosing = match.endsWith('/>') || normalizedTag === 'br' || normalizedTag === 'hr' || normalizedTag === 'img' || normalizedTag === 'input';
171
369
  const attributeSuffix = filteredAttributes.length > 0 ? ` ${filteredAttributes.join(' ')}` : '';
172
370
  return isSelfClosing ? `<${normalizedTag}${attributeSuffix} />` : `<${normalizedTag}${attributeSuffix}>`;
173
371
  });