cms-renderer 0.0.0 → 0.1.0

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.
Files changed (51) hide show
  1. package/dist/chunk-G22U6UHQ.js +45 -0
  2. package/dist/chunk-G22U6UHQ.js.map +1 -0
  3. package/dist/chunk-HVKFEZBT.js +116 -0
  4. package/dist/chunk-HVKFEZBT.js.map +1 -0
  5. package/dist/chunk-RPM73PQZ.js +17 -0
  6. package/dist/chunk-RPM73PQZ.js.map +1 -0
  7. package/dist/lib/block-renderer.d.ts +32 -0
  8. package/dist/lib/block-renderer.js +7 -0
  9. package/dist/lib/block-renderer.js.map +1 -0
  10. package/dist/lib/cms-api.d.ts +24 -0
  11. package/dist/lib/cms-api.js +7 -0
  12. package/dist/lib/cms-api.js.map +1 -0
  13. package/dist/lib/data-utils.d.ts +218 -0
  14. package/dist/lib/data-utils.js +247 -0
  15. package/dist/lib/data-utils.js.map +1 -0
  16. package/dist/lib/image/lazy-load.d.ts +75 -0
  17. package/dist/lib/image/lazy-load.js +83 -0
  18. package/dist/lib/image/lazy-load.js.map +1 -0
  19. package/dist/lib/markdown-utils.d.ts +172 -0
  20. package/dist/lib/markdown-utils.js +137 -0
  21. package/dist/lib/markdown-utils.js.map +1 -0
  22. package/dist/lib/renderer.d.ts +36 -0
  23. package/dist/lib/renderer.js +343 -0
  24. package/dist/lib/renderer.js.map +1 -0
  25. package/{lib/result.ts → dist/lib/result.d.ts} +32 -146
  26. package/dist/lib/result.js +37 -0
  27. package/dist/lib/result.js.map +1 -0
  28. package/dist/lib/schema.d.ts +15 -0
  29. package/dist/lib/schema.js +35 -0
  30. package/dist/lib/schema.js.map +1 -0
  31. package/{lib/trpc.ts → dist/lib/trpc.d.ts} +6 -4
  32. package/dist/lib/trpc.js +7 -0
  33. package/dist/lib/trpc.js.map +1 -0
  34. package/dist/lib/types.d.ts +163 -0
  35. package/dist/lib/types.js +1 -0
  36. package/dist/lib/types.js.map +1 -0
  37. package/package.json +50 -11
  38. package/.turbo/turbo-check-types.log +0 -2
  39. package/lib/__tests__/enrich-block-images.test.ts +0 -394
  40. package/lib/block-renderer.tsx +0 -60
  41. package/lib/cms-api.ts +0 -86
  42. package/lib/data-utils.ts +0 -572
  43. package/lib/image/lazy-load.ts +0 -209
  44. package/lib/markdown-utils.ts +0 -368
  45. package/lib/renderer.tsx +0 -189
  46. package/lib/schema.ts +0 -74
  47. package/lib/types.ts +0 -201
  48. package/next.config.ts +0 -39
  49. package/postcss.config.mjs +0 -5
  50. package/tsconfig.json +0 -12
  51. package/tsconfig.tsbuildinfo +0 -1
