@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 +57 -43
- package/dist/ssr.js.map +1 -1
- package/package.json +3 -3
- package/src/collect-records.js +7 -1
- package/src/components/BlockRenderer.jsx +10 -2
- package/src/components/Layout.jsx +3 -2
- package/src/default-fetcher.js +4 -1
- package/src/hooks/useHeadMeta.js +42 -7
- package/src/hooks/useLinkInterceptor.js +20 -23
- package/src/load-page-data.js +6 -1
- package/src/page-renderer.js +21 -40
- package/src/prepare-props.js +31 -19
- package/src/require-transport.js +35 -0
- package/src/ssr-renderer.js +60 -39
- package/src/sticky-check.js +106 -33
- package/src/sticky.js +5 -3
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,
|
|
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,
|
|
125
|
-
if (!block.website?.entityStore?.dev) return;
|
|
126
|
-
const
|
|
127
|
-
for (const
|
|
128
|
-
const
|
|
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 \`${
|
|
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
|
-
|
|
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
|
-
|
|
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;
|