Skip to content

Build your first site

This is one build, start to finish. No detours into concepts — you will have something running first, and then it will be worth explaining why it works.

At the end you will have a CMS with an admin UI, a schema you wrote in TypeScript, and an Astro page rendering content from it with types that came from your schema rather than from you.

You need Node 18 or later, and the Trokky CLI:

Terminal window
brew install trokky/tap/trokky

Trokky’s packages are published to GitHub Packages, so npm needs to know where to find the @trokky scope. Create ~/.npmrc (or add to an existing one):

@trokky:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}

Then export a GitHub token with read:packages:

Terminal window
export NODE_AUTH_TOKEN=$(gh auth token)
Terminal window
trokky create my-site --template blog --data filesystem
cd my-site
npm install

--data filesystem stores your content as JSON files on disk. That is the right default while you are learning: you can open the files, read them, and commit them to git. Switching to Postgres later is a configuration change, not a rewrite.

Two files matter. First, the schema — this is your content model, and it is ordinary TypeScript:

schemas/post.ts
export const postSchema = {
name: 'post',
title: 'Post',
type: 'document',
fields: {
title: { type: 'string', title: 'Title', required: true },
slug: { type: 'slug', title: 'Slug', source: 'title' },
excerpt: { type: 'text', title: 'Excerpt' },
body: { type: 'richtext', title: 'Body' },
coverImage: { type: 'media', title: 'Cover image' },
publishedAt: { type: 'date', title: 'Published at', options: { includeTime: true } },
},
}

Second, the server — this is the whole backend:

server.ts
import express from 'express'
import { TrokkyExpress } from '@trokky/trokky/express'
import '@trokky/trokky/adapters/filesystem-data'
import '@trokky/trokky/adapters/filesystem-media'
import config from './trokky.config'
const app = express()
const trokky = await TrokkyExpress.create(config)
trokky.mount(app)
app.listen(3000, () => console.log('http://localhost:3000/studio'))

The two bare imports are not decoration. Adapters register themselves as a side effect of being imported, so importing one is how you choose it. Import neither and Trokky has nowhere to put anything.

Terminal window
npm run dev

Open http://localhost:3000/studio and sign in with the credentials the scaffold printed.

Create a post. Give it a title, write something in the body, upload a cover image. Save it, then hit Publish.

Now look on disk:

Terminal window
cat data/content/post/*.json

Your content is right there, as JSON, in your repository. No export step, no vendor to ask. This is what “you own your content” means in practice, and it is why the filesystem adapter is the default.

Here is the part worth staying for.

Terminal window
trokky login http://localhost:3000/api
trokky generate-types --output site/src/types/trokky

That reads the schemas the running server actually has and writes TypeScript interfaces for them:

export interface PostDocument extends BaseDocument {
_type: 'post'
title: string
slug?: string
excerpt?: string
body?: RichTextValue
coverImage?: MediaFieldValue | null
publishedAt?: string
}

You did not write that file, and you should not edit it. It is derived from the schema, which means the schema is the single definition of your content model — for storage, for the admin UI, for validation, and now for your frontend’s compiler.

site/src/lib/trokky.ts
import { TrokkyClient } from '@trokky/client'
export const client = new TrokkyClient({
baseUrl: process.env.TROKKY_API_URL ?? 'http://localhost:3000/api',
apiToken: process.env.TROKKY_API_TOKEN ?? '',
})
site/src/pages/blog/[slug].astro
---
import { client } from '../../lib/trokky'
import type { PostDocument } from '../../types/trokky'
const { slug } = Astro.params
const post = await client
.from<PostDocument>('post')
.eq('slug', slug)
.first()
if (!post) return Astro.redirect('/404')
---
<article>
<h1>{post.title}</h1>
<p>{post.excerpt}</p>
<div set:html={post.body} />
</article>

post.title is typed. Rename title to heading in your schema, re-run generate-types, and this page stops compiling — before it stops working in front of anyone.

That loop is the point of Trokky.

You have the shape of it. What to read depends on what you are building:

  • Publishing more than one thing? References connects documents to each other.
  • One-of-a-kind pages like a homepage or a settings record? Singletons — and read it before you deploy, not after.
  • Ready to put this somewhere? Deployment shapes.
  • Before any of that, ten minutes on Traps will save you an afternoon. It is the list of things that are true about Trokky but not obvious, written down honestly.