@uniweb/runtime 1.1.20 → 1.2.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/dist/ssr.js CHANGED
@@ -106,7 +106,7 @@ function assembleData(block, meta, entityData) {
106
106
  else if (entityData && entityData[key] !== void 0) data[key] = entityData[key];
107
107
  else data[key] = null;
108
108
  }
109
- warnUndeclaredBlocks(block, declared);
109
+ warnUndeclaredBlocks(block, Object.keys(held).filter((key) => !Object.hasOwn(data, key)));
110
110
  block.parsedContent.data = data;
111
111
  return declared;
112
112
  }
@@ -121,17 +121,18 @@ function holdOneRecord(block, declared) {
121
121
  if (out) block.parsedContent.data = out;
122
122
  }
123
123
  const warnedBlocks = /* @__PURE__ */ new Set();
124
- function warnUndeclaredBlocks(block, declared) {
125
- if (!block.website?.entityStore?.dev) return;
126
- const keys = new Set(declared.map(([key]) => key));
127
- for (const item of block.parsedContent?.sequence || []) {
128
- const kind = item?.type === "dataBlock" ? "data block" : item?.type === "concept_block" ? "concept block" : null;
129
- if (!kind || typeof item.tag !== "string" || keys.has(item.tag)) continue;
130
- const memo = `${block.type}::${item.tag}`;
124
+ function warnUndeclaredBlocks(block, leftOut) {
125
+ if (!leftOut.length || !block.website?.entityStore?.dev) return;
126
+ const sequence = block.parsedContent?.sequence || [];
127
+ for (const tag of leftOut) {
128
+ const memo = `${block.type}::${tag}`;
131
129
  if (warnedBlocks.has(memo)) continue;
130
+ const element = sequence.find((e) => (e?.type === "dataBlock" || e?.type === "concept_block") && e.tag === tag);
131
+ if (!element) continue;
132
132
  warnedBlocks.add(memo);
133
+ const kind = element.type === "dataBlock" ? "data block" : "concept block";
133
134
  console.warn(
134
- `[uniweb] ${block.type}: the \`${item.tag}\` ${kind} is not in content.data — ${block.type} does not declare \`${item.tag}\` in its meta.js \`data:\`. It stays in content.sequence.`
135
+ `[uniweb] ${block.type}: the \`${tag}\` ${kind} is not in content.data — ${block.type} does not declare \`${tag}\` in its meta.js \`data:\`. It stays in content.sequence.`
135
136
  );
136
137
  }
137
138
  }
@@ -221,9 +222,12 @@ function siteUrl(url) {
221
222
  function withOpacity(color, opacity) {
222
223
  if (typeof color !== "string" || !color) return color;
223
224
  if (color.startsWith("#")) {
224
- const r = parseInt(color.slice(1, 3), 16);
225
- const g = parseInt(color.slice(3, 5), 16);
226
- const b = parseInt(color.slice(5, 7), 16);
225
+ let hex = color.slice(1);
226
+ if (hex.length === 3 || hex.length === 4) hex = [...hex].map((c) => c + c).join("");
227
+ if (!/^[0-9a-f]{6}([0-9a-f]{2})?$/i.test(hex)) return color;
228
+ const r = parseInt(hex.slice(0, 2), 16);
229
+ const g = parseInt(hex.slice(2, 4), 16);
230
+ const b = parseInt(hex.slice(4, 6), 16);
227
231
  return `rgba(${r}, ${g}, ${b}, ${opacity})`;
228
232
  }
229
233
  if (color.startsWith("rgb")) {
@@ -232,6 +236,76 @@ function withOpacity(color, opacity) {
232
236
  }
233
237
  return color;
234
238
  }
239
+ const h = React.createElement;
240
+ const FILL = { position: "absolute", inset: 0 };
241
+ function ColorLayer({ color }) {
242
+ if (!color) return null;
243
+ return h("div", { className: "background-color", style: { ...FILL, backgroundColor: color }, "aria-hidden": "true" });
244
+ }
245
+ function GradientLayer({ gradient }) {
246
+ if (!gradient) return null;
247
+ if (typeof gradient === "string") {
248
+ return h("div", { className: "background-gradient", style: { ...FILL, background: gradient }, "aria-hidden": "true" });
249
+ }
250
+ const start = gradient.start ?? "transparent";
251
+ const end = gradient.end ?? "transparent";
252
+ const startOpacity = gradient.startOpacity ?? 1;
253
+ const endOpacity = gradient.endOpacity ?? 1;
254
+ const startColor = startOpacity < 1 ? withOpacity(start, startOpacity) : start;
255
+ const endColor = endOpacity < 1 ? withOpacity(end, endOpacity) : end;
256
+ const background = `linear-gradient(${gradient.angle ?? 0}deg, ${startColor} ${gradient.startPosition ?? 0}%, ${endColor} ${gradient.endPosition ?? 100}%)`;
257
+ return h("div", { className: "background-gradient", style: { ...FILL, background }, "aria-hidden": "true" });
258
+ }
259
+ function ImageLayer({ image }) {
260
+ if (!image?.src) return null;
261
+ const style = {
262
+ ...FILL,
263
+ backgroundImage: `url(${siteUrl(image.src)})`,
264
+ backgroundPosition: image.position ?? "center",
265
+ backgroundSize: image.size ?? "cover",
266
+ backgroundRepeat: "no-repeat"
267
+ };
268
+ return h("div", { className: "background-image", style, "aria-hidden": "true" });
269
+ }
270
+ function VideoPoster({ video }) {
271
+ if (!video?.src || !video.poster) return null;
272
+ return h(ImageLayer, { image: { src: video.poster, size: "cover", position: "center" } });
273
+ }
274
+ function Overlay({ overlay }) {
275
+ if (!overlay?.enabled) return null;
276
+ const opacity = overlay.opacity ?? 0.5;
277
+ if (overlay.gradient) {
278
+ const g = overlay.gradient;
279
+ const background = `linear-gradient(${g.angle ?? 180}deg, ${g.start ?? "rgba(0,0,0,0.7)"} ${g.startPosition ?? 0}%, ${g.end ?? "rgba(0,0,0,0)"} ${g.endPosition ?? 100}%)`;
280
+ return h("div", {
281
+ className: "background-overlay background-overlay--gradient",
282
+ style: { ...FILL, background, opacity, pointerEvents: "none" },
283
+ "aria-hidden": "true"
284
+ });
285
+ }
286
+ const base = overlay.type === "light" ? "255, 255, 255" : "0, 0, 0";
287
+ return h("div", {
288
+ className: "background-overlay background-overlay--solid",
289
+ style: { ...FILL, backgroundColor: `rgba(${base}, ${opacity})`, pointerEvents: "none" },
290
+ "aria-hidden": "true"
291
+ });
292
+ }
293
+ function Background({ mode, color, gradient, image, video, overlay, className = "", Video = VideoPoster }) {
294
+ if (!mode) return null;
295
+ return h(
296
+ "div",
297
+ {
298
+ className: `background background--${mode} ${className}`.trim(),
299
+ style: { ...FILL, overflow: "hidden", zIndex: 0 },
300
+ "aria-hidden": "true"
301
+ },
302
+ mode === "color" ? h(ColorLayer, { color }) : null,
303
+ mode === "gradient" ? h(GradientLayer, { gradient }) : null,
304
+ mode === "image" ? h(ImageLayer, { image }) : null,
305
+ mode === "video" ? h(Video, { video }) : null,
306
+ h(Overlay, { overlay })
307
+ );
308
+ }
235
309
  function default404Html(basePath = "") {
236
310
  const homeHref = basePath ? `${basePath}/` : "/";
237
311
  return `<div class="page-not-found" style="min-height:80vh;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:2rem;text-align:center"><h1 style="font-size:3rem;font-weight:bold;color:#1f2937;margin-bottom:1rem">404</h1><p style="color:#64748b;margin-bottom:2rem">Page not found</p><a href="${homeHref}" style="color:#3b82f6;text-decoration:underline">Go to homepage</a></div>`;
@@ -451,105 +525,7 @@ function getWrapperProps(block) {
451
525
  }
452
526
  function renderBackground(background) {
453
527
  if (!background?.mode) return null;
454
- const containerStyle = {
455
- position: "absolute",
456
- inset: "0",
457
- overflow: "hidden",
458
- zIndex: 0
459
- };
460
- const children = [];
461
- if (background.mode === "color" && background.color) {
462
- children.push(
463
- React.createElement("div", {
464
- key: "bg-color",
465
- className: "background-color",
466
- style: { position: "absolute", inset: "0", backgroundColor: background.color },
467
- "aria-hidden": "true"
468
- })
469
- );
470
- }
471
- if (background.mode === "gradient" && background.gradient) {
472
- const g = background.gradient;
473
- let bgValue;
474
- if (typeof g === "string") {
475
- bgValue = g;
476
- } else {
477
- const {
478
- start = "transparent",
479
- end = "transparent",
480
- angle = 0,
481
- startPosition = 0,
482
- endPosition = 100,
483
- startOpacity = 1,
484
- endOpacity = 1
485
- } = g;
486
- const startColor = startOpacity < 1 ? withOpacity(start, startOpacity) : start;
487
- const endColor = endOpacity < 1 ? withOpacity(end, endOpacity) : end;
488
- bgValue = `linear-gradient(${angle}deg, ${startColor} ${startPosition}%, ${endColor} ${endPosition}%)`;
489
- }
490
- children.push(
491
- React.createElement("div", {
492
- key: "bg-gradient",
493
- className: "background-gradient",
494
- style: { position: "absolute", inset: "0", background: bgValue },
495
- "aria-hidden": "true"
496
- })
497
- );
498
- }
499
- if (background.mode === "image" && background.image?.src) {
500
- const img = background.image;
501
- children.push(
502
- React.createElement("div", {
503
- key: "bg-image",
504
- className: "background-image",
505
- style: {
506
- position: "absolute",
507
- inset: "0",
508
- backgroundImage: `url(${siteUrl(img.src)})`,
509
- backgroundPosition: img.position || "center",
510
- backgroundSize: img.size || "cover",
511
- backgroundRepeat: "no-repeat"
512
- },
513
- "aria-hidden": "true"
514
- })
515
- );
516
- }
517
- if (background.overlay?.enabled) {
518
- const ov = background.overlay;
519
- let overlayStyle;
520
- if (ov.gradient) {
521
- const g = ov.gradient;
522
- overlayStyle = {
523
- position: "absolute",
524
- inset: "0",
525
- pointerEvents: "none",
526
- background: `linear-gradient(${g.angle || 180}deg, ${g.start || "rgba(0,0,0,0.7)"} ${g.startPosition || 0}%, ${g.end || "rgba(0,0,0,0)"} ${g.endPosition || 100}%)`,
527
- opacity: ov.opacity ?? 0.5
528
- };
529
- } else {
530
- const baseColor = ov.type === "light" ? "255, 255, 255" : "0, 0, 0";
531
- overlayStyle = {
532
- position: "absolute",
533
- inset: "0",
534
- pointerEvents: "none",
535
- backgroundColor: `rgba(${baseColor}, ${ov.opacity ?? 0.5})`
536
- };
537
- }
538
- children.push(
539
- React.createElement("div", {
540
- key: "bg-overlay",
541
- className: ov.gradient ? "background-overlay background-overlay--gradient" : "background-overlay background-overlay--solid",
542
- style: overlayStyle,
543
- "aria-hidden": "true"
544
- })
545
- );
546
- }
547
- if (children.length === 0) return null;
548
- return React.createElement("div", {
549
- className: `background background--${background.mode}`,
550
- style: containerStyle,
551
- "aria-hidden": "true"
552
- }, ...children);
528
+ return React.createElement(Background, background);
553
529
  }
554
530
  function renderBlock(block, { as = "section", stickySelf = false } = {}) {
555
531
  const Component = block.initComponent();
@@ -562,10 +538,13 @@ function renderBlock(block, { as = "section", stickySelf = false } = {}) {
562
538
  const meta = getComponentMeta(block.type);
563
539
  const entityStore = block.website?.entityStore;
564
540
  let entityData = null;
541
+ let pending = false;
565
542
  if (entityStore) {
566
543
  const resolved = entityStore.resolve(block, meta);
567
544
  if (resolved.status === "ready") entityData = resolved.data;
545
+ pending = resolved.status === "pending";
568
546
  }
547
+ block.dataLoading = pending;
569
548
  const prepared = prepareProps(block, meta, entityData);
570
549
  const params = prepared.params;
571
550
  const content = { ...prepared.content, ...block.properties };
@@ -690,9 +669,7 @@ function initPrerender(content, foundation, extensionsOrOptions, maybeOptions) {
690
669
  )
691
670
  );
692
671
  };
693
- uniweb.backgroundRenderer = function InlineBackground(background) {
694
- return renderBackground(background);
695
- };
672
+ uniweb.backgroundRenderer = Background;
696
673
  wireFoundationCapabilities(uniweb, foundation);
697
674
  ensureThemeCss(uniweb, foundation);
698
675
  const website = uniweb.activeWebsite;
@@ -822,37 +799,46 @@ ${options.sectionOverrideCSS}
822
799
  `<title>${escapeHtml(title)}</title>`
823
800
  );
824
801
  }
825
- if (page.description) {
826
- const metaDesc = `<meta name="description" content="${escapeHtml(page.description)}">`;
827
- if (result.includes('<meta name="description"')) {
828
- result = result.replace(/<meta name="description"[^>]*>/, metaDesc);
829
- } else {
830
- result = result.replace("</head>", `${metaDesc}
831
- </head>`);
832
- }
833
- }
834
- const headMeta = page.getHeadMeta?.();
835
- if (headMeta) {
836
- const og = headMeta.og || {};
837
- const keywords = Array.isArray(headMeta.keywords) ? headMeta.keywords.join(", ") : headMeta.keywords;
838
- const tags = [];
839
- if (keywords) tags.push(`<meta name="keywords" content="${escapeHtml(keywords)}">`);
840
- if (headMeta.robots) tags.push(`<meta name="robots" content="${escapeHtml(headMeta.robots)}">`);
841
- if (og.title) tags.push(`<meta property="og:title" content="${escapeHtml(og.title)}">`);
842
- if (og.description) tags.push(`<meta property="og:description" content="${escapeHtml(og.description)}">`);
843
- if (og.image) tags.push(`<meta property="og:image" content="${escapeHtml(og.image)}">`);
844
- if (og.url) tags.push(`<meta property="og:url" content="${escapeHtml(og.url)}">`);
845
- tags.push('<meta property="og:type" content="website">');
846
- tags.push(`<meta name="twitter:card" content="${og.image ? "summary_large_image" : "summary"}">`);
847
- if (og.title) tags.push(`<meta name="twitter:title" content="${escapeHtml(og.title)}">`);
848
- if (og.description) tags.push(`<meta name="twitter:description" content="${escapeHtml(og.description)}">`);
849
- if (og.image) tags.push(`<meta name="twitter:image" content="${escapeHtml(og.image)}">`);
850
- if (headMeta.canonical) tags.push(`<link rel="canonical" href="${escapeHtml(headMeta.canonical)}">`);
851
- if (tags.length) result = result.replace("</head>", `${tags.join("\n")}
852
- </head>`);
853
- }
802
+ result = writeHeadMeta(result, page);
854
803
  return result;
855
804
  }
805
+ function writeHeadMeta(html, page) {
806
+ const meta = typeof page?.getHeadMeta === "function" ? page.getHeadMeta() : { description: page?.description || null };
807
+ const end = html.indexOf("</head>");
808
+ if (!meta || end === -1) return html;
809
+ const og = meta.og || {};
810
+ const keywords = Array.isArray(meta.keywords) ? meta.keywords.join(", ") : meta.keywords;
811
+ const metaTags = [
812
+ ["name", "description", meta.description],
813
+ ["name", "keywords", keywords],
814
+ ["name", "robots", meta.robots],
815
+ ["property", "og:type", "website"],
816
+ ["property", "og:site_name", og.siteName],
817
+ ["property", "og:title", og.title],
818
+ ["property", "og:description", og.description],
819
+ ["property", "og:image", og.image],
820
+ ["property", "og:url", og.url],
821
+ ["name", "twitter:card", og.image ? "summary_large_image" : "summary"],
822
+ ["name", "twitter:site", meta.twitter?.site],
823
+ ["name", "twitter:title", og.title],
824
+ ["name", "twitter:description", og.description],
825
+ ["name", "twitter:image", og.image]
826
+ ];
827
+ let head = html.slice(0, end);
828
+ const written = [];
829
+ for (const [attr, key, value] of metaTags) {
830
+ head = head.replace(new RegExp(`<meta\\b[^>]*\\b(?:name|property)=["']${key}["'][^>]*>\\s*`, "gi"), "");
831
+ if (value) written.push(`<meta ${attr}="${key}" content="${escapeHtml(value)}">`);
832
+ }
833
+ head = head.replace(/<link\b[^>]*\brel=["']canonical["'][^>]*>\s*/gi, "");
834
+ if (meta.canonical) written.push(`<link rel="canonical" href="${escapeHtml(meta.canonical)}">`);
835
+ head = head.replace(/<link\b[^>]*\bhreflang=["'][^"']*["'][^>]*>\s*/gi, "");
836
+ for (const { hreflang, url } of meta.alternates || []) {
837
+ written.push(`<link rel="alternate" hreflang="${escapeHtml(hreflang)}" href="${escapeHtml(url)}">`);
838
+ }
839
+ return `${head}${written.join("\n")}
840
+ ${html.slice(end)}`;
841
+ }
856
842
  function generate404Html({ baseHtml, website, siteContent }) {
857
843
  const dynamicTemplates = siteContent.pages?.filter((p) => p.isDynamic || isDynamicRoute(p.route)) || [];
858
844
  const routePatterns = dynamicTemplates.map((p) => routePatternToRegex(p.route).regex.source);
@@ -1171,6 +1157,12 @@ function safeParseJSON(text) {
1171
1157
  return void 0;
1172
1158
  }
1173
1159
  }
1160
+ function requireTransport(fetch2, entry) {
1161
+ if (typeof fetch2 === "function") return fetch2;
1162
+ throw new Error(
1163
+ `${entry}: \`fetch\` must be a function. A transport does not survive a JSON-serialized isolate boundary — pass it as an RPC method argument. To use the ambient fetch deliberately, pass \`fetch: globalThis.fetch\`.`
1164
+ );
1165
+ }
1174
1166
  function blocksOf(page) {
1175
1167
  const out = [];
1176
1168
  const walk = (list) => {
@@ -1189,6 +1181,7 @@ async function loadPageData({ website, route, fetch: fetch2, dev = false, locale
1189
1181
  if (prerender !== "author" && prerender !== "always") {
1190
1182
  throw new Error(`loadPageData: prerender must be 'author' or 'always', got ${JSON.stringify(prerender)}`);
1191
1183
  }
1184
+ requireTransport(fetch2, "loadPageData");
1192
1185
  const active = website?.getActiveLocale?.() ?? null;
1193
1186
  if (locale && active && locale !== active) {
1194
1187
  throw new Error(
@@ -1259,16 +1252,13 @@ async function prefetchAndHydrate({ website, content, route, locale = null, fetc
1259
1252
  if (!website?.dataStore) {
1260
1253
  throw new Error("prefetchAndHydrate: `website` must be an initialized Website with a dataStore");
1261
1254
  }
1262
- if (typeof fetch2 !== "function") {
1263
- throw new Error(
1264
- "prefetchAndHydrate: `fetch` must be a function. A transport does not survive a JSON-serialized isolate boundary — pass it as an RPC method argument. To use the ambient fetch deliberately, pass `fetch: globalThis.fetch`."
1265
- );
1266
- }
1255
+ requireTransport(fetch2, "prefetchAndHydrate");
1267
1256
  const fetched = await loadPageData({ website, route, fetch: fetch2, dev, locale, prerender });
1268
1257
  hydrateDataStore(website, fetched);
1269
1258
  return fetched;
1270
1259
  }
1271
1260
  async function collectSiteRecords(content, { locale, fetch: fetch2, signal, only = null, whole = false, maxPages } = {}) {
1261
+ requireTransport(fetch2, "collectSiteRecords");
1272
1262
  const config = content?.config;
1273
1263
  const services = config?.services ?? null;
1274
1264
  const queries = config?.queries ?? null;