@svebcomponents/ssr-astro
Server-side rendering support for custom elements inside Astro apps.
This is the Astro counterpart to @svebcomponents/ssr’s Svelte integration.
The element renderers, the DOM shim, the renderer registry and the client-side
hydration machinery are all shared — this package only supplies what is
specific to Astro: finding custom element tags in .astro templates, and
emitting declarative shadow DOM around them.
This package is experimental. Its API may change before it is released alongside the rest of the toolchain.
Setup
Add the integration. That is the entire configuration:
import { defineConfig } from "astro/config";import svebcomponents from "@svebcomponents/ssr-astro";
export default defineConfig({ integrations: [svebcomponents()],});Load the component package’s browser entry (which defines the custom element)
and its /ssr entry (which registers the renderer), then write custom elements
in any .astro file:
---import "my-component-package";import "my-component-package/ssr";---
<my-component title="Hello" count="5"> <p>light dom child</p></my-component>The Simplest Client Story of the Three
Astro ships no client-side JavaScript for these elements, so there is no client-side counterpart to this integration and nothing to register in the browser. The parser attaches the server-rendered shadow root, the element upgrades when its bundle loads, and it hydrates its own shadow content.
That removes the entire class of problem the Vue and React integrations have to manage — matching a client render against the server’s markup. There is no host render to match.
The exception is Astro islands: a custom element inside a client:* React,
Vue or Svelte island belongs to that framework, and the corresponding
svebcomponents integration applies there.
Async Components
Astro frontmatter is an async module scope, so the wrapper simply awaits the
element renderer. A component that awaits while rendering, or that has an
async SsrPrepare hook, works through the same wrapper as a synchronous one —
no sync/async split as in the Svelte integration, and no degradation as in
React’s.
Being a non-Svelte host, an Astro app does need to opt into Svelte’s async SSR mode explicitly:
import "@svebcomponents/ssr/enable-async";Any Custom Element, Not Just Svelte-built Ones
This integration depends only on Lit’s ElementRenderer contract, so it will
server-render any custom element that has a renderer registered — including
Lit elements:
import { LitElementRenderer } from "@lit-labs/ssr/lib/lit-element-renderer.js";import { ElementRendererRegistry } from "@svebcomponents/ssr";
ElementRendererRegistry.use(LitElementRenderer);Requirements
@svebcomponents/ssr calls Svelte’s server renderer, so svelte must be
installed as a server-side dependency of the Astro app even though no Svelte
components appear in it. The component package’s own server bundle ships its
Svelte runtime, but the renderer entry point does not.
How It Works
A Vite plugin rewrites custom element tags in .astro source to a wrapper
component, which renders:
<my-component title="Hello"> <template shadowrootmode="open"><!-- shadow content --></template> <!-- light dom children --></my-component>Two implementation details are worth knowing, because both are easy to get wrong:
The rewrite happens in Vite’s load hook, not transform. Astro compiles
.astro to JavaScript in its own transform, and its plugin is enforce: "pre" and registered ahead of anything an integration contributes — so a
transform here would receive compiled JavaScript with no custom element tags
left in it. Astro’s load hook only serves its virtual ?astro=…
sub-requests, so supplying the component file’s contents there means Astro’s
compiler transforms the rewritten source regardless of plugin order.
The wrapper closes its <template> explicitly. A self-closing
<template ... /> is not treated as void by Astro’s compiler; it swallows the
following <slot />, putting the light-dom children inside the shadow content.
Detection needs no heuristics: Astro’s parser classifies a dashed tag as its
own custom-element AST node, so unlike the Svelte and Vue integrations there
is no “contains a dash” rule and no reserved SVG/MathML exclusion list.
Not an Astro “Renderer”
Astro’s addRenderer() API is for UI-framework components used as islands,
with client:* directives — it expects check() and renderToStaticMarkup()
entrypoints. A custom element is not an island: it hydrates itself and needs no
directive. So this ships as a template rewrite plus a wrapper component rather
than a renderer registration.
Current Limitations
- Experimental; the API may change.
- Only
.astrotemplates are rewritten. Custom elements inside.mdxare untested. - The consuming app must import the browser custom element module and register the matching SSR renderer, exactly as with the other integrations.