# Porting a site to Twyla This is a field guide for an agent porting an existing static site (today: [Zola](https://www.getzola.org)) to Twyla. It assumes you can read the target site's templates and run shell commands. It was written from a real port of a ~17-page Zola blog; every gotcha below is one we actually hit. The goal is **not** a line-by-line translation of the old templates. A naive conversion of Tera/Liquid templates produces Typst that reads like "templating-but-worse." Instead you build a small, composable **library of components** in Typst and assemble each page template from a few lines of it. --- ## The loop ``` zola build --drafts # 1. produce the ground truth in public/ twyla convert --from zola --base-url https://example.com # 2. scaffold .typ drafts # 3. write templates/ (a component library) twyla convert --verify --base-url https://example.com # 4. diff vs ground truth; fix; repeat ``` `twyla convert` compiles the whole site in memory, **diffs every page against the ground-truth `public/`** (structure, whitespace-relaxed), and runs a link audit. `--verify` is the read-only gate: it never writes, just reports. Drive it to `RESULT: OK`. ### 1. Ground truth Twyla diffs against the source SSG's built output. Build it first: ``` zola build --drafts # --drafts so draft pages are included ``` This populates `public/`. If `public/` is stale or missing, `convert` will tell you. (Override the location with `--ground-truth `.) ### 2. Scaffold drafts ``` twyla convert --from zola --base-url ``` For every `content/**/*.md` this writes a sibling `.typ` draft (e.g. `guis-1.md → guis-1.typ`, `_index.md → main.typ`) and a placeholder `templates/lib.typ`. The drafts carry the markdown body translated to Typst, frontmatter as `#set document(...)`, and shortcode calls as `#name(...)`. - It **never clobbers** an existing `.typ` (generate mode). To regenerate after the importer improves, use `--overwrite`, or run `twyla md2typ ` to re-translate a single page to stdout. - The drafts say "Manual cleanup expected!" at the top. Expect to hand-fix a few per-page issues (see gotchas). ### 3. Build the component library This is the real work. See "Architecture" below. ### 4. Iterate ``` twyla convert --verify --base-url # all pages twyla convert --verify --base-url --only guis-1 # scope to one route ``` Read the diff (see "Reading the diff"), fix a template or a draft, re-run. The site must **compile as a whole** — one broken page fails the run for all — so clear compile errors first, then chase diffs. --- ## The Twyla API you build against Twyla registers these on top of stock Typst. This is your whole toolbox. **Per-page metadata** — set near the top of a page, read back contextually: ```typ #set document( title: "My Post", date: datetime(year: 2024, month: 1, day: 2), description: [A short blurb.], kind: "post", // optional; defaults from filename (see kinds) draft: false, extra: (any: "dict"), // arbitrary author data output: "custom/index.html", // override the route if needed ) #context document.title // readable on this page inside #context ``` **Cross-page listing** — `documents()` returns one dict per page: ```typ #context for doc in documents() { // doc.url, doc.output, doc.title, doc.date, doc.description, // doc.kind, doc.draft, doc.extra } ``` **Assets** — fingerprinted, emitted to `/assets/...`, resolved contextually: ```typ #context asset.sass("/sass/site.sass").url() // compile SCSS/Sass → CSS #context asset.file("/static/logo.png").url() // copy a file verbatim #context asset.image("/img/hero.jpg", width: 1024, format: "webp").url() // resize + transcode #context asset.typst("/resume/cv.typ", format: "pdf").url() // compile a typst doc (svg/png/pdf/html) ``` Paths are project-root-relative when they start with `/`. `.url()` is contextual — call it inside `#context` (or inside a function the template wraps in `#context`). **Raw HTML & text:** ```typ #raw-html("literal HTML") // spliced in verbatim #plain-text(some-content) // flatten content → string ``` **`sys.inputs.base_url`** — the `--base-url` value (empty if unset). **Native rules already applied for you** (don't reimplement): - headings get auto-slug `id`s (zola parity); an explicit `