diff --git a/examples/blog/README.md b/examples/blog/README.md
new file mode 100644
index 00000000..e215bc4c
--- /dev/null
+++ b/examples/blog/README.md
@@ -0,0 +1,36 @@
+This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
+
+## Getting Started
+
+First, run the development server:
+
+```bash
+npm run dev
+# or
+yarn dev
+# or
+pnpm dev
+# or
+bun dev
+```
+
+Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
+
+You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
+
+This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
+
+## Learn More
+
+To learn more about Next.js, take a look at the following resources:
+
+- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
+- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
+
+You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
+
+## Deploy on Vercel
+
+The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
+
+Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
diff --git a/examples/blog/next.config.ts b/examples/blog/next.config.ts
new file mode 100644
index 00000000..73290639
--- /dev/null
+++ b/examples/blog/next.config.ts
@@ -0,0 +1,7 @@
+import type { NextConfig } from 'next'
+
+const nextConfig: NextConfig = {
+ /* config options here */
+}
+
+export default nextConfig
diff --git a/examples/blog/package.json b/examples/blog/package.json
new file mode 100644
index 00000000..a302ff33
--- /dev/null
+++ b/examples/blog/package.json
@@ -0,0 +1,28 @@
+{
+ "name": "@deepdish/blog",
+ "version": "0.0.1",
+ "private": true,
+ "scripts": {
+ "dev": "next dev --turbopack",
+ "build": "next build",
+ "start": "next start"
+ },
+ "dependencies": {
+ "@deepdish/cms": "workspace:0.8.0",
+ "@deepdish/nextjs": "workspace:0.8.0",
+ "@deepdish/resolvers": "workspace:0.13.0",
+ "@deepdish/ui": "workspace:0.15.0",
+ "next": "15.1.4",
+ "react": "catalog:react",
+ "react-dom": "catalog:react",
+ "valibot": "1.0.0-rc.0"
+ },
+ "devDependencies": {
+ "@tailwindcss/postcss": "catalog:tailwind",
+ "@types/node": "catalog:repo",
+ "@types/react": "catalog:react",
+ "@types/react-dom": "catalog:react",
+ "tailwindcss": "catalog:tailwind",
+ "typescript": "catalog:repo"
+ }
+}
diff --git a/examples/blog/postcss.config.mjs b/examples/blog/postcss.config.mjs
new file mode 100644
index 00000000..86e8e3c4
--- /dev/null
+++ b/examples/blog/postcss.config.mjs
@@ -0,0 +1,5 @@
+const config = {
+ plugins: ['@tailwindcss/postcss'],
+}
+
+export default config
diff --git a/examples/blog/src/app/blog/[slug]/page.tsx b/examples/blog/src/app/blog/[slug]/page.tsx
new file mode 100644
index 00000000..551abdc7
--- /dev/null
+++ b/examples/blog/src/app/blog/[slug]/page.tsx
@@ -0,0 +1,17 @@
+import { BlogPost } from '@/cms'
+
+export default async function BlogArticle({
+ params,
+}: {
+ params: Promise<{ slug: string }>
+}) {
+ const { slug } = await params
+
+ return (
+
+ )
+}
diff --git a/examples/blog/src/app/blog/page.tsx b/examples/blog/src/app/blog/page.tsx
new file mode 100644
index 00000000..4da16ce0
--- /dev/null
+++ b/examples/blog/src/app/blog/page.tsx
@@ -0,0 +1,17 @@
+import { BlogCard } from '@/cms'
+
+export default function Blog() {
+ return (
+
+
+
Johnny Pizza Blog
+
+ Check out our cool blog articles and learn more about our pizza!
+
+
+
+
+
+
+ )
+}
diff --git a/examples/blog/src/app/favicon.ico b/examples/blog/src/app/favicon.ico
new file mode 100644
index 00000000..718d6fea
Binary files /dev/null and b/examples/blog/src/app/favicon.ico differ
diff --git a/examples/blog/src/app/globals.css b/examples/blog/src/app/globals.css
new file mode 100644
index 00000000..2a89a481
--- /dev/null
+++ b/examples/blog/src/app/globals.css
@@ -0,0 +1,19 @@
+@import "tailwindcss";
+
+:root {
+ --background: #ffffff;
+ --foreground: #171717;
+}
+
+@theme inline {
+ --color-background: var(--background);
+ --color-foreground: var(--foreground);
+ --font-sans: var(--font-geist-sans);
+ --font-mono: var(--font-geist-mono);
+}
+
+body {
+ background: var(--background);
+ color: var(--foreground);
+ font-family: Arial, Helvetica, sans-serif;
+}
diff --git a/examples/blog/src/app/layout.tsx b/examples/blog/src/app/layout.tsx
new file mode 100644
index 00000000..b86befea
--- /dev/null
+++ b/examples/blog/src/app/layout.tsx
@@ -0,0 +1,40 @@
+import type { Metadata } from 'next'
+import { Geist, Geist_Mono } from 'next/font/google'
+import './globals.css'
+import { Navbar } from '@/components/navbar'
+
+import { DeepDishProvider } from '@/cms'
+
+const geistSans = Geist({
+ variable: '--font-geist-sans',
+ subsets: ['latin'],
+})
+
+const geistMono = Geist_Mono({
+ variable: '--font-geist-mono',
+ subsets: ['latin'],
+})
+
+export const metadata: Metadata = {
+ title: 'DeepDish Blog Example',
+ description: 'Blog example showcasing the DeepDish CMS',
+}
+
+export default function RootLayout({
+ children,
+}: Readonly<{
+ children: React.ReactNode
+}>) {
+ return (
+
+
+
+
+ {children}
+
+
+
+ )
+}
diff --git a/examples/blog/src/app/page.tsx b/examples/blog/src/app/page.tsx
new file mode 100644
index 00000000..9438f8bd
--- /dev/null
+++ b/examples/blog/src/app/page.tsx
@@ -0,0 +1,25 @@
+import { Paragraph } from '@/cms'
+import Link from 'next/link'
+
+export default function Home() {
+ return (
+
+
+
+ Welcome to Johnny Pizza
+
+
+ We pride ourselves on our pizza. We use only the freshest ingredients
+ and our team of expert chefs to create the best pizza in town.
+
+
Check out our blog!
+
+
+ )
+}
diff --git a/examples/blog/src/cms.tsx b/examples/blog/src/cms.tsx
new file mode 100644
index 00000000..ed2cbcde
--- /dev/null
+++ b/examples/blog/src/cms.tsx
@@ -0,0 +1,181 @@
+import fs from 'node:fs/promises'
+import { truncate } from '@/utils'
+import { DeepDishProvider as InternalDeepDishProvider } from '@deepdish/cms'
+import { getBaseUrl } from '@deepdish/cms/vercel'
+import { createJsonResolver } from '@deepdish/resolvers/json'
+import { createComponents } from '@deepdish/ui/components'
+import { configure } from '@deepdish/ui/config'
+import type { IntrinsicElement, SetChildren } from '@deepdish/ui/deepdish'
+import Link from 'next/link'
+import * as v from 'valibot'
+
+// TODO: get local data dir to work
+export const contentPaths = {
+ blog: '/tmp/deepdish_blog_blog.json',
+ text: '/tmp/deepdish_blog_text.json',
+} as const
+
+async function initContent() {
+ for (const path of Object.values(contentPaths)) {
+ const exists = await fs
+ .stat(path)
+ .then(() => true)
+ .catch(() => false)
+
+ if (!exists) {
+ await fs.writeFile(path, JSON.stringify({}))
+ }
+ }
+}
+
+const blogSchema = v.object({
+ title: v.string(),
+ author: v.object({
+ name: v.string(),
+ email: v.string(),
+ }),
+ body: v.string(),
+ slug: v.string(),
+})
+
+const textSchema = v.string()
+
+const contracts = {
+ blog: {
+ resolver: createJsonResolver(contentPaths.blog, blogSchema, {
+ maxBatchSize: 10,
+ }),
+ schema: blogSchema,
+ },
+ text: {
+ resolver: createJsonResolver(contentPaths.text, textSchema, {
+ maxBatchSize: 10,
+ }),
+ schema: textSchema,
+ },
+}
+
+async function cms() {
+ await initContent()
+
+ await configure({
+ contracts,
+ logging: {
+ enabled: process.env.NODE_ENV === 'development',
+ },
+ settings: {
+ baseUrl: getBaseUrl({}),
+ draft: true,
+ },
+ })
+
+ return createComponents(contracts)
+}
+
+const components = await cms()
+
+const Blog = components.blog
+const Text = components.text
+
+type DeepDishWithoutContract =
+ | {
+ key: string
+ collection?: never
+ }
+ | {
+ collection: string
+ }
+
+type ElementProps = SetChildren<
+ JSX.IntrinsicElements[E],
+ C
+> & {
+ deepdish: DeepDishWithoutContract
+}
+
+type ParagraphProps = ElementProps
+
+export function Paragraph(props: ParagraphProps<'p'>) {
+ return (
+ {
+ return {content}
+ }}
+ />
+ )
+}
+
+export function BlogCard(props: {
+ deepdish: DeepDishWithoutContract
+}) {
+ return (
+ {
+ if (!content) {
+ return Blog Card Placeholder
+ }
+
+ return (
+
+
{content.title}
+
+ Written by {content.author.name}
+
+
{truncate(content.body, 100)}
+
+ Read article
+
+
+ )
+ }}
+ />
+ )
+}
+
+export function BlogPost(props: {
+ deepdish: DeepDishWithoutContract
+}) {
+ return (
+ {
+ if (!content) {
+ return Blog Article Placeholder
+ }
+
+ return (
+
+
+ {content.title}
+
+
+ Written by {content.author.name}
+
+
{content.body}
+
+ )
+ }}
+ />
+ )
+}
+
+export function DeepDishProvider(props: {
+ children: React.ReactNode
+}) {
+ return
+}
diff --git a/examples/blog/src/components/navbar.tsx b/examples/blog/src/components/navbar.tsx
new file mode 100644
index 00000000..907e958d
--- /dev/null
+++ b/examples/blog/src/components/navbar.tsx
@@ -0,0 +1,17 @@
+import Link from 'next/link'
+
+export function Navbar() {
+ return (
+
+
+
+ Johnny Pizza
+
+
+ Home
+ Blog
+
+
+
+ )
+}
diff --git a/examples/blog/src/middleware.ts b/examples/blog/src/middleware.ts
new file mode 100644
index 00000000..148a1317
--- /dev/null
+++ b/examples/blog/src/middleware.ts
@@ -0,0 +1,36 @@
+import { getBaseUrl } from '@deepdish/cms/vercel'
+import { deepdishMiddleware } from '@deepdish/nextjs'
+import { type NextRequest, NextResponse } from 'next/server'
+
+let signedIn = false
+
+async function signIn() {
+ await new Promise((resolve) => setTimeout(resolve, 1000))
+ signedIn = true
+
+ return NextResponse.redirect(getBaseUrl({}))
+}
+
+async function signOut() {
+ await new Promise((resolve) => setTimeout(resolve, 1000))
+ signedIn = false
+
+ return NextResponse.redirect(getBaseUrl({}))
+}
+
+async function verify() {
+ await new Promise((resolve) => setTimeout(resolve, 1000))
+ return signedIn
+}
+
+export default (request: NextRequest) => {
+ return deepdishMiddleware(
+ {
+ draft: true,
+ verify,
+ signIn,
+ signOut,
+ },
+ request,
+ )
+}
diff --git a/examples/blog/src/utils.ts b/examples/blog/src/utils.ts
new file mode 100644
index 00000000..ea474def
--- /dev/null
+++ b/examples/blog/src/utils.ts
@@ -0,0 +1,7 @@
+export function truncate(str: string, maxLength: number) {
+ if (str.length <= maxLength) {
+ return str
+ }
+
+ return `${str.slice(0, maxLength)}...`
+}
diff --git a/examples/blog/tsconfig.json b/examples/blog/tsconfig.json
new file mode 100644
index 00000000..c1334095
--- /dev/null
+++ b/examples/blog/tsconfig.json
@@ -0,0 +1,27 @@
+{
+ "compilerOptions": {
+ "target": "ES2017",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ },
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
+ "exclude": ["node_modules"]
+}
diff --git a/package.json b/package.json
index cef81f26..1fa39966 100644
--- a/package.json
+++ b/package.json
@@ -3,6 +3,7 @@
"private": true,
"workspaces": [
"apps/*",
+ "examples/*",
"packages/*"
],
"scripts": {
diff --git a/packages/ui/src/deepdish.tsx b/packages/ui/src/deepdish.tsx
index 6b39c0b9..c082ad6c 100644
--- a/packages/ui/src/deepdish.tsx
+++ b/packages/ui/src/deepdish.tsx
@@ -1,3 +1,4 @@
+///
import 'server-only'
import { withResult } from '@byteslice/result'
@@ -10,6 +11,9 @@ import type {
DeepDishCollectionProps,
DeepDishElementProps,
DeepDishProps,
+ ElementProps,
+ IntrinsicElement,
+ SetChildren,
} from './types'
const logger = getLogger(['deepdish', 'ui'])
@@ -250,4 +254,4 @@ export async function DeepDish(props: {
)
}
-export type { DeepDishProps }
+export type { DeepDishProps, ElementProps, IntrinsicElement, SetChildren }
diff --git a/packages/ui/src/types.ts b/packages/ui/src/types.ts
index 0c217760..d096c2d8 100644
--- a/packages/ui/src/types.ts
+++ b/packages/ui/src/types.ts
@@ -28,4 +28,4 @@ export type ElementProps<
export type IntrinsicElement = keyof JSX.IntrinsicElements
-type SetChildren = Omit & { children?: C }
+export type SetChildren = Omit & { children?: C }
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 64514145..ff619b41 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -17,6 +17,12 @@ catalogs:
specifier: ^19.0.0 || ^19.0.0-0
version: 19.0.0
react:
+ '@types/react':
+ specifier: 19.0.0
+ version: 19.0.0
+ '@types/react-dom':
+ specifier: 19.0.0
+ version: 19.0.0
react:
specifier: 19.0.0
version: 19.0.0
@@ -248,6 +254,52 @@ importers:
specifier: 5.5.4
version: 5.5.4
+ examples/blog:
+ dependencies:
+ '@deepdish/cms':
+ specifier: workspace:0.8.0
+ version: link:../../packages/cms
+ '@deepdish/nextjs':
+ specifier: workspace:0.8.0
+ version: link:../../packages/nextjs
+ '@deepdish/resolvers':
+ specifier: workspace:0.13.0
+ version: link:../../packages/resolvers
+ '@deepdish/ui':
+ specifier: workspace:0.15.0
+ version: link:../../packages/ui
+ next:
+ specifier: 15.1.4
+ version: 15.1.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+ react:
+ specifier: catalog:react
+ version: 19.0.0
+ react-dom:
+ specifier: catalog:react
+ version: 19.0.0(react@19.0.0)
+ valibot:
+ specifier: 1.0.0-rc.0
+ version: 1.0.0-rc.0(typescript@5.7.2)
+ devDependencies:
+ '@tailwindcss/postcss':
+ specifier: catalog:tailwind
+ version: 4.0.3
+ '@types/node':
+ specifier: catalog:repo
+ version: 20.9.0
+ '@types/react':
+ specifier: catalog:react
+ version: 19.0.0
+ '@types/react-dom':
+ specifier: catalog:react
+ version: 19.0.0
+ tailwindcss:
+ specifier: catalog:tailwind
+ version: 4.0.3
+ typescript:
+ specifier: catalog:repo
+ version: 5.7.2
+
packages/cms:
dependencies:
'@deepdish-cloud/resolvers':
@@ -3302,6 +3354,9 @@ packages:
'@types/react-dom@18.3.0':
resolution: {integrity: sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==}
+ '@types/react-dom@19.0.0':
+ resolution: {integrity: sha512-1KfiQKsH1o00p9m5ag12axHQSb3FOU9H20UTrujVSkNhuCrRHiQWFqgEnTNK5ZNfnzZv8UWrnXVqCmCF9fgY3w==}
+
'@types/react-dom@19.0.4':
resolution: {integrity: sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg==}
peerDependencies:
@@ -3313,6 +3368,9 @@ packages:
'@types/react@18.3.3':
resolution: {integrity: sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==}
+ '@types/react@19.0.0':
+ resolution: {integrity: sha512-MY3oPudxvMYyesqs/kW1Bh8y9VqSmf+tzqw3ae8a9DZW68pUe3zAdHeI1jc6iAysuRdACnVknHP8AhwD4/dxtg==}
+
'@types/statuses@2.0.5':
resolution: {integrity: sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A==}
@@ -3445,7 +3503,6 @@ packages:
bun@1.2.4:
resolution: {integrity: sha512-ZY0EZ/UKqheaLeAtMsfJA6jWoWvV9HAtfFaOJSmS3LrNpFKs1Sg5fZLSsczN1h3a+Dtheo4O3p3ZYWrf40kRGw==}
- cpu: [arm64, x64, aarch64]
os: [darwin, linux, win32]
hasBin: true
@@ -8087,6 +8144,10 @@ snapshots:
dependencies:
'@types/react': 18.3.3
+ '@types/react-dom@19.0.0':
+ dependencies:
+ '@types/react': 18.3.3
+
'@types/react-dom@19.0.4(@types/react@18.3.3)':
dependencies:
'@types/react': 18.3.3
@@ -8101,6 +8162,10 @@ snapshots:
'@types/prop-types': 15.7.14
csstype: 3.1.3
+ '@types/react@19.0.0':
+ dependencies:
+ csstype: 3.1.3
+
'@types/statuses@2.0.5': {}
'@types/tough-cookie@4.0.5': {}
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 9979b851..5ff84a27 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -1,6 +1,7 @@
packages:
- - "packages/*"
- "apps/*"
+ - "examples/*"
+ - "packages/*"
catalogs:
react: