rankrunners-cms 0.0.16 → 0.0.18

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 CHANGED
@@ -1,35 +1,509 @@
1
1
  # RankRunners CMS Integration Guide
2
2
 
3
- This guide provides a comprehensive overview of how to integrate `rankrunners-cms` into a TanStack React website (specifically using TanStack Start and TanStack Router).
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 designed to enhance TanStack-based websites with SEO capabilities and a modular CMS editor powered by [Puck](https://puckeditor.com). It provides utilities for SEO management, script injection, and a structured pattern for building editable components.
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. Prerequisites
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
12
58
 
13
- Ensure your project has the following dependencies installed:
59
+ ```bash
60
+ # Using bun
61
+ bun add rankrunners-cms @puckeditor/core
62
+
63
+ # Using npm
64
+ npm install rankrunners-cms @puckeditor/core
65
+ ```
66
+
67
+ ### Required Peer Dependencies
68
+
69
+ Ensure you have TanStack Start and Router installed:
14
70
 
15
71
  ```bash
16
- bun add rankrunners-cms @puckeditor/core @tanstack/react-router @tanstack/react-start
72
+ bun add @tanstack/react-router @tanstack/react-start
17
73
  ```
18
74
 
75
+ ### Environment Variables
76
+
77
+ Add the following to your `.env` file:
78
+
79
+ ```env
80
+ VITE_PUBLIC_SITE_ID=your-site-id
81
+ ```
82
+
83
+ > **Important**: Also add `VITE_PUBLIC_SITE_ID` to your Dockerfile for production builds.
84
+
19
85
  ---
20
86
 
21
- ## 3. SEO Integration
87
+ ## 4. Project Structure
22
88
 
23
- The CMS provides a seamless way to handle SEO metadata and scripts across all routes.
89
+ Create the following structure in your `src/` directory:
90
+
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
+ ```
116
+
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:
124
+
125
+ ```tsx
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'
131
+
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
152
+ ```
153
+
154
+ ### 5.2 Type Definitions (`src/editor/types.ts`)
155
+
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
+ ```
172
+
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
+ )
213
+ ```
214
+
215
+ ---
216
+
217
+ ## 6. Creating Component Blocks
218
+
219
+ ### 6.1 Block Naming Convention
220
+
221
+ **Important**: All block names must end with `Block` suffix (e.g., `HeroBlock`, `TrustBarBlock`).
222
+
223
+ ### 6.2 Block Structure Pattern
224
+
225
+ Each block wraps a React component with Puck configuration:
226
+
227
+ ```tsx
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
250
+ },
251
+ render: (props) => <MyComponent {...props} />,
252
+ })
253
+ ```
254
+
255
+ ### 6.3 Available Field Types
256
+
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: {...} }` |
265
+
266
+ ### 6.4 Select Field Example
267
+
268
+ ```tsx
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
+ ```
279
+
280
+ ### 6.5 Array Field Example
281
+
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' },
291
+ },
292
+ defaultItemProps: {
293
+ name: 'New Service',
294
+ description: '',
295
+ imageUrl: '/assets/placeholder.webp',
296
+ },
297
+ },
298
+ ```
299
+
300
+ ### 6.6 The `withLayout` Wrapper
301
+
302
+ **Critical**: Always wrap your block config with `withLayout()` to enable grid/flex layout support:
303
+
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
+ ```
321
+
322
+ ### 6.7 Complete Block File Example
323
+
324
+ ```tsx
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
+ })
344
+
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
357
+ },
358
+ render: (props) => <ContactToday {...props} />,
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
+ }
372
+ ```
373
+
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**
381
+
382
+ ```tsx
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
+ ```
419
+
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
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`)
24
498
 
25
- ### Root Route Configuration
26
499
  In your [src/routes/__root.tsx](src/routes/__root.tsx), use `headWithSEO` and `seoScripts` to initialize the global SEO state.
27
500
 
28
501
  You can freely configure the head meta, links and scripts if you want.
29
502
 
30
503
  ```tsx
31
- import { headWithSEO } from 'rankrunners-cms/src/tanstack/seo/head';
32
- import { seoScripts } from 'rankrunners-cms/src/tanstack/seo/scripts';
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'
33
507
 
