Skip to content

Astro

@sigmx/astro does two things: it injects the client into every page, and it gives your endpoints helpers for reading signals and answering with HTML, JSON or a stream of patches. This documentation site is built with it; every live example that talks to a server goes through an Astro endpoint.

Terminal window
npm install sigmx @sigmx/astro
astro.config.mjs
import { defineConfig } from 'astro/config'
import sigmx from '@sigmx/astro'
export default defineConfig({
integrations: [sigmx({ plugins: 'all' })],
})

Endpoints that answer requests need to run on the server: add an adapter and set export const prerender = false in each route.

option default
plugins 'all' 'all', 'minimal', 'auto' (scan the source; see Auto mode), or a list of export names from sigmx/plugins
auto options for auto mode: always, custom, include, extensions, log
prefix 'data-' attribute prefix(es)
eventPrefix 'sigmx-' accepted server event prefixes
expose 'sigmx' global name for the instance; false for none
inject true inject on every page; false to opt in per page
entrypoint an app-local module that creates the instance itself, e.g. '/src/sigmx.ts'
precompile false compile attribute expressions at build time; see Precompiled expressions

An app-local entrypoint lets you add your own plugins:

src/sigmx.ts
import { createSigmx, all, attribute } from '@sigmx/astro/client'
const shout = attribute({ name: 'shout', value: 'required', mount: ({ el, evaluate, effect }) => effect(() => (el.textContent = String(evaluate()).toUpperCase())) })
export const app = createSigmx({ plugins: [...all, shout] })
import {
readSignals, isSigmxRequest, // request side
sse, sseStream, html, json, // responses
patchElements, patchSignals, removeElements, removeSignals, executeScript, // event builders
SigmxStream, formatEvent, // lower level
} from '@sigmx/astro/server'

These are the same helpers as sigmx/server, re-exported for convenience.

const signals = await readSignals(request) // { q: 'bri', page: 2 }
const { q } = await readSignals(request, schema) // validated with any Standard Schema validator (Zod, Valibot, ArkType…)

GET and DELETE requests carry signals in the sigmx query parameter, other methods in a JSON body, and contentType: 'form' requests as form fields. Validation failures throw a SignalsError whose .response() is a ready-made 422.

return html(renderedFragment) // morphed by id
return html(row, { selector: '#rows', mode: 'append' }) // targeted
return json({ count: 3 }) // merged as signals
return sse(patchElements(fragment), patchSignals({ busy: false })) // several patches at once
return sseStream(async (s) => { // patches over time
s.patchSignals({ progress: 0 })
for (const step of steps) { await work(step); s.patchSignals({ progress: step.pct }) }
})

Astro’s container API renders a component to a string on the server:

import { experimental_AstroContainer as AstroContainer } from 'astro/container'
const container = await AstroContainer.create()
const fragment = await container.renderToString(Results, { props: { q } })
Live
  • Aberdeen
  • Bath
  • Belfast
  • Birmingham
  • Bristol
  • Cambridge
  • Cardiff
  • Chester
  • Derby
  • Dundee
  • Durham
  • Edinburgh
  • Exeter
  • Glasgow
  • Inverness
  • Leeds
  • Leicester
  • Lincoln
  • Liverpool
  • London
  • Manchester
  • Newcastle
  • Norwich
  • Nottingham
  • Oxford
  • Plymouth
  • Sheffield
  • Southampton
  • Swansea
  • York
---
import { towns } from '../../data/towns';
import Towns from '../partials/Towns.astro';
---
<div class="ex search-app" data-signals="{ q: '', results: 30 }">
<header class="bar">
<label class="search"><span aria-hidden="true">⌕</span><input class="field" type="search" placeholder="Filter towns…" data-bind:q data-on:input__debounce.250ms="@get('/api/search')" data-indicator:searching></label>
<span class="chip" data-text="$searching ? 'searching…' : $results + ' towns'"></span>
</header>
<Towns q="" towns={towns} />
</div>
<style is:global>
.search-app { background: var(--ex-surface); }
.search-app .bar { display: flex; align-items: center; gap: .8rem; padding: .7rem 1rem; border-bottom: 1px solid var(--ex-line); }
.search-app .search { position: relative; flex: 1; }
.search-app .search span { position: absolute; left: .7rem; top: 50%; transform: translateY(-50%); color: var(--ex-muted); }
.search-app .search .field { padding-left: 2rem; }
.search-app #towns { list-style: none; margin: 0; padding: .6rem 1rem; display: flex; flex-wrap: wrap; gap: .4rem; max-height: 9rem; overflow: auto; }
.search-app #towns li { padding: .25rem .7rem; border-radius: 999px; background: var(--ex-surface-2); border: 1px solid var(--ex-line); font-size: .85rem; animation: msg-in .2s ease-out; }
.search-app #towns li.empty { background: transparent; border: 0; color: var(--ex-muted); }
</style>
src/pages/api/search.ts
import type { APIRoute } from 'astro'
import { experimental_AstroContainer as AstroContainer } from 'astro/container'
import { patchElements, patchSignals, readSignals, sse } from '@sigmx/astro/server'
import Towns from '../../components/partials/Towns.astro'
import { towns } from '../../data/towns'
export const prerender = false
export const GET: APIRoute = async ({ request }) => {
const { q = '' } = await readSignals(request)
const matches = towns.filter((t) => t.toLowerCase().includes(String(q).toLowerCase()))
const container = await AstroContainer.create()
const list = await container.renderToString(Towns, { props: { q, towns: matches } })
return sse(patchElements(list), patchSignals({ results: matches.length }))
}
src/components/partials/Towns.astro
---
interface Props { q: string; towns: string[] }
const { q, towns } = Astro.props
---
<ul id="towns">
{towns.length === 0 && <li class="muted">No towns match “{q}”.</li>}
{towns.map((t) => <li>{t}</li>)}
</ul>

More endpoints from this site: a streaming job, a todo list that morphs, a validated form.

Astro’s <ClientRouter /> swaps the body on navigation. The client observes the document root, so swapped-in markup mounts automatically and signals survive the navigation. Use data-signals__ifmissing for per-page defaults that should not reset on return.