Dyrecteddyrected
Integrations

Next.js Integration

Complete guide to using Dyrected in a Next.js App Router project.

Dyrected integrates natively with the Next.js App Router via a catch-all API route. You can fetch data server-side, client-side, or use the SDK in Server Components.

Easiest path: paste the Dyrected setup prompt into an AI coding tool with access to your repo — it runs the CLI, models your content as collections and globals, and mounts the admin for you.

Setting up manually? The CLI is still the safest way to install — it detects your App Router and optional src/ layout, installs dependencies, writes all required files, and generates instrumentation.ts to log URLs on startup.

npx dyrected init

The steps below cover manual setup and all available options.


Installation

pnpm add @dyrected/core @dyrected/sdk @dyrected/next @dyrected/react @dyrected/db-postgres

@dyrected/next provides the API route handler, getDyrectedClient, and Next.js-optimised <DyrectedImage /> / <DyrectedMedia /> components. @dyrected/react is the React integration layer — it provides <DyrectedAdmin /> , DyrectedProvider, useDyrected, and useLivePreview. @dyrected/next re-exports all of these so you can import from a single package.


Step 0 — Wrap your Next.js config

Wrap your next.config.ts with withDyrected to ensure a single React instance is shared between your app and the Dyrected Admin UI bundle. Without this you may see an "Invalid hook call" error when rendering <DyrectedAdmin />.

// next.config.ts
import type { NextConfig } from "next";
import { withDyrected } from "@dyrected/next/config";

const nextConfig: NextConfig = {
  // your config here
};

export default withDyrected(nextConfig);

This applies the correct react and react-dom resolve aliases for both Webpack and Turbopack.


Step 1 — Create your config

// dyrected.config.ts (project root)
import { defineConfig } from "@dyrected/core";
import { PostgresAdapter } from "@dyrected/db-postgres";

export default defineConfig({
  db: new PostgresAdapter({ url: process.env.DATABASE_URL! }),
  collections: [
    {
      slug: "posts",
      admin: { useAsTitle: "title" },
      fields: [
        { name: "title", label: "Title", type: "text", required: true },
        { name: "slug", label: "Slug", type: "text", required: true, unique: true },
        { name: "body", label: "Body", type: "richText" },
        {
          name: "status",
          label: "Status",
          type: "select",
          options: ["draft", "published"],
          defaultValue: "draft",
        },
      ],
    },
  ],
});

Step 2 — Mount the API route

App Router

// app/api/[...route]/route.ts  (or src/app/api/[...route]/route.ts)
import { dyrectedNextHandler } from "@dyrected/next";
import config from "../../../dyrected.config";

export const { GET, POST, PUT, PATCH, DELETE, OPTIONS } = dyrectedNextHandler(config, { basePath: "" });

All Dyrected REST endpoints are now available at /api/....

If your catch-all route uses another prefix, pass it explicitly—for example, dyrectedNextHandler(config, { basePath: '/cms' }).


Step 3 — Fetch data in Server Components

// app/blog/page.tsx
import { createClient } from '@dyrected/sdk'

const client = createClient({
baseUrl: process.env.NEXT_PUBLIC_DYRECTED_URL!,
apiKey: process.env.DYRECTED_API_KEY!,
})

