cms-renderer 0.2.6 → 0.2.9

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.
@@ -1,5 +1,5 @@
1
1
  import React__default from 'react';
2
- import { BlockData, BlockComponentRegistry } from './types.js';
2
+ import { BlockData, BlockComponentRegistry, ResolvedRouteParams } from './types.js';
3
3
  import '@repo/cms-schema/blocks';
4
4
 
5
5
  /**
@@ -55,6 +55,11 @@ interface BlockRendererProps {
55
55
  * If true, renders the component without any tree walking or editable wrappers.
56
56
  */
57
57
  disableEditable?: boolean;
58
+ /**
59
+ * Resolved route parameters from parametric routes.
60
+ * Each key is a param name (e.g., "country") with its value, schema name, and full document.
61
+ */
62
+ routeParams?: ResolvedRouteParams;
58
63
  }
59
64
  /**
60
65
  * Renders a single block by dispatching to the appropriate component.
@@ -67,6 +72,6 @@ interface BlockRendererProps {
67
72
  * 2. Extracts all string values from block.content
68
73
  * 3. Walks the rendered tree and wraps matching text nodes with spans
69
74
  */
70
- declare function BlockRenderer({ block, registry, disableEditable }: BlockRendererProps): React__default.JSX.Element | null;
75
+ declare function BlockRenderer({ block, registry, disableEditable, routeParams, }: BlockRendererProps): React__default.JSX.Element | null;
71
76
 
72
77
  export { BlockRenderer, walkReactNode };
@@ -97,7 +97,12 @@ function renderToWalkableTree(node, keyPrefix = "") {
97
97
  }
98
98
  return node;
99
99
  }
