@uniweb/runtime 0.2.13 → 0.2.16

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 ADDED
@@ -0,0 +1,385 @@
1
+ import React, { useState, useEffect } from "react";
2
+ import { jsxs, jsx, Fragment } from "react/jsx-runtime";
3
+ function guaranteeItemStructure(item) {
4
+ return {
5
+ title: item.title || "",
6
+ pretitle: item.pretitle || "",
7
+ subtitle: item.subtitle || "",
8
+ paragraphs: item.paragraphs || [],
9
+ links: item.links || [],
10
+ imgs: item.imgs || [],
11
+ lists: item.lists || [],
12
+ icons: item.icons || [],
13
+ videos: item.videos || [],
14
+ buttons: item.buttons || [],
15
+ data: item.data || {},
16
+ cards: item.cards || [],
17
+ documents: item.documents || [],
18
+ forms: item.forms || [],
19
+ quotes: item.quotes || [],
20
+ headings: item.headings || []
21
+ };
22
+ }
23
+ function guaranteeContentStructure(parsedContent) {
24
+ const content = parsedContent || {};
25
+ return {
26
+ // Flat header fields
27
+ title: content.title || "",
28
+ pretitle: content.pretitle || "",
29
+ subtitle: content.subtitle || "",
30
+ subtitle2: content.subtitle2 || "",
31
+ alignment: content.alignment || null,
32
+ // Flat body fields
33
+ paragraphs: content.paragraphs || [],
34
+ links: content.links || [],
35
+ imgs: content.imgs || [],
36
+ lists: content.lists || [],
37
+ icons: content.icons || [],
38
+ videos: content.videos || [],
39
+ buttons: content.buttons || [],
40
+ data: content.data || {},
41
+ cards: content.cards || [],
42
+ documents: content.documents || [],
43
+ forms: content.forms || [],
44
+ quotes: content.quotes || [],
45
+ headings: content.headings || [],
46
+ // Items with guaranteed structure
47
+ items: (content.items || []).map(guaranteeItemStructure),
48
+ // Sequence for ordered rendering
49
+ sequence: content.sequence || [],
50
+ // Preserve raw content if present
51
+ raw: content.raw
52
+ };
53
+ }
54
+ function applySchemaToObject(obj, schema) {
55
+ if (!obj || typeof obj !== "object" || Array.isArray(obj)) {
56
+ return obj;
57
+ }
58
+ const result = { ...obj };
59
+ for (const [field, fieldDef] of Object.entries(schema)) {
60
+ const defaultValue = typeof fieldDef === "object" ? fieldDef.default : void 0;
61
+ if (result[field] === void 0 && defaultValue !== void 0) {
62
+ result[field] = defaultValue;
63
+ }
64
+ if (typeof fieldDef === "object" && fieldDef.options && Array.isArray(fieldDef.options)) {
65
+ if (result[field] !== void 0 && !fieldDef.options.includes(result[field])) {
66
+ if (defaultValue !== void 0) {
67
+ result[field] = defaultValue;
68
+ }
69
+ }
70
+ }
71
+ if (typeof fieldDef === "object" && fieldDef.type === "object" && fieldDef.schema && result[field]) {
72
+ result[field] = applySchemaToObject(result[field], fieldDef.schema);
73
+ }
74
+ if (typeof fieldDef === "object" && fieldDef.type === "array" && fieldDef.of && result[field]) {
75
+ if (typeof fieldDef.of === "object") {
76
+ result[field] = result[field].map((item) => applySchemaToObject(item, fieldDef.of));
77
+ }
78
+ }
79
+ }
80
+ return result;
81
+ }
82
+ function applySchemaToValue(value, schema) {
83
+ if (Array.isArray(value)) {
84
+ return value.map((item) => applySchemaToObject(item, schema));
85
+ }
86
+ return applySchemaToObject(value, schema);
87
+ }
88
+ function applySchemas(data, schemas) {
89
+ if (!schemas || !data || typeof data !== "object") {
90
+ return data || {};
91
+ }
92
+ const result = { ...data };
93
+ for (const [tag, rawValue] of Object.entries(data)) {
94
+ const schema = schemas[tag];
95
+ if (!schema) continue;
96
+ result[tag] = applySchemaToValue(rawValue, schema);
97
+ }
98
+ return result;
99
+ }
100
+ function applyDefaults(params, defaults) {
101
+ if (!defaults || Object.keys(defaults).length === 0) {
102
+ return params || {};
103
+ }
104
+ return {
105
+ ...defaults,
106
+ ...params || {}
107
+ };
108
+ }
109
+ function applyCascadedData(localData, cascadedData, inheritData) {
110
+ if (!inheritData || !cascadedData || Object.keys(cascadedData).length === 0) {
111
+ return localData;
112
+ }
113
+ if (inheritData === true) {
114
+ return { ...cascadedData, ...localData };
115
+ }
116
+ if (Array.isArray(inheritData)) {
117
+ const result = { ...localData };
118
+ for (const key of inheritData) {
119
+ if (cascadedData[key] !== void 0 && result[key] === void 0) {
120
+ result[key] = cascadedData[key];
121
+ }
122
+ }
123
+ return result;
124
+ }
125
+ return localData;
126
+ }
127
+ function prepareProps(block, meta) {
128
+ const defaults = meta?.defaults || {};
129
+ const params = applyDefaults(block.properties, defaults);
130
+ const content = guaranteeContentStructure(block.parsedContent);
131
+ const inheritData = meta?.inheritData;
132
+ const cascadedData = block.cascadedData || {};
133
+ if (inheritData) {
134
+ content.data = applyCascadedData(content.data, cascadedData, inheritData);
135
+ }
136
+ const schemas = meta?.schemas || null;
137
+ if (schemas && content.data) {
138
+ content.data = applySchemas(content.data, schemas);
139
+ }
140
+ return { content, params };
141
+ }
142
+ function getComponentMeta(componentName) {
143
+ return globalThis.uniweb?.getComponentMeta?.(componentName) || null;
144
+ }
145
+ function getComponentDefaults(componentName) {
146
+ return globalThis.uniweb?.getComponentDefaults?.(componentName) || {};
147
+ }
148
+ function getNestedValue(obj, path) {
149
+ if (!obj || !path) return obj;
150
+ const parts = path.split(".");
151
+ let current = obj;
152
+ for (const part of parts) {
153
+ if (current === null || current === void 0) return void 0;
154
+ current = current[part];
155
+ }
156
+ return current;
157
+ }
158
+ async function executeFetchClient(config) {
159
+ if (!config) return { data: null };
160
+ const { path, url, transform } = config;
161
+ try {
162
+ const fetchUrl = path || url;
163
+ if (!fetchUrl) {
164
+ return { data: [], error: "No path or url specified" };
165
+ }
166
+ const response = await fetch(fetchUrl);
167
+ if (!response.ok) {
168
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
169
+ }
170
+ const contentType = response.headers.get("content-type") || "";
171
+ let data;
172
+ if (contentType.includes("application/json")) {
173
+ data = await response.json();
174
+ } else {
175
+ const text = await response.text();
176
+ try {
177
+ data = JSON.parse(text);
178
+ } catch {
179
+ console.warn("[data-fetcher] Response is not JSON, returning as text");
180
+ data = text;
181
+ }
182
+ }
183
+ if (transform && data) {
184
+ data = getNestedValue(data, transform);
185
+ }
186
+ return { data: data ?? [] };
187
+ } catch (error) {
188
+ console.warn(`[data-fetcher] Client fetch failed: ${error.message}`);
189
+ return { data: [], error: error.message };
190
+ }
191
+ }
192
+ function mergeIntoData(currentData, fetchedData, schema, merge = false) {
193
+ if (fetchedData === null || fetchedData === void 0 || !schema) {
194
+ return currentData;
195
+ }
196
+ const result = { ...currentData || {} };
197
+ if (merge && result[schema] !== void 0) {
198
+ const existing = result[schema];
199
+ if (Array.isArray(existing) && Array.isArray(fetchedData)) {
200
+ result[schema] = [...existing, ...fetchedData];
201
+ } else if (typeof existing === "object" && existing !== null && typeof fetchedData === "object" && fetchedData !== null && !Array.isArray(existing) && !Array.isArray(fetchedData)) {
202
+ result[schema] = { ...existing, ...fetchedData };
203
+ } else {
204
+ result[schema] = fetchedData;
205
+ }
206
+ } else {
207
+ result[schema] = fetchedData;
208
+ }
209
+ return result;
210
+ }
211
+ const hexToRgba = (hex, opacity) => {
212
+ const r = parseInt(hex.slice(1, 3), 16);
213
+ const g = parseInt(hex.slice(3, 5), 16);
214
+ const b = parseInt(hex.slice(5, 7), 16);
215
+ return `rgba(${r},${g},${b},${opacity})`;
216
+ };
217
+ const getWrapperProps = (block) => {
218
+ const theme = block.themeName;
219
+ const blockClassName = block.state?.className || "";
220
+ let className = theme || "";
221
+ if (blockClassName) {
222
+ className = className ? `${className} ${blockClassName}` : blockClassName;
223
+ }
224
+ const { background = {}, colors = {} } = block.standardOptions;
225
+ const style = {};
226
+ if (background.mode === "gradient") {
227
+ const {
228
+ enabled = false,
229
+ start = "transparent",
230
+ end = "transparent",
231
+ angle = 0,
232
+ startPosition = 0,
233
+ endPosition = 100,
234
+ startOpacity = 0.7,
235
+ endOpacity = 0.3
236
+ } = background.gradient || {};
237
+ if (enabled) {
238
+ style["--bg-color"] = `linear-gradient(${angle}deg,
239
+ ${hexToRgba(start, startOpacity)} ${startPosition}%,
240
+ ${hexToRgba(end, endOpacity)} ${endPosition}%)`;
241
+ }
242
+ } else if (background.mode === "image" || background.mode === "video") {
243
+ const settings = background[background.mode] || {};
244
+ const { url = "", file = "" } = settings;
245
+ if (url || file) {
246
+ style["--bg-color"] = "transparent";
247
+ style.position = "relative";
248
+ style.maxWidth = "100%";
249
+ }
250
+ }
251
+ return {
252
+ id: `Section${block.id}`,
253
+ style,
254
+ className
255
+ };
256
+ };
257
+ function BlockRenderer({ block, pure = false, extra = {} }) {
258
+ const [runtimeData, setRuntimeData] = useState(null);
259
+ const [fetchError, setFetchError] = useState(null);
260
+ const Component = block.initComponent();
261
+ const fetchConfig = block.fetch;
262
+ const shouldFetchAtRuntime = fetchConfig && fetchConfig.prerender === false;
263
+ useEffect(() => {
264
+ if (!shouldFetchAtRuntime) return;
265
+ let cancelled = false;
266
+ async function doFetch() {
267
+ const result = await executeFetchClient(fetchConfig);
268
+ if (cancelled) return;
269
+ if (result.error) {
270
+ setFetchError(result.error);
271
+ }
272
+ if (result.data) {
273
+ setRuntimeData({ [fetchConfig.schema]: result.data });
274
+ }
275
+ }
276
+ doFetch();
277
+ return () => {
278
+ cancelled = true;
279
+ };
280
+ }, [shouldFetchAtRuntime, fetchConfig]);
281
+ if (!Component) {
282
+ return /* @__PURE__ */ jsxs("div", { className: "block-error", style: { padding: "1rem", background: "#fef2f2", color: "#dc2626" }, children: [
283
+ "Component not found: ",
284
+ block.type
285
+ ] });
286
+ }
287
+ let content, params;
288
+ if (block.parsedContent?._isPoc) {
289
+ content = block.parsedContent._pocContent;
290
+ params = block.properties;
291
+ } else {
292
+ const meta = getComponentMeta(block.type);
293
+ const prepared = prepareProps(block, meta);
294
+ params = prepared.params;
295
+ content = {
296
+ ...prepared.content,
297
+ ...block.properties,
298
+ // Frontmatter params overlay (legacy support)
299
+ _prosemirror: block.parsedContent
300
+ // Keep original for components that need raw access
301
+ };
302
+ if (runtimeData && shouldFetchAtRuntime) {
303
+ content.data = mergeIntoData(content.data, runtimeData[fetchConfig.schema], fetchConfig.schema, fetchConfig.merge);
304
+ }
305
+ }
306
+ const componentProps = {
307
+ content,
308
+ params,
309
+ block,
310
+ input: block.input
311
+ };
312
+ if (pure) {
313
+ return /* @__PURE__ */ jsx(Component, { ...componentProps, extra });
314
+ }
315
+ const wrapperProps = getWrapperProps(block);
316
+ return /* @__PURE__ */ jsx("div", { ...wrapperProps, children: /* @__PURE__ */ jsx(Component, { ...componentProps }) });
317
+ }
318
+ function Blocks({ blocks, extra = {} }) {
319
+ if (!blocks || blocks.length === 0) return null;
320
+ return blocks.map((block, index) => /* @__PURE__ */ jsx(React.Fragment, { children: /* @__PURE__ */ jsx(BlockRenderer, { block, extra }) }, block.id || index));
321
+ }
322
+ function DefaultLayout({ header, body, footer }) {
323
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
324
+ header,
325
+ body,
326
+ footer
327
+ ] });
328
+ }
329
+ function Layout({ page, website }) {
330
+ const RemoteLayout = website.getRemoteLayout();
331
+ const headerBlocks = page.getHeaderBlocks();
332
+ const bodyBlocks = page.getBodyBlocks();
333
+ const footerBlocks = page.getFooterBlocks();
334
+ const leftBlocks = page.getLeftBlocks();
335
+ const rightBlocks = page.getRightBlocks();
336
+ const headerElement = headerBlocks ? /* @__PURE__ */ jsx(Blocks, { blocks: headerBlocks }) : null;
337
+ const bodyElement = bodyBlocks ? /* @__PURE__ */ jsx(Blocks, { blocks: bodyBlocks }) : null;
338
+ const footerElement = footerBlocks ? /* @__PURE__ */ jsx(Blocks, { blocks: footerBlocks }) : null;
339
+ const leftElement = leftBlocks ? /* @__PURE__ */ jsx(Blocks, { blocks: leftBlocks }) : null;
340
+ const rightElement = rightBlocks ? /* @__PURE__ */ jsx(Blocks, { blocks: rightBlocks }) : null;
341
+ if (RemoteLayout) {
342
+ return /* @__PURE__ */ jsx(
343
+ RemoteLayout,
344
+ {
345
+ page,
346
+ website,
347
+ header: headerElement,
348
+ body: bodyElement,
349
+ footer: footerElement,
350
+ left: leftElement,
351
+ right: rightElement,
352
+ leftPanel: leftElement,
353
+ rightPanel: rightElement
354
+ }
355
+ );
356
+ }
357
+ return /* @__PURE__ */ jsx(
358
+ DefaultLayout,
359
+ {
360
+ header: headerElement,
361
+ body: bodyElement,
362
+ footer: footerElement
363
+ }
364
+ );
365
+ }
366
+ function PageElement({ page, website }) {
367
+ return React.createElement(
368
+ "main",
369
+ null,
370
+ React.createElement(Layout, { page, website })
371
+ );
372
+ }
373
+ export {
374
+ BlockRenderer,
375
+ Blocks,
376
+ Layout,
377
+ PageElement,
378
+ applyDefaults,
379
+ applySchemas,
380
+ getComponentDefaults,
381
+ getComponentMeta,
382
+ guaranteeContentStructure,
383
+ prepareProps
384
+ };
385
+ //# sourceMappingURL=ssr.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ssr.js","sources":["../src/prepare-props.js","../src/data-fetcher-client.js","../src/components/BlockRenderer.jsx","../src/components/Blocks.jsx","../src/components/Layout.jsx","../src/ssr.js"],"sourcesContent":["/**\n * Props Preparation for Runtime Guarantees\n *\n * Prepares props for foundation components with:\n * - Param defaults from runtime schema\n * - Guaranteed content structure (no null checks needed)\n *\n * This enables simpler component code by ensuring predictable prop shapes.\n */\n\n/**\n * Guarantee item has flat content structure\n *\n * @param {Object} item - Raw item from parser\n * @returns {Object} Item with guaranteed flat structure\n */\nfunction guaranteeItemStructure(item) {\n return {\n title: item.title || '',\n pretitle: item.pretitle || '',\n subtitle: item.subtitle || '',\n paragraphs: item.paragraphs || [],\n links: item.links || [],\n imgs: item.imgs || [],\n lists: item.lists || [],\n icons: item.icons || [],\n videos: item.videos || [],\n buttons: item.buttons || [],\n data: item.data || {},\n cards: item.cards || [],\n documents: item.documents || [],\n forms: item.forms || [],\n quotes: item.quotes || [],\n headings: item.headings || [],\n }\n}\n\n/**\n * Guarantee content structure exists\n * Returns a flat content object with all standard fields guaranteed to exist\n *\n * @param {Object} parsedContent - Raw parsed content from semantic parser (flat structure)\n * @returns {Object} Content with guaranteed flat structure\n */\nexport function guaranteeContentStructure(parsedContent) {\n const content = parsedContent || {}\n\n return {\n // Flat header fields\n title: content.title || '',\n pretitle: content.pretitle || '',\n subtitle: content.subtitle || '',\n subtitle2: content.subtitle2 || '',\n alignment: content.alignment || null,\n\n // Flat body fields\n paragraphs: content.paragraphs || [],\n links: content.links || [],\n imgs: content.imgs || [],\n lists: content.lists || [],\n icons: content.icons || [],\n videos: content.videos || [],\n buttons: content.buttons || [],\n data: content.data || {},\n cards: content.cards || [],\n documents: content.documents || [],\n forms: content.forms || [],\n quotes: content.quotes || [],\n headings: content.headings || [],\n\n // Items with guaranteed structure\n items: (content.items || []).map(guaranteeItemStructure),\n\n // Sequence for ordered rendering\n sequence: content.sequence || [],\n\n // Preserve raw content if present\n raw: content.raw,\n }\n}\n\n/**\n * Apply a schema to a single object\n * Only processes fields defined in the schema, preserves unknown fields\n *\n * @param {Object} obj - The object to process\n * @param {Object} schema - Schema definition (fieldName -> fieldDef)\n * @returns {Object} Object with schema defaults applied\n */\nfunction applySchemaToObject(obj, schema) {\n if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {\n return obj\n }\n\n const result = { ...obj }\n\n for (const [field, fieldDef] of Object.entries(schema)) {\n // Get the default value - handle both shorthand and full form\n const defaultValue = typeof fieldDef === 'object' ? fieldDef.default : undefined\n\n // Apply default if field is missing and default exists\n if (result[field] === undefined && defaultValue !== undefined) {\n result[field] = defaultValue\n }\n\n // For select fields with options, apply default if value is not among valid options\n if (typeof fieldDef === 'object' && fieldDef.options && Array.isArray(fieldDef.options)) {\n if (result[field] !== undefined && !fieldDef.options.includes(result[field])) {\n // Value exists but is not valid - apply default if available\n if (defaultValue !== undefined) {\n result[field] = defaultValue\n }\n }\n }\n\n // Handle nested object schema\n if (typeof fieldDef === 'object' && fieldDef.type === 'object' && fieldDef.schema && result[field]) {\n result[field] = applySchemaToObject(result[field], fieldDef.schema)\n }\n\n // Handle array with inline schema\n if (typeof fieldDef === 'object' && fieldDef.type === 'array' && fieldDef.of && result[field]) {\n if (typeof fieldDef.of === 'object') {\n result[field] = result[field].map(item => applySchemaToObject(item, fieldDef.of))\n }\n }\n }\n\n return result\n}\n\n/**\n * Apply a schema to a value (object or array of objects)\n *\n * @param {Object|Array} value - The value to process\n * @param {Object} schema - Schema definition\n * @returns {Object|Array} Value with schema defaults applied\n */\nfunction applySchemaToValue(value, schema) {\n if (Array.isArray(value)) {\n return value.map(item => applySchemaToObject(item, schema))\n }\n return applySchemaToObject(value, schema)\n}\n\n/**\n * Apply schemas to content.data\n * Only processes tags that have a matching schema, leaves others untouched\n *\n * @param {Object} data - The data object from content\n * @param {Object} schemas - Schema definitions from runtime meta\n * @returns {Object} Data with schemas applied\n */\nexport function applySchemas(data, schemas) {\n if (!schemas || !data || typeof data !== 'object') {\n return data || {}\n }\n\n const result = { ...data }\n\n for (const [tag, rawValue] of Object.entries(data)) {\n const schema = schemas[tag]\n if (!schema) continue // No schema for this tag - leave as-is\n\n result[tag] = applySchemaToValue(rawValue, schema)\n }\n\n return result\n}\n\n/**\n * Apply param defaults from runtime schema\n *\n * @param {Object} params - Params from frontmatter\n * @param {Object} defaults - Default values from runtime schema\n * @returns {Object} Merged params with defaults applied\n */\nexport function applyDefaults(params, defaults) {\n if (!defaults || Object.keys(defaults).length === 0) {\n return params || {}\n }\n\n return {\n ...defaults,\n ...(params || {}),\n }\n}\n\n/**\n * Apply cascaded data based on component's inheritData setting\n *\n * @param {Object} localData - content.data from the section itself\n * @param {Object} cascadedData - Data from page/site level fetches\n * @param {boolean|Array} inheritData - Component's inheritData setting\n * @returns {Object} Merged data object\n */\nfunction applyCascadedData(localData, cascadedData, inheritData) {\n if (!inheritData || !cascadedData || Object.keys(cascadedData).length === 0) {\n return localData\n }\n\n if (inheritData === true) {\n // Inherit all: cascaded data as base, local data overrides\n return { ...cascadedData, ...localData }\n }\n\n if (Array.isArray(inheritData)) {\n // Selective: only specified schemas, local data takes precedence\n const result = { ...localData }\n for (const key of inheritData) {\n if (cascadedData[key] !== undefined && result[key] === undefined) {\n result[key] = cascadedData[key]\n }\n }\n return result\n }\n\n return localData\n}\n\n/**\n * Prepare props for a component with runtime guarantees\n *\n * @param {Object} block - The block instance\n * @param {Object} meta - Runtime metadata for the component (from meta[componentName])\n * @returns {Object} Prepared props: { content, params }\n */\nexport function prepareProps(block, meta) {\n // Apply param defaults\n const defaults = meta?.defaults || {}\n const params = applyDefaults(block.properties, defaults)\n\n // Guarantee content structure\n const content = guaranteeContentStructure(block.parsedContent)\n\n // Apply cascaded data based on component's inheritData setting\n const inheritData = meta?.inheritData\n const cascadedData = block.cascadedData || {}\n if (inheritData) {\n content.data = applyCascadedData(content.data, cascadedData, inheritData)\n }\n\n // Apply schemas to content.data\n const schemas = meta?.schemas || null\n if (schemas && content.data) {\n content.data = applySchemas(content.data, schemas)\n }\n\n return { content, params }\n}\n\n/**\n * Get runtime metadata for a component from the global uniweb instance\n *\n * @param {string} componentName\n * @returns {Object|null}\n */\nexport function getComponentMeta(componentName) {\n return globalThis.uniweb?.getComponentMeta?.(componentName) || null\n}\n\n/**\n * Get default param values for a component\n *\n * @param {string} componentName\n * @returns {Object}\n */\nexport function getComponentDefaults(componentName) {\n return globalThis.uniweb?.getComponentDefaults?.(componentName) || {}\n}\n","/**\n * Client-side Data Fetcher\n *\n * Executes fetch operations in the browser for runtime data loading.\n * Used when prerender: false is set on fetch configurations.\n *\n * @module @uniweb/runtime/data-fetcher-client\n */\n\n/**\n * Get a nested value from an object using dot notation\n *\n * @param {object} obj - Source object\n * @param {string} path - Dot-separated path (e.g., 'data.items')\n * @returns {any} The nested value or undefined\n */\nfunction getNestedValue(obj, path) {\n if (!obj || !path) return obj\n\n const parts = path.split('.')\n let current = obj\n\n for (const part of parts) {\n if (current === null || current === undefined) return undefined\n current = current[part]\n }\n\n return current\n}\n\n/**\n * Execute a fetch operation in the browser\n *\n * @param {object} config - Normalized fetch config\n * @param {string} config.path - Local path (relative to site root)\n * @param {string} config.url - Remote URL\n * @param {string} config.schema - Schema key for data\n * @param {string} config.transform - Optional path to extract from response\n * @returns {Promise<{ data: any, error?: string }>} Fetched data or error\n *\n * @example\n * const result = await executeFetchClient({\n * path: '/data/team.json',\n * schema: 'team'\n * })\n * // result.data contains the parsed JSON array\n */\nexport async function executeFetchClient(config) {\n if (!config) return { data: null }\n\n const { path, url, transform } = config\n\n try {\n // Determine the fetch URL\n // For local paths, they're relative to the site root (served from public/)\n // For remote URLs, use as-is\n const fetchUrl = path || url\n\n if (!fetchUrl) {\n return { data: [], error: 'No path or url specified' }\n }\n\n const response = await fetch(fetchUrl)\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`)\n }\n\n // Parse response based on content type\n const contentType = response.headers.get('content-type') || ''\n let data\n\n if (contentType.includes('application/json')) {\n data = await response.json()\n } else {\n // Try JSON first\n const text = await response.text()\n try {\n data = JSON.parse(text)\n } catch {\n // Return text as-is if not JSON\n console.warn('[data-fetcher] Response is not JSON, returning as text')\n data = text\n }\n }\n\n // Apply transform if specified (extract nested path)\n if (transform && data) {\n data = getNestedValue(data, transform)\n }\n\n return { data: data ?? [] }\n } catch (error) {\n console.warn(`[data-fetcher] Client fetch failed: ${error.message}`)\n return { data: [], error: error.message }\n }\n}\n\n/**\n * Merge fetched data into existing content.data\n *\n * @param {object} currentData - Current content.data object\n * @param {any} fetchedData - Data from fetch\n * @param {string} schema - Schema key to store under\n * @param {boolean} [merge=false] - If true, merge with existing; if false, replace\n * @returns {object} Updated data object\n */\nexport function mergeIntoData(currentData, fetchedData, schema, merge = false) {\n if (fetchedData === null || fetchedData === undefined || !schema) {\n return currentData\n }\n\n const result = { ...(currentData || {}) }\n\n if (merge && result[schema] !== undefined) {\n // Merge mode: combine with existing data\n const existing = result[schema]\n\n if (Array.isArray(existing) && Array.isArray(fetchedData)) {\n // Arrays: concatenate\n result[schema] = [...existing, ...fetchedData]\n } else if (\n typeof existing === 'object' &&\n existing !== null &&\n typeof fetchedData === 'object' &&\n fetchedData !== null &&\n !Array.isArray(existing) &&\n !Array.isArray(fetchedData)\n ) {\n // Objects: shallow merge\n result[schema] = { ...existing, ...fetchedData }\n } else {\n // Different types: fetched data wins\n result[schema] = fetchedData\n }\n } else {\n // Replace mode (default): fetched data overwrites\n result[schema] = fetchedData\n }\n\n return result\n}\n\nexport default executeFetchClient\n","/**\n * BlockRenderer\n *\n * Bridges Block data to foundation components.\n * Handles theming, wrapper props, and runtime guarantees.\n * Supports runtime data fetching for prerender: false configs.\n */\n\nimport React, { useState, useEffect } from 'react'\nimport { prepareProps, getComponentMeta } from '../prepare-props.js'\nimport { executeFetchClient, mergeIntoData } from '../data-fetcher-client.js'\n\n/**\n * Convert hex color to rgba\n */\nconst hexToRgba = (hex, opacity) => {\n const r = parseInt(hex.slice(1, 3), 16)\n const g = parseInt(hex.slice(3, 5), 16)\n const b = parseInt(hex.slice(5, 7), 16)\n return `rgba(${r},${g},${b},${opacity})`\n}\n\n/**\n * Build wrapper props from block configuration\n */\nconst getWrapperProps = (block) => {\n const theme = block.themeName\n const blockClassName = block.state?.className || ''\n\n let className = theme || ''\n if (blockClassName) {\n className = className ? `${className} ${blockClassName}` : blockClassName\n }\n\n const { background = {}, colors = {} } = block.standardOptions\n const style = {}\n\n // Handle background modes\n if (background.mode === 'gradient') {\n const {\n enabled = false,\n start = 'transparent',\n end = 'transparent',\n angle = 0,\n startPosition = 0,\n endPosition = 100,\n startOpacity = 0.7,\n endOpacity = 0.3\n } = background.gradient || {}\n\n if (enabled) {\n style['--bg-color'] = `linear-gradient(${angle}deg,\n ${hexToRgba(start, startOpacity)} ${startPosition}%,\n ${hexToRgba(end, endOpacity)} ${endPosition}%)`\n }\n } else if (background.mode === 'image' || background.mode === 'video') {\n const settings = background[background.mode] || {}\n const { url = '', file = '' } = settings\n\n if (url || file) {\n style['--bg-color'] = 'transparent'\n style.position = 'relative'\n style.maxWidth = '100%'\n }\n }\n\n return {\n id: `Section${block.id}`,\n style,\n className\n }\n}\n\n/**\n * BlockRenderer component\n */\nexport default function BlockRenderer({ block, pure = false, extra = {} }) {\n // State for runtime-fetched data (when prerender: false)\n const [runtimeData, setRuntimeData] = useState(null)\n const [fetchError, setFetchError] = useState(null)\n\n const Component = block.initComponent()\n\n // Runtime fetch for prerender: false configurations\n const fetchConfig = block.fetch\n const shouldFetchAtRuntime = fetchConfig && fetchConfig.prerender === false\n\n useEffect(() => {\n if (!shouldFetchAtRuntime) return\n\n let cancelled = false\n\n async function doFetch() {\n const result = await executeFetchClient(fetchConfig)\n if (cancelled) return\n\n if (result.error) {\n setFetchError(result.error)\n }\n if (result.data) {\n setRuntimeData({ [fetchConfig.schema]: result.data })\n }\n }\n\n doFetch()\n\n return () => {\n cancelled = true\n }\n }, [shouldFetchAtRuntime, fetchConfig])\n\n if (!Component) {\n return (\n <div className=\"block-error\" style={{ padding: '1rem', background: '#fef2f2', color: '#dc2626' }}>\n Component not found: {block.type}\n </div>\n )\n }\n\n // Build content and params with runtime guarantees\n // Sources:\n // 1. parsedContent._isPoc - simple PoC format (hardcoded content)\n // 2. parsedContent - semantic parser output (flat: title, paragraphs, links, etc.)\n // 3. block.properties - params from frontmatter (theme, alignment, etc.)\n // 4. meta - defaults from component meta.js\n let content, params\n\n if (block.parsedContent?._isPoc) {\n // Simple PoC format - content was passed directly\n content = block.parsedContent._pocContent\n params = block.properties\n } else {\n // Get runtime metadata for this component (has defaults, data binding, etc.)\n const meta = getComponentMeta(block.type)\n\n // Prepare props with runtime guarantees:\n // - Apply param defaults from meta.js\n // - Guarantee content structure exists\n // - Apply cascaded data based on inheritData\n const prepared = prepareProps(block, meta)\n params = prepared.params\n\n // Merge prepared content with raw access for components that need it\n content = {\n ...prepared.content,\n ...block.properties, // Frontmatter params overlay (legacy support)\n _prosemirror: block.parsedContent // Keep original for components that need raw access\n }\n\n // Merge runtime-fetched data if available\n if (runtimeData && shouldFetchAtRuntime) {\n content.data = mergeIntoData(content.data, runtimeData[fetchConfig.schema], fetchConfig.schema, fetchConfig.merge)\n }\n }\n\n const componentProps = {\n content,\n params,\n block,\n input: block.input\n }\n\n if (pure) {\n return <Component {...componentProps} extra={extra} />\n }\n\n const wrapperProps = getWrapperProps(block)\n\n return (\n <div {...wrapperProps}>\n <Component {...componentProps} />\n </div>\n )\n}\n","/**\n * Blocks\n *\n * Renders an array of blocks for a layout area (header, body, footer, panels).\n * Used by the Layout component to pre-render each area.\n */\n\nimport React from 'react'\nimport BlockRenderer from './BlockRenderer.jsx'\n\n/**\n * Render a list of blocks\n *\n * @param {Object} props\n * @param {Block[]} props.blocks - Array of Block instances to render\n * @param {Object} [props.extra] - Extra props to pass to each block\n */\nexport default function Blocks({ blocks, extra = {} }) {\n if (!blocks || blocks.length === 0) return null\n\n return blocks.map((block, index) => (\n <React.Fragment key={block.id || index}>\n <BlockRenderer block={block} extra={extra} />\n </React.Fragment>\n ))\n}\n","/**\n * Layout\n *\n * Orchestrates page rendering by assembling layout areas (header, body, footer, panels).\n * Supports foundation-provided custom Layout components via website.getRemoteLayout().\n *\n * Layout Areas:\n * - header: Top navigation, branding (from @header page)\n * - body: Main page content (from page sections)\n * - footer: Bottom navigation, copyright (from @footer page)\n * - left: Left sidebar/panel (from @left page)\n * - right: Right sidebar/panel (from @right page)\n *\n * Custom Layouts:\n * Foundations can provide a custom Layout via src/exports.js:\n *\n * ```jsx\n * // src/exports.js\n * import Layout from './components/Layout'\n *\n * export default {\n * Layout,\n * props: {\n * themeToggleEnabled: true,\n * }\n * }\n * ```\n *\n * The Layout component receives pre-rendered areas as props:\n * - page, website: Runtime context\n * - header, body, footer: Pre-rendered React elements\n * - left, right (or leftPanel, rightPanel): Sidebar panels\n */\n\nimport Blocks from './Blocks.jsx'\n\n/**\n * Default layout - renders header, body, footer in sequence\n * (no panels in default layout)\n */\nfunction DefaultLayout({ header, body, footer }) {\n return (\n <>\n {header}\n {body}\n {footer}\n </>\n )\n}\n\n/**\n * Layout component\n *\n * @param {Object} props\n * @param {Page} props.page - Current page instance\n * @param {Website} props.website - Website instance\n */\nexport default function Layout({ page, website }) {\n // Check if foundation provides a custom Layout\n const RemoteLayout = website.getRemoteLayout()\n\n // Get block groups from page (respects layout preferences)\n const headerBlocks = page.getHeaderBlocks()\n const bodyBlocks = page.getBodyBlocks()\n const footerBlocks = page.getFooterBlocks()\n const leftBlocks = page.getLeftBlocks()\n const rightBlocks = page.getRightBlocks()\n\n // Pre-render each area as React elements\n const headerElement = headerBlocks ? <Blocks blocks={headerBlocks} /> : null\n const bodyElement = bodyBlocks ? <Blocks blocks={bodyBlocks} /> : null\n const footerElement = footerBlocks ? <Blocks blocks={footerBlocks} /> : null\n const leftElement = leftBlocks ? <Blocks blocks={leftBlocks} /> : null\n const rightElement = rightBlocks ? <Blocks blocks={rightBlocks} /> : null\n\n // Use foundation's custom Layout if provided\n if (RemoteLayout) {\n return (\n <RemoteLayout\n page={page}\n website={website}\n header={headerElement}\n body={bodyElement}\n footer={footerElement}\n left={leftElement}\n right={rightElement}\n // Aliases for backwards compatibility\n leftPanel={leftElement}\n rightPanel={rightElement}\n />\n )\n }\n\n // Default layout\n return (\n <DefaultLayout\n header={headerElement}\n body={bodyElement}\n footer={footerElement}\n />\n )\n}\n","/**\n * @uniweb/runtime/ssr - Server-Side Rendering Entry Point\n *\n * Node.js-compatible exports for SSG/prerendering.\n * This module is built to a standalone bundle that can be imported\n * directly by Node.js without Vite transpilation.\n *\n * Usage in prerender.js:\n * import { renderPage, Blocks, BlockRenderer } from '@uniweb/runtime/ssr'\n */\n\nimport React from 'react'\n\n// Props preparation (no browser APIs)\nexport {\n prepareProps,\n applySchemas,\n applyDefaults,\n guaranteeContentStructure,\n getComponentMeta,\n getComponentDefaults\n} from './prepare-props.js'\n\n// Components for rendering\nexport { default as BlockRenderer } from './components/BlockRenderer.jsx'\nexport { default as Blocks } from './components/Blocks.jsx'\nexport { default as Layout } from './components/Layout.jsx'\n\n// Re-export Layout's DefaultLayout for direct use\nimport LayoutComponent from './components/Layout.jsx'\n\n/**\n * Render a page to React elements\n *\n * This is the main entry point for SSG. It returns a React element\n * that can be passed to renderToString().\n *\n * @param {Object} props\n * @param {Page} props.page - The page instance to render\n * @param {Website} props.website - The website instance\n * @returns {React.ReactElement}\n */\nexport function PageElement({ page, website }) {\n return React.createElement(\n 'main',\n null,\n React.createElement(LayoutComponent, { page, website })\n )\n}\n"],"names":["LayoutComponent"],"mappings":"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"}
package/package.json CHANGED
@@ -1,13 +1,15 @@
1
1
  {
2
2
  "name": "@uniweb/runtime",
3
- "version": "0.2.13",
3
+ "version": "0.2.16",
4
4
  "description": "Minimal runtime for loading Uniweb foundations",
5
5
  "type": "module",
6
6
  "exports": {
7
- ".": "./src/index.jsx"
7
+ ".": "./src/index.jsx",
8
+ "./ssr": "./dist/ssr.js"
8
9
  },
9
10
  "files": [
10
- "src"
11
+ "src",
12
+ "dist"
11
13
  ],
12
14
  "keywords": [
13
15
  "uniweb",
@@ -29,11 +31,18 @@
29
31
  "node": ">=20.19"
30
32
  },
31
33
  "dependencies": {
32
- "@uniweb/core": "0.1.12"
34
+ "@uniweb/core": "0.1.13"
35
+ },
36
+ "devDependencies": {
37
+ "@vitejs/plugin-react": "^4.5.2",
38
+ "vite": "^7.3.1"
33
39
  },
34
40
  "peerDependencies": {
35
41
  "react": "^18.0.0 || ^19.0.0",
36
42
  "react-dom": "^18.0.0 || ^19.0.0",
37
43
  "react-router-dom": "^6.0.0 || ^7.0.0"
44
+ },
45
+ "scripts": {
46
+ "build:ssr": "vite build --config vite.config.ssr.js"
38
47
  }
39
48
  }
