rankrunners-cms 0.0.17 → 0.0.19
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 +722 -124
- package/package.json +8 -9
- package/src/api/client/pages.ts +57 -0
- package/src/editor/render/PageRendererContent.tsx +2 -51
- package/src/editor/render/PageRendererSSR.tsx +15 -0
- package/src/editor/render/Preview.tsx +1 -4
- package/src/tanstack/editor/Editor.tsx +3 -5
- package/src/tanstack/editor/PageRenderer.tsx +4 -0
package/README.md
CHANGED
|
@@ -1,200 +1,798 @@
|
|
|
1
1
|
# RankRunners CMS Integration Guide
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A comprehensive guide for integrating `rankrunners-cms` into TanStack Start websites with a block-based visual editor powered by [Puck](https://puckeditor.com).
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
1. [Overview](#1-overview)
|
|
8
|
+
2. [Sample Projects](#2-sample-projects)
|
|
9
|
+
3. [Installation](#3-installation)
|
|
10
|
+
4. [Project Structure](#4-project-structure)
|
|
11
|
+
5. [Setting Up the Editor](#5-setting-up-the-editor)
|
|
12
|
+
6. [Creating Component Blocks](#6-creating-component-blocks)
|
|
13
|
+
7. [Creating Page Data](#7-creating-page-data)
|
|
14
|
+
8. [Setting Up Routes](#8-setting-up-routes)
|
|
15
|
+
9. [Sitemap & Robots.txt](#9-sitemap--robotstxt)
|
|
16
|
+
10. [Best Practices](#10-best-practices)
|
|
17
|
+
11. [Developer Checklist](#11-developer-checklist)
|
|
18
|
+
12. [Troubleshooting](#12-troubleshooting)
|
|
19
|
+
|
|
20
|
+
---
|
|
4
21
|
|
|
5
22
|
## 1. Overview
|
|
6
23
|
|
|
7
|
-
`rankrunners-cms` is a specialized library
|
|
24
|
+
`rankrunners-cms` is a specialized library that provides:
|
|
25
|
+
|
|
26
|
+
- **Visual Block Editor**: Real-time content editing using Puck
|
|
27
|
+
- **SEO Management**: Automatic meta tags and script injection
|
|
28
|
+
- **Reusable Components**: Pattern for creating editable component blocks
|
|
29
|
+
- **Page Renderer**: Automatic page rendering from initial data
|
|
30
|
+
|
|
31
|
+
### Key Concepts
|
|
32
|
+
|
|
33
|
+
- **Blocks**: Editable components with configurable fields
|
|
34
|
+
- **Initial Data**: Default page content that can be edited in the CMS
|
|
35
|
+
- **Root Config**: Layout wrapper (Header, Footer) applied to all pages
|
|
36
|
+
- **Page Renderer**: Component that renders pages based on URL path
|
|
8
37
|
|
|
9
38
|
---
|
|
10
39
|
|
|
11
|
-
## 2.
|
|
40
|
+
## 2. Sample Projects
|
|
41
|
+
|
|
42
|
+
**Only TypeScript-based projects are supported. JavaScript projects are not supported.**
|
|
43
|
+
|
|
44
|
+
There are two implementations of the CMS support library as of now:
|
|
45
|
+
1. **The TanStack Start implementation**: This is the recommended implementation. Use this for new projects. Full support for `HeaderScripts` and `FooterScripts`.
|
|
46
|
+
2. **The Next.js implementation**: This implementation exists for legacy sites. There are outstanding issues with the SEO integration. Next.js hydration does not load the script tags in time, and often removes them after hydration. There are many issues with Next.js script tags. `HeaderScripts` and `FooterScripts` are completely impossible to implement.
|
|
47
|
+
|
|
48
|
+
The following projects use the Next.js implementation:
|
|
49
|
+
- [Dependable Roofing](https://github.com/rankrunners-dev/dependable-roofing)
|
|
50
|
+
- [Pristine Roofing](https://github.com/rankrunners-dev/pristine-roofing)
|
|
51
|
+
|
|
52
|
+
The following projects use the TanStack Start implementation:
|
|
53
|
+
- [A+ Handyman Services](https://github.com/rankrunners-dev/aplus-handyman-services)
|
|
54
|
+
|
|
55
|
+
## 3. Installation
|
|
56
|
+
|
|
57
|
+
### Add Dependencies
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# Using bun
|
|
61
|
+
bun add rankrunners-cms @puckeditor/core
|
|
62
|
+
|
|
63
|
+
# Using npm
|
|
64
|
+
npm install rankrunners-cms @puckeditor/core
|
|
65
|
+
```
|
|
12
66
|
|
|
13
|
-
|
|
67
|
+
### Required Peer Dependencies
|
|
68
|
+
|
|
69
|
+
Ensure you have TanStack Start and Router installed:
|
|
14
70
|
|
|
15
71
|
```bash
|
|
16
|
-
bun add
|
|
72
|
+
bun add @tanstack/react-router @tanstack/react-start
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Environment Variables
|
|
76
|
+
|
|
77
|
+
Add the following to your `.env` file:
|
|
78
|
+
|
|
79
|
+
```env
|
|
80
|
+
VITE_PUBLIC_SITE_ID=your-site-id
|
|
17
81
|
```
|
|
18
82
|
|
|
83
|
+
> **Important**: Also add `VITE_PUBLIC_SITE_ID` to your Dockerfile for production builds.
|
|
84
|
+
|
|
19
85
|
---
|
|
20
86
|
|
|
21
|
-
##
|
|
87
|
+
## 4. Project Structure
|
|
22
88
|
|
|
23
|
-
|
|
89
|
+
Create the following structure in your `src/` directory:
|
|
24
90
|
|
|
25
|
-
|
|
26
|
-
|
|
91
|
+
```
|
|
92
|
+
src/
|
|
93
|
+
├── editor/
|
|
94
|
+
│ ├── index.ts # Main config export
|
|
95
|
+
│ ├── root.tsx # Root layout (Header/Footer wrapper)
|
|
96
|
+
│ ├── types.ts # TypeScript type definitions
|
|
97
|
+
│ ├── blocks/
|
|
98
|
+
│ │ ├── main-page-blocks.tsx # Home page specific blocks
|
|
99
|
+
│ │ ├── page-blocks.tsx # General page blocks
|
|
100
|
+
│ │ └── service-page-blocks.tsx # Service-specific blocks, etc.
|
|
101
|
+
│ └── initial-data/
|
|
102
|
+
│ ├── index.ts # Exports all page data
|
|
103
|
+
│ ├── initial.ts # Maps paths to page data
|
|
104
|
+
│ ├── index-page.ts # Home page data
|
|
105
|
+
│ ├── contact-page.ts # Contact page data
|
|
106
|
+
│ └── ... # Other page data files
|
|
107
|
+
├── components/
|
|
108
|
+
│ ├── Hero.tsx # React components
|
|
109
|
+
│ ├── TrustBar.tsx
|
|
110
|
+
│ └── ...
|
|
111
|
+
└── routes/
|
|
112
|
+
├── __root.tsx # Root route with SEO
|
|
113
|
+
├── $.ts # Catch-all route for CMS pages
|
|
114
|
+
└── ...
|
|
115
|
+
```
|
|
27
116
|
|
|
28
|
-
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## 5. Setting Up the Editor
|
|
120
|
+
|
|
121
|
+
### 5.1 Root Configuration (`src/editor/root.tsx`)
|
|
122
|
+
|
|
123
|
+
The root config wraps all pages with your site's Header and Footer:
|
|
29
124
|
|
|
30
125
|
```tsx
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
126
|
+
import { Footer } from '@/components/Footer'
|
|
127
|
+
import { Header } from '@/components/Header'
|
|
128
|
+
import { ContactUs } from '@/components/ContactUs'
|
|
129
|
+
import { ScrollToTopButton } from '@/components/ScrollToTopButton'
|
|
130
|
+
import type { DefaultRootProps, RootConfig } from '@puckeditor/core'
|
|
33
131
|
|
|
34
|
-
export
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
132
|
+
export type RootProps = DefaultRootProps
|
|
133
|
+
|
|
134
|
+
export const Root: RootConfig = {
|
|
135
|
+
defaultProps: {
|
|
136
|
+
title: 'Your Site Title',
|
|
137
|
+
},
|
|
138
|
+
render: ({ children }) => {
|
|
139
|
+
return (
|
|
140
|
+
<>
|
|
141
|
+
<Header />
|
|
142
|
+
{children}
|
|
143
|
+
<ContactUs title="Contact Us" />
|
|
144
|
+
<Footer />
|
|
145
|
+
<ScrollToTopButton />
|
|
146
|
+
</>
|
|
147
|
+
)
|
|
148
|
+
},
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export default Root
|
|
48
152
|
```
|
|
49
153
|
|
|
50
|
-
###
|
|
51
|
-
For specific pages, use `seoHead` to allow the CMS to inject page-specific metadata.
|
|
154
|
+
### 5.2 Type Definitions (`src/editor/types.ts`)
|
|
52
155
|
|
|
53
|
-
|
|
54
|
-
|
|
156
|
+
Define types for your custom components:
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
import type { CMSUserConfig, CMSUserData } from 'rankrunners-cms/src/editor/types'
|
|
160
|
+
|
|
161
|
+
import type { MainPageBlockTypes } from './blocks/main-page-blocks'
|
|
162
|
+
import type { PageBlockTypes } from './blocks/page-blocks'
|
|
163
|
+
import type { ServicePageBlockTypes } from './blocks/service-page-blocks'
|
|
164
|
+
|
|
165
|
+
// Combine all block types
|
|
166
|
+
export type CustomComponents = MainPageBlockTypes & PageBlockTypes & ServicePageBlockTypes
|
|
167
|
+
|
|
168
|
+
// Export config and data types
|
|
169
|
+
export type UserConfig = CMSUserConfig<CustomComponents, ['custom']>
|
|
170
|
+
export type UserData = CMSUserData<CustomComponents>
|
|
171
|
+
```
|
|
55
172
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
173
|
+
### 5.3 Main Config (`src/editor/index.ts`)
|
|
174
|
+
|
|
175
|
+
Register all blocks and create the config:
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
import Root from './root'
|
|
179
|
+
import type { CustomComponents } from './types'
|
|
180
|
+
import { createCMSConfig } from 'rankrunners-cms/src/editor/index'
|
|
181
|
+
import { MainPageBlocks } from './blocks/main-page-blocks'
|
|
182
|
+
import { PageBlocks } from './blocks/page-blocks'
|
|
183
|
+
import { ServicePageBlocks } from './blocks/service-page-blocks'
|
|
184
|
+
|
|
185
|
+
// Define categories for the editor sidebar
|
|
186
|
+
export const customCategories = {
|
|
187
|
+
custom: {
|
|
188
|
+
title: 'Custom',
|
|
189
|
+
components: [
|
|
190
|
+
'HeroBlock',
|
|
191
|
+
'TrustBarBlock',
|
|
192
|
+
'IntroductionBlock',
|
|
193
|
+
'PageTitleBlock',
|
|
194
|
+
'ContactTodayBlock',
|
|
195
|
+
// ... list all block names
|
|
196
|
+
],
|
|
197
|
+
},
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Combine all blocks
|
|
201
|
+
export const customComponents = {
|
|
202
|
+
...MainPageBlocks,
|
|
203
|
+
...PageBlocks,
|
|
204
|
+
...ServicePageBlocks,
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Create and export the config
|
|
208
|
+
export const config = createCMSConfig<CustomComponents, ['custom']>(
|
|
209
|
+
Root,
|
|
210
|
+
customCategories,
|
|
211
|
+
customComponents
|
|
212
|
+
)
|
|
60
213
|
```
|
|
61
214
|
|
|
62
215
|
---
|
|
63
216
|
|
|
64
|
-
##
|
|
217
|
+
## 6. Creating Component Blocks
|
|
218
|
+
|
|
219
|
+
### 6.1 Block Naming Convention
|
|
65
220
|
|
|
66
|
-
|
|
221
|
+
**Important**: All block names must end with `Block` suffix (e.g., `HeroBlock`, `TrustBarBlock`).
|
|
67
222
|
|
|
68
|
-
###
|
|
69
|
-
|
|
223
|
+
### 6.2 Block Structure Pattern
|
|
224
|
+
|
|
225
|
+
Each block wraps a React component with Puck configuration:
|
|
70
226
|
|
|
71
227
|
```tsx
|
|
72
|
-
import {
|
|
73
|
-
import
|
|
74
|
-
import {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
return <Render config={config} data={initialData} />;
|
|
228
|
+
import type { ComponentConfig } from '@puckeditor/core'
|
|
229
|
+
import { withLayout } from 'rankrunners-cms/src/editor/components/Layout'
|
|
230
|
+
import { MyComponent, type MyComponentProps } from '@/components/MyComponent'
|
|
231
|
+
|
|
232
|
+
export const MyComponentBlock: ComponentConfig<MyComponentProps> = withLayout({
|
|
233
|
+
label: 'My Component', // Display name in editor
|
|
234
|
+
fields: {
|
|
235
|
+
title: {
|
|
236
|
+
type: 'text',
|
|
237
|
+
label: 'Title',
|
|
238
|
+
contentEditable: true, // Allow inline editing
|
|
239
|
+
},
|
|
240
|
+
description: {
|
|
241
|
+
type: 'textarea',
|
|
242
|
+
label: 'Description'
|
|
243
|
+
},
|
|
244
|
+
// ... more fields
|
|
245
|
+
},
|
|
246
|
+
defaultProps: {
|
|
247
|
+
title: 'Default Title',
|
|
248
|
+
description: 'Default description text',
|
|
249
|
+
// ... default values for all props
|
|
96
250
|
},
|
|
97
|
-
}
|
|
251
|
+
render: (props) => <MyComponent {...props} />,
|
|
252
|
+
})
|
|
98
253
|
```
|
|
99
254
|
|
|
100
|
-
|
|
255
|
+
### 6.3 Available Field Types
|
|
101
256
|
|
|
102
|
-
|
|
257
|
+
| Type | Description | Example |
|
|
258
|
+
|------|-------------|---------|
|
|
259
|
+
| `text` | Single-line text input | `{ type: 'text', label: 'Title' }` |
|
|
260
|
+
| `textarea` | Multi-line text input | `{ type: 'textarea', label: 'Description' }` |
|
|
261
|
+
| `number` | Numeric input | `{ type: 'number', label: 'Count' }` |
|
|
262
|
+
| `select` | Dropdown selection | See below |
|
|
263
|
+
| `array` | List of items | See below |
|
|
264
|
+
| `object` | Nested object | `{ type: 'object', objectFields: {...} }` |
|
|
103
265
|
|
|
104
|
-
|
|
266
|
+
### 6.4 Select Field Example
|
|
105
267
|
|
|
106
|
-
### Example Block: [src/editor/blocks/Heading/index.tsx](src/editor/blocks/Heading/index.tsx)
|
|
107
268
|
```tsx
|
|
108
|
-
|
|
269
|
+
category: {
|
|
270
|
+
type: 'select',
|
|
271
|
+
label: 'Category',
|
|
272
|
+
options: [
|
|
273
|
+
{ label: 'Repairs', value: 'Repairs' },
|
|
274
|
+
{ label: 'Installations', value: 'Installations' },
|
|
275
|
+
{ label: 'Remodeling', value: 'Remodeling' },
|
|
276
|
+
],
|
|
277
|
+
},
|
|
278
|
+
```
|
|
109
279
|
|
|
110
|
-
|
|
111
|
-
text: string;
|
|
112
|
-
level: 'h1' | 'h2' | 'h3';
|
|
113
|
-
};
|
|
280
|
+
### 6.5 Array Field Example
|
|
114
281
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
],
|
|
125
|
-
},
|
|
282
|
+
```tsx
|
|
283
|
+
services: {
|
|
284
|
+
type: 'array',
|
|
285
|
+
label: 'Services',
|
|
286
|
+
getItemSummary: (item) => item.name || 'Service',
|
|
287
|
+
arrayFields: {
|
|
288
|
+
name: { type: 'text', label: 'Service Name' },
|
|
289
|
+
description: { type: 'textarea', label: 'Description' },
|
|
290
|
+
imageUrl: { type: 'text', label: 'Image URL' },
|
|
126
291
|
},
|
|
127
|
-
|
|
128
|
-
|
|
292
|
+
defaultItemProps: {
|
|
293
|
+
name: 'New Service',
|
|
294
|
+
description: '',
|
|
295
|
+
imageUrl: '/assets/placeholder.webp',
|
|
296
|
+
},
|
|
297
|
+
},
|
|
129
298
|
```
|
|
130
299
|
|
|
131
|
-
|
|
300
|
+
### 6.6 The `withLayout` Wrapper
|
|
132
301
|
|
|
133
|
-
|
|
302
|
+
**Critical**: Always wrap your block config with `withLayout()` to enable grid/flex layout support:
|
|
134
303
|
|
|
135
|
-
|
|
304
|
+
```tsx
|
|
305
|
+
// ✅ CORRECT
|
|
306
|
+
export const MyBlock: ComponentConfig<MyProps> = withLayout({
|
|
307
|
+
label: 'My Block',
|
|
308
|
+
fields: { ... },
|
|
309
|
+
defaultProps: { ... },
|
|
310
|
+
render: (props) => <MyComponent {...props} />,
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
// ❌ WRONG - Will cause "Element type is invalid" error
|
|
314
|
+
export const MyBlock: ComponentConfig<MyProps> = {
|
|
315
|
+
label: 'My Block',
|
|
316
|
+
fields: { ... },
|
|
317
|
+
defaultProps: { ... },
|
|
318
|
+
render: withLayout((props) => <MyComponent {...props} />), // DON'T do this
|
|
319
|
+
}
|
|
320
|
+
```
|
|
136
321
|
|
|
137
|
-
###
|
|
138
|
-
Create a file called `src/routes/$sitemap.xml.tsx`:
|
|
322
|
+
### 6.7 Complete Block File Example
|
|
139
323
|
|
|
140
324
|
```tsx
|
|
141
|
-
|
|
142
|
-
import {
|
|
325
|
+
// src/editor/blocks/page-blocks.tsx
|
|
326
|
+
import type { ComponentConfig } from '@puckeditor/core'
|
|
327
|
+
import { withLayout } from 'rankrunners-cms/src/editor/components/Layout'
|
|
328
|
+
import { PageTitle, type PageTitleProps } from '@/components/PageTitle'
|
|
329
|
+
import { ContactToday, type ContactTodayProps } from '@/components/ContactToday'
|
|
330
|
+
|
|
331
|
+
// PageTitle Block
|
|
332
|
+
export const PageTitleBlock: ComponentConfig<PageTitleProps> = withLayout({
|
|
333
|
+
label: 'Page Title',
|
|
334
|
+
fields: {
|
|
335
|
+
title: { type: 'text', label: 'Title' },
|
|
336
|
+
backgroundImageUrl: { type: 'text', label: 'Background Image URL' },
|
|
337
|
+
},
|
|
338
|
+
defaultProps: {
|
|
339
|
+
title: 'Page Title',
|
|
340
|
+
backgroundImageUrl: '/assets/hero-bg.webp',
|
|
341
|
+
},
|
|
342
|
+
render: (props) => <PageTitle {...props} />,
|
|
343
|
+
})
|
|
143
344
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
},
|
|
345
|
+
// ContactToday Block
|
|
346
|
+
export const ContactTodayBlock: ComponentConfig<ContactTodayProps> = withLayout({
|
|
347
|
+
label: 'Contact Today',
|
|
348
|
+
fields: {
|
|
349
|
+
mainTitle: { type: 'text', label: 'Main Title' },
|
|
350
|
+
phoneNumber: { type: 'text', label: 'Phone Number' },
|
|
351
|
+
// ... more fields
|
|
352
|
+
},
|
|
353
|
+
defaultProps: {
|
|
354
|
+
mainTitle: 'Contact Us Today',
|
|
355
|
+
phoneNumber: '15551234567',
|
|
356
|
+
// ... more defaults
|
|
149
357
|
},
|
|
358
|
+
render: (props) => <ContactToday {...props} />,
|
|
150
359
|
})
|
|
360
|
+
|
|
361
|
+
// Export types for type.ts
|
|
362
|
+
export type PageBlockTypes = {
|
|
363
|
+
PageTitleBlock: PageTitleProps
|
|
364
|
+
ContactTodayBlock: ContactTodayProps
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Export blocks for index.ts
|
|
368
|
+
export const PageBlocks = {
|
|
369
|
+
PageTitleBlock,
|
|
370
|
+
ContactTodayBlock,
|
|
371
|
+
}
|
|
151
372
|
```
|
|
152
373
|
|
|
153
|
-
###
|
|
154
|
-
|
|
374
|
+
### 6.8 Component Design Guidelines
|
|
375
|
+
|
|
376
|
+
When creating React components for use with the CMS:
|
|
377
|
+
|
|
378
|
+
1. **Make ALL text configurable via props**
|
|
379
|
+
2. **Use the same defaults in both the component AND the block**
|
|
380
|
+
3. **Export the props interface for type safety**
|
|
155
381
|
|
|
156
382
|
```tsx
|
|
157
|
-
|
|
158
|
-
import {
|
|
383
|
+
// src/components/PageTitle.tsx
|
|
384
|
+
import { Link } from '@tanstack/react-router'
|
|
385
|
+
import React from 'react'
|
|
386
|
+
|
|
387
|
+
export interface PageTitleProps {
|
|
388
|
+
title?: string
|
|
389
|
+
backgroundImageUrl?: string
|
|
390
|
+
homeText?: string
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Default props - keep in sync with block defaultProps
|
|
394
|
+
const defaultProps: Required<PageTitleProps> = {
|
|
395
|
+
title: 'Page Title',
|
|
396
|
+
backgroundImageUrl: '/assets/hero-bg.webp',
|
|
397
|
+
homeText: 'Home',
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export const PageTitle: React.FC<PageTitleProps> = (props) => {
|
|
401
|
+
const { title, backgroundImageUrl, homeText } = { ...defaultProps, ...props }
|
|
402
|
+
|
|
403
|
+
return (
|
|
404
|
+
<div
|
|
405
|
+
className="relative bg-cover bg-center min-h-[15rem] flex justify-center items-center"
|
|
406
|
+
style={{ backgroundImage: `url('\${backgroundImageUrl}')` }}
|
|
407
|
+
>
|
|
408
|
+
<div className="absolute inset-0 bg-black/25" />
|
|
409
|
+
<div className="relative text-center text-white">
|
|
410
|
+
<h1 className="text-4xl uppercase">{title}</h1>
|
|
411
|
+
<div className="mt-2">
|
|
412
|
+
<Link to="/">{homeText}</Link> / {title}
|
|
413
|
+
</div>
|
|
414
|
+
</div>
|
|
415
|
+
</div>
|
|
416
|
+
)
|
|
417
|
+
}
|
|
418
|
+
```
|
|
159
419
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
420
|
+
---
|
|
421
|
+
|
|
422
|
+
## 7. Creating Page Data
|
|
423
|
+
|
|
424
|
+
### 7.1 Page Data Structure
|
|
425
|
+
|
|
426
|
+
Each page has an initial data file:
|
|
427
|
+
|
|
428
|
+
```typescript
|
|
429
|
+
// src/editor/initial-data/contact-page.ts
|
|
430
|
+
// @ts-nocheck
|
|
431
|
+
import type { UserData } from '../types'
|
|
432
|
+
|
|
433
|
+
export const contactPageData: UserData = {
|
|
434
|
+
root: {
|
|
435
|
+
title: 'Contact Us | Your Site Name', // Page title for SEO
|
|
165
436
|
},
|
|
437
|
+
content: [
|
|
438
|
+
{
|
|
439
|
+
type: 'PageTitleBlock', // Must match block name exactly
|
|
440
|
+
props: {
|
|
441
|
+
id: 'page-title', // Unique ID required
|
|
442
|
+
title: 'Contact Us',
|
|
443
|
+
backgroundImageUrl: '/assets/contact-bg.webp',
|
|
444
|
+
homeText: 'Home',
|
|
445
|
+
},
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
type: 'ContactTodayBlock',
|
|
449
|
+
props: {
|
|
450
|
+
id: 'contact-section',
|
|
451
|
+
mainTitle: 'Get In Touch',
|
|
452
|
+
phoneNumber: '15551234567',
|
|
453
|
+
// ... all props for the block
|
|
454
|
+
},
|
|
455
|
+
},
|
|
456
|
+
],
|
|
457
|
+
zones: {}, // For advanced layouts with drop zones
|
|
458
|
+
}
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
### 7.2 Registering Page Data (`src/editor/initial-data/initial.ts`)
|
|
462
|
+
|
|
463
|
+
Map URL paths to page data:
|
|
464
|
+
|
|
465
|
+
```typescript
|
|
466
|
+
import { indexPageData } from './index-page'
|
|
467
|
+
import { contactPageData } from './contact-page'
|
|
468
|
+
import { testimonialsPageData } from './testimonials-page'
|
|
469
|
+
import { plumbingPageData } from './services/plumbing-page'
|
|
470
|
+
|
|
471
|
+
export const allPageData = {
|
|
472
|
+
'': indexPageData, // Home page (/)
|
|
473
|
+
contact: contactPageData, // /contact
|
|
474
|
+
testimonials: testimonialsPageData, // /testimonials
|
|
475
|
+
'services/plumbing': plumbingPageData, // /services/plumbing
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
export {
|
|
479
|
+
indexPageData,
|
|
480
|
+
contactPageData,
|
|
481
|
+
testimonialsPageData,
|
|
482
|
+
plumbingPageData,
|
|
483
|
+
}
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
### 7.3 Export Initial Data (`src/editor/initial-data/index.ts`)
|
|
487
|
+
|
|
488
|
+
```typescript
|
|
489
|
+
export { allPageData as initialData } from './initial'
|
|
490
|
+
export * from './initial'
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
---
|
|
494
|
+
|
|
495
|
+
## 8. Setting Up Routes
|
|
496
|
+
|
|
497
|
+
### 8.1 Root Route (`src/routes/__root.tsx`)
|
|
498
|
+
|
|
499
|
+
In your [src/routes/__root.tsx](src/routes/__root.tsx), use `headWithSEO` and `seoScripts` to initialize the global SEO state.
|
|
500
|
+
|
|
501
|
+
You can freely configure the head meta, links and scripts if you want.
|
|
502
|
+
|
|
503
|
+
```tsx
|
|
504
|
+
import { HeadContent, Scripts, createRootRoute } from '@tanstack/react-router'
|
|
505
|
+
import { headWithSEO, seoScripts } from 'rankrunners-cms/src/tanstack'
|
|
506
|
+
import appCss from '../styles.css?url'
|
|
507
|
+
|
|
508
|
+
export const Route = createRootRoute({
|
|
509
|
+
head: headWithSEO<any>(() => ({
|
|
510
|
+
meta: [
|
|
511
|
+
{ charSet: 'utf-8' },
|
|
512
|
+
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
|
|
513
|
+
],
|
|
514
|
+
links: [
|
|
515
|
+
{ rel: 'stylesheet', href: appCss },
|
|
516
|
+
{ rel: 'icon', href: '/favicon.png' },
|
|
517
|
+
],
|
|
518
|
+
})),
|
|
519
|
+
scripts: seoScripts as any,
|
|
520
|
+
shellComponent: RootDocument,
|
|
166
521
|
})
|
|
522
|
+
|
|
523
|
+
function RootDocument({ children }: { children: React.ReactNode }) {
|
|
524
|
+
return (
|
|
525
|
+
<html lang="en">
|
|
526
|
+
<head>
|
|
527
|
+
<HeadContent />
|
|
528
|
+
</head>
|
|
529
|
+
<body>
|
|
530
|
+
{children}
|
|
531
|
+
<Scripts />
|
|
532
|
+
</body>
|
|
533
|
+
</html>
|
|
534
|
+
)
|
|
535
|
+
}
|
|
167
536
|
```
|
|
168
537
|
|
|
538
|
+
### 8.2 Catch-All Page Route (`src/routes/$.ts`)
|
|
539
|
+
|
|
540
|
+
This single route handles ALL CMS pages:
|
|
541
|
+
|
|
542
|
+
```typescript
|
|
543
|
+
import { config } from '@/editor'
|
|
544
|
+
import { initialData } from '@/editor/initial-data'
|
|
545
|
+
import { PageRendererTanstack } from 'rankrunners-cms/src/tanstack'
|
|
546
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
547
|
+
|
|
548
|
+
export const Route = createFileRoute('/\$')({
|
|
549
|
+
component: () =>
|
|
550
|
+
PageRendererTanstack({
|
|
551
|
+
config,
|
|
552
|
+
allPageData: initialData,
|
|
553
|
+
})(),
|
|
554
|
+
})
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
### 8.3 How the Page Renderer Works
|
|
558
|
+
|
|
559
|
+
1. Extracts the pathname from the URL (e.g., `/contact` -> `contact`)
|
|
560
|
+
2. Looks up the page data in `allPageData`
|
|
561
|
+
3. If `?preview=token` query param exists, shows the Puck editor
|
|
562
|
+
4. Otherwise, renders the page using the `Render` component
|
|
563
|
+
|
|
169
564
|
---
|
|
170
565
|
|
|
171
|
-
##
|
|
566
|
+
## 9. Sitemap & Robots.txt
|
|
567
|
+
|
|
568
|
+
Update the `server.ts` script, or add a Tanstack Start middleware to handle `[sitemap].xml` and `robots.txt` requests:
|
|
569
|
+
|
|
570
|
+
```typescript
|
|
571
|
+
// Build static routes with intelligent preloading
|
|
572
|
+
const { routes } = await initializeStaticRoutes(CLIENT_DIRECTORY)
|
|
573
|
+
|
|
574
|
+
// Import sitemap handler dynamically to help Bun bundle it
|
|
575
|
+
const downloadSitemapAsResponse = (
|
|
576
|
+
await import('rankrunners-cms/src/api/client/sitemap')
|
|
577
|
+
).downloadSitemapAsResponse
|
|
578
|
+
|
|
579
|
+
// Create Bun server
|
|
580
|
+
const server = Bun.serve({
|
|
581
|
+
port: SERVER_PORT,
|
|
582
|
+
|
|
583
|
+
routes: {
|
|
584
|
+
// Serve static assets, robots.txt, sitemaps (*.xml), and all other routes
|
|
585
|
+
...routes,
|
|
586
|
+
'/*': (req: Request) => {
|
|
587
|
+
const url = new URL(req.url)
|
|
588
|
+
const pathname = url.pathname
|
|
589
|
+
|
|
590
|
+
// Check if it's a sitemap file (ends with .xml)
|
|
591
|
+
if (pathname === 'robots.txt' || pathname.endsWith('.xml')) {
|
|
592
|
+
const sitemapFile = pathname.substring(1) // Remove leading slash
|
|
593
|
+
return downloadSitemapAsResponse(sitemapFile)
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Otherwise, pass to TanStack Start handler
|
|
597
|
+
try {
|
|
598
|
+
return handler.fetch(req)
|
|
599
|
+
} catch (error) {
|
|
600
|
+
log.error(`Server handler error: ${String(error)}`)
|
|
601
|
+
return new Response('Internal Server Error', { status: 500 })
|
|
602
|
+
}
|
|
603
|
+
},
|
|
604
|
+
},
|
|
172
605
|
|
|
173
|
-
|
|
606
|
+
// Global error handler
|
|
607
|
+
error(error) {
|
|
608
|
+
log.error(
|
|
609
|
+
`Uncaught server error: ${error instanceof Error ? error.message : String(error)}`,
|
|
610
|
+
)
|
|
611
|
+
return new Response('Internal Server Error', { status: 500 })
|
|
612
|
+
},
|
|
613
|
+
})
|
|
614
|
+
```
|
|
174
615
|
|
|
175
|
-
|
|
176
|
-
- [ ] Install `rankrunners-cms` and `@puckeditor/core`.
|
|
177
|
-
- [ ] Add `headWithSEO` to `createRootRoute` in [src/routes/__root.tsx](src/routes/__root.tsx).
|
|
178
|
-
- [ ] Add `seoScripts` to `createRootRoute` in [src/routes/__root.tsx](src/routes/__root.tsx).
|
|
179
|
-
- [ ] Add `<Scripts />` after the body and `<HeadContent />` in the `<head>` to [src/routes/__root.tsx](src/routes/__root.tsx)
|
|
616
|
+
---
|
|
180
617
|
|
|
181
|
-
|
|
182
|
-
- [ ] Include `seoHead` in all page routes (`createFileRoute`).
|
|
183
|
-
- [ ] Ensure `VITE_PUBLIC_SITE_ID` and other CMS-related environment variables are set, as well in Dockerfile.
|
|
618
|
+
## 10. Best Practices
|
|
184
619
|
|
|
185
|
-
###
|
|
186
|
-
-
|
|
187
|
-
-
|
|
188
|
-
-
|
|
189
|
-
-
|
|
620
|
+
### Component Design
|
|
621
|
+
- ✅ Make ALL text configurable (no hardcoded strings)
|
|
622
|
+
- ✅ Keep default props in sync between component and block
|
|
623
|
+
- ✅ Export component props interface
|
|
624
|
+
- ✅ Use semantic HTML for accessibility
|
|
625
|
+
|
|
626
|
+
### Block Design
|
|
627
|
+
- ✅ Always use `withLayout()` wrapper
|
|
628
|
+
- ✅ End block names with `Block` suffix
|
|
629
|
+
- ✅ Provide meaningful `label` for editor sidebar
|
|
630
|
+
- ✅ Use `contentEditable: true` for inline-editable text fields
|
|
631
|
+
- ✅ Use `getItemSummary` for array fields to show previews
|
|
632
|
+
|
|
633
|
+
### Page Data
|
|
634
|
+
- ✅ Always include unique `id` in each block's props
|
|
635
|
+
- ✅ Match block `type` exactly to exported block name
|
|
636
|
+
|
|
637
|
+
### Common Pitfalls
|
|
638
|
+
- ❌ Don't use `@measured/puck` - it's been renamed to `@puckeditor/core`
|
|
639
|
+
- ❌ Don't wrap only the render function with `withLayout()`
|
|
640
|
+
- ❌ Don't use `type: 'list'` for array fields (use `type: 'array'`)
|
|
641
|
+
|
|
642
|
+
---
|
|
643
|
+
|
|
644
|
+
## 11. Developer Checklist
|
|
645
|
+
|
|
646
|
+
### Initial Setup
|
|
647
|
+
- [ ] Install `rankrunners-cms` and `@puckeditor/core`
|
|
648
|
+
- [ ] Set `VITE_PUBLIC_SITE_ID` environment variable
|
|
649
|
+
- [ ] Add `VITE_PUBLIC_SITE_ID` to Dockerfile
|
|
650
|
+
|
|
651
|
+
### Root Route Configuration
|
|
652
|
+
- [ ] Add `headWithSEO` to `createRootRoute`
|
|
653
|
+
- [ ] Add `seoScripts` to `createRootRoute`
|
|
654
|
+
- [ ] Add `<HeadContent />` in `<head>`
|
|
655
|
+
- [ ] Add `<Scripts />` after body content
|
|
656
|
+
|
|
657
|
+
### Editor Structure
|
|
658
|
+
- [ ] Create `src/editor/index.ts` with config export
|
|
659
|
+
- [ ] Create `src/editor/root.tsx` with Header/Footer wrapper
|
|
660
|
+
- [ ] Create `src/editor/types.ts` with type definitions
|
|
661
|
+
|
|
662
|
+
### Block Implementation
|
|
663
|
+
- [ ] Create blocks in `src/editor/blocks/`
|
|
664
|
+
- [ ] Use `withLayout()` wrapper for each block
|
|
665
|
+
- [ ] Export block types (e.g., `PageBlockTypes`)
|
|
666
|
+
- [ ] Export blocks object (e.g., `PageBlocks`)
|
|
667
|
+
- [ ] Register blocks in `customCategories`
|
|
668
|
+
- [ ] Register blocks in `customComponents`
|
|
669
|
+
|
|
670
|
+
### Page Data
|
|
671
|
+
- [ ] Create page data files in `src/editor/initial-data/`
|
|
672
|
+
- [ ] Register paths in `initial.ts` `allPageData` object
|
|
673
|
+
|
|
674
|
+
### Routes
|
|
675
|
+
- [ ] Create catch-all route `src/routes/$.ts`
|
|
676
|
+
|
|
677
|
+
### Sitemaps
|
|
678
|
+
- [ ] Implement sitemap serving middleware in `server.ts`
|
|
190
679
|
|
|
191
680
|
### Component Implementation
|
|
192
681
|
- [ ] Migrate/Create components in `src/editor/blocks/`.
|
|
193
682
|
- [ ] Register all blocks in `src/editor/index.tsx`.
|
|
194
683
|
- [ ] Organize blocks into `categories` within the config.
|
|
195
684
|
|
|
196
|
-
###
|
|
197
|
-
- [ ]
|
|
198
|
-
|
|
685
|
+
### CMS configuration
|
|
686
|
+
- [ ] Added the new pages to the site in the [CMS user interface](https://cms.rankrunners.net) and saved at least once.
|
|
687
|
+
|
|
688
|
+
---
|
|
689
|
+
|
|
690
|
+
## 12. Troubleshooting
|
|
691
|
+
|
|
692
|
+
### "Element type is invalid: expected a string...but got: object"
|
|
693
|
+
|
|
694
|
+
**Cause**: Using `withLayout()` incorrectly.
|
|
695
|
+
|
|
696
|
+
**Fix**: Wrap the entire ComponentConfig object, not just the render function:
|
|
697
|
+
|
|
698
|
+
```tsx
|
|
699
|
+
// ✅ Correct
|
|
700
|
+
export const MyBlock = withLayout({
|
|
701
|
+
label: '...',
|
|
702
|
+
fields: {...},
|
|
703
|
+
render: (props) => <MyComponent {...props} />,
|
|
704
|
+
})
|
|
705
|
+
|
|
706
|
+
// ❌ Wrong
|
|
707
|
+
export const MyBlock = {
|
|
708
|
+
label: '...',
|
|
709
|
+
fields: {...},
|
|
710
|
+
render: withLayout((props) => <MyComponent {...props} />),
|
|
711
|
+
}
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
### Page shows "Loading page..." forever
|
|
715
|
+
|
|
716
|
+
**Cause**: Path mismatch between URL and `allPageData` keys.
|
|
717
|
+
|
|
718
|
+
**Fix**: Ensure keys in `allPageData` match URL paths without leading slashes:
|
|
719
|
+
- URL `/contact` -> key `contact`
|
|
720
|
+
- URL `/services/plumbing` -> key `services/plumbing`
|
|
721
|
+
- URL `/` -> key `''` (empty string)
|
|
722
|
+
|
|
723
|
+
### Build error: Unexpected character
|
|
724
|
+
|
|
725
|
+
**Cause**: Using special characters in strings.
|
|
726
|
+
|
|
727
|
+
**Fix**: Replace curly quotes and em-dashes:
|
|
728
|
+
- Curly single quotes -> straight single quote `'`
|
|
729
|
+
- Curly double quotes -> straight double quote `"`
|
|
730
|
+
- Em-dash -> hyphen `-` or double hyphen `--`
|
|
731
|
+
|
|
732
|
+
### TypeScript error with field type
|
|
733
|
+
|
|
734
|
+
**Cause**: Using unsupported field type.
|
|
735
|
+
|
|
736
|
+
**Fix**: Use supported types: `text`, `textarea`, `number`, `select`, `array`, `object`
|
|
199
737
|
|
|
200
738
|
---
|
|
739
|
+
|
|
740
|
+
## Quick Start Template
|
|
741
|
+
|
|
742
|
+
Create a new CMS-enabled page in 3 steps:
|
|
743
|
+
|
|
744
|
+
**1. Create the component** (`src/components/MyPage.tsx`)
|
|
745
|
+
```tsx
|
|
746
|
+
export interface MyPageProps {
|
|
747
|
+
title?: string
|
|
748
|
+
content?: string
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
export const MyPage: React.FC<MyPageProps> = ({
|
|
752
|
+
title = 'Default Title',
|
|
753
|
+
content = 'Default content'
|
|
754
|
+
}) => (
|
|
755
|
+
<div className="max-w-4xl mx-auto py-8 px-4">
|
|
756
|
+
<h1>{title}</h1>
|
|
757
|
+
<p>{content}</p>
|
|
758
|
+
</div>
|
|
759
|
+
)
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
**2. Create the block** (add to `src/editor/blocks/page-blocks.tsx`)
|
|
763
|
+
```tsx
|
|
764
|
+
export const MyPageBlock: ComponentConfig<MyPageProps> = withLayout({
|
|
765
|
+
label: 'My Page',
|
|
766
|
+
fields: {
|
|
767
|
+
title: { type: 'text', label: 'Title' },
|
|
768
|
+
content: { type: 'textarea', label: 'Content' },
|
|
769
|
+
},
|
|
770
|
+
defaultProps: {
|
|
771
|
+
title: 'Default Title',
|
|
772
|
+
content: 'Default content',
|
|
773
|
+
},
|
|
774
|
+
render: (props) => <MyPage {...props} />,
|
|
775
|
+
})
|
|
776
|
+
```
|
|
777
|
+
|
|
778
|
+
**3. Create the page data** (`src/editor/initial-data/mypage-page.ts`)
|
|
779
|
+
```typescript
|
|
780
|
+
export const mypagePageData: UserData = {
|
|
781
|
+
root: { title: 'My Page | Site Name' },
|
|
782
|
+
content: [
|
|
783
|
+
{
|
|
784
|
+
type: 'MyPageBlock',
|
|
785
|
+
props: { id: 'my-page', title: 'Welcome', content: 'Hello world!' },
|
|
786
|
+
},
|
|
787
|
+
],
|
|
788
|
+
zones: {},
|
|
789
|
+
}
|
|
790
|
+
```
|
|
791
|
+
|
|
792
|
+
Then register in `initial.ts`:
|
|
793
|
+
```typescript
|
|
794
|
+
export const allPageData = {
|
|
795
|
+
// ...
|
|
796
|
+
'mypage': mypagePageData,
|
|
797
|
+
}
|
|
798
|
+
```
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rankrunners-cms",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.19",
|
|
5
5
|
"peerDependencies": {
|
|
6
|
-
"@puckeditor/core": "^0.21.
|
|
6
|
+
"@puckeditor/core": "^0.21.1",
|
|
7
7
|
"next": "^16.1.2",
|
|
8
8
|
"@tanstack/router-core": "^1.150.0",
|
|
9
9
|
"@tanstack/react-router": "^1.150.0",
|
|
@@ -29,15 +29,14 @@
|
|
|
29
29
|
}
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
|
-
"@types/react": "^19.2.
|
|
33
|
-
"bun-types": "^1.3.
|
|
32
|
+
"@types/react": "^19.2.11",
|
|
33
|
+
"bun-types": "^1.3.8",
|
|
34
34
|
"typescript": "^5.9.3",
|
|
35
|
-
"@tanstack/router-core": "^1.
|
|
36
|
-
"@tanstack/react-router": "^1.
|
|
37
|
-
"next": "^16.1.
|
|
35
|
+
"@tanstack/router-core": "^1.158.0",
|
|
36
|
+
"@tanstack/react-router": "^1.158.0",
|
|
37
|
+
"next": "^16.1.6",
|
|
38
38
|
"valibot": "^1.2.0",
|
|
39
|
-
"lucide-react": "^0.
|
|
40
|
-
"react": "^19.2.3"
|
|
39
|
+
"lucide-react": "^0.563.0"
|
|
41
40
|
},
|
|
42
41
|
"dependencies": {
|
|
43
42
|
"classnames": "^2.5.1",
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { CMSUserData } from "../../editor";
|
|
2
|
+
import { fetchWithCache } from "../../libs";
|
|
3
|
+
import { CMS_BASE_URL, SITE_ID } from "../constants";
|
|
4
|
+
|
|
5
|
+
export const getPageContents = async (
|
|
6
|
+
pathname: string,
|
|
7
|
+
allPageData: Record<string, CMSUserData<any>>,
|
|
8
|
+
previewToken?: string,
|
|
9
|
+
) => {
|
|
10
|
+
// remove trailing (left and right) slashes
|
|
11
|
+
pathname = pathname.replace(/^\/+|\/+$/g, "");
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const res = await fetchWithCache(
|
|
15
|
+
`${CMS_BASE_URL}/public/seo/${SITE_ID}/pages/${pathname}`,
|
|
16
|
+
{
|
|
17
|
+
headers: {
|
|
18
|
+
"Content-Type": "application/json",
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
if (res.ok) {
|
|
24
|
+
const json = (await res.json()) as { content?: string };
|
|
25
|
+
|
|
26
|
+
if (json.content) {
|
|
27
|
+
const content = JSON.parse(json.content) as CMSUserData<any>;
|
|
28
|
+
|
|
29
|
+
if (content.root && content.content) {
|
|
30
|
+
return content;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const actualData = allPageData[pathname as keyof typeof allPageData];
|
|
36
|
+
|
|
37
|
+
if (actualData) {
|
|
38
|
+
// Starting new page from existing published data
|
|
39
|
+
return actualData as CMSUserData<any>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Starting new blank page
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error("Error fetching page data:", error);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (previewToken) {
|
|
48
|
+
// New blank page in preview mode
|
|
49
|
+
return {
|
|
50
|
+
content: [],
|
|
51
|
+
root: { props: { title: "New Page" } },
|
|
52
|
+
zones: {},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return null;
|
|
57
|
+
};
|
|
@@ -1,56 +1,7 @@
|
|
|
1
1
|
import { useEffect, useState } from "react";
|
|
2
2
|
import type { CMSUserData } from "../types";
|
|
3
3
|
import { Render, type Config } from "@puckeditor/core";
|
|
4
|
-
import {
|
|
5
|
-
import { fetchWithCache } from "../../libs";
|
|
6
|
-
|
|
7
|
-
const getPageContents = async (
|
|
8
|
-
pathname: string,
|
|
9
|
-
allPageData: Record<string, CMSUserData<any>>,
|
|
10
|
-
) => {
|
|
11
|
-
// remove trailing (left and right) slashes
|
|
12
|
-
pathname = pathname.replace(/^\/+|\/+$/g, "");
|
|
13
|
-
|
|
14
|
-
try {
|
|
15
|
-
const res = await fetchWithCache(
|
|
16
|
-
`${CMS_BASE_URL}/public/seo/${SITE_ID}/pages/${pathname}`,
|
|
17
|
-
{
|
|
18
|
-
headers: {
|
|
19
|
-
"Content-Type": "application/json",
|
|
20
|
-
},
|
|
21
|
-
},
|
|
22
|
-
);
|
|
23
|
-
|
|
24
|
-
if (res.ok) {
|
|
25
|
-
const json = (await res.json()) as { content?: string };
|
|
26
|
-
|
|
27
|
-
if (json.content) {
|
|
28
|
-
const content = JSON.parse(json.content) as CMSUserData<any>;
|
|
29
|
-
|
|
30
|
-
if (content.root && content.content) {
|
|
31
|
-
return content;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const actualData = allPageData[pathname as keyof typeof allPageData];
|
|
37
|
-
|
|
38
|
-
if (actualData) {
|
|
39
|
-
// Starting new page from existing published data
|
|
40
|
-
return actualData as CMSUserData<any>;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Starting new blank page
|
|
44
|
-
} catch (error) {
|
|
45
|
-
console.error("Error fetching page data:", error);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return {
|
|
49
|
-
content: [],
|
|
50
|
-
root: { props: { title: "New Page" } },
|
|
51
|
-
zones: {},
|
|
52
|
-
};
|
|
53
|
-
};
|
|
4
|
+
import { getPageContents } from "../../api/client/pages";
|
|
54
5
|
|
|
55
6
|
export type PageRendererContentProps = {
|
|
56
7
|
pathname: string;
|
|
@@ -82,4 +33,4 @@ export const PageRendererContent = ({
|
|
|
82
33
|
}
|
|
83
34
|
|
|
84
35
|
return <Render config={config} data={data} />;
|
|
85
|
-
};
|
|
36
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Render, type Config } from "@puckeditor/core";
|
|
2
|
+
import type { CMSUserData } from "../types";
|
|
3
|
+
|
|
4
|
+
export type PageRendererSSRProps = {
|
|
5
|
+
config: Config;
|
|
6
|
+
data: CMSUserData<any>;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* SSR-compatible PageRenderer that receives pre-fetched data from the loader.
|
|
11
|
+
* Use this component with TanStack Router's loader for server-side rendering.
|
|
12
|
+
*/
|
|
13
|
+
export const PageRendererSSR = ({ config, data }: PageRendererSSRProps) => {
|
|
14
|
+
return <Render config={config} data={data} />;
|
|
15
|
+
};
|
|
@@ -126,7 +126,7 @@ export const BaseEditor =
|
|
|
126
126
|
alert("Failed to upload changes to CMS");
|
|
127
127
|
} else {
|
|
128
128
|
alert(
|
|
129
|
-
"Changes uploaded successfully to CMS! The page will now reflect the changes."
|
|
129
|
+
"Changes uploaded successfully to CMS! The page will now reflect the changes.",
|
|
130
130
|
);
|
|
131
131
|
}
|
|
132
132
|
} catch (error) {
|
|
@@ -141,9 +141,6 @@ export const BaseEditor =
|
|
|
141
141
|
iframe={{
|
|
142
142
|
enabled: true,
|
|
143
143
|
}}
|
|
144
|
-
fieldTransforms={{
|
|
145
|
-
userField: ({ value }) => value, // Included to check types
|
|
146
|
-
}}
|
|
147
144
|
overrides={{
|
|
148
145
|
fieldTypes: {
|
|
149
146
|
// Example of user field provided via overrides
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { BaseEditor } from "../../editor/render/Preview";
|
|
4
|
-
import { usePathnameTanstack, useSearchParamsTanstack } from "../hooks";
|
|
1
|
+
import { BaseEditor } from '../../editor/render/Preview'
|
|
2
|
+
import { usePathnameTanstack, useSearchParamsTanstack } from '../hooks'
|
|
5
3
|
|
|
6
4
|
export const EditorTanstack = BaseEditor({
|
|
7
5
|
usePathname: usePathnameTanstack,
|
|
8
6
|
useSearchParams: useSearchParamsTanstack,
|
|
9
|
-
})
|
|
7
|
+
})
|
|
@@ -9,6 +9,10 @@ export type PageRendererTanstackInitializerProps = {
|
|
|
9
9
|
allPageData: Record<string, CMSUserData<any>>;
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Use PageRendererTanstackSSR for SSR support instead.
|
|
14
|
+
* This component fetches data on the client side.
|
|
15
|
+
*/
|
|
12
16
|
export const PageRendererTanstack =
|
|
13
17
|
({ config, allPageData }: PageRendererTanstackInitializerProps) =>
|
|
14
18
|
() => {
|