cms-renderer 0.0.0 → 0.1.1
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-HVKFEZBT.js +116 -0
- package/dist/chunk-HVKFEZBT.js.map +1 -0
- package/dist/chunk-JHKDRASN.js +39 -0
- package/dist/chunk-JHKDRASN.js.map +1 -0
- package/dist/chunk-RPM73PQZ.js +17 -0
- package/dist/chunk-RPM73PQZ.js.map +1 -0
- package/dist/lib/block-renderer.d.ts +32 -0
- package/dist/lib/block-renderer.js +7 -0
- package/dist/lib/block-renderer.js.map +1 -0
- package/dist/lib/cms-api.d.ts +25 -0
- package/dist/lib/cms-api.js +7 -0
- package/dist/lib/cms-api.js.map +1 -0
- package/dist/lib/data-utils.d.ts +218 -0
- package/dist/lib/data-utils.js +247 -0
- package/dist/lib/data-utils.js.map +1 -0
- package/dist/lib/image/lazy-load.d.ts +75 -0
- package/dist/lib/image/lazy-load.js +83 -0
- package/dist/lib/image/lazy-load.js.map +1 -0
- package/dist/lib/markdown-utils.d.ts +172 -0
- package/dist/lib/markdown-utils.js +137 -0
- package/dist/lib/markdown-utils.js.map +1 -0
- package/dist/lib/renderer.d.ts +40 -0
- package/dist/lib/renderer.js +371 -0
- package/dist/lib/renderer.js.map +1 -0
- package/{lib/result.ts → dist/lib/result.d.ts} +32 -146
- package/dist/lib/result.js +37 -0
- package/dist/lib/result.js.map +1 -0
- package/dist/lib/schema.d.ts +15 -0
- package/dist/lib/schema.js +35 -0
- package/dist/lib/schema.js.map +1 -0
- package/{lib/trpc.ts → dist/lib/trpc.d.ts} +6 -4
- package/dist/lib/trpc.js +7 -0
- package/dist/lib/trpc.js.map +1 -0
- package/dist/lib/types.d.ts +163 -0
- package/dist/lib/types.js +1 -0
- package/dist/lib/types.js.map +1 -0
- package/package.json +50 -11
- package/.turbo/turbo-check-types.log +0 -2
- package/lib/__tests__/enrich-block-images.test.ts +0 -394
- package/lib/block-renderer.tsx +0 -60
- package/lib/cms-api.ts +0 -86
- package/lib/data-utils.ts +0 -572
- package/lib/image/lazy-load.ts +0 -209
- package/lib/markdown-utils.ts +0 -368
- package/lib/renderer.tsx +0 -189
- package/lib/schema.ts +0 -74
- package/lib/types.ts +0 -201
- package/next.config.ts +0 -39
- package/postcss.config.mjs +0 -5
- package/tsconfig.json +0 -12
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import {
|
|
2
|
+
err,
|
|
3
|
+
ok
|
|
4
|
+
} from "../chunk-HVKFEZBT.js";
|
|
5
|
+
|
|
6
|
+
// lib/markdown-utils.ts
|
|
7
|
+
import { mdToJSON } from "md4w";
|
|
8
|
+
var ParseErrorCode = {
|
|
9
|
+
INVALID_INPUT: "INVALID_INPUT",
|
|
10
|
+
EMPTY_CONTENT: "EMPTY_CONTENT",
|
|
11
|
+
PARSE_FAILED: "PARSE_FAILED",
|
|
12
|
+
CONTENT_TOO_LONG: "CONTENT_TOO_LONG",
|
|
13
|
+
NESTED_TOO_DEEP: "NESTED_TOO_DEEP"
|
|
14
|
+
};
|
|
15
|
+
var DEFAULT_MAX_LENGTH = 5e5;
|
|
16
|
+
var XSS_PATTERNS = [
|
|
17
|
+
/<script\b/i,
|
|
18
|
+
/javascript:/i,
|
|
19
|
+
/on\w+\s*=/i,
|
|
20
|
+
// onclick=, onerror=, etc.
|
|
21
|
+
/data:/i,
|
|
22
|
+
// data: URIs can be dangerous
|
|
23
|
+
/vbscript:/i
|
|
24
|
+
];
|
|
25
|
+
function parseMarkdownV1(content) {
|
|
26
|
+
return mdToJSON(content);
|
|
27
|
+
}
|
|
28
|
+
function parseMarkdownV2(content) {
|
|
29
|
+
try {
|
|
30
|
+
if (content == null) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
return mdToJSON(content);
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function createParseError(code, message, cause) {
|
|
39
|
+
return { code, message, cause };
|
|
40
|
+
}
|
|
41
|
+
function detectXssPattern(content) {
|
|
42
|
+
for (const pattern of XSS_PATTERNS) {
|
|
43
|
+
if (pattern.test(content)) {
|
|
44
|
+
return pattern;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
function sanitizeContent(content) {
|
|
50
|
+
let sanitized = content;
|
|
51
|
+
sanitized = sanitized.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "");
|
|
52
|
+
sanitized = sanitized.replace(/javascript:/gi, "removed:");
|
|
53
|
+
sanitized = sanitized.replace(/vbscript:/gi, "removed:");
|
|
54
|
+
sanitized = sanitized.replace(/\bon\w+\s*=\s*["'][^"']*["']/gi, "");
|
|
55
|
+
sanitized = sanitized.replace(/\bon\w+\s*=\s*\S+/gi, "");
|
|
56
|
+
return sanitized;
|
|
57
|
+
}
|
|
58
|
+
function parseMarkdownV3(content, options = {}) {
|
|
59
|
+
const { maxLength = DEFAULT_MAX_LENGTH, sanitize = true, allowEmpty = false } = options;
|
|
60
|
+
if (content === null || content === void 0) {
|
|
61
|
+
return err(
|
|
62
|
+
createParseError(
|
|
63
|
+
ParseErrorCode.INVALID_INPUT,
|
|
64
|
+
`Content must be a string, received ${content === null ? "null" : "undefined"}`
|
|
65
|
+
)
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
if (typeof content !== "string") {
|
|
69
|
+
return err(
|
|
70
|
+
createParseError(
|
|
71
|
+
ParseErrorCode.INVALID_INPUT,
|
|
72
|
+
`Content must be a string, received ${typeof content}`
|
|
73
|
+
)
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
const trimmed = content.trim();
|
|
77
|
+
if (trimmed.length === 0 && !allowEmpty) {
|
|
78
|
+
return err(
|
|
79
|
+
createParseError(ParseErrorCode.EMPTY_CONTENT, "Content is empty or contains only whitespace")
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
if (content.length > maxLength) {
|
|
83
|
+
return err(
|
|
84
|
+
createParseError(
|
|
85
|
+
ParseErrorCode.CONTENT_TOO_LONG,
|
|
86
|
+
`Content exceeds maximum length of ${maxLength.toLocaleString()} characters (received ${content.length.toLocaleString()})`
|
|
87
|
+
)
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
const xssPattern = detectXssPattern(content);
|
|
91
|
+
if (xssPattern) {
|
|
92
|
+
if (process.env.NODE_ENV === "development") {
|
|
93
|
+
console.warn(
|
|
94
|
+
`[parseMarkdownV3] XSS pattern detected: ${xssPattern.toString()}`,
|
|
95
|
+
sanitize ? "Content will be sanitized." : "Sanitization is disabled!"
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const processedContent = sanitize ? sanitizeContent(content) : content;
|
|
100
|
+
try {
|
|
101
|
+
const ast = mdToJSON(processedContent);
|
|
102
|
+
return ok(ast);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
const cause = error instanceof Error ? error : new Error(String(error));
|
|
105
|
+
return err(
|
|
106
|
+
createParseError(
|
|
107
|
+
ParseErrorCode.PARSE_FAILED,
|
|
108
|
+
`Failed to parse markdown: ${cause.message}`,
|
|
109
|
+
cause
|
|
110
|
+
)
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
var parseMarkdown = parseMarkdownV3;
|
|
115
|
+
function isSafeMarkdown(content) {
|
|
116
|
+
return detectXssPattern(content) === null;
|
|
117
|
+
}
|
|
118
|
+
function estimateWordCount(content) {
|
|
119
|
+
const plainText = content.replace(/#+\s/g, "").replace(/\*\*|__|~~|`/g, "").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/!\[([^\]]*)\]\([^)]+\)/g, "").replace(/```[\s\S]*?```/g, "").replace(/`[^`]+`/g, "");
|
|
120
|
+
const words = plainText.trim().split(/\s+/).filter(Boolean);
|
|
121
|
+
return words.length;
|
|
122
|
+
}
|
|
123
|
+
function estimateReadingTime(content, wordsPerMinute = 200) {
|
|
124
|
+
const wordCount = estimateWordCount(content);
|
|
125
|
+
return Math.ceil(wordCount / wordsPerMinute);
|
|
126
|
+
}
|
|
127
|
+
export {
|
|
128
|
+
ParseErrorCode,
|
|
129
|
+
estimateReadingTime,
|
|
130
|
+
estimateWordCount,
|
|
131
|
+
isSafeMarkdown,
|
|
132
|
+
parseMarkdown,
|
|
133
|
+
parseMarkdownV1,
|
|
134
|
+
parseMarkdownV2,
|
|
135
|
+
parseMarkdownV3
|
|
136
|
+
};
|
|
137
|
+
//# sourceMappingURL=markdown-utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../lib/markdown-utils.ts"],"sourcesContent":["/**\n * Markdown Parsing Utilities\n *\n * Three implementations showing the progression from naive to production-ready:\n * - v1 (Naive): Direct call, crashes on bad input\n * - v2 (Defensive): Try-catch with null fallback\n * - v3 (Robust): Result type, validation, sanitization\n *\n * The robust version (v3) is exported as the default `parseMarkdown`.\n */\n\nimport type { MDTree } from 'md4w';\nimport { mdToJSON } from 'md4w';\n\nimport { err, ok, type Result } from './result';\n\n// -----------------------------------------------------------------------------\n// Types\n// -----------------------------------------------------------------------------\n\n/**\n * Error codes for markdown parsing failures.\n */\nexport const ParseErrorCode = {\n INVALID_INPUT: 'INVALID_INPUT',\n EMPTY_CONTENT: 'EMPTY_CONTENT',\n PARSE_FAILED: 'PARSE_FAILED',\n CONTENT_TOO_LONG: 'CONTENT_TOO_LONG',\n NESTED_TOO_DEEP: 'NESTED_TOO_DEEP',\n} as const;\n\nexport type ParseErrorCode = (typeof ParseErrorCode)[keyof typeof ParseErrorCode];\n\n/**\n * Structured error for markdown parsing failures.\n */\nexport interface ParseError {\n code: ParseErrorCode;\n message: string;\n cause?: Error;\n}\n\n/**\n * Options for robust markdown parsing.\n */\nexport interface ParseOptions {\n /**\n * Maximum content length in characters.\n * @default 500000 (500KB of text, ~100K words)\n */\n maxLength?: number;\n\n /**\n * Whether to sanitize XSS attempts in the output.\n * @default true\n */\n sanitize?: boolean;\n\n /**\n * Whether to allow empty content.\n * @default false\n */\n allowEmpty?: boolean;\n}\n\n// -----------------------------------------------------------------------------\n// Constants\n// -----------------------------------------------------------------------------\n\nconst DEFAULT_MAX_LENGTH = 500_000; // 500KB of text\n\n/**\n * Patterns that indicate potential XSS attempts in markdown.\n * These are checked in raw content before parsing.\n */\nconst XSS_PATTERNS = [\n /<script\\b/i,\n /javascript:/i,\n /on\\w+\\s*=/i, // onclick=, onerror=, etc.\n /data:/i, // data: URIs can be dangerous\n /vbscript:/i,\n] as const;\n\n// -----------------------------------------------------------------------------\n// V1: Naive Implementation\n// -----------------------------------------------------------------------------\n\n/**\n * Naive markdown parser - crashes on bad input.\n *\n * DO NOT USE IN PRODUCTION. This is for demonstration only.\n *\n * Problems:\n * - No input validation\n * - No error handling\n * - Will crash the entire app on malformed input\n * - No protection against XSS\n * - No content limits\n *\n * @example\n * ```ts\n * // This crashes if content is null, undefined, or malformed\n * const ast = parseMarkdownV1(userInput);\n * ```\n */\nexport function parseMarkdownV1(content: string): MDTree {\n return mdToJSON(content);\n}\n\n// -----------------------------------------------------------------------------\n// V2: Defensive Implementation\n// -----------------------------------------------------------------------------\n\n/**\n * Defensive markdown parser - catches errors but loses context.\n *\n * Better than v1 but still problematic:\n * - Returns null on any error (loses error details)\n * - Caller can't distinguish between empty content and parse failure\n * - Still no XSS protection\n * - Still no content limits\n *\n * @example\n * ```ts\n * const ast = parseMarkdownV2(userInput);\n * if (!ast) {\n * // What went wrong? We don't know.\n * return <ErrorFallback />;\n * }\n * ```\n */\nexport function parseMarkdownV2(content: string): MDTree | null {\n try {\n // Basic null check\n if (content == null) {\n return null;\n }\n return mdToJSON(content);\n } catch {\n return null;\n }\n}\n\n// -----------------------------------------------------------------------------\n// V3: Robust Implementation\n// -----------------------------------------------------------------------------\n\n/**\n * Creates a ParseError with the given code and message.\n */\nfunction createParseError(code: ParseErrorCode, message: string, cause?: Error): ParseError {\n return { code, message, cause };\n}\n\n/**\n * Checks content for potential XSS patterns.\n * Returns the first matched pattern or null if clean.\n */\nfunction detectXssPattern(content: string): RegExp | null {\n for (const pattern of XSS_PATTERNS) {\n if (pattern.test(content)) {\n return pattern;\n }\n }\n return null;\n}\n\n/**\n * Sanitizes content by removing dangerous patterns.\n * This is a basic sanitizer; production should use DOMPurify.\n */\nfunction sanitizeContent(content: string): string {\n let sanitized = content;\n\n // Remove script tags\n sanitized = sanitized.replace(/<script\\b[^<]*(?:(?!<\\/script>)<[^<]*)*<\\/script>/gi, '');\n\n // Remove javascript: and vbscript: URLs\n sanitized = sanitized.replace(/javascript:/gi, 'removed:');\n sanitized = sanitized.replace(/vbscript:/gi, 'removed:');\n\n // Remove event handlers (onclick, onerror, etc.)\n sanitized = sanitized.replace(/\\bon\\w+\\s*=\\s*[\"'][^\"']*[\"']/gi, '');\n sanitized = sanitized.replace(/\\bon\\w+\\s*=\\s*\\S+/gi, '');\n\n return sanitized;\n}\n\n/**\n * Robust markdown parser - production-ready with full error context.\n *\n * Features:\n * - Input validation with specific error codes\n * - Result type for explicit error handling\n * - XSS pattern detection and optional sanitization\n * - Content length limits\n * - Preserves error context for debugging\n *\n * @param content - Markdown string to parse\n * @param options - Parsing options\n * @returns Result containing the AST or a structured error\n *\n * @example\n * ```ts\n * const result = parseMarkdownV3(userInput);\n *\n * if (!result.ok) {\n * switch (result.error.code) {\n * case 'INVALID_INPUT':\n * return <InvalidInputError message={result.error.message} />;\n * case 'CONTENT_TOO_LONG':\n * return <ContentTooLongError />;\n * case 'PARSE_FAILED':\n * console.error('Parse error:', result.error.cause);\n * return <ParseError />;\n * default:\n * return <GenericError />;\n * }\n * }\n *\n * return <MarkdownRenderer ast={result.value} />;\n * ```\n */\nexport function parseMarkdownV3(\n content: string,\n options: ParseOptions = {}\n): Result<MDTree, ParseError> {\n const { maxLength = DEFAULT_MAX_LENGTH, sanitize = true, allowEmpty = false } = options;\n\n // 1. Validate input type\n if (content === null || content === undefined) {\n return err(\n createParseError(\n ParseErrorCode.INVALID_INPUT,\n `Content must be a string, received ${content === null ? 'null' : 'undefined'}`\n )\n );\n }\n\n if (typeof content !== 'string') {\n return err(\n createParseError(\n ParseErrorCode.INVALID_INPUT,\n `Content must be a string, received ${typeof content}`\n )\n );\n }\n\n // 2. Handle empty content\n const trimmed = content.trim();\n if (trimmed.length === 0 && !allowEmpty) {\n return err(\n createParseError(ParseErrorCode.EMPTY_CONTENT, 'Content is empty or contains only whitespace')\n );\n }\n\n // 3. Check content length\n if (content.length > maxLength) {\n return err(\n createParseError(\n ParseErrorCode.CONTENT_TOO_LONG,\n `Content exceeds maximum length of ${maxLength.toLocaleString()} characters ` +\n `(received ${content.length.toLocaleString()})`\n )\n );\n }\n\n // 4. Check for XSS patterns\n const xssPattern = detectXssPattern(content);\n if (xssPattern) {\n if (process.env.NODE_ENV === 'development') {\n console.warn(\n `[parseMarkdownV3] XSS pattern detected: ${xssPattern.toString()}`,\n sanitize ? 'Content will be sanitized.' : 'Sanitization is disabled!'\n );\n }\n }\n\n // 5. Optionally sanitize content\n const processedContent = sanitize ? sanitizeContent(content) : content;\n\n // 6. Parse markdown with error handling\n try {\n const ast = mdToJSON(processedContent);\n return ok(ast);\n } catch (error) {\n const cause = error instanceof Error ? error : new Error(String(error));\n return err(\n createParseError(\n ParseErrorCode.PARSE_FAILED,\n `Failed to parse markdown: ${cause.message}`,\n cause\n )\n );\n }\n}\n\n// -----------------------------------------------------------------------------\n// Default Export\n// -----------------------------------------------------------------------------\n\n/**\n * Production-ready markdown parser.\n *\n * This is an alias for `parseMarkdownV3` - the robust implementation\n * with validation, sanitization, and Result-based error handling.\n *\n * @example\n * ```ts\n * import { parseMarkdown } from '@/lib/markdown-utils';\n *\n * const result = parseMarkdown(content);\n * if (!result.ok) {\n * // Handle error with full context\n * console.error(`[${result.error.code}] ${result.error.message}`);\n * return null;\n * }\n * return result.value;\n * ```\n */\nexport const parseMarkdown = parseMarkdownV3;\n\n// -----------------------------------------------------------------------------\n// Utility Functions\n// -----------------------------------------------------------------------------\n\n/**\n * Checks if content is safe markdown (no XSS patterns detected).\n *\n * @param content - Markdown string to check\n * @returns true if no XSS patterns detected\n */\nexport function isSafeMarkdown(content: string): boolean {\n return detectXssPattern(content) === null;\n}\n\n/**\n * Estimates the word count of markdown content.\n * Useful for content length limits and reading time estimates.\n *\n * @param content - Markdown string to count\n * @returns Estimated word count\n */\nexport function estimateWordCount(content: string): number {\n // Remove markdown syntax for more accurate count\n const plainText = content\n .replace(/#+\\s/g, '') // Remove headings\n .replace(/\\*\\*|__|~~|`/g, '') // Remove formatting\n .replace(/\\[([^\\]]+)\\]\\([^)]+\\)/g, '$1') // Convert links to text\n .replace(/!\\[([^\\]]*)\\]\\([^)]+\\)/g, '') // Remove images\n .replace(/```[\\s\\S]*?```/g, '') // Remove code blocks\n .replace(/`[^`]+`/g, ''); // Remove inline code\n\n const words = plainText.trim().split(/\\s+/).filter(Boolean);\n return words.length;\n}\n\n/**\n * Estimates reading time for markdown content.\n *\n * @param content - Markdown string\n * @param wordsPerMinute - Reading speed (default 200 WPM)\n * @returns Reading time in minutes\n */\nexport function estimateReadingTime(content: string, wordsPerMinute = 200): number {\n const wordCount = estimateWordCount(content);\n return Math.ceil(wordCount / wordsPerMinute);\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { Metadata } from 'next';
|
|
3
|
+
import { BlockComponentRegistry } from './types.js';
|
|
4
|
+
import '@repo/cms-schema/blocks';
|
|
5
|
+
|
|
6
|
+
type PageProps = {
|
|
7
|
+
params: Promise<{
|
|
8
|
+
slug: string[];
|
|
9
|
+
}>;
|
|
10
|
+
/** CMS API base URL (e.g., 'http://localhost:3000') */
|
|
11
|
+
cmsUrl: string;
|
|
12
|
+
registry?: Partial<BlockComponentRegistry>;
|
|
13
|
+
/** API key for CMS API authentication */
|
|
14
|
+
apiKey?: string;
|
|
15
|
+
/** Website ID (required if not using env variables) */
|
|
16
|
+
websiteId?: string;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Force dynamic rendering to ensure routes are always fresh.
|
|
20
|
+
* This prevents Next.js from caching pages when routes are published.
|
|
21
|
+
*/
|
|
22
|
+
declare const dynamic = "force-dynamic";
|
|
23
|
+
/**
|
|
24
|
+
* Catch-all route handler for parametric routes.
|
|
25
|
+
*
|
|
26
|
+
* Handles paths like:
|
|
27
|
+
* - /us/en/products -> slug = ['us', 'en', 'products']
|
|
28
|
+
* - /about -> slug = ['about']
|
|
29
|
+
*
|
|
30
|
+
* Reconstructs the full path and fetches route via tRPC.
|
|
31
|
+
*/
|
|
32
|
+
declare function ParametricRoutePage({ params, registry, apiKey, cmsUrl, websiteId: providedWebsiteId, }: PageProps): Promise<react.JSX.Element>;
|
|
33
|
+
/**
|
|
34
|
+
* Generate metadata for the page.
|
|
35
|
+
* Uses Next.js 15+ async params pattern.
|
|
36
|
+
*/
|
|
37
|
+
declare function generateMetadata({ params, apiKey, cmsUrl, websiteId: providedWebsiteId, }: PageProps): Promise<Metadata>;
|
|
38
|
+
declare function normalizePath(path: string): string;
|
|
39
|
+
|
|
40
|
+
export { ParametricRoutePage as default, dynamic, generateMetadata, normalizePath };
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BlockRenderer
|
|
3
|
+
} from "../chunk-RPM73PQZ.js";
|
|
4
|
+
import {
|
|
5
|
+
getCmsClient
|
|
6
|
+
} from "../chunk-JHKDRASN.js";
|
|
7
|
+
|
|
8
|
+
// ../../packages/cms-schema/src/blocks/article.ts
|
|
9
|
+
function normalizeArticleContent(payload) {
|
|
10
|
+
if (!payload || typeof payload !== "object") {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
const record = payload;
|
|
14
|
+
const headline = typeof record.headline === "string" ? record.headline : null;
|
|
15
|
+
const body = typeof record.body === "string" ? record.body : null;
|
|
16
|
+
if (!headline || !body) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
const author = typeof record.author === "string" ? record.author : void 0;
|
|
20
|
+
const publishedAt = typeof record.publishedAt === "string" ? record.publishedAt : void 0;
|
|
21
|
+
const tags = Array.isArray(record.tags) ? record.tags.map((tag) => String(tag)) : void 0;
|
|
22
|
+
const statusRaw = typeof record.status === "string" ? record.status.trim() : void 0;
|
|
23
|
+
return {
|
|
24
|
+
headline,
|
|
25
|
+
body,
|
|
26
|
+
author,
|
|
27
|
+
publishedAt,
|
|
28
|
+
tags,
|
|
29
|
+
status: statusRaw || void 0
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function isArticlePublished(article) {
|
|
33
|
+
const now = /* @__PURE__ */ new Date();
|
|
34
|
+
const publishedAt = article.publishedAt ? new Date(article.publishedAt) : null;
|
|
35
|
+
return article.status === "published" && publishedAt !== null && !Number.isNaN(publishedAt.getTime()) && publishedAt <= now;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ../../packages/cms-schema/src/validation/image.ts
|
|
39
|
+
import { z } from "zod";
|
|
40
|
+
var ALLOWED_MIME_TYPES = ["image/jpeg", "image/png", "image/webp"];
|
|
41
|
+
var MAX_FILE_SIZE = 10 * 1024 * 1024;
|
|
42
|
+
var MIN_FILE_SIZE = 1024;
|
|
43
|
+
var MAX_DIMENSION = 8192;
|
|
44
|
+
var MIN_DIMENSION = 10;
|
|
45
|
+
var MimeTypeSchema = z.enum(ALLOWED_MIME_TYPES);
|
|
46
|
+
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`);
|
|
47
|
+
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`);
|
|
48
|
+
var UploadRequestSchema = z.object({
|
|
49
|
+
filename: z.string().min(1, "Filename is required").max(255, "Filename too long").regex(/^[^<>:"/\\|?*]+$/, "Filename contains invalid characters"),
|
|
50
|
+
mimeType: MimeTypeSchema,
|
|
51
|
+
fileSize: FileSizeSchema
|
|
52
|
+
});
|
|
53
|
+
var ConfirmUploadSchema = z.object({
|
|
54
|
+
assetId: z.uuid().optional(),
|
|
55
|
+
width: DimensionSchema,
|
|
56
|
+
height: DimensionSchema
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// ../../packages/cms-schema/src/blocks/schemas/article-block.ts
|
|
60
|
+
import { z as z2 } from "zod";
|
|
61
|
+
var ArticleBlockContentSchema = z2.object({
|
|
62
|
+
headline: z2.string().min(1, "Article headline is required").max(300, "Headline too long").trim(),
|
|
63
|
+
author: z2.string().max(100, "Article author too long").trim().optional(),
|
|
64
|
+
publishedAt: z2.iso.datetime({ message: "Article publishedAt must be a valid ISO 8601 datetime string." }).optional(),
|
|
65
|
+
body: z2.string().min(1, "Article body content is required"),
|
|
66
|
+
tags: z2.array(z2.string()).optional(),
|
|
67
|
+
status: z2.enum(["draft", "review", "published"])
|
|
68
|
+
});
|
|
69
|
+
var ARTICLE_BLOCK_SCHEMA_NAME = "article";
|
|
70
|
+
|
|
71
|
+
// ../../packages/cms-schema/src/blocks/schemas/cta-block.ts
|
|
72
|
+
import { z as z3 } from "zod";
|
|
73
|
+
var CTAButtonSchema = z3.object({
|
|
74
|
+
text: z3.string().min(1, "Button text is required").max(50, "Button text too long"),
|
|
75
|
+
url: z3.string().refine(
|
|
76
|
+
(val) => {
|
|
77
|
+
if (val.startsWith("http://") || val.startsWith("https://")) {
|
|
78
|
+
try {
|
|
79
|
+
new URL(val);
|
|
80
|
+
return true;
|
|
81
|
+
} catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (val.startsWith("/")) {
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
if (val.startsWith("#")) {
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
return false;
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
message: "URL must be a valid full URL (http://... or https://...), relative path (/path), or anchor (#anchor)"
|
|
95
|
+
}
|
|
96
|
+
)
|
|
97
|
+
});
|
|
98
|
+
var CTABlockContentSchema = z3.object({
|
|
99
|
+
headline: z3.string().min(1, "Headline is required").max(100, "Headline too long"),
|
|
100
|
+
description: z3.string().max(500, "Description too long").optional(),
|
|
101
|
+
primaryButton: CTAButtonSchema,
|
|
102
|
+
secondaryButton: CTAButtonSchema.optional()
|
|
103
|
+
});
|
|
104
|
+
var CTA_BLOCK_SCHEMA_NAME = "cta-block";
|
|
105
|
+
|
|
106
|
+
// ../../packages/cms-schema/src/blocks/schemas/features-block.ts
|
|
107
|
+
import { z as z4 } from "zod";
|
|
108
|
+
var FeaturesLayout = ["grid", "list", "carousel"];
|
|
109
|
+
var FeatureItemSchema = z4.object({
|
|
110
|
+
icon: z4.string().max(50, "Icon name too long").optional(),
|
|
111
|
+
title: z4.string().min(1, "Title is required").max(100, "Title too long"),
|
|
112
|
+
description: z4.string().max(500, "Description too long").optional()
|
|
113
|
+
});
|
|
114
|
+
var FeaturesBlockContentSchema = z4.object({
|
|
115
|
+
title: z4.string().min(1, "Section title is required").max(100, "Title too long"),
|
|
116
|
+
subtitle: z4.string().max(200, "Subtitle too long").optional(),
|
|
117
|
+
features: z4.array(FeatureItemSchema).min(1, "At least one feature is required").max(6, "Maximum 6 features allowed"),
|
|
118
|
+
layout: z4.enum(FeaturesLayout).default("grid")
|
|
119
|
+
});
|
|
120
|
+
var FEATURES_BLOCK_SCHEMA_NAME = "features-block";
|
|
121
|
+
|
|
122
|
+
// ../../packages/cms-schema/src/blocks/schemas/hero-block.ts
|
|
123
|
+
import { z as z6 } from "zod";
|
|
124
|
+
|
|
125
|
+
// ../../packages/cms-schema/src/fields/complex/media.ts
|
|
126
|
+
import { z as z5 } from "zod";
|
|
127
|
+
var ImageAssetSchema = z5.object({
|
|
128
|
+
/** UUID primary key */
|
|
129
|
+
id: z5.uuid(),
|
|
130
|
+
/** R2/S3 storage URL for the original file */
|
|
131
|
+
url: z5.url(),
|
|
132
|
+
/** Image width in pixels */
|
|
133
|
+
width: DimensionSchema,
|
|
134
|
+
/** Image height in pixels */
|
|
135
|
+
height: DimensionSchema,
|
|
136
|
+
/** Original filename from upload */
|
|
137
|
+
originalFilename: z5.string().min(1).max(255),
|
|
138
|
+
/** MIME type (only web-safe formats allowed) */
|
|
139
|
+
mimeType: MimeTypeSchema,
|
|
140
|
+
/** File size in bytes */
|
|
141
|
+
fileSize: FileSizeSchema,
|
|
142
|
+
/** Base64-encoded tiny preview for blur-up loading */
|
|
143
|
+
lqip: z5.string().optional()
|
|
144
|
+
});
|
|
145
|
+
var HotspotSchema = z5.object({
|
|
146
|
+
x: z5.number().min(0).max(1),
|
|
147
|
+
y: z5.number().min(0).max(1)
|
|
148
|
+
});
|
|
149
|
+
var CropSchema = z5.object({
|
|
150
|
+
/** X coordinate of top-left corner in pixels */
|
|
151
|
+
x: z5.number().int().nonnegative(),
|
|
152
|
+
/** Y coordinate of top-left corner in pixels */
|
|
153
|
+
y: z5.number().int().nonnegative(),
|
|
154
|
+
/** Width of crop region in pixels (must be > 0) */
|
|
155
|
+
width: z5.number().int().positive(),
|
|
156
|
+
/** Height of crop region in pixels (must be > 0) */
|
|
157
|
+
height: z5.number().int().positive()
|
|
158
|
+
});
|
|
159
|
+
var ImageReferenceSchema = z5.object({
|
|
160
|
+
// Alt text is REQUIRED for accessibility
|
|
161
|
+
alt: z5.string().min(1, "Alt text is required for accessibility"),
|
|
162
|
+
// Optional metadata
|
|
163
|
+
caption: z5.string().max(500).optional(),
|
|
164
|
+
attribution: z5.string().max(255).optional(),
|
|
165
|
+
// Reference to the ImageAsset with stored transformation
|
|
166
|
+
_asset: z5.object({
|
|
167
|
+
id: z5.uuid(),
|
|
168
|
+
transformation: z5.string().nullable().optional()
|
|
169
|
+
})
|
|
170
|
+
});
|
|
171
|
+
var fileSchema = z5.object({
|
|
172
|
+
url: z5.url(),
|
|
173
|
+
name: z5.string(),
|
|
174
|
+
size: z5.number().int().positive(),
|
|
175
|
+
type: z5.string()
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// ../../packages/cms-schema/src/blocks/schemas/hero-block.ts
|
|
179
|
+
var HeroAlignment = ["left", "center", "right"];
|
|
180
|
+
var HeroBlockContentSchema = z6.object({
|
|
181
|
+
headline: z6.string().min(1, "Headline is required").max(100, "Headline too long"),
|
|
182
|
+
subheadline: z6.string().max(200, "Subheadline too long").optional(),
|
|
183
|
+
ctaText: z6.string().max(50, "CTA text too long").optional(),
|
|
184
|
+
ctaUrl: z6.string().refine(
|
|
185
|
+
(val) => {
|
|
186
|
+
if (val === "") return true;
|
|
187
|
+
if (val.startsWith("http://") || val.startsWith("https://")) {
|
|
188
|
+
try {
|
|
189
|
+
new URL(val);
|
|
190
|
+
return true;
|
|
191
|
+
} catch {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (val.startsWith("/")) {
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
if (val.startsWith("#")) {
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
return false;
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
message: "URL must be a valid full URL (http://... or https://...), relative path (/path), anchor (#anchor), or empty string"
|
|
205
|
+
}
|
|
206
|
+
).optional().or(z6.literal("")),
|
|
207
|
+
backgroundImage: ImageReferenceSchema.nullable().optional(),
|
|
208
|
+
alignment: z6.enum(HeroAlignment).default("center")
|
|
209
|
+
});
|
|
210
|
+
var HERO_BLOCK_SCHEMA_NAME = "hero-block";
|
|
211
|
+
|
|
212
|
+
// ../../packages/cms-schema/src/blocks/schemas/logo-trust-block.ts
|
|
213
|
+
import { z as z7 } from "zod";
|
|
214
|
+
var LogoItemSchema = z7.object({
|
|
215
|
+
/** Unique ID for this logo item */
|
|
216
|
+
id: z7.uuid(),
|
|
217
|
+
/** Image reference (alt + _asset with transformation) */
|
|
218
|
+
image: ImageReferenceSchema,
|
|
219
|
+
/** Optional company/brand name to display */
|
|
220
|
+
name: z7.string().max(100, "Name too long").optional()
|
|
221
|
+
});
|
|
222
|
+
var LegacyLogoItemSchema = z7.object({
|
|
223
|
+
/** Direct URL to the logo image */
|
|
224
|
+
url: z7.string(),
|
|
225
|
+
/** Alt text for the image */
|
|
226
|
+
alt: z7.string(),
|
|
227
|
+
/** Optional company/brand name */
|
|
228
|
+
name: z7.string().optional()
|
|
229
|
+
});
|
|
230
|
+
var LogoTrustBlockContentSchema = z7.object({
|
|
231
|
+
title: z7.string().max(100, "Title too long").optional(),
|
|
232
|
+
logos: z7.array(LogoItemSchema).max(20, "Maximum 20 logos allowed")
|
|
233
|
+
});
|
|
234
|
+
var LOGO_TRUST_BLOCK_SCHEMA_NAME = "logo-trust-block";
|
|
235
|
+
|
|
236
|
+
// ../../packages/cms-schema/src/blocks/registry.ts
|
|
237
|
+
var BLOCK_SCHEMA_NAMES = [
|
|
238
|
+
ARTICLE_BLOCK_SCHEMA_NAME,
|
|
239
|
+
HERO_BLOCK_SCHEMA_NAME,
|
|
240
|
+
FEATURES_BLOCK_SCHEMA_NAME,
|
|
241
|
+
CTA_BLOCK_SCHEMA_NAME,
|
|
242
|
+
LOGO_TRUST_BLOCK_SCHEMA_NAME
|
|
243
|
+
];
|
|
244
|
+
function isValidBlockSchemaName(name) {
|
|
245
|
+
return BLOCK_SCHEMA_NAMES.includes(name);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// lib/renderer.tsx
|
|
249
|
+
import { unstable_noStore } from "next/cache";
|
|
250
|
+
import { notFound } from "next/navigation";
|
|
251
|
+
import { jsx } from "react/jsx-runtime";
|
|
252
|
+
function getWebsiteId(providedWebsiteId) {
|
|
253
|
+
const websiteId = providedWebsiteId ?? process.env.NEXT_PUBLIC_WEBSITE_ID ?? process.env.WEBSITE_ID ?? process.env.CMS_WEBSITE_ID;
|
|
254
|
+
if (!websiteId) {
|
|
255
|
+
throw new Error(
|
|
256
|
+
"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."
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
260
|
+
if (!uuidRegex.test(websiteId)) {
|
|
261
|
+
throw new Error(
|
|
262
|
+
`Invalid websiteId "${websiteId}". Provide a valid UUID via prop or set NEXT_PUBLIC_WEBSITE_ID (or WEBSITE_ID/CMS_WEBSITE_ID).`
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
return websiteId;
|
|
266
|
+
}
|
|
267
|
+
var dynamic = "force-dynamic";
|
|
268
|
+
async function ParametricRoutePage({
|
|
269
|
+
params,
|
|
270
|
+
registry,
|
|
271
|
+
apiKey,
|
|
272
|
+
cmsUrl,
|
|
273
|
+
websiteId: providedWebsiteId
|
|
274
|
+
}) {
|
|
275
|
+
unstable_noStore();
|
|
276
|
+
const websiteId = getWebsiteId(providedWebsiteId);
|
|
277
|
+
const { slug } = await params;
|
|
278
|
+
const rawPath = `/${slug.join("/")}`;
|
|
279
|
+
const path = normalizePath(rawPath);
|
|
280
|
+
const client = getCmsClient({ apiKey, cmsUrl });
|
|
281
|
+
try {
|
|
282
|
+
const { route } = await client.route.getByPath.query({ websiteId, path });
|
|
283
|
+
if (route.state !== "Live") {
|
|
284
|
+
console.error(`Route found but not Live. Path: ${path}, State: ${route.state}`);
|
|
285
|
+
notFound();
|
|
286
|
+
}
|
|
287
|
+
const blockPromises = route.block_ids.map(async (blockId) => {
|
|
288
|
+
try {
|
|
289
|
+
const result = await client.block.getById.query({ websiteId, id: blockId });
|
|
290
|
+
return result.block;
|
|
291
|
+
} catch (error) {
|
|
292
|
+
console.error(`Failed to fetch block ${blockId}:`, error);
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
const blockResults = await Promise.all(blockPromises);
|
|
297
|
+
const blocks = [];
|
|
298
|
+
for (const block of blockResults) {
|
|
299
|
+
if (!block || block.published_content === null) continue;
|
|
300
|
+
const content = block.published_content;
|
|
301
|
+
if (!content) continue;
|
|
302
|
+
if (block.schema_name === "article") {
|
|
303
|
+
const article = normalizeArticleContent(content);
|
|
304
|
+
const isPublished = article ? isArticlePublished(article) : null;
|
|
305
|
+
if (article && isPublished) {
|
|
306
|
+
blocks.push({ id: block.id, type: "article", content: article });
|
|
307
|
+
}
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
if (!isValidBlockSchemaName(block.schema_name)) {
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
blocks.push({
|
|
314
|
+
id: block.id,
|
|
315
|
+
type: block.schema_name,
|
|
316
|
+
content
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
return /* @__PURE__ */ jsx("main", { children: blocks.map((block) => /* @__PURE__ */ jsx(BlockRenderer, { registry: registry ?? {}, block }, block.id)) });
|
|
320
|
+
} catch (error) {
|
|
321
|
+
console.error(`Route fetch error for path: ${path}`, error);
|
|
322
|
+
const errorCode = error instanceof Error && "data" in error ? error.data?.code : error instanceof Error && "code" in error ? error.code : void 0;
|
|
323
|
+
if (errorCode === "NOT_FOUND" || errorCode === "P0002") {
|
|
324
|
+
notFound();
|
|
325
|
+
}
|
|
326
|
+
throw error;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
async function generateMetadata({
|
|
330
|
+
params,
|
|
331
|
+
apiKey,
|
|
332
|
+
cmsUrl,
|
|
333
|
+
websiteId: providedWebsiteId
|
|
334
|
+
}) {
|
|
335
|
+
const websiteId = getWebsiteId(providedWebsiteId);
|
|
336
|
+
const { slug } = await params;
|
|
337
|
+
const rawPath = `/${slug.join("/")}`;
|
|
338
|
+
const path = normalizePath(rawPath);
|
|
339
|
+
const client = getCmsClient({ apiKey, cmsUrl });
|
|
340
|
+
try {
|
|
341
|
+
const { route } = await client.route.getByPath.query({ websiteId, path });
|
|
342
|
+
return {
|
|
343
|
+
title: `${route.path} | Website`,
|
|
344
|
+
description: `Content page: ${route.path}`
|
|
345
|
+
};
|
|
346
|
+
} catch {
|
|
347
|
+
return {
|
|
348
|
+
title: "Page Not Found | Website",
|
|
349
|
+
description: "The requested page could not be found."
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
function normalizePath(path) {
|
|
354
|
+
if (!path || path === "/") {
|
|
355
|
+
return "/";
|
|
356
|
+
}
|
|
357
|
+
let normalized = path.trim();
|
|
358
|
+
normalized = normalized.replace(/\/+$/, "");
|
|
359
|
+
if (!normalized.startsWith("/")) {
|
|
360
|
+
normalized = `/${normalized}`;
|
|
361
|
+
}
|
|
362
|
+
normalized = normalized.replace(/\/+/g, "/");
|
|
363
|
+
return normalized;
|
|
364
|
+
}
|
|
365
|
+
export {
|
|
366
|
+
ParametricRoutePage as default,
|
|
367
|
+
dynamic,
|
|
368
|
+
generateMetadata,
|
|
369
|
+
normalizePath
|
|
370
|
+
};
|
|
371
|
+
//# sourceMappingURL=renderer.js.map
|