Next.js Kitchen Sink
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()withstatus/sort/limit/offset/qsearch /selectprojection. - Detail by slug:
findBySlug()withinclude+depthrelation expansion. - Relations:
caseStudies -> project(single) andposts/projects -> tags(many), rendered from included records. - Globals:
siteSettings(site name, description,socialLinksarray) in the root layout. - Markdown: renders the CMS-provided
bodyHtmlcompanion (already sanitized) viadangerouslySetInnerHTML. No client-side markdown pipeline. - Assets:
assets.findMany()gallery; featured images resolved viaassets.findById(). - Auth (local dev):
auth.session()in the nav;/login+/logoutproxy Better Auth and forwardSet-Cookie;/admindoes authenticatedcreate/update/delete,assets.upload(), andexportContent()via Server Actions. Cookies are forwarded withnext/headerscookies(). - Errors:
TimblHttpErrorproducesnotFound()(404) or an inline structured error page withcode+requestId. - System:
/healthcallshealth()/ready()/openApi().
Run
# 1. Start your timbl CMS (repo root)bun installbun run dev # CMS on http://localhost:3000
# 2. Run this example (from examples/nextjs-kitchen-sink)bun installcp .env.example .env # set TIMBL_BASE_URL if not localhost:3000bun run dev # app on http://localhost:4321Auth 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
- Quick Start: install and run timbl itself
- Client SDK: the method surface this example exercises
- Astro Kitchen Sink: the same surface in Astro SSR