package/lib/renderer.tsx DELETED
@@ -1,189 +0,0 @@
1
- /**
2
- * Catch-all Route Handler for Parametric Routes
3
- *
4
- * Handles routes with multiple segments like /us/en/products.
5
- * Uses the CMS API to fetch and render blocks.
6
- */
7
-
8
- import {
9
- isArticlePublished,
10
- isValidBlockSchemaName,
11
- normalizeArticleContent,
12
- } from '@repo/cms-schema/blocks';
13
- import type { Metadata } from 'next';
14
- import { unstable_noStore } from 'next/cache';
15
- import { notFound } from 'next/navigation';
16
- import { BlockRenderer } from './block-renderer';
17
- import { getCmsClient } from './cms-api';
18
- import type { BlockComponentRegistry, BlockData } from './types';
19
-
20
- type PageProps = {
21
- params: Promise<{ slug: string[] }>;
22
- registry?: Partial<BlockComponentRegistry>;
23
- /** API key for CMS API authentication */
24
- apiKey?: string;
25
- };
26
-
27
- /**
28
- * Force dynamic rendering to ensure routes are always fresh.
29
- * This prevents Next.js from caching pages when routes are published.
30
- */
31
- export const dynamic = 'force-dynamic';
32
-
33
- /**
34
- * Catch-all route handler for parametric routes.
35
- *
36
- * Handles paths like:
37
- * - /us/en/products -> slug = ['us', 'en', 'products']
38
- * - /about -> slug = ['about']
39
- *
40
- * Reconstructs the full path and fetches route via tRPC.
41
- */
42
- export default async function ParametricRoutePage({ params, registry, apiKey }: PageProps) {
43
- // Prevent any caching - ensure we always fetch fresh route data
44
- unstable_noStore();
45
-
46
- const { slug } = await params;
47
-
48
- // Reconstruct full path from slug segments and normalize it
49
- const rawPath = `/${slug.join('/')}`;
50
- const path = normalizePath(rawPath);
51
-
52
- // Get CMS API client with optional API key
53
- const client = getCmsClient({ apiKey });
54
-
55
- try {
56
- // Fetch route by path via CMS API
57
- const { route } = await client.route.getByPath.query({ path });
58
-
59
- // Only show Live routes on public website
60
- if (route.state !== 'Live') {
61
- console.error(`Route found but not Live. Path: ${path}, State: ${route.state}`);
62
- notFound();
63
- }
64
-
65
- // Fetch all blocks by ID via CMS API (skip any that fail)
66
- const blockPromises = route.block_ids.map(async (blockId) => {
67
- try {
68
- const result = await client.block.getById.query({ id: blockId });
69
- return result.block;
70
- } catch (error) {
71
- // Log error but don't fail the entire page
72
- console.error(`Failed to fetch block ${blockId}:`, error);
73
- return null;
74
- }
75
- });
76
- const blockResults = await Promise.all(blockPromises);
77
-
78
- // Transform blocks to BlockData format for BlockRenderer
79
- // Filter out any blocks that failed to load
80
- const blocks: BlockData[] = [];
81
-
82
- for (const block of blockResults) {
83
- if (!block || block.published_content === null) continue;
84
-
85
- const content = block.published_content as Record<string, unknown> | null;
86
- if (!content) continue;
87
-
88
- // Handle 'article' blocks separately before checking schema type
89
- if (block.schema_name === 'article') {
90
- const article = normalizeArticleContent(content);
91
- const isPublished = article ? isArticlePublished(article) : null;
92
- if (article && isPublished) {
93
- blocks.push({ id: block.id, type: 'article', content: article });
94
- }
95
- continue;
96
- }
97
-
98
- // Skip blocks with invalid schema names (after handling 'article')
99
- if (!isValidBlockSchemaName(block.schema_name)) {
100
- continue;
101
- }
102
-
103
- // For all block types, map schema_name to type and include content
104
- // Image references are automatically resolved by block.getById
105
- blocks.push({
106
- id: block.id,
107
- type: block.schema_name,
108
- content,
109
- } as BlockData);
110
- }
111
-
112
- return (
113
- <main>
114
- {blocks.map((block) => (
115
- <BlockRenderer registry={registry ?? {}} key={block.id} block={block} />
116
- ))}
117
- </main>
118
- );
119
- } catch (error) {
120
- // Log error for debugging
121
- console.error(`Route fetch error for path: ${path}`, error);
122
-
123
- // If route not found or param validation fails, show 404
124
- // TRPCClientError has data.code for the error code
125
- const errorCode =
126
- error instanceof Error && 'data' in error
127
- ? (error as { data?: { code?: string } }).data?.code
128
- : error instanceof Error && 'code' in error
129
- ? (error as { code: string }).code
130
- : undefined;
131
-
132
- if (errorCode === 'NOT_FOUND' || errorCode === 'P0002') {
133
- notFound();
134
- }
135
-
136
- // Re-throw other errors
137
- throw error;
138
- }
139
- }
140
-
141
- // -----------------------------------------------------------------------------
142
- // Metadata
143
- // -----------------------------------------------------------------------------
144
-
145
- /**
146
- * Generate metadata for the page.
147
- * Uses Next.js 15+ async params pattern.
148
- */
149
- export async function generateMetadata({ params, apiKey }: PageProps): Promise<Metadata> {
150
- const { slug } = await params;
151
- const rawPath = `/${slug.join('/')}`;
152
- const path = normalizePath(rawPath);
153
- const client = getCmsClient({ apiKey });
154
-
155
- try {
156
- const { route } = await client.route.getByPath.query({ path });
157
- return {
158
- title: `${route.path} | Website`,
159
- description: `Content page: ${route.path}`,
160
- };
161
- } catch {
162
- return {
163
- title: 'Page Not Found | Website',
164
- description: 'The requested page could not be found.',
165
- };
166
- }
167
- }
168
-
169
- export function normalizePath(path: string): string {
170
- if (!path || path === '/') {
171
- return '/';
172
- }
173
-
174
- // Remove trailing slashes, ensure leading slash
175
- let normalized = path.trim();
176
-
177
- // Remove trailing slashes (but keep root "/")
178
- normalized = normalized.replace(/\/+$/, '');
179
-
180
- // Ensure leading slash
181
- if (!normalized.startsWith('/')) {
182
- normalized = `/${normalized}`;
183
- }
184
-
185
- // Collapse multiple consecutive slashes to single slash
186
- normalized = normalized.replace(/\/+/g, '/');
187
-
188
- return normalized;
189
- }
package/lib/schema.ts DELETED
@@ -1,74 +0,0 @@
1
- // Fetch schema from the CMS for a headless setup
2
- // This should look like this
3
- //
4
- // import { schema, configureSchema } from "@profound/cms";
5
- //
6
- // // Option 1: Use default config (reads from CMS_API_URL env var)
7
- // const footerData = schema.name("footer").fetchAll();
8
- //
9
- // // Option 2: Configure with custom API base and fetch options
10
- // const customSchema = configureSchema({
11
- // apiBase: "https://my-cms.example.com/api",
12
- // fetchOptions: { headers: { Authorization: "Bearer token" } },
13
- // });
14
- // const footerData = customSchema.name("footer").fetchAll();
15
- //
16
- // console.log(footerData); # [{ logo: "<cloudflare_signed_img_url>", "Footer_text": "Profound" }]
17
-
18
- const DEFAULT_API_BASE = process.env.NEXT_PUBLIC_CMS_API_URL ?? 'http://localhost:4000/api';
19
-
20
- interface SchemaConfig {
21
- apiBase?: string;
22
- fetchOptions?: RequestInit;
23
- }
24
-
25
- interface SchemaQuery {
26
- fetchAll: <T = Record<string, unknown>>() => Promise<T[]>;
27
- fetchSingle: <T = Record<string, unknown>>() => Promise<T | null>;
28
- }
29
-
30
- interface SchemaClient {
31
- name: (schemaName: string) => SchemaQuery;
32
- }
33
-
34
- function createSchemaQuery(
35
- apiBase: string,
36
- schemaName: string,
37
- fetchOptions: RequestInit = {}
38
- ): SchemaQuery {
39
- const baseUrl = `${apiBase}/schemas/${schemaName}`;
40
-
41
- return {
42
- async fetchAll<T = Record<string, unknown>>(): Promise<T[]> {
43
- const response = await fetch(baseUrl, fetchOptions);
44
-
45
- if (!response.ok) {
46
- throw new Error(`Failed to fetch schema "${schemaName}": ${response.statusText}`);
47
- }
48
-
49
- return response.json();
50
- },
51
-
52
- async fetchSingle<T = Record<string, unknown>>(): Promise<T | null> {
53
- const response = await fetch(`${baseUrl}?limit=1`, fetchOptions);
54
-
55
- if (!response.ok) {
56
- throw new Error(`Failed to fetch schema "${schemaName}": ${response.statusText}`);
57
- }
58
-
59
- const data = await response.json();
60
- return Array.isArray(data) ? (data[0] ?? null) : data;
61
- },
62
- };
63
- }
64
-
65
- export function configureSchema(config: SchemaConfig = {}): SchemaClient {
66
- const apiBase = config.apiBase || DEFAULT_API_BASE;
67
- const fetchOptions = config.fetchOptions || {};
68
-
69
- return {
70
- name: (schemaName: string): SchemaQuery => createSchemaQuery(apiBase, schemaName, fetchOptions),
71
- };
72
- }
73
-
74
- export const schema: SchemaClient = configureSchema();
package/lib/types.ts DELETED
@@ -1,201 +0,0 @@
1
- /**
2
- * Block Rendering Types
3
- *
4
- * Type definitions for the ComponentMap pattern.
5
- * Block types map to React components via the blockComponents registry.
6
- */
7
-
8
- import type { ComponentType } from 'react';
9
-
10
- // -----------------------------------------------------------------------------
11
- // Block Type Discriminant
12
- // -----------------------------------------------------------------------------
13
-
14
- /**
15
- * Valid block type strings.
16
- * Must match the schema_name field in block data from the tRPC API.
17
- */
18
- export type BlockType =
19
- | 'navigation'
20
- | 'header'
21
- | 'article'
22
- | 'hero-block'
23
- | 'features-block'
24
- | 'cta-block'
25
- | 'logo-trust-block';
26
-
27
- // -----------------------------------------------------------------------------
28
- // Content Types (inferred from seed data)
29
- // -----------------------------------------------------------------------------
30
-
31
- /**
32
- * Navigation link button structure.
33
- */
34
- export interface NavigationButton {
35
- label: string;
36
- href: string;
37
- ariaLabel: string;
38
- }
39
-
40
- /**
41
- * Navigation link with type and button.
42
- */
43
- export interface NavigationLink {
44
- button: NavigationButton;
45
- type: 'Default' | 'Flyout';
46
- }
47
-
48
- /**
49
- * Level 3 navigation item (leaf node).
50
- */
51
- export interface Level3Link {
52
- link: NavigationLink;
53
- }
54
-
55
- /**
56
- * Level 2 navigation item with optional Level 3 children.
57
- */
58
- export interface Level2Link {
59
- link: NavigationLink;
60
- children?: Level3Link[];
61
- }
62
-
63
- /**
64
- * Level 1 navigation item with optional Level 2 children.
65
- */
66
- export interface Level1Link {
67
- link: NavigationLink;
68
- children?: Level2Link[];
69
- }
70
-
71
- /**
72
- * Navigation block content.
73
- */
74
- export interface NavigationContent {
75
- logo?: {
76
- url: string;
77
- alt: string;
78
- };
79
- ariaLabel: string;
80
- links: Level1Link[];
81
- }
82
-
83
- /**
84
- * Header block content.
85
- */
86
- export interface HeaderContent {
87
- headline: string;
88
- subheadline?: string;
89
- backgroundImage?: {
90
- url: string;
91
- alt: string;
92
- };
93
- ctaButton?: {
94
- label: string;
95
- href: string;
96
- };
97
- alignment: 'left' | 'center' | 'right';
98
- }
99
-
100
- /**
101
- * Article block content.
102
- */
103
- export interface ArticleContent {
104
- headline: string;
105
- author?: string;
106
- publishedAt?: string;
107
- body: string;
108
- tags?: readonly string[] | string[];
109
- status?: string;
110
- }
111
-
112
- /**
113
- * Hero block content (from CMS schema).
114
- */
115
- export type HeroBlockContent = import('@repo/cms-schema/blocks').HeroBlockContent;
116
-
117
- /**
118
- * Features block content (from CMS schema).
119
- */
120
- export type FeaturesBlockContent = import('@repo/cms-schema/blocks').FeaturesBlockContent;
121
-
122
- /**
123
- * CTA block content (from CMS schema).
124
- */
125
- export type CTABlockContent = import('@repo/cms-schema/blocks').CTABlockContent;
126
-
127
- /**
128
- * Logo Trust block content (from CMS schema).
129
- */
130
- export type LogoTrustBlockContent = import('@repo/cms-schema/blocks').LogoTrustBlockContent;
131
-
132
- // -----------------------------------------------------------------------------
133
- // Block Data Union
134
- // -----------------------------------------------------------------------------
135
-
136
- /**
137
- * Discriminated union of all block types.
138
- * Use the `type` field to narrow to specific content types.
139
- *
140
- * Each block also carries its stable CMS `id`, which should be used as the
141
- * React `key` when rendering lists of blocks.
142
- *
143
- * @example
144
- * ```tsx
145
- * function renderBlock(block: BlockData) {
146
- * if (block.type === 'header') {
147
- * // TypeScript knows block.content is HeaderContent
148
- * return <h1>{block.content.headline}</h1>;
149
- * }
150
- * }
151
- * ```
152
- */
153
- export type BlockData =
154
- | { id: string; type: 'navigation'; content: NavigationContent }
155
- | { id: string; type: 'header'; content: HeaderContent }
156
- | { id: string; type: 'article'; content: ArticleContent }
157
- | { id: string; type: 'hero-block'; content: HeroBlockContent }
158
- | { id: string; type: 'features-block'; content: FeaturesBlockContent }
159
- | { id: string; type: 'cta-block'; content: CTABlockContent }
160
- | { id: string; type: 'logo-trust-block'; content: LogoTrustBlockContent };
161
-
162
- // -----------------------------------------------------------------------------
163
- // Component Types
164
- // -----------------------------------------------------------------------------
165
-
166
- /**
167
- * Props for a block component.
168
- * Each block component receives its typed content.
169
- */
170
- export interface BlockComponentProps<T> {
171
- content: T;
172
- }
173
-
174
- /**
175
- * A React component that renders a specific block type.
176
- */
177
- export type BlockComponent<T> = ComponentType<BlockComponentProps<T>>;
178
-
179
- /**
180
- * Registry of block type to component mappings.
181
- * This is the core of the ComponentMap pattern.
182
- */
183
- export type BlockComponentRegistry = {
184
- [K in BlockType]: BlockComponent<
185
- K extends 'navigation'
186
- ? NavigationContent
187
- : K extends 'header'
188
- ? HeaderContent
189
- : K extends 'article'
190
- ? ArticleContent
191
- : K extends 'hero-block'
192
- ? HeroBlockContent
193
- : K extends 'features-block'
194
- ? FeaturesBlockContent
195
- : K extends 'cta-block'
196
- ? CTABlockContent
197
- : K extends 'logo-trust-block'
198
- ? LogoTrustBlockContent
199
- : never
200
- >;
201
- };
package/next.config.ts DELETED
@@ -1,39 +0,0 @@
1
- import { resolve } from 'node:path';
2
- import type { NextConfig } from 'next';
3
-
4
- const nextConfig: NextConfig = {
5
- turbopack: {
6
- root: resolve(import.meta.dirname, '../..'),
7
- },
8
- transpilePackages: ['@repo/cms-schema', '@repo/markdown-wasm'],
9
- serverExternalPackages: ['md4w'],
10
- images: {
11
- remotePatterns: [
12
- {
13
- protocol: 'https',
14
- hostname: '**',
15
- },
16
- {
17
- protocol: 'http',
18
- hostname: 'localhost',
19
- },
20
- ],
21
- },
22
- // Prevent browser caching of dynamic pages
23
- async headers() {
24
- return [
25
- {
26
- // Apply to all pages
27
- source: '/:path*',
28
- headers: [
29
- {
30
- key: 'Cache-Control',
31
- value: 'no-store, must-revalidate',
32
- },
33
- ],
34
- },
35
- ];
36
- },
37
- };
38
-
39
- export default nextConfig;
@@ -1,5 +0,0 @@
1
- export default {
2
- plugins: {
3
- '@tailwindcss/postcss': {},
4
- },
5
- };
package/tsconfig.json DELETED
@@ -1,12 +0,0 @@
1
- {
2
- "extends": "@repo/typescript-config/nextjs.json",
3
- "compilerOptions": {
4
- "strict": true,
5
- "baseUrl": ".",
6
- "paths": {
7
- "@/*": ["./*"]
8
- }
9
- },
10
- "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
11
- "exclude": ["node_modules", "tests", "**/__tests__/**", "**/*.test.ts", "**/*.test.tsx", "e2e"]
12
- }