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 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,3 @@
1
+ # Get your site key from Dispatch (Sites → your site)
2
+ # https://dispatch-cms.vercel.app
3
+ NEXT_PUBLIC_DISPATCH_SITE_KEY=
@@ -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,10 @@
1
+ import { PostContent } from "./PostContent";
2
+
3
+ export default async function PostPage({
4
+ params,
5
+ }: {
6
+ params: Promise<{ slug: string }>;
7
+ }) {
8
+ const { slug } = await params;
9
+ return <PostContent slug={slug} />;
10
+ }
@@ -0,0 +1,3 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
@@ -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,2 @@
1
+ /// <reference types="next" />
2
+ /// <reference types="next/image-types/global" />
@@ -0,0 +1,5 @@
1
+ import type { NextConfig } from "next";
2
+
3
+ const nextConfig: NextConfig = {};
4
+
5
+ export default nextConfig;
@@ -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,8 @@
1
+ /** @type {import('postcss-load-config').Config} */
2
+ const config = {
3
+ plugins: {
4
+ tailwindcss: {},
5
+ },
6
+ };
7
+
8
+ export default config;
@@ -0,0 +1,12 @@
1
+ import type { Config } from "tailwindcss";
2
+
3
+ const config: Config = {
4
+ content: [
5
+ "./app/**/*.{js,ts,jsx,tsx,mdx}",
6
+ "./components/**/*.{js,ts,jsx,tsx,mdx}",
7
+ ],
8
+ theme: { extend: {} },
9
+ plugins: [],
10
+ };
11
+
12
+ export default config;
@@ -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
+ }