create-puck-app 0.19.0-canary.1fc19b5 → 0.19.0-canary.226c08da
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 +1 -1
- package/package.json +2 -2
- package/templates/next/README.md +1 -1
- package/templates/next/package.json.hbs +1 -1
- package/templates/react-router/README.md +39 -0
- package/templates/react-router/app/components/puck-render.tsx +8 -0
- package/templates/react-router/app/lib/pages.server.ts +29 -0
- package/templates/react-router/app/lib/resolve-puck-path.server.ts +19 -0
- package/templates/react-router/app/root.tsx +65 -0
- package/templates/react-router/app/routes/_index.tsx +31 -0
- package/templates/react-router/app/routes/puck-splat.tsx +95 -0
- package/templates/react-router/app/routes.ts +7 -0
- package/templates/react-router/database.json +1 -0
- package/templates/react-router/gitignore +6 -0
- package/templates/react-router/package.json.hbs +33 -0
- package/templates/react-router/public/favicon.ico +0 -0
- package/templates/react-router/puck.config.tsx +23 -0
- package/templates/react-router/react-router.config.ts +7 -0
- package/templates/react-router/tsconfig.json.hbs +27 -0
- package/templates/react-router/vite.config.ts +7 -0
- package/templates/remix/README.md +1 -1
- package/templates/remix/package.json.hbs +1 -1
package/README.md
CHANGED
package/package.json
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
{
|
2
2
|
"name": "create-puck-app",
|
3
|
-
"version": "0.19.0-canary.
|
4
|
-
"author": "
|
3
|
+
"version": "0.19.0-canary.226c08da",
|
4
|
+
"author": "Chris Villa <chris@puckeditor.com>",
|
5
5
|
"repository": "measuredco/puck",
|
6
6
|
"bugs": "https://github.com/measuredco/puck/issues",
|
7
7
|
"homepage": "https://puckeditor.com",
|
package/templates/next/README.md
CHANGED
@@ -0,0 +1,39 @@
|
|
1
|
+
# `react-router` recipe
|
2
|
+
|
3
|
+
The `react-router` recipe showcases one of the most powerful ways to implement Puck using to provide an authoring tool for any route in your React Router app.
|
4
|
+
|
5
|
+
## Demonstrates
|
6
|
+
|
7
|
+
- React Router v7 (framework) implementation
|
8
|
+
- JSON database implementation
|
9
|
+
- Splat route to use puck for any route on the platform
|
10
|
+
|
11
|
+
## Usage
|
12
|
+
|
13
|
+
Run the generator and enter `react-router` when prompted
|
14
|
+
|
15
|
+
```
|
16
|
+
npx create-puck-app my-app
|
17
|
+
```
|
18
|
+
|
19
|
+
Start the server
|
20
|
+
|
21
|
+
```
|
22
|
+
yarn dev
|
23
|
+
```
|
24
|
+
|
25
|
+
Navigate to the homepage at http://localhost:5173/. To edit the homepage, access the Puck editor at http://localhost:5173/edit.
|
26
|
+
|
27
|
+
You can do this for any **base** route on the application, **even if the page doesn't exist**. For example, visit http://localhost:5173/hello-world and you'll receive a 404. You can author and publish a page by visiting http://localhost:5173/hello-world/edit. After publishing, go back to the original URL to see your page.
|
28
|
+
|
29
|
+
## Using this recipe
|
30
|
+
|
31
|
+
To adopt this recipe, you will need to:
|
32
|
+
|
33
|
+
- **IMPORTANT** Add authentication to `/edit` routes. This can be done by modifying the [route module action](https://reactrouter.com/start/framework/route-module#action) in the splat route `/app/routes/puck-splat.tsx`. **If you don't do this, Puck will be completely public.**
|
34
|
+
- Integrate your database into the functions in `/lib/pages.server.ts`
|
35
|
+
- Implement a custom puck configuration in `/app/puck.config.tsx`
|
36
|
+
|
37
|
+
## License
|
38
|
+
|
39
|
+
MIT © [The Puck Contributors](https://github.com/measuredco/puck/graphs/contributors)
|
@@ -0,0 +1,29 @@
|
|
1
|
+
import path from "path";
|
2
|
+
import { fileURLToPath } from "url";
|
3
|
+
import fs from "fs/promises";
|
4
|
+
import type { Data } from "@measured/puck";
|
5
|
+
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
7
|
+
const __dirname = path.dirname(__filename);
|
8
|
+
const databasePath = path.join(__dirname, "..", "..", "database.json");
|
9
|
+
|
10
|
+
export async function getPage(path: string) {
|
11
|
+
const pages = await readDatabase();
|
12
|
+
return pages[path];
|
13
|
+
}
|
14
|
+
|
15
|
+
export async function savePage(path: string, data: Data) {
|
16
|
+
const pages = await readDatabase();
|
17
|
+
pages[path] = data;
|
18
|
+
await fs.writeFile(databasePath, JSON.stringify(pages), { encoding: "utf8" });
|
19
|
+
}
|
20
|
+
|
21
|
+
async function readDatabase() {
|
22
|
+
try {
|
23
|
+
const file = await fs.readFile(databasePath, "utf8");
|
24
|
+
return JSON.parse(file) as Record<string, Data>;
|
25
|
+
} catch (error: unknown) {
|
26
|
+
console.error(error);
|
27
|
+
return {};
|
28
|
+
}
|
29
|
+
}
|
@@ -0,0 +1,19 @@
|
|
1
|
+
export function resolvePuckPath(
|
2
|
+
path = "",
|
3
|
+
// `base` can be any valid origin, it is required for the URL constructor so
|
4
|
+
// we can return a pathname - you can change this if you want, but it isn't
|
5
|
+
// important
|
6
|
+
base = "https://placeholder.com/"
|
7
|
+
) {
|
8
|
+
const url = new URL(path, base);
|
9
|
+
const segments = url.pathname.split("/");
|
10
|
+
const isEditorRoute = segments.at(-1) === "edit";
|
11
|
+
const pathname = isEditorRoute
|
12
|
+
? segments.slice(0, -1).join("/")
|
13
|
+
: url.pathname;
|
14
|
+
|
15
|
+
return {
|
16
|
+
isEditorRoute,
|
17
|
+
path: new URL(pathname, base).pathname,
|
18
|
+
};
|
19
|
+
}
|
@@ -0,0 +1,65 @@
|
|
1
|
+
import {
|
2
|
+
isRouteErrorResponse,
|
3
|
+
Links,
|
4
|
+
Meta,
|
5
|
+
Outlet,
|
6
|
+
Scripts,
|
7
|
+
ScrollRestoration,
|
8
|
+
} from "react-router";
|
9
|
+
|
10
|
+
import type { Route } from "./+types/root";
|
11
|
+
|
12
|
+
export function Layout({ children }: { children: React.ReactNode }) {
|
13
|
+
return (
|
14
|
+
<html lang="en">
|
15
|
+
<head>
|
16
|
+
<meta charSet="utf-8" />
|
17
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
18
|
+
<Meta />
|
19
|
+
<Links />
|
20
|
+
</head>
|
21
|
+
<body>
|
22
|
+
{children}
|
23
|
+
<ScrollRestoration />
|
24
|
+
<Scripts />
|
25
|
+
</body>
|
26
|
+
</html>
|
27
|
+
);
|
28
|
+
}
|
29
|
+
|
30
|
+
export default function App() {
|
31
|
+
return <Outlet />;
|
32
|
+
}
|
33
|
+
|
34
|
+
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
|
35
|
+
let message = "Oops!";
|
36
|
+
let details = "An unexpected error occurred.";
|
37
|
+
let stack: string | undefined;
|
38
|
+
|
39
|
+
if (isRouteErrorResponse(error)) {
|
40
|
+
message = error.status === 404 ? "404" : "Error";
|
41
|
+
details =
|
42
|
+
error.status === 404
|
43
|
+
? "The requested page could not be found."
|
44
|
+
: error.statusText || details;
|
45
|
+
} else if (
|
46
|
+
import.meta.env.NODE_ENV !== "production" &&
|
47
|
+
error &&
|
48
|
+
error instanceof Error
|
49
|
+
) {
|
50
|
+
details = error.message;
|
51
|
+
stack = error.stack;
|
52
|
+
}
|
53
|
+
|
54
|
+
return (
|
55
|
+
<main>
|
56
|
+
<h1>{message}</h1>
|
57
|
+
<p>{details}</p>
|
58
|
+
{stack && (
|
59
|
+
<pre>
|
60
|
+
<code>{stack}</code>
|
61
|
+
</pre>
|
62
|
+
)}
|
63
|
+
</main>
|
64
|
+
);
|
65
|
+
}
|
@@ -0,0 +1,31 @@
|
|
1
|
+
import type { Route } from "./+types/_index";
|
2
|
+
import { PuckRender } from "~/components/puck-render";
|
3
|
+
import { resolvePuckPath } from "~/lib/resolve-puck-path.server";
|
4
|
+
import { getPage } from "~/lib/pages.server";
|
5
|
+
|
6
|
+
export async function loader() {
|
7
|
+
const { isEditorRoute, path } = resolvePuckPath("/");
|
8
|
+
let page = await getPage(path);
|
9
|
+
|
10
|
+
if (!page) {
|
11
|
+
throw new Response("Not Found", { status: 404 });
|
12
|
+
}
|
13
|
+
|
14
|
+
return {
|
15
|
+
isEditorRoute,
|
16
|
+
path,
|
17
|
+
data: page,
|
18
|
+
};
|
19
|
+
}
|
20
|
+
|
21
|
+
export function meta({ data: loaderData }: Route.MetaArgs) {
|
22
|
+
return [
|
23
|
+
{
|
24
|
+
title: loaderData.data.root.title,
|
25
|
+
},
|
26
|
+
];
|
27
|
+
}
|
28
|
+
|
29
|
+
export default function PuckSplatRoute({ loaderData }: Route.ComponentProps) {
|
30
|
+
return <PuckRender data={loaderData.data} />;
|
31
|
+
}
|
@@ -0,0 +1,95 @@
|
|
1
|
+
import { useFetcher, useLoaderData } from "react-router";
|
2
|
+
import type { Data } from "@measured/puck";
|
3
|
+
import { Puck, Render } from "@measured/puck";
|
4
|
+
|
5
|
+
import type { Route } from "./+types/puck-splat";
|
6
|
+
import { config } from "../../puck.config";
|
7
|
+
import { resolvePuckPath } from "~/lib/resolve-puck-path.server";
|
8
|
+
import { getPage, savePage } from "~/lib/pages.server";
|
9
|
+
import editorStyles from "@measured/puck/puck.css?url";
|
10
|
+
|
11
|
+
export async function loader({ params }: Route.LoaderArgs) {
|
12
|
+
const pathname = params["*"] ?? "/";
|
13
|
+
const { isEditorRoute, path } = resolvePuckPath(pathname);
|
14
|
+
let page = await getPage(path);
|
15
|
+
|
16
|
+
// Throw a 404 if we're not rendering the editor and data for the page does not exist
|
17
|
+
if (!isEditorRoute && !page) {
|
18
|
+
throw new Response("Not Found", { status: 404 });
|
19
|
+
}
|
20
|
+
|
21
|
+
// Empty shell for new pages
|
22
|
+
if (isEditorRoute && !page) {
|
23
|
+
page = {
|
24
|
+
content: [],
|
25
|
+
root: {
|
26
|
+
props: {
|
27
|
+
title: "",
|
28
|
+
},
|
29
|
+
},
|
30
|
+
};
|
31
|
+
}
|
32
|
+
|
33
|
+
return {
|
34
|
+
isEditorRoute,
|
35
|
+
path,
|
36
|
+
data: page,
|
37
|
+
};
|
38
|
+
}
|
39
|
+
|
40
|
+
export function meta({ data: loaderData }: Route.MetaArgs) {
|
41
|
+
return [
|
42
|
+
{
|
43
|
+
title: loaderData.isEditorRoute
|
44
|
+
? `Edit: ${loaderData.path}`
|
45
|
+
: loaderData.data.root.title,
|
46
|
+
},
|
47
|
+
];
|
48
|
+
}
|
49
|
+
|
50
|
+
export async function action({ params, request }: Route.ActionArgs) {
|
51
|
+
const pathname = params["*"] ?? "/";
|
52
|
+
const { path } = resolvePuckPath(pathname);
|
53
|
+
const body = (await request.json()) as { data: Data };
|
54
|
+
|
55
|
+
await savePage(path, body.data);
|
56
|
+
}
|
57
|
+
|
58
|
+
function Editor() {
|
59
|
+
const loaderData = useLoaderData<typeof loader>();
|
60
|
+
const fetcher = useFetcher<typeof action>();
|
61
|
+
|
62
|
+
return (
|
63
|
+
<>
|
64
|
+
<link rel="stylesheet" href={editorStyles} id="puck-css" />
|
65
|
+
<Puck
|
66
|
+
config={config}
|
67
|
+
data={loaderData.data}
|
68
|
+
onPublish={async (data) => {
|
69
|
+
await fetcher.submit(
|
70
|
+
{
|
71
|
+
data,
|
72
|
+
},
|
73
|
+
{
|
74
|
+
action: "",
|
75
|
+
method: "post",
|
76
|
+
encType: "application/json",
|
77
|
+
}
|
78
|
+
);
|
79
|
+
}}
|
80
|
+
/>
|
81
|
+
</>
|
82
|
+
);
|
83
|
+
}
|
84
|
+
|
85
|
+
export default function PuckSplatRoute({ loaderData }: Route.ComponentProps) {
|
86
|
+
return (
|
87
|
+
<div>
|
88
|
+
{loaderData.isEditorRoute ? (
|
89
|
+
<Editor />
|
90
|
+
) : (
|
91
|
+
<Render config={config} data={loaderData.data} />
|
92
|
+
)}
|
93
|
+
</div>
|
94
|
+
);
|
95
|
+
}
|
@@ -0,0 +1 @@
|
|
1
|
+
{"/":{"content":[{"type":"HeadingBlock","props":{"title":"Edit this page by adding /edit to the end of the URL","id":"HeadingBlock-1694032984497"}}],"root":{"props":{"title":"Puck + React Router 7 demo"}},"zones":{}}}
|
@@ -0,0 +1,33 @@
|
|
1
|
+
{
|
2
|
+
"name": "{{appName}}",
|
3
|
+
"version": "1.0.0",
|
4
|
+
"private": true,
|
5
|
+
"type": "module",
|
6
|
+
"scripts": {
|
7
|
+
"build": "react-router build",
|
8
|
+
"dev": "react-router dev",
|
9
|
+
"start": "react-router-serve ./build/server/index.js",
|
10
|
+
"typecheck": "react-router typegen && tsc"
|
11
|
+
},
|
12
|
+
"dependencies": {
|
13
|
+
"@measured/puck": "{{puckVersion}}",
|
14
|
+
"@react-router/node": "^7.5.3",
|
15
|
+
"@react-router/serve": "^7.5.3",
|
16
|
+
"isbot": "^5",
|
17
|
+
"react": "^19.1.0",
|
18
|
+
"react-dom": "^19.1.0",
|
19
|
+
"react-router": "^7.5.3"
|
20
|
+
},
|
21
|
+
"devDependencies": {
|
22
|
+
"@react-router/dev": "^7.5.3",
|
23
|
+
"@types/node": "^20",
|
24
|
+
"@types/react": "^19.1.2",
|
25
|
+
"@types/react-dom": "^19.1.2",
|
26
|
+
"typescript": "^5.8.3",
|
27
|
+
"vite": "^6.3.3",
|
28
|
+
"vite
|
29
|
+
},
|
30
|
+
"engines": {
|
31
|
+
"node": ">=20.0.0"
|
32
|
+
}
|
33
|
+
}
|
Binary file
|
@@ -0,0 +1,23 @@
|
|
1
|
+
import type { Config } from "@measured/puck";
|
2
|
+
|
3
|
+
type Props = {
|
4
|
+
HeadingBlock: { title: string };
|
5
|
+
};
|
6
|
+
|
7
|
+
export const config: Config<Props> = {
|
8
|
+
components: {
|
9
|
+
HeadingBlock: {
|
10
|
+
fields: {
|
11
|
+
title: { type: "text" },
|
12
|
+
},
|
13
|
+
defaultProps: {
|
14
|
+
title: "Heading",
|
15
|
+
},
|
16
|
+
render: ({ title }) => (
|
17
|
+
<div style={{ padding: 64 }}>
|
18
|
+
<h1>{title}</h1>
|
19
|
+
</div>
|
20
|
+
),
|
21
|
+
},
|
22
|
+
},
|
23
|
+
};
|
@@ -0,0 +1,27 @@
|
|
1
|
+
{
|
2
|
+
"include": [
|
3
|
+
"**/*",
|
4
|
+
"**/.server/**/*",
|
5
|
+
"**/.client/**/*",
|
6
|
+
".react-router/types/**/*"
|
7
|
+
],
|
8
|
+
"compilerOptions": {
|
9
|
+
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
10
|
+
"types": ["node", "vite/client"],
|
11
|
+
"target": "ES2022",
|
12
|
+
"module": "ES2022",
|
13
|
+
"moduleResolution": "bundler",
|
14
|
+
"jsx": "react-jsx",
|
15
|
+
"rootDirs": [".", "./.react-router/types"],
|
16
|
+
"baseUrl": ".",
|
17
|
+
"paths": {
|
18
|
+
"~/*": ["./app/*"]
|
19
|
+
},
|
20
|
+
"esModuleInterop": true,
|
21
|
+
"verbatimModuleSyntax": true,
|
22
|
+
"noEmit": true,
|
23
|
+
"resolveJsonModule": true,
|
24
|
+
"skipLibCheck": true,
|
25
|
+
"strict": true
|
26
|
+
}
|
27
|
+
}
|