@pantheon-systems/create-p1-starter-kit 0.1.0 → 0.4.2
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/lib/messages.js +2 -1
- package/lib/messages.test.js +29 -0
- package/package.json +11 -2
- package/template/.env.example +4 -0
- package/template/CHANGELOG.md +11 -0
- package/template/app/[...puckPath]/client.tsx +33 -2
- package/template/app/[...puckPath]/page.tsx +10 -1
- package/template/app/layout.tsx +1 -1
- package/template/app/p1/[[...p1]]/editor-client.tsx +72 -7
- package/template/app/page.tsx +10 -1
- package/template/components/puck/paragraph-block.tsx +5 -1
- package/template/components/puck/root.tsx +1 -2
- package/template/package.json +3 -3
- package/template/README.md +0 -53
package/lib/messages.js
CHANGED
|
@@ -13,8 +13,9 @@ export function showSuccess(projectName, projectPath, packageManager) {
|
|
|
13
13
|
console.log(` ${pc.dim('# Copy .env.example to .env and fill in your credentials:')}`);
|
|
14
14
|
console.log(` ${pc.cyan('cp')} .env.example .env`);
|
|
15
15
|
console.log(` ${pc.dim('# Edit .env with your PCC_SITE_ID and PCC_TOKEN')}\n`);
|
|
16
|
+
const devCmd = packageManager === 'npm' ? 'npm run dev' : `${packageManager} dev`;
|
|
16
17
|
console.log(` ${pc.dim('# Start the dev server:')}`);
|
|
17
|
-
console.log(` ${pc.cyan(
|
|
18
|
+
console.log(` ${pc.cyan(devCmd)}\n`);
|
|
18
19
|
console.log(pc.bold('Happy building! 🚀\n'));
|
|
19
20
|
}
|
|
20
21
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { showSuccess } from './messages.js';
|
|
3
|
+
|
|
4
|
+
describe('showSuccess', () => {
|
|
5
|
+
let output;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
output = [];
|
|
9
|
+
vi.spyOn(console, 'log').mockImplementation((...args) => output.push(args.join(' ')));
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('shows "npm run dev" for npm users', () => {
|
|
13
|
+
showSuccess('my-app', '/tmp/my-app', 'npm');
|
|
14
|
+
const allOutput = output.join('\n');
|
|
15
|
+
expect(allOutput).toContain('npm run dev');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('shows "pnpm dev" for pnpm users', () => {
|
|
19
|
+
showSuccess('my-app', '/tmp/my-app', 'pnpm');
|
|
20
|
+
const allOutput = output.join('\n');
|
|
21
|
+
expect(allOutput).toContain('pnpm dev');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('shows "yarn dev" for yarn users', () => {
|
|
25
|
+
showSuccess('my-app', '/tmp/my-app', 'yarn');
|
|
26
|
+
const allOutput = output.join('\n');
|
|
27
|
+
expect(allOutput).toContain('yarn dev');
|
|
28
|
+
});
|
|
29
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pantheon-systems/create-p1-starter-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "Scaffold a new P1 starter project",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -11,6 +11,11 @@
|
|
|
11
11
|
"lib/",
|
|
12
12
|
"template/"
|
|
13
13
|
],
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public",
|
|
16
|
+
"registry": "https://registry.npmjs.org/",
|
|
17
|
+
"provenance": false
|
|
18
|
+
},
|
|
14
19
|
"repository": {
|
|
15
20
|
"type": "git",
|
|
16
21
|
"url": "https://github.com/pantheon-systems/puck-css-integration.git",
|
|
@@ -27,12 +32,16 @@
|
|
|
27
32
|
"engines": {
|
|
28
33
|
"node": ">=20.12.0"
|
|
29
34
|
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"vitest": "^4.1.5"
|
|
37
|
+
},
|
|
30
38
|
"dependencies": {
|
|
31
39
|
"@clack/prompts": "^1.5.1",
|
|
32
40
|
"picocolors": "^1.1.1"
|
|
33
41
|
},
|
|
34
42
|
"license": "MIT",
|
|
35
43
|
"scripts": {
|
|
36
|
-
"build": "node scripts/build-template.js"
|
|
44
|
+
"build": "node scripts/build-template.js",
|
|
45
|
+
"test": "vitest run"
|
|
37
46
|
}
|
|
38
47
|
}
|
package/template/.env.example
CHANGED
|
@@ -6,5 +6,9 @@ NEXT_PUBLIC_CSS_BASE_URL=https://css.example.com
|
|
|
6
6
|
NEXT_PUBLIC_CSS_SITE_ID=site-123
|
|
7
7
|
P1_CSS_API_KEY=your-api-key
|
|
8
8
|
|
|
9
|
+
# --- P1 Admin Dashboard (optional) ---
|
|
10
|
+
# Override the default dashboard URL (https://content.pantheon.io)
|
|
11
|
+
# NEXT_PUBLIC_P1_ADMIN_DASHBOARD_URL=https://staging.content.pantheon.io
|
|
12
|
+
|
|
9
13
|
# Branch is auto-detected (defaults to main) unless specified:
|
|
10
14
|
# NEXT_PUBLIC_CSS_BRANCH_ID=branch-456
|
|
@@ -4,6 +4,37 @@ import type { Data } from "@puckeditor/core";
|
|
|
4
4
|
import { RenderClient } from "@pantheon-systems/puck-css";
|
|
5
5
|
import config from "../../puck.config";
|
|
6
6
|
|
|
7
|
-
export function Client({
|
|
8
|
-
|
|
7
|
+
export function Client({
|
|
8
|
+
data,
|
|
9
|
+
pageMetadata,
|
|
10
|
+
}: {
|
|
11
|
+
data: Data;
|
|
12
|
+
pageMetadata?: {
|
|
13
|
+
route: string;
|
|
14
|
+
documentName?: string;
|
|
15
|
+
pageType?: "page" | "template" | "override";
|
|
16
|
+
};
|
|
17
|
+
}) {
|
|
18
|
+
return (
|
|
19
|
+
<>
|
|
20
|
+
<RenderClient config={config} data={data} />
|
|
21
|
+
{pageMetadata && (
|
|
22
|
+
<footer className="mt-16 border-t border-gray-200 py-4 text-center text-sm text-gray-500">
|
|
23
|
+
Rendered with{" "}
|
|
24
|
+
<span className="font-medium">
|
|
25
|
+
{pageMetadata.documentName || pageMetadata.route}
|
|
26
|
+
</span>{" "}
|
|
27
|
+
from{" "}
|
|
28
|
+
<span className="font-medium">
|
|
29
|
+
{pageMetadata.pageType === "page" && "page"}
|
|
30
|
+
{pageMetadata.pageType === "template" && "page template"}
|
|
31
|
+
{pageMetadata.pageType === "override" && "page template override"}
|
|
32
|
+
{!pageMetadata.pageType && "page"}
|
|
33
|
+
</span>{" "}
|
|
34
|
+
at route{" "}
|
|
35
|
+
<span className="font-mono text-xs">{pageMetadata.route}</span>
|
|
36
|
+
</footer>
|
|
37
|
+
)}
|
|
38
|
+
</>
|
|
39
|
+
);
|
|
9
40
|
}
|
|
@@ -125,7 +125,16 @@ export default async function Page({
|
|
|
125
125
|
});
|
|
126
126
|
const resolvedData = await resolveDataTemplates(data, context);
|
|
127
127
|
|
|
128
|
-
return
|
|
128
|
+
return (
|
|
129
|
+
<Client
|
|
130
|
+
data={resolvedData}
|
|
131
|
+
pageMetadata={{
|
|
132
|
+
route: path,
|
|
133
|
+
documentName: data.root.props?.title as string | undefined,
|
|
134
|
+
pageType: "page",
|
|
135
|
+
}}
|
|
136
|
+
/>
|
|
137
|
+
);
|
|
129
138
|
}
|
|
130
139
|
|
|
131
140
|
export const dynamic = "force-dynamic";
|
package/template/app/layout.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useCallback } from "react";
|
|
3
|
+
import React, { useCallback } from "react";
|
|
4
4
|
import { useRouter } from "next/navigation";
|
|
5
5
|
import { Puck } from "@puckeditor/core";
|
|
6
6
|
import {
|
|
@@ -31,6 +31,9 @@ try {
|
|
|
31
31
|
const editorConfig = wrapConfigForEditorPreview(config);
|
|
32
32
|
|
|
33
33
|
export function EditorClientWrapper({ path }: { path: string }) {
|
|
34
|
+
// Keep ref at this level - EditorClientWrapper doesn't unmount on branch switch
|
|
35
|
+
const lastGoodStateRef = React.useRef<{ puckKey: string; puckProps: any } | null>(null);
|
|
36
|
+
|
|
34
37
|
if (!p1Config) {
|
|
35
38
|
return (
|
|
36
39
|
<div style={{ textAlign: "center", padding: "4rem", fontFamily: "system-ui" }}>
|
|
@@ -53,14 +56,20 @@ export function EditorClientWrapper({ path }: { path: string }) {
|
|
|
53
56
|
config={p1Config}
|
|
54
57
|
loginPageProps={{ title: "P1 Starter", subtitle: "Sign in to edit" }}
|
|
55
58
|
>
|
|
56
|
-
<EditorContent path={path} />
|
|
59
|
+
<EditorContent path={path} lastGoodStateRef={lastGoodStateRef} />
|
|
57
60
|
</P1App>
|
|
58
61
|
</P1NextRouterProvider>
|
|
59
62
|
</P1QueryProvider>
|
|
60
63
|
);
|
|
61
64
|
}
|
|
62
65
|
|
|
63
|
-
function EditorContent({
|
|
66
|
+
function EditorContent({
|
|
67
|
+
path,
|
|
68
|
+
lastGoodStateRef,
|
|
69
|
+
}: {
|
|
70
|
+
path: string;
|
|
71
|
+
lastGoodStateRef: React.MutableRefObject<{ puckKey: string; puckProps: any } | null>;
|
|
72
|
+
}) {
|
|
64
73
|
const router = useRouter();
|
|
65
74
|
const p1Plugins = useP1Plugins(path, config);
|
|
66
75
|
|
|
@@ -78,6 +87,8 @@ function EditorContent({ path }: { path: string }) {
|
|
|
78
87
|
pluginOptions: {
|
|
79
88
|
onDocumentSelect: handleDocumentSelect,
|
|
80
89
|
selectedDocumentPath: path,
|
|
90
|
+
siteId: process.env.NEXT_PUBLIC_CSS_SITE_ID,
|
|
91
|
+
dashboardUrl: process.env.NEXT_PUBLIC_P1_ADMIN_DASHBOARD_URL,
|
|
81
92
|
},
|
|
82
93
|
overrideOptions: {
|
|
83
94
|
showDefaultPublish: false,
|
|
@@ -90,7 +101,15 @@ function EditorContent({ path }: { path: string }) {
|
|
|
90
101
|
},
|
|
91
102
|
});
|
|
92
103
|
|
|
93
|
-
|
|
104
|
+
// Update last good state when loading completes successfully (ref passed from parent)
|
|
105
|
+
React.useEffect(() => {
|
|
106
|
+
if (!loading && !error) {
|
|
107
|
+
lastGoodStateRef.current = { puckKey, puckProps };
|
|
108
|
+
}
|
|
109
|
+
}, [loading, error, puckKey, puckProps]);
|
|
110
|
+
|
|
111
|
+
// Show full loading screen only on first load (no previous state)
|
|
112
|
+
if (loading && !lastGoodStateRef.current) {
|
|
94
113
|
return (
|
|
95
114
|
<div style={{ textAlign: "center", padding: "4rem", fontFamily: "system-ui" }}>
|
|
96
115
|
Loading editor...
|
|
@@ -98,7 +117,8 @@ function EditorContent({ path }: { path: string }) {
|
|
|
98
117
|
);
|
|
99
118
|
}
|
|
100
119
|
|
|
101
|
-
|
|
120
|
+
// Show error only if we have no previous state to fall back to
|
|
121
|
+
if (error && !lastGoodStateRef.current) {
|
|
102
122
|
return (
|
|
103
123
|
<div style={{ textAlign: "center", padding: "4rem", fontFamily: "system-ui" }}>
|
|
104
124
|
<h3>Error loading document</h3>
|
|
@@ -107,10 +127,55 @@ function EditorContent({ path }: { path: string }) {
|
|
|
107
127
|
);
|
|
108
128
|
}
|
|
109
129
|
|
|
130
|
+
// Use current state if loaded, otherwise keep showing last good state
|
|
131
|
+
const displayState = (!loading && !error)
|
|
132
|
+
? { puckKey, puckProps }
|
|
133
|
+
: lastGoodStateRef.current ?? { puckKey, puckProps };
|
|
134
|
+
|
|
110
135
|
return (
|
|
111
|
-
<div className="puck-editor-theme">
|
|
136
|
+
<div className="puck-editor-theme" style={{ position: "relative" }}>
|
|
137
|
+
{/* Loading overlay - shown during branch switch */}
|
|
138
|
+
{loading && lastGoodStateRef.current && (
|
|
139
|
+
<div
|
|
140
|
+
style={{
|
|
141
|
+
position: "fixed",
|
|
142
|
+
top: "50%",
|
|
143
|
+
left: "50%",
|
|
144
|
+
transform: "translate(-50%, -50%)",
|
|
145
|
+
zIndex: 9999,
|
|
146
|
+
background: "rgba(255, 255, 255, 0.95)",
|
|
147
|
+
padding: "1rem 2rem",
|
|
148
|
+
borderRadius: "8px",
|
|
149
|
+
boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)",
|
|
150
|
+
fontFamily: "system-ui",
|
|
151
|
+
fontSize: "14px",
|
|
152
|
+
color: "#333",
|
|
153
|
+
fontWeight: 500,
|
|
154
|
+
display: "flex",
|
|
155
|
+
alignItems: "center",
|
|
156
|
+
gap: "0.75rem",
|
|
157
|
+
}}
|
|
158
|
+
>
|
|
159
|
+
<div
|
|
160
|
+
style={{
|
|
161
|
+
width: "16px",
|
|
162
|
+
height: "16px",
|
|
163
|
+
border: "2px solid #e0e0e0",
|
|
164
|
+
borderTopColor: "#2563eb",
|
|
165
|
+
borderRadius: "50%",
|
|
166
|
+
animation: "spin 0.6s linear infinite",
|
|
167
|
+
}}
|
|
168
|
+
/>
|
|
169
|
+
Switching workstream...
|
|
170
|
+
<style>{`
|
|
171
|
+
@keyframes spin {
|
|
172
|
+
to { transform: rotate(360deg); }
|
|
173
|
+
}
|
|
174
|
+
`}</style>
|
|
175
|
+
</div>
|
|
176
|
+
)}
|
|
112
177
|
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
|
|
113
|
-
<Puck key={puckKey} {...puckProps as any} _experimentalFullScreenCanvas={true} />
|
|
178
|
+
<Puck key={displayState.puckKey} {...displayState.puckProps as any} _experimentalFullScreenCanvas={true} />
|
|
114
179
|
</div>
|
|
115
180
|
);
|
|
116
181
|
}
|
package/template/app/page.tsx
CHANGED
|
@@ -64,7 +64,16 @@ export default async function HomePage() {
|
|
|
64
64
|
referencedDatasourceIds,
|
|
65
65
|
});
|
|
66
66
|
const resolvedData = await resolveDataTemplates(data, context);
|
|
67
|
-
return
|
|
67
|
+
return (
|
|
68
|
+
<Client
|
|
69
|
+
data={resolvedData}
|
|
70
|
+
pageMetadata={{
|
|
71
|
+
route: "/",
|
|
72
|
+
documentName: data.root.props?.title as string | undefined,
|
|
73
|
+
pageType: "page",
|
|
74
|
+
}}
|
|
75
|
+
/>
|
|
76
|
+
);
|
|
68
77
|
}
|
|
69
78
|
|
|
70
79
|
const routes = await listRoutes();
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isValidElement, type ReactNode } from "react";
|
|
1
2
|
import { blockPaddingClass } from "./block-padding";
|
|
2
3
|
import ReactMarkdown from "react-markdown";
|
|
3
4
|
|
|
@@ -19,7 +20,10 @@ export const paragraphBlock = {
|
|
|
19
20
|
defaultProps: {
|
|
20
21
|
text: "Add your copy here. You can use multiple lines.",
|
|
21
22
|
},
|
|
22
|
-
render: ({ text }: { text?: string }) => {
|
|
23
|
+
render: ({ text }: { text?: string | ReactNode }) => {
|
|
24
|
+
if (isValidElement(text)) {
|
|
25
|
+
return <div className={blockPaddingClass}>{text}</div>;
|
|
26
|
+
}
|
|
23
27
|
const markdown = asMarkdownText(text);
|
|
24
28
|
return (
|
|
25
29
|
<div className={blockPaddingClass}>
|
|
@@ -9,10 +9,9 @@ export const puckRoot = {
|
|
|
9
9
|
title: "My Puck Editor",
|
|
10
10
|
},
|
|
11
11
|
render: (props: { children?: ReactNode; title?: string }) => {
|
|
12
|
-
const { children
|
|
12
|
+
const { children } = props;
|
|
13
13
|
return (
|
|
14
14
|
<div className="font-sans antialiased">
|
|
15
|
-
<h1>{title}</h1>
|
|
16
15
|
{children}
|
|
17
16
|
</div>
|
|
18
17
|
);
|
package/template/package.json
CHANGED
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"@pantheon-systems/cpub-react-sdk": "^5.2.1",
|
|
14
|
-
"@pantheon-systems/css-client": "^0.4.
|
|
15
|
-
"@pantheon-systems/p1-next-sdk": "^0.
|
|
16
|
-
"@pantheon-systems/puck-css": "^0.4.
|
|
14
|
+
"@pantheon-systems/css-client": "^0.4.2",
|
|
15
|
+
"@pantheon-systems/p1-next-sdk": "^0.4.2",
|
|
16
|
+
"@pantheon-systems/puck-css": "^0.4.2",
|
|
17
17
|
"@puckeditor/core": "^0.21.1",
|
|
18
18
|
"@tailwindcss/postcss": "^4.2.2",
|
|
19
19
|
"classnames": "^2.5.1",
|
package/template/README.md
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
# Content Publisher Site Sample
|
|
2
|
-
|
|
3
|
-
A sample Next.js site demonstrating the Content Publisher CMS — Pantheon's visual page-building and content management system. Content Publisher combines a Puck-based drag-and-drop editor with server-side datasource resolution, enabling authors to build dynamic, data-driven pages without writing code.
|
|
4
|
-
|
|
5
|
-
## What's included
|
|
6
|
-
|
|
7
|
-
- **Visual page editor** — Puck-powered drag-and-drop editing at `/p1/<path>` with built-in blocks for typography, media, layout, and actions
|
|
8
|
-
- **Site structure management** — Dashboard at `/p1` for creating and organizing pages and templates
|
|
9
|
-
- **Datasource bindings** — Connect page blocks to live data from Content Publisher articles, external APIs (SWAPI, Pokemon GraphQL), and URL route parameters
|
|
10
|
-
- **Next.js App Router** — Server components, catch-all routing, and API route handlers via `@pantheon-systems/p1-client-sdk`
|
|
11
|
-
|
|
12
|
-
## Getting started
|
|
13
|
-
|
|
14
|
-
1. Copy the environment file and fill in your credentials:
|
|
15
|
-
|
|
16
|
-
```
|
|
17
|
-
cp .env.example .env
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
| Variable | Description |
|
|
21
|
-
| ------------- | -------------------------- |
|
|
22
|
-
| `PCC_SITE_ID` | Your Content Cloud site ID |
|
|
23
|
-
| `PCC_TOKEN` | API token for your site |
|
|
24
|
-
|
|
25
|
-
2. Install dependencies and start the dev server:
|
|
26
|
-
|
|
27
|
-
```
|
|
28
|
-
npm install
|
|
29
|
-
npm run dev
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
3. Open http://localhost:3000 to view the site, or http://localhost:3000/p1 for the page management dashboard.
|
|
33
|
-
|
|
34
|
-
## Project structure
|
|
35
|
-
|
|
36
|
-
```
|
|
37
|
-
app/
|
|
38
|
-
page.tsx # Site root (renders via Puck)
|
|
39
|
-
[...puckPath]/ # Catch-all route for published pages
|
|
40
|
-
p1/
|
|
41
|
-
page.tsx # P1 dashboard — lists pages, links to editor
|
|
42
|
-
[...p1]/ # Editor & renderer for any page path
|
|
43
|
-
api/[...p1]/route.ts # API handler (GET/POST/DELETE) for page data
|
|
44
|
-
components/puck/ # Block definitions (Heading, Paragraph, Image, etc.)
|
|
45
|
-
lib/builtin-datasources.ts # Datasource registry (SWAPI, Pokemon, articles, URL params)
|
|
46
|
-
puck.config.tsx # Puck editor configuration — block registry & categories
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
## Customization
|
|
50
|
-
|
|
51
|
-
- **Add blocks** — Create a new component in `components/puck/`, then register it in `puck.config.tsx`
|
|
52
|
-
- **Add datasources** — Define a new `DatasourceDefinition` in `lib/builtin-datasources.ts` to make external data available to block fields via `{{ datasource.field }}` expressions
|
|
53
|
-
- **Change styling** — The project uses Tailwind CSS v4; edit `app/styles.css` or individual block components
|