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.
Before you start
Section titled “Before you start”You need Node 18 or later, and the Trokky CLI:
brew install trokky/tap/trokkyTrokky’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:
export NODE_AUTH_TOKEN=$(gh auth token)1. Create the project
Section titled “1. Create the project”trokky create my-site --template blog --data filesystemcd my-sitenpm 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.
2. Look at what you got
Section titled “2. Look at what you got”Two files matter. First, the schema — this is your content model, and it is ordinary TypeScript:
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:
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.
3. Run it
Section titled “3. Run it”npm run devOpen 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:
cat data/content/post/*.jsonYour 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.
4. Generate types from your schema
Section titled “4. Generate types from your schema”Here is the part worth staying for.
trokky login http://localhost:3000/apitrokky generate-types --output site/src/types/trokkyThat 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.
5. Render it
Section titled “5. Render it”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 ?? '',})---import { client } from '../../lib/trokky'import type { PostDocument } from '../../types/trokky'
const { slug } = Astro.paramsconst 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.
6. Where to go next
Section titled “6. Where to go next”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.