@magnet-js/astro
v0.2.1
Published
Astro integration for Magnet.
Readme
@magnet/astro
Astro integration for Magnet.
@magnet/astro lets you use Magnet components
inside Astro pages: server-side rendering to static HTML,
plus client-side hydration through Astro's client:* directives. No JSX —
components are plain functions that build real DOM trees through the Magnet
context.
Install
JSR
deno add jsr:@magnet/astronpm via JSR
npx jsr add @magnet/astroSetup
Register the integration in astro.config.*:
import { defineConfig } from "astro/config";
import magnet from "@magnet/astro";
export default defineConfig({ integrations: [magnet()] });The integration registers the Magnet server renderer and client hydrator with
Astro. All built-in hydration directives work out of the box: client:load,
client:idle, client:visible, client:media, and client:only="magnet".
Options
magnet({
signals: "preact", // "tc39" (default) | "preact" | "alien"
rendererName: "magnet", // id matched by client:only directives
extender: "./src/magnet-context.ts", // optional context extender module
});signals— the signal implementation used to create component contexts on the server and in the browser. Each adapter ships in its own entrypoint (@magnet/astro/server/server.preact,@magnet/astro/client/client.preact, and so on), so the client bundle contains exactly one signal implementation. Set it to match the adapter your Magnet application code uses.rendererName— the name matched byclient:onlydirectives in Astro pages. Defaults to"magnet".extender— module specifier of a context extender applied to every component context, on the server and in the browser. Use it when your components expect an extended context created withm.extend():// src/magnet-context.ts export default (m) => m.extend({ analytics: track });Relative specifiers resolve against the project root. The context is created and extended once per signal implementation and shared across all islands.
adopt— whether islands adopt their server-rendered DOM during hydration, attaching listeners and reactive subscriptions to the existing nodes instead of re-rendering from scratch. Defaults totrue. When the tree does not match the component's render (third-party DOM changes, extensions, user input before hydration), the hydrator falls back to a fresh render, so correctness never depends on the tree being untouched.name— the integration's name in Astro's config and logs (defaults to@magnet/astro).clientEntrypoint/serverEntrypoint— override the registered entrypoint specifiers when the package is installed under a different name (for example a scoped npm build).
Writing components
A Magnet Astro component is a function wrapped in component(). The brand gives
the renderer's check hook a deterministic, side-effect-free way to recognize
Magnet components (JSX renderers must test-render components to detect them;
Magnet does not need to).
// src/components/Counter.ts
import { component } from "@magnet/astro";
export default component((m, props, slots) => {
const count = m.state(0);
return m.html.div({ class: "counter" }, [
m.html.button(
{ onclick: () => count.set(count.get() + 1) },
["count: ", count],
),
slots.default,
]);
});The component receives:
m— the Magnet context (html,svg,mathML,custom,render,text,comment,shadow,extend, and the signal primitives selected by thesignalsoption). On the server it is bound to@magnet/ssr's string-rendering window; in the browser to the realwindow.props— the props passed from the Astro page.slots— the page's rendered slots as placeable node arrays (see below).
It returns a node, an array of nodes, a string/number rendered as text, or
null/undefined to render nothing. Components may be async — the renderer
awaits them.
Use it in an .astro page like any framework component:
---
import Counter from "../components/Counter.ts";
---
<Counter client:load>
<p>rendered inside the counter</p>
</Counter>How Astro slots map onto Magnet
Astro and Magnet have fundamentally different component models, and slots are where they meet:
- Astro renders slot content itself and hands each framework renderer the
result as an opaque HTML string per slot. Renderers place that markup by
wrapping it in
<astro-slot>elements (hydrated components) or<astro-static-slot>elements (static components); Astro strips the static wrappers from final output and reads the hydration wrappers back inside<astro-island>to distribute slot DOM on the client. - Magnet has no virtual DOM and no props/children indirection: components build real nodes, and children arrays accept nodes, text, signals, and cleanup functions — not raw HTML strings (strings are escaped as text).
The integration bridges this by turning each slot into an opaque placeholder node that components drop into children arrays:
component((m, props, slots) =>
m.html.div({ class: "layout" }, [
m.html.aside({}, [slots.sidebar]),
m.html.main({}, [slots.default]),
])
);slots.defaultis the unnamed slot; named slots appear under their name.- A slot the page did not provide is
undefined— safe to spread into children arrays, since Magnet skipsundefinedchildren. - On the server each placeholder stringifies to the slot markup wrapped in the
<astro-slot>/<astro-static-slot>marker Astro expects, so slots land exactly where the component placed them. On the client each placeholder is an<astro-slot>element whose children are parsed from the markup with the browser's native<template>parsing. - Placeholders are text-shaped, so they type-check anywhere textual children are
accepted. Do not put them inside textual-only elements (
title,textarea): their markup is raw HTML and is never escaped.
Hydration
All built-in directives work once the renderer is registered: client:load,
client:idle, client:visible, client:media, and client:only="magnet".
Islands hydrate with a fresh client render: the server markup is discarded and the component re-renders from scratch, re-establishing event listeners, attributes, and reactive subscriptions on brand-new nodes.
Constraints at the island boundary:
- Props must be serializable by Astro (JSON-like values) to reach the client. During server-only rendering, props may be anything, including signals and functions.
_useactions and event listeners only run in the browser — that is Magnet's own SSR semantics, not a limitation of this package.- Signal-based children render their initial values on the server. Non-leading
signal children are anchored by
<!---->comment markers; leading signal children render bare. - Unmounting (for example after view transitions) runs the tree's full cleanup
through Astro's
astro:unmountevent.
Public API
Entry point @magnet/astro:
magnetAstro(also the default export) — creates the Astro integration.IntegrationOptions—name,rendererName,signals,extender,clientEntrypoint,serverEntrypoint.component— brands a function as a Magnet component (re-exported from@magnet/commonwith the signature narrowed toMagnetComponent).isComponent— detects branded components.MagnetComponent/ComponentContext/ComponentResult— the component contract types.ContextExtender— the extender function type.Slots/SlotValue/SlotNode— slot placeholder types.ComponentChildren/ComponentTagFor/ComponentHTML— tag types whose children accept any element (unlikeHTMLChildrenfrom@magnet/ui, which locks element children to the parent's own tag name).ClientHydrator/IslandHydrate— the client hydrator types. Astro's own contract types (AstroIntegration,AstroComponentMetadata, and friends) come from theastropackage, which is a peer dependency.
Entry points @magnet/astro/server/server.* (one per signal adapter:
server.tc39, server.preact, server.alien):
- default export — the SSR renderer (
check,renderToStaticMarkup,supportsAstroStaticSlot) registered with Astro asserverEntrypoint. createServerRenderer— the renderer factory, for composing custom entrypoints.
Entry points @magnet/astro/client/client.* (one per signal adapter:
client.tc39, client.preact, client.alien):
- default export — the client hydrator registered as
clientEntrypoint. createHydrator/sharedHydrator— hydrator factories for custom entrypoints, tests, and non-standard environments.
License
MIT © 2026 Fernando G. Vilar.