export default async function BlogPage() {
  const { docs: posts } = await client.collection('posts').find({
    where: { status: { equals: 'published' } },
    sort: '-createdAt',
    depth: 1,
  })

return (

<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}

For environments where the SDK isn't available, use raw HTTP:

// app/blog/page.tsx
export default async function BlogPage() {
  const query = new URLSearchParams({
    where: JSON.stringify({ status: { equals: 'published' } }),
  })

  const res = await fetch(
    `${process.env.NEXT_PUBLIC_DYRECTED_URL}/api/collections/posts?${query}`,
    {
      headers: { 'x-api-key': process.env.DYRECTED_API_KEY! },
      next: { revalidate: 3600 }
    }
  )
  const { docs: posts } = await res.json()

  return (
    <ul>
      {posts.map((post: any) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

Step 4 — Embed the Admin UI

The @dyrected/next package provides a <DyrectedAdmin /> component that handles Next.js router integration, CSS imports, and client-only mounting.

If you used npx dyrected init, this file is already written in the correct location — the CLI detects your optional src/ layout and writes an App Router page.

// app/admin/page.tsx  (or src/app/admin/page.tsx)
import { DyrectedAdmin } from "@dyrected/next/admin";

export default function AdminPage() {
  return <DyrectedAdmin />;
}

@dyrected/next requires the Next.js App Router. Pages Router projects need an app/ directory before running npx dyrected init.

DyrectedAdmin automatically reads your environment variables (like NEXT_PUBLIC_DYRECTED_URL) if they follow the standard naming convention. You only need to pass props if you want to override the defaults.


Dev server URL logging

When you run npx dyrected init, it generates an instrumentation.ts file at your project root (or src/instrumentation.ts for src/ layouts). This uses Next.js's built-in instrumentation hook to log the admin and API URLs whenever the server starts:

  ➜  Dyrected admin:  http://localhost:3000/admin
  ➜  Dyrected API:    http://localhost:3000/api

The base URL is read from NEXT_PUBLIC_APP_URL, falling back to http://localhost:3000. This works in both development and production.


Environment Variables

# .env.local
DATABASE_URL=postgresql://user:pass@localhost:5432/mydb

# Dyrected site URL (SDK methods append /api themselves)
NEXT_PUBLIC_DYRECTED_URL=http://localhost:3000

# Server-side API key (never expose to browser)
DYRECTED_API_KEY=sk_live_...

# Client-side keys (safe to expose — access is controlled by Dyrected access functions)
NEXT_PUBLIC_DYRECTED_API_KEY=pk_live_...
NEXT_PUBLIC_SITE_ID=site_...

ISR Cache Revalidation

When content is published, you often want to revalidate cached Next.js pages. Use an afterChange hook to call revalidatePath or revalidateTag:

// dyrected.config.ts
import { revalidatePath } from 'next/cache'

{
  slug: 'posts',
  hooks: {
    afterChange: [
      async ({ doc, operation }) => {
        if (doc.status === 'published') {
          revalidatePath('/blog')
          revalidatePath(`/blog/${doc.slug}`)
        }
      }
    ]
  }
}

Or use webhook-style revalidation via a dedicated endpoint:

// app/api/revalidate/route.ts
import { revalidatePath } from "next/cache";
import { NextRequest } from "next/server";

export async function POST(req: NextRequest) {
  const secret = req.headers.get("x-revalidate-secret");
  if (secret !== process.env.REVALIDATE_SECRET) {
    return new Response("Unauthorized", { status: 401 });
  }
  const { path } = await req.json();
  revalidatePath(path);
  return Response.json({ revalidated: true });
}

Live Preview

Add live preview support to any page with the useLivePreview hook:

// app/blog/[slug]/preview/page.tsx
"use client";
import { useLivePreview } from "@dyrected/react";

export default function BlogPreview({ initialPost }: { initialPost: Post }) {
  const { data: post, isLive } = useLivePreview({
    initialData: initialPost,
    serverURL: process.env.NEXT_PUBLIC_DYRECTED_ADMIN_URL!,
  });

  return (
    <article>
      {isLive && <div className="preview-badge">Preview Mode</div>}
      <h1>{post.title}</h1>
    </article>
  );
}

Enable it on the collection:

{
  slug: 'posts',
  admin: {
    previewUrl: (doc) =>
      doc?.slug ? `${process.env.NEXT_PUBLIC_SITE_URL}/blog/${doc.slug}` : null,
  }
}

Click-to-edit

Annotate elements with useDyPath so clicking them in the preview jumps to the field. Render block arrays with Blocks — it scopes each block's base path automatically. All three are re-exported from @dyrected/next:

"use client";
import { useLivePreview, Blocks, useDyPath } from "@dyrected/next";
import Hero from "@/components/blocks/hero";

export default function PagePreview({ initialData }: { initialData: any }) {
  const { data: page } = useLivePreview({ initialData });
  if (!page) return null;
  return <Blocks items={page.layout} components={{ hero: Hero }} path="layout" />;
}

// components/blocks/hero.tsx — useDyPath('heading') → data-dy-path="layout.<i>.heading"
export default function Hero({ heading }: { heading: string }) {
  return <h1 {...useDyPath("heading")}>{heading}</h1>;
}

See Live Preview for the full guide.


Middleware & Route Protection

Protect your admin route with Next.js middleware:

// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export function middleware(request: NextRequest) {
  if (request.nextUrl.pathname.startsWith("/admin")) {
    const token = request.cookies.get("dyrected-token")?.value;
    if (!token) {
      return NextResponse.redirect(new URL("/admin/login", request.url));
    }
  }
  return NextResponse.next();
}

export const config = {
  matcher: ["/admin/:path*"],
};

On this page