34
508
  export const Route = createRootRoute({
35
509
  head: headWithSEO<any>(() => ({
@@ -44,120 +518,260 @@ export const Route = createRootRoute({
44
518
  })),
45
519
  scripts: seoScripts as any,
46
520
  shellComponent: RootDocument,
47
- });
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
+ }
48
536
  ```
49
537
 
50
- ### Individual Route SEO
51
- For specific pages, use `seoHead` to allow the CMS to inject page-specific metadata.
538
+ ### 8.2 Catch-All Page Route (`src/routes/$.ts`)
52
539
 
53
- ```tsx
54
- import { seoHead } from 'rankrunners-cms/src/tanstack/seo/head';
540
+ This single route handles ALL CMS pages:
55
541
 
56
- export const Route = createFileRoute('/my-page')({
57
- head: seoHead as any,
58
- component: MyPageComponent,
59
- });
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
+ })
60
555
  ```
61
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
+
62
564
  ---
63
565
 
64
- ## 4. CMS Editor Setup (Puck)
566
+ ## 9. Sitemap & Robots.txt
65
567
 
66
- The CMS editor is typically located at `/editor` and allows real-time content editing.
568
+ Update the `server.ts` script, or add a Tanstack Start middleware to handle `[sitemap].xml` and `robots.txt` requests:
67
569
 
68
- ### The Editor Route
69
- Implement the editor in [src/routes/editor.tsx](src/routes/editor.tsx). It should toggle between the `Puck` editor and the `Render` component based on query parameters.
570
+ ```typescript
571
+ // Build static routes with intelligent preloading
572
+ const { routes } = await initializeStaticRoutes(CLIENT_DIRECTORY)
70
573
 
71
- ```tsx
72
- import { Puck, Render } from '@puckeditor/core';
73
- import config from '@/editor';
74
- import { initialData } from '@/editor/initial-data';
75
- import '@puckeditor/core/no-external.css';
76
-
77
- export const Route = createFileRoute('/editor')({
78
- component: () => {
79
- const params = new URLSearchParams(window.location.search);
80
- const isEdit = params.get('edit') === 'true';
81
-
82
- if (isEdit) {
83
- return (
84
- <Puck
85
- config={config}
86
- data={initialData}
87
- onPublish={(data) => {
88
- // Save data to localStorage or your API
89
- localStorage.setItem('cms-data', JSON.stringify(data));
90
- }}
91
- />
92
- );
574
+ // Add sitemap routes
575
+ routes['/*'] = (req: Request) => {
576
+ const url = new URL(req.url)
577
+ const pathname = url.pathname
578
+
579
+ // Serve the sitemap if we are asking for it
580
+ if (pathname === 'robots.txt' || pathname.endsWith('.xml')) {
581
+ const sitemapFile = pathname.substring(1)
582
+ return downloadSitemapAsResponse(sitemapFile)
93
583
  }
94
584
 
95
- return <Render config={config} data={initialData} />;
96
- },
97
- });
585
+ // Otherwise, pass to TanStack Start handler
586
+ try {
587
+ return handler.fetch(req)
588
+ } catch (error) {
589
+ log.error(`Server handler error: ${String(error)}`)
590
+ return new Response('Internal Server Error', { status: 500 })
591
+ }
592
+ }
98
593
  ```
99
594
 
100
595
  ---
101
596
 
102
- ## 5. Developing Component Blocks
597
+ ## 10. Best Practices
103
598
 
104
- Each editable component (Block) should be modular and define its props and configuration.
599
+ ### Component Design
600
+ - ✅ Make ALL text configurable (no hardcoded strings)
601
+ - ✅ Keep default props in sync between component and block
602
+ - ✅ Export component props interface
603
+ - ✅ Use semantic HTML for accessibility
105
604
 
106
- ### Example Block: [src/editor/blocks/Heading/index.tsx](src/editor/blocks/Heading/index.tsx)
107
- ```tsx
108
- import { ComponentConfig } from '@puckeditor/core';
605
+ ### Block Design
606
+ - ✅ Always use `withLayout()` wrapper
607
+ - End block names with `Block` suffix
608
+ - ✅ Provide meaningful `label` for editor sidebar
609
+ - ✅ Use `contentEditable: true` for inline-editable text fields
610
+ - ✅ Use `getItemSummary` for array fields to show previews
109
611
 
110
- export type HeadingProps = {
111
- text: string;
112
- level: 'h1' | 'h2' | 'h3';
113
- };
612
+ ### Page Data
613
+ - ✅ Always include unique `id` in each block's props
614
+ - Match block `type` exactly to exported block name
114
615
 
115
- export const Heading: ComponentConfig<HeadingProps> = {
116
- fields: {
117
- text: { type: 'text' },
118
- level: {
119
- type: 'select',
120
- options: [
121
- { label: 'H1', value: 'h1' },
122
- { label: 'H2', value: 'h2' },
123
- { label: 'H3', value: 'h3' },
124
- ],
125
- },
126
- },
127
- render: ({ text, level: Tag }) => <Tag>{text}</Tag>,
128
- };
129
- ```
616
+ ### Common Pitfalls
617
+ - ❌ Don't use `@measured/puck` - it's been renamed to `@puckeditor/core`
618
+ - Don't wrap only the render function with `withLayout()`
619
+ - ❌ Don't use `type: 'list'` for array fields (use `type: 'array'`)
130
620
 