@@ -3,10 +3,12 @@
3
3
  *
4
4
  * Bridges Block data to foundation components.
5
5
  * Handles theming, wrapper props, and runtime guarantees.
6
+ * Supports runtime data fetching for prerender: false configs.
6
7
  */
7
8
 
8
- import React from 'react'
9
+ import React, { useState, useEffect } from 'react'
9
10
  import { prepareProps, getComponentMeta } from '../prepare-props.js'
11
+ import { executeFetchClient, mergeIntoData } from '../data-fetcher-client.js'
10
12
 
11
13
  /**
12
14
  * Convert hex color to rgba
@@ -73,8 +75,40 @@ const getWrapperProps = (block) => {
73
75
  * BlockRenderer component
74
76
  */
75
77
  export default function BlockRenderer({ block, pure = false, extra = {} }) {
78
+ // State for runtime-fetched data (when prerender: false)
79
+ const [runtimeData, setRuntimeData] = useState(null)
80
+ const [fetchError, setFetchError] = useState(null)
81
+
76
82
  const Component = block.initComponent()
77
83
 
84
+ // Runtime fetch for prerender: false configurations
85
+ const fetchConfig = block.fetch
86
+ const shouldFetchAtRuntime = fetchConfig && fetchConfig.prerender === false
87
+
88
+ useEffect(() => {
89
+ if (!shouldFetchAtRuntime) return
90
+
91
+ let cancelled = false
92
+
93
+ async function doFetch() {
94
+ const result = await executeFetchClient(fetchConfig)
95
+ if (cancelled) return
96
+
97
+ if (result.error) {
98
+ setFetchError(result.error)
99
+ }
100
+ if (result.data) {
101
+ setRuntimeData({ [fetchConfig.schema]: result.data })
102
+ }
103
+ }
104
+
105
+ doFetch()
106
+
107
+ return () => {
108
+ cancelled = true
109
+ }
110
+ }, [shouldFetchAtRuntime, fetchConfig])
111
+
78
112
  if (!Component) {
79
113
  return (
80
114
  <div className="block-error" style={{ padding: '1rem', background: '#fef2f2', color: '#dc2626' }}>
@@ -102,6 +136,7 @@ export default function BlockRenderer({ block, pure = false, extra = {} }) {
102
136
  // Prepare props with runtime guarantees:
103
137
  // - Apply param defaults from meta.js
104
138
  // - Guarantee content structure exists
139
+ // - Apply cascaded data based on inheritData
105
140
  const prepared = prepareProps(block, meta)
106
141
  params = prepared.params
107
142
 
@@ -111,6 +146,11 @@ export default function BlockRenderer({ block, pure = false, extra = {} }) {
111
146
  ...block.properties, // Frontmatter params overlay (legacy support)
112
147
  _prosemirror: block.parsedContent // Keep original for components that need raw access
113
148
  }
149
+
150
+ // Merge runtime-fetched data if available
151
+ if (runtimeData && shouldFetchAtRuntime) {
152
+ content.data = mergeIntoData(content.data, runtimeData[fetchConfig.schema], fetchConfig.schema, fetchConfig.merge)
153
+ }
114
154
  }
115
155
 
116
156
  const componentProps = {
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Client-side Data Fetcher
3
+ *
4
+ * Executes fetch operations in the browser for runtime data loading.
5
+ * Used when prerender: false is set on fetch configurations.
6
+ *
7
+ * @module @uniweb/runtime/data-fetcher-client
8
+ */
9
+
10
+ /**
11
+ * Get a nested value from an object using dot notation
12
+ *
13
+ * @param {object} obj - Source object
14
+ * @param {string} path - Dot-separated path (e.g., 'data.items')
15
+ * @returns {any} The nested value or undefined
16
+ */
17
+ function getNestedValue(obj, path) {
18
+ if (!obj || !path) return obj
19
+
20
+ const parts = path.split('.')
21
+ let current = obj
22
+
23
+ for (const part of parts) {
24
+ if (current === null || current === undefined) return undefined
25
+ current = current[part]
26
+ }
27
+
28
+ return current
29
+ }
30
+
31
+ /**
32
+ * Execute a fetch operation in the browser
33
+ *
34
+ * @param {object} config - Normalized fetch config
35
+ * @param {string} config.path - Local path (relative to site root)
36
+ * @param {string} config.url - Remote URL
37
+ * @param {string} config.schema - Schema key for data
38
+ * @param {string} config.transform - Optional path to extract from response
39
+ * @returns {Promise<{ data: any, error?: string }>} Fetched data or error
40
+ *
41
+ * @example
42
+ * const result = await executeFetchClient({
43
+ * path: '/data/team.json',
44
+ * schema: 'team'
45
+ * })
46
+ * // result.data contains the parsed JSON array
47
+ */
48
+ export async function executeFetchClient(config) {
49
+ if (!config) return { data: null }
50
+
51
+ const { path, url, transform } = config
52
+
53
+ try {
54
+ // Determine the fetch URL
55
+ // For local paths, they're relative to the site root (served from public/)
56
+ // For remote URLs, use as-is
57
+ const fetchUrl = path || url
58
+
59
+ if (!fetchUrl) {
60
+ return { data: [], error: 'No path or url specified' }
61
+ }
62
+
63
+ const response = await fetch(fetchUrl)
64
+
65
+ if (!response.ok) {
66
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`)
67
+ }
68
+
69
+ // Parse response based on content type
70
+ const contentType = response.headers.get('content-type') || ''
71
+ let data
72
+
73
+ if (contentType.includes('application/json')) {
74
+ data = await response.json()
75
+ } else {
76
+ // Try JSON first
77
+ const text = await response.text()
78
+ try {
79
+ data = JSON.parse(text)
80
+ } catch {
81
+ // Return text as-is if not JSON
82
+ console.warn('[data-fetcher] Response is not JSON, returning as text')
83
+ data = text
84
+ }
85
+ }
86
+
87
+ // Apply transform if specified (extract nested path)
88
+ if (transform && data) {
89
+ data = getNestedValue(data, transform)
90
+ }
91
+
92
+ return { data: data ?? [] }
93
+ } catch (error) {
94
+ console.warn(`[data-fetcher] Client fetch failed: ${error.message}`)
95
+ return { data: [], error: error.message }
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Merge fetched data into existing content.data
101
+ *
102
+ * @param {object} currentData - Current content.data object
103
+ * @param {any} fetchedData - Data from fetch
104
+ * @param {string} schema - Schema key to store under
105
+ * @param {boolean} [merge=false] - If true, merge with existing; if false, replace
106
+ * @returns {object} Updated data object
107
+ */
108
+ export function mergeIntoData(currentData, fetchedData, schema, merge = false) {
109
+ if (fetchedData === null || fetchedData === undefined || !schema) {
110
+ return currentData
111
+ }
112
+
113
+ const result = { ...(currentData || {}) }
114
+
115
+ if (merge && result[schema] !== undefined) {
116
+ // Merge mode: combine with existing data
117
+ const existing = result[schema]
118
+
119
+ if (Array.isArray(existing) && Array.isArray(fetchedData)) {
120
+ // Arrays: concatenate
121
+ result[schema] = [...existing, ...fetchedData]
122
+ } else if (
123
+ typeof existing === 'object' &&
124
+ existing !== null &&
125
+ typeof fetchedData === 'object' &&
126
+ fetchedData !== null &&
127
+ !Array.isArray(existing) &&
128
+ !Array.isArray(fetchedData)
129
+ ) {
130
+ // Objects: shallow merge
131
+ result[schema] = { ...existing, ...fetchedData }
132
+ } else {
133
+ // Different types: fetched data wins
134
+ result[schema] = fetchedData
135
+ }
136
+ } else {
137
+ // Replace mode (default): fetched data overwrites
138
+ result[schema] = fetchedData
139
+ }
140
+
141
+ return result
142
+ }
143
+
144
+ export default executeFetchClient
@@ -26,7 +26,7 @@ function guaranteeItemStructure(item) {
26
26
  icons: item.icons || [],
27
27
  videos: item.videos || [],
28
28
  buttons: item.buttons || [],
29
- properties: item.properties || {},
29
+ data: item.data || {},
30
30
  cards: item.cards || [],
31
31
  documents: item.documents || [],
32
32
  forms: item.forms || [],
@@ -61,8 +61,7 @@ export function guaranteeContentStructure(parsedContent) {
61
61
  icons: content.icons || [],
62
62
  videos: content.videos || [],
63
63
  buttons: content.buttons || [],
64
- properties: content.properties || {},
65
- propertyBlocks: content.propertyBlocks || [],
64
+ data: content.data || {},
66
65
  cards: content.cards || [],
67
66
  documents: content.documents || [],
68
67
  forms: content.forms || [],
@@ -80,6 +79,95 @@ export function guaranteeContentStructure(parsedContent) {
80
79
  }
81
80
  }
82
81
 
82
+ /**
83
+ * Apply a schema to a single object
84
+ * Only processes fields defined in the schema, preserves unknown fields
85
+ *
86
+ * @param {Object} obj - The object to process
87
+ * @param {Object} schema - Schema definition (fieldName -> fieldDef)
88
+ * @returns {Object} Object with schema defaults applied
89
+ */
90
+ function applySchemaToObject(obj, schema) {
91
+ if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
92
+ return obj
93
+ }
94
+
95
+ const result = { ...obj }
96
+
97
+ for (const [field, fieldDef] of Object.entries(schema)) {
98
+ // Get the default value - handle both shorthand and full form
99
+ const defaultValue = typeof fieldDef === 'object' ? fieldDef.default : undefined
100
+
101
+ // Apply default if field is missing and default exists
102
+ if (result[field] === undefined && defaultValue !== undefined) {
103
+ result[field] = defaultValue
104
+ }
105
+
106
+ // For select fields with options, apply default if value is not among valid options
107
+ if (typeof fieldDef === 'object' && fieldDef.options && Array.isArray(fieldDef.options)) {
108
+ if (result[field] !== undefined && !fieldDef.options.includes(result[field])) {
109
+ // Value exists but is not valid - apply default if available
110
+ if (defaultValue !== undefined) {
111
+ result[field] = defaultValue
112
+ }
113
+ }
114
+ }
115
+
116
+ // Handle nested object schema
117
+ if (typeof fieldDef === 'object' && fieldDef.type === 'object' && fieldDef.schema && result[field]) {
118
+ result[field] = applySchemaToObject(result[field], fieldDef.schema)
119
+ }
120
+
121
+ // Handle array with inline schema
122
+ if (typeof fieldDef === 'object' && fieldDef.type === 'array' && fieldDef.of && result[field]) {
123
+ if (typeof fieldDef.of === 'object') {
124
+ result[field] = result[field].map(item => applySchemaToObject(item, fieldDef.of))
125
+ }
126
+ }
127
+ }
128
+
129
+ return result
130
+ }
131
+
132
+ /**
133
+ * Apply a schema to a value (object or array of objects)
134
+ *
135
+ * @param {Object|Array} value - The value to process
136
+ * @param {Object} schema - Schema definition
137
+ * @returns {Object|Array} Value with schema defaults applied
138
+ */
139
+ function applySchemaToValue(value, schema) {
140
+ if (Array.isArray(value)) {
141
+ return value.map(item => applySchemaToObject(item, schema))
142
+ }
143
+ return applySchemaToObject(value, schema)
144
+ }
145
+
146
+ /**
147
+ * Apply schemas to content.data
148
+ * Only processes tags that have a matching schema, leaves others untouched
149
+ *
150
+ * @param {Object} data - The data object from content
151
+ * @param {Object} schemas - Schema definitions from runtime meta
152
+ * @returns {Object} Data with schemas applied
153
+ */
154
+ export function applySchemas(data, schemas) {
155
+ if (!schemas || !data || typeof data !== 'object') {
156
+ return data || {}
157
+ }
158
+
159
+ const result = { ...data }
160
+
161
+ for (const [tag, rawValue] of Object.entries(data)) {
162
+ const schema = schemas[tag]
163
+ if (!schema) continue // No schema for this tag - leave as-is
164
+
165
+ result[tag] = applySchemaToValue(rawValue, schema)
166
+ }
167
+
168
+ return result
169
+ }
170
+
83
171
  /**
84
172
  * Apply param defaults from runtime schema
85
173
  *
@@ -98,6 +186,38 @@ export function applyDefaults(params, defaults) {
98
186
  }
99
187
  }
100
188
 
189
+ /**
190
+ * Apply cascaded data based on component's inheritData setting
191
+ *
192
+ * @param {Object} localData - content.data from the section itself
193
+ * @param {Object} cascadedData - Data from page/site level fetches
194
+ * @param {boolean|Array} inheritData - Component's inheritData setting
195
+ * @returns {Object} Merged data object
196
+ */
197
+ function applyCascadedData(localData, cascadedData, inheritData) {
198
+ if (!inheritData || !cascadedData || Object.keys(cascadedData).length === 0) {
199
+ return localData
200
+ }
201
+
202
+ if (inheritData === true) {
203
+ // Inherit all: cascaded data as base, local data overrides
204
+ return { ...cascadedData, ...localData }
205
+ }
206
+
207
+ if (Array.isArray(inheritData)) {
208
+ // Selective: only specified schemas, local data takes precedence
209
+ const result = { ...localData }
210
+ for (const key of inheritData) {
211
+ if (cascadedData[key] !== undefined && result[key] === undefined) {
212
+ result[key] = cascadedData[key]
213
+ }
214
+ }
215
+ return result
216
+ }
217
+
218
+ return localData
219
+ }
220
+
101
221
  /**
102
222
  * Prepare props for a component with runtime guarantees
103
223
  *
@@ -113,6 +233,19 @@ export function prepareProps(block, meta) {
113
233
  // Guarantee content structure
114
234
  const content = guaranteeContentStructure(block.parsedContent)
115
235
 
236
+ // Apply cascaded data based on component's inheritData setting
237
+ const inheritData = meta?.inheritData
238
+ const cascadedData = block.cascadedData || {}
239
+ if (inheritData) {
240
+ content.data = applyCascadedData(content.data, cascadedData, inheritData)
241
+ }
242
+
243
+ // Apply schemas to content.data
244
+ const schemas = meta?.schemas || null
245
+ if (schemas && content.data) {
246
+ content.data = applySchemas(content.data, schemas)
247
+ }
248
+
116
249
  return { content, params }
117
250
  }
118
251
 
package/src/ssr.js ADDED
@@ -0,0 +1,49 @@
1
+ /**
2
+ * @uniweb/runtime/ssr - Server-Side Rendering Entry Point
3
+ *
4
+ * Node.js-compatible exports for SSG/prerendering.
5
+ * This module is built to a standalone bundle that can be imported
6
+ * directly by Node.js without Vite transpilation.
7
+ *
8
+ * Usage in prerender.js:
9
+ * import { renderPage, Blocks, BlockRenderer } from '@uniweb/runtime/ssr'
10
+ */
11
+
12
+ import React from 'react'
13
+
14
+ // Props preparation (no browser APIs)
15
+ export {
16
+ prepareProps,
17
+ applySchemas,
18
+ applyDefaults,
19
+ guaranteeContentStructure,
20
+ getComponentMeta,
21
+ getComponentDefaults
22
+ } from './prepare-props.js'
23
+
24
+ // Components for rendering
25
+ export { default as BlockRenderer } from './components/BlockRenderer.jsx'
26
+ export { default as Blocks } from './components/Blocks.jsx'
27
+ export { default as Layout } from './components/Layout.jsx'
28
+
29
+ // Re-export Layout's DefaultLayout for direct use
30
+ import LayoutComponent from './components/Layout.jsx'
31
+
32
+ /**
33
+ * Render a page to React elements
34
+ *
35
+ * This is the main entry point for SSG. It returns a React element
36
+ * that can be passed to renderToString().
37
+ *
38
+ * @param {Object} props
39
+ * @param {Page} props.page - The page instance to render
40
+ * @param {Website} props.website - The website instance
41
+ * @returns {React.ReactElement}
42
+ */
43
+ export function PageElement({ page, website }) {
44
+ return React.createElement(
45
+ 'main',
46
+ null,
47
+ React.createElement(LayoutComponent, { page, website })
48
+ )
49
+ }