@magnet-js/astro
v0.1.0
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
adopt: true, // adopt server-rendered DOM during hydration (default)
});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".
By default, islands adopt their server-rendered DOM: the component runs
against a cursor that serves the existing nodes in deterministic render order,
so event listeners, attributes, and reactive subscriptions attach to the nodes
already in the page. If anything changed the tree since SSR (third-party
scripts, browser extensions, autofill, user input before hydration), the cursor
reports a mismatch and the hydrator falls back to a fresh client render that
replaces the island's content. Correctness never depends on the tree being
untouched; adoption only affects whether nodes are reused or recreated. Disable
adoption per project with adopt: false.
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,adopt,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.
