@zeropress/build-core 0.1.12 → 0.1.13

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.13",
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),
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,33 @@ function normalizePreviewData(previewData) {
281
298
  };
282
299
  }
283
300
 
301
+ function normalizeWidgetAreas(widgetAreas) {
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) => ({
314
+ ...item,
315
+ title: normalizeNonEmptyString(item?.title, 'Widget'),
316
+ }))
317
+ : [],
318
+ },
319
+ ]),
320
+ );
321
+ }
322
+
323
+ function normalizeCustomCss(customCss) {
324
+ const content = normalizeOptionalString(customCss?.content);
325
+ return content ? { content } : undefined;
326
+ }
327
+
284
328
  function createRenderData(previewData) {
285
329
  const authorsById = new Map(previewData.content.authors.map((author) => [author.id, author]));
286
330
  const categoriesBySlug = new Map(previewData.content.categories.map((category) => [category.slug, category]));
@@ -355,6 +399,253 @@ function createRenderData(previewData) {
355
399
  };
356
400
  }
357
401
 
402
+ function renderWidgetAreas(previewData, renderData) {
403
+ if (!previewData.widgets || typeof previewData.widgets !== 'object') {
404
+ return {};
405
+ }
406
+
407
+ return Object.fromEntries(
408
+ Object.entries(previewData.widgets).map(([widgetAreaId, widgetArea]) => [
409
+ widgetAreaId,
410
+ renderWidgetArea(widgetArea, previewData, renderData, widgetAreaId),
411
+ ]),
412
+ );
413
+ }
414
+
415
+ function renderWidgetArea(widgetArea, previewData, renderData, widgetAreaId) {
416
+ if (!widgetArea || !Array.isArray(widgetArea.items) || widgetArea.items.length === 0) {
417
+ return '';
418
+ }
419
+
420
+ return widgetArea.items
421
+ .map((item, index) => renderWidgetItem(item, previewData, renderData, widgetAreaId, index))
422
+ .filter(Boolean)
423
+ .join('\n');
424
+ }
425
+
426
+ function renderWidgetItem(item, previewData, renderData, widgetAreaId, index) {
427
+ if (!item || typeof item !== 'object') {
428
+ return '';
429
+ }
430
+
431
+ const title = normalizeNonEmptyString(item.title, 'Widget');
432
+
433
+ switch (item.type) {
434
+ case 'recent-posts':
435
+ return renderRecentPostsWidget(title, item.settings, renderData);
436
+ case 'categories':
437
+ return renderCategoriesWidget(title, item.settings, previewData);
438
+ case 'tags':
439
+ return renderTagsWidget(title, item.settings, previewData);
440
+ case 'archives':
441
+ return renderArchivesWidget(title, item.settings, previewData);
442
+ case 'image':
443
+ return renderImageWidget(title, item.settings);
444
+ case 'text':
445
+ return renderTextWidget(title, item.settings);
446
+ case 'link-list':
447
+ return renderLinkListWidget(title, item.settings);
448
+ case 'search':
449
+ return renderSearchWidget(title, item.settings, widgetAreaId, index);
450
+ case 'profile':
451
+ return renderProfileWidget(title, item.settings);
452
+ default:
453
+ return '';
454
+ }
455
+ }
456
+
457
+ function renderRecentPostsWidget(title, settings, renderData) {
458
+ const limit = clampInteger(settings?.limit, 5, 1, 20);
459
+ const showDate = settings?.show_date !== false;
460
+ const items = renderData.posts
461
+ .slice(0, limit)
462
+ .map((post) => {
463
+ const dateMeta = showDate
464
+ ? `<span class="widget-list__meta">${escapeHtml(post.published_at)}</span>`
465
+ : '';
466
+
467
+ return `<li class="widget-list__item"><a href="/posts/${escapeHtml(encodeSlugSegment(post.slug))}/">${escapeHtml(post.title)}</a>${dateMeta}</li>`;
468
+ })
469
+ .join('');
470
+
471
+ if (!items) {
472
+ return '';
473
+ }
474
+
475
+ return renderWidgetCard(title, 'recent-posts', `<ul class="widget-list">${items}</ul>`);
476
+ }
477
+
478
+ function renderCategoriesWidget(title, settings, previewData) {
479
+ const countBySlug = buildTaxonomyCountMap(previewData.content.posts, 'category_slugs');
480
+ const items = previewData.content.categories
481
+ .map((category) => {
482
+ const count = countBySlug.get(category.slug) || 0;
483
+ if (count === 0) {
484
+ return '';
485
+ }
486
+
487
+ const suffix = settings?.show_count === true ? ` <span class="widget-taxonomy__count">(${count})</span>` : '';
488
+ return `<a href="/categories/${escapeHtml(encodeSlugSegment(category.slug))}/" class="category-link">${escapeHtml(category.name)}${suffix}</a>`;
489
+ })
490
+ .filter(Boolean)
491
+ .join('');
492
+
493
+ if (!items) {
494
+ return '';
495
+ }
496
+
497
+ return renderWidgetCard(title, 'categories', `<div class="taxonomy-list taxonomy-list--stack widget-taxonomy">${items}</div>`);
498
+ }
499
+
500
+ function renderTagsWidget(title, settings, previewData) {
501
+ const countBySlug = buildTaxonomyCountMap(previewData.content.posts, 'tag_slugs');
502
+ const limit = clampInteger(settings?.limit, 20, 1, 100);
503
+ const items = previewData.content.tags
504
+ .map((tag) => ({
505
+ ...tag,
506
+ count: countBySlug.get(tag.slug) || 0,
507
+ }))
508
+ .filter((tag) => tag.count > 0)
509
+ .slice(0, limit)
510
+ .map((tag) => {
511
+ const suffix = settings?.show_count === true ? ` <span class="widget-taxonomy__count">(${tag.count})</span>` : '';
512
+ return `<a href="/tags/${escapeHtml(encodeSlugSegment(tag.slug))}/" class="tag-link">${escapeHtml(tag.name)}${suffix}</a>`;
513
+ })
514
+ .join('');
515
+
516
+ if (!items) {
517
+ return '';
518
+ }
519
+
520
+ return renderWidgetCard(title, 'tags', `<div class="taxonomy-list widget-taxonomy">${items}</div>`);
521
+ }
522
+
523
+ function renderArchivesWidget(title, settings, previewData) {
524
+ const limit = clampInteger(settings?.limit, 12, 1, 120);
525
+ const archiveEntries = buildArchiveEntries(previewData.content.posts, previewData.site).slice(0, limit);
526
+ const items = archiveEntries
527
+ .map((entry) => `<li class="widget-list__item"><a href="/archive/">${escapeHtml(entry.label)}</a><span class="widget-list__meta">${entry.count} posts</span></li>`)
528
+ .join('');
529
+
530
+ if (!items) {
531
+ return '';
532
+ }
533
+
534
+ return renderWidgetCard(title, 'archives', `<ul class="widget-list">${items}</ul>`);
535
+ }
536
+
537
+ function renderImageWidget(title, settings) {
538
+ const src = normalizeOptionalString(settings?.src);
539
+ if (!src) {
540
+ return '';
541
+ }
542
+
543
+ const alt = escapeHtml(normalizeOptionalString(settings?.alt));
544
+ const caption = normalizeOptionalString(settings?.caption);
545
+ const href = normalizeOptionalString(settings?.href);
546
+ const target = normalizeLinkTarget(settings?.target);
547
+ const rel = target === '_blank' ? ' rel="noreferrer noopener"' : '';
548
+ const image = `<img class="widget-image__media" src="${escapeHtml(src)}" alt="${alt}">`;
549
+ const linkedImage = href ? `<a href="${escapeHtml(href)}" target="${target}"${rel}>${image}</a>` : image;
550
+ const captionHtml = caption ? `<figcaption class="widget-image__caption">${escapeHtml(caption)}</figcaption>` : '';
551
+
552
+ return renderWidgetCard(title, 'image', `<figure class="widget-image">${linkedImage}${captionHtml}</figure>`);
553
+ }
554
+
555
+ function renderTextWidget(title, settings) {
556
+ const content = typeof settings?.content === 'string' ? settings.content : '';
557
+ const html = renderDocumentContent(content, normalizeDocumentType(settings?.document_type));
558
+ if (!normalizeOptionalString(html)) {
559
+ return '';
560
+ }
561
+
562
+ return renderWidgetCard(title, 'text', `<div class="widget-copy">${html}</div>`);
563
+ }
564
+
565
+ function renderLinkListWidget(title, settings) {
566
+ const links = Array.isArray(settings?.links) ? settings.links : [];
567
+ const items = links
568
+ .map((link) => {
569
+ const label = normalizeOptionalString(link?.label);
570
+ const url = normalizeOptionalString(link?.url);
571
+ if (!label || !url) {
572
+ return '';
573
+ }
574
+
575
+ const target = normalizeLinkTarget(link?.target);
576
+ const rel = target === '_blank' ? ' rel="noreferrer noopener"' : '';
577
+ return `<a href="${escapeHtml(url)}" target="${target}"${rel}>${escapeHtml(label)}</a>`;
578
+ })
579
+ .filter(Boolean)
580
+ .join('');
581
+
582
+ if (!items) {
583
+ return '';
584
+ }
585
+
586
+ return renderWidgetCard(title, 'link-list', `<div class="taxonomy-list taxonomy-list--stack widget-link-list">${items}</div>`);
587
+ }
588
+
589
+ function renderSearchWidget(title, settings, widgetAreaId, index) {
590
+ const placeholder = escapeHtml(normalizeNonEmptyString(settings?.placeholder, 'Search...'));
591
+ const buttonLabel = escapeHtml(normalizeNonEmptyString(settings?.button_label, 'Search'));
592
+ const searchId = `widget-search-${widgetAreaId}-${index + 1}`;
593
+
594
+ return renderWidgetCard(title, 'search', [
595
+ '<div class="widget-search" data-search-root>',
596
+ ` <label class="sr-only" for="${escapeHtml(searchId)}">${escapeHtml(title)}</label>`,
597
+ ' <form class="search-theme-wrapper" data-search-form>',
598
+ ` <input id="${escapeHtml(searchId)}" class="search-input" data-theme-search type="search" placeholder="${placeholder}" autocomplete="off" enterkeyhint="search">`,
599
+ ` <button class="widget-search-button" type="submit">${buttonLabel}</button>`,
600
+ ' </form>',
601
+ ' <div class="search-results" data-search-results hidden></div>',
602
+ ' <p class="search-feedback search-feedback--sidebar" data-search-feedback aria-live="polite"></p>',
603
+ '</div>',
604
+ ].join('\n'));
605
+ }
606
+
607
+ function renderProfileWidget(title, settings) {
608
+ const displayName = normalizeOptionalString(settings?.display_name);
609
+ const affiliation = normalizeOptionalString(settings?.affiliation);
610
+ const bioShort = normalizeOptionalString(settings?.bio_short);
611
+ const avatar = normalizeOptionalString(settings?.avatar);
612
+
613
+ if (!displayName && !bioShort && !avatar) {
614
+ return '';
615
+ }
616
+
617
+ const avatarHtml = avatar
618
+ ? `<img class="widget-profile__avatar" src="${escapeHtml(avatar)}" alt="${escapeHtml(displayName || title)}">`
619
+ : '';
620
+ const affiliationHtml = affiliation ? `<p class="widget-profile__affiliation">${escapeHtml(affiliation)}</p>` : '';
621
+ const bioHtml = bioShort ? `<p class="sidebar-copy">${escapeHtml(bioShort)}</p>` : '';
622
+
623
+ return renderWidgetCard(title, 'profile', [
624
+ '<div class="widget-profile">',
625
+ avatarHtml,
626
+ ' <div class="widget-profile__body">',
627
+ displayName ? ` <p class="widget-profile__name">${escapeHtml(displayName)}</p>` : '',
628
+ affiliationHtml ? ` ${affiliationHtml}` : '',
629
+ bioHtml ? ` ${bioHtml}` : '',
630
+ ' </div>',
631
+ '</div>',
632
+ ].filter(Boolean).join('\n'));
633
+ }
634
+
635
+ function renderWidgetCard(title, modifier, body) {
636
+ if (!normalizeOptionalString(body)) {
637
+ return '';
638
+ }
639
+
640
+ return [
641
+ `<section class="content-card sidebar-card widget-card widget-card--${escapeHtml(modifier)}">`,
642
+ ' <p class="section-kicker">Widget</p>',
643
+ ` <h2>${escapeHtml(title)}</h2>`,
644
+ ` ${body}`,
645
+ '</section>',
646
+ ].join('\n');
647
+ }
648
+
358
649
  function preparePage(page) {
359
650
  const documentType = normalizeDocumentType(page.document_type);
360
651
 
@@ -641,6 +932,51 @@ function renderPagination(paginationData) {
641
932
  return `<nav class="pagination">\n ${prevLink}\n <span class="pages">${pageLinks}</span>\n ${nextLink}\n</nav>`;
642
933
  }
643
934
 
935
+ function buildTaxonomyCountMap(posts, fieldName) {
936
+ const counts = new Map();
937
+
938
+ for (const post of posts) {
939
+ const values = Array.isArray(post?.[fieldName]) ? post[fieldName] : [];
940
+ for (const value of values) {
941
+ counts.set(value, (counts.get(value) || 0) + 1);
942
+ }
943
+ }
944
+
945
+ return counts;
946
+ }
947
+
948
+ function buildArchiveEntries(posts, site) {
949
+ const entries = new Map();
950
+
951
+ for (const post of posts) {
952
+ const publishedAt = normalizeIsoTimestamp(post?.published_at_iso);
953
+ if (!publishedAt) {
954
+ continue;
955
+ }
956
+
957
+ const date = toDate(publishedAt);
958
+ const key = `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, '0')}`;
959
+ const current = entries.get(key) || { date, count: 0 };
960
+ current.count += 1;
961
+ entries.set(key, current);
962
+ }
963
+
964
+ return Array.from(entries.values())
965
+ .sort((left, right) => right.date.getTime() - left.date.getTime())
966
+ .map((entry) => ({
967
+ label: formatArchiveLabel(entry.date, site),
968
+ count: entry.count,
969
+ }));
970
+ }
971
+
972
+ function formatArchiveLabel(date, site) {
973
+ return new Intl.DateTimeFormat(normalizeLocale(site.locale || DEFAULT_LOCALE), {
974
+ timeZone: normalizeNonEmptyString(site.timezone, DEFAULT_TIMEZONE),
975
+ year: 'numeric',
976
+ month: 'long',
977
+ }).format(date);
978
+ }
979
+
644
980
  function formatTimestamp(value, site) {
645
981
  const date = toDate(value);
646
982
  const locale = normalizeLocale(site.locale || DEFAULT_LOCALE);
@@ -779,6 +1115,8 @@ async function assertValidThemePackage(themePackage) {
779
1115
  ...(themePackage.metadata.author ? { author: themePackage.metadata.author } : {}),
780
1116
  ...(themePackage.metadata.description ? { description: themePackage.metadata.description } : {}),
781
1117
  ...(themePackage.metadata.thumbnail ? { thumbnail: themePackage.metadata.thumbnail } : {}),
1118
+ ...(themePackage.metadata.menuSlots ? { menuSlots: themePackage.metadata.menuSlots } : {}),
1119
+ ...(themePackage.metadata.widgetAreas ? { widgetAreas: themePackage.metadata.widgetAreas } : {}),
782
1120
  settings: themePackage.metadata.settings || {},
783
1121
  }));
