@uniweb/runtime 1.1.19 → 1.2.0

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
  }
@@ -822,37 +823,46 @@ ${options.sectionOverrideCSS}
822
823
  `<title>${escapeHtml(title)}</title>`
823
824
  );
824
825
  }
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
- }
826
+ result = writeHeadMeta(result, page);
854
827
  return result;
855
828
  }
829
+ function writeHeadMeta(html, page) {
830
+ const meta = typeof page?.getHeadMeta === "function" ? page.getHeadMeta() : { description: page?.description || null };
831
+ const end = html.indexOf("</head>");
832
+ if (!meta || end === -1) return html;
833
+ const og = meta.og || {};
834
+ const keywords = Array.isArray(meta.keywords) ? meta.keywords.join(", ") : meta.keywords;
835
+ const metaTags = [
836
+ ["name", "description", meta.description],
837
+ ["name", "keywords", keywords],
838
+ ["name", "robots", meta.robots],
839
+ ["property", "og:type", "website"],
840
+ ["property", "og:site_name", og.siteName],
841
+ ["property", "og:title", og.title],
842
+ ["property", "og:description", og.description],
843
+ ["property", "og:image", og.image],
844
+ ["property", "og:url", og.url],
845
+ ["name", "twitter:card", og.image ? "summary_large_image" : "summary"],
846
+ ["name", "twitter:site", meta.twitter?.site],
847
+ ["name", "twitter:title", og.title],
848
+ ["name", "twitter:description", og.description],
849
+ ["name", "twitter:image", og.image]
850
+ ];
851
+ let head = html.slice(0, end);
852
+ const written = [];
853
+ for (const [attr, key, value] of metaTags) {
854
+ head = head.replace(new RegExp(`<meta\\b[^>]*\\b(?:name|property)=["']${key}["'][^>]*>\\s*`, "gi"), "");
855
+ if (value) written.push(`<meta ${attr}="${key}" content="${escapeHtml(value)}">`);
856
+ }
857
+ head = head.replace(/<link\b[^>]*\brel=["']canonical["'][^>]*>\s*/gi, "");
858
+ if (meta.canonical) written.push(`<link rel="canonical" href="${escapeHtml(meta.canonical)}">`);
859
+ head = head.replace(/<link\b[^>]*\bhreflang=["'][^"']*["'][^>]*>\s*/gi, "");
860
+ for (const { hreflang, url } of meta.alternates || []) {
861
+ written.push(`<link rel="alternate" hreflang="${escapeHtml(hreflang)}" href="${escapeHtml(url)}">`);
862
+ }
863
+ return `${head}${written.join("\n")}
864
+ ${html.slice(end)}`;
865
+ }
856
866
  function generate404Html({ baseHtml, website, siteContent }) {
857
867
  const dynamicTemplates = siteContent.pages?.filter((p) => p.isDynamic || isDynamicRoute(p.route)) || [];
858
868
  const routePatterns = dynamicTemplates.map((p) => routePatternToRegex(p.route).regex.source);
@@ -1171,6 +1181,12 @@ function safeParseJSON(text) {
1171
1181
  return void 0;
1172
1182
  }
1173
1183
  }
1184
+ function requireTransport(fetch2, entry) {
1185
+ if (typeof fetch2 === "function") return fetch2;
1186
+ throw new Error(
1187
+ `${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\`.`
1188
+ );
1189
+ }
1174
1190
  function blocksOf(page) {
1175
1191
  const out = [];
1176
1192
  const walk = (list) => {
@@ -1189,6 +1205,7 @@ async function loadPageData({ website, route, fetch: fetch2, dev = false, locale
1189
1205
  if (prerender !== "author" && prerender !== "always") {
1190
1206
  throw new Error(`loadPageData: prerender must be 'author' or 'always', got ${JSON.stringify(prerender)}`);
1191
1207
  }
1208
+ requireTransport(fetch2, "loadPageData");
1192
1209
  const active = website?.getActiveLocale?.() ?? null;
1193
1210
  if (locale && active && locale !== active) {
1194
1211
  throw new Error(
@@ -1259,16 +1276,13 @@ async function prefetchAndHydrate({ website, content, route, locale = null, fetc
1259
1276
  if (!website?.dataStore) {
1260
1277
  throw new Error("prefetchAndHydrate: `website` must be an initialized Website with a dataStore");
1261
1278
  }
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
- }
1279
+ requireTransport(fetch2, "prefetchAndHydrate");
1267
1280
  const fetched = await loadPageData({ website, route, fetch: fetch2, dev, locale, prerender });
1268
1281
  hydrateDataStore(website, fetched);
1269
1282
  return fetched;
1270
1283
  }
1271
1284
  async function collectSiteRecords(content, { locale, fetch: fetch2, signal, only = null, whole = false, maxPages } = {}) {
1285
+ requireTransport(fetch2, "collectSiteRecords");
1272
1286
  const config = content?.config;
1273
1287
  const services = config?.services ?? null;
1274
1288
  const queries = config?.queries ?? null;