@estation/create-cms-site 1.0.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 (61) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +81 -0
  3. package/dist/index.d.ts +3 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +91 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/tools/list-sections.d.ts +19 -0
  8. package/dist/tools/list-sections.d.ts.map +1 -0
  9. package/dist/tools/list-sections.js +152 -0
  10. package/dist/tools/list-sections.js.map +1 -0
  11. package/dist/tools/scaffold-project.d.ts +11 -0
  12. package/dist/tools/scaffold-project.d.ts.map +1 -0
  13. package/dist/tools/scaffold-project.js +90 -0
  14. package/dist/tools/scaffold-project.js.map +1 -0
  15. package/dist/tools/validate-config.d.ts +9 -0
  16. package/dist/tools/validate-config.d.ts.map +1 -0
  17. package/dist/tools/validate-config.js +72 -0
  18. package/dist/tools/validate-config.js.map +1 -0
  19. package/dist/utils/helpers.d.ts +2 -0
  20. package/dist/utils/helpers.d.ts.map +1 -0
  21. package/dist/utils/helpers.js +5 -0
  22. package/dist/utils/helpers.js.map +1 -0
  23. package/package.json +36 -0
  24. package/template/.env.example +9 -0
  25. package/template/LIVE-PREVIEW.md +267 -0
  26. package/template/README.md +210 -0
  27. package/template/next.config.ts +14 -0
  28. package/template/package.json +24 -0
  29. package/template/postcss.config.mjs +8 -0
  30. package/template/src/app/[slug]/page.tsx +38 -0
  31. package/template/src/app/api/revalidate/route.ts +34 -0
  32. package/template/src/app/blog/[slug]/page.tsx +60 -0
  33. package/template/src/app/blog/page.tsx +68 -0
  34. package/template/src/app/collections/[uuid]/page.tsx +28 -0
  35. package/template/src/app/events/[slug]/page.tsx +62 -0
  36. package/template/src/app/events/page.tsx +70 -0
  37. package/template/src/app/layout.tsx +27 -0
  38. package/template/src/app/news/[slug]/page.tsx +66 -0
  39. package/template/src/app/news/page.tsx +68 -0
  40. package/template/src/app/page.tsx +40 -0
  41. package/template/src/app/search/page.tsx +136 -0
  42. package/template/src/app/sitemap.ts +48 -0
  43. package/template/src/components/Footer.tsx +47 -0
  44. package/template/src/components/Navigation.tsx +65 -0
  45. package/template/src/components/SectionRenderer.tsx +43 -0
  46. package/template/src/components/cms-preview-listener.tsx +114 -0
  47. package/template/src/components/sections/CTASection.tsx +32 -0
  48. package/template/src/components/sections/ContactSection.tsx +74 -0
  49. package/template/src/components/sections/FAQSection.tsx +48 -0
  50. package/template/src/components/sections/FeaturesSection.tsx +42 -0
  51. package/template/src/components/sections/GallerySection.tsx +44 -0
  52. package/template/src/components/sections/GenericSection.tsx +63 -0
  53. package/template/src/components/sections/HeroSection.tsx +27 -0
  54. package/template/src/components/sections/SliderSection.tsx +66 -0
  55. package/template/src/components/sections/TestimonialsSection.tsx +52 -0
  56. package/template/src/components/sections/TextSection.tsx +31 -0
  57. package/template/src/lib/cms-api.ts +103 -0
  58. package/template/src/lib/content-helpers.ts +18 -0
  59. package/template/src/lib/types.ts +109 -0
  60. package/template/src/styles/globals.css +1 -0
  61. package/template/tsconfig.json +23 -0
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@estation/create-cms-site",
3
+ "version": "1.0.0",
4
+ "description": "MCP server to scaffold Next.js sites powered by eSTATION CMS",
5
+ "type": "module",
6
+ "bin": {
7
+ "create-cms-site": "dist/index.js"
8
+ },
9
+ "main": "dist/index.js",
10
+ "files": [
11
+ "dist",
12
+ "template"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "dev": "tsc --watch",
17
+ "inspect": "npm run build && npx @modelcontextprotocol/inspector node dist/index.js"
18
+ },
19
+ "keywords": [
20
+ "mcp",
21
+ "cms",
22
+ "nextjs",
23
+ "scaffold",
24
+ "ctrla",
25
+ "estation"
26
+ ],
27
+ "license": "MIT",
28
+ "dependencies": {
29
+ "@modelcontextprotocol/sdk": "^1.0.0",
30
+ "zod": "^3.23.0"
31
+ },
32
+ "devDependencies": {
33
+ "@types/node": "^22.0.0",
34
+ "typescript": "^5.7.0"
35
+ }
36
+ }
@@ -0,0 +1,9 @@
1
+ # CMS API
2
+ CMS_API_URL=https://cms-gateway.estation.io/api/v1
3
+ CMS_API_TOKEN=your-tenant-api-token
4
+
5
+ # On-demand ISR revalidation
6
+ REVALIDATE_SECRET=your-revalidation-secret
7
+
8
+ # Public site URL (used for sitemap generation)
9
+ NEXT_PUBLIC_SITE_URL=https://your-site.com
@@ -0,0 +1,267 @@
1
+ # CMS Live Preview — Developer Guide
2
+
3
+ Real-time preview lets content editors see field changes instantly in the website template while editing in the CMS admin. This guide explains how the system works and how to implement it in your own components.
4
+
5
+ ## How It Works
6
+
7
+ ```
8
+ CMS Admin Website Template (iframe)
9
+ +--------------------------+ +----------------------------+
10
+ | AdminPageCompositionEditor| | CMSPreviewListener |
11
+ | | | |
12
+ | User types in a field | postMessage | Listens for "message" |
13
+ | ───────────────────────> | ─────────────> | events on window |
14
+ | | | |
15
+ | Resolves: | | Finds element by: |
16
+ | - blockTag (first tag) | | [data-cms-block="{tag}"] |
17
+ | - fieldName | | └─ [data-cms-field="…"] |
18
+ | - fieldType | | |
19
+ | - value (string) | | Updates DOM directly: |
20
+ | | | text → .textContent |
21
+ | | | richtext → .innerHTML |
22
+ | | | image → .src |
23
+ | | | list → <li> elements |
24
+ +--------------------------+ +----------------------------+
25
+ ```
26
+
27
+ The admin embeds your website in an `<iframe>`. When a content editor changes any field, the admin sends a `postMessage` to the iframe. The `CMSPreviewListener` component (already installed in the root layout) receives the message, finds the matching DOM element via data attributes, and updates it in place — no page reload required.
28
+
29
+ ## Message Protocol
30
+
31
+ ### `cms-preview-update`
32
+
33
+ Sent when a field value changes.
34
+
35
+ ```typescript
36
+ interface PreviewUpdateMessage {
37
+ type: "cms-preview-update";
38
+ blockTag: string; // First tag of the content block (e.g., "hero", "text", "faq")
39
+ fieldName: string; // Field key (e.g., "title", "description", "body")
40
+ fieldType: string; // "text" | "richtext" | "image" | "list"
41
+ value: string; // New value (string or JSON-stringified for lists)
42
+ }
43
+ ```
44
+
45
+ ### `cms-preview-highlight`
46
+
47
+ Sent when the editor hovers over or focuses a field in the admin UI.
48
+
49
+ ```typescript
50
+ interface PreviewHighlightMessage {
51
+ type: "cms-preview-highlight";
52
+ blockTag: string; // First tag of the content block
53
+ fieldName: string; // Field key
54
+ active: boolean; // true = show highlight, false = remove highlight
55
+ }
56
+ ```
57
+
58
+ When active, the target element gets a blue outline and is scrolled into view.
59
+
60
+ ## Data Attributes
61
+
62
+ The preview system relies on two HTML data attributes to locate elements:
63
+
64
+ | Attribute | Purpose | Applied to |
65
+ |-----------|---------|------------|
66
+ | `data-cms-block="{tag}"` | Identifies the content block | The wrapping `<section>` element |
67
+ | `data-cms-field="{fieldName}"` | Identifies the field within the block | The element rendering the field value |
68
+
69
+ The listener finds elements using this selector chain:
70
+
71
+ ```
72
+ document.querySelector(`[data-cms-block="${blockTag}"]`)
73
+ .querySelector(`[data-cms-field="${fieldName}"]`)
74
+ ```
75
+
76
+ ### `data-cms-block`
77
+
78
+ Applied automatically by `SectionRenderer.tsx` — you don't need to add this yourself:
79
+
80
+ ```tsx
81
+ // SectionRenderer.tsx — already handled
82
+ <section key={block.uuid} data-cms-block={tag}>
83
+ <Component block={block} />
84
+ </section>
85
+ ```
86
+
87
+ The `tag` value is `block.tags[0]` (the first tag on the content block).
88
+
89
+ ### `data-cms-field`
90
+
91
+ You must add this to every element in your component whose content comes from a CMS field. The attribute value must match the field key exactly.
92
+
93
+ ## How to Build a Live-Preview-Ready Component
94
+
95
+ ### Step 1: Create the component
96
+
97
+ ```tsx
98
+ // src/components/sections/MySection.tsx
99
+ import type { SectionProps } from "@/lib/types";
100
+ import { str } from "@/lib/types";
101
+
102
+ export function MySection({ block }: SectionProps) {
103
+ const c = block.content;
104
+
105
+ return (
106
+ <div className="py-16 px-6 max-w-3xl mx-auto">
107
+ {/* data-cms-field must match the field key in the CMS */}
108
+ <h2 data-cms-field="title" className="text-3xl font-bold mb-4">
109
+ {str(c.title, "Default Title")}
110
+ </h2>
111
+
112
+ <p data-cms-field="subtitle" className="text-gray-600 mb-6">
113
+ {str(c.subtitle)}
114
+ </p>
115
+
116
+ {/* Richtext: use dangerouslySetInnerHTML */}
117
+ <div
118
+ data-cms-field="body"
119
+ className="prose prose-gray max-w-none"
120
+ dangerouslySetInnerHTML={{ __html: str(c.body) }}
121
+ />
122
+
123
+ {/* Image: must be on an <img> element */}
124
+ <img
125
+ data-cms-field="heroImage"
126
+ src={str(c.heroImage)}
127
+ alt={str(c.title)}
128
+ className="w-full rounded-lg mt-8"
129
+ />
130
+ </div>
131
+ );
132
+ }
133
+ ```
134
+
135
+ ### Step 2: Register in SectionRenderer
136
+
137
+ ```tsx
138
+ // src/components/SectionRenderer.tsx
139
+ import { MySection } from "./sections/MySection";
140
+
141
+ const SECTION_MAP: Record<string, React.FC<SectionProps>> = {
142
+ // ... existing entries
143
+ "my-section": MySection,
144
+ };
145
+ ```
146
+
147
+ The key in `SECTION_MAP` must match the first tag you'll assign to this block type in the CMS.
148
+
149
+ ### Step 3: Create content in the CMS
150
+
151
+ 1. Create a content block with tag `my-section`
152
+ 2. Add fields: `title` (text), `subtitle` (text), `body` (richtext), `heroImage` (image)
153
+ 3. Add the block to a page composition
154
+ 4. Open the page editor — the preview iframe shows your component with live updates
155
+
156
+ ## Field Type Handling
157
+
158
+ The preview listener updates DOM elements differently based on `fieldType`:
159
+
160
+ | `fieldType` | Update method | Element requirement |
161
+ |-------------|--------------|---------------------|
162
+ | `text` | `el.textContent = value` | Any element (`<h1>`, `<p>`, `<span>`, etc.) |
163
+ | `richtext` | `el.innerHTML = value` | Any element (typically a `<div>`) |
164
+ | `image` | `el.src = value` | Must be an `<img>` element |
165
+ | `list` | Parses JSON, renders `<li>` elements | Typically a `<ul>` or `<div>` |
166
+ | *(default)* | `el.textContent = value` | Any element |
167
+
168
+ ### Text fields
169
+
170
+ ```tsx
171
+ <h2 data-cms-field="title">{str(c.title)}</h2>
172
+ <p data-cms-field="description">{str(c.description)}</p>
173
+ ```
174
+
175
+ ### Richtext fields
176
+
177
+ Use `dangerouslySetInnerHTML` so the preview listener can update via `innerHTML`:
178
+
179
+ ```tsx
180
+ <div
181
+ data-cms-field="body"
182
+ className="prose max-w-none"
183
+ dangerouslySetInnerHTML={{ __html: str(c.body) }}
184
+ />
185
+ ```
186
+
187
+ ### Image fields
188
+
189
+ Must use an `<img>` tag — the listener checks `el instanceof HTMLImageElement` before setting `.src`:
190
+
191
+ ```tsx
192
+ <img data-cms-field="featuredImage" src={str(c.featuredImage)} alt="..." />
193
+ ```
194
+
195
+ ### List fields
196
+
197
+ The listener receives a JSON string, parses it, and renders `<li>` elements. For basic lists, wrap in a `<ul>`:
198
+
199
+ ```tsx
200
+ <ul data-cms-field="items" className="list-disc pl-6">
201
+ {items.map((item, i) => (
202
+ <li key={i}>{item.title}</li>
203
+ ))}
204
+ </ul>
205
+ ```
206
+
207
+ > **Note:** List preview updates replace the entire inner HTML with simple `<li>` elements. For complex list rendering (cards, grids), the live preview will show a simplified version — the full rendering appears after save + page reload.
208
+
209
+ ## Existing Components Reference
210
+
211
+ These built-in components are already wired for live preview:
212
+
213
+ | Component | Tag | Fields with `data-cms-field` |
214
+ |-----------|-----|------------------------------|
215
+ | HeroSection | `hero` | `header`, `description`, `buttonText` |
216
+ | TextSection | `text` | `title`, `subtitle`, `body` |
217
+ | FeaturesSection | `features` | `title`, `description`, `items` |
218
+ | FAQSection | `faq` | `title`, `items` |
219
+ | TestimonialsSection | `testimonials` | `title`, `items` |
220
+ | CTASection | `cta` | `title`, `description`, `buttonText` |
221
+ | SliderSection | `slider` | `items` |
222
+ | GallerySection | `gallery` | `title`, `items` |
223
+ | ContactSection | `contact` | `title`, `description`, `email`, `phone`, `address` |
224
+
225
+ ## Highlight Behavior
226
+
227
+ When the content editor hovers over a field in the admin panel, the preview listener:
228
+
229
+ 1. Injects a CSS style (once) for `.cms-preview-highlight` — a 2px blue outline with 2px offset
230
+ 2. Finds the matching element via `data-cms-block` + `data-cms-field`
231
+ 3. Adds the `cms-preview-highlight` class and scrolls the element into view
232
+ 4. Removes the class when the editor moves away
233
+
234
+ No extra markup is needed in your components — this works automatically if you add `data-cms-field` attributes.
235
+
236
+ ## Checklist
237
+
238
+ When building a new section component, verify:
239
+
240
+ - [ ] Every visible CMS field has `data-cms-field="{fieldKey}"` on the element that displays it
241
+ - [ ] The `fieldKey` in the attribute exactly matches the field name in the CMS content block
242
+ - [ ] Richtext fields use `dangerouslySetInnerHTML`
243
+ - [ ] Image fields are on `<img>` elements
244
+ - [ ] The component is registered in `SectionRenderer.tsx` with the block tag as key
245
+ - [ ] The block tag in the CMS matches the key in `SECTION_MAP`
246
+
247
+ ## Debugging Preview
248
+
249
+ If live preview isn't working:
250
+
251
+ 1. **Check the iframe loads** — open your site URL directly, it should render
252
+ 2. **Inspect data attributes** — in DevTools, verify `data-cms-block` and `data-cms-field` exist on the right elements
253
+ 3. **Monitor messages** — add to your browser console in the iframe:
254
+ ```js
255
+ window.addEventListener("message", (e) => console.log("CMS message:", e.data));
256
+ ```
257
+ 4. **Verify the tag** — the `blockTag` in the message must match the `data-cms-block` value. The admin uses `block.tags[0]` as the tag
258
+ 5. **Check the field name** — the `fieldName` must exactly match the `data-cms-field` value
259
+
260
+ ## Architecture Notes
261
+
262
+ - **No backend required** — preview is purely client-side via `window.postMessage`
263
+ - **No WebSocket/SSE** — the iframe and parent communicate directly
264
+ - **Changes are not persisted** — preview updates are DOM-only. The actual save happens when the editor clicks Save in the admin
265
+ - **`CMSPreviewListener` is already in the root layout** — you don't need to add it to individual pages
266
+ - **Server components work fine** — the preview listener is a client component that runs alongside server-rendered content. It patches the DOM after hydration
267
+ - **Sandbox restrictions** — the iframe uses `sandbox="allow-same-origin allow-scripts"` which is sufficient for postMessage communication
@@ -0,0 +1,210 @@
1
+ # CMS Website Template
2
+
3
+ A full-featured Next.js 15 website template that renders content from the eSTATION CMS. Includes SSR + ISR, real-time live preview, blog, news, events, search, collections, dynamic navigation/footer, SEO metadata, and sitemap generation.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ cp .env.example .env.local
9
+ # Edit .env.local with your CMS credentials
10
+
11
+ npm install
12
+ npm run dev
13
+ ```
14
+
15
+ Open [http://localhost:3000](http://localhost:3000).
16
+
17
+ ## Environment Variables
18
+
19
+ | Variable | Description |
20
+ |----------|-------------|
21
+ | `CMS_API_URL` | CMS API base URL (e.g., `https://cms-gateway.estation.io/api/v1`) |
22
+ | `CMS_API_TOKEN` | Your tenant's API token |
23
+ | `REVALIDATE_SECRET` | Secret for on-demand ISR revalidation webhook |
24
+ | `NEXT_PUBLIC_SITE_URL` | Public site URL for sitemap generation (e.g., `https://your-site.com`) |
25
+
26
+ ## Architecture
27
+
28
+ ```
29
+ src/
30
+ ├── app/
31
+ │ ├── page.tsx # Homepage (slug: "index")
32
+ │ ├── [slug]/page.tsx # Dynamic CMS pages
33
+ │ ├── blog/
34
+ │ │ ├── page.tsx # Blog listing
35
+ │ │ └── [slug]/page.tsx # Blog detail
36
+ │ ├── news/
37
+ │ │ ├── page.tsx # News listing
38
+ │ │ └── [slug]/page.tsx # News detail
39
+ │ ├── events/
40
+ │ │ ├── page.tsx # Events listing
41
+ │ │ └── [slug]/page.tsx # Event detail
42
+ │ ├── search/page.tsx # Search page
43
+ │ ├── collections/[uuid]/page.tsx # Collection results
44
+ │ ├── sitemap.ts # Dynamic sitemap
45
+ │ ├── layout.tsx # Root layout (nav + footer)
46
+ │ └── api/revalidate/route.ts # ISR revalidation endpoint
47
+ ├── components/
48
+ │ ├── Navigation.tsx # Dynamic navigation
49
+ │ ├── Footer.tsx # Dynamic footer
50
+ │ ├── SectionRenderer.tsx # Block → component mapper
51
+ │ ├── cms-preview-listener.tsx # Live preview handler
52
+ │ └── sections/ # Section components
53
+ │ ├── HeroSection.tsx
54
+ │ ├── TextSection.tsx
55
+ │ ├── FeaturesSection.tsx
56
+ │ ├── FAQSection.tsx
57
+ │ ├── TestimonialsSection.tsx
58
+ │ ├── CTASection.tsx
59
+ │ ├── SliderSection.tsx
60
+ │ ├── GallerySection.tsx
61
+ │ ├── ContactSection.tsx
62
+ │ └── GenericSection.tsx
63
+ └── lib/
64
+ ├── types.ts # TypeScript interfaces
65
+ ├── cms-api.ts # CMS API client
66
+ └── content-helpers.ts # Utility functions
67
+ ```
68
+
69
+ ## Section Components
70
+
71
+ | Component | Block Tag(s) | Fields |
72
+ |-----------|-------------|--------|
73
+ | HeroSection | `hero` | `header`, `description`, `buttonText`, `buttonLink` |
74
+ | TextSection | `text` | `title`, `subtitle`, `body` (richtext) |
75
+ | FeaturesSection | `features`, `feature` | `title`, `description`, `items` (list) |
76
+ | FAQSection | `faq` | `title`, `items` (list: `question`, `answer`) |
77
+ | TestimonialsSection | `testimonials`, `testimonial` | `title`, `items` (list: `name`, `quote`, `role`) |
78
+ | CTASection | `cta` | `title`, `description`, `buttonText`, `buttonLink` |
79
+ | SliderSection | `slider`, `sliders` | `items` (list: `title`, `subtitle`, `image`, `linkUrl`, `linkText`) |
80
+ | GallerySection | `gallery` | `title`, `items` (list: `image`, `caption`) |
81
+ | ContactSection | `contact`, `form` | `title`, `description`, `email`, `phone`, `address` |
82
+ | GenericSection | (fallback) | Renders all fields dynamically |
83
+
84
+ ## Content Types
85
+
86
+ ### Blog Posts
87
+
88
+ - **CMS tag:** `blogs`
89
+ - **Fields:** `slug`, `title`, `author`, `publishDate`, `featuredImage`, `excerpt`, `content` (richtext)
90
+ - **Routes:** `/blog` (listing), `/blog/[slug]` (detail)
91
+
92
+ ### News Articles
93
+
94
+ - **CMS tag:** `news`
95
+ - **Fields:** `slug`, `title`, `author`, `category`, `publishDate`, `featuredImage`, `excerpt`, `content` (richtext)
96
+ - **Routes:** `/news` (listing), `/news/[slug]` (detail)
97
+
98
+ ### Events
99
+
100
+ - **CMS tag:** `events`
101
+ - **Fields:** `slug`, `title`, `description`, `location`, `startDate`, `endDate`, `organizer`, `content` (richtext)
102
+ - **Routes:** `/events` (listing), `/events/[slug]` (detail)
103
+
104
+ ## Routing
105
+
106
+ | Route | Source |
107
+ |-------|--------|
108
+ | `/` | Homepage — CMS page with slug `index` |
109
+ | `/[slug]` | Dynamic CMS pages |
110
+ | `/blog` | Blog listing |
111
+ | `/blog/[slug]` | Blog post detail |
112
+ | `/news` | News listing |
113
+ | `/news/[slug]` | News article detail |
114
+ | `/events` | Events listing |
115
+ | `/events/[slug]` | Event detail |
116
+ | `/search?q=...` | Search results |
117
+ | `/collections/[uuid]` | Collection execution results |
118
+ | `/sitemap.xml` | Auto-generated sitemap |
119
+
120
+ ## Navigation
121
+
122
+ The `Navigation` component fetches blocks tagged `navigation` from the CMS. Expected field structure:
123
+
124
+ - `items` (list): each item has `label` and `href`
125
+
126
+ If no `navigation` block exists, it auto-generates links from all published CMS pages.
127
+
128
+ ## Footer
129
+
130
+ The `Footer` component fetches blocks tagged `footer`. Expected field structure:
131
+
132
+ - `copyright` (text)
133
+ - `links` (list): each item has `label` and `href`
134
+
135
+ Falls back to a default copyright notice if no footer block exists.
136
+
137
+ ## SEO
138
+
139
+ Every page generates `<title>` and `<meta name="description">` from CMS content:
140
+
141
+ - Homepage and dynamic pages use the page composition's `title` and `description`
142
+ - Blog, news, and event detail pages use the block's `title` and `excerpt`/`description`
143
+
144
+ ## Sitemap
145
+
146
+ `/sitemap.xml` is dynamically generated from:
147
+
148
+ - All published CMS pages
149
+ - All blog posts, news articles, and events with slugs
150
+
151
+ Set `NEXT_PUBLIC_SITE_URL` to your production URL for correct sitemap URLs.
152
+
153
+ ## Search
154
+
155
+ `/search` provides server-side search. Query parameters:
156
+
157
+ - `q` — search query (required)
158
+ - `type` — filter by `block` or `page` (optional)
159
+ - `page` — pagination (optional)
160
+
161
+ Results link to the appropriate content type page.
162
+
163
+ ## Collections
164
+
165
+ `/collections/[uuid]` executes a CMS collection query and renders the resulting blocks through `SectionRenderer`.
166
+
167
+ ## On-Demand Revalidation
168
+
169
+ `POST /api/revalidate` with JSON body:
170
+
171
+ ```json
172
+ { "secret": "your-secret", "slug": "about" }
173
+ { "secret": "your-secret", "tag": "tag-hero" }
174
+ { "secret": "your-secret", "type": "blogs" }
175
+ { "secret": "your-secret" }
176
+ ```
177
+
178
+ - `slug` — revalidate a specific page
179
+ - `tag` — revalidate a specific cache tag
180
+ - `type` — revalidate a content type (`blogs`, `news`, `events`)
181
+ - No target — revalidates the entire site
182
+
183
+ ## CMS Preview Protocol
184
+
185
+ When embedded in the CMS admin iframe, the template supports real-time preview via `postMessage`:
186
+
187
+ - **`cms-preview-update`** — updates field content in-place (text, richtext, image, list)
188
+ - **`cms-preview-highlight`** — highlights the targeted field with a blue outline
189
+
190
+ Elements are targeted via `data-cms-block="{tag}"` and `data-cms-field="{fieldName}"` attributes.
191
+
192
+ ## Customization
193
+
194
+ 1. Add new section components in `src/components/sections/`
195
+ 2. Register them in `SectionRenderer.tsx`'s `SECTION_MAP` with the block tag as key
196
+ 3. Each component receives `block: ContentBlock` with `block.content` containing field data
197
+ 4. Use `str(block.content.fieldName)` to safely extract string field values
198
+
199
+ ## API Reference (`src/lib/cms-api.ts`)
200
+
201
+ | Function | Description |
202
+ |----------|-------------|
203
+ | `getPageBySlug(slug)` | Get page composition with resolved blocks |
204
+ | `getBlocksByTags(tags)` | Get all blocks matching any of the given tags |
205
+ | `getBlocksByTagPaginated(tag, page, size)` | Paginated blocks by tag |
206
+ | `getBlockByUUID(uuid)` | Get a single block by UUID |
207
+ | `getAllPages()` | Get all published page compositions |
208
+ | `executeCollection(uuid)` | Execute a collection query and get results |
209
+ | `getCollections()` | List all collections |
210
+ | `searchContent(query, type, page, size)` | Search across content |
@@ -0,0 +1,14 @@
1
+ import type { NextConfig } from "next";
2
+
3
+ const nextConfig: NextConfig = {
4
+ images: {
5
+ remotePatterns: [
6
+ {
7
+ protocol: "https",
8
+ hostname: "**.amazonaws.com",
9
+ },
10
+ ],
11
+ },
12
+ };
13
+
14
+ export default nextConfig;
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "cms-website-template",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev --turbopack",
7
+ "build": "next build",
8
+ "start": "next start",
9
+ "lint": "next lint"
10
+ },
11
+ "dependencies": {
12
+ "next": "^15.3.2",
13
+ "react": "^19.0.0",
14
+ "react-dom": "^19.0.0"
15
+ },
16
+ "devDependencies": {
17
+ "@tailwindcss/postcss": "^4.1.0",
18
+ "@types/node": "^22.0.0",
19
+ "@types/react": "^19.0.0",
20
+ "@types/react-dom": "^19.0.0",
21
+ "tailwindcss": "^4.1.0",
22
+ "typescript": "^5.7.0"
23
+ }
24
+ }
@@ -0,0 +1,8 @@
1
+ /** @type {import('postcss-load-config').Config} */
2
+ const config = {
3
+ plugins: {
4
+ "@tailwindcss/postcss": {},
5
+ },
6
+ };
7
+
8
+ export default config;
@@ -0,0 +1,38 @@
1
+ import { getPageBySlug } from "@/lib/cms-api";
2
+ import { SectionRenderer } from "@/components/SectionRenderer";
3
+ import type { ContentBlock } from "@/lib/types";
4
+ import type { Metadata } from "next";
5
+
6
+ interface PageProps {
7
+ params: Promise<{ slug: string }>;
8
+ }
9
+
10
+ export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
11
+ const { slug } = await params;
12
+ try {
13
+ const data = await getPageBySlug(slug);
14
+ return {
15
+ title: data.page.title || slug,
16
+ description: data.page.description || undefined,
17
+ };
18
+ } catch {
19
+ return { title: slug };
20
+ }
21
+ }
22
+
23
+ export default async function DynamicPage({ params }: PageProps) {
24
+ const { slug } = await params;
25
+ const data = await getPageBySlug(slug);
26
+ const orderedBlocks = getOrderedBlocks(data.page.blocks, data.blocks);
27
+
28
+ return <SectionRenderer blocks={orderedBlocks} />;
29
+ }
30
+
31
+ function getOrderedBlocks(
32
+ blockUuids: string[],
33
+ blocksMap: Record<string, ContentBlock>
34
+ ): ContentBlock[] {
35
+ return blockUuids
36
+ .map((uuid) => blocksMap[uuid])
37
+ .filter(Boolean);
38
+ }
@@ -0,0 +1,34 @@
1
+ import { revalidateTag, revalidatePath } from "next/cache";
2
+ import { NextRequest, NextResponse } from "next/server";
3
+
4
+ export async function POST(request: NextRequest) {
5
+ const body = await request.json().catch(() => null);
6
+ if (!body) {
7
+ return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
8
+ }
9
+
10
+ const { secret, slug, tag, type } = body;
11
+
12
+ if (secret !== process.env.REVALIDATE_SECRET) {
13
+ return NextResponse.json({ error: "Invalid secret" }, { status: 401 });
14
+ }
15
+
16
+ if (tag) {
17
+ revalidateTag(tag);
18
+ return NextResponse.json({ revalidated: true, tag });
19
+ }
20
+
21
+ if (type) {
22
+ revalidateTag(`tag-${type}`);
23
+ return NextResponse.json({ revalidated: true, type });
24
+ }
25
+
26
+ if (slug) {
27
+ revalidateTag(`page-${slug}`);
28
+ return NextResponse.json({ revalidated: true, slug });
29
+ }
30
+
31
+ // No specific target — revalidate everything
32
+ revalidatePath("/", "layout");
33
+ return NextResponse.json({ revalidated: true, all: true });
34
+ }