Getting Started
Aihu is a web meta-framework whose components are single-file components
(SFCs) — .aihu files with a small set of macro blocks. The same
component is a custom element for humans and a discoverable, governed surface
for AI agents. This guide builds one from scratch.
Hello, world
After scaffolding a project, open src/pages/index.aihu. The
smallest useful component declares reactive state and a template that reads it:
@state {
let name = prop({ default: 'world' })
}
@template {
<p>Hello, {name}!</p>
}
@route {
path: "/",
name: "home"
}Three blocks, three jobs: @state owns reactivity,
@template owns structure, @route registers the page.
Everything else composes from here.
The @state block
@state is where reactive values live. It is ordinary TypeScript —
you can import, declare helpers, and call the reactive intrinsics.
Props are the component's public, host-settable surface. Declare one with
prop(); it is reactive, reflects to an attribute, and can be
exposed to agents.
@state {
let count = prop({
default: 0,
describe: 'The current tally',
expose: 'read write',
})
}Local state that is not part of the public surface uses state():
@state {
let open = state(false)
}Reach for
state()for private working values, andprop()when the value is part of the component's public, host-settable, agent-visible contract.
Derived values are lazy and memoized — declare them with derived():
@state {
let count = prop({ default: 0 })
const doubled = derived(() => count * 2)
}Actions are named, agent-invocable operations. The wrapped form adds
describe / expose metadata so an MCP agent can
discover and call them:
@state {
let count = prop({ default: 0 })
const increment = action(() => { count++ })
const reset = action(
{ describe: 'Reset the tally to zero', expose: 'read write' },
() => { count = 0 },
)
}The @template block
@template describes the DOM with aihu's template grammar:
{expr}interpolates a reactive expression.href={expr}binds an attribute reactively.on:click={handler}attaches an event listener.if/elseif/elseandshowhandle conditionals.each/keyrender keyed lists.
@template {
<section class="counter">
<output class="count">{count}</output>
<div class="controls">
<button on:click={decrement}>−</button>
<button on:click={reset}>Reset</button>
<button on:click={increment}>+</button>
</div>
</section>
}The @route block
@route registers a component as a page. During build the Rust
compiler emits a .route.json sidecar; the router assembles them
into a fully static manifest — no filesystem scanning at runtime.
@route {
path: "/",
name: "home",
layout: "docs",
head: {
title: "Home — my app",
description: "Rendered to static HTML, hydrated on load."
}
}With output: 'static' every route without a loader is prerendered
to a content-ful index.html carrying its own head, then hydrated
into the live app on load — crawlers and agents read real content, humans get
the SPA.
A complete example
Put together, the canonical counter is about forty lines, has an agent surface (all three actions are agent-callable), and pulls in nothing beyond the reactive intrinsics:
@state {
let count = prop({ default: 0, describe: 'The tally', expose: 'read write' })
const increment = action(() => { count++ })
const decrement = action(() => { count-- })
const reset = action(() => { count = 0 })
}
@template {
<section class="counter">
<h2>Count: {count}</h2>
<div class="controls">
<button on:click={decrement}>−</button>
<button on:click={reset}>Reset</button>
<button on:click={increment}>+</button>
</div>
</section>
}Next steps
- Reactivity — the
signal,computed, andeffectprimitives behindpropandderived. - Authoring Components — the full reference for
@state,@template,@style, and@agent. - Agent Discovery — how state and actions become an MCP-discoverable surface.