@uniweb/runtime 0.6.8 → 0.6.10

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
@@ -28,7 +28,6 @@ function guaranteeContentStructure(parsedContent) {
28
28
  title: content.title || "",
29
29
  pretitle: content.pretitle || "",
30
30
  subtitle: content.subtitle || "",
31
- subtitle2: content.subtitle2 || "",
32
31
  alignment: content.alignment || null,
33
32
  // Flat body fields
34
33
  paragraphs: content.paragraphs || [],
package/dist/ssr.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"ssr.js","sources":["../src/prepare-props.js","../src/components/Background.jsx","../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 images: item.images || [],\n lists: item.lists || [],\n icons: item.icons || [],\n videos: item.videos || [],\n snippets: item.snippets || [],\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 images: content.images || [],\n lists: content.lists || [],\n icons: content.icons || [],\n videos: content.videos || [],\n insets: content.insets || [],\n snippets: content.snippets || [],\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 * 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 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 * Background\n *\n * Renders section backgrounds (color, gradient, image, video) with optional overlay.\n * Positioned absolutely behind content with proper z-index stacking.\n *\n * @module @uniweb/runtime/components/Background\n */\n\nimport React from 'react'\n\n/**\n * Background modes\n */\nconst MODES = {\n COLOR: 'color',\n GRADIENT: 'gradient',\n IMAGE: 'image',\n VIDEO: 'video',\n}\n\n/**\n * Default overlay colors\n */\nconst OVERLAY_COLORS = {\n light: 'rgba(255, 255, 255, 0.5)',\n dark: 'rgba(0, 0, 0, 0.5)',\n}\n\n/**\n * Resolve a URL against the site's base path\n * Prepends basePath to absolute URLs (starting with /) so they work\n * under subdirectory deployments (e.g., /templates/international/)\n */\nfunction resolveUrl(url) {\n if (!url || !url.startsWith('/')) return url\n const basePath = globalThis.uniweb?.activeWebsite?.basePath || ''\n if (!basePath) return url\n // Avoid double-prepending\n if (url.startsWith(basePath + '/') || url === basePath) return url\n return basePath + url\n}\n\n/**\n * Render gradient overlay\n */\nfunction GradientOverlay({ gradient, opacity = 0.5 }) {\n const {\n start = 'rgba(0,0,0,0.7)',\n end = 'rgba(0,0,0,0)',\n angle = 180,\n startPosition = 0,\n endPosition = 100,\n } = gradient\n\n const style = {\n position: 'absolute',\n inset: 0,\n background: `linear-gradient(${angle}deg, ${start} ${startPosition}%, ${end} ${endPosition}%)`,\n opacity,\n pointerEvents: 'none',\n }\n\n return <div className=\"background-overlay background-overlay--gradient\" style={style} aria-hidden=\"true\" />\n}\n\n/**\n * Render solid overlay\n */\nfunction SolidOverlay({ type = 'dark', opacity = 0.5 }) {\n const baseColor = type === 'light' ? '255, 255, 255' : '0, 0, 0'\n\n const style = {\n position: 'absolute',\n inset: 0,\n backgroundColor: `rgba(${baseColor}, ${opacity})`,\n pointerEvents: 'none',\n }\n\n return <div className=\"background-overlay background-overlay--solid\" style={style} aria-hidden=\"true\" />\n}\n\n/**\n * Render overlay (gradient or solid)\n */\nfunction Overlay({ overlay }) {\n if (!overlay?.enabled) return null\n\n if (overlay.gradient) {\n return <GradientOverlay gradient={overlay.gradient} opacity={overlay.opacity} />\n }\n\n return <SolidOverlay type={overlay.type} opacity={overlay.opacity} />\n}\n\n/**\n * Color background\n */\nfunction ColorBackground({ color }) {\n if (!color) return null\n\n const style = {\n position: 'absolute',\n inset: 0,\n backgroundColor: color,\n }\n\n return <div className=\"background-color\" style={style} aria-hidden=\"true\" />\n}\n\n/**\n * Gradient background\n */\nfunction GradientBackground({ gradient }) {\n if (!gradient) return null\n\n // Raw CSS gradient string (e.g., \"linear-gradient(to bottom, #000, #333)\")\n if (typeof gradient === 'string') {\n const style = { position: 'absolute', inset: 0, background: gradient }\n return <div className=\"background-gradient\" style={style} aria-hidden=\"true\" />\n }\n\n const {\n start = 'transparent',\n end = 'transparent',\n angle = 0,\n startPosition = 0,\n endPosition = 100,\n startOpacity = 1,\n endOpacity = 1,\n } = gradient\n\n // Convert colors to rgba if opacity is specified\n const startColor = startOpacity < 1 ? withOpacity(start, startOpacity) : start\n const endColor = endOpacity < 1 ? withOpacity(end, endOpacity) : end\n\n const style = {\n position: 'absolute',\n inset: 0,\n background: `linear-gradient(${angle}deg, ${startColor} ${startPosition}%, ${endColor} ${endPosition}%)`,\n }\n\n return <div className=\"background-gradient\" style={style} aria-hidden=\"true\" />\n}\n\n/**\n * Convert hex color to rgba with opacity\n */\nfunction withOpacity(color, opacity) {\n // Handle hex colors\n if (color.startsWith('#')) {\n const r = parseInt(color.slice(1, 3), 16)\n const g = parseInt(color.slice(3, 5), 16)\n const b = parseInt(color.slice(5, 7), 16)\n return `rgba(${r}, ${g}, ${b}, ${opacity})`\n }\n // Handle rgb/rgba\n if (color.startsWith('rgb')) {\n const match = color.match(/rgba?\\((\\d+),\\s*(\\d+),\\s*(\\d+)/)\n if (match) {\n return `rgba(${match[1]}, ${match[2]}, ${match[3]}, ${opacity})`\n }\n }\n // Fallback - return as is\n return color\n}\n\n/**\n * Image background\n */\nfunction ImageBackground({ image }) {\n if (!image?.src) return null\n\n const {\n src,\n position = 'center',\n size = 'cover',\n lazy = true,\n } = image\n\n const style = {\n position: 'absolute',\n inset: 0,\n backgroundImage: `url(${resolveUrl(src)})`,\n backgroundPosition: position,\n backgroundSize: size,\n backgroundRepeat: 'no-repeat',\n }\n\n // For lazy loading, we could use an img tag with loading=\"lazy\"\n // But for backgrounds, CSS is more appropriate\n // The lazy prop could be used for future intersection observer optimization\n\n return <div className=\"background-image\" style={style} aria-hidden=\"true\" />\n}\n\n/**\n * Check if user prefers reduced motion\n */\nfunction prefersReducedMotion() {\n if (typeof window === 'undefined') return false\n return window.matchMedia('(prefers-reduced-motion: reduce)').matches\n}\n\n/**\n * Video background\n *\n * Supports multiple source formats with automatic fallback.\n * Respects prefers-reduced-motion by showing poster image instead.\n */\nfunction VideoBackground({ video }) {\n if (!video?.src) return null\n\n const {\n src,\n sources, // Array of { src, type } for multiple formats\n poster,\n loop = true,\n muted = true,\n } = video\n\n // Respect reduced motion preference - show poster image instead\n if (prefersReducedMotion() && poster) {\n return <ImageBackground image={{ src: poster, size: 'cover', position: 'center' }} />\n }\n\n const style = {\n position: 'absolute',\n inset: 0,\n width: '100%',\n height: '100%',\n objectFit: 'cover',\n }\n\n // Build source list: explicit sources array, or infer from src\n const sourceList = (sources || inferSources(src)).map(s => ({\n ...s,\n src: resolveUrl(s.src)\n }))\n\n return (\n <video\n className=\"background-video\"\n style={style}\n autoPlay\n loop={loop}\n muted={muted}\n playsInline\n poster={resolveUrl(poster)}\n aria-hidden=\"true\"\n >\n {sourceList.map(({ src: sourceSrc, type }, index) => (\n <source key={index} src={sourceSrc} type={type} />\n ))}\n </video>\n )\n}\n\n/**\n * Infer multiple source formats from a single src\n *\n * If given \"video.mp4\", also tries \"video.webm\" (better compression)\n * Browser will use first supported format\n */\nfunction inferSources(src) {\n const sources = []\n const ext = src.split('.').pop()?.toLowerCase()\n const basePath = src.slice(0, src.lastIndexOf('.'))\n\n // Prefer webm (better compression), fall back to original\n if (ext === 'mp4') {\n sources.push({ src: `${basePath}.webm`, type: 'video/webm' })\n sources.push({ src, type: 'video/mp4' })\n } else if (ext === 'webm') {\n sources.push({ src, type: 'video/webm' })\n sources.push({ src: `${basePath}.mp4`, type: 'video/mp4' })\n } else {\n // Single source for other formats\n sources.push({ src, type: getVideoMimeType(src) })\n }\n\n return sources\n}\n\n/**\n * Get video MIME type from URL\n */\nfunction getVideoMimeType(src) {\n if (src.endsWith('.webm')) return 'video/webm'\n if (src.endsWith('.ogg') || src.endsWith('.ogv')) return 'video/ogg'\n return 'video/mp4'\n}\n\n/**\n * Background component\n *\n * @param {Object} props\n * @param {string} props.mode - Background mode: 'color', 'gradient', 'image', 'video'\n * @param {string} props.color - Color value (for color mode)\n * @param {Object} props.gradient - Gradient configuration\n * @param {Object} props.image - Image configuration\n * @param {Object} props.video - Video configuration\n * @param {Object} props.overlay - Overlay configuration\n * @param {string} props.className - Additional CSS class\n */\nexport default function Background({\n mode,\n color,\n gradient,\n image,\n video,\n overlay,\n className = '',\n}) {\n // No background to render\n if (!mode) return null\n\n const containerStyle = {\n position: 'absolute',\n inset: 0,\n overflow: 'hidden',\n zIndex: 0,\n }\n\n return (\n <div\n className={`background background--${mode} ${className}`.trim()}\n style={containerStyle}\n aria-hidden=\"true\"\n >\n {/* Render background based on mode */}\n {mode === MODES.COLOR && <ColorBackground color={color} />}\n {mode === MODES.GRADIENT && <GradientBackground gradient={gradient} />}\n {mode === MODES.IMAGE && <ImageBackground image={image} />}\n {mode === MODES.VIDEO && <VideoBackground video={video} />}\n\n {/* Overlay on top of background */}\n <Overlay overlay={overlay} />\n </div>\n )\n}\n\n/**\n * Export background modes for external use\n */\nexport { MODES as BackgroundModes }\n","/**\n * BlockRenderer\n *\n * Bridges Block data to foundation components.\n * Handles theming, wrapper props, and runtime guarantees.\n * Uses EntityStore for entity-aware data resolution.\n */\n\nimport React, { useState, useEffect } from 'react'\nimport { prepareProps, getComponentMeta } from '../prepare-props.js'\nimport Background from './Background.jsx'\n\n/**\n * Valid color contexts\n */\nconst VALID_CONTEXTS = ['light', 'medium', 'dark']\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 // Build context class (context-light, context-medium, context-dark)\n let contextClass = ''\n if (theme && VALID_CONTEXTS.includes(theme)) {\n contextClass = `context-${theme}`\n }\n\n let className = contextClass\n if (blockClassName) {\n className = className ? `${className} ${blockClassName}` : blockClassName\n }\n\n const { background = {} } = block.standardOptions\n const style = {}\n\n // If background has content, ensure relative positioning for z-index stacking\n if (background.mode) {\n style.position = 'relative'\n }\n\n // Apply context overrides as inline CSS custom properties.\n // These override the context class tokens for this specific section.\n if (block.contextOverrides) {\n for (const [key, value] of Object.entries(block.contextOverrides)) {\n style[`--${key}`] = value\n }\n }\n\n // Use stableId for DOM ID if available (stable across reordering)\n // Falls back to positional id for backwards compatibility\n const sectionId = block.stableId || block.id\n\n return {\n id: `section-${sectionId}`,\n style,\n className,\n background\n }\n}\n\n/**\n * BlockRenderer component\n *\n * @param {Object} props\n * @param {Block} props.block - Block instance to render\n * @param {boolean} props.pure - If true, render component without wrapper\n * @param {string|false} props.as - Element type to render as ('section', 'div', 'article', etc.) or false for Fragment\n * @param {Object} props.extra - Extra props to pass to the component\n */\nexport default function BlockRenderer({ block, pure = false, as = 'section', extra = {} }) {\n const Component = block.initComponent()\n\n // Entity-aware data resolution via EntityStore\n const entityStore = block.website.entityStore\n const meta = getComponentMeta(block.type)\n const resolved = entityStore.resolve(block, meta)\n\n // Async data for when resolve returns 'pending' (runtime fetches)\n const [asyncData, setAsyncData] = useState(null)\n\n // Reset async data when block changes (SPA navigation)\n useEffect(() => {\n setAsyncData(null)\n }, [block])\n\n // Fetch missing data asynchronously\n useEffect(() => {\n if (resolved.status !== 'pending') return\n\n let cancelled = false\n entityStore.fetch(block, meta).then((result) => {\n if (!cancelled && result.data) setAsyncData(result.data)\n })\n return () => { cancelled = true }\n }, [block])\n\n // Use sync resolved data when available, fall back to async\n const entityData = resolved.status === 'ready' ? resolved.data : asyncData\n\n // Signal to component that data is loading\n block.dataLoading = resolved.status === 'pending' && !entityData\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 - semantic parser output (flat: title, paragraphs, links, etc.)\n // 2. block.properties - params from frontmatter (theme, alignment, etc.)\n // 3. meta - defaults from component meta.js\n const prepared = prepareProps(block, meta)\n let params = prepared.params\n\n let content = {\n ...prepared.content,\n ...block.properties, // Frontmatter params overlay (legacy support)\n }\n\n // Merge entity data resolved by EntityStore\n // Only fill in keys that don't already exist — section-level fetch data\n // (already in content.data from parsedContent) takes priority over inherited data.\n if (entityData) {\n const merged = { ...content.data }\n for (const key of Object.keys(entityData)) {\n if (merged[key] === undefined) {\n merged[key] = entityData[key]\n }\n }\n content.data = merged\n }\n\n const { background, ...wrapperProps } = getWrapperProps(block)\n\n // Merge Component.className (static classes declared on the component function)\n // Order: context-{theme} + block.state.className + Component.className\n const componentClassName = Component.className\n if (componentClassName) {\n wrapperProps.className = wrapperProps.className\n ? `${wrapperProps.className} ${componentClassName}`\n : componentClassName\n }\n\n // Check if component handles its own background (background: 'self' in meta.js)\n // Components that render their own background layer (solid colors, insets, effects)\n // opt out so the runtime doesn't render an occluded layer underneath.\n const hasBackground = background?.mode && meta?.background !== 'self'\n\n // Signal to the component that the engine is rendering a background.\n // Components check block.hasBackground to skip their own opaque bg\n // and let the engine background show through.\n block.hasBackground = hasBackground\n\n const componentProps = {\n content,\n params,\n block\n }\n\n if (pure) {\n return <Component {...componentProps} extra={extra} />\n }\n\n // Determine wrapper element:\n // - as={false} → Fragment (no wrapper)\n // - as prop explicitly set (not default 'section') → use as prop\n // - Component.as → use component's declared tag\n // - fallback → 'section'\n const componentAs = Component.as\n const Wrapper = as === false ? React.Fragment : (as !== 'section' ? as : componentAs || 'section')\n // Fragment doesn't accept props, so only pass them for real elements\n const wrapperElementProps = as === false ? {} : wrapperProps\n\n // Render with or without background\n if (hasBackground) {\n return (\n <Wrapper {...wrapperElementProps}>\n {/* Background layer (positioned absolutely) */}\n <Background\n mode={background.mode}\n color={background.color}\n gradient={background.gradient}\n image={background.image}\n video={background.video}\n overlay={background.overlay}\n />\n\n {/* Content layer (above background) */}\n <div className=\"relative z-10\">\n <Component {...componentProps} />\n </div>\n </Wrapper>\n )\n }\n\n // No background - simpler render without extra wrapper\n return (\n <Wrapper {...wrapperElementProps}>\n <Component {...componentProps} />\n </Wrapper>\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, and\n * any custom areas defined in the layout directory).\n * Supports foundation-provided custom Layout components via website.getRemoteLayout().\n *\n * Layout Areas:\n * Areas are general — any name works. Common conventions:\n * - header: Top navigation, branding (from layout/header.md)\n * - body: Main page content (from page sections)\n * - footer: Bottom navigation, copyright (from layout/footer.md)\n * - left: Left sidebar/panel (from layout/left.md)\n * - right: Right sidebar/panel (from layout/right.md)\n *\n * Custom Layouts:\n * Foundations provide custom layouts via src/layouts/:\n *\n * ```\n * src/layouts/\n * ├── DocsLayout/\n * │ ├── index.jsx\n * │ └── meta.js\n * └── MarketingLayout.jsx\n * ```\n *\n * The Layout component receives pre-rendered areas as props:\n * - page, website: Runtime context\n * - params: Layout params (merged with meta.js defaults)\n * - body: Pre-rendered body React element\n * - header, footer, left, right, ...: Pre-rendered area React elements\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 && <header>{header}</header>}\n {body && <main>{body}</main>}\n {footer && <footer>{footer}</footer>}\n </>\n )\n}\n\n/**\n * Initialize all blocks to ensure cross-block communication works.\n * Must be called before rendering so getNextBlockInfo() can access sibling contexts.\n *\n * @param {Block[][]} blockGroups - Arrays of blocks from all layout areas\n */\nfunction initializeAllBlocks(...blockGroups) {\n for (const blocks of blockGroups) {\n if (!blocks) continue\n for (const block of blocks) {\n block.initComponent()\n }\n }\n}\n\n/**\n * Merge page-level layout params with meta.js defaults\n */\nfunction mergeParams(pageParams = {}, defaults = {}) {\n return { ...defaults, ...pageParams }\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 const layoutName = page.getLayoutName()\n const RemoteLayout = website.getRemoteLayout(layoutName)\n const layoutMeta = website.getLayoutMeta(layoutName)\n\n const bodyBlocks = page.getBodyBlocks()\n const areas = page.getLayoutAreas()\n\n // Pre-initialize all blocks before rendering any.\n // This ensures cross-block communication (getNextBlockInfo, getPrevBlockInfo)\n // can access sibling block contexts that are set in initComponent().\n const allBlockGroups = [bodyBlocks, ...Object.values(areas)]\n initializeAllBlocks(...allBlockGroups)\n\n // Pre-render each area as React elements\n const bodyElement = bodyBlocks ? <Blocks blocks={bodyBlocks} /> : null\n const areaElements = {}\n for (const [name, blocks] of Object.entries(areas)) {\n areaElements[name] = <Blocks blocks={blocks} />\n }\n\n // Use foundation's custom Layout if provided\n if (RemoteLayout) {\n const params = mergeParams(page.getLayoutParams(), layoutMeta?.defaults)\n\n return (\n <RemoteLayout\n key={layoutName}\n page={page}\n website={website}\n params={params}\n body={bodyElement}\n {...areaElements}\n />\n )\n }\n\n // Default layout\n return (\n <DefaultLayout\n body={bodyElement}\n {...areaElements}\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":["style","LayoutComponent"],"mappings":"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"}
1
+ {"version":3,"file":"ssr.js","sources":["../src/prepare-props.js","../src/components/Background.jsx","../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 images: item.images || [],\n lists: item.lists || [],\n icons: item.icons || [],\n videos: item.videos || [],\n snippets: item.snippets || [],\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 alignment: content.alignment || null,\n\n // Flat body fields\n paragraphs: content.paragraphs || [],\n links: content.links || [],\n images: content.images || [],\n lists: content.lists || [],\n icons: content.icons || [],\n videos: content.videos || [],\n insets: content.insets || [],\n snippets: content.snippets || [],\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 * 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 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 * Background\n *\n * Renders section backgrounds (color, gradient, image, video) with optional overlay.\n * Positioned absolutely behind content with proper z-index stacking.\n *\n * @module @uniweb/runtime/components/Background\n */\n\nimport React from 'react'\n\n/**\n * Background modes\n */\nconst MODES = {\n COLOR: 'color',\n GRADIENT: 'gradient',\n IMAGE: 'image',\n VIDEO: 'video',\n}\n\n/**\n * Default overlay colors\n */\nconst OVERLAY_COLORS = {\n light: 'rgba(255, 255, 255, 0.5)',\n dark: 'rgba(0, 0, 0, 0.5)',\n}\n\n/**\n * Resolve a URL against the site's base path\n * Prepends basePath to absolute URLs (starting with /) so they work\n * under subdirectory deployments (e.g., /templates/international/)\n */\nfunction resolveUrl(url) {\n if (!url || !url.startsWith('/')) return url\n const basePath = globalThis.uniweb?.activeWebsite?.basePath || ''\n if (!basePath) return url\n // Avoid double-prepending\n if (url.startsWith(basePath + '/') || url === basePath) return url\n return basePath + url\n}\n\n/**\n * Render gradient overlay\n */\nfunction GradientOverlay({ gradient, opacity = 0.5 }) {\n const {\n start = 'rgba(0,0,0,0.7)',\n end = 'rgba(0,0,0,0)',\n angle = 180,\n startPosition = 0,\n endPosition = 100,\n } = gradient\n\n const style = {\n position: 'absolute',\n inset: 0,\n background: `linear-gradient(${angle}deg, ${start} ${startPosition}%, ${end} ${endPosition}%)`,\n opacity,\n pointerEvents: 'none',\n }\n\n return <div className=\"background-overlay background-overlay--gradient\" style={style} aria-hidden=\"true\" />\n}\n\n/**\n * Render solid overlay\n */\nfunction SolidOverlay({ type = 'dark', opacity = 0.5 }) {\n const baseColor = type === 'light' ? '255, 255, 255' : '0, 0, 0'\n\n const style = {\n position: 'absolute',\n inset: 0,\n backgroundColor: `rgba(${baseColor}, ${opacity})`,\n pointerEvents: 'none',\n }\n\n return <div className=\"background-overlay background-overlay--solid\" style={style} aria-hidden=\"true\" />\n}\n\n/**\n * Render overlay (gradient or solid)\n */\nfunction Overlay({ overlay }) {\n if (!overlay?.enabled) return null\n\n if (overlay.gradient) {\n return <GradientOverlay gradient={overlay.gradient} opacity={overlay.opacity} />\n }\n\n return <SolidOverlay type={overlay.type} opacity={overlay.opacity} />\n}\n\n/**\n * Color background\n */\nfunction ColorBackground({ color }) {\n if (!color) return null\n\n const style = {\n position: 'absolute',\n inset: 0,\n backgroundColor: color,\n }\n\n return <div className=\"background-color\" style={style} aria-hidden=\"true\" />\n}\n\n/**\n * Gradient background\n */\nfunction GradientBackground({ gradient }) {\n if (!gradient) return null\n\n // Raw CSS gradient string (e.g., \"linear-gradient(to bottom, #000, #333)\")\n if (typeof gradient === 'string') {\n const style = { position: 'absolute', inset: 0, background: gradient }\n return <div className=\"background-gradient\" style={style} aria-hidden=\"true\" />\n }\n\n const {\n start = 'transparent',\n end = 'transparent',\n angle = 0,\n startPosition = 0,\n endPosition = 100,\n startOpacity = 1,\n endOpacity = 1,\n } = gradient\n\n // Convert colors to rgba if opacity is specified\n const startColor = startOpacity < 1 ? withOpacity(start, startOpacity) : start\n const endColor = endOpacity < 1 ? withOpacity(end, endOpacity) : end\n\n const style = {\n position: 'absolute',\n inset: 0,\n background: `linear-gradient(${angle}deg, ${startColor} ${startPosition}%, ${endColor} ${endPosition}%)`,\n }\n\n return <div className=\"background-gradient\" style={style} aria-hidden=\"true\" />\n}\n\n/**\n * Convert hex color to rgba with opacity\n */\nfunction withOpacity(color, opacity) {\n // Handle hex colors\n if (color.startsWith('#')) {\n const r = parseInt(color.slice(1, 3), 16)\n const g = parseInt(color.slice(3, 5), 16)\n const b = parseInt(color.slice(5, 7), 16)\n return `rgba(${r}, ${g}, ${b}, ${opacity})`\n }\n // Handle rgb/rgba\n if (color.startsWith('rgb')) {\n const match = color.match(/rgba?\\((\\d+),\\s*(\\d+),\\s*(\\d+)/)\n if (match) {\n return `rgba(${match[1]}, ${match[2]}, ${match[3]}, ${opacity})`\n }\n }\n // Fallback - return as is\n return color\n}\n\n/**\n * Image background\n */\nfunction ImageBackground({ image }) {\n if (!image?.src) return null\n\n const {\n src,\n position = 'center',\n size = 'cover',\n lazy = true,\n } = image\n\n const style = {\n position: 'absolute',\n inset: 0,\n backgroundImage: `url(${resolveUrl(src)})`,\n backgroundPosition: position,\n backgroundSize: size,\n backgroundRepeat: 'no-repeat',\n }\n\n // For lazy loading, we could use an img tag with loading=\"lazy\"\n // But for backgrounds, CSS is more appropriate\n // The lazy prop could be used for future intersection observer optimization\n\n return <div className=\"background-image\" style={style} aria-hidden=\"true\" />\n}\n\n/**\n * Check if user prefers reduced motion\n */\nfunction prefersReducedMotion() {\n if (typeof window === 'undefined') return false\n return window.matchMedia('(prefers-reduced-motion: reduce)').matches\n}\n\n/**\n * Video background\n *\n * Supports multiple source formats with automatic fallback.\n * Respects prefers-reduced-motion by showing poster image instead.\n */\nfunction VideoBackground({ video }) {\n if (!video?.src) return null\n\n const {\n src,\n sources, // Array of { src, type } for multiple formats\n poster,\n loop = true,\n muted = true,\n } = video\n\n // Respect reduced motion preference - show poster image instead\n if (prefersReducedMotion() && poster) {\n return <ImageBackground image={{ src: poster, size: 'cover', position: 'center' }} />\n }\n\n const style = {\n position: 'absolute',\n inset: 0,\n width: '100%',\n height: '100%',\n objectFit: 'cover',\n }\n\n // Build source list: explicit sources array, or infer from src\n const sourceList = (sources || inferSources(src)).map(s => ({\n ...s,\n src: resolveUrl(s.src)\n }))\n\n return (\n <video\n className=\"background-video\"\n style={style}\n autoPlay\n loop={loop}\n muted={muted}\n playsInline\n poster={resolveUrl(poster)}\n aria-hidden=\"true\"\n >\n {sourceList.map(({ src: sourceSrc, type }, index) => (\n <source key={index} src={sourceSrc} type={type} />\n ))}\n </video>\n )\n}\n\n/**\n * Infer multiple source formats from a single src\n *\n * If given \"video.mp4\", also tries \"video.webm\" (better compression)\n * Browser will use first supported format\n */\nfunction inferSources(src) {\n const sources = []\n const ext = src.split('.').pop()?.toLowerCase()\n const basePath = src.slice(0, src.lastIndexOf('.'))\n\n // Prefer webm (better compression), fall back to original\n if (ext === 'mp4') {\n sources.push({ src: `${basePath}.webm`, type: 'video/webm' })\n sources.push({ src, type: 'video/mp4' })\n } else if (ext === 'webm') {\n sources.push({ src, type: 'video/webm' })\n sources.push({ src: `${basePath}.mp4`, type: 'video/mp4' })\n } else {\n // Single source for other formats\n sources.push({ src, type: getVideoMimeType(src) })\n }\n\n return sources\n}\n\n/**\n * Get video MIME type from URL\n */\nfunction getVideoMimeType(src) {\n if (src.endsWith('.webm')) return 'video/webm'\n if (src.endsWith('.ogg') || src.endsWith('.ogv')) return 'video/ogg'\n return 'video/mp4'\n}\n\n/**\n * Background component\n *\n * @param {Object} props\n * @param {string} props.mode - Background mode: 'color', 'gradient', 'image', 'video'\n * @param {string} props.color - Color value (for color mode)\n * @param {Object} props.gradient - Gradient configuration\n * @param {Object} props.image - Image configuration\n * @param {Object} props.video - Video configuration\n * @param {Object} props.overlay - Overlay configuration\n * @param {string} props.className - Additional CSS class\n */\nexport default function Background({\n mode,\n color,\n gradient,\n image,\n video,\n overlay,\n className = '',\n}) {\n // No background to render\n if (!mode) return null\n\n const containerStyle = {\n position: 'absolute',\n inset: 0,\n overflow: 'hidden',\n zIndex: 0,\n }\n\n return (\n <div\n className={`background background--${mode} ${className}`.trim()}\n style={containerStyle}\n aria-hidden=\"true\"\n >\n {/* Render background based on mode */}\n {mode === MODES.COLOR && <ColorBackground color={color} />}\n {mode === MODES.GRADIENT && <GradientBackground gradient={gradient} />}\n {mode === MODES.IMAGE && <ImageBackground image={image} />}\n {mode === MODES.VIDEO && <VideoBackground video={video} />}\n\n {/* Overlay on top of background */}\n <Overlay overlay={overlay} />\n </div>\n )\n}\n\n/**\n * Export background modes for external use\n */\nexport { MODES as BackgroundModes }\n","/**\n * BlockRenderer\n *\n * Bridges Block data to foundation components.\n * Handles theming, wrapper props, and runtime guarantees.\n * Uses EntityStore for entity-aware data resolution.\n */\n\nimport React, { useState, useEffect } from 'react'\nimport { prepareProps, getComponentMeta } from '../prepare-props.js'\nimport Background from './Background.jsx'\n\n/**\n * Valid color contexts\n */\nconst VALID_CONTEXTS = ['light', 'medium', 'dark']\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 // Build context class (context-light, context-medium, context-dark)\n let contextClass = ''\n if (theme && VALID_CONTEXTS.includes(theme)) {\n contextClass = `context-${theme}`\n }\n\n let className = contextClass\n if (blockClassName) {\n className = className ? `${className} ${blockClassName}` : blockClassName\n }\n\n const { background = {} } = block.standardOptions\n const style = {}\n\n // If background has content, ensure relative positioning for z-index stacking\n if (background.mode) {\n style.position = 'relative'\n }\n\n // Apply context overrides as inline CSS custom properties.\n // These override the context class tokens for this specific section.\n if (block.contextOverrides) {\n for (const [key, value] of Object.entries(block.contextOverrides)) {\n style[`--${key}`] = value\n }\n }\n\n // Use stableId for DOM ID if available (stable across reordering)\n // Falls back to positional id for backwards compatibility\n const sectionId = block.stableId || block.id\n\n return {\n id: `section-${sectionId}`,\n style,\n className,\n background\n }\n}\n\n/**\n * BlockRenderer component\n *\n * @param {Object} props\n * @param {Block} props.block - Block instance to render\n * @param {boolean} props.pure - If true, render component without wrapper\n * @param {string|false} props.as - Element type to render as ('section', 'div', 'article', etc.) or false for Fragment\n * @param {Object} props.extra - Extra props to pass to the component\n */\nexport default function BlockRenderer({ block, pure = false, as = 'section', extra = {} }) {\n const Component = block.initComponent()\n\n // Entity-aware data resolution via EntityStore\n const entityStore = block.website.entityStore\n const meta = getComponentMeta(block.type)\n const resolved = entityStore.resolve(block, meta)\n\n // Async data for when resolve returns 'pending' (runtime fetches)\n const [asyncData, setAsyncData] = useState(null)\n\n // Reset async data when block changes (SPA navigation)\n useEffect(() => {\n setAsyncData(null)\n }, [block])\n\n // Fetch missing data asynchronously\n useEffect(() => {\n if (resolved.status !== 'pending') return\n\n let cancelled = false\n entityStore.fetch(block, meta).then((result) => {\n if (!cancelled && result.data) setAsyncData(result.data)\n })\n return () => { cancelled = true }\n }, [block])\n\n // Use sync resolved data when available, fall back to async\n const entityData = resolved.status === 'ready' ? resolved.data : asyncData\n\n // Signal to component that data is loading\n block.dataLoading = resolved.status === 'pending' && !entityData\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 - semantic parser output (flat: title, paragraphs, links, etc.)\n // 2. block.properties - params from frontmatter (theme, alignment, etc.)\n // 3. meta - defaults from component meta.js\n const prepared = prepareProps(block, meta)\n let params = prepared.params\n\n let content = {\n ...prepared.content,\n ...block.properties, // Frontmatter params overlay (legacy support)\n }\n\n // Merge entity data resolved by EntityStore\n // Only fill in keys that don't already exist — section-level fetch data\n // (already in content.data from parsedContent) takes priority over inherited data.\n if (entityData) {\n const merged = { ...content.data }\n for (const key of Object.keys(entityData)) {\n if (merged[key] === undefined) {\n merged[key] = entityData[key]\n }\n }\n content.data = merged\n }\n\n const { background, ...wrapperProps } = getWrapperProps(block)\n\n // Merge Component.className (static classes declared on the component function)\n // Order: context-{theme} + block.state.className + Component.className\n const componentClassName = Component.className\n if (componentClassName) {\n wrapperProps.className = wrapperProps.className\n ? `${wrapperProps.className} ${componentClassName}`\n : componentClassName\n }\n\n // Check if component handles its own background (background: 'self' in meta.js)\n // Components that render their own background layer (solid colors, insets, effects)\n // opt out so the runtime doesn't render an occluded layer underneath.\n const hasBackground = background?.mode && meta?.background !== 'self'\n\n // Signal to the component that the engine is rendering a background.\n // Components check block.hasBackground to skip their own opaque bg\n // and let the engine background show through.\n block.hasBackground = hasBackground\n\n const componentProps = {\n content,\n params,\n block\n }\n\n if (pure) {\n return <Component {...componentProps} extra={extra} />\n }\n\n // Determine wrapper element:\n // - as={false} → Fragment (no wrapper)\n // - as prop explicitly set (not default 'section') → use as prop\n // - Component.as → use component's declared tag\n // - fallback → 'section'\n const componentAs = Component.as\n const Wrapper = as === false ? React.Fragment : (as !== 'section' ? as : componentAs || 'section')\n // Fragment doesn't accept props, so only pass them for real elements\n const wrapperElementProps = as === false ? {} : wrapperProps\n\n // Render with or without background\n if (hasBackground) {\n return (\n <Wrapper {...wrapperElementProps}>\n {/* Background layer (positioned absolutely) */}\n <Background\n mode={background.mode}\n color={background.color}\n gradient={background.gradient}\n image={background.image}\n video={background.video}\n overlay={background.overlay}\n />\n\n {/* Content layer (above background) */}\n <div className=\"relative z-10\">\n <Component {...componentProps} />\n </div>\n </Wrapper>\n )\n }\n\n // No background - simpler render without extra wrapper\n return (\n <Wrapper {...wrapperElementProps}>\n <Component {...componentProps} />\n </Wrapper>\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, and\n * any custom areas defined in the layout directory).\n * Supports foundation-provided custom Layout components via website.getRemoteLayout().\n *\n * Layout Areas:\n * Areas are general — any name works. Common conventions:\n * - header: Top navigation, branding (from layout/header.md)\n * - body: Main page content (from page sections)\n * - footer: Bottom navigation, copyright (from layout/footer.md)\n * - left: Left sidebar/panel (from layout/left.md)\n * - right: Right sidebar/panel (from layout/right.md)\n *\n * Custom Layouts:\n * Foundations provide custom layouts via src/layouts/:\n *\n * ```\n * src/layouts/\n * ├── DocsLayout/\n * │ ├── index.jsx\n * │ └── meta.js\n * └── MarketingLayout.jsx\n * ```\n *\n * The Layout component receives pre-rendered areas as props:\n * - page, website: Runtime context\n * - params: Layout params (merged with meta.js defaults)\n * - body: Pre-rendered body React element\n * - header, footer, left, right, ...: Pre-rendered area React elements\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 && <header>{header}</header>}\n {body && <main>{body}</main>}\n {footer && <footer>{footer}</footer>}\n </>\n )\n}\n\n/**\n * Initialize all blocks to ensure cross-block communication works.\n * Must be called before rendering so getNextBlockInfo() can access sibling contexts.\n *\n * @param {Block[][]} blockGroups - Arrays of blocks from all layout areas\n */\nfunction initializeAllBlocks(...blockGroups) {\n for (const blocks of blockGroups) {\n if (!blocks) continue\n for (const block of blocks) {\n block.initComponent()\n }\n }\n}\n\n/**\n * Merge page-level layout params with meta.js defaults\n */\nfunction mergeParams(pageParams = {}, defaults = {}) {\n return { ...defaults, ...pageParams }\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 const layoutName = page.getLayoutName()\n const RemoteLayout = website.getRemoteLayout(layoutName)\n const layoutMeta = website.getLayoutMeta(layoutName)\n\n const bodyBlocks = page.getBodyBlocks()\n const areas = page.getLayoutAreas()\n\n // Pre-initialize all blocks before rendering any.\n // This ensures cross-block communication (getNextBlockInfo, getPrevBlockInfo)\n // can access sibling block contexts that are set in initComponent().\n const allBlockGroups = [bodyBlocks, ...Object.values(areas)]\n initializeAllBlocks(...allBlockGroups)\n\n // Pre-render each area as React elements\n const bodyElement = bodyBlocks ? <Blocks blocks={bodyBlocks} /> : null\n const areaElements = {}\n for (const [name, blocks] of Object.entries(areas)) {\n areaElements[name] = <Blocks blocks={blocks} />\n }\n\n // Use foundation's custom Layout if provided\n if (RemoteLayout) {\n const params = mergeParams(page.getLayoutParams(), layoutMeta?.defaults)\n\n return (\n <RemoteLayout\n key={layoutName}\n page={page}\n website={website}\n params={params}\n body={bodyElement}\n {...areaElements}\n />\n )\n }\n\n // Default layout\n return (\n <DefaultLayout\n body={bodyElement}\n {...areaElements}\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":["style","LayoutComponent"],"mappings":"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"}
package/package.json CHANGED
@@ -1,11 +1,14 @@
1
1
  {
2
2
  "name": "@uniweb/runtime",
3
- "version": "0.6.8",
3
+ "version": "0.6.10",
4
4
  "description": "Minimal runtime for loading Uniweb foundations",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": "./src/index.jsx",
8
- "./ssr": "./dist/ssr.js"
8
+ "./ssr": "./dist/ssr.js",
9
+ "./provider": "./src/RuntimeProvider.jsx",
10
+ "./setup": "./src/setup.js",
11
+ "./foundation-loader": "./src/foundation-loader.js"
9
12
  },
10
13
  "files": [
11
14
  "src",
@@ -31,7 +34,7 @@
31
34
  "node": ">=20.19"
32
35
  },
33
36
  "dependencies": {
34
- "@uniweb/core": "0.5.8"
37
+ "@uniweb/core": "0.5.10"
35
38
  },
36
39
  "devDependencies": {
37
40
  "@vitejs/plugin-react": "^4.5.2",
@@ -0,0 +1,42 @@
1
+ /**
2
+ * RuntimeProvider
3
+ *
4
+ * Encapsulates the full React rendering tree for a Uniweb site:
5
+ * ErrorBoundary → BrowserRouter → Routes → WebsiteRenderer.
6
+ *
7
+ * The Uniweb singleton (globalThis.uniweb) must be set up BEFORE rendering
8
+ * this component. RuntimeProvider reads from the singleton — it does not
9
+ * manage initialization. The singleton is imperative infrastructure that
10
+ * sits underneath React; React is a rendering layer within it.
11
+ *
12
+ * @param {Object} props
13
+ * @param {string} [props.basename] - Router basename for subdirectory deployments
14
+ * @param {boolean} [props.development] - Enable React StrictMode
15
+ */
16
+
17
+ import React from 'react'
18
+ import { BrowserRouter, Routes, Route } from 'react-router-dom'
19
+ import WebsiteRenderer from './components/WebsiteRenderer.jsx'
20
+ import ErrorBoundary from './components/ErrorBoundary.jsx'
21
+
22
+ export default function RuntimeProvider({ basename, development = false }) {
23
+ const website = globalThis.uniweb?.activeWebsite
24
+ if (!website) return null
25
+
26
+ // Set basePath for subdirectory deployments
27
+ if (website.setBasePath) {
28
+ website.setBasePath(basename || '')
29
+ }
30
+
31
+ const app = (
32
+ <ErrorBoundary>
33
+ <BrowserRouter basename={basename}>
34
+ <Routes>
35
+ <Route path="/*" element={<WebsiteRenderer />} />
36
+ </Routes>
37
+ </BrowserRouter>
38
+ </ErrorBoundary>
39
+ )
40
+
41
+ return development ? <React.StrictMode>{app}</React.StrictMode> : app
42
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Foundation and extension loading
3
+ *
4
+ * Handles dynamic import of foundations (primary and extensions)
5
+ * with CSS loading in parallel.
6
+ */
7
+
8
+ /**
9
+ * Load foundation CSS from URL
10
+ * @param {string} url - URL to foundation's CSS file
11
+ */
12
+ async function loadFoundationCSS(url) {
13
+ if (!url) return
14
+
15
+ return new Promise((resolve) => {
16
+ const link = document.createElement('link')
17
+ link.rel = 'stylesheet'
18
+ link.href = url
19
+ link.onload = () => {
20
+ console.log('[Runtime] Foundation CSS loaded')
21
+ resolve()
22
+ }
23
+ link.onerror = () => {
24
+ console.warn('[Runtime] Could not load foundation CSS from:', url)
25
+ resolve() // Don't fail for CSS
26
+ }
27
+ document.head.appendChild(link)
28
+ })
29
+ }
30
+
31
+ /**
32
+ * Load a foundation module via dynamic import
33
+ * @param {string|Object} source - URL string or {url, cssUrl} object
34
+ * @returns {Promise<Object>} The loaded foundation module
35
+ */
36
+ export async function loadFoundation(source) {
37
+ const url = typeof source === 'string' ? source : source.url
38
+ // Auto-derive CSS URL from JS URL by convention: foundation.js → assets/foundation.css
39
+ const cssUrl = typeof source === 'object' ? source.cssUrl
40
+ : url.replace(/[^/]+\.js$/, 'assets/foundation.css')
41
+
42
+ console.log(`[Runtime] Loading foundation from: ${url}`)
43
+
44
+ try {
45
+ // Load CSS and JS in parallel
46
+ const [, foundation] = await Promise.all([
47
+ cssUrl ? loadFoundationCSS(cssUrl) : Promise.resolve(),
48
+ import(/* @vite-ignore */ url)
49
+ ])
50
+
51
+ const componentNames = Object.keys(foundation).filter(k => k !== 'default')
52
+ console.log('[Runtime] Foundation loaded. Available components:', componentNames)
53
+
54
+ return foundation
55
+ } catch (error) {
56
+ console.error('[Runtime] Failed to load foundation:', error)
57
+ throw error
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Load extensions (secondary foundations) in parallel
63
+ * @param {Array<string|Object>} urls - Extension URLs or {url, cssUrl} objects
64
+ * @param {Object} uniwebInstance - The Uniweb instance to register extensions on
65
+ */
66
+ export async function loadExtensions(urls, uniwebInstance) {
67
+ if (!urls?.length) return
68
+
69
+ // Resolve extension URLs against base path for subdirectory deployments
70
+ // e.g., /effects/foundation.js → /templates/extensions/effects/foundation.js
71
+ const basePath = import.meta.env?.BASE_URL || '/'
72
+ function resolveUrl(source) {
73
+ if (basePath === '/') return source
74
+ if (typeof source === 'string' && source.startsWith('/')) {
75
+ return basePath + source.slice(1)
76
+ }
77
+ if (typeof source === 'object' && source.url?.startsWith('/')) {
78
+ return { ...source, url: basePath + source.url.slice(1) }
79
+ }
80
+ return source
81
+ }
82
+
83
+ const results = await Promise.allSettled(
84
+ urls.map(url => loadFoundation(resolveUrl(url)))
85
+ )
86
+
87
+ for (let i = 0; i < results.length; i++) {
88
+ if (results[i].status === 'fulfilled') {
89
+ uniwebInstance.registerExtension(results[i].value)
90
+ console.log(`[Runtime] Extension loaded: ${urls[i]}`)
91
+ } else {
92
+ console.warn(`[Runtime] Extension failed to load: ${urls[i]}`, results[i].reason)
93
+ }
94
+ }
95
+ }
package/src/index.jsx CHANGED
@@ -7,307 +7,10 @@
7
7
 
8
8
  import React from 'react'
9
9
  import { createRoot } from 'react-dom/client'
10
- import {
11
- BrowserRouter,
12
- Routes,
13
- Route,
14
- Link as RouterLink,
15
- useNavigate,
16
- useParams,
17
- useLocation
18
- } from 'react-router-dom'
19
10
 
20
- // Data fetcher (registered on dataStore so core can call it without importing runtime)
21
- import { executeFetchClient } from './data-fetcher-client.js'
22
-
23
- // Components
24
- import { ChildBlocks } from './components/PageRenderer.jsx'
25
- import WebsiteRenderer from './components/WebsiteRenderer.jsx'
26
- import ErrorBoundary from './components/ErrorBoundary.jsx'
27
-
28
- // Core factory from @uniweb/core
29
- import { createUniweb } from '@uniweb/core'
30
-
31
- /**
32
- * Decode combined data from __DATA__ element
33
- *
34
- * Encoding is signaled via MIME type:
35
- * - application/json: plain JSON (no compression)
36
- * - application/gzip: gzip + base64 encoded
37
- *
38
- * @returns {Promise<{foundation: Object, content: Object}|null>}
39
- */
40
- async function decodeData() {
41
- const el = document.getElementById('__DATA__')
42
- if (!el?.textContent) return null
43
-
44
- const raw = el.textContent
45
-
46
- // Plain JSON (uncompressed)
47
- if (el.type === 'application/json') {
48
- try {
49
- return JSON.parse(raw)
50
- } catch {
51
- return null
52
- }
53
- }
54
-
55
- // Compressed (application/gzip or legacy application/octet-stream)
56
- if (typeof DecompressionStream !== 'undefined') {
57
- try {
58
- const bytes = Uint8Array.from(atob(raw), c => c.charCodeAt(0))
59
- const stream = new DecompressionStream('gzip')
60
- const writer = stream.writable.getWriter()
61
- writer.write(bytes)
62
- writer.close()
63
- const json = await new Response(stream.readable).text()
64
- return JSON.parse(json)
65
- } catch {
66
- return null
67
- }
68
- }
69
-
70
- // Fallback for old browsers: try plain JSON (server can detect User-Agent)
71
- try {
72
- return JSON.parse(raw)
73
- } catch {
74
- return null
75
- }
76
- }
77
-
78
- /**
79
- * Load foundation CSS from URL
80
- * @param {string} url - URL to foundation's CSS file
81
- */
82
- async function loadFoundationCSS(url) {
83
- if (!url) return
84
-
85
- return new Promise((resolve) => {
86
- const link = document.createElement('link')
87
- link.rel = 'stylesheet'
88
- link.href = url
89
- link.onload = () => {
90
- console.log('[Runtime] Foundation CSS loaded')
91
- resolve()
92
- }
93
- link.onerror = () => {
94
- console.warn('[Runtime] Could not load foundation CSS from:', url)
95
- resolve() // Don't fail for CSS
96
- }
97
- document.head.appendChild(link)
98
- })
99
- }
100
-
101
- /**
102
- * Load a foundation module via dynamic import
103
- * @param {string|Object} source - URL string or {url, cssUrl} object
104
- * @returns {Promise<Object>} The loaded foundation module
105
- */
106
- async function loadFoundation(source) {
107
- const url = typeof source === 'string' ? source : source.url
108
- // Auto-derive CSS URL from JS URL by convention: foundation.js → assets/foundation.css
109
- const cssUrl = typeof source === 'object' ? source.cssUrl
110
- : url.replace(/[^/]+\.js$/, 'assets/foundation.css')
111
-
112
- console.log(`[Runtime] Loading foundation from: ${url}`)
113
-
114
- try {
115
- // Load CSS and JS in parallel
116
- const [, foundation] = await Promise.all([
117
- cssUrl ? loadFoundationCSS(cssUrl) : Promise.resolve(),
118
- import(/* @vite-ignore */ url)
119
- ])
120
-
121
- const componentNames = Object.keys(foundation).filter(k => k !== 'default')
122
- console.log('[Runtime] Foundation loaded. Available components:', componentNames)
123
-
124
- return foundation
125
- } catch (error) {
126
- console.error('[Runtime] Failed to load foundation:', error)
127
- throw error
128
- }
129
- }
130
-
131
- /**
132
- * Load extensions (secondary foundations) in parallel
133
- * @param {Array<string|Object>} urls - Extension URLs or {url, cssUrl} objects
134
- * @param {Object} uniwebInstance - The Uniweb instance to register extensions on
135
- */
136
- async function loadExtensions(urls, uniwebInstance) {
137
- if (!urls?.length) return
138
-
139
- // Resolve extension URLs against base path for subdirectory deployments
140
- // e.g., /effects/foundation.js → /templates/extensions/effects/foundation.js
141
- const basePath = import.meta.env?.BASE_URL || '/'
142
- function resolveUrl(source) {
143
- if (basePath === '/') return source
144
- if (typeof source === 'string' && source.startsWith('/')) {
145
- return basePath + source.slice(1)
146
- }
147
- if (typeof source === 'object' && source.url?.startsWith('/')) {
148
- return { ...source, url: basePath + source.url.slice(1) }
149
- }
150
- return source
151
- }
152
-
153
- const results = await Promise.allSettled(
154
- urls.map(url => loadFoundation(resolveUrl(url)))
155
- )
156
-
157
- for (let i = 0; i < results.length; i++) {
158
- if (results[i].status === 'fulfilled') {
159
- uniwebInstance.registerExtension(results[i].value)
160
- console.log(`[Runtime] Extension loaded: ${urls[i]}`)
161
- } else {
162
- console.warn(`[Runtime] Extension failed to load: ${urls[i]}`, results[i].reason)
163
- }
164
- }
165
- }
166
-
167
- /**
168
- * Map friendly family names to react-icons codes
169
- * The existing CDN uses react-icons structure: /{familyCode}/{familyCode}-{name}.svg
170
- */
171
- const ICON_FAMILY_MAP = {
172
- // Friendly names
173
- lucide: 'lu',
174
- heroicons: 'hi',
175
- heroicons2: 'hi2',
176
- phosphor: 'pi',
177
- tabler: 'tb',
178
- feather: 'fi',
179
- // Font Awesome (multiple versions)
180
- fa: 'fa',
181
- fa6: 'fa6',
182
- // Additional families from react-icons
183
- bootstrap: 'bs',
184
- 'material-design': 'md',
185
- 'ant-design': 'ai',
186
- remix: 'ri',
187
- 'simple-icons': 'si',
188
- ionicons: 'io5',
189
- boxicons: 'bi',
190
- vscode: 'vsc',
191
- weather: 'wi',
192
- game: 'gi',
193
- // Also support direct codes for power users
194
- lu: 'lu',
195
- hi: 'hi',
196
- hi2: 'hi2',
197
- pi: 'pi',
198
- tb: 'tb',
199
- fi: 'fi',
200
- bs: 'bs',
201
- md: 'md',
202
- ai: 'ai',
203
- ri: 'ri',
204
- io5: 'io5',
205
- bi: 'bi',
206
- si: 'si',
207
- vsc: 'vsc',
208
- wi: 'wi',
209
- gi: 'gi'
210
- }
211
-
212
- /**
213
- * Create CDN-based icon resolver
214
- * @param {Object} iconConfig - From site.yml icons:
215
- * @returns {Function} Resolver: (library, name) => Promise<string|null>
216
- */
217
- function createIconResolver(iconConfig = {}) {
218
- // Default to GitHub Pages CDN, can be overridden in site.yml
219
- const CDN_BASE = iconConfig.cdnUrl || 'https://uniweb.github.io/icons'
220
- const useCdn = iconConfig.cdn !== false
221
-
222
- // Cache resolved icons
223
- const cache = new Map()
224
-
225
- return async function resolve(library, name) {
226
- // Map friendly name to react-icons code
227
- const familyCode = ICON_FAMILY_MAP[library.toLowerCase()]
228
- if (!familyCode) {
229
- console.warn(`[icons] Unknown family "${library}"`)
230
- return null
231
- }
232
-
233
- // Check cache
234
- const key = `${familyCode}:${name}`
235
- if (cache.has(key)) return cache.get(key)
236
-
237
- // Fetch from CDN
238
- if (!useCdn) {
239
- cache.set(key, null)
240
- return null
241
- }
242
-
243
- try {
244
- // CDN structure: /{familyCode}/{familyCode}-{name}.svg
245
- // e.g., lucide:home → /lu/lu-home.svg
246
- const iconFileName = `${familyCode}-${name}`
247
- const url = `${CDN_BASE}/${familyCode}/${iconFileName}.svg`
248
- const response = await fetch(url)
249
- if (!response.ok) throw new Error(`HTTP ${response.status}`)
250
- const svg = await response.text()
251
- cache.set(key, svg)
252
- return svg
253
- } catch (err) {
254
- console.warn(`[icons] Failed to load ${library}:${name}`, err.message)
255
- cache.set(key, null)
256
- return null
257
- }
258
- }
259
- }
260
-
261
- /**
262
- * Initialize the Uniweb instance
263
- * @param {Object} configData - Site configuration data
264
- * @returns {Uniweb}
265
- */
266
- function initUniweb(configData) {
267
- // Create singleton via @uniweb/core (also assigns to globalThis.uniweb)
268
- const uniwebInstance = createUniweb(configData)
269
-
270
- // Pre-populate DataStore from build-time fetched data
271
- if (configData.fetchedData && uniwebInstance.activeWebsite?.dataStore) {
272
- for (const entry of configData.fetchedData) {
273
- uniwebInstance.activeWebsite.dataStore.set(entry.config, entry.data)
274
- }
275
- }
276
-
277
- // Set up child block renderer for nested blocks
278
- uniwebInstance.childBlockRenderer = ChildBlocks
279
-
280
- // Register routing components for kit and foundation components
281
- // This enables the bridge pattern: components access routing via
282
- // website.getRoutingComponents() instead of direct imports
283
- uniwebInstance.routingComponents = {
284
- Link: RouterLink,
285
- useNavigate,
286
- useParams,
287
- useLocation
288
- }
289
-
290
- // Set up icon resolver based on site config
291
- uniwebInstance.iconResolver = createIconResolver(configData.icons)
292
-
293
- // Populate icon cache from prerendered data (if available)
294
- // This allows icons to render immediately without CDN fetches
295
- if (typeof document !== 'undefined') {
296
- try {
297
- const cacheEl = document.getElementById('__ICON_CACHE__')
298
- if (cacheEl) {
299
- const cached = JSON.parse(cacheEl.textContent)
300
- for (const [key, svg] of Object.entries(cached)) {
301
- uniwebInstance.iconCache.set(key, svg)
302
- }
303
- }
304
- } catch (e) {
305
- // Ignore parse errors
306
- }
307
- }
308
-
309
- return uniwebInstance
310
- }
11
+ import { setupUniweb, registerFoundation, decodeData } from './setup.js'
12
+ import { loadFoundation, loadExtensions } from './foundation-loader.js'
13
+ import RuntimeProvider from './RuntimeProvider.jsx'
311
14
 
312
15
  /**
313
16
  * Get the router basename from Vite's BASE_URL
@@ -321,63 +24,6 @@ function getBasename() {
321
24
  return baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl
322
25
  }
323
26
 
324
- /**
325
- * Render the application
326
- * @param {Object} options
327
- */
328
- function render({ development = false, basename } = {}) {
329
- const container = document.getElementById('root')
330
- if (!container) {
331
- console.error('[Runtime] Root element not found')
332
- return
333
- }
334
-
335
- // Use provided basename, or derive from Vite's BASE_URL
336
- const routerBasename = basename ?? getBasename()
337
-
338
- // Set initial active page from browser URL so getLocaleUrl() works on first render
339
- const website = globalThis.uniweb?.activeWebsite
340
- if (website && typeof window !== 'undefined') {
341
- const rawPath = window.location.pathname
342
- const basePath = routerBasename || ''
343
- const routePath = basePath && rawPath.startsWith(basePath)
344
- ? rawPath.slice(basePath.length) || '/'
345
- : rawPath
346
- website.setActivePage(routePath)
347
-
348
- // Store base path on Website for components that need it (e.g., Link reload)
349
- if (website.setBasePath) {
350
- website.setBasePath(routerBasename || '')
351
- }
352
-
353
- // Register data fetcher on the DataStore so BlockRenderer can use it
354
- if (website.dataStore) {
355
- website.dataStore.registerFetcher(executeFetchClient)
356
- }
357
- }
358
-
359
- const root = createRoot(container)
360
-
361
- const app = (
362
- <ErrorBoundary
363
- fallback={
364
- <div style={{ padding: '2rem', textAlign: 'center' }}>
365
- <h2>Something went wrong</h2>
366
- <p>Please try refreshing the page</p>
367
- </div>
368
- }
369
- >
370
- <BrowserRouter basename={routerBasename}>
371
- <Routes>
372
- <Route path="/*" element={<WebsiteRenderer />} />
373
- </Routes>
374
- </BrowserRouter>
375
- </ErrorBoundary>
376
- )
377
-
378
- root.render(development ? <React.StrictMode>{app}</React.StrictMode> : app)
379
- }
380
-
381
27
  /**
382
28
  * Initialize the Runtime Environment
383
29
  *
@@ -409,7 +55,7 @@ async function initRuntime(foundationSource, options = {}) {
409
55
  }
410
56
 
411
57
  // Initialize core runtime
412
- const uniwebInstance = initUniweb(configData)
58
+ const uniwebInstance = setupUniweb(configData)
413
59
 
414
60
  try {
415
61
  let foundation
@@ -432,18 +78,8 @@ async function initRuntime(foundationSource, options = {}) {
432
78
  throw new Error('Failed to load foundation')
433
79
  }
434
80
 
435
- // Set the foundation on the runtime
436
- uniwebInstance.setFoundation(foundation)
437
-
438
- // Set foundation capabilities (layouts, props, etc.) if provided
439
- if (foundation.default?.capabilities) {
440
- uniwebInstance.setFoundationConfig(foundation.default.capabilities)
441
- }
442
-
443
- // Attach layout metadata (areas, transitions, defaults) from foundation entry point
444
- if (foundation.default?.layoutMeta && uniwebInstance.foundationConfig) {
445
- uniwebInstance.foundationConfig.layoutMeta = foundation.default.layoutMeta
446
- }
81
+ // Register foundation on the runtime
82
+ registerFoundation(uniwebInstance, foundation)
447
83
 
448
84
  // Load extensions (secondary foundations)
449
85
  const extensions = configData?.config?.extensions
@@ -451,8 +87,31 @@ async function initRuntime(foundationSource, options = {}) {
451
87
  await loadExtensions(extensions, uniwebInstance)
452
88
  }
453
89
 
90
+ // Derive basename
91
+ const routerBasename = basename ?? getBasename()
92
+
93
+ // Set initial active page from browser URL so getLocaleUrl() works on first render
94
+ const website = uniwebInstance.activeWebsite
95
+ if (website && typeof window !== 'undefined') {
96
+ const rawPath = window.location.pathname
97
+ const basePath = routerBasename || ''
98
+ const routePath = basePath && rawPath.startsWith(basePath)
99
+ ? rawPath.slice(basePath.length) || '/'
100
+ : rawPath
101
+ website.setActivePage(routePath)
102
+ }
103
+
454
104
  // Render the app
455
- render({ development, basename })
105
+ const container = document.getElementById('root')
106
+ if (!container) {
107
+ console.error('[Runtime] Root element not found')
108
+ return
109
+ }
110
+
111
+ const root = createRoot(container)
112
+ root.render(
113
+ <RuntimeProvider basename={routerBasename} development={development} />
114
+ )
456
115
 
457
116
  // Log success
458
117
  if (!development) {
@@ -51,7 +51,6 @@ export function guaranteeContentStructure(parsedContent) {
51
51
  title: content.title || '',
52
52
  pretitle: content.pretitle || '',
53
53
  subtitle: content.subtitle || '',
54
- subtitle2: content.subtitle2 || '',
55
54
  alignment: content.alignment || null,
56
55
 
57
56
  // Flat body fields
package/src/setup.js ADDED
@@ -0,0 +1,257 @@
1
+ /**
2
+ * Runtime setup — singleton initialization and data decoding
3
+ *
4
+ * Creates and configures the Uniweb singleton (globalThis.uniweb)
5
+ * with routing components, icon resolver, data fetcher, etc.
6
+ */
7
+
8
+ import { createUniweb, Website } from '@uniweb/core'
9
+ import {
10
+ Link as RouterLink,
11
+ useNavigate,
12
+ useParams,
13
+ useLocation
14
+ } from 'react-router-dom'
15
+
16
+ import { ChildBlocks } from './components/PageRenderer.jsx'
17
+ import { executeFetchClient } from './data-fetcher-client.js'
18
+
19
+ /**
20
+ * Map friendly family names to react-icons codes
21
+ * The existing CDN uses react-icons structure: /{familyCode}/{familyCode}-{name}.svg
22
+ */
23
+ const ICON_FAMILY_MAP = {
24
+ // Friendly names
25
+ lucide: 'lu',
26
+ heroicons: 'hi',
27
+ heroicons2: 'hi2',
28
+ phosphor: 'pi',
29
+ tabler: 'tb',
30
+ feather: 'fi',
31
+ // Font Awesome (multiple versions)
32
+ fa: 'fa',
33
+ fa6: 'fa6',
34
+ // Additional families from react-icons
35
+ bootstrap: 'bs',
36
+ 'material-design': 'md',
37
+ 'ant-design': 'ai',
38
+ remix: 'ri',
39
+ 'simple-icons': 'si',
40
+ ionicons: 'io5',
41
+ boxicons: 'bi',
42
+ vscode: 'vsc',
43
+ weather: 'wi',
44
+ game: 'gi',
45
+ // Also support direct codes for power users
46
+ lu: 'lu',
47
+ hi: 'hi',
48
+ hi2: 'hi2',
49
+ pi: 'pi',
50
+ tb: 'tb',
51
+ fi: 'fi',
52
+ bs: 'bs',
53
+ md: 'md',
54
+ ai: 'ai',
55
+ ri: 'ri',
56
+ io5: 'io5',
57
+ bi: 'bi',
58
+ si: 'si',
59
+ vsc: 'vsc',
60
+ wi: 'wi',
61
+ gi: 'gi'
62
+ }
63
+
64
+ /**
65
+ * Create CDN-based icon resolver
66
+ * @param {Object} iconConfig - From site.yml icons:
67
+ * @returns {Function} Resolver: (library, name) => Promise<string|null>
68
+ */
69
+ function createIconResolver(iconConfig = {}) {
70
+ // Default to GitHub Pages CDN, can be overridden in site.yml
71
+ const CDN_BASE = iconConfig.cdnUrl || 'https://uniweb.github.io/icons'
72
+ const useCdn = iconConfig.cdn !== false
73
+
74
+ // Cache resolved icons
75
+ const cache = new Map()
76
+
77
+ return async function resolve(library, name) {
78
+ // Map friendly name to react-icons code
79
+ const familyCode = ICON_FAMILY_MAP[library.toLowerCase()]
80
+ if (!familyCode) {
81
+ console.warn(`[icons] Unknown family "${library}"`)
82
+ return null
83
+ }
84
+
85
+ // Check cache
86
+ const key = `${familyCode}:${name}`
87
+ if (cache.has(key)) return cache.get(key)
88
+
89
+ // Fetch from CDN
90
+ if (!useCdn) {
91
+ cache.set(key, null)
92
+ return null
93
+ }
94
+
95
+ try {
96
+ // CDN structure: /{familyCode}/{familyCode}-{name}.svg
97
+ // e.g., lucide:home → /lu/lu-home.svg
98
+ const iconFileName = `${familyCode}-${name}`
99
+ const url = `${CDN_BASE}/${familyCode}/${iconFileName}.svg`
100
+ const response = await fetch(url)
101
+ if (!response.ok) throw new Error(`HTTP ${response.status}`)
102
+ const svg = await response.text()
103
+ cache.set(key, svg)
104
+ return svg
105
+ } catch (err) {
106
+ console.warn(`[icons] Failed to load ${library}:${name}`, err.message)
107
+ cache.set(key, null)
108
+ return null
109
+ }
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Initialize the Uniweb singleton.
115
+ *
116
+ * Creates globalThis.uniweb with Website, routing components, icons,
117
+ * data fetcher, and pre-populated DataStore.
118
+ *
119
+ * @param {Object} configData - Site configuration data
120
+ * @returns {Uniweb}
121
+ */
122
+ export function setupUniweb(configData) {
123
+ // Create singleton via @uniweb/core (also assigns to globalThis.uniweb)
124
+ const uniwebInstance = createUniweb(configData)
125
+
126
+ // Pre-populate DataStore from build-time fetched data
127
+ if (configData.fetchedData && uniwebInstance.activeWebsite?.dataStore) {
128
+ for (const entry of configData.fetchedData) {
129
+ uniwebInstance.activeWebsite.dataStore.set(entry.config, entry.data)
130
+ }
131
+ }
132
+
133
+ // Set up child block renderer for nested blocks
134
+ uniwebInstance.childBlockRenderer = ChildBlocks
135
+
136
+ // Register routing components for kit and foundation components
137
+ // This enables the bridge pattern: components access routing via
138
+ // website.getRoutingComponents() instead of direct imports
139
+ uniwebInstance.routingComponents = {
140
+ Link: RouterLink,
141
+ useNavigate,
142
+ useParams,
143
+ useLocation
144
+ }
145
+
146
+ // Set up icon resolver based on site config
147
+ uniwebInstance.iconResolver = createIconResolver(configData.icons)
148
+
149
+ // Populate icon cache from prerendered data (if available)
150
+ // This allows icons to render immediately without CDN fetches
151
+ if (typeof document !== 'undefined') {
152
+ try {
153
+ const cacheEl = document.getElementById('__ICON_CACHE__')
154
+ if (cacheEl) {
155
+ const cached = JSON.parse(cacheEl.textContent)
156
+ for (const [key, svg] of Object.entries(cached)) {
157
+ uniwebInstance.iconCache.set(key, svg)
158
+ }
159
+ }
160
+ } catch (e) {
161
+ // Ignore parse errors
162
+ }
163
+ }
164
+
165
+ // Register data fetcher on DataStore so BlockRenderer can fetch data
166
+ if (uniwebInstance.activeWebsite?.dataStore) {
167
+ uniwebInstance.activeWebsite.dataStore.registerFetcher(executeFetchClient)
168
+ }
169
+
170
+ return uniwebInstance
171
+ }
172
+
173
+ /**
174
+ * Rebuild the Website within the existing singleton.
175
+ *
176
+ * For live editing: creates a new Website from modified configData
177
+ * and assigns it to the singleton. The singleton itself (foundation,
178
+ * icon resolver, routing components) stays unchanged.
179
+ *
180
+ * @param {Object} configData - Modified site configuration data
181
+ * @returns {Website} The new Website instance
182
+ */
183
+ export function rebuildWebsite(configData) {
184
+ const uniweb = globalThis.uniweb
185
+ const newWebsite = new Website(configData)
186
+ newWebsite.dataStore.registerFetcher(executeFetchClient)
187
+ uniweb.activeWebsite = newWebsite
188
+ return newWebsite
189
+ }
190
+
191
+ /**
192
+ * Register a foundation on the Uniweb singleton.
193
+ *
194
+ * Separated from setupUniweb because foundation loading is async
195
+ * and happens independently.
196
+ *
197
+ * @param {Object} uniwebInstance - The Uniweb singleton
198
+ * @param {Object} foundation - The loaded foundation module
199
+ */
200
+ export function registerFoundation(uniwebInstance, foundation) {
201
+ uniwebInstance.setFoundation(foundation)
202
+
203
+ if (foundation.default?.capabilities) {
204
+ uniwebInstance.setFoundationConfig(foundation.default.capabilities)
205
+ }
206
+
207
+ if (foundation.default?.layoutMeta && uniwebInstance.foundationConfig) {
208
+ uniwebInstance.foundationConfig.layoutMeta = foundation.default.layoutMeta
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Decode combined data from __DATA__ element
214
+ *
215
+ * Encoding is signaled via MIME type:
216
+ * - application/json: plain JSON (no compression)
217
+ * - application/gzip: gzip + base64 encoded
218
+ *
219
+ * @returns {Promise<{foundation: Object, content: Object}|null>}
220
+ */
221
+ export async function decodeData() {
222
+ const el = document.getElementById('__DATA__')
223
+ if (!el?.textContent) return null
224
+
225
+ const raw = el.textContent
226
+
227
+ // Plain JSON (uncompressed)
228
+ if (el.type === 'application/json') {
229
+ try {
230
+ return JSON.parse(raw)
231
+ } catch {
232
+ return null
233
+ }
234
+ }
235
+
236
+ // Compressed (application/gzip or legacy application/octet-stream)
237
+ if (typeof DecompressionStream !== 'undefined') {
238
+ try {
239
+ const bytes = Uint8Array.from(atob(raw), c => c.charCodeAt(0))
240
+ const stream = new DecompressionStream('gzip')
241
+ const writer = stream.writable.getWriter()
242
+ writer.write(bytes)
243
+ writer.close()
244
+ const json = await new Response(stream.readable).text()
245
+ return JSON.parse(json)
246
+ } catch {
247
+ return null
248
+ }
249
+ }
250
+
251
+ // Fallback for old browsers: try plain JSON (server can detect User-Agent)
252
+ try {
253
+ return JSON.parse(raw)
254
+ } catch {
255
+ return null
256
+ }
257
+ }