Skip to content

Next.js Kitchen Sink

Next.js Kitchen Sink
Files 23
  • src/app 4
  • src/app/admin 2
  • src/app/api/auth/login 1
  • src/app/api/auth/logout 1
  • src/app/assets 1
  • src/app/case-studies/[slug] 1
  • src/app/health 1
  • src/app/login 1
  • src/app/posts 1
  • src/app/posts/[slug] 1
  • src/app/projects/[slug] 1
  • src/components 1
  • src/lib 1
content.config.ts
typescript
import { defineCMS, defineCollection, defineGlobal } from "@timbl/core";

// Self-contained schema for this example. Consumed type-only (InferCMS), so
// @timbl/core's runtime is never bundled — the frontend ships just @timbl/client.
// Point TIMBL_BASE_URL at a timbl instance whose schema matches this file.
export default defineCMS({
  collections: [
    defineCollection({
      key: "projects",
      labels: { singular: "Project", plural: "Projects" },
      admin: { titleField: "title", defaultSort: "-createdAt" },
      fields: [
        { key: "title", type: "text", required: true },
        { key: "slug", type: "slug", required: true },
        { key: "date", type: "date" },
        { key: "summary", type: "textarea" },
        { key: "body", type: "markdown" },
        { key: "featuredImage", type: "upload" },
        { key: "tags", type: "relation", to: "tags", many: true },
        { key: "status", type: "select", options: ["draft", "published"], defaultValue: "draft" },
      ],
    }),
    defineCollection({
      key: "caseStudies",
      labels: { singular: "Case Study", plural: "Case Studies" },
      admin: { titleField: "title", defaultSort: "-createdAt" },
      fields: [
        { key: "title", type: "text", required: true },
        { key: "slug", type: "slug", required: true },
        { key: "summary", type: "textarea" },
        { key: "body", type: "markdown" },
        { key: "featuredImage", type: "upload" },
        { key: "project", type: "relation", to: "projects" },
        { key: "status", type: "select", options: ["draft", "published"], defaultValue: "draft" },
      ],
    }),
    defineCollection({
      key: "posts",
      labels: { singular: "Post", plural: "Posts" },
      admin: { titleField: "title", defaultSort: "-createdAt" },
      fields: [
        { key: "title", type: "text", required: true },
        { key: "slug", type: "slug", required: true },
        { key: "body", type: "markdown" },
        { key: "tags", type: "relation", to: "tags", many: true },
        { key: "status", type: "select", options: ["draft", "published"], defaultValue: "draft" },
      ],
    }),
    defineCollection({
      key: "tags",
      labels: { singular: "Tag", plural: "Tags" },
      admin: { titleField: "name", defaultSort: "name" },
      fields: [
        { key: "name", type: "text", required: true },
        { key: "slug", type: "slug", required: true },
      ],
    }),
  ],
  globals: [
    defineGlobal({
      key: "siteSettings",
      label: "Site Settings",
      fields: [
        { key: "siteName", type: "text" },
        { key: "siteDescription", type: "textarea" },
        { key: "siteUrl", type: "text" },
        {
          key: "socialLinks",
          type: "array",
          of: {
            key: "socialLink",
            type: "group",
            fields: [
              { key: "label", type: "text", required: true },
              { key: "url", type: "text", required: true },
            ],
          },
        },
      ],
    }),
  ],
  plugins: [],
});

A self-contained Next.js (App Router) app that exercises the full @timbl/client surface against a timbl CMS instance. Located at examples/nextjs-kitchen-sink/ in the repo.

What it demonstrates

  • Typed reads in Server Components: collection().findMany() with status / sort / limit / offset / q search / select projection.
  • Detail by slug: findBySlug() with include + depth relation expansion.
  • Relations: caseStudies -> project (single) and posts/projects -> tags (many), rendered from included records.
  • Globals: siteSettings (site name, description, socialLinks array) in the root layout.
  • Markdown: renders the CMS-provided bodyHtml companion (already sanitized) via dangerouslySetInnerHTML. No client-side markdown pipeline.
  • Assets: assets.findMany() gallery; featured images resolved via assets.findById().
  • Auth (local dev): auth.session() in the nav; /login + /logout proxy Better Auth and forward Set-Cookie; /admin does authenticated create / update / delete, assets.upload(), and exportContent() via Server Actions. Cookies are forwarded with next/headers cookies().
  • Errors: TimblHttpError produces notFound() (404) or an inline structured error page with code + requestId.
  • System: /health calls health() / ready() / openApi().

Run

Terminal window
# 1. Start your timbl CMS (repo root)
bun install
bun run dev # CMS on http://localhost:3000
# 2. Run this example (from examples/nextjs-kitchen-sink)
bun install
cp .env.example .env # set TIMBL_BASE_URL if not localhost:3000
bun run dev # app on http://localhost:4321

Auth writes (/admin) need a seeded user in the CMS and same-site local dev. For production cross-origin, deploy behind the same domain or use bearer tokens. Cookie auth does not work cross-origin.

Every CMS-reading route sets export const dynamic = "force-dynamic", so next build does not call the CMS at build time and builds without a running instance.

Schema

content.config.ts mirrors the collections this app uses and is consumed type-only via InferCMS<typeof cms>. The @timbl/core runtime is never bundled. The frontend ships just @timbl/client (zero runtime deps). If your CMS schema differs, edit content.config.ts to match it.

See also