784
1122
 
@@ -830,6 +1168,24 @@ async function buildAssetOutputs(assets, assetProcessor, options) {
830
1168
  return outputs;
831
1169
  }
832
1170
 
1171
+ async function buildCustomCssAsset(customCss, assetProcessor, options) {
1172
+ const content = normalizeOptionalString(customCss?.content);
1173
+ if (!content) {
1174
+ return null;
1175
+ }
1176
+
1177
+ const sourceBuffer = new TextEncoder().encode(content);
1178
+ const processedContent = await assetProcessor.processCSS(content);
1179
+ const hash = options.assetHashing ? `.${assetProcessor.generateAssetHash(sourceBuffer)}` : '';
1180
+
1181
+ return {
1182
+ originalPath: '__zeropress_custom.css',
1183
+ path: `assets/zeropress-custom${hash}.css`,
1184
+ content: processedContent,
1185
+ contentType: 'text/css',
1186
+ };
1187
+ }
1188
+
833
1189
  function createRenderContext(site, currentUrl) {
834
1190
  return {
835
1191
  site,
@@ -1097,6 +1453,14 @@ function injectHtmxScript(html) {
1097
1453
  return html.replace('</body>', '<script src="https://unpkg.com/htmx.org@2.0.4" crossorigin="anonymous"></script>\n</body>');
1098
1454
  }
1099
1455
 
1456
+ function injectCustomCssAssetLink(html, href) {
1457
+ if (!normalizeOptionalString(href)) {
1458
+ return html;
1459
+ }
1460
+
1461
+ return html.replace('</head>', ` <link rel="stylesheet" href="${escapeHtml(href)}">\n</head>`);
1462
+ }
1463
+
1100
1464
  function buildSitemapXml(site, emitted, generatedAt) {
1101
1465
  const entries = [
1102
1466
  ...emitted.indexRoutes
@@ -1263,3 +1627,12 @@ function escapeHtml(value) {
1263
1627
  .replaceAll('"', '&quot;')
1264
1628
  .replaceAll("'", '&#39;');
1265
1629
  }
1630
+
1631
+ function clampInteger(value, fallback, min, max) {
1632
+ const normalized = Number.isInteger(value) ? value : fallback;
1633
+ return Math.min(max, Math.max(min, normalized));
1634
+ }
1635
+
1636
+ function normalizeLinkTarget(value) {
1637
+ return value === '_blank' ? '_blank' : '_self';
1638
+ }
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,