The build folder

With the data model and the engine that drives it behind us, the rest of the chapter is hands-on — and it all happens in one folder. build/ is the only folder structure we document publicly, because it’s the only one you work in. Everything below it is yours to shape; everything above it is platform machinery. That single boundary is what lets a non-technical editor change content all day without ever risking your code, and lets you ship a template change without ever touching their content.

What’s inside

build/
├── pages/     routable pages            (a node with an @path renders here)
│   ├── place.php
│   └── home.php
├── blocks/    embeddable fragments      (a node without a path renders here)
│   ├── place-card.php
│   └── stop.php
├── menus/     navigation elements
│   └── mainmenu.php
├── forms/     structured edit/submit forms
│   └── place.php
└── assets/
    ├── css/   site styling — fully yours
    └── js/    browser modules — yours, behind the lint-gate

Those five are what every space has. Three more are optional overrides — the platform ships working defaults, and you only create the folder when you want to replace one:

build/
├── email/     email-template overrides   (login code, address-change, …)
├── i18n/      per-locale UI strings        ({locale}.json)
└── pwa/       PWA icons + offline page

A node’s @build property names its template. @build: pages/place resolves to build/pages/place.php; @build: blocks/stop resolves to build/blocks/stop.php. The mapping is direct — no router config, no registration step.

Routable vs embedded

The folder a template lives in answers one question: can this node be visited at a URL?

has an @path  →  pages/    →  a full HTML page at /that-path
no @path      →  blocks/   →  rendered only inside another node

A routable node has a @path and a pages/* build — together they grant it a URL. An embedded node has no path; it lives inside other nodes as a card, a stop, a list item. The same type can be either: a Place can be a full page in one space and a card in another. Type describes what something is; build and path decide how it’s reached. (More in Nodes & types.)

Push and pull, with a gate

You work on build/ the way you’d expect — locally, in your editor, in version control. Pushing it to a space and pulling it back is a Git-like sync. But unlike a raw git push, every push passes through a lint-gate before it’s accepted. The gate is what makes the code/content boundary safe rather than merely conventional.

The gate rejects anything that could let a template escape its sandbox:

The gate blocks Why
Dangerous functions (exec, eval, system, raw file_* on absolute paths) A template renders data; it must not run arbitrary code or touch the host
Out-of-bounds reads A template may read its own space’s graph, nothing outside it
External dependencies No Composer, no vendor/, no npm install — templates use the platform’s helpers and PHP’s standard library only

If the push lints clean, it lands. If it doesn’t, you get a specific error and nothing changes on the live space.

Why the boundary holds both ways

The split runs deeper than “don’t break each other’s stuff.” Alongside build/, the space root holds folders you don’t reach as files — data/ (the node graph), config/ (space settings), feed/ (generated public extracts) and files/ (uploads) — managed entirely at runtime by the editor, the API and the compiler. You never read or write them by hand.

code-push    →  changes build/        →  never touches data/ or config/
runtime edit →  changes data/ config/ →  never touches build/

That clean separation is why an editor approving a draft can’t corrupt a template, and why deploying a new template can’t lose a single edit. The lint-gate guards the code side; the tinycycle and API guard the data side. Neither can reach across.

One thing does span the boundary on purpose — the helper API that turns static markup into editable content — but that gets its own page shortly. First, the look of the site: Theming & assets.