131
621
  ---
132
622
 
133
- ## 6. Developer Checklist
134
-
135
- Use this checklist to ensure a complete and correct integration:
623
+ ## 11. Developer Checklist
136
624
 
137
- ### Core Setup
138
- - [ ] Install `rankrunners-cms` and `@puckeditor/core`.
139
- - [ ] Add `headWithSEO` to `createRootRoute` in [src/routes/__root.tsx](src/routes/__root.tsx).
140
- - [ ] Add `seoScripts` to `createRootRoute` in [src/routes/__root.tsx](src/routes/__root.tsx).
141
- - [ ] Add `<Scripts />` after the body and `<HeadContent />` in the `<head>` to [src/routes/__root.tsx](src/routes/__root.tsx)
625
+ ### Initial Setup
626
+ - [ ] Install `rankrunners-cms` and `@puckeditor/core`
627
+ - [ ] Set `VITE_PUBLIC_SITE_ID` environment variable
628
+ - [ ] Add `VITE_PUBLIC_SITE_ID` to Dockerfile
142
629
 
143
- ### SEO & Metadata
144
- - [ ] Include `seoHead` in all page routes (`createFileRoute`).
145
- - [ ] Ensure `VITE_PUBLIC_SITE_ID` and other CMS-related environment variables are set, as well in Dockerfile.
146
-
147
- ### Editor Infrastructure
148
- - [ ] Create `src/editor/` structure.
149
- - [ ] Define `UserConfig` and `UserData` types in `types.ts`.
150
- - [ ] Set up `src/editor/root.tsx` with Header/Footer and `DropZone`.
151
- - [ ] Create `initial-data.ts` to populate the editor initially.
630
+ ### Root Route Configuration
631
+ - [ ] Add `headWithSEO` to `createRootRoute`
632
+ - [ ] Add `seoScripts` to `createRootRoute`
633
+ - [ ] Add `<HeadContent />` in `<head>`
634
+ - [ ] Add `<Scripts />` after body content
635
+
636
+ ### Editor Structure
637
+ - [ ] Create `src/editor/index.ts` with config export
638
+ - [ ] Create `src/editor/root.tsx` with Header/Footer wrapper
639
+ - [ ] Create `src/editor/types.ts` with type definitions
640
+
641
+ ### Block Implementation
642
+ - [ ] Create blocks in `src/editor/blocks/`
643
+ - [ ] Use `withLayout()` wrapper for each block
644
+ - [ ] Export block types (e.g., `PageBlockTypes`)
645
+ - [ ] Export blocks object (e.g., `PageBlocks`)
646
+ - [ ] Register blocks in `customCategories`
647
+ - [ ] Register blocks in `customComponents`
648
+
649
+ ### Page Data
650
+ - [ ] Create page data files in `src/editor/initial-data/`
651
+ - [ ] Register paths in `initial.ts` `allPageData` object
652
+
653
+ ### Routes
654
+ - [ ] Create catch-all route `src/routes/$.ts`
655
+
656
+ ### Sitemaps
657
+ - [ ] Implement sitemap serving middleware in `server.ts`
152
658
 
153
659
  ### Component Implementation
154
660
  - [ ] Migrate/Create components in `src/editor/blocks/`.
155
661
  - [ ] Register all blocks in `src/editor/index.tsx`.
156
662
  - [ ] Organize blocks into `categories` within the config.
157
663
 
