create-dispatch-app 0.0.6
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/bin.js +47 -0
- package/package.json +14 -0
- package/template/.env.local.example +3 -0
- package/template/README.md +20 -0
- package/template/app/blog/[slug]/PostContent.tsx +45 -0
- package/template/app/blog/[slug]/TipTapContent.tsx +28 -0
- package/template/app/blog/[slug]/page.tsx +10 -0
- package/template/app/globals.css +3 -0
- package/template/app/layout.tsx +24 -0
- package/template/app/page.tsx +53 -0
- package/template/next-env.d.ts +2 -0
- package/template/next.config.ts +5 -0
- package/template/package.json +27 -0
- package/template/postcss.config.mjs +8 -0
- package/template/tailwind.config.ts +12 -0
- package/template/tsconfig.json +21 -0
package/bin.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const { execSync } = require("child_process");
|
|
6
|
+
|
|
7
|
+
const templateDir = path.join(__dirname, "template");
|
|
8
|
+
const appName = process.argv[2];
|
|
9
|
+
|
|
10
|
+
if (!appName) {
|
|
11
|
+
console.error("Usage: npx create-dispatch-app <app-name>");
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const targetDir = path.resolve(process.cwd(), appName);
|
|
16
|
+
|
|
17
|
+
if (fs.existsSync(targetDir)) {
|
|
18
|
+
console.error(`Error: Directory "${appName}" already exists.`);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function copyRecursive(src, dest) {
|
|
23
|
+
const stat = fs.statSync(src);
|
|
24
|
+
if (stat.isDirectory()) {
|
|
25
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
26
|
+
for (const entry of fs.readdirSync(src)) {
|
|
27
|
+
copyRecursive(path.join(src, entry), path.join(dest, entry));
|
|
28
|
+
}
|
|
29
|
+
} else {
|
|
30
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
31
|
+
fs.copyFileSync(src, dest);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
console.log(`Creating ${appName}...`);
|
|
36
|
+
copyRecursive(templateDir, targetDir);
|
|
37
|
+
|
|
38
|
+
console.log("Installing dependencies...");
|
|
39
|
+
execSync("npm install", { cwd: targetDir, stdio: "inherit" });
|
|
40
|
+
|
|
41
|
+
console.log("");
|
|
42
|
+
console.log("Done! Next steps:");
|
|
43
|
+
console.log(` cd ${appName}`);
|
|
44
|
+
console.log(" cp .env.local.example .env.local");
|
|
45
|
+
console.log(" Add your site key from Dispatch to .env.local");
|
|
46
|
+
console.log(" npm run dev");
|
|
47
|
+
console.log("");
|
package/package.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-dispatch-app",
|
|
3
|
+
"version": "0.0.6",
|
|
4
|
+
"description": "Scaffold a Next.js app with Dispatch CMS",
|
|
5
|
+
"bin": {
|
|
6
|
+
"create-dispatch-app": "./bin.js"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {},
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=20.9"
|
|
11
|
+
},
|
|
12
|
+
"keywords": ["dispatch", "cms", "next.js", "scaffold", "cli"],
|
|
13
|
+
"license": "MIT"
|
|
14
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Dispatch Blog
|
|
2
|
+
|
|
3
|
+
A minimal Next.js blog powered by [Dispatch](https://dispatch-cms.vercel.app) CMS.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
1. **Get your site key** from [Dispatch](https://dispatch-cms.vercel.app): log in, open **Sites**, and create a site if needed. Copy the site key.
|
|
8
|
+
|
|
9
|
+
2. **Configure env**
|
|
10
|
+
- Copy `.env.local.example` to `.env.local`
|
|
11
|
+
- Set `NEXT_PUBLIC_DISPATCH_SITE_KEY` to your site key
|
|
12
|
+
|
|
13
|
+
3. **Run the app**
|
|
14
|
+
```bash
|
|
15
|
+
npm run dev
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
4. Open [http://localhost:3000](http://localhost:3000). The homepage lists posts; each post has a page at `/blog/[slug]`.
|
|
19
|
+
|
|
20
|
+
5. **Create and publish a post** in the Dispatch dashboard, then refresh the page. If the post does not appear, ensure it is published.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
import { usePost } from "@dispatchcms/react";
|
|
5
|
+
import { TipTapContent } from "./TipTapContent";
|
|
6
|
+
|
|
7
|
+
export function PostContent({ slug }: { slug: string }) {
|
|
8
|
+
const { post, isLoading, error } = usePost(slug);
|
|
9
|
+
|
|
10
|
+
if (isLoading) {
|
|
11
|
+
return (
|
|
12
|
+
<main className="min-h-screen p-8">
|
|
13
|
+
<p className="text-muted-foreground">Loading…</p>
|
|
14
|
+
</main>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (error || !post) {
|
|
19
|
+
return (
|
|
20
|
+
<main className="min-h-screen p-8">
|
|
21
|
+
<p className="text-destructive">{error ?? "Post not found"}</p>
|
|
22
|
+
<Link href="/" className="mt-4 inline-block text-primary underline">
|
|
23
|
+
Back to blog
|
|
24
|
+
</Link>
|
|
25
|
+
</main>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<main className="min-h-screen p-8">
|
|
31
|
+
<article className="mx-auto max-w-2xl">
|
|
32
|
+
<Link href="/" className="text-sm text-muted-foreground hover:underline">
|
|
33
|
+
← Back to blog
|
|
34
|
+
</Link>
|
|
35
|
+
<h1 className="mt-4 text-3xl font-bold">{post.title}</h1>
|
|
36
|
+
{post.excerpt && (
|
|
37
|
+
<p className="mt-2 text-muted-foreground">{post.excerpt}</p>
|
|
38
|
+
)}
|
|
39
|
+
<div className="mt-6 prose prose-neutral dark:prose-invert">
|
|
40
|
+
<TipTapContent key={post.id} content={post.content} />
|
|
41
|
+
</div>
|
|
42
|
+
</article>
|
|
43
|
+
</main>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEditor, EditorContent } from "@tiptap/react";
|
|
4
|
+
import StarterKit from "@tiptap/starter-kit";
|
|
5
|
+
|
|
6
|
+
const defaultDoc = { type: "doc", content: [{ type: "paragraph" }] } as const;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Renders TipTap (ProseMirror) JSON from Dispatch CMS using a read-only editor.
|
|
10
|
+
*/
|
|
11
|
+
export function TipTapContent({ content }: { content: unknown }) {
|
|
12
|
+
const initialContent =
|
|
13
|
+
content != null && typeof content === "object" ? content : defaultDoc;
|
|
14
|
+
|
|
15
|
+
const editor = useEditor({
|
|
16
|
+
extensions: [StarterKit],
|
|
17
|
+
content: initialContent as Record<string, unknown>,
|
|
18
|
+
editable: false,
|
|
19
|
+
editorProps: {
|
|
20
|
+
attributes: {
|
|
21
|
+
class: "prose prose-neutral dark:prose-invert max-w-none focus:outline-none",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
if (!editor) return null;
|
|
27
|
+
return <EditorContent editor={editor} />;
|
|
28
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import { DispatchProvider } from "@dispatchcms/react";
|
|
3
|
+
import "./globals.css";
|
|
4
|
+
|
|
5
|
+
export const metadata: Metadata = {
|
|
6
|
+
title: "Next.js Blog with Dispatch",
|
|
7
|
+
description: "Minimal blog powered by Dispatch CMS",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default function RootLayout({
|
|
11
|
+
children,
|
|
12
|
+
}: {
|
|
13
|
+
children: React.ReactNode;
|
|
14
|
+
}) {
|
|
15
|
+
const siteKey = process.env.NEXT_PUBLIC_DISPATCH_SITE_KEY ?? "";
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<html lang="en">
|
|
19
|
+
<body>
|
|
20
|
+
<DispatchProvider siteKey={siteKey}>{children}</DispatchProvider>
|
|
21
|
+
</body>
|
|
22
|
+
</html>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
import { usePosts } from "@dispatchcms/react";
|
|
5
|
+
|
|
6
|
+
export default function HomePage() {
|
|
7
|
+
const { posts, isLoading, error } = usePosts();
|
|
8
|
+
|
|
9
|
+
if (isLoading) {
|
|
10
|
+
return (
|
|
11
|
+
<main className="min-h-screen p-8">
|
|
12
|
+
<p className="text-muted-foreground">Loading posts…</p>
|
|
13
|
+
</main>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (error) {
|
|
18
|
+
return (
|
|
19
|
+
<main className="min-h-screen p-8">
|
|
20
|
+
<p className="text-destructive">{error}</p>
|
|
21
|
+
</main>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<main className="min-h-screen p-8">
|
|
27
|
+
<div className="mx-auto max-w-2xl">
|
|
28
|
+
<h1 className="text-2xl font-bold">Blog</h1>
|
|
29
|
+
<ul className="mt-6 space-y-4">
|
|
30
|
+
{posts.length === 0 ? (
|
|
31
|
+
<li className="text-muted-foreground">No posts yet.</li>
|
|
32
|
+
) : (
|
|
33
|
+
posts.map((post) => (
|
|
34
|
+
<li key={post.id}>
|
|
35
|
+
<Link
|
|
36
|
+
href={`/blog/${post.slug}`}
|
|
37
|
+
className="text-primary underline hover:no-underline"
|
|
38
|
+
>
|
|
39
|
+
{post.title}
|
|
40
|
+
</Link>
|
|
41
|
+
{post.excerpt && (
|
|
42
|
+
<p className="mt-1 text-sm text-muted-foreground line-clamp-2">
|
|
43
|
+
{post.excerpt}
|
|
44
|
+
</p>
|
|
45
|
+
)}
|
|
46
|
+
</li>
|
|
47
|
+
))
|
|
48
|
+
)}
|
|
49
|
+
</ul>
|
|
50
|
+
</div>
|
|
51
|
+
</main>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "my-dispatch-app",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "next dev",
|
|
7
|
+
"build": "next build",
|
|
8
|
+
"start": "next start"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@dispatchcms/react": "^0.0.5",
|
|
12
|
+
"@tiptap/react": "^2.10.3",
|
|
13
|
+
"@tiptap/starter-kit": "^2.10.3",
|
|
14
|
+
"next": "^16.0.0",
|
|
15
|
+
"react": "^19.0.0",
|
|
16
|
+
"react-dom": "^19.0.0"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/node": "^22.10.1",
|
|
20
|
+
"@types/react": "^19.0.1",
|
|
21
|
+
"@types/react-dom": "^19.0.1",
|
|
22
|
+
"autoprefixer": "^10.4.20",
|
|
23
|
+
"postcss": "^8.4.49",
|
|
24
|
+
"tailwindcss": "^3.4.15",
|
|
25
|
+
"typescript": "^5.7.2"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2017",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
5
|
+
"allowJs": true,
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"module": "esnext",
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"jsx": "preserve",
|
|
15
|
+
"incremental": true,
|
|
16
|
+
"plugins": [{ "name": "next" }],
|
|
17
|
+
"paths": { "@/*": ["./*"] }
|
|
18
|
+
},
|
|
19
|
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
20
|
+
"exclude": ["node_modules"]
|
|
21
|
+
}
|