Skip to main content
A Slurp project is a directory of .slurp files. There is no config file, no scaffolding command, and no manifest the compiler reads. slurp build walks the tree it is given and writes a mirror of it. This page uses examples/basic-theme from the repository as the worked example.

Building it

The output:

The output tree mirrors the input tree

There is no rewriting. A .slurp file keeps its relative path and changes extension, so pages/index.slurp becomes dist/pages/index.html, and pages/ stays in the URL unless whatever serves the site strips it.

Special directories

components/ and layouts/ are compiled and checked but never written out. A component or a layout rendered on its own is a fragment rather than a document, and a layout in particular comes out carrying literal <slot> elements, so emitting them would fill the output with files nothing can serve. They are still compiled. A syntax error, a security violation or an unknown filter in a component that no page imports yet still fails the build.
Every other directory is emitted. A sections/Hero.slurp builds to dist/sections/Hero.html, a real file containing an unfilled fragment. The rule is a blocklist of those two names, not an allowlist of pages/, and it matches on the first path component only, so components-archive/ is not caught by it either.

pages/ is a convention, not a rule

The compiler does not require it. A lone index.slurp at the root builds to dist/index.html, which is what the Quickstart does. slurp-dev and the hosts that embed Slurp route an extensionless request path into it, resolving / to pages/index.html and /product to pages/product.html or pages/product/index.html. Anything with a file extension is served straight from the theme root as an asset. slurp build also prints a warning when it emits no pages at all, and that warning names pages/:
The message is about the routing convention rather than the compiler’s own rule. A build of only components and middleware is legal and produces nothing.

Static assets are copied verbatim

Every non-.slurp file in the input tree is copied to the output at the same relative path. There is no assets directory and no manifest: CSS, images, fonts and JavaScript go wherever the markup references them, and they land at the same path.
That is why README.md and .gitignore appear in the listing above. If you do not want a file published, keep it out of the input tree. The copy walk skips the same directories the compile walk does:
  • any directory whose name begins with .,
  • node_modules, dist and target, by name, wherever they appear,
  • the --output directory itself, so a build cannot copy its own output back into a nested copy of itself,
  • symlinks, which are never followed.
dist and target are excluded by NAME, not because they happen to be the defaults. A directory you genuinely want published cannot be called either.

The globals file is input, not an asset

--globals seeds the render context with values a template must resolve at compile time. It is read and not copied to the output, even though it sits inside the input tree in the obvious layout. An earlier version of the copy walk shipped it to production at a guessable path.
A --globals file that is missing, malformed, or not a JSON object is fatal, not a quiet fallback to an empty context:
A globals file is passed because some attribute cannot be written without it, so a build that silently continued would come out clean and break in the browser.
Commit the globals file. It holds no secrets, and without it a fresh clone builds with those values empty. A build-time secret belongs in .env, never here.

How imports resolve

An @ path is the file’s path from the input root, without the extension.
@components/ui/Card resolves components/ui/Card.slurp, so nesting works with no configuration. The local name is the last path segment, or the alias in using "./Card" as Renamed.
An unresolvable import does not fail the build. <Nope title="x" /> renders as <div data-slurp-props='{"title":"x"}' data-slurp-component=Nope></div>, so a typo in an import path looks like a working build with a piece missing.

.gitignore

Writes a .gitignore suited to a theme into the current directory. It refuses rather than clobbering an existing one, and prints the three options:
The rules it adds are dist/, node_modules/, .env and editor files. The rest of the file names what must not be ignored.
Commit your compiled CSS. If the theme compiles a stylesheet with Tailwind or anything else, the compiled output is committed alongside the source, and it must compile to a path the theme actually serves rather than into dist/.A *.css or styles/ line in .gitignore breaks this invisibly: the theme publishes, every page renders, every static check stays green, and every page is unstyled HTML because the stylesheet 404s.
The globals file and a theme.json manifest, if you have one, are committed for the same reason: they are inputs, not build artifacts.

Next

Writing pages

File-based routing, page data, and output paths.

Components

Props, slots, and imports.

Middleware

The --middleware flag, scope rules, and redirects.

CLI

Every subcommand and flag.