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 CHANGED
@@ -18,4 +18,4 @@ yarn create puck-app my-app
18
18
 
19
19
  ## License
20
20
 
21
- MIT © [Measured Corporation Ltd](https://measured.co)
21
+ MIT © [The Puck Contributors](https://github.com/measuredco/puck/graphs/contributors)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "create-puck-app",
3
- "version": "0.19.0-canary.1fc19b5",
4
- "author": "Measured Corporation Ltd <hello@measured.co>",
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",
@@ -39,4 +39,4 @@ By default, this recipe will generate static pages by setting `dynamic` to [`for
39
39
 
40
40
  ## License
41
41
 
42
- MIT © [Measured Co.](https://github.com/measuredco)
42
+ MIT © [The Puck Contributors](https://github.com/measuredco/puck/graphs/contributors)
@@ -11,7 +11,7 @@
11
11
  "dependencies": {
12
12
  "@measured/puck": "{{puckVersion}}",
13
13
  "classnames": "^2.3.2",
14
- "next": "^15.1.0",
14
+ "next": "^15.2.4",
15
15
  "react": "^19.0.0",
16
16
  "react-dom": "^19.0.0"
17
17
  },
@@ -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,8 @@
1
+ import type { Data } from "@measured/puck";
2
+ import { Render } from "@measured/puck";
3
+
4
+ import { config } from "../../puck.config";
5
+
6
+ export function PuckRender({ data }: { data: Data }) {
7
+ return <Render config={config} data={data} />;
8
+ }
@@ -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,7 @@
1
+ import type { RouteConfig } from "@react-router/dev/routes";
2
+ import { route, index } from "@react-router/dev/routes";
3
+
4
+ export default [
5
+ index("routes/_index.tsx"),
6
+ route("*", "routes/puck-splat.tsx"),
7
+ ] satisfies RouteConfig;
@@ -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,6 @@
1
+ .DS_Store
2
+ /node_modules/
3
+
4
+ # React Router
5
+ /.react-router/
6
+ /build/
@@ -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
+ }
@@ -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,7 @@
1
+ import type { Config } from "@react-router/dev/config";
2
+
3
+ export default {
4
+ // Config options...
5
+ // Server-side render by default, to enable SPA mode set this to `false`
6
+ ssr: true,
7
+ } satisfies Config;
@@ -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
+ }
@@ -0,0 +1,7 @@
1
+ import { reactRouter } from "@react-router/dev/vite";
2
+ import { defineConfig } from "vite";
3
+ import tsconfigPaths from "vite-tsconfig-paths";
4
+
5
+ export default defineConfig({
6
+ plugins: [reactRouter(), tsconfigPaths()],
7
+ });
@@ -43,4 +43,4 @@ This recipe can be adapted to disable JavaScript. See the [Remix docs](https://r
43
43
 
44
44
  ## License
45
45
 
46
- MIT © [Measured Co.](https://github.com/measuredco)
46
+ MIT © [The Puck Contributors](https://github.com/measuredco/puck/graphs/contributors)
@@ -29,6 +29,6 @@
29
29
  "typescript": "^5.1.6"
30
30
  },
31
31
  "engines": {
32
- "node": ">=18.0.0"
32
+ "node": ">=20.0.0"
33
33
  }
34
34
  }