Skip to content

TanStack Start Kitchen Sink

TanStack Start Kitchen Sink
Files 21
  • src 2
  • src/lib 2
  • src/routes 5
  • src/routes/api/auth 2
  • src/routes/assets 1
  • src/routes/case-studies 1
  • src/routes/posts 2
  • src/routes/projects 1
content.config.ts
typescript
import { defineCMS, defineCollection, defineGlobal } from "@timbl/core";

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 TanStack Start app that exercises the full @timbl/client surface against a timbl CMS instance. Located at examples/tanstack-start-kitchen-sink/ in the repo.

What it demonstrates

  • Typed reads via server functions + route loaders: collection().findMany() with status / sort / limit/offset / q / select / include + depth.
  • Detail by slug: findBySlug() with relation expansion, rendered through a discriminated-union view (ok | notfound | error).
  • Relations: caseStudies -> project (single) and posts/projects -> tags (many), narrowed through honest type guards at the server-fn boundary.
  • Globals: siteSettings in the root route.
  • Markdown: renders the CMS-provided bodyHtml companion (already sanitized).
  • Assets: assets.findMany() gallery.
  • Auth (local dev): auth.session() in the nav; /login + /logout proxy Better Auth via server routes and re-home Set-Cookie; /admin does create / delete, assets.upload(), exportContent() via server functions (cookie forwarded with getRequestHeader).
  • Errors: TimblHttpError -> discriminated notfound / error views with code + requestId (exhaustive switch).
  • System: /health calls health() / ready() / openApi().
  • Server fns return serializable DTOs (the server fn is the serialization boundary — CMS entry types with unknown never reach the client).

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/tanstack-start-kitchen-sink)
bun install
cp .env.example .env
bun run dev # app on http://localhost:4321

Auth writes (/admin) need a seeded user 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.

bun run build produces client + server bundles without calling the CMS (loaders run on demand), so it builds without a running instance.

Schema

content.config.ts mirrors the collections this app uses and is consumed type-only via InferCMS<typeof cms>. src/lib/cms.ts is client-safe (types + pure guards only); src/lib/server.ts holds the @timbl/client calls and maps CMS entries to serializable DTOs at the boundary.

See also