158
- ### Persistence & Deployment
159
- - [ ] Implement `onPublish` logic in the editor route.
160
- - [ ] (Optional) Set up `server.tsx` blocks for optimal SSR performance.
161
- - [ ] Verify that `@puckeditor/core/no-external.css` is imported in the editor route.
664
+ ### CMS configuration
665
+ - [ ] Added the new pages to the site in the [CMS user interface](https://cms.rankrunners.net) and saved at least once.
666
+
667
+ ---
668
+
669
+ ## 12. Troubleshooting
670
+
671
+ ### "Element type is invalid: expected a string...but got: object"
672
+
673
+ **Cause**: Using `withLayout()` incorrectly.
674
+
675
+ **Fix**: Wrap the entire ComponentConfig object, not just the render function:
676
+
677
+ ```tsx
678
+ // ✅ Correct
679
+ export const MyBlock = withLayout({
680
+ label: '...',
681
+ fields: {...},
682
+ render: (props) => <MyComponent {...props} />,
683
+ })
684
+
685
+ // ❌ Wrong
686
+ export const MyBlock = {
687
+ label: '...',
688
+ fields: {...},
689
+ render: withLayout((props) => <MyComponent {...props} />),
690
+ }
691
+ ```
692
+
693
+ ### Page shows "Loading page..." forever
694
+
695
+ **Cause**: Path mismatch between URL and `allPageData` keys.
696
+
697
+ **Fix**: Ensure keys in `allPageData` match URL paths without leading slashes:
698
+ - URL `/contact` -> key `contact`
699
+ - URL `/services/plumbing` -> key `services/plumbing`
700
+ - URL `/` -> key `''` (empty string)
701
+
702
+ ### Build error: Unexpected character
703
+
704
+ **Cause**: Using special characters in strings.
705
+
706
+ **Fix**: Replace curly quotes and em-dashes:
707
+ - Curly single quotes -> straight single quote `'`
708
+ - Curly double quotes -> straight double quote `"`
709
+ - Em-dash -> hyphen `-` or double hyphen `--`
710
+
711
+ ### TypeScript error with field type
712
+
713
+ **Cause**: Using unsupported field type.
714
+
715
+ **Fix**: Use supported types: `text`, `textarea`, `number`, `select`, `array`, `object`
162
716
 
163
717
  ---
718
+
719
+ ## Quick Start Template
720
+
721
+ Create a new CMS-enabled page in 3 steps:
722
+
723
+ **1. Create the component** (`src/components/MyPage.tsx`)
724
+ ```tsx
725
+ export interface MyPageProps {
726
+ title?: string
727
+ content?: string
728
+ }
729
+
730
+ export const MyPage: React.FC<MyPageProps> = ({
731
+ title = 'Default Title',
732
+ content = 'Default content'
733
+ }) => (
734
+ <div className="max-w-4xl mx-auto py-8 px-4">
735
+ <h1>{title}</h1>
736
+ <p>{content}</p>
737
+ </div>
738
+ )
739
+ ```
740
+
741
+ **2. Create the block** (add to `src/editor/blocks/page-blocks.tsx`)
742
+ ```tsx
743
+ export const MyPageBlock: ComponentConfig<MyPageProps> = withLayout({
744
+ label: 'My Page',
745
+ fields: {
746
+ title: { type: 'text', label: 'Title' },
747
+ content: { type: 'textarea', label: 'Content' },
748
+ },
749
+ defaultProps: {
750
+ title: 'Default Title',
751
+ content: 'Default content',
752
+ },
753
+ render: (props) => <MyPage {...props} />,
754
+ })
755
+ ```
756
+
757
+ **3. Create the page data** (`src/editor/initial-data/mypage-page.ts`)
758
+ ```typescript
759
+ export const mypagePageData: UserData = {
760
+ root: { title: 'My Page | Site Name' },
761
+ content: [
762
+ {
763
+ type: 'MyPageBlock',
764
+ props: { id: 'my-page', title: 'Welcome', content: 'Hello world!' },
765
+ },
766
+ ],
767
+ zones: {},
768
+ }
769
+ ```
770
+
771
+ Then register in `initial.ts`:
772
+ ```typescript
773
+ export const allPageData = {
774
+ // ...
775
+ 'mypage': mypagePageData,
776
+ }
777
+ ```
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "rankrunners-cms",
3
3
  "type": "module",
4
- "version": "0.0.16",
4
+ "version": "0.0.18",
5
5
  "peerDependencies": {
6
6
  "@puckeditor/core": "^0.21.0",
7
7
  "next": "^16.1.2",
@@ -34,7 +34,7 @@
34
34
  "typescript": "^5.9.3",
35
35
  "@tanstack/router-core": "^1.150.0",
36
36
  "@tanstack/react-router": "^1.150.0",
37
- "next": "^16.1.2",
37
+ "next": "^16.1.3",
38
38
  "valibot": "^1.2.0",
39
39
  "lucide-react": "^0.562.0",
40
40
  "react": "^19.2.3"
@@ -1,4 +1,4 @@
1
- import { useState } from "react";
1
+ import { useEffect, useState } from "react";
2
2
  import type { CMSUserData } from "../types";
3
3
  import { Render, type Config } from "@puckeditor/core";
4
4
  import { CMS_BASE_URL, SITE_ID } from "../../api";
@@ -6,7 +6,7 @@ import { fetchWithCache } from "../../libs";
6
6
 
7
7
  const getPageContents = async (
8
8
  pathname: string,
9
- allPageData: Record<string, CMSUserData<any>>
9
+ allPageData: Record<string, CMSUserData<any>>,
10
10
  ) => {
11
11
  // remove trailing (left and right) slashes
12
12
  pathname = pathname.replace(/^\/+|\/+$/g, "");
@@ -18,13 +18,10 @@ const getPageContents = async (
18
18
  headers: {
19
19
  "Content-Type": "application/json",
20
20
  },
21
- }
21
+ },
22
22
  );
23
23
 
24
24
  if (res.ok) {
25
- // console.error("Failed to fetch page data from CMS");
26
- // throw new Error("Failed to fetch page data from CMS");
27
- //}
28
25
  const json = (await res.json()) as { content?: string };
29
26
 
30
27
  if (json.content) {
@@ -68,13 +65,13 @@ export const PageRendererContent = ({
68
65
  }: PageRendererContentProps) => {
69
66
  const [data, setData] = useState<CMSUserData<any> | null>(null);
70
67
 
71
- useState(() => {
68
+ useEffect(() => {
72
69
  const fetchData = async () => {
73
70
  const pageData = await getPageContents(pathname, allPageData);
74
71
  setData(pageData);
75
72
  };
76
73
  fetchData();
77
- });
74
+ }, [pathname]);
78
75
 
79
76
  if (!data) {
80
77
  return (
@@ -0,0 +1,30 @@
1
+ import type { Config } from "@puckeditor/core";
2
+ import { PageRendererContent } from "../../editor";
3
+ import { EditorTanstack } from ".";
4
+ import { usePathnameTanstack, useSearchParamsTanstack } from "../hooks";
5
+ import type { CMSUserData } from "../../editor";
6
+
7
+ export type PageRendererTanstackInitializerProps = {
8
+ config: Config;
9
+ allPageData: Record<string, CMSUserData<any>>;
10
+ };
11
+
12
+ export const PageRendererTanstack =
13
+ ({ config, allPageData }: PageRendererTanstackInitializerProps) =>
14
+ () => {
15
+ const pathname = usePathnameTanstack();
16
+ const searchParams = useSearchParamsTanstack();
17
+ const previewToken = searchParams.get("preview");
18
+
19
+ if (previewToken) {
20
+ return <EditorTanstack config={config} allPageData={allPageData} />;
21
+ }
22
+
23
+ return (
24
+ <PageRendererContent
25
+ config={config}
26
+ allPageData={allPageData}
27
+ pathname={pathname}
28
+ />
29
+ );
30
+ };
@@ -1 +1,2 @@
1
1
  export * from "./Editor";
2
+ export * from "./PageRenderer";
@@ -1,31 +0,0 @@
1
- import { downloadSitemap } from "../../api/client/sitemap";
2
-
3
- /**
4
- * Handler for sitemap routes in TanStack Router
5
- * Use this in your route file with a loader
6
- */
7
- export const createSitemapRoute = async (sitemap: string) => {
8
- const sitemapData = await downloadSitemap(`${sitemap}.xml`);
9
-
10
- return new Response(sitemapData, {
11
- status: 200,
12
- headers: {
13
- "Content-Type": "application/xml",
14
- },
15
- });
16
- };
17
-
18
- /**
19
- * Handler for robots.txt route in TanStack Router
20
- * Use this in your route file with a loader
21
- */
22
- export const createRobotsTxtRoute = async () => {
23
- const robotsTxt = await downloadSitemap("robots.txt");
24
-
25
- return new Response(robotsTxt, {
26
- status: 200,
27
- headers: {
28
- "Content-Type": "text/plain",
29
- },
30
- });
31
- };