cms-renderer 0.6.6 → 0.6.7
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/README.md +2 -2
- package/dist/lib/custom-schemas.js.map +1 -1
- package/dist/lib/docs-markdown.js +6 -2
- package/dist/lib/docs-markdown.js.map +1 -1
- package/dist/lib/parametric-route.d.ts +41 -0
- package/dist/lib/parametric-route.js +686 -0
- package/dist/lib/parametric-route.js.map +1 -0
- package/dist/lib/renderer.d.ts +7 -23
- package/dist/lib/renderer.js +175 -86
- package/dist/lib/renderer.js.map +1 -1
- package/dist/lib/schema.d.ts +5 -0
- package/dist/lib/schema.js +21 -1
- package/dist/lib/schema.js.map +1 -1
- package/package.json +8 -1
|
@@ -0,0 +1,686 @@
|
|
|
1
|
+
// ../../packages/cms-schema/src/blocks/article.ts
|
|
2
|
+
function normalizeArticleContent(payload) {
|
|
3
|
+
if (!payload || typeof payload !== "object") {
|
|
4
|
+
return null;
|
|
5
|
+
}
|
|
6
|
+
const record = payload;
|
|
7
|
+
const headline = typeof record.headline === "string" ? record.headline : null;
|
|
8
|
+
const body = typeof record.body === "string" ? record.body : null;
|
|
9
|
+
if (!headline || !body) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
const author = typeof record.author === "string" ? record.author : void 0;
|
|
13
|
+
const publishedAt = typeof record.publishedAt === "string" ? record.publishedAt : void 0;
|
|
14
|
+
const tags = Array.isArray(record.tags) ? record.tags.map((tag) => String(tag)) : void 0;
|
|
15
|
+
const statusRaw = typeof record.status === "string" ? record.status.trim() : void 0;
|
|
16
|
+
return {
|
|
17
|
+
headline,
|
|
18
|
+
body,
|
|
19
|
+
author,
|
|
20
|
+
publishedAt,
|
|
21
|
+
tags,
|
|
22
|
+
status: statusRaw || void 0
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function isArticlePublished(article) {
|
|
26
|
+
const now = /* @__PURE__ */ new Date();
|
|
27
|
+
const publishedAt = article.publishedAt ? new Date(article.publishedAt) : null;
|
|
28
|
+
return article.status === "published" && publishedAt !== null && !Number.isNaN(publishedAt.getTime()) && publishedAt <= now;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ../../packages/cms-schema/src/validation/image.ts
|
|
32
|
+
import { z } from "zod";
|
|
33
|
+
var ALLOWED_MIME_TYPES = ["image/jpeg", "image/png", "image/webp"];
|
|
34
|
+
var MAX_FILE_SIZE = 10 * 1024 * 1024;
|
|
35
|
+
var MIN_FILE_SIZE = 1024;
|
|
36
|
+
var MAX_DIMENSION = 8192;
|
|
37
|
+
var MIN_DIMENSION = 10;
|
|
38
|
+
var MimeTypeSchema = z.enum(ALLOWED_MIME_TYPES);
|
|
39
|
+
var FileSizeSchema = z.number().int().min(MIN_FILE_SIZE, `File too small. Minimum: ${MIN_FILE_SIZE} bytes`).max(MAX_FILE_SIZE, `File too large. Maximum: ${MAX_FILE_SIZE / 1024 / 1024}MB`);
|
|
40
|
+
var DimensionSchema = z.number().int().min(MIN_DIMENSION, `Dimension too small. Minimum: ${MIN_DIMENSION}px`).max(MAX_DIMENSION, `Dimension too large. Maximum: ${MAX_DIMENSION}px`);
|
|
41
|
+
var UploadRequestSchema = z.object({
|
|
42
|
+
filename: z.string().min(1, "Filename is required").max(255, "Filename too long").regex(/^[^<>:"/\\|?*]+$/, "Filename contains invalid characters"),
|
|
43
|
+
mimeType: MimeTypeSchema,
|
|
44
|
+
fileSize: FileSizeSchema
|
|
45
|
+
});
|
|
46
|
+
var ConfirmUploadSchema = z.object({
|
|
47
|
+
assetId: z.uuid().optional(),
|
|
48
|
+
width: DimensionSchema,
|
|
49
|
+
height: DimensionSchema
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// ../../packages/cms-schema/src/blocks/schemas/article-block.ts
|
|
53
|
+
import { z as z2 } from "zod";
|
|
54
|
+
var ArticleBlockContentSchema = z2.object({
|
|
55
|
+
headline: z2.string().min(1, "Article headline is required").max(300, "Headline too long").trim(),
|
|
56
|
+
author: z2.string().max(100, "Article author too long").trim().optional(),
|
|
57
|
+
publishedAt: z2.iso.datetime({ message: "Article publishedAt must be a valid ISO 8601 datetime string." }).optional(),
|
|
58
|
+
body: z2.string().min(1, "Article body content is required"),
|
|
59
|
+
tags: z2.array(z2.string()).optional(),
|
|
60
|
+
status: z2.enum(["draft", "review", "published"])
|
|
61
|
+
});
|
|
62
|
+
var ARTICLE_BLOCK_SCHEMA_NAME = "article";
|
|
63
|
+
|
|
64
|
+
// ../../packages/cms-schema/src/blocks/schemas/cta-block.ts
|
|
65
|
+
import { z as z3 } from "zod";
|
|
66
|
+
var CTAButtonSchema = z3.object({
|
|
67
|
+
text: z3.string().min(1, "Button text is required").max(50, "Button text too long"),
|
|
68
|
+
url: z3.string().refine(
|
|
69
|
+
(val) => {
|
|
70
|
+
if (val.startsWith("http://") || val.startsWith("https://")) {
|
|
71
|
+
try {
|
|
72
|
+
new URL(val);
|
|
73
|
+
return true;
|
|
74
|
+
} catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (val.startsWith("/")) {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
if (val.startsWith("#")) {
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
message: "URL must be a valid full URL (http://... or https://...), relative path (/path), or anchor (#anchor)"
|
|
88
|
+
}
|
|
89
|
+
)
|
|
90
|
+
});
|
|
91
|
+
var CTABlockContentSchema = z3.object({
|
|
92
|
+
headline: z3.string().min(1, "Headline is required").max(100, "Headline too long"),
|
|
93
|
+
description: z3.string().max(500, "Description too long").optional(),
|
|
94
|
+
primaryButton: CTAButtonSchema,
|
|
95
|
+
secondaryButton: CTAButtonSchema.optional()
|
|
96
|
+
});
|
|
97
|
+
var CTA_BLOCK_SCHEMA_NAME = "cta-block";
|
|
98
|
+
|
|
99
|
+
// ../../packages/cms-schema/src/blocks/schemas/features-block.ts
|
|
100
|
+
import { z as z4 } from "zod";
|
|
101
|
+
var FeaturesLayout = ["grid", "list", "carousel"];
|
|
102
|
+
var FeatureItemSchema = z4.object({
|
|
103
|
+
icon: z4.string().max(50, "Icon name too long").optional(),
|
|
104
|
+
title: z4.string().min(1, "Title is required").max(100, "Title too long"),
|
|
105
|
+
description: z4.string().max(500, "Description too long").optional()
|
|
106
|
+
});
|
|
107
|
+
var FeaturesBlockContentSchema = z4.object({
|
|
108
|
+
title: z4.string().min(1, "Section title is required").max(100, "Title too long"),
|
|
109
|
+
subtitle: z4.string().max(200, "Subtitle too long").optional(),
|
|
110
|
+
features: z4.array(FeatureItemSchema).min(1, "At least one feature is required").max(6, "Maximum 6 features allowed"),
|
|
111
|
+
layout: z4.enum(FeaturesLayout).default("grid")
|
|
112
|
+
});
|
|
113
|
+
var FEATURES_BLOCK_SCHEMA_NAME = "features-block";
|
|
114
|
+
|
|
115
|
+
// ../../packages/cms-schema/src/blocks/schemas/hero-block.ts
|
|
116
|
+
import { z as z6 } from "zod";
|
|
117
|
+
|
|
118
|
+
// ../../packages/cms-schema/src/fields/complex/media.ts
|
|
119
|
+
import { z as z5 } from "zod";
|
|
120
|
+
var ImageAssetSchema = z5.object({
|
|
121
|
+
/** UUID primary key */
|
|
122
|
+
id: z5.uuid(),
|
|
123
|
+
/** R2/S3 storage URL for the original file */
|
|
124
|
+
url: z5.url(),
|
|
125
|
+
/** Image width in pixels */
|
|
126
|
+
width: DimensionSchema,
|
|
127
|
+
/** Image height in pixels */
|
|
128
|
+
height: DimensionSchema,
|
|
129
|
+
/** Original filename from upload */
|
|
130
|
+
originalFilename: z5.string().min(1).max(255),
|
|
131
|
+
/** MIME type (only web-safe formats allowed) */
|
|
132
|
+
mimeType: MimeTypeSchema,
|
|
133
|
+
/** File size in bytes */
|
|
134
|
+
fileSize: FileSizeSchema,
|
|
135
|
+
/** Base64-encoded tiny preview for blur-up loading */
|
|
136
|
+
lqip: z5.string().optional()
|
|
137
|
+
});
|
|
138
|
+
var HotspotSchema = z5.object({
|
|
139
|
+
x: z5.number().min(0).max(1),
|
|
140
|
+
y: z5.number().min(0).max(1)
|
|
141
|
+
});
|
|
142
|
+
var CropSchema = z5.object({
|
|
143
|
+
/** X coordinate of top-left corner in pixels */
|
|
144
|
+
x: z5.number().int().nonnegative(),
|
|
145
|
+
/** Y coordinate of top-left corner in pixels */
|
|
146
|
+
y: z5.number().int().nonnegative(),
|
|
147
|
+
/** Width of crop region in pixels (must be > 0) */
|
|
148
|
+
width: z5.number().int().positive(),
|
|
149
|
+
/** Height of crop region in pixels (must be > 0) */
|
|
150
|
+
height: z5.number().int().positive()
|
|
151
|
+
});
|
|
152
|
+
var ImageReferenceSchema = z5.object({
|
|
153
|
+
// Alt text is REQUIRED for accessibility
|
|
154
|
+
alt: z5.string().min(1, "Alt text is required for accessibility"),
|
|
155
|
+
// Optional metadata
|
|
156
|
+
caption: z5.string().max(500).optional(),
|
|
157
|
+
attribution: z5.string().max(255).optional(),
|
|
158
|
+
// Reference to the ImageAsset with stored transformation
|
|
159
|
+
_asset: z5.object({
|
|
160
|
+
id: z5.uuid(),
|
|
161
|
+
transformation: z5.string().nullable().optional()
|
|
162
|
+
})
|
|
163
|
+
});
|
|
164
|
+
var fileSchema = z5.object({
|
|
165
|
+
url: z5.url(),
|
|
166
|
+
name: z5.string(),
|
|
167
|
+
size: z5.number().int().positive(),
|
|
168
|
+
type: z5.string()
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// ../../packages/cms-schema/src/blocks/schemas/hero-block.ts
|
|
172
|
+
var HeroAlignment = ["left", "center", "right"];
|
|
173
|
+
var HeroBlockContentSchema = z6.object({
|
|
174
|
+
headline: z6.string().min(1, "Headline is required").max(100, "Headline too long"),
|
|
175
|
+
subheadline: z6.string().max(200, "Subheadline too long").optional(),
|
|
176
|
+
ctaText: z6.string().max(50, "CTA text too long").optional(),
|
|
177
|
+
ctaUrl: z6.string().refine(
|
|
178
|
+
(val) => {
|
|
179
|
+
if (val === "") return true;
|
|
180
|
+
if (val.startsWith("http://") || val.startsWith("https://")) {
|
|
181
|
+
try {
|
|
182
|
+
new URL(val);
|
|
183
|
+
return true;
|
|
184
|
+
} catch {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (val.startsWith("/")) {
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
if (val.startsWith("#")) {
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
return false;
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
message: "URL must be a valid full URL (http://... or https://...), relative path (/path), anchor (#anchor), or empty string"
|
|
198
|
+
}
|
|
199
|
+
).optional().or(z6.literal("")),
|
|
200
|
+
backgroundImage: ImageReferenceSchema.nullable().optional(),
|
|
201
|
+
alignment: z6.enum(HeroAlignment).default("center")
|
|
202
|
+
});
|
|
203
|
+
var HERO_BLOCK_SCHEMA_NAME = "hero-block";
|
|
204
|
+
|
|
205
|
+
// ../../packages/cms-schema/src/blocks/schemas/logo-trust-block.ts
|
|
206
|
+
import { z as z7 } from "zod";
|
|
207
|
+
var LogoItemSchema = z7.object({
|
|
208
|
+
/** Unique ID for this logo item */
|
|
209
|
+
id: z7.uuid(),
|
|
210
|
+
/** Image reference (alt + _asset with transformation) */
|
|
211
|
+
image: ImageReferenceSchema,
|
|
212
|
+
/** Optional company/brand name to display */
|
|
213
|
+
name: z7.string().max(100, "Name too long").optional()
|
|
214
|
+
});
|
|
215
|
+
var LegacyLogoItemSchema = z7.object({
|
|
216
|
+
/** Direct URL to the logo image */
|
|
217
|
+
url: z7.string(),
|
|
218
|
+
/** Alt text for the image */
|
|
219
|
+
alt: z7.string(),
|
|
220
|
+
/** Optional company/brand name */
|
|
221
|
+
name: z7.string().optional()
|
|
222
|
+
});
|
|
223
|
+
var LogoTrustBlockContentSchema = z7.object({
|
|
224
|
+
title: z7.string().max(100, "Title too long").optional(),
|
|
225
|
+
logos: z7.array(LogoItemSchema).max(20, "Maximum 20 logos allowed")
|
|
226
|
+
});
|
|
227
|
+
var LOGO_TRUST_BLOCK_SCHEMA_NAME = "logo-trust-block";
|
|
228
|
+
|
|
229
|
+
// ../../packages/cms-schema/src/blocks/registry.ts
|
|
230
|
+
var BLOCK_SCHEMA_NAMES = [
|
|
231
|
+
ARTICLE_BLOCK_SCHEMA_NAME,
|
|
232
|
+
HERO_BLOCK_SCHEMA_NAME,
|
|
233
|
+
FEATURES_BLOCK_SCHEMA_NAME,
|
|
234
|
+
CTA_BLOCK_SCHEMA_NAME,
|
|
235
|
+
LOGO_TRUST_BLOCK_SCHEMA_NAME
|
|
236
|
+
];
|
|
237
|
+
function isValidBlockSchemaName(name) {
|
|
238
|
+
return BLOCK_SCHEMA_NAMES.includes(name);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ../../packages/cms-schema/src/routing/normalize-path.ts
|
|
242
|
+
function normalizePath(path) {
|
|
243
|
+
if (!path || path === "/") {
|
|
244
|
+
return "/";
|
|
245
|
+
}
|
|
246
|
+
let normalized = path.trim();
|
|
247
|
+
if (!normalized) {
|
|
248
|
+
return "/";
|
|
249
|
+
}
|
|
250
|
+
if (normalized === "/") {
|
|
251
|
+
return "/";
|
|
252
|
+
}
|
|
253
|
+
let end = normalized.length;
|
|
254
|
+
while (end > 1 && normalized.charCodeAt(end - 1) === 47) {
|
|
255
|
+
end--;
|
|
256
|
+
}
|
|
257
|
+
if (end < normalized.length) {
|
|
258
|
+
normalized = normalized.slice(0, end);
|
|
259
|
+
}
|
|
260
|
+
if (!normalized.startsWith("/")) {
|
|
261
|
+
normalized = `/${normalized}`;
|
|
262
|
+
}
|
|
263
|
+
const segments = normalized.split("/").filter((s) => s.length > 0);
|
|
264
|
+
if (segments.length === 0) {
|
|
265
|
+
return "/";
|
|
266
|
+
}
|
|
267
|
+
return `/${segments.join("/")}`;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ../../packages/cms-schema/src/routing/path-validation.ts
|
|
271
|
+
import { z as z8 } from "zod";
|
|
272
|
+
var pathRegex = /^\/[\p{L}\p{N}\-_{}/:]*$/u;
|
|
273
|
+
var placeholderSegmentRegex = /^(:[a-zA-Z0-9_]+|\{[a-zA-Z0-9_]+\})$/;
|
|
274
|
+
function hasValidPlaceholderStructure(path) {
|
|
275
|
+
if (path === "/") {
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
const segments = path.split("/").filter(Boolean);
|
|
279
|
+
return segments.every((segment) => {
|
|
280
|
+
const hasPlaceholderSyntax = segment.includes(":") || segment.includes("{") || segment.includes("}");
|
|
281
|
+
if (!hasPlaceholderSyntax) {
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
return placeholderSegmentRegex.test(segment);
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
var pathSchema = z8.string().min(1, "Path is required").regex(
|
|
288
|
+
pathRegex,
|
|
289
|
+
"Path must start with / and contain only letters, numbers, hyphens, underscores, slashes, colons, and braces"
|
|
290
|
+
).refine(
|
|
291
|
+
hasValidPlaceholderStructure,
|
|
292
|
+
"Path placeholders must be in the form :name or {name} with non-empty names"
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
// ../../packages/cms-schema/src/documents/schemas/language.ts
|
|
296
|
+
import { z as z9 } from "zod";
|
|
297
|
+
var LanguageSchema = z9.object({
|
|
298
|
+
/** 2-letter ISO 639-1 language code */
|
|
299
|
+
code: z9.string().length(2, "Language code must be 2 characters"),
|
|
300
|
+
/** English name of the language */
|
|
301
|
+
name: z9.string().min(1, "Language name required"),
|
|
302
|
+
/** Name in the language itself (optional but recommended) */
|
|
303
|
+
nativeName: z9.string().optional()
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// lib/block-renderer.tsx
|
|
307
|
+
import React from "react";
|
|
308
|
+
import { ClientEditableBlock } from "./client-editable-block.js";
|
|
309
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
310
|
+
function extractContentValues(content, basePath = []) {
|
|
311
|
+
const map = /* @__PURE__ */ new Map();
|
|
312
|
+
function walk(obj, path) {
|
|
313
|
+
if (typeof obj === "string" && obj.trim() !== "") {
|
|
314
|
+
const contentPath = path.join(".");
|
|
315
|
+
const existing = map.get(obj) || [];
|
|
316
|
+
existing.push({ contentPath, value: obj });
|
|
317
|
+
map.set(obj, existing);
|
|
318
|
+
} else if (Array.isArray(obj)) {
|
|
319
|
+
for (let index = 0; index < obj.length; index++) {
|
|
320
|
+
walk(obj[index], [...path, String(index)]);
|
|
321
|
+
}
|
|
322
|
+
} else if (obj && typeof obj === "object") {
|
|
323
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
324
|
+
walk(value, [...path, key]);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
walk(content, basePath);
|
|
329
|
+
return map;
|
|
330
|
+
}
|
|
331
|
+
function CmsEditableInit() {
|
|
332
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
333
|
+
/* @__PURE__ */ jsx("style", { children: `
|
|
334
|
+
[data-cms-editable] {
|
|
335
|
+
cursor: pointer;
|
|
336
|
+
border-radius: 2px;
|
|
337
|
+
}
|
|
338
|
+
[data-cms-editable]:hover {
|
|
339
|
+
outline: 2px solid #3b82f6;
|
|
340
|
+
outline-offset: 2px;
|
|
341
|
+
}
|
|
342
|
+
.cms-block-toolbar {
|
|
343
|
+
position: fixed;
|
|
344
|
+
display: flex;
|
|
345
|
+
gap: 4px;
|
|
346
|
+
background: #1f2937;
|
|
347
|
+
border-radius: 6px;
|
|
348
|
+
padding: 4px;
|
|
349
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
|
350
|
+
transition: opacity 0.15s ease;
|
|
351
|
+
z-index: 99999;
|
|
352
|
+
pointer-events: auto;
|
|
353
|
+
}
|
|
354
|
+
.cms-block-toolbar button {
|
|
355
|
+
display: flex;
|
|
356
|
+
align-items: center;
|
|
357
|
+
justify-content: center;
|
|
358
|
+
width: 28px;
|
|
359
|
+
height: 28px;
|
|
360
|
+
border: none;
|
|
361
|
+
background: transparent;
|
|
362
|
+
color: #9ca3af;
|
|
363
|
+
border-radius: 4px;
|
|
364
|
+
cursor: pointer;
|
|
365
|
+
transition: background 0.15s ease, color 0.15s ease;
|
|
366
|
+
}
|
|
367
|
+
.cms-block-toolbar button:hover {
|
|
368
|
+
background: #374151;
|
|
369
|
+
color: #fff;
|
|
370
|
+
}
|
|
371
|
+
.cms-block-toolbar button.delete:hover {
|
|
372
|
+
background: #dc2626;
|
|
373
|
+
color: #fff;
|
|
374
|
+
}
|
|
375
|
+
.cms-block-toolbar button:disabled {
|
|
376
|
+
opacity: 0.4;
|
|
377
|
+
cursor: not-allowed;
|
|
378
|
+
}
|
|
379
|
+
.cms-block-toolbar button:disabled:hover {
|
|
380
|
+
background: transparent;
|
|
381
|
+
color: #9ca3af;
|
|
382
|
+
}
|
|
383
|
+
.cms-block-toolbar svg {
|
|
384
|
+
width: 16px;
|
|
385
|
+
height: 16px;
|
|
386
|
+
}
|
|
387
|
+
` }),
|
|
388
|
+
/* @__PURE__ */ jsx(
|
|
389
|
+
"script",
|
|
390
|
+
{
|
|
391
|
+
dangerouslySetInnerHTML: {
|
|
392
|
+
__html: `
|
|
393
|
+
(function() {
|
|
394
|
+
if (!window.__cmsEditableInitialized) {
|
|
395
|
+
window.__cmsEditableInitialized = true;
|
|
396
|
+
|
|
397
|
+
document.addEventListener('click', function(e) {
|
|
398
|
+
if (e.target.closest('.cms-block-toolbar')) return;
|
|
399
|
+
|
|
400
|
+
var editableTarget = e.target.closest('[data-cms-editable]');
|
|
401
|
+
if (editableTarget) {
|
|
402
|
+
var message = {
|
|
403
|
+
type: 'cms-editable-click',
|
|
404
|
+
blockId: editableTarget.getAttribute('data-block-id'),
|
|
405
|
+
blockType: editableTarget.getAttribute('data-block-type'),
|
|
406
|
+
contentPath: editableTarget.getAttribute('data-content-path')
|
|
407
|
+
};
|
|
408
|
+
if (window.parent && window.parent !== window) {
|
|
409
|
+
window.parent.postMessage(message, '*');
|
|
410
|
+
}
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
var blockTarget = e.target.closest('[data-cms-block]');
|
|
415
|
+
if (blockTarget) {
|
|
416
|
+
var message = {
|
|
417
|
+
type: 'cms-editable-click',
|
|
418
|
+
blockId: blockTarget.getAttribute('data-block-id'),
|
|
419
|
+
blockType: blockTarget.getAttribute('data-block-type'),
|
|
420
|
+
contentPath: null
|
|
421
|
+
};
|
|
422
|
+
if (window.parent && window.parent !== window) {
|
|
423
|
+
window.parent.postMessage(message, '*');
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
})();
|
|
429
|
+
`
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
)
|
|
433
|
+
] });
|
|
434
|
+
}
|
|
435
|
+
function pathMatchesPattern(path, pattern) {
|
|
436
|
+
const pathSegs = path.split("/").filter(Boolean);
|
|
437
|
+
const patternSegs = pattern.split("/").filter(Boolean);
|
|
438
|
+
if (pathSegs.length !== patternSegs.length) return false;
|
|
439
|
+
for (let i = 0; i < patternSegs.length; i++) {
|
|
440
|
+
const seg = patternSegs[i];
|
|
441
|
+
if (!seg) return false;
|
|
442
|
+
if (seg.startsWith("{") && seg.endsWith("}") || seg.startsWith("(") && seg.endsWith(")")) {
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
if (seg !== pathSegs[i]) return false;
|
|
446
|
+
}
|
|
447
|
+
return true;
|
|
448
|
+
}
|
|
449
|
+
function resolveComponent(registry, blockType, path) {
|
|
450
|
+
if (path) {
|
|
451
|
+
for (const key of Object.keys(registry)) {
|
|
452
|
+
if (!key.startsWith("/")) continue;
|
|
453
|
+
const spaceIdx = key.indexOf(" ");
|
|
454
|
+
if (spaceIdx === -1) continue;
|
|
455
|
+
const pathPattern = key.slice(0, spaceIdx);
|
|
456
|
+
const registeredType = key.slice(spaceIdx + 1);
|
|
457
|
+
if (registeredType !== blockType) continue;
|
|
458
|
+
if (pathMatchesPattern(path, pathPattern)) {
|
|
459
|
+
return registry[key];
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return registry[blockType];
|
|
464
|
+
}
|
|
465
|
+
function BlockRenderer({
|
|
466
|
+
block,
|
|
467
|
+
registry,
|
|
468
|
+
disableEditable,
|
|
469
|
+
routeParams,
|
|
470
|
+
path
|
|
471
|
+
}) {
|
|
472
|
+
const Component = resolveComponent(registry, block.type, path);
|
|
473
|
+
if (!Component) {
|
|
474
|
+
if (process.env.NODE_ENV === "development") {
|
|
475
|
+
console.warn(`[BlockRenderer] Unknown block type: ${block.type}`);
|
|
476
|
+
}
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
const language = routeParams ? Object.values(routeParams).find((p) => p.schemaName === "language")?.value : void 0;
|
|
480
|
+
const component = /* @__PURE__ */ jsx(Component, { content: block.content, routeParams, language });
|
|
481
|
+
if (disableEditable) {
|
|
482
|
+
return component;
|
|
483
|
+
}
|
|
484
|
+
const contentValueMap = extractContentValues(block.content);
|
|
485
|
+
const contentEntries = Array.from(contentValueMap.entries()).map(([value, matches]) => ({ v: value, p: matches[0]?.contentPath })).filter((e) => !!e.p);
|
|
486
|
+
return /* @__PURE__ */ jsx(ClientEditableBlock, { blockId: block.id, blockType: block.type, contentEntries, children: component });
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// lib/cms-api.ts
|
|
490
|
+
import { createTRPCClient, httpBatchLink } from "@trpc/client";
|
|
491
|
+
import superjson from "superjson";
|
|
492
|
+
function getCmsApiUrl(cmsUrl) {
|
|
493
|
+
return new URL("/api/trpc", cmsUrl).toString();
|
|
494
|
+
}
|
|
495
|
+
function createFetchWithApiKey(apiKey, websiteId) {
|
|
496
|
+
return async (url, options) => {
|
|
497
|
+
let finalUrl = url;
|
|
498
|
+
const urlObj = new URL(url.toString());
|
|
499
|
+
if (apiKey) {
|
|
500
|
+
urlObj.searchParams.set("api_key", apiKey);
|
|
501
|
+
}
|
|
502
|
+
if (websiteId) {
|
|
503
|
+
urlObj.searchParams.set("website_id", websiteId);
|
|
504
|
+
}
|
|
505
|
+
if (apiKey || websiteId) {
|
|
506
|
+
finalUrl = urlObj.toString();
|
|
507
|
+
}
|
|
508
|
+
const response = await fetch(finalUrl, options);
|
|
509
|
+
return response;
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
function createCmsClient(options) {
|
|
513
|
+
const url = getCmsApiUrl(options.cmsUrl);
|
|
514
|
+
return createTRPCClient({
|
|
515
|
+
links: [
|
|
516
|
+
httpBatchLink({
|
|
517
|
+
url,
|
|
518
|
+
transformer: superjson,
|
|
519
|
+
fetch: createFetchWithApiKey(options.apiKey, options.websiteId)
|
|
520
|
+
})
|
|
521
|
+
]
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
var clientCache = /* @__PURE__ */ new Map();
|
|
525
|
+
function getClientCacheKey(options) {
|
|
526
|
+
return `${options.cmsUrl}|${options.apiKey ?? ""}|${options.websiteId ?? ""}`;
|
|
527
|
+
}
|
|
528
|
+
function getCmsClient(options) {
|
|
529
|
+
const cacheKey = getClientCacheKey(options);
|
|
530
|
+
let client = clientCache.get(cacheKey);
|
|
531
|
+
if (!client) {
|
|
532
|
+
client = createCmsClient(options);
|
|
533
|
+
clientCache.set(cacheKey, client);
|
|
534
|
+
}
|
|
535
|
+
return client;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// lib/parametric-route.tsx
|
|
539
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
540
|
+
function getWebsiteId(providedWebsiteId) {
|
|
541
|
+
const websiteId = providedWebsiteId ?? process.env.NEXT_PUBLIC_WEBSITE_ID ?? process.env.WEBSITE_ID ?? process.env.CMS_WEBSITE_ID;
|
|
542
|
+
if (!websiteId) {
|
|
543
|
+
throw new Error(
|
|
544
|
+
"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."
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
548
|
+
if (!uuidRegex.test(websiteId)) {
|
|
549
|
+
throw new Error(
|
|
550
|
+
`Invalid websiteId "${websiteId}". Provide a valid UUID via prop or set NEXT_PUBLIC_WEBSITE_ID (or WEBSITE_ID/CMS_WEBSITE_ID).`
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
return websiteId;
|
|
554
|
+
}
|
|
555
|
+
async function renderParametricRoute({
|
|
556
|
+
params,
|
|
557
|
+
searchParams,
|
|
558
|
+
registry,
|
|
559
|
+
apiKey,
|
|
560
|
+
cmsUrl,
|
|
561
|
+
websiteId: providedWebsiteId
|
|
562
|
+
}) {
|
|
563
|
+
const websiteId = getWebsiteId(providedWebsiteId);
|
|
564
|
+
const { slug } = await params;
|
|
565
|
+
const resolvedSearchParams = await searchParams;
|
|
566
|
+
let aiPreviewIndex = null;
|
|
567
|
+
const aiPreviewParam = resolvedSearchParams?.ai_preview;
|
|
568
|
+
if (aiPreviewParam) {
|
|
569
|
+
const paramValue = Array.isArray(aiPreviewParam) ? aiPreviewParam[0] : aiPreviewParam;
|
|
570
|
+
if (paramValue) {
|
|
571
|
+
const parsed = parseInt(paramValue, 10);
|
|
572
|
+
if (!Number.isNaN(parsed)) {
|
|
573
|
+
aiPreviewIndex = parsed;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
const editModeParam = resolvedSearchParams?.edit_mode;
|
|
578
|
+
const editMode = editModeParam === "true" || editModeParam === "1";
|
|
579
|
+
const rawPath = `/${slug.join("/")}`;
|
|
580
|
+
const path = normalizePath(rawPath);
|
|
581
|
+
if (/\.[a-zA-Z0-9]+$/.test(path)) {
|
|
582
|
+
return { status: "not_found" };
|
|
583
|
+
}
|
|
584
|
+
const client = getCmsClient({ apiKey, cmsUrl });
|
|
585
|
+
try {
|
|
586
|
+
const { route, resolvedParams } = await client.route.getByPath.query({
|
|
587
|
+
websiteId,
|
|
588
|
+
path
|
|
589
|
+
});
|
|
590
|
+
if (route.state !== "Live") {
|
|
591
|
+
return { status: "not_found" };
|
|
592
|
+
}
|
|
593
|
+
const blockResultsPromise = client.block.getByIds.query({ websiteId, ids: route.block_ids }).catch((error) => {
|
|
594
|
+
console.error("Failed to fetch blocks:", error);
|
|
595
|
+
return [];
|
|
596
|
+
});
|
|
597
|
+
const generatedBlocksPromise = aiPreviewIndex !== null ? client.block.getGeneratedByBlockIds.query({ websiteId, blockIds: route.block_ids }).catch((error) => {
|
|
598
|
+
console.error("Failed to fetch generated blocks:", error);
|
|
599
|
+
return { generatedBlocks: {} };
|
|
600
|
+
}) : Promise.resolve({ generatedBlocks: {} });
|
|
601
|
+
const [blockResults, { generatedBlocks }] = await Promise.all([
|
|
602
|
+
blockResultsPromise,
|
|
603
|
+
generatedBlocksPromise
|
|
604
|
+
]);
|
|
605
|
+
const blocks = [];
|
|
606
|
+
for (const block of blockResults) {
|
|
607
|
+
if (!block || block.published_content === null) continue;
|
|
608
|
+
let content = null;
|
|
609
|
+
if (aiPreviewIndex !== null) {
|
|
610
|
+
const generatedBlock = generatedBlocks[block.id];
|
|
611
|
+
const variantIndex = aiPreviewIndex - 1;
|
|
612
|
+
const variants = generatedBlock?.generated_content;
|
|
613
|
+
if (variants && Array.isArray(variants) && variants[variantIndex]) {
|
|
614
|
+
content = variants[variantIndex].content;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
content = content ?? block.published_content;
|
|
618
|
+
if (!content) continue;
|
|
619
|
+
if (block.schema_name === "article") {
|
|
620
|
+
const article = normalizeArticleContent(content);
|
|
621
|
+
const isPublished = article ? isArticlePublished(article) : null;
|
|
622
|
+
if (article && isPublished) {
|
|
623
|
+
blocks.push({ id: block.id, type: "article", content: article });
|
|
624
|
+
}
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
627
|
+
if (block.schema_id) {
|
|
628
|
+
blocks.push({ id: block.id, type: block.schema_name, content });
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
631
|
+
if (!isValidBlockSchemaName(block.schema_name)) {
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
blocks.push({
|
|
635
|
+
id: block.id,
|
|
636
|
+
type: block.schema_name,
|
|
637
|
+
content
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
const routeParams = resolvedParams ? Object.fromEntries(
|
|
641
|
+
Object.entries(resolvedParams).map(([key, param]) => [
|
|
642
|
+
key,
|
|
643
|
+
{
|
|
644
|
+
value: param.value,
|
|
645
|
+
schemaName: param.schemaName,
|
|
646
|
+
document: {
|
|
647
|
+
id: param.document.id,
|
|
648
|
+
title: param.document.title,
|
|
649
|
+
content: param.document.content,
|
|
650
|
+
schema_name: param.document.schema_name
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
])
|
|
654
|
+
) : void 0;
|
|
655
|
+
return {
|
|
656
|
+
status: "ok",
|
|
657
|
+
node: /* @__PURE__ */ jsxs2("main", { children: [
|
|
658
|
+
editMode && /* @__PURE__ */ jsx2(CmsEditableInit, {}),
|
|
659
|
+
blocks.map((block) => /* @__PURE__ */ jsx2(
|
|
660
|
+
BlockRenderer,
|
|
661
|
+
{
|
|
662
|
+
block,
|
|
663
|
+
registry: registry ?? {},
|
|
664
|
+
disableEditable: !editMode,
|
|
665
|
+
routeParams,
|
|
666
|
+
path
|
|
667
|
+
},
|
|
668
|
+
block.id
|
|
669
|
+
))
|
|
670
|
+
] })
|
|
671
|
+
};
|
|
672
|
+
} catch (error) {
|
|
673
|
+
console.error(`Route fetch error for path: ${path}`, error);
|
|
674
|
+
const errorCode = error instanceof Error && "data" in error ? error.data?.code : error instanceof Error && "code" in error ? error.code : void 0;
|
|
675
|
+
if (errorCode === "NOT_FOUND" || errorCode === "P0002" || errorCode === "BAD_REQUEST") {
|
|
676
|
+
return { status: "not_found" };
|
|
677
|
+
}
|
|
678
|
+
return { status: "error", error };
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
export {
|
|
682
|
+
getWebsiteId,
|
|
683
|
+
normalizePath,
|
|
684
|
+
renderParametricRoute
|
|
685
|
+
};
|
|
686
|
+
//# sourceMappingURL=parametric-route.js.map
|