Therracode CMS

A CMS that comes with the site.

Each site describes its content in code, and its editors get an admin to match: drafts, versions, any number of languages, media and SEO, with no separate service to run.

Type
Our own product
Stack
Next.js 16 · React 19 · Postgres 18
Built
2026
Status
Runs this site
On the left the content model in cms.config.ts, on the right the admin it gives, in the CMS's Terra look.

What it needed to solve

A company site should not need a developer for every comma. Yet that is where most sites end up.

01Every change is an email to the developer.

02Small fixes wait in someone's notes.

03The CMS meant to fix that wants plugin updates every month.

04Or it bills per editor, every month.

05And the translations live in a spreadsheet next to it.

We wanted a CMS that comes with the site, speaks every language it needs and stays out of the way. So we built one.

We wanted a CMS that comes with the site, speaks every language it needs and stays out of the way. So we built one.

How we built it

The model in code, the content in the admin.

01

The model lives in code.

A site describes its content in cms.config.ts: collections, fields, blocks and languages. The site reads it with types, so a renamed field is a build error, not an empty page.

Admin · this case study in the editor
02

An admin editors understand.

Each field is what the model says it is: text with a counter, a choice, an image with its alt text, rich text, blocks to add and reorder. Nothing to set up in a dashboard.

Admin · a page and its blocks
03

Drafts first, then publish.

Every change starts as a draft. Publish takes it live on the next request, and every publish keeps a version that can come back.

Admin · the versions of a page
04

Every language side by side.

A site has as many languages as it needs, each with its own draft and its own publish. A page can be copied from another language and translated in place.

Admin · copying a page from English
05

Media in one library.

Uploads land in one library, with alt text, dimensions and the pages that use them. A file in use cannot be deleted by accident.

Admin · the media library
06

Search engines included.

Every page has its title, description, share image and noindex in an SEO panel. The sitemap and the links between languages follow by themselves.

Admin · the SEO panel of a page

How a site gets it.

The CMS is a package that goes into the site, not a service next to it. One site, one database, nothing else to run.

  1. 01

    Install the package

    It comes from our private registry and brings its admin, its API and its database migrations.

    Terminal
    $ pnpm add @therracode-dev/cms
    $ pnpm exec cms migrate
  2. 02

    Describe the content

    Collections, fields and blocks, in the code of the site. The admin and the types follow from this one file.

    cms.config.ts
    import { block, collection, defineConfig, f } from "@therracode-dev/cms";
    
    const hero = block("hero", {
      label: "Hero",
      fields: {
        heading: f.text({ label: "Heading", required: true }),
        image: f.image({ label: "Image" }),
      },
    });
    
    export default defineConfig({
      locales: { default: "en", available: ["en", "hr"] },
      collections: {
        pages: collection({
          label: "Pages",
          routing: "path",
          fields: {
            title: f.text({ label: "Title", required: true }),
            body: f.blocks([hero], { label: "Content" }),
          },
        }),
      },
    });
  3. 03

    Add the admin

    lib/cms.ts makes the CMS from the config. One route gives the site its admin at /admin, another the API behind it.

    app/(cms)/admin/[[...path]]/page.tsx
    import { cms } from "@/lib/cms";
    
    export default cms.AdminPage;
  4. 04

    Read it on a page

    Pages read published content with types inferred from the model, cached until the next publish.

    app/[locale]/[[...path]]/page.tsx
    import { notFound } from "next/navigation";
    import { cms } from "@/lib/cms";
    
    type Props = PageProps<"/[locale]/[[...path]]">;
    
    export default async function Page({ params }: Props) {
      const { locale, path = [] } = await params;
      const page = await cms.findByPath(`/${path.join("/")}`, { locale });
      if (!page) notFound();
    
      return <PageBlocks blocks={page.data.body} />;
    }

Stack

Framework
Next.js 16 · React 19
Language
TypeScript
Database
Postgres 18 · Kysely
Rich text
TipTap
Admin
React · MUI
Hosting
Docker · Caddy · Hetzner

What it does

  • Types for every read, inferred from cms.config.ts
  • Drafts and publishing per language
  • Versions with restore
  • Preview of drafts on the real site
  • Delete protection for anything still in use
  • Roles, invites and password resets
  • SEO panel, sitemap and links between languages
  • Nightly backups of the database and uploads

Live on this site.

Every page, case study, person and client logo on therracode.com comes from the CMS, edited in the admin you see above.

  • Pages, work, people and clients edited in the admin
  • A publish is live on the next request
  • English and Croatian here, and as many languages as a site needs
  • No fees per editor
  • Each client site gets its own instance and database

Want a site your team can edit?

Tell us what is on it. We'll model the content, move it in and show your editors around.

Next project · Platform

UK Cheerleading Association

A full-stack platform that runs the whole sport: clubs, competitions, courses, certifications and payments in one place.