Summary
Custom elements (tags containing hyphens like <my-element>) in .astro files are routed through the renderer pipeline, allowing any registered renderer to SSR them. The same elements in .mdx files are treated as raw HTML strings — renderers are never consulted.
This affects any web component library (Lit, Stencil, custom implementations) that registers a renderer for custom elements.
Expected behavior
<my-element> in an MDX file should produce the same SSR output as <my-element> in an .astro file when a renderer claims it.
Actual behavior
.astro: <my-element> → renderComponent() → renderer check() → SSR output ✅
.mdx: <my-element> → renderElement() → raw HTML string concatenation ❌
Root cause
The .astro compiler (Go) explicitly detects custom elements and routes them through renderComponent:
// parser.go:414
func isCustomElement(data string) bool {
return strings.Contains(data, "-")
}
// print-to-js.go:381
isComponent := isFragment || n.Component || n.CustomElement
The JSX runtime (jsx.ts:96) has no equivalent — all string-typed vnodes go to renderElement():
case typeof vnode.type === 'string' && vnode.type !== ClientOnlyPlaceholder:
return markHTMLString(await renderElement(result, vnode.type, vnode.props ?? {}));
renderElement() is pure string concatenation. No renderer is ever consulted.
Proposed fix
Add a hyphen check so custom elements fall through to the existing renderComponentToString() path (which already handles string-typed components correctly at line 147–153):
case typeof vnode.type === 'string' && vnode.type !== ClientOnlyPlaceholder && !vnode.type.includes('-'):
return markHTMLString(await renderElement(result, vnode.type, vnode.props ?? {}));
Custom elements then reach renderComponentToString() → renderComponent() → renderFrameworkComponent() → renderer check() — the same path the .astro compiler uses. If no renderer claims the element, the existing fallback in renderComponent() renders it as raw HTML, so there's no regression for projects without a matching renderer.
Impact
Tested across a production site with 80+ web components used in MDX content pages:
| Metric |
Before |
After |
| SSR'd custom elements |
77% |
99% |
| Build regressions |
0 |
0 |
Reproduction
- Create an Astro 6 project with
@astrojs/mdx and a custom element renderer
- Register a web component:
my-element
- Use
<my-element> in a .astro file → inspect output, SSR'd ✅
- Use
<my-element> in an .mdx file → inspect output, raw HTML ❌
The change is in packages/astro/src/runtime/server/jsx.ts, line 96.
Summary
Custom elements (tags containing hyphens like
<my-element>) in.astrofiles are routed through the renderer pipeline, allowing any registered renderer to SSR them. The same elements in.mdxfiles are treated as raw HTML strings — renderers are never consulted.This affects any web component library (Lit, Stencil, custom implementations) that registers a renderer for custom elements.
Expected behavior
<my-element>in an MDX file should produce the same SSR output as<my-element>in an.astrofile when a renderer claims it.Actual behavior
.astro:<my-element>→renderComponent()→ renderercheck()→ SSR output ✅.mdx:<my-element>→renderElement()→ raw HTML string concatenation ❌Root cause
The
.astrocompiler (Go) explicitly detects custom elements and routes them throughrenderComponent:The JSX runtime (
jsx.ts:96) has no equivalent — all string-typed vnodes go torenderElement():renderElement()is pure string concatenation. No renderer is ever consulted.Proposed fix
Add a hyphen check so custom elements fall through to the existing
renderComponentToString()path (which already handles string-typed components correctly at line 147–153):Custom elements then reach
renderComponentToString()→renderComponent()→renderFrameworkComponent()→ renderercheck()— the same path the.astrocompiler uses. If no renderer claims the element, the existing fallback inrenderComponent()renders it as raw HTML, so there's no regression for projects without a matching renderer.Impact
Tested across a production site with 80+ web components used in MDX content pages:
Reproduction
@astrojs/mdxand a custom element renderermy-element<my-element>in a.astrofile → inspect output, SSR'd ✅<my-element>in an.mdxfile → inspect output, raw HTML ❌The change is in
packages/astro/src/runtime/server/jsx.ts, line 96.