100
- function BlockRenderer({ block, registry, disableEditable }) {
100
+ function BlockRenderer({
101
+ block,
102
+ registry,
103
+ disableEditable,
104
+ routeParams
105
+ }) {
101
106
  const Component = registry[block.type];
102
107
  if (!Component) {
103
108
  if (process.env.NODE_ENV === "development") {
@@ -105,7 +110,7 @@ function BlockRenderer({ block, registry, disableEditable }) {
105
110
  }
106
111
  return null;
107
112
  }
108
- const component = /* @__PURE__ */ jsx(Component, { content: block.content });
113
+ const component = /* @__PURE__ */ jsx(Component, { content: block.content, routeParams });
109
114
  if (disableEditable) {
110
115
  return component;
111
116
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../lib/block-renderer.tsx"],"sourcesContent":["/**\n * Block Renderer Component\n *\n * Dispatches block data to the appropriate component using the ComponentMap pattern.\n * This is the main entry point for rendering blocks from the CMS.\n */\n\nimport React from 'react';\nimport { BlockToolbar } from './block-toolbar';\nimport type { BlockComponentRegistry, BlockData } from './types';\n\ntype TextInfo = {\n value: string;\n path: Array<string | number>;\n parentType?: React.ElementType;\n key?: React.Key | null;\n};\n\ntype ElementInfo = {\n element: React.ReactElement;\n path: Array<string | number>;\n};\n\ntype WalkVisitors = {\n /**\n * Called for every string/number child encountered.\n * Return:\n * - same string (or modified)\n * - a ReactNode (e.g. wrap in <span/>)\n */\n onText?: (info: TextInfo) => React.ReactNode;\n\n /**\n * Called for every ReactElement encountered (after children are processed).\n * Return:\n * - same element\n * - a cloned/modified element\n */\n onElement?: (info: ElementInfo) => React.ReactElement;\n};\n\n/**\n * Recursively maps a ReactNode tree, allowing transformations of text nodes and/or elements.\n * SSR-safe: does not touch DOM APIs.\n */\nexport function walkReactNode(\n node: React.ReactNode,\n visitors: WalkVisitors,\n ctx: {\n path?: Array<string | number>;\n parentType?: React.ElementType;\n key?: React.Key | null;\n } = {}\n): React.ReactNode {\n const path = ctx.path ?? [];\n\n // Fast-path primitives\n if (node == null || typeof node === 'boolean') return node;\n\n if (typeof node === 'string' || typeof node === 'number') {\n const value = String(node);\n return visitors.onText\n ? visitors.onText({ value, path, parentType: ctx.parentType, key: ctx.key })\n : node;\n }\n\n // Arrays\n if (Array.isArray(node)) {\n return node.map((child, i) => {\n // biome-ignore lint/suspicious/noExplicitAny: React child key access\n const childKey = (child as any)?.key ?? null;\n const result = walkReactNode(child, visitors, {\n path: [...path, i],\n parentType: ctx.parentType,\n key: childKey,\n });\n // Ensure array children have keys\n if (React.isValidElement(result) && result.key == null) {\n return React.cloneElement(result, { key: childKey ?? `arr-${path.join('-')}-${i}` });\n }\n return result;\n });\n }\n\n // ReactElement (including Fragment)\n if (React.isValidElement(node)) {\n // biome-ignore lint/suspicious/noExplicitAny: React element props access\n const el = node as React.ReactElement<any>;\n const elProps = el.props as Record<string, unknown> | null;\n\n // Recurse into children (if any)\n const hasChildren = elProps && 'children' in elProps;\n const nextChildren = hasChildren\n ? React.Children.map(elProps.children as React.ReactNode, (child, i) => {\n // biome-ignore lint/suspicious/noExplicitAny: React child key access\n const childKey = (child as any)?.key ?? null;\n const result = walkReactNode(child, visitors, {\n path: [...path, 'children', i],\n parentType: el.type as React.ElementType,\n key: childKey,\n });\n // Ensure children have keys\n if (React.isValidElement(result) && result.key == null) {\n return React.cloneElement(result, { key: childKey ?? `child-${path.join('-')}-${i}` });\n }\n return result;\n })\n : (elProps?.children as React.ReactNode);\n\n // Only clone if children changed (or if you want to force a clone)\n const cloned = hasChildren\n ? React.cloneElement(el, undefined, nextChildren as React.ReactNode)\n : el;\n\n return visitors.onElement ? visitors.onElement({ element: cloned, path }) : cloned;\n }\n\n // Functions, symbols, portals, etc. are rare here; return as-is\n return node;\n}\n\n// -----------------------------------------------------------------------------\n// Content Value Extraction\n// -----------------------------------------------------------------------------\n\ntype ContentMatch = {\n contentPath: string;\n value: string;\n};\n\n/**\n * Extracts all string values from a content object with their paths.\n * Returns a Map where keys are string values and values are arrays of content paths.\n */\nfunction extractContentValues(\n content: Record<string, unknown>,\n basePath: string[] = []\n): Map<string, ContentMatch[]> {\n const map = new Map<string, ContentMatch[]>();\n\n function walk(obj: unknown, path: string[]) {\n if (typeof obj === 'string' && obj.trim() !== '') {\n const contentPath = path.join('.');\n const existing = map.get(obj) || [];\n existing.push({ contentPath, value: obj });\n map.set(obj, existing);\n } else if (Array.isArray(obj)) {\n for (let index = 0; index < obj.length; index++) {\n walk(obj[index], [...path, String(index)]);\n }\n } else if (obj && typeof obj === 'object') {\n for (const [key, value] of Object.entries(obj)) {\n walk(value, [...path, key]);\n }\n }\n }\n\n walk(content, basePath);\n return map;\n}\n\n// -----------------------------------------------------------------------------\n// Props\n// -----------------------------------------------------------------------------\n\ninterface BlockRendererProps {\n /**\n * The block data to render.\n * Must have a `type` field that maps to a registered component.\n */\n block: BlockData;\n registry: Partial<BlockComponentRegistry>;\n /**\n * If true, renders the component without any tree walking or editable wrappers.\n */\n disableEditable?: boolean;\n}\n\n// -----------------------------------------------------------------------------\n// Component\n// -----------------------------------------------------------------------------\n\n/**\n * Recursively renders a React node, invoking function components to get their output.\n * This allows us to walk the full rendered tree, not just the element wrappers.\n */\nfunction renderToWalkableTree(node: React.ReactNode, keyPrefix = ''): React.ReactNode {\n if (node == null || typeof node === 'boolean') return node;\n if (typeof node === 'string' || typeof node === 'number') return node;\n\n if (Array.isArray(node)) {\n return node.map((child, i) => {\n const result = renderToWalkableTree(child, `${keyPrefix}${i}-`);\n // Ensure array children have keys\n if (React.isValidElement(result) && result.key == null) {\n // biome-ignore lint/suspicious/noExplicitAny: Adding key to element\n const existingKey = (child as any)?.key;\n return React.cloneElement(result, { key: existingKey ?? `${keyPrefix}${i}` });\n }\n return result;\n });\n }\n\n if (React.isValidElement(node)) {\n // biome-ignore lint/suspicious/noExplicitAny: React element props access\n const el = node as React.ReactElement<any>;\n const elProps = el.props as Record<string, unknown> | null;\n\n // If it's a function component, invoke it to get the rendered output\n if (typeof el.type === 'function') {\n try {\n // biome-ignore lint/complexity/noBannedTypes: Need to invoke React function component\n const rendered = (el.type as Function)(el.props);\n return renderToWalkableTree(rendered, keyPrefix);\n } catch {\n // If component throws (e.g., uses hooks), return as-is\n return node;\n }\n }\n\n // For host elements (div, span, etc.), recurse into children\n if (elProps && 'children' in elProps) {\n const newChildren = renderToWalkableTree(elProps.children as React.ReactNode, keyPrefix);\n return React.cloneElement(el, undefined, newChildren);\n }\n\n return node;\n }\n\n return node;\n}\n\n/**\n * Renders a single block by dispatching to the appropriate component.\n *\n * Uses the ComponentMap pattern: the block's `type` field determines which\n * component renders the block's `content`.\n *\n * Internally, it:\n * 1. Renders the component tree by invoking function components\n * 2. Extracts all string values from block.content\n * 3. Walks the rendered tree and wraps matching text nodes with spans\n */\nexport function BlockRenderer({ block, registry, disableEditable }: BlockRendererProps) {\n const Component = registry[block.type];\n\n if (!Component) {\n // Log warning in development, render nothing in production\n if (process.env.NODE_ENV === 'development') {\n console.warn(`[BlockRenderer] Unknown block type: ${block.type}`);\n }\n return null;\n }\n\n // biome-ignore lint/suspicious/noExplicitAny: Type safety ensured by BlockData discriminated union\n const component = <Component content={block.content as any} />;\n\n if (disableEditable) {\n return component;\n }\n\n // Extract all string values from content with their paths\n const contentValueMap = extractContentValues(block.content as Record<string, unknown>);\n\n // Try server-side tree walking for text span injection (best-effort).\n // This works for sync server components but fails for client components,\n // async components, and components using hooks.\n let renderedComponent: React.ReactNode = component;\n let needsClientSideSpans = true;\n\n try {\n const renderedTree = renderToWalkableTree(component);\n\n // Check if tree walking produced host elements (div, section, etc.)\n // vs the original component reference (function type = couldn't be invoked)\n const isWalkable =\n React.isValidElement(renderedTree) &&\n typeof (renderedTree as React.ReactElement<Record<string, unknown>>).type === 'string';\n\n if (isWalkable) {\n const usedPaths = new Set<string>();\n\n renderedComponent = walkReactNode(renderedTree, {\n onText: ({ value, key, path }) => {\n const matches = contentValueMap.get(value);\n if (!matches || matches.length === 0) return value;\n\n const match = matches.find((m) => !usedPaths.has(m.contentPath)) ?? matches[0];\n if (!match) return value;\n\n usedPaths.add(match.contentPath);\n const spanKey = key ?? `${block.id}-${match.contentPath}-${path.join('-')}`;\n\n return (\n <span\n key={spanKey}\n data-cms-editable\n data-block-id={block.id}\n data-block-type={block.type}\n data-content-path={match.contentPath}\n >\n {value}\n </span>\n );\n },\n });\n\n needsClientSideSpans = false;\n }\n } catch {\n // Tree walking failed entirely, fall back to client-side injection\n }\n\n // Build content entries for client-side text matching when server-side fails\n const contentEntries = needsClientSideSpans\n ? Array.from(contentValueMap.entries())\n .map(([value, matches]) => ({ v: value, p: matches[0]?.contentPath }))\n .filter((e): e is { v: string; p: string } => !!e.p)\n : [];\n\n return (\n <div\n key={block.id}\n data-cms-block\n data-block-id={block.id}\n data-block-type={block.type}\n style={{ position: 'relative' }}\n >\n <style>{`\n [data-cms-block] {\n position: relative;\n }\n [data-cms-block]:hover {\n outline: 2px solid #3b82f6;\n outline-offset: 4px;\n }\n [data-cms-editable] {\n cursor: pointer;\n border-radius: 2px;\n }\n [data-cms-editable]:hover {\n outline: 2px solid #3b82f6;\n outline-offset: 2px;\n }\n .cms-block-toolbar {\n position: absolute;\n bottom: 8px;\n left: 50%;\n transform: translateX(-50%);\n display: flex;\n gap: 4px;\n background: #1f2937;\n border-radius: 6px;\n padding: 4px;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);\n opacity: 0;\n pointer-events: none;\n transition: opacity 0.15s ease;\n z-index: 1000;\n }\n [data-cms-block]:hover .cms-block-toolbar {\n opacity: 1;\n pointer-events: auto;\n }\n .cms-block-toolbar button {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n border: none;\n background: transparent;\n color: #9ca3af;\n border-radius: 4px;\n cursor: pointer;\n transition: background 0.15s ease, color 0.15s ease;\n }\n .cms-block-toolbar button:hover {\n background: #374151;\n color: #fff;\n }\n .cms-block-toolbar button.delete:hover {\n background: #dc2626;\n color: #fff;\n }\n .cms-block-toolbar button:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n }\n .cms-block-toolbar button:disabled:hover {\n background: transparent;\n color: #9ca3af;\n }\n .cms-block-toolbar svg {\n width: 16px;\n height: 16px;\n }\n `}</style>\n <script\n // biome-ignore lint/security/noDangerouslySetInnerHtml: Inline script for iframe postMessage and client-side text matching\n dangerouslySetInnerHTML={{\n __html: `\n (function() {\n if (!window.__cmsEditableInitialized) {\n window.__cmsEditableInitialized = true;\n\n document.addEventListener('click', function(e) {\n if (e.target.closest('.cms-block-toolbar')) return;\n\n var editableTarget = e.target.closest('[data-cms-editable]');\n if (editableTarget) {\n var message = {\n type: 'cms-editable-click',\n blockId: editableTarget.getAttribute('data-block-id'),\n blockType: editableTarget.getAttribute('data-block-type'),\n contentPath: editableTarget.getAttribute('data-content-path')\n };\n if (window.parent && window.parent !== window) {\n window.parent.postMessage(message, '*');\n }\n return;\n }\n\n var blockTarget = e.target.closest('[data-cms-block]');\n if (blockTarget) {\n var message = {\n type: 'cms-editable-click',\n blockId: blockTarget.getAttribute('data-block-id'),\n blockType: blockTarget.getAttribute('data-block-type'),\n contentPath: null\n };\n if (window.parent && window.parent !== window) {\n window.parent.postMessage(message, '*');\n }\n }\n });\n }\n })();\n ${\n contentEntries.length > 0\n ? `\n (function() {\n var blockId = ${JSON.stringify(block.id)};\n var blockType = ${JSON.stringify(block.type)};\n var entries = ${JSON.stringify(contentEntries)};\n\n function injectSpans() {\n var block = document.querySelector('[data-block-id=\"' + blockId + '\"]');\n if (!block || block.getAttribute('data-cms-spans-injected')) return;\n block.setAttribute('data-cms-spans-injected', 'true');\n\n var used = {};\n var walker = document.createTreeWalker(block, NodeFilter.SHOW_TEXT, null);\n var textNodes = [];\n var n;\n while (n = walker.nextNode()) {\n if (n.parentNode && n.parentNode.closest && n.parentNode.closest('.cms-block-toolbar')) continue;\n if (n.nodeValue && n.nodeValue.trim()) textNodes.push(n);\n }\n\n for (var i = 0; i < textNodes.length; i++) {\n var node = textNodes[i];\n var text = node.nodeValue;\n if (!text) continue;\n\n for (var j = 0; j < entries.length; j++) {\n var e = entries[j];\n if (used[e.p]) continue;\n if (text.indexOf(e.v) !== -1 && text.trim() === e.v.trim()) {\n used[e.p] = true;\n var span = document.createElement('span');\n span.setAttribute('data-cms-editable', '');\n span.setAttribute('data-block-id', blockId);\n span.setAttribute('data-block-type', blockType);\n span.setAttribute('data-content-path', e.p);\n node.parentNode.insertBefore(span, node);\n span.appendChild(node);\n break;\n }\n }\n }\n }\n\n if (document.readyState === 'complete') {\n requestAnimationFrame(injectSpans);\n } else {\n window.addEventListener('load', function() {\n requestAnimationFrame(injectSpans);\n });\n }\n })();\n `\n : ''\n }\n `,\n }}\n />\n {renderedComponent}\n <BlockToolbar key=\"cms-toolbar\" blockId={block.id} />\n </div>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../lib/block-renderer.tsx"],"sourcesContent":["/**\n * Block Renderer Component\n *\n * Dispatches block data to the appropriate component using the ComponentMap pattern.\n * This is the main entry point for rendering blocks from the CMS.\n */\n\nimport React from 'react';\nimport { BlockToolbar } from './block-toolbar';\nimport type { BlockComponentRegistry, BlockData, ResolvedRouteParams } from './types';\n\ntype TextInfo = {\n value: string;\n path: Array<string | number>;\n parentType?: React.ElementType;\n key?: React.Key | null;\n};\n\ntype ElementInfo = {\n element: React.ReactElement;\n path: Array<string | number>;\n};\n\ntype WalkVisitors = {\n /**\n * Called for every string/number child encountered.\n * Return:\n * - same string (or modified)\n * - a ReactNode (e.g. wrap in <span/>)\n */\n onText?: (info: TextInfo) => React.ReactNode;\n\n /**\n * Called for every ReactElement encountered (after children are processed).\n * Return:\n * - same element\n * - a cloned/modified element\n */\n onElement?: (info: ElementInfo) => React.ReactElement;\n};\n\n/**\n * Recursively maps a ReactNode tree, allowing transformations of text nodes and/or elements.\n * SSR-safe: does not touch DOM APIs.\n */\nexport function walkReactNode(\n node: React.ReactNode,\n visitors: WalkVisitors,\n ctx: {\n path?: Array<string | number>;\n parentType?: React.ElementType;\n key?: React.Key | null;\n } = {}\n): React.ReactNode {\n const path = ctx.path ?? [];\n\n // Fast-path primitives\n if (node == null || typeof node === 'boolean') return node;\n\n if (typeof node === 'string' || typeof node === 'number') {\n const value = String(node);\n return visitors.onText\n ? visitors.onText({ value, path, parentType: ctx.parentType, key: ctx.key })\n : node;\n }\n\n // Arrays\n if (Array.isArray(node)) {\n return node.map((child, i) => {\n // biome-ignore lint/suspicious/noExplicitAny: React child key access\n const childKey = (child as any)?.key ?? null;\n const result = walkReactNode(child, visitors, {\n path: [...path, i],\n parentType: ctx.parentType,\n key: childKey,\n });\n // Ensure array children have keys\n if (React.isValidElement(result) && result.key == null) {\n return React.cloneElement(result, { key: childKey ?? `arr-${path.join('-')}-${i}` });\n }\n return result;\n });\n }\n\n // ReactElement (including Fragment)\n if (React.isValidElement(node)) {\n // biome-ignore lint/suspicious/noExplicitAny: React element props access\n const el = node as React.ReactElement<any>;\n const elProps = el.props as Record<string, unknown> | null;\n\n // Recurse into children (if any)\n const hasChildren = elProps && 'children' in elProps;\n const nextChildren = hasChildren\n ? React.Children.map(elProps.children as React.ReactNode, (child, i) => {\n // biome-ignore lint/suspicious/noExplicitAny: React child key access\n const childKey = (child as any)?.key ?? null;\n const result = walkReactNode(child, visitors, {\n path: [...path, 'children', i],\n parentType: el.type as React.ElementType,\n key: childKey,\n });\n // Ensure children have keys\n if (React.isValidElement(result) && result.key == null) {\n return React.cloneElement(result, { key: childKey ?? `child-${path.join('-')}-${i}` });\n }\n return result;\n })\n : (elProps?.children as React.ReactNode);\n\n // Only clone if children changed (or if you want to force a clone)\n const cloned = hasChildren\n ? React.cloneElement(el, undefined, nextChildren as React.ReactNode)\n : el;\n\n return visitors.onElement ? visitors.onElement({ element: cloned, path }) : cloned;\n }\n\n // Functions, symbols, portals, etc. are rare here; return as-is\n return node;\n}\n\n// -----------------------------------------------------------------------------\n// Content Value Extraction\n// -----------------------------------------------------------------------------\n\ntype ContentMatch = {\n contentPath: string;\n value: string;\n};\n\n/**\n * Extracts all string values from a content object with their paths.\n * Returns a Map where keys are string values and values are arrays of content paths.\n */\nfunction extractContentValues(\n content: Record<string, unknown>,\n basePath: string[] = []\n): Map<string, ContentMatch[]> {\n const map = new Map<string, ContentMatch[]>();\n\n function walk(obj: unknown, path: string[]) {\n if (typeof obj === 'string' && obj.trim() !== '') {\n const contentPath = path.join('.');\n const existing = map.get(obj) || [];\n existing.push({ contentPath, value: obj });\n map.set(obj, existing);\n } else if (Array.isArray(obj)) {\n for (let index = 0; index < obj.length; index++) {\n walk(obj[index], [...path, String(index)]);\n }\n } else if (obj && typeof obj === 'object') {\n for (const [key, value] of Object.entries(obj)) {\n walk(value, [...path, key]);\n }\n }\n }\n\n walk(content, basePath);\n return map;\n}\n\n// -----------------------------------------------------------------------------\n// Props\n// -----------------------------------------------------------------------------\n\ninterface BlockRendererProps {\n /**\n * The block data to render.\n * Must have a `type` field that maps to a registered component.\n */\n block: BlockData;\n registry: Partial<BlockComponentRegistry>;\n /**\n * If true, renders the component without any tree walking or editable wrappers.\n */\n disableEditable?: boolean;\n /**\n * Resolved route parameters from parametric routes.\n * Each key is a param name (e.g., \"country\") with its value, schema name, and full document.\n */\n routeParams?: ResolvedRouteParams;\n}\n\n// -----------------------------------------------------------------------------\n// Component\n// -----------------------------------------------------------------------------\n\n/**\n * Recursively renders a React node, invoking function components to get their output.\n * This allows us to walk the full rendered tree, not just the element wrappers.\n */\nfunction renderToWalkableTree(node: React.ReactNode, keyPrefix = ''): React.ReactNode {\n if (node == null || typeof node === 'boolean') return node;\n if (typeof node === 'string' || typeof node === 'number') return node;\n\n if (Array.isArray(node)) {\n return node.map((child, i) => {\n const result = renderToWalkableTree(child, `${keyPrefix}${i}-`);\n // Ensure array children have keys\n if (React.isValidElement(result) && result.key == null) {\n // biome-ignore lint/suspicious/noExplicitAny: Adding key to element\n const existingKey = (child as any)?.key;\n return React.cloneElement(result, { key: existingKey ?? `${keyPrefix}${i}` });\n }\n return result;\n });\n }\n\n if (React.isValidElement(node)) {\n // biome-ignore lint/suspicious/noExplicitAny: React element props access\n const el = node as React.ReactElement<any>;\n const elProps = el.props as Record<string, unknown> | null;\n\n // If it's a function component, invoke it to get the rendered output\n if (typeof el.type === 'function') {\n try {\n // biome-ignore lint/complexity/noBannedTypes: Need to invoke React function component\n const rendered = (el.type as Function)(el.props);\n return renderToWalkableTree(rendered, keyPrefix);\n } catch {\n // If component throws (e.g., uses hooks), return as-is\n return node;\n }\n }\n\n // For host elements (div, span, etc.), recurse into children\n if (elProps && 'children' in elProps) {\n const newChildren = renderToWalkableTree(elProps.children as React.ReactNode, keyPrefix);\n return React.cloneElement(el, undefined, newChildren);\n }\n\n return node;\n }\n\n return node;\n}\n\n/**\n * Renders a single block by dispatching to the appropriate component.\n *\n * Uses the ComponentMap pattern: the block's `type` field determines which\n * component renders the block's `content`.\n *\n * Internally, it:\n * 1. Renders the component tree by invoking function components\n * 2. Extracts all string values from block.content\n * 3. Walks the rendered tree and wraps matching text nodes with spans\n */\nexport function BlockRenderer({\n block,\n registry,\n disableEditable,\n routeParams,\n}: BlockRendererProps) {\n const Component = registry[block.type];\n\n if (!Component) {\n // Log warning in development, render nothing in production\n if (process.env.NODE_ENV === 'development') {\n console.warn(`[BlockRenderer] Unknown block type: ${block.type}`);\n }\n return null;\n }\n\n // biome-ignore lint/suspicious/noExplicitAny: Type safety ensured by BlockData discriminated union\n const component = <Component content={block.content as any} routeParams={routeParams} />;\n\n if (disableEditable) {\n return component;\n }\n\n // Extract all string values from content with their paths\n const contentValueMap = extractContentValues(block.content as Record<string, unknown>);\n\n // Try server-side tree walking for text span injection (best-effort).\n // This works for sync server components but fails for client components,\n // async components, and components using hooks.\n let renderedComponent: React.ReactNode = component;\n let needsClientSideSpans = true;\n\n try {\n const renderedTree = renderToWalkableTree(component);\n\n // Check if tree walking produced host elements (div, section, etc.)\n // vs the original component reference (function type = couldn't be invoked)\n const isWalkable =\n React.isValidElement(renderedTree) &&\n typeof (renderedTree as React.ReactElement<Record<string, unknown>>).type === 'string';\n\n if (isWalkable) {\n const usedPaths = new Set<string>();\n\n renderedComponent = walkReactNode(renderedTree, {\n onText: ({ value, key, path }) => {\n const matches = contentValueMap.get(value);\n if (!matches || matches.length === 0) return value;\n\n const match = matches.find((m) => !usedPaths.has(m.contentPath)) ?? matches[0];\n if (!match) return value;\n\n usedPaths.add(match.contentPath);\n const spanKey = key ?? `${block.id}-${match.contentPath}-${path.join('-')}`;\n\n return (\n <span\n key={spanKey}\n data-cms-editable\n data-block-id={block.id}\n data-block-type={block.type}\n data-content-path={match.contentPath}\n >\n {value}\n </span>\n );\n },\n });\n\n needsClientSideSpans = false;\n }\n } catch {\n // Tree walking failed entirely, fall back to client-side injection\n }\n\n // Build content entries for client-side text matching when server-side fails\n const contentEntries = needsClientSideSpans\n ? Array.from(contentValueMap.entries())\n .map(([value, matches]) => ({ v: value, p: matches[0]?.contentPath }))\n .filter((e): e is { v: string; p: string } => !!e.p)\n : [];\n\n return (\n <div\n key={block.id}\n data-cms-block\n data-block-id={block.id}\n data-block-type={block.type}\n style={{ position: 'relative' }}\n >\n <style>{`\n [data-cms-block] {\n position: relative;\n }\n [data-cms-block]:hover {\n outline: 2px solid #3b82f6;\n outline-offset: 4px;\n }\n [data-cms-editable] {\n cursor: pointer;\n border-radius: 2px;\n }\n [data-cms-editable]:hover {\n outline: 2px solid #3b82f6;\n outline-offset: 2px;\n }\n .cms-block-toolbar {\n position: absolute;\n bottom: 8px;\n left: 50%;\n transform: translateX(-50%);\n display: flex;\n gap: 4px;\n background: #1f2937;\n border-radius: 6px;\n padding: 4px;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);\n opacity: 0;\n pointer-events: none;\n transition: opacity 0.15s ease;\n z-index: 1000;\n }\n [data-cms-block]:hover .cms-block-toolbar {\n opacity: 1;\n pointer-events: auto;\n }\n .cms-block-toolbar button {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n border: none;\n background: transparent;\n color: #9ca3af;\n border-radius: 4px;\n cursor: pointer;\n transition: background 0.15s ease, color 0.15s ease;\n }\n .cms-block-toolbar button:hover {\n background: #374151;\n color: #fff;\n }\n .cms-block-toolbar button.delete:hover {\n background: #dc2626;\n color: #fff;\n }\n .cms-block-toolbar button:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n }\n .cms-block-toolbar button:disabled:hover {\n background: transparent;\n color: #9ca3af;\n }\n .cms-block-toolbar svg {\n width: 16px;\n height: 16px;\n }\n `}</style>\n <script\n // biome-ignore lint/security/noDangerouslySetInnerHtml: Inline script for iframe postMessage and client-side text matching\n dangerouslySetInnerHTML={{\n __html: `\n (function() {\n if (!window.__cmsEditableInitialized) {\n window.__cmsEditableInitialized = true;\n\n document.addEventListener('click', function(e) {\n if (e.target.closest('.cms-block-toolbar')) return;\n\n var editableTarget = e.target.closest('[data-cms-editable]');\n if (editableTarget) {\n var message = {\n type: 'cms-editable-click',\n blockId: editableTarget.getAttribute('data-block-id'),\n blockType: editableTarget.getAttribute('data-block-type'),\n contentPath: editableTarget.getAttribute('data-content-path')\n };\n if (window.parent && window.parent !== window) {\n window.parent.postMessage(message, '*');\n }\n return;\n }\n\n var blockTarget = e.target.closest('[data-cms-block]');\n if (blockTarget) {\n var message = {\n type: 'cms-editable-click',\n blockId: blockTarget.getAttribute('data-block-id'),\n blockType: blockTarget.getAttribute('data-block-type'),\n contentPath: null\n };\n if (window.parent && window.parent !== window) {\n window.parent.postMessage(message, '*');\n }\n }\n });\n }\n })();\n ${\n contentEntries.length > 0\n ? `\n (function() {\n var blockId = ${JSON.stringify(block.id)};\n var blockType = ${JSON.stringify(block.type)};\n var entries = ${JSON.stringify(contentEntries)};\n\n function injectSpans() {\n var block = document.querySelector('[data-block-id=\"' + blockId + '\"]');\n if (!block || block.getAttribute('data-cms-spans-injected')) return;\n block.setAttribute('data-cms-spans-injected', 'true');\n\n var used = {};\n var walker = document.createTreeWalker(block, NodeFilter.SHOW_TEXT, null);\n var textNodes = [];\n var n;\n while (n = walker.nextNode()) {\n if (n.parentNode && n.parentNode.closest && n.parentNode.closest('.cms-block-toolbar')) continue;\n if (n.nodeValue && n.nodeValue.trim()) textNodes.push(n);\n }\n\n for (var i = 0; i < textNodes.length; i++) {\n var node = textNodes[i];\n var text = node.nodeValue;\n if (!text) continue;\n\n for (var j = 0; j < entries.length; j++) {\n var e = entries[j];\n if (used[e.p]) continue;\n if (text.indexOf(e.v) !== -1 && text.trim() === e.v.trim()) {\n used[e.p] = true;\n var span = document.createElement('span');\n span.setAttribute('data-cms-editable', '');\n span.setAttribute('data-block-id', blockId);\n span.setAttribute('data-block-type', blockType);\n span.setAttribute('data-content-path', e.p);\n node.parentNode.insertBefore(span, node);\n span.appendChild(node);\n break;\n }\n }\n }\n }\n\n if (document.readyState === 'complete') {\n requestAnimationFrame(injectSpans);\n } else {\n window.addEventListener('load', function() {\n requestAnimationFrame(injectSpans);\n });\n }\n })();\n `\n : ''\n }\n `,\n }}\n />\n {renderedComponent}\n <BlockToolbar key=\"cms-toolbar\" blockId={block.id} />\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -2,6 +2,7 @@
2
2
  "use client";
3
3
 
4
4
  // lib/block-toolbar.tsx
5
+ import { ChevronDown, ChevronUp, Plus, Trash2 } from "lucide-react";
5
6
  import { jsx, jsxs } from "react/jsx-runtime";
6
7
  function BlockToolbar({ blockId }) {
7
8
  const handleAction = (action) => {
@@ -21,24 +22,13 @@ function BlockToolbar({ blockId }) {
21
22
  {
22
23
  type: "button",
23
24
  title: "Move up",
25
+ "aria-label": "Move up",
24
26
  "data-action": "move-up",
25
27
  onClick: (e) => {
26
28
  e.stopPropagation();
27
29
  handleAction("move-up");
28
30
  },
29
- children: /* @__PURE__ */ jsx(
30
- "svg",
31
- {
32
- xmlns: "http://www.w3.org/2000/svg",
33
- viewBox: "0 0 24 24",
34
- fill: "none",
35
- stroke: "currentColor",
36
- strokeWidth: "2",
37
- strokeLinecap: "round",
38
- strokeLinejoin: "round",
39
- children: /* @__PURE__ */ jsx("path", { d: "m18 15-6-6-6 6" })
40
- }
41
- )
31
+ children: /* @__PURE__ */ jsx(ChevronUp, {})
42
32
  }
43
33
  ),
44
34
  /* @__PURE__ */ jsx(
@@ -46,24 +36,13 @@ function BlockToolbar({ blockId }) {
46
36
  {
47
37
  type: "button",
48
38
  title: "Move down",
39
+ "aria-label": "Move down",
49
40
  "data-action": "move-down",
50
41
  onClick: (e) => {
51
42
  e.stopPropagation();
52
43
  handleAction("move-down");
53
44
  },
54
- children: /* @__PURE__ */ jsx(
55
- "svg",
56
- {
57
- xmlns: "http://www.w3.org/2000/svg",
58
- viewBox: "0 0 24 24",
59
- fill: "none",
60
- stroke: "currentColor",
61
- strokeWidth: "2",
62
- strokeLinecap: "round",
63
- strokeLinejoin: "round",
64
- children: /* @__PURE__ */ jsx("path", { d: "m6 9 6 6 6-6" })
65
- }
66
- )
45
+ children: /* @__PURE__ */ jsx(ChevronDown, {})
67
46
  }
68
47
  ),
69
48
  /* @__PURE__ */ jsx(
@@ -71,27 +50,13 @@ function BlockToolbar({ blockId }) {
71
50
  {
72
51
  type: "button",
73
52
  title: "Add block",
53
+ "aria-label": "Add block",
74
54
  "data-action": "add-block",
75
55
  onClick: (e) => {
76
56
  e.stopPropagation();
77
57
  handleAction("add-block");
78
58
  },
79
- children: /* @__PURE__ */ jsxs(
80
- "svg",
81
- {
82
- xmlns: "http://www.w3.org/2000/svg",
83
- viewBox: "0 0 24 24",
84
- fill: "none",
85
- stroke: "currentColor",
86
- strokeWidth: "2",
87
- strokeLinecap: "round",
88
- strokeLinejoin: "round",
89
- children: [
90
- /* @__PURE__ */ jsx("line", { x1: "12", x2: "12", y1: "5", y2: "19" }),
91
- /* @__PURE__ */ jsx("line", { x1: "5", x2: "19", y1: "12", y2: "12" })
92
- ]
93
- }
94
- )
59
+ children: /* @__PURE__ */ jsx(Plus, {})
95
60
  }
96
61
  ),
97
62
  /* @__PURE__ */ jsx(
@@ -100,30 +65,13 @@ function BlockToolbar({ blockId }) {
100
65
  type: "button",
101
66
  className: "delete",
102
67
  title: "Delete block",
68
+ "aria-label": "Delete block",
103
69
  "data-action": "delete",
104
70
  onClick: (e) => {
105
71
  e.stopPropagation();
106
72
  handleAction("delete");
107
73
  },
108
- children: /* @__PURE__ */ jsxs(
109
- "svg",
110
- {
111
- xmlns: "http://www.w3.org/2000/svg",
112
- viewBox: "0 0 24 24",
113
- fill: "none",
114
- stroke: "currentColor",
115
- strokeWidth: "2",
116
- strokeLinecap: "round",
117
- strokeLinejoin: "round",
118
- children: [
119
- /* @__PURE__ */ jsx("path", { d: "M3 6h18" }),
120
- /* @__PURE__ */ jsx("path", { d: "M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" }),
121
- /* @__PURE__ */ jsx("path", { d: "M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" }),
122
- /* @__PURE__ */ jsx("line", { x1: "10", x2: "10", y1: "11", y2: "17" }),
123
- /* @__PURE__ */ jsx("line", { x1: "14", x2: "14", y1: "11", y2: "17" })
124
- ]
125
- }
126
- )
74
+ children: /* @__PURE__ */ jsx(Trash2, {})
127
75
  }
128
76
  )
129
77
  ]
@@ -1 +1 @@
1
- {"version":3,"sources":["../../lib/block-toolbar.tsx"],"sourcesContent":["'use client';\n\n/**\n * Block Toolbar Component\n *\n * Provides move up/down and delete controls for blocks in edit mode.\n * This is a Client Component because it requires onClick handlers.\n */\n\nexport function BlockToolbar({ blockId }: { blockId: string }) {\n const handleAction = (action: string) => {\n if (typeof window !== 'undefined' && window.parent && window.parent !== window) {\n window.parent.postMessage({ type: 'cms-block-action', action, blockId }, '*');\n }\n };\n\n return (\n <div\n className=\"cms-block-toolbar\"\n data-cms-toolbar=\"true\"\n style={{ position: 'absolute', zIndex: 9999 }}\n >\n <button\n type=\"button\"\n title=\"Move up\"\n data-action=\"move-up\"\n onClick={(e) => {\n e.stopPropagation();\n handleAction('move-up');\n }}\n >\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <path d=\"m18 15-6-6-6 6\" />\n </svg>\n </button>\n <button\n type=\"button\"\n title=\"Move down\"\n data-action=\"move-down\"\n onClick={(e) => {\n e.stopPropagation();\n handleAction('move-down');\n }}\n >\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <path d=\"m6 9 6 6 6-6\" />\n </svg>\n </button>\n <button\n type=\"button\"\n title=\"Add block\"\n data-action=\"add-block\"\n onClick={(e) => {\n e.stopPropagation();\n handleAction('add-block');\n }}\n >\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <line x1=\"12\" x2=\"12\" y1=\"5\" y2=\"19\" />\n <line x1=\"5\" x2=\"19\" y1=\"12\" y2=\"12\" />\n </svg>\n </button>\n <button\n type=\"button\"\n className=\"delete\"\n title=\"Delete block\"\n data-action=\"delete\"\n onClick={(e) => {\n e.stopPropagation();\n handleAction('delete');\n }}\n >\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <path d=\"M3 6h18\" />\n <path d=\"M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6\" />\n <path d=\"M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2\" />\n <line x1=\"10\" x2=\"10\" y1=\"11\" y2=\"17\" />\n <line x1=\"14\" x2=\"14\" y1=\"11\" y2=\"17\" />\n </svg>\n </button>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../lib/block-toolbar.tsx"],"sourcesContent":["'use client';\n\nimport { ChevronDown, ChevronUp, Plus, Trash2 } from 'lucide-react';\n\n/**\n * Block Toolbar Component\n *\n * Provides move up/down and delete controls for blocks in edit mode.\n * This is a Client Component because it requires onClick handlers.\n */\n\nexport function BlockToolbar({ blockId }: { blockId: string }) {\n const handleAction = (action: string) => {\n if (typeof window !== 'undefined' && window.parent && window.parent !== window) {\n window.parent.postMessage({ type: 'cms-block-action', action, blockId }, '*');\n }\n };\n\n return (\n <div\n className=\"cms-block-toolbar\"\n data-cms-toolbar=\"true\"\n style={{ position: 'absolute', zIndex: 9999 }}\n >\n <button\n type=\"button\"\n title=\"Move up\"\n aria-label=\"Move up\"\n data-action=\"move-up\"\n onClick={(e) => {\n e.stopPropagation();\n handleAction('move-up');\n }}\n >\n <ChevronUp />\n </button>\n <button\n type=\"button\"\n title=\"Move down\"\n aria-label=\"Move down\"\n data-action=\"move-down\"\n onClick={(e) => {\n e.stopPropagation();\n handleAction('move-down');\n }}\n >\n <ChevronDown />\n </button>\n <button\n type=\"button\"\n title=\"Add block\"\n aria-label=\"Add block\"\n data-action=\"add-block\"\n onClick={(e) => {\n e.stopPropagation();\n handleAction('add-block');\n }}\n >\n <Plus />\n </button>\n <button\n type=\"button\"\n className=\"delete\"\n title=\"Delete block\"\n aria-label=\"Delete block\"\n data-action=\"delete\"\n onClick={(e) => {\n e.stopPropagation();\n handleAction('delete');\n }}\n >\n <Trash2 />\n </button>\n </div>\n );\n}\n"],"mappings":";;;;AAEA,SAAS,aAAa,WAAW,MAAM,cAAc;AAiBjD,SAeI,KAfJ;AARG,SAAS,aAAa,EAAE,QAAQ,GAAwB;AAC7D,QAAM,eAAe,CAAC,WAAmB;AACvC,QAAI,OAAO,WAAW,eAAe,OAAO,UAAU,OAAO,WAAW,QAAQ;AAC9E,aAAO,OAAO,YAAY,EAAE,MAAM,oBAAoB,QAAQ,QAAQ,GAAG,GAAG;AAAA,IAC9E;AAAA,EACF;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAU;AAAA,MACV,oBAAiB;AAAA,MACjB,OAAO,EAAE,UAAU,YAAY,QAAQ,KAAK;AAAA,MAE5C;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,OAAM;AAAA,YACN,cAAW;AAAA,YACX,eAAY;AAAA,YACZ,SAAS,CAAC,MAAM;AACd,gBAAE,gBAAgB;AAClB,2BAAa,SAAS;AAAA,YACxB;AAAA,YAEA,8BAAC,aAAU;AAAA;AAAA,QACb;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,OAAM;AAAA,YACN,cAAW;AAAA,YACX,eAAY;AAAA,YACZ,SAAS,CAAC,MAAM;AACd,gBAAE,gBAAgB;AAClB,2BAAa,WAAW;AAAA,YAC1B;AAAA,YAEA,8BAAC,eAAY;AAAA;AAAA,QACf;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,OAAM;AAAA,YACN,cAAW;AAAA,YACX,eAAY;AAAA,YACZ,SAAS,CAAC,MAAM;AACd,gBAAE,gBAAgB;AAClB,2BAAa,WAAW;AAAA,YAC1B;AAAA,YAEA,8BAAC,QAAK;AAAA;AAAA,QACR;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,WAAU;AAAA,YACV,OAAM;AAAA,YACN,cAAW;AAAA,YACX,eAAY;AAAA,YACZ,SAAS,CAAC,MAAM;AACd,gBAAE,gBAAgB;AAClB,2BAAa,QAAQ;AAAA,YACvB;AAAA,YAEA,8BAAC,UAAO;AAAA;AAAA,QACV;AAAA;AAAA;AAAA,EACF;AAEJ;","names":[]}
@@ -10,16 +10,18 @@ import { CreateTRPCClient } from '@trpc/client';
10
10
 
11
11
  /** Type alias for the CMS API client */
12
12
  type CmsClient = CreateTRPCClient<AppRouter>;
13
- /** Options for creating a CMS client */
14
- interface CmsClientOptions {
13
+ /** Unified configuration for all CMS API access */
14
+ interface CmsConfig {
15
15
  /** CMS API base URL (e.g., 'http://localhost:3000') */
16
16
  cmsUrl: string;
17
- /** API key for authentication (passed as query parameter) */
17
+ /** API key for authentication */
18
18
  apiKey?: string;
19
+ /** Website ID to scope API requests */
20
+ websiteId?: string;
19
21
  }
20
22
  /**
21
23
  * Get a CMS client for the specified CMS URL.
22
24
  */
23
- declare function getCmsClient(options: CmsClientOptions): CmsClient;
25
+ declare function getCmsClient(options: CmsConfig): CmsClient;
24
26
 
25
- export { type CmsClientOptions, getCmsClient };
27
+ export { type CmsConfig, getCmsClient };
@@ -2,14 +2,19 @@
2
2
  import { createTRPCClient, httpBatchLink } from "@trpc/client";
3
3
  import superjson from "superjson";
4
4
  function getCmsApiUrl(cmsUrl) {
5
- return `${cmsUrl}/api/trpc`;
5
+ return new URL("/api/trpc", cmsUrl).toString();
6
6
  }
7
- function createFetchWithApiKey(apiKey) {
7
+ function createFetchWithApiKey(apiKey, websiteId) {
8
8
  return async (url, options) => {
9
9
  let finalUrl = url;
10
+ const urlObj = new URL(url.toString());
10
11
  if (apiKey) {
11
- const urlObj = new URL(url.toString());
12
12
  urlObj.searchParams.set("api_key", apiKey);
13
+ }
14
+ if (websiteId) {
15
+ urlObj.searchParams.set("website_id", websiteId);
16
+ }
17
+ if (apiKey || websiteId) {
13
18
  finalUrl = urlObj.toString();
14
19
  }
15
20
  const response = await fetch(finalUrl, options);
@@ -18,13 +23,12 @@ function createFetchWithApiKey(apiKey) {
18
23
  }
19
24
  function createCmsClient(options) {
20
25
  const url = getCmsApiUrl(options.cmsUrl);
21
- console.log("[CMS API] Creating client with URL:", url);
22
26
  return createTRPCClient({
23
27
  links: [
24
28
  httpBatchLink({
25
29
  url,
26
30
  transformer: superjson,
27
- fetch: createFetchWithApiKey(options.apiKey)
31
+ fetch: createFetchWithApiKey(options.apiKey, options.websiteId)
28
32
  })
29
33
  ]
30
34
  });
@@ -1 +1 @@
1
- {"version":3,"sources":["../../lib/cms-api.ts"],"sourcesContent":["/**\n * CMS API Client\n *\n * Creates an HTTP-based tRPC client for calling the CMS API.\n * Used in Server Components to fetch routes and blocks from the CMS.\n */\n\nimport type { AppRouter } from '@repo/cms-schema/trpc';\nimport { type CreateTRPCClient, createTRPCClient, httpBatchLink } from '@trpc/client';\nimport superjson from 'superjson';\n\n/** Type alias for the CMS API client */\ntype CmsClient = CreateTRPCClient<AppRouter>;\n\n/**\n * Get the CMS API URL from the provided base URL.\n */\nfunction getCmsApiUrl(cmsUrl: string): string {\n return `${cmsUrl}/api/trpc`;\n}\n\n/** Options for creating a CMS client */\nexport interface CmsClientOptions {\n /** CMS API base URL (e.g., 'http://localhost:3000') */\n cmsUrl: string;\n /** API key for authentication (passed as query parameter) */\n apiKey?: string;\n}\n\n/**\n * Create a custom fetch function that appends API key as query parameter.\n */\nfunction createFetchWithApiKey(apiKey?: string) {\n return async (url: URL | RequestInfo, options?: RequestInit): Promise<Response> => {\n let finalUrl = url;\n\n // Append api_key to URL if provided\n if (apiKey) {\n const urlObj = new URL(url.toString());\n urlObj.searchParams.set('api_key', apiKey);\n finalUrl = urlObj.toString();\n }\n\n const response = await fetch(finalUrl, options);\n\n return response;\n };\n}\n\n/**\n * Create a tRPC client for the CMS API.\n */\nfunction createCmsClient(options: CmsClientOptions): CmsClient {\n const url = getCmsApiUrl(options.cmsUrl);\n console.log('[CMS API] Creating client with URL:', url);\n\n return createTRPCClient<AppRouter>({\n links: [\n httpBatchLink({\n url,\n transformer: superjson,\n fetch: createFetchWithApiKey(options.apiKey),\n }),\n ],\n });\n}\n\n/**\n * Get a CMS client for the specified CMS URL.\n */\nexport function getCmsClient(options: CmsClientOptions): CmsClient {\n return createCmsClient(options);\n}\n"],"mappings":";AAQA,SAAgC,kBAAkB,qBAAqB;AACvE,OAAO,eAAe;AAQtB,SAAS,aAAa,QAAwB;AAC5C,SAAO,GAAG,MAAM;AAClB;AAaA,SAAS,sBAAsB,QAAiB;AAC9C,SAAO,OAAO,KAAwB,YAA6C;AACjF,QAAI,WAAW;AAGf,QAAI,QAAQ;AACV,YAAM,SAAS,IAAI,IAAI,IAAI,SAAS,CAAC;AACrC,aAAO,aAAa,IAAI,WAAW,MAAM;AACzC,iBAAW,OAAO,SAAS;AAAA,IAC7B;AAEA,UAAM,WAAW,MAAM,MAAM,UAAU,OAAO;AAE9C,WAAO;AAAA,EACT;AACF;AAKA,SAAS,gBAAgB,SAAsC;AAC7D,QAAM,MAAM,aAAa,QAAQ,MAAM;AACvC,UAAQ,IAAI,uCAAuC,GAAG;AAEtD,SAAO,iBAA4B;AAAA,IACjC,OAAO;AAAA,MACL,cAAc;AAAA,QACZ;AAAA,QACA,aAAa;AAAA,QACb,OAAO,sBAAsB,QAAQ,MAAM;AAAA,MAC7C,CAAC;AAAA,IACH;AAAA,EACF,CAAC;AACH;AAKO,SAAS,aAAa,SAAsC;AACjE,SAAO,gBAAgB,OAAO;AAChC;","names":[]}
1
+ {"version":3,"sources":["../../lib/cms-api.ts"],"sourcesContent":["/**\n * CMS API Client\n *\n * Creates an HTTP-based tRPC client for calling the CMS API.\n * Used in Server Components to fetch routes and blocks from the CMS.\n */\n\nimport type { AppRouter } from '@repo/cms-schema/trpc';\nimport { type CreateTRPCClient, createTRPCClient, httpBatchLink } from '@trpc/client';\nimport superjson from 'superjson';\n\n/** Type alias for the CMS API client */\ntype CmsClient = CreateTRPCClient<AppRouter>;\n\n/**\n * Get the CMS API URL from the provided base URL.\n */\nfunction getCmsApiUrl(cmsUrl: string): string {\n return new URL('/api/trpc', cmsUrl).toString();\n}\n\n/** Unified configuration for all CMS API access */\nexport interface CmsConfig {\n /** CMS API base URL (e.g., 'http://localhost:3000') */\n cmsUrl: string;\n /** API key for authentication */\n apiKey?: string;\n /** Website ID to scope API requests */\n websiteId?: string;\n}\n\n/**\n * Create a custom fetch function that appends API key as query parameter.\n */\nfunction createFetchWithApiKey(apiKey?: string, websiteId?: string) {\n return async (url: URL | RequestInfo, options?: RequestInit): Promise<Response> => {\n let finalUrl = url;\n\n const urlObj = new URL(url.toString());\n if (apiKey) {\n urlObj.searchParams.set('api_key', apiKey);\n }\n if (websiteId) {\n urlObj.searchParams.set('website_id', websiteId);\n }\n if (apiKey || websiteId) {\n finalUrl = urlObj.toString();\n }\n\n\n\n const response = await fetch(finalUrl, options);\n\n return response;\n };\n}\n\n/**\n * Create a tRPC client for the CMS API.\n */\nfunction createCmsClient(options: CmsConfig): CmsClient {\n const url = getCmsApiUrl(options.cmsUrl);\n\n return createTRPCClient<AppRouter>({\n links: [\n httpBatchLink({\n url,\n transformer: superjson,\n fetch: createFetchWithApiKey(options.apiKey, options.websiteId),\n }),\n ],\n });\n}\n\n/**\n * Get a CMS client for the specified CMS URL.\n */\nexport function getCmsClient(options: CmsConfig): CmsClient {\n return createCmsClient(options);\n}\n"],"mappings":";AAQA,SAAgC,kBAAkB,qBAAqB;AACvE,OAAO,eAAe;AAQtB,SAAS,aAAa,QAAwB;AAC5C,SAAO,IAAI,IAAI,aAAa,MAAM,EAAE,SAAS;AAC/C;AAeA,SAAS,sBAAsB,QAAiB,WAAoB;AAClE,SAAO,OAAO,KAAwB,YAA6C;AACjF,QAAI,WAAW;AAEf,UAAM,SAAS,IAAI,IAAI,IAAI,SAAS,CAAC;AACrC,QAAI,QAAQ;AACV,aAAO,aAAa,IAAI,WAAW,MAAM;AAAA,IAC3C;AACA,QAAI,WAAW;AACb,aAAO,aAAa,IAAI,cAAc,SAAS;AAAA,IACjD;AACA,QAAI,UAAU,WAAW;AACvB,iBAAW,OAAO,SAAS;AAAA,IAC7B;AAIA,UAAM,WAAW,MAAM,MAAM,UAAU,OAAO;AAE9C,WAAO;AAAA,EACT;AACF;AAKA,SAAS,gBAAgB,SAA+B;AACtD,QAAM,MAAM,aAAa,QAAQ,MAAM;AAEvC,SAAO,iBAA4B;AAAA,IACjC,OAAO;AAAA,MACL,cAAc;AAAA,QACZ;AAAA,QACA,aAAa;AAAA,QACb,OAAO,sBAAsB,QAAQ,QAAQ,QAAQ,SAAS;AAAA,MAChE,CAAC;AAAA,IACH;AAAA,EACF,CAAC;AACH;AAKO,SAAS,aAAa,SAA+B;AAC1D,SAAO,gBAAgB,OAAO;AAChC;","names":[]}
@@ -1,22 +1,19 @@
1
1
  import * as React from 'react';
2
2
  import { Metadata } from 'next';
3
+ import { CmsConfig } from './cms-api.js';
3
4
  import { BlockComponentRegistry } from './types.js';
5
+ import '@repo/cms-schema/trpc';
6
+ import '@trpc/client';
4
7
  import '@repo/cms-schema/blocks';
5
8
 
6
- type PageProps = {
9
+ type PageProps = CmsConfig & {
7
10
  params: Promise<{
8
11
  slug: string[];
9
12
  }>;
10
13
  searchParams?: Promise<{
11
14
  [key: string]: string | string[] | undefined;
12
15
  }>;
13
- /** CMS API base URL (e.g., 'http://localhost:3000') */
14
- cmsUrl: string;
15
16
  registry?: Partial<BlockComponentRegistry>;
16
- /** API key for CMS API authentication */
17
- apiKey?: string;
18
- /** Website ID (required if not using env variables) */
19
- websiteId?: string;
20
17
  };
21
18
  /**
22
19
  * Force dynamic rendering to ensure routes are always fresh.
@@ -341,7 +341,12 @@ function renderToWalkableTree(node, keyPrefix = "") {
341
341
  }
342
342
  return node;
343
343
  }
344
- function BlockRenderer({ block, registry, disableEditable }) {
344
+ function BlockRenderer({
345
+ block,
346
+ registry,
347
+ disableEditable,
348
+ routeParams
349
+ }) {
345
350
  const Component = registry[block.type];
346
351
  if (!Component) {
347
352
  if (process.env.NODE_ENV === "development") {
@@ -349,7 +354,7 @@ function BlockRenderer({ block, registry, disableEditable }) {
349
354
  }
350
355
  return null;
351
356
  }
352
- const component = /* @__PURE__ */ jsx(Component, { content: block.content });
357
+ const component = /* @__PURE__ */ jsx(Component, { content: block.content, routeParams });
353
358
  if (disableEditable) {
354
359
  return component;
355
360
  }
@@ -574,14 +579,19 @@ function BlockRenderer({ block, registry, disableEditable }) {
574
579
  import { createTRPCClient, httpBatchLink } from "@trpc/client";
575
580
  import superjson from "superjson";
576
581
  function getCmsApiUrl(cmsUrl) {
577
- return `${cmsUrl}/api/trpc`;
582
+ return new URL("/api/trpc", cmsUrl).toString();
578
583
  }
579
- function createFetchWithApiKey(apiKey) {
584
+ function createFetchWithApiKey(apiKey, websiteId) {
580
585
  return async (url, options) => {
581
586
  let finalUrl = url;
587
+ const urlObj = new URL(url.toString());
582
588
  if (apiKey) {
583
- const urlObj = new URL(url.toString());
584
589
  urlObj.searchParams.set("api_key", apiKey);
590
+ }
591
+ if (websiteId) {
592
+ urlObj.searchParams.set("website_id", websiteId);
593
+ }
594
+ if (apiKey || websiteId) {
585
595
  finalUrl = urlObj.toString();
586
596
  }
587
597
  const response = await fetch(finalUrl, options);
@@ -590,13 +600,12 @@ function createFetchWithApiKey(apiKey) {
590
600
  }
591
601
  function createCmsClient(options) {
592
602
  const url = getCmsApiUrl(options.cmsUrl);
593
- console.log("[CMS API] Creating client with URL:", url);
594
603
  return createTRPCClient({
595
604
  links: [
596
605
  httpBatchLink({
597
606
  url,
598
607
  transformer: superjson,
599
- fetch: createFetchWithApiKey(options.apiKey)
608
+ fetch: createFetchWithApiKey(options.apiKey, options.websiteId)
600
609
  })
601
610
  ]
602
611
  });
@@ -652,7 +661,7 @@ async function ParametricRoutePage({
652
661
  const path = normalizePath(rawPath);
653
662
  const client = getCmsClient({ apiKey, cmsUrl });
654
663
  try {
655
- const { route } = await client.route.getByPath.query({ websiteId, path });
664
+ const { route, resolvedParams } = await client.route.getByPath.query({ websiteId, path });
656
665
  if (route.state !== "Live") {
657
666
  console.error(`Route found but not Live. Path: ${path}, State: ${route.state}`);
658
667
  notFound();
@@ -709,12 +718,28 @@ async function ParametricRoutePage({
709
718
  content
710
719
  });
711
720
  }
721
+ const routeParams = resolvedParams ? Object.fromEntries(
722
+ Object.entries(resolvedParams).map(([key, param]) => [
723
+ key,
724
+ {
725
+ value: param.value,
726
+ schemaName: param.schemaName,
727
+ document: {
728
+ id: param.document.id,
729
+ title: param.document.title,
730
+ content: param.document.content,
731
+ schema_name: param.document.schema_name
732
+ }
733
+ }
734
+ ])
735
+ ) : void 0;
712
736
  return /* @__PURE__ */ jsx2("main", { children: blocks.map((block) => /* @__PURE__ */ jsx2(
713
737
  BlockRenderer,
714
738
  {
715
739
  block,
716
740
  registry: registry ?? {},
717
- disableEditable: !editMode
741
+ disableEditable: !editMode,
742
+ routeParams
718
743
  },
719
744
  block.id
720
745
  )) });