Skip to content

How the pieces fit

A map for developers working on Cratly rather than with it. It answers the questions that are expensive to reconstruct after a break: which repository owns what, what the pieces promise each other, and what a change in one of them obliges you to do in the others.

If you only want to build or edit a website, start with the Guide instead.

Decisions

The reasoning behind the contracts, recorded when it was made:

The repositories

RepositoryPublished asOwns
site (this one)cratly.ioThe framework-agnostic specifications, the section-type schema, the user guide, and this map
editoreditor.cratly.ioThe browser-based editor and the GitLab review workflow
scavoldnpm scavold, scavold.ioThe reference adapter: turns a content repository into a VitePress site
templates/scavold-plainOne of the site templates — starting points for a new website repository. Further templates are flat sibling repositories in cratly/templates/, listed in the same catalogue
test-websiteFixture site: the surface on which adapter changes are exercised
toolscontainer registryCI base image that pre-seeds the package cache for site pipelines

Alongside them sit the website repositories themselves — one per site, holding content, theme and pipeline. They are the only repositories editors ever touch.

The three contracts

Everything the pieces know about each other passes through three artifacts. Each bug found so far has lived in one of these seams, not inside a single repository.

.cratly.config.yaml — hand-written, read by both sides

Describes the website: where pages and media live, which container types and frontmatter fields exist. The editor reads it to configure its UI; the adapter reads it to locate sources and apply content rules. Neither may extend it unilaterally — the specification is owned here.

.cratly/sections.json — generated by the adapter, committed, read by the editor

The section-type manifest: which typed properties each container kind accepts, so the editor can render matching controls. Written by the adapter's build, committed to the website repository, and fetched by the editor through the GitLab API. See ADR 0001 for why it is split this way.

It records the adapter's version, so every adapter upgrade changes this file in every website — even when no container type changed. Rebuilding and committing it is part of an upgrade, not an afterthought, and the site pipeline fails when the committed copy has drifted from what the build produces.

The build artifact — produced by the site pipeline, read by the editor

The site pipeline publishes the built site as a GitLab job artifact, which the editor downloads to show a preview of a draft. See CI / CD.

Who depends on whom

        specifications (this repo)

        ┌────────┴────────┐
        │                 │
     editor            scavold
        │                 │
        └──── website repository ────┘

             site pipeline ── uses ── CI image (tools)

The rule that keeps this workable: the editor knows nothing about Scavold, and Scavold knows nothing about the editor. Both know the contracts. A second adapter (Next.js, Astro, …) is meant to be possible without touching the editor — so anything framework-specific that creeps into the editor is a bug, and anything about editing that creeps into an adapter is too.

Rules that follow

  • Versions decouple the repositories, not branches. Websites depend on a published scavold release, so the adapter's main may move freely. Publish the producer first — a -rc.N pre-release is the tool for that — then upgrade the consumers. A consumer that depends on unreleased behaviour is how a contract silently rots.
  • Two generated files are committed on purpose: bun.lock and .cratly/sections.json. CI verifies the second is current. Never let CI write either of them back.
  • Website repositories keep a protected default branch. Drafts are branches, publishing is a merge — that is the editor's workflow, not a preference. Its Check repository setup panel verifies it.
  • Everything an author uploads into the media folder has to reach the published site. The editor accepts any file type there and writes references relative to that folder, as the media-file type specifies; turning those into working URLs is the adapter's job. Images become scaled variants, everything else is published by copying — and references are rewritten wherever they occur: image syntax, links, and container arguments declared as media-file. An adapter that covers only images leaves authors with dead links they cannot diagnose, because nothing fails during the build. (The reference adapter covers all of it from 0.2.0-rc.3 onwards.)
  • The CI image seeds a cache. It can never make a build wrong, only slower — each website's own lockfile and --frozen-lockfile decide what gets installed.

Where to change what

I want to …Change it hereThen
add or alter a config keythe specification in this repoteach the adapter, then the editor
add a property type for the editor's controlsthe section-type schema hereadapter emits it, editor renders it
add a container type to one websitethat website's .cratly.config.yamlrebuild so the manifest picks it up
add a built-in container to the adapterscavold (component, manifest, docs)exercise it in test-website
change how editing behaveseditor
speed up or fix site pipelinestools, then rebuild the image
start a new websitecopy a site templatesee its README, and Starting a new website
add a site templatea new sibling repository, plus one entry in docs/public/templates.json heresee the templates reference

Verifying an adapter change

Unit tests in scavold cover the build-time logic. What they cannot cover is the seam: whether a real site still builds and still produces the markup the documentation promises.

scavold therefore carries its own fixture site, built and checked on every commit:

sh
bun run test:fixture     # builds test/fixture-site and asserts what came out

It generates its media inputs, builds the site from scratch and checks the emitted manifest, the container markup, the image ladder, the published non-image media and the video container — the places where defects have actually appeared. Its CI job makes the check unavoidable rather than optional, which is why it exists: three defects were found this way in one afternoon, each documented as working for months, and each had cost a release to verify beforehand.

Before publishing, the same fixture is worth building once against the packed package rather than the working tree, because npm pack honours the files list and the exports map while a local path does not:

sh
cd scavold && npm pack
# install the tarball into a scratch copy of a site, then build it

test-website is a different fixture with a different job: it is the live GitLab project the editor's end-to-end tests authenticate against, with real branches and merge requests. Keeping the two apart is ADR 0003 — while they were one, neither was maintained for both purposes.

Two habits earned their place while writing those checks:

Show that a new test fails without the fix. Put the defect back, run the test, expect red, restore. Twice a convincing-looking test guarded nothing — once because the outcome was identical with and without the fix, once because the defect needed a different trigger than the test used (automatic linking fires while typing, not while loading a document). A test that has never been red has not been tested.

Everything the build produces stays out of git. public/media/ is generated in full — image variants and verbatim copies of other media — while bun.lock and .cratly/sections.json are the two generated files that are committed on purpose. When a release starts emitting a new kind of output, the ignore rules need to catch up, or every download ends up duplicated in the repository.

Local setup

Every repository uses Bun and the same three commands: bun install, bun run dev, bun run build. Documentation sites (site, scavold) additionally serve their docs with bun run dev / bun run docs:dev; test-website and the site templates are ordinary VitePress sites.