Episode 3 — Writing your first piece
Engine installed, workspace green. Time to actually make something. In this
episode we write a real blog post — and once you've seen the anatomy of one
content item, every other type works the same way.
What a content item actually is
A content item is a directory, not a file. That trips people up at first,
so let's be concrete. A blog post named hello-world lives like this:
content/resources/blog/hello-world/ parts/ body/ meta.toml # the part's identity en.md # frontmatter + the actual writing
Three ideas in that little tree:
- The item is the
hello-world/directory. Its name is the slug. - An item is built from one or more parts. A blog post has exactly one
part,body. Richer types have more — aprojecthasoverview,goals,
challenges, and so on. Each part is a subdirectory underparts/. - Inside a part, each language is its own file:
en.md,zh.md. The
prose lives there.
Frontmatter: the structured half
Every part body file opens with a YAML frontmatter block — the fenced ---
section. This is the structured data the engine reads into the database.
Here's a minimal, valid blog post:
---
slug: hello-world
title: Hello, world
kind: blog
status: draft
visibility: private
excerpt: My very first post on a system I actually trust.
tags:
- first-post
---
## Hello, world
This is the body. Plain markdown, write whatever you want.
Worth knowing about those fields:
slug,title,kind,status,visibilityare required on a blog
post. Leave one out and the engine raises a fatal error — it won't sync.kindmust match the type. A file underblog/must saykind: blog. A
mismatch is a fatal error on purpose — it stops misfiled content cold.statusdefaults todraft,visibilitytoprivate. So a brand-new post
is invisible by default. You opt into publishing, never out of it.- Everything else —
excerpt,tags,category,is_featured— is
optional. Add what you need, skip what you don't.
The single source of truth for every field of every type is SCHEMA.md at the
root of your content tree. When you forget what a project allows, that file
is the answer — not your memory.
meta.toml: the part's identity
Next to en.md sits meta.toml. It's small and you mostly leave it alone:
type = "body"
shape = "prose"
canonical_lang = "en"
type is the part's role, shape is prose for ordinary writing, and
canonical_lang is the source language for any language-neutral fields. There
will also be a part_id — a stable ULID the engine generates and owns.
Don't write part_id by hand; let the engine assign it. It's the
permanent identity of that part, and it stays put even if you rename the
directory.
The shortcut: let the CLI scaffold it
You can hand-build that directory tree, but you don't have to. The CLI
scaffolds a correct skeleton for you:
silan new blog hello-world
That creates the directory, the parts/body/ folder, a meta.toml with a
fresh part_id, and an en.md pre-filled with valid frontmatter. You open
en.md and start writing prose. No fighting the structure — it's already
right.
Validate as you go
Don't wait until the end to find out you broke something. After any edit:
silan lint
lint parses your tree and reports problems by severity — fatal (won't
sync: a missing required field, a bad enum value, a kind mismatch), warn
(suspicious but allowed), info (notes). Get to a clean lint and you know the
next step will work.
On linking pieces together
One feature worth meeting now: relations. In the frontmatter you can link an
item to another:
relations:
- { type: documents, to: /projects/hello-world }
Items address each other by silan:// URI. Relation types include
documents, references, evolved_into, part_of. This is what turns a
folder of files into a connected graph of thought — the thing that made the
system worth building. You don't need it for your first post; just know it's
there.
You can now write a valid content item and lint it clean. It still lives only
on your disk, though. Final episode: turning these files into a live site —
and the one deliberate gate between a draft and the public.