cms-renderer 0.1.0 → 0.1.2
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/{chunk-G22U6UHQ.js → chunk-JHKDRASN.js} +4 -10
- package/dist/chunk-JHKDRASN.js.map +1 -0
- package/dist/chunk-SJZTIW2I.js +290 -0
- package/dist/chunk-SJZTIW2I.js.map +1 -0
- package/dist/chunk-TIR3RJMY.js +98 -0
- package/dist/chunk-TIR3RJMY.js.map +1 -0
- package/dist/lib/block-renderer.d.ts +53 -13
- package/dist/lib/block-renderer.js +6 -3
- package/dist/lib/block-toolbar.d.ts +13 -0
- package/dist/lib/block-toolbar.js +8 -0
- package/dist/lib/block-toolbar.js.map +1 -0
- package/dist/lib/cms-api.d.ts +4 -3
- package/dist/lib/cms-api.js +1 -1
- package/dist/lib/proxy.d.ts +52 -0
- package/dist/lib/proxy.js +143 -0
- package/dist/lib/proxy.js.map +1 -0
- package/dist/lib/renderer.d.ts +10 -3
- package/dist/lib/renderer.js +81 -13
- package/dist/lib/renderer.js.map +1 -1
- package/package.json +9 -1
- package/dist/chunk-G22U6UHQ.js.map +0 -1
- package/dist/chunk-RPM73PQZ.js +0 -17
- package/dist/chunk-RPM73PQZ.js.map +0 -1
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Configuration options for the CMS proxy.
|
|
5
|
+
*/
|
|
6
|
+
interface ProxyConfig {
|
|
7
|
+
/**
|
|
8
|
+
* The upstream CMS server URL (e.g., 'https://cms.example.com').
|
|
9
|
+
* Defaults to ADMIN_UPSTREAM_ORIGIN environment variable.
|
|
10
|
+
*/
|
|
11
|
+
upstream?: string;
|
|
12
|
+
/**
|
|
13
|
+
* Additional path prefixes to proxy (beyond /admin, /api, /auth).
|
|
14
|
+
*/
|
|
15
|
+
additionalPaths?: string[];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Creates a proxy middleware function for Next.js.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* // middleware.ts
|
|
23
|
+
* import { createCmsProxy } from 'cms-renderer/lib/proxy';
|
|
24
|
+
*
|
|
25
|
+
* const cmsProxy = createCmsProxy({
|
|
26
|
+
* upstream: process.env.ADMIN_UPSTREAM_ORIGIN,
|
|
27
|
+
* });
|
|
28
|
+
*
|
|
29
|
+
* export async function middleware(request: NextRequest) {
|
|
30
|
+
* return cmsProxy(request);
|
|
31
|
+
* }
|
|
32
|
+
*
|
|
33
|
+
* export const config = {
|
|
34
|
+
* matcher: cmsProxyMatcher,
|
|
35
|
+
* };
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
declare function createCmsProxy(config?: ProxyConfig): (request: NextRequest) => Promise<NextResponse>;
|
|
39
|
+
/**
|
|
40
|
+
* Default matcher configuration for the CMS proxy middleware.
|
|
41
|
+
* Use this in your middleware.ts config export.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```ts
|
|
45
|
+
* export const config = {
|
|
46
|
+
* matcher: cmsProxyMatcher,
|
|
47
|
+
* };
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
declare const cmsProxyMatcher: string[];
|
|
51
|
+
|
|
52
|
+
export { type ProxyConfig, cmsProxyMatcher, createCmsProxy };
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// lib/proxy.ts
|
|
2
|
+
import { NextResponse } from "next/server";
|
|
3
|
+
var STATIC_FILE_REGEX = /\.(css|js|map|png|jpg|jpeg|gif|svg|ico|webp|avif|woff|woff2|ttf|eot|txt|xml)$/;
|
|
4
|
+
function isFromAdminPage(request) {
|
|
5
|
+
const referer = request.headers.get("referer");
|
|
6
|
+
if (!referer) return false;
|
|
7
|
+
try {
|
|
8
|
+
const refererUrl = new URL(referer);
|
|
9
|
+
return refererUrl.pathname.startsWith("/admin");
|
|
10
|
+
} catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
async function proxyToUpstream(request, pathname, upstream) {
|
|
15
|
+
const upstreamUrl = new URL(pathname, upstream);
|
|
16
|
+
upstreamUrl.search = request.nextUrl.search;
|
|
17
|
+
const headers = new Headers(request.headers);
|
|
18
|
+
headers.set("x-forwarded-host", request.headers.get("host") ?? "");
|
|
19
|
+
headers.set("x-forwarded-proto", request.nextUrl.protocol.replace(":", ""));
|
|
20
|
+
headers.set("x-forwarded-for", request.headers.get("x-forwarded-for") ?? "");
|
|
21
|
+
const response = await fetch(upstreamUrl.toString(), {
|
|
22
|
+
method: request.method,
|
|
23
|
+
headers,
|
|
24
|
+
body: request.body,
|
|
25
|
+
// @ts-expect-error - duplex is required for streaming bodies
|
|
26
|
+
duplex: "half",
|
|
27
|
+
redirect: "manual"
|
|
28
|
+
// Don't follow redirects, let the client handle them
|
|
29
|
+
});
|
|
30
|
+
const responseHeaders = new Headers();
|
|
31
|
+
const upstreamUrlObj = new URL(upstream);
|
|
32
|
+
const upstreamOrigin = upstreamUrlObj.origin;
|
|
33
|
+
const currentOrigin = request.nextUrl.origin;
|
|
34
|
+
response.headers.forEach((value, key) => {
|
|
35
|
+
const lowerKey = key.toLowerCase();
|
|
36
|
+
if (lowerKey === "set-cookie") {
|
|
37
|
+
let modifiedCookie = value;
|
|
38
|
+
modifiedCookie = modifiedCookie.replace(/;\s*Domain=[^;]*/gi, "");
|
|
39
|
+
if (!/;\s*Path=/i.test(modifiedCookie)) {
|
|
40
|
+
modifiedCookie += "; Path=/";
|
|
41
|
+
}
|
|
42
|
+
if (!/;\s*SameSite=/i.test(modifiedCookie)) {
|
|
43
|
+
modifiedCookie += "; SameSite=Lax";
|
|
44
|
+
}
|
|
45
|
+
responseHeaders.append(key, modifiedCookie);
|
|
46
|
+
} else if (lowerKey === "location") {
|
|
47
|
+
try {
|
|
48
|
+
const locationUrl = new URL(value, upstream);
|
|
49
|
+
if (locationUrl.origin === upstreamOrigin) {
|
|
50
|
+
const newLocation = `${currentOrigin}${locationUrl.pathname}${locationUrl.search}`;
|
|
51
|
+
responseHeaders.set(key, newLocation);
|
|
52
|
+
} else {
|
|
53
|
+
let finalLocation = value;
|
|
54
|
+
const redirectUri = locationUrl.searchParams.get("redirect_uri");
|
|
55
|
+
if (redirectUri) {
|
|
56
|
+
try {
|
|
57
|
+
const redirectUriUrl = new URL(redirectUri);
|
|
58
|
+
if (redirectUriUrl.host === upstreamUrlObj.host) {
|
|
59
|
+
const newRedirectUri = `${currentOrigin}${redirectUriUrl.pathname}${redirectUriUrl.search}`;
|
|
60
|
+
locationUrl.searchParams.set("redirect_uri", newRedirectUri);
|
|
61
|
+
finalLocation = locationUrl.toString();
|
|
62
|
+
}
|
|
63
|
+
} catch {
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
responseHeaders.set(key, finalLocation);
|
|
67
|
+
}
|
|
68
|
+
} catch {
|
|
69
|
+
responseHeaders.set(key, value);
|
|
70
|
+
}
|
|
71
|
+
} else if (lowerKey !== "transfer-encoding" && lowerKey !== "content-encoding" && lowerKey !== "content-length") {
|
|
72
|
+
responseHeaders.set(key, value);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
responseHeaders.set("x-proxied-by", "cms-proxy");
|
|
76
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
77
|
+
if (contentType.includes("text/html") && response.body) {
|
|
78
|
+
let text = await response.text();
|
|
79
|
+
const upstreamHost = upstreamUrlObj.host;
|
|
80
|
+
text = text.replaceAll(upstreamOrigin, currentOrigin);
|
|
81
|
+
text = text.replaceAll(`//${upstreamHost}`, `//${request.nextUrl.host}`);
|
|
82
|
+
text = text.replaceAll(`https://${upstreamHost}`, currentOrigin);
|
|
83
|
+
text = text.replaceAll(`http://${upstreamHost}`, currentOrigin);
|
|
84
|
+
return new NextResponse(text, {
|
|
85
|
+
status: response.status,
|
|
86
|
+
statusText: response.statusText,
|
|
87
|
+
headers: responseHeaders
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return new NextResponse(response.body, {
|
|
91
|
+
status: response.status,
|
|
92
|
+
statusText: response.statusText,
|
|
93
|
+
headers: responseHeaders
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
function createCmsProxy(config = {}) {
|
|
97
|
+
const upstream = (config.upstream ?? process.env.ADMIN_UPSTREAM_ORIGIN ?? "").replace(/\/$/, "");
|
|
98
|
+
const additionalPaths = config.additionalPaths ?? [];
|
|
99
|
+
if (!upstream) {
|
|
100
|
+
console.warn(
|
|
101
|
+
"[cms-proxy] No upstream URL configured. Set ADMIN_UPSTREAM_ORIGIN or pass upstream option."
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
return async function cmsProxy(request) {
|
|
105
|
+
if (!upstream) {
|
|
106
|
+
return NextResponse.next();
|
|
107
|
+
}
|
|
108
|
+
const { pathname } = request.nextUrl;
|
|
109
|
+
if (pathname.startsWith("/admin")) {
|
|
110
|
+
return proxyToUpstream(request, pathname, upstream);
|
|
111
|
+
}
|
|
112
|
+
if (pathname.startsWith("/api")) {
|
|
113
|
+
return proxyToUpstream(request, pathname, upstream);
|
|
114
|
+
}
|
|
115
|
+
if (pathname.startsWith("/auth")) {
|
|
116
|
+
return proxyToUpstream(request, pathname, upstream);
|
|
117
|
+
}
|
|
118
|
+
for (const pathPrefix of additionalPaths) {
|
|
119
|
+
if (pathname.startsWith(pathPrefix)) {
|
|
120
|
+
return proxyToUpstream(request, pathname, upstream);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (isFromAdminPage(request)) {
|
|
124
|
+
if (pathname.startsWith("/_next") || STATIC_FILE_REGEX.test(pathname)) {
|
|
125
|
+
return proxyToUpstream(request, pathname, upstream);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return NextResponse.next();
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
var cmsProxyMatcher = [
|
|
132
|
+
"/admin",
|
|
133
|
+
"/admin/:path*",
|
|
134
|
+
"/api/:path*",
|
|
135
|
+
"/auth/:path*",
|
|
136
|
+
"/_next/:path*",
|
|
137
|
+
"/((?:.*\\.(?:css|js|map|png|jpg|jpeg|gif|svg|ico|webp|avif|woff|woff2|ttf|eot|txt|xml))$)"
|
|
138
|
+
];
|
|
139
|
+
export {
|
|
140
|
+
cmsProxyMatcher,
|
|
141
|
+
createCmsProxy
|
|
142
|
+
};
|
|
143
|
+
//# sourceMappingURL=proxy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../lib/proxy.ts"],"sourcesContent":["import { type NextRequest, NextResponse } from 'next/server';\n\n/**\n * Configuration options for the CMS proxy.\n */\nexport interface ProxyConfig {\n /**\n * The upstream CMS server URL (e.g., 'https://cms.example.com').\n * Defaults to ADMIN_UPSTREAM_ORIGIN environment variable.\n */\n upstream?: string;\n /**\n * Additional path prefixes to proxy (beyond /admin, /api, /auth).\n */\n additionalPaths?: string[];\n}\n\n// Static file extensions to proxy to upstream\nconst STATIC_FILE_REGEX =\n /\\.(css|js|map|png|jpg|jpeg|gif|svg|ico|webp|avif|woff|woff2|ttf|eot|txt|xml)$/;\n\n/**\n * Check if the request originates from an admin page (via Referer header).\n */\nfunction isFromAdminPage(request: NextRequest): boolean {\n const referer = request.headers.get('referer');\n if (!referer) return false;\n\n try {\n const refererUrl = new URL(referer);\n return refererUrl.pathname.startsWith('/admin');\n } catch {\n return false;\n }\n}\n\n/**\n * Proxy a request to the upstream CMS server with proper cookie handling.\n */\nasync function proxyToUpstream(\n request: NextRequest,\n pathname: string,\n upstream: string\n): Promise<NextResponse> {\n const upstreamUrl = new URL(pathname, upstream);\n upstreamUrl.search = request.nextUrl.search;\n\n // Clone all headers from the request\n const headers = new Headers(request.headers);\n\n // Keep the original host header so the upstream app knows the real origin\n // This is important for auth redirects (WorkOS) to use the correct domain\n // The x-forwarded-* headers provide additional context\n headers.set('x-forwarded-host', request.headers.get('host') ?? '');\n headers.set('x-forwarded-proto', request.nextUrl.protocol.replace(':', ''));\n headers.set('x-forwarded-for', request.headers.get('x-forwarded-for') ?? '');\n\n const response = await fetch(upstreamUrl.toString(), {\n method: request.method,\n headers,\n body: request.body,\n // @ts-expect-error - duplex is required for streaming bodies\n duplex: 'half',\n redirect: 'manual', // Don't follow redirects, let the client handle them\n });\n\n // Create response with proper header handling\n const responseHeaders = new Headers();\n\n const upstreamUrlObj = new URL(upstream);\n const upstreamOrigin = upstreamUrlObj.origin;\n const currentOrigin = request.nextUrl.origin;\n\n // Copy headers from upstream response\n response.headers.forEach((value, key) => {\n const lowerKey = key.toLowerCase();\n\n // Handle Set-Cookie specially - rewrite domain to current host\n if (lowerKey === 'set-cookie') {\n let modifiedCookie = value;\n\n // Remove Domain attribute so cookie defaults to current host\n modifiedCookie = modifiedCookie.replace(/;\\s*Domain=[^;]*/gi, '');\n\n // Ensure Path is set (usually /admin or /)\n if (!/;\\s*Path=/i.test(modifiedCookie)) {\n modifiedCookie += '; Path=/';\n }\n\n // For secure cookies in production, ensure SameSite is appropriate\n if (!/;\\s*SameSite=/i.test(modifiedCookie)) {\n modifiedCookie += '; SameSite=Lax';\n }\n\n responseHeaders.append(key, modifiedCookie);\n }\n // Handle Location header - rewrite upstream URLs to current host\n else if (lowerKey === 'location') {\n try {\n // Parse the location (handles both absolute and relative URLs)\n const locationUrl = new URL(value, upstream);\n\n // If redirect points to upstream, rewrite to current origin\n if (locationUrl.origin === upstreamOrigin) {\n const newLocation = `${currentOrigin}${locationUrl.pathname}${locationUrl.search}`;\n responseHeaders.set(key, newLocation);\n } else {\n // External redirect (e.g., to WorkOS)\n // Rewrite redirect_uri parameter if it points to upstream\n let finalLocation = value;\n\n // Check if this is a WorkOS/OAuth redirect with redirect_uri parameter\n const redirectUri = locationUrl.searchParams.get('redirect_uri');\n if (redirectUri) {\n try {\n const redirectUriUrl = new URL(redirectUri);\n // If redirect_uri points to upstream, rewrite to current origin\n if (redirectUriUrl.host === upstreamUrlObj.host) {\n const newRedirectUri = `${currentOrigin}${redirectUriUrl.pathname}${redirectUriUrl.search}`;\n locationUrl.searchParams.set('redirect_uri', newRedirectUri);\n finalLocation = locationUrl.toString();\n }\n } catch {\n // If redirect_uri parsing fails, keep original\n }\n }\n\n responseHeaders.set(key, finalLocation);\n }\n } catch {\n // If URL parsing fails, keep original\n responseHeaders.set(key, value);\n }\n }\n // Skip headers that cause issues after fetch decompresses the body\n else if (\n lowerKey !== 'transfer-encoding' &&\n lowerKey !== 'content-encoding' &&\n lowerKey !== 'content-length'\n ) {\n responseHeaders.set(key, value);\n }\n });\n\n // Add debug header to verify middleware is running\n responseHeaders.set('x-proxied-by', 'cms-proxy');\n\n // For HTML responses, rewrite upstream URLs in the body\n const contentType = response.headers.get('content-type') ?? '';\n if (contentType.includes('text/html') && response.body) {\n let text = await response.text();\n\n // Get the upstream host for more comprehensive replacement\n const upstreamHost = upstreamUrlObj.host;\n\n // Replace full origin (https://cms.example.com)\n text = text.replaceAll(upstreamOrigin, currentOrigin);\n\n // Replace protocol-relative URLs (//cms.example.com)\n text = text.replaceAll(`//${upstreamHost}`, `//${request.nextUrl.host}`);\n\n // Replace any remaining absolute URLs with the upstream host\n // This catches cases where protocol might differ\n text = text.replaceAll(`https://${upstreamHost}`, currentOrigin);\n text = text.replaceAll(`http://${upstreamHost}`, currentOrigin);\n\n return new NextResponse(text, {\n status: response.status,\n statusText: response.statusText,\n headers: responseHeaders,\n });\n }\n\n return new NextResponse(response.body, {\n status: response.status,\n statusText: response.statusText,\n headers: responseHeaders,\n });\n}\n\n/**\n * Creates a proxy middleware function for Next.js.\n *\n * @example\n * ```ts\n * // middleware.ts\n * import { createCmsProxy } from 'cms-renderer/lib/proxy';\n *\n * const cmsProxy = createCmsProxy({\n * upstream: process.env.ADMIN_UPSTREAM_ORIGIN,\n * });\n *\n * export async function middleware(request: NextRequest) {\n * return cmsProxy(request);\n * }\n *\n * export const config = {\n * matcher: cmsProxyMatcher,\n * };\n * ```\n */\nexport function createCmsProxy(config: ProxyConfig = {}) {\n const upstream = (config.upstream ?? process.env.ADMIN_UPSTREAM_ORIGIN ?? '').replace(/\\/$/, '');\n const additionalPaths = config.additionalPaths ?? [];\n\n if (!upstream) {\n console.warn(\n '[cms-proxy] No upstream URL configured. Set ADMIN_UPSTREAM_ORIGIN or pass upstream option.'\n );\n }\n\n return async function cmsProxy(request: NextRequest): Promise<NextResponse> {\n if (!upstream) {\n return NextResponse.next();\n }\n\n const { pathname } = request.nextUrl;\n\n // Proxy /admin routes to the upstream CMS\n if (pathname.startsWith('/admin')) {\n return proxyToUpstream(request, pathname, upstream);\n }\n\n // Proxy /api routes to the upstream CMS\n if (pathname.startsWith('/api')) {\n return proxyToUpstream(request, pathname, upstream);\n }\n\n // Proxy auth routes to the upstream CMS (WorkOS callbacks, signin, etc.)\n if (pathname.startsWith('/auth')) {\n return proxyToUpstream(request, pathname, upstream);\n }\n\n // Proxy additional custom paths\n for (const pathPrefix of additionalPaths) {\n if (pathname.startsWith(pathPrefix)) {\n return proxyToUpstream(request, pathname, upstream);\n }\n }\n\n // Only proxy /_next and static files if the request comes from an admin page\n // This prevents breaking the web app's own assets\n if (isFromAdminPage(request)) {\n if (pathname.startsWith('/_next') || STATIC_FILE_REGEX.test(pathname)) {\n return proxyToUpstream(request, pathname, upstream);\n }\n }\n\n return NextResponse.next();\n };\n}\n\n/**\n * Default matcher configuration for the CMS proxy middleware.\n * Use this in your middleware.ts config export.\n *\n * @example\n * ```ts\n * export const config = {\n * matcher: cmsProxyMatcher,\n * };\n * ```\n */\nexport const cmsProxyMatcher = [\n '/admin',\n '/admin/:path*',\n '/api/:path*',\n '/auth/:path*',\n '/_next/:path*',\n '/((?:.*\\\\.(?:css|js|map|png|jpg|jpeg|gif|svg|ico|webp|avif|woff|woff2|ttf|eot|txt|xml))$)',\n];\n"],"mappings":"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","names":[]}
|
package/dist/lib/renderer.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as
|
|
1
|
+
import * as React from 'react';
|
|
2
2
|
import { Metadata } from 'next';
|
|
3
3
|
import { BlockComponentRegistry } from './types.js';
|
|
4
4
|
import '@repo/cms-schema/blocks';
|
|
@@ -7,9 +7,16 @@ type PageProps = {
|
|
|
7
7
|
params: Promise<{
|
|
8
8
|
slug: string[];
|
|
9
9
|
}>;
|
|
10
|
+
searchParams?: Promise<{
|
|
11
|
+
[key: string]: string | string[] | undefined;
|
|
12
|
+
}>;
|
|
13
|
+
/** CMS API base URL (e.g., 'http://localhost:3000') */
|
|
14
|
+
cmsUrl: string;
|
|
10
15
|
registry?: Partial<BlockComponentRegistry>;
|
|
11
16
|
/** API key for CMS API authentication */
|
|
12
17
|
apiKey?: string;
|
|
18
|
+
/** Website ID (required if not using env variables) */
|
|
19
|
+
websiteId?: string;
|
|
13
20
|
};
|
|
14
21
|
/**
|
|
15
22
|
* Force dynamic rendering to ensure routes are always fresh.
|
|
@@ -25,12 +32,12 @@ declare const dynamic = "force-dynamic";
|
|
|
25
32
|
*
|
|
26
33
|
* Reconstructs the full path and fetches route via tRPC.
|
|
27
34
|
*/
|
|
28
|
-
declare function ParametricRoutePage({ params, registry, apiKey }: PageProps): Promise<
|
|
35
|
+
declare function ParametricRoutePage({ params, searchParams, registry, apiKey, cmsUrl, websiteId: providedWebsiteId, }: PageProps): Promise<React.JSX.Element>;
|
|
29
36
|
/**
|
|
30
37
|
* Generate metadata for the page.
|
|
31
38
|
* Uses Next.js 15+ async params pattern.
|
|
32
39
|
*/
|
|
33
|
-
declare function generateMetadata({ params, apiKey }: PageProps): Promise<Metadata>;
|
|
40
|
+
declare function generateMetadata({ params, apiKey, cmsUrl, websiteId: providedWebsiteId, }: PageProps): Promise<Metadata>;
|
|
34
41
|
declare function normalizePath(path: string): string;
|
|
35
42
|
|
|
36
43
|
export { ParametricRoutePage as default, dynamic, generateMetadata, normalizePath };
|
package/dist/lib/renderer.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
BlockRenderer
|
|
3
|
-
} from "../chunk-
|
|
3
|
+
} from "../chunk-SJZTIW2I.js";
|
|
4
|
+
import "../chunk-TIR3RJMY.js";
|
|
4
5
|
import {
|
|
5
6
|
getCmsClient
|
|
6
|
-
} from "../chunk-
|
|
7
|
+
} from "../chunk-JHKDRASN.js";
|
|
7
8
|
|
|
8
9
|
// ../../packages/cms-schema/src/blocks/article.ts
|
|
9
10
|
function normalizeArticleContent(payload) {
|
|
@@ -204,7 +205,7 @@ var HeroBlockContentSchema = z6.object({
|
|
|
204
205
|
message: "URL must be a valid full URL (http://... or https://...), relative path (/path), anchor (#anchor), or empty string"
|
|
205
206
|
}
|
|
206
207
|
).optional().or(z6.literal("")),
|
|
207
|
-
backgroundImage: ImageReferenceSchema.optional(),
|
|
208
|
+
backgroundImage: ImageReferenceSchema.nullable().optional(),
|
|
208
209
|
alignment: z6.enum(HeroAlignment).default("center")
|
|
209
210
|
});
|
|
210
211
|
var HERO_BLOCK_SCHEMA_NAME = "hero-block";
|
|
@@ -249,33 +250,86 @@ function isValidBlockSchemaName(name) {
|
|
|
249
250
|
import { unstable_noStore } from "next/cache";
|
|
250
251
|
import { notFound } from "next/navigation";
|
|
251
252
|
import { jsx } from "react/jsx-runtime";
|
|
253
|
+
function getWebsiteId(providedWebsiteId) {
|
|
254
|
+
const websiteId = providedWebsiteId ?? process.env.NEXT_PUBLIC_WEBSITE_ID ?? process.env.WEBSITE_ID ?? process.env.CMS_WEBSITE_ID;
|
|
255
|
+
if (!websiteId) {
|
|
256
|
+
throw new Error(
|
|
257
|
+
"Missing websiteId for website renderer. Either pass websiteId prop or set NEXT_PUBLIC_WEBSITE_ID (or WEBSITE_ID/CMS_WEBSITE_ID) to a valid UUID."
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
261
|
+
if (!uuidRegex.test(websiteId)) {
|
|
262
|
+
throw new Error(
|
|
263
|
+
`Invalid websiteId "${websiteId}". Provide a valid UUID via prop or set NEXT_PUBLIC_WEBSITE_ID (or WEBSITE_ID/CMS_WEBSITE_ID).`
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
return websiteId;
|
|
267
|
+
}
|
|
252
268
|
var dynamic = "force-dynamic";
|
|
253
|
-
async function ParametricRoutePage({
|
|
269
|
+
async function ParametricRoutePage({
|
|
270
|
+
params,
|
|
271
|
+
searchParams,
|
|
272
|
+
registry,
|
|
273
|
+
apiKey,
|
|
274
|
+
cmsUrl,
|
|
275
|
+
websiteId: providedWebsiteId
|
|
276
|
+
}) {
|
|
254
277
|
unstable_noStore();
|
|
278
|
+
const websiteId = getWebsiteId(providedWebsiteId);
|
|
255
279
|
const { slug } = await params;
|
|
280
|
+
const resolvedSearchParams = await searchParams;
|
|
281
|
+
let aiPreviewIndex = null;
|
|
282
|
+
const aiPreviewParam = resolvedSearchParams?.ai_preview;
|
|
283
|
+
if (aiPreviewParam) {
|
|
284
|
+
const paramValue = Array.isArray(aiPreviewParam) ? aiPreviewParam[0] : aiPreviewParam;
|
|
285
|
+
if (paramValue) {
|
|
286
|
+
const parsed = parseInt(paramValue, 10);
|
|
287
|
+
if (!Number.isNaN(parsed)) {
|
|
288
|
+
aiPreviewIndex = parsed;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
const editModeParam = resolvedSearchParams?.edit_mode;
|
|
293
|
+
const editMode = editModeParam === "true" || editModeParam === "1";
|
|
256
294
|
const rawPath = `/${slug.join("/")}`;
|
|
257
295
|
const path = normalizePath(rawPath);
|
|
258
|
-
const client = getCmsClient({ apiKey });
|
|
296
|
+
const client = getCmsClient({ apiKey, cmsUrl });
|
|
259
297
|
try {
|
|
260
|
-
const { route } = await client.route.getByPath.query({ path });
|
|
298
|
+
const { route } = await client.route.getByPath.query({ websiteId, path });
|
|
261
299
|
if (route.state !== "Live") {
|
|
262
300
|
console.error(`Route found but not Live. Path: ${path}, State: ${route.state}`);
|
|
263
301
|
notFound();
|
|
264
302
|
}
|
|
265
303
|
const blockPromises = route.block_ids.map(async (blockId) => {
|
|
266
304
|
try {
|
|
267
|
-
const result = await client.block.getById.query({ id: blockId });
|
|
305
|
+
const result = await client.block.getById.query({ websiteId, id: blockId });
|
|
268
306
|
return result.block;
|
|
269
307
|
} catch (error) {
|
|
270
308
|
console.error(`Failed to fetch block ${blockId}:`, error);
|
|
271
309
|
return null;
|
|
272
310
|
}
|
|
273
311
|
});
|
|
274
|
-
const
|
|
312
|
+
const generatedBlocksPromise = aiPreviewIndex !== null ? client.block.getGeneratedByBlockIds.query({ websiteId, blockIds: route.block_ids }).catch((error) => {
|
|
313
|
+
console.error("Failed to fetch generated blocks:", error);
|
|
314
|
+
return { generatedBlocks: {} };
|
|
315
|
+
}) : Promise.resolve({ generatedBlocks: {} });
|
|
316
|
+
const [blockResults, { generatedBlocks }] = await Promise.all([
|
|
317
|
+
Promise.all(blockPromises),
|
|
318
|
+
generatedBlocksPromise
|
|
319
|
+
]);
|
|
275
320
|
const blocks = [];
|
|
276
321
|
for (const block of blockResults) {
|
|
277
322
|
if (!block || block.published_content === null) continue;
|
|
278
|
-
|
|
323
|
+
let content = null;
|
|
324
|
+
if (aiPreviewIndex !== null) {
|
|
325
|
+
const generatedBlock = generatedBlocks[block.id];
|
|
326
|
+
const variantIndex = aiPreviewIndex - 1;
|
|
327
|
+
const variants = generatedBlock?.generated_content;
|
|
328
|
+
if (variants && Array.isArray(variants) && variants[variantIndex]) {
|
|
329
|
+
content = variants[variantIndex].content;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
content = content ?? block.published_content;
|
|
279
333
|
if (!content) continue;
|
|
280
334
|
if (block.schema_name === "article") {
|
|
281
335
|
const article = normalizeArticleContent(content);
|
|
@@ -294,7 +348,15 @@ async function ParametricRoutePage({ params, registry, apiKey }) {
|
|
|
294
348
|
content
|
|
295
349
|
});
|
|
296
350
|
}
|
|
297
|
-
return /* @__PURE__ */ jsx("main", { children: blocks.map((block) => /* @__PURE__ */ jsx(
|
|
351
|
+
return /* @__PURE__ */ jsx("main", { children: blocks.map((block) => /* @__PURE__ */ jsx(
|
|
352
|
+
BlockRenderer,
|
|
353
|
+
{
|
|
354
|
+
block,
|
|
355
|
+
registry: registry ?? {},
|
|
356
|
+
disableEditable: !editMode
|
|
357
|
+
},
|
|
358
|
+
block.id
|
|
359
|
+
)) });
|
|
298
360
|
} catch (error) {
|
|
299
361
|
console.error(`Route fetch error for path: ${path}`, error);
|
|
300
362
|
const errorCode = error instanceof Error && "data" in error ? error.data?.code : error instanceof Error && "code" in error ? error.code : void 0;
|
|
@@ -304,13 +366,19 @@ async function ParametricRoutePage({ params, registry, apiKey }) {
|
|
|
304
366
|
throw error;
|
|
305
367
|
}
|
|
306
368
|
}
|
|
307
|
-
async function generateMetadata({
|
|
369
|
+
async function generateMetadata({
|
|
370
|
+
params,
|
|
371
|
+
apiKey,
|
|
372
|
+
cmsUrl,
|
|
373
|
+
websiteId: providedWebsiteId
|
|
374
|
+
}) {
|
|
375
|
+
const websiteId = getWebsiteId(providedWebsiteId);
|
|
308
376
|
const { slug } = await params;
|
|
309
377
|
const rawPath = `/${slug.join("/")}`;
|
|
310
378
|
const path = normalizePath(rawPath);
|
|
311
|
-
const client = getCmsClient({ apiKey });
|
|
379
|
+
const client = getCmsClient({ apiKey, cmsUrl });
|
|
312
380
|
try {
|
|
313
|
-
const { route } = await client.route.getByPath.query({ path });
|
|
381
|
+
const { route } = await client.route.getByPath.query({ websiteId, path });
|
|
314
382
|
return {
|
|
315
383
|
title: `${route.path} | Website`,
|
|
316
384
|
description: `Content page: ${route.path}`
|