@zeropress/build-core 0.1.12 → 0.1.14

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.1.12",
3
+ "version": "0.1.14",
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.4.1",
38
- "@zeropress/theme-validator": "^0.3.0",
37
+ "@zeropress/preview-data-validator": "^0.4.3",
38
+ "@zeropress/theme-validator": "^0.3.2",
39
39
  "highlight.js": "^11.11.1",
40
40
  "markdown-it": "^14.1.0",
41
41
  "markdown-it-anchor": "^9.2.0"
@@ -84,10 +84,15 @@ async function createBuildState(input, options) {
84
84
  const summaries = [];
85
85
  const themePackage = input.themePackage;
86
86
  const previewData = normalizePreviewData(input.previewData);
87
+ const renderData = createRenderData(previewData);
87
88
 
88
89
  engine.initialize(themePackage);
89
90
 
90
91
  const assetOutputs = await buildAssetOutputs(themePackage.assets, assetProcessor, options);
92
+ const customCssAsset = await buildCustomCssAsset(previewData.custom_css, assetProcessor, options);
93
+ if (customCssAsset) {
94
+ assetOutputs.push(customCssAsset);
95
+ }
91
96
  const assetMap = new Map(
92
97
  assetOutputs.map((asset) => [`/assets/${asset.originalPath}`, `/${asset.path}`]),
93
98
  );
@@ -95,12 +100,14 @@ async function createBuildState(input, options) {
95
100
  return {
96
101
  writer: input.writer,
97
102
  previewData,
98
- renderData: createRenderData(previewData),
103
+ renderData,
104
+ renderedWidgets: renderWidgetAreas(previewData, renderData),
99
105
  engine,
100
106
  assetProcessor,
101
107
  summaries,
102
108
  assetOutputs,
103
109
  assetMap,
110
+ customCssHref: customCssAsset ? `/${customCssAsset.path}` : '',
104
111
  options,
105
112
  generatedAt: new Date(),
106
113
  emitted: {
@@ -142,6 +149,7 @@ async function renderRoute(state, templateName, route) {
142
149
  templateName,
143
150
  {
144
151
  menus: state.previewData.menus,
152
+ widgets: state.renderedWidgets,
145
153
  ...route,
146
154
  meta: buildPageMeta(state.previewData.site, {
147
155
  currentUrl,
@@ -153,6 +161,7 @@ async function renderRoute(state, templateName, route) {
153
161
  createRenderContext(state.previewData.site, currentUrl),
154
162
  );
155
163
  html = state.assetProcessor.updateAssetReferences(html, state.assetMap);
164
+ html = injectCustomCssAssetLink(html, state.customCssHref);
156
165
  await writeOutput(state.writer, state.summaries, routePathToOutputPath(route.path), html, 'text/html');
157
166
  recordRouteEmission(state, templateName, route, currentUrl);
158
167
  }
@@ -163,6 +172,7 @@ async function renderPost(state, post) {
163
172
  'post',
164
173
  {
165
174
  menus: state.previewData.menus,
175
+ widgets: state.renderedWidgets,
166
176
  post,
167
177
  meta: buildPageMeta(state.previewData.site, {
168
178
  currentUrl,
@@ -177,6 +187,7 @@ async function renderPost(state, post) {
177
187
  createRenderContext(state.previewData.site, currentUrl),
178
188
  );
179
189
  html = state.assetProcessor.updateAssetReferences(html, state.assetMap);
190
+ html = injectCustomCssAssetLink(html, state.customCssHref);
180
191
  if (state.options.injectHtmx) {
181
192
  html = injectHtmxScript(html);
182
193
  }
@@ -197,6 +208,7 @@ async function renderPage(state, page) {
197
208
  'page',
198
209
  {
199
210
  menus: state.previewData.menus,
211
+ widgets: state.renderedWidgets,
200
212
  page,
201
213
  meta: buildPageMeta(state.previewData.site, {
202
214
  currentUrl,
@@ -209,6 +221,7 @@ async function renderPage(state, page) {
209
221
  createRenderContext(state.previewData.site, currentUrl),
210
222
  );
211
223
  html = state.assetProcessor.updateAssetReferences(html, state.assetMap);
224
+ html = injectCustomCssAssetLink(html, state.customCssHref);
212
225
  await writeOutput(state.writer, state.summaries, `${encodeSlugSegment(page.slug)}/index.html`, html, 'text/html');
213
226
  state.emitted.pages.push({
214
227
  url: currentUrl,
@@ -227,6 +240,7 @@ async function maybeRenderNotFoundPage(state) {
227
240
  '404',
228
241
  {
229
242
  menus: state.previewData.menus,
243
+ widgets: state.renderedWidgets,
230
244
  meta: buildPageMeta(state.previewData.site, {
231
245
  currentUrl: '/404.html',
232
246
  title: state.previewData.site.title,
@@ -237,6 +251,7 @@ async function maybeRenderNotFoundPage(state) {
237
251
  createRenderContext(state.previewData.site, '/404.html'),
238
252
  );
239
253
  html = state.assetProcessor.updateAssetReferences(html, state.assetMap);
254
+ html = injectCustomCssAssetLink(html, state.customCssHref);
240
255
  await writeOutput(state.writer, state.summaries, '404.html', html, 'text/html');
241
256
  }
242
257
 
@@ -257,6 +272,8 @@ function normalizePreviewData(previewData) {
257
272
  return {
258
273
  ...previewData,
259
274
  site: normalizedSite,
275
+ widgets: normalizeWidgetAreas(previewData.widgets, normalizedSite.mediaBaseUrl),
276
+ custom_css: normalizeCustomCss(previewData.custom_css),
260
277
  content: {
261
278
  ...previewData.content,
262
279
  authors: previewData.content.authors.map((author) => ({
@@ -281,6 +298,49 @@ function normalizePreviewData(previewData) {
281
298
  };
282
299
  }
283
300
 
301
+ function normalizeWidgetAreas(widgetAreas, mediaBaseUrl) {
302
+ if (!widgetAreas || typeof widgetAreas !== 'object') {
303
+ return {};
304
+ }
305
+
306
+ return Object.fromEntries(
307
+ Object.entries(widgetAreas).map(([widgetAreaId, widgetArea]) => [
308
+ widgetAreaId,
309
+ {
310
+ ...widgetArea,
311
+ name: normalizeNonEmptyString(widgetArea?.name, widgetAreaId),
312
+ items: Array.isArray(widgetArea?.items)
313
+ ? widgetArea.items.map((item) => normalizeWidgetItem(item, mediaBaseUrl))
314
+ : [],
315
+ },
316
+ ]),
317
+ );
318
+ }
319
+
320
+ function normalizeWidgetItem(item, mediaBaseUrl) {
321
+ const normalizedItem = {
322
+ ...item,
323
+ title: normalizeNonEmptyString(item?.title, 'Widget'),
324
+ };
325
+
326
+ if (item?.type === 'profile' && item?.settings && typeof item.settings === 'object') {
327
+ return {
328
+ ...normalizedItem,
329
+ settings: {
330
+ ...item.settings,
331
+ avatar: normalizeMediaField(item.settings.avatar, mediaBaseUrl),
332
+ },
333
+ };
334
+ }
335
+
336
+ return normalizedItem;
337
+ }
338
+
339
+ function normalizeCustomCss(customCss) {
340
+ const content = normalizeOptionalString(customCss?.content);
341
+ return content ? { content } : undefined;
342
+ }
343
+
284
344
  function createRenderData(previewData) {
285
345
  const authorsById = new Map(previewData.content.authors.map((author) => [author.id, author]));
286
346
  const categoriesBySlug = new Map(previewData.content.categories.map((category) => [category.slug, category]));
@@ -355,6 +415,253 @@ function createRenderData(previewData) {
355
415
  };
356
416
  }
357
417
 
418
+ function renderWidgetAreas(previewData, renderData) {
419
+ if (!previewData.widgets || typeof previewData.widgets !== 'object') {
420
+ return {};
421
+ }
422
+
423
+ return Object.fromEntries(
424
+ Object.entries(previewData.widgets).map(([widgetAreaId, widgetArea]) => [
425
+ widgetAreaId,
426
+ renderWidgetArea(widgetArea, previewData, renderData, widgetAreaId),
427
+ ]),
428
+ );
429
+ }
430
+
431
+ function renderWidgetArea(widgetArea, previewData, renderData, widgetAreaId) {
432
+ if (!widgetArea || !Array.isArray(widgetArea.items) || widgetArea.items.length === 0) {
433
+ return '';
434
+ }
435
+
436
+ return widgetArea.items
437
+ .map((item, index) => renderWidgetItem(item, previewData, renderData, widgetAreaId, index))
438
+ .filter(Boolean)
439
+ .join('\n');
440
+ }
441
+
442
+ function renderWidgetItem(item, previewData, renderData, widgetAreaId, index) {
443
+ if (!item || typeof item !== 'object') {
444
+ return '';
445
+ }
446
+
447
+ const title = normalizeNonEmptyString(item.title, 'Widget');
448
+
449
+ switch (item.type) {
450
+ case 'recent-posts':
451
+ return renderRecentPostsWidget(title, item.settings, renderData);
452
+ case 'categories':
453
+ return renderCategoriesWidget(title, item.settings, previewData);
454
+ case 'tags':
455
+ return renderTagsWidget(title, item.settings, previewData);
456
+ case 'archives':
457
+ return renderArchivesWidget(title, item.settings, previewData);
458
+ case 'image':
459
+ return renderImageWidget(title, item.settings);
460
+ case 'text':
461
+ return renderTextWidget(title, item.settings);
462
+ case 'link-list':
463
+ return renderLinkListWidget(title, item.settings);
464
+ case 'search':
465
+ return renderSearchWidget(title, item.settings, widgetAreaId, index);
466
+ case 'profile':
467
+ return renderProfileWidget(title, item.settings);
468
+ default:
469
+ return '';
470
+ }
471
+ }
472
+
473
+ function renderRecentPostsWidget(title, settings, renderData) {
474
+ const limit = clampInteger(settings?.limit, 5, 1, 20);
475
+ const showDate = settings?.show_date !== false;
476
+ const items = renderData.posts
477
+ .slice(0, limit)
478
+ .map((post) => {
479
+ const dateMeta = showDate
480
+ ? `<span class="widget-list__meta">${escapeHtml(post.published_at)}</span>`
481
+ : '';
482
+
483
+ return `<li class="widget-list__item"><a href="/posts/${escapeHtml(encodeSlugSegment(post.slug))}/">${escapeHtml(post.title)}</a>${dateMeta}</li>`;
484
+ })
485
+ .join('');
486
+
487
+ if (!items) {
488
+ return '';
489
+ }
490
+
491
+ return renderWidgetCard(title, 'recent-posts', `<ul class="widget-list">${items}</ul>`);
492
+ }
493
+
494
+ function renderCategoriesWidget(title, settings, previewData) {
495
+ const countBySlug = buildTaxonomyCountMap(previewData.content.posts, 'category_slugs');
496
+ const items = previewData.content.categories
497
+ .map((category) => {
498
+ const count = countBySlug.get(category.slug) || 0;
499
+ if (count === 0) {
500
+ return '';
501
+ }
502
+
503
+ const suffix = settings?.show_count === true ? ` <span class="widget-taxonomy__count">(${count})</span>` : '';
504
+ return `<a href="/categories/${escapeHtml(encodeSlugSegment(category.slug))}/" class="category-link">${escapeHtml(category.name)}${suffix}</a>`;
505
+ })
506
+ .filter(Boolean)
507
+ .join('');
508
+
509
+ if (!items) {
510
+ return '';
511
+ }
512
+
513
+ return renderWidgetCard(title, 'categories', `<div class="taxonomy-list taxonomy-list--stack widget-taxonomy">${items}</div>`);
514
+ }
515
+
516
+ function renderTagsWidget(title, settings, previewData) {
517
+ const countBySlug = buildTaxonomyCountMap(previewData.content.posts, 'tag_slugs');
518
+ const limit = clampInteger(settings?.limit, 20, 1, 100);
519
+ const items = previewData.content.tags
520
+ .map((tag) => ({
521
+ ...tag,
522
+ count: countBySlug.get(tag.slug) || 0,
523
+ }))
524
+ .filter((tag) => tag.count > 0)
525
+ .slice(0, limit)
526
+ .map((tag) => {
527
+ const suffix = settings?.show_count === true ? ` <span class="widget-taxonomy__count">(${tag.count})</span>` : '';
528
+ return `<a href="/tags/${escapeHtml(encodeSlugSegment(tag.slug))}/" class="tag-link">${escapeHtml(tag.name)}${suffix}</a>`;
529
+ })
530
+ .join('');
531
+
532
+ if (!items) {
533
+ return '';
534
+ }
535
+
536
+ return renderWidgetCard(title, 'tags', `<div class="taxonomy-list widget-taxonomy">${items}</div>`);
537
+ }
538
+
539
+ function renderArchivesWidget(title, settings, previewData) {
540
+ const limit = clampInteger(settings?.limit, 12, 1, 120);
541
+ const archiveEntries = buildArchiveEntries(previewData.content.posts, previewData.site).slice(0, limit);
542
+ const items = archiveEntries
543
+ .map((entry) => `<li class="widget-list__item"><a href="/archive/">${escapeHtml(entry.label)}</a><span class="widget-list__meta">${entry.count} posts</span></li>`)
544
+ .join('');
545
+
546
+ if (!items) {
547
+ return '';
548
+ }
549
+
550
+ return renderWidgetCard(title, 'archives', `<ul class="widget-list">${items}</ul>`);
551
+ }
552
+
553
+ function renderImageWidget(title, settings) {
554
+ const src = normalizeOptionalString(settings?.src);
555
+ if (!src) {
556
+ return '';
557
+ }
558
+
559
+ const alt = escapeHtml(normalizeOptionalString(settings?.alt));
560
+ const caption = normalizeOptionalString(settings?.caption);
561
+ const href = normalizeOptionalString(settings?.href);
562
+ const target = normalizeLinkTarget(settings?.target);
563
+ const rel = target === '_blank' ? ' rel="noreferrer noopener"' : '';
564
+ const image = `<img class="widget-image__media" src="${escapeHtml(src)}" alt="${alt}">`;
565
+ const linkedImage = href ? `<a href="${escapeHtml(href)}" target="${target}"${rel}>${image}</a>` : image;
566
+ const captionHtml = caption ? `<figcaption class="widget-image__caption">${escapeHtml(caption)}</figcaption>` : '';
567
+
568
+ return renderWidgetCard(title, 'image', `<figure class="widget-image">${linkedImage}${captionHtml}</figure>`);
569
+ }
570
+
571
+ function renderTextWidget(title, settings) {
572
+ const content = typeof settings?.content === 'string' ? settings.content : '';
573
+ const html = renderDocumentContent(content, normalizeDocumentType(settings?.document_type));
574
+ if (!normalizeOptionalString(html)) {
575
+ return '';
576
+ }
577
+
578
+ return renderWidgetCard(title, 'text', `<div class="widget-copy">${html}</div>`);
579
+ }
580
+
581
+ function renderLinkListWidget(title, settings) {
582
+ const links = Array.isArray(settings?.links) ? settings.links : [];
583
+ const items = links
584
+ .map((link) => {
585
+ const label = normalizeOptionalString(link?.label);
586
+ const url = normalizeOptionalString(link?.url);
587
+ if (!label || !url) {
588
+ return '';
589
+ }
590
+
591
+ const target = normalizeLinkTarget(link?.target);
592
+ const rel = target === '_blank' ? ' rel="noreferrer noopener"' : '';
593
+ return `<a href="${escapeHtml(url)}" target="${target}"${rel}>${escapeHtml(label)}</a>`;
594
+ })
595
+ .filter(Boolean)
596
+ .join('');
597
+
598
+ if (!items) {
599
+ return '';
600
+ }
601
+
602
+ return renderWidgetCard(title, 'link-list', `<div class="taxonomy-list taxonomy-list--stack widget-link-list">${items}</div>`);
603
+ }
604
+
605
+ function renderSearchWidget(title, settings, widgetAreaId, index) {
606
+ const placeholder = escapeHtml(normalizeNonEmptyString(settings?.placeholder, 'Search...'));
607
+ const buttonLabel = escapeHtml(normalizeNonEmptyString(settings?.button_label, 'Search'));
608
+ const searchId = `widget-search-${widgetAreaId}-${index + 1}`;
609
+
610
+ return renderWidgetCard(title, 'search', [
611
+ '<div class="widget-search" data-search-root>',
612
+ ` <label class="sr-only" for="${escapeHtml(searchId)}">${escapeHtml(title)}</label>`,
613
+ ' <form class="search-theme-wrapper" data-search-form>',
614
+ ` <input id="${escapeHtml(searchId)}" class="search-input" data-theme-search type="search" placeholder="${placeholder}" autocomplete="off" enterkeyhint="search">`,
615
+ ` <button class="widget-search-button" type="submit">${buttonLabel}</button>`,
616
+ ' </form>',
617
+ ' <div class="search-results" data-search-results hidden></div>',
618
+ ' <p class="search-feedback search-feedback--sidebar" data-search-feedback aria-live="polite"></p>',
619
+ '</div>',
620
+ ].join('\n'));
621
+ }
622
+
623
+ function renderProfileWidget(title, settings) {
624
+ const displayName = normalizeOptionalString(settings?.display_name);
625
+ const affiliation = normalizeOptionalString(settings?.affiliation);
626
+ const bioShort = normalizeOptionalString(settings?.bio_short);
627
+ const avatar = normalizeOptionalString(settings?.avatar);
628
+
629
+ if (!displayName && !bioShort && !avatar) {
630
+ return '';
631
+ }
632
+
633
+ const avatarHtml = avatar
634
+ ? `<img class="widget-profile__avatar" src="${escapeHtml(avatar)}" alt="${escapeHtml(displayName || title)}">`
635
+ : '';
636
+ const affiliationHtml = affiliation ? `<p class="widget-profile__affiliation">${escapeHtml(affiliation)}</p>` : '';
637
+ const bioHtml = bioShort ? `<p class="sidebar-copy">${escapeHtml(bioShort)}</p>` : '';
638
+
639
+ return renderWidgetCard(title, 'profile', [
640
+ '<div class="widget-profile">',
641
+ avatarHtml,
642
+ ' <div class="widget-profile__body">',
643
+ displayName ? ` <p class="widget-profile__name">${escapeHtml(displayName)}</p>` : '',
644
+ affiliationHtml ? ` ${affiliationHtml}` : '',
645
+ bioHtml ? ` ${bioHtml}` : '',
646
+ ' </div>',
647
+ '</div>',
648
+ ].filter(Boolean).join('\n'));
649
+ }
650
+
651
+ function renderWidgetCard(title, modifier, body) {
652
+ if (!normalizeOptionalString(body)) {
653
+ return '';
654
+ }
655
+
656
+ return [
657
+ `<section class="content-card sidebar-card widget-card widget-card--${escapeHtml(modifier)}">`,
658
+ ' <p class="section-kicker">Widget</p>',
659
+ ` <h2>${escapeHtml(title)}</h2>`,
660
+ ` ${body}`,
661
+ '</section>',
662
+ ].join('\n');
663
+ }
664
+
358
665
  function preparePage(page) {
359
666
  const documentType = normalizeDocumentType(page.document_type);
360
667
 
@@ -641,6 +948,51 @@ function renderPagination(paginationData) {
641
948
  return `<nav class="pagination">\n ${prevLink}\n <span class="pages">${pageLinks}</span>\n ${nextLink}\n</nav>`;
642
949
  }
643
950
 
951
+ function buildTaxonomyCountMap(posts, fieldName) {
952
+ const counts = new Map();
953
+
954
+ for (const post of posts) {
955
+ const values = Array.isArray(post?.[fieldName]) ? post[fieldName] : [];
956
+ for (const value of values) {
957
+ counts.set(value, (counts.get(value) || 0) + 1);
958
+ }
959
+ }
960
+
961
+ return counts;
962
+ }
963
+
964
+ function buildArchiveEntries(posts, site) {
965
+ const entries = new Map();
966
+
967
+ for (const post of posts) {
968
+ const publishedAt = normalizeIsoTimestamp(post?.published_at_iso);
969
+ if (!publishedAt) {
970
+ continue;
971
+ }
972
+
973
+ const date = toDate(publishedAt);
974
+ const key = `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, '0')}`;
975
+ const current = entries.get(key) || { date, count: 0 };
976
+ current.count += 1;
977
+ entries.set(key, current);
978
+ }
979
+
980
+ return Array.from(entries.values())
981
+ .sort((left, right) => right.date.getTime() - left.date.getTime())
982
+ .map((entry) => ({
983
+ label: formatArchiveLabel(entry.date, site),
984
+ count: entry.count,
985
+ }));
986
+ }
987
+
988
+ function formatArchiveLabel(date, site) {
989
+ return new Intl.DateTimeFormat(normalizeLocale(site.locale || DEFAULT_LOCALE), {
990
+ timeZone: normalizeNonEmptyString(site.timezone, DEFAULT_TIMEZONE),
991
+ year: 'numeric',
992
+ month: 'long',
993
+ }).format(date);
994
+ }
995
+
644
996
  function formatTimestamp(value, site) {
645
997
  const date = toDate(value);
646
998
  const locale = normalizeLocale(site.locale || DEFAULT_LOCALE);
@@ -779,6 +1131,8 @@ async function assertValidThemePackage(themePackage) {
779
1131
  ...(themePackage.metadata.author ? { author: themePackage.metadata.author } : {}),
780
1132
  ...(themePackage.metadata.description ? { description: themePackage.metadata.description } : {}),
781
1133
  ...(themePackage.metadata.thumbnail ? { thumbnail: themePackage.metadata.thumbnail } : {}),
1134
+ ...(themePackage.metadata.menuSlots ? { menuSlots: themePackage.metadata.menuSlots } : {}),
1135
+ ...(themePackage.metadata.widgetAreas ? { widgetAreas: themePackage.metadata.widgetAreas } : {}),
782
1136
  settings: themePackage.metadata.settings || {},
783
1137
  }));
784
1138
 
@@ -830,6 +1184,24 @@ async function buildAssetOutputs(assets, assetProcessor, options) {
830
1184
  return outputs;
831
1185
  }
832
1186
 
1187
+ async function buildCustomCssAsset(customCss, assetProcessor, options) {
1188
+ const content = normalizeOptionalString(customCss?.content);
1189
+ if (!content) {
1190
+ return null;
1191
+ }
1192
+
1193
+ const sourceBuffer = new TextEncoder().encode(content);
1194
+ const processedContent = await assetProcessor.processCSS(content);
1195
+ const hash = options.assetHashing ? `.${assetProcessor.generateAssetHash(sourceBuffer)}` : '';
1196
+
1197
+ return {
1198
+ originalPath: '__zeropress_custom.css',
1199
+ path: `assets/zeropress-custom${hash}.css`,
1200
+ content: processedContent,
1201
+ contentType: 'text/css',
1202
+ };
1203
+ }
1204
+
833
1205
  function createRenderContext(site, currentUrl) {
834
1206
  return {
835
1207
  site,
@@ -1097,6 +1469,14 @@ function injectHtmxScript(html) {
1097
1469
  return html.replace('</body>', '<script src="https://unpkg.com/htmx.org@2.0.4" crossorigin="anonymous"></script>\n</body>');
1098
1470
  }
1099
1471
 
1472
+ function injectCustomCssAssetLink(html, href) {
1473
+ if (!normalizeOptionalString(href)) {
1474
+ return html;
1475
+ }
1476
+
1477
+ return html.replace('</head>', ` <link rel="stylesheet" href="${escapeHtml(href)}">\n</head>`);
1478
+ }
1479
+
1100
1480
  function buildSitemapXml(site, emitted, generatedAt) {
1101
1481
  const entries = [
1102
1482
  ...emitted.indexRoutes
@@ -1263,3 +1643,12 @@ function escapeHtml(value) {
1263
1643
  .replaceAll('"', '&quot;')
1264
1644
  .replaceAll("'", '&#39;');
1265
1645
  }
1646
+
1647
+ function clampInteger(value, fallback, min, max) {
1648
+ const normalized = Number.isInteger(value) ? value : fallback;
1649
+ return Math.min(max, Math.max(min, normalized));
1650
+ }
1651
+
1652
+ function normalizeLinkTarget(value) {
1653
+ return value === '_blank' ? '_blank' : '_self';
1654
+ }
package/src/index.d.ts CHANGED
@@ -12,6 +12,10 @@ export interface ThemePackage {
12
12
  title: string;
13
13
  description?: string;
14
14
  }>;
15
+ widgetAreas?: Record<string, {
16
+ title: string;
17
+ description?: string;
18
+ }>;
15
19
  namespace?: string;
16
20
  slug?: string;
17
21
  license?: 'MIT' | 'Apache-2.0' | 'BSD-3-Clause' | 'GPL-3.0-only' | 'GPL-3.0-or-later';
@@ -51,6 +51,8 @@ export async function loadThemePackageFromDir(themeDir) {
51
51
  description: themeJson.description,
52
52
  thumbnail: themeJson.thumbnail,
53
53
  settings: themeJson.settings || {},
54
+ menuSlots: themeJson.menuSlots,
55
+ widgetAreas: themeJson.widgetAreas,
54
56
  namespace: themeJson.namespace,
55
57
  slug: themeJson.slug,
56
58
  license: themeJson.license,