Skip to main content
A component is a .slurp file included from another file. It takes props, renders markup, and can accept children through a <slot />.

Declaring a component

Components live in components/, where they are checked but never emitted as a page.
components/Card.slurp
Import it in the frontmatter of whatever uses it, then use it as a tag:
index.slurp
A tag is a component if its first character is uppercase. <Card /> is a component; <card /> is a plain HTML element and is emitted as one. There is no registration step beyond the using line.

Props are passed by expression

A quoted COMPONENT prop is a literal string. A quoted ELEMENT attribute interpolates. The same characters mean two different things depending on whether the tag starts with an uppercase letter, and nothing reports the difference.
This is the most common mistake in the language. If a prop shows up on the page as literal ${...} text, this is why.
To pass a value, brace it. To build a string around a value, brace a template literal.

Prop forms

Prop declarations and defaults

The props block records names and types for tooling. Types are never checked at render time, so a missing prop is simply null and renders as the empty string. No error, no warning. Two things follow from that:
  • An undeclared prop still arrives. Passing <Card note={"hi"} /> makes note readable inside the component even though props never mentions it. Declaring it serves the reader and the editor, not the renderer.
  • A = default does have a render-time effect. Omit featured above and the component sees false, not null. The default expression is evaluated in the caller’s scope, so it can read the caller’s globals.
Write one declaration per line:
A type expression runs to the end of the line. Two declarations sharing a line with no comma between them merge into one:
a swallows b’s declaration as part of its own type and takes "fallback" as its own default. b is never recorded, so b’s default never applies. A comma fixes it; one declaration per line avoids the ambiguity entirely.

Scope: page globals, overlaid by props

A component renders in its own root scope. It sees the page globals, with whatever was passed to it layered on top and winning on a name collision. So a component can read a global it was never passed:
components/Footer.slurp
What it cannot see is the caller’s local bindings. An {each} loop variable and loop.index do not exist inside a component.
Pass it in:
Nothing reports this. It renders as blanks inside the component.
loop.index works the same way. It is a caller-side value, so build the string in the caller and pass the result:

Children and the slot

Children of a component tag land at its <slot />.
components/Badge.slurp
Two rules apply:
<NoSlot>children here</NoSlot> renders the component and drops the text. Silently. If content vanishes, check that the component has a <slot />.
A component has exactly one content slot. <slot name="sidebar" /> renders as a literal <slot> element carrying its fallback content, and a child written <nav slot="sidebar"> is not routed to it: it lands in the default slot with everything else. You get both copies, in the wrong place, with no diagnostic.Anything that would have been a second slot becomes a prop, or its own component. See Layouts and slots for the one exception, which is the layout head slot.

Passing a list

There is no special array prop. Pass the array and loop inside the component.
components/List.slurp
any is the annotation when the element shape is not declared. items: Product[] renders identically, because no annotation is ever enforced.

Importing

The @ path is the file’s path from the input root with the extension dropped, so components/ui/Button.slurp is @components/ui/Button. Nested directories work. Relative paths such as "./components/Card" do not resolve on the CLI. Imports are per file. A component that uses another component imports it itself; it does not inherit the page’s imports.
components/Outer.slurp

When a component does not resolve

An unresolvable component does NOT fail the build, and neither build --verbose nor validate --warnings mentions it. It renders a placeholder carrying the name and the props it was given, with its children inside:
So a typo in an import path looks like a clean build with a missing card. If a component is not appearing, grep the output for data-slurp-component.
The same placeholder appears when a component includes itself. Recursion is stopped by a cycle guard rather than by an error, so <Recur /> renders one level and then a placeholder.

Next

Layouts and slots

Layouts, slots, and the head block.

Displaying data

Expressions, property access, and operators.

Control flow

{if}, {each}, {match} and the loop variables.

Common mistakes

The silent failures, collected in one place.