Skip to content

@astrojs/cloudflare dev server nests hoisted CSS inside Starlight's <template>, page renders unstyled #16291

@p-linnane

Description

@p-linnane

Astro Info

Astro                    v6.1.5
Vite                     v7.3.2
Node                     v25.9.0
System                   macOS (arm64)
Package Manager          npm
Output                   static
Adapter                  @astrojs/cloudflare (v13.1.8)
Integrations             @astrojs/starlight (v0.38.3)

If this issue only occurs in one browser, which browser is a problem?

No response

Describe the Bug

When @astrojs/cloudflare is configured as the adapter, astro dev serves HTML where Vite's inline <style data-vite-dev-id="..."> blocks are nested inside Starlight's <template id="theme-icons"> element (from ThemeProvider.astro). Because <template> content is inert per the HTML spec, every style block trapped inside is non-functional. The page renders with browser defaults — no Starlight layout, no custom CSS, nothing.

Narrowed to Cloudflare-specific:

  • Vanilla Starlight starter (no adapter): template span ≈ 2000 bytes, styles outside the template ✅ works
  • Starlight + @astrojs/node: same — template span ≈ 2000 bytes, styles outside ✅ works
  • Starlight + @astrojs/cloudflare: template span ≈ 99000 bytes, 43 style blocks nested inside the template ❌ broken

Production builds (astro build && astro preview) are unaffected with any adapter, because CSS is extracted to external _astro/*.css files and there's no inline positional injection.

Root cause hypothesis:

ThemeProvider.astro places <template id="theme-icons"> in <head> with three <Icon> children, each an Astro component with scoped CSS. In dev mode, Vite hoists those components' styles and injects them at the current SSR render position. With most adapters (and no adapter at all), the template's open tag, children, and close tag serialize contiguously — style hoisting happens before or after the template boundary. With @astrojs/cloudflare, the SSR stream emits style-hoist injections between the template's opening tag and its first child. Every subsequent style block appends inside the template, and the three SVG icons end up after the styles immediately before the closing </template>.

Verification one-liner (paste into the browser console while viewing any page served by the reproduction):

(async () => {
  const t = await fetch(location.pathname).then((r) => r.text());
  const tOpen = t.indexOf('<template');
  const tClose = t.indexOf('</template>');
  console.log({
    templateSpan: tClose - tOpen,
    stylesInsideTemplate: t.indexOf('<style', tOpen) < tClose,
  });
})();

Broken: { templateSpan: ~99000, stylesInsideTemplate: true }.
Working (same repo with the adapter swapped to @astrojs/node or removed): { templateSpan: ~2000, stylesInsideTemplate: false }.

Workaround: astro build && astro preview — HMR is lost but the layout renders correctly.

What's the expected result?

<template id="theme-icons"> should contain only its three <Icon> children (sun/moon/laptop SVGs), the way it does both in production builds and in dev mode with any other adapter. Style blocks should appear before or after the template, not inside it.

Link to Minimal Reproducible Example

https://github.com/p-linnane/starlight-cloudflare-dev-template-bug

Participation

  • I am willing to submit a pull request for this issue.

Metadata

Metadata

Assignees

No one assigned

    Labels

    - P4: importantViolate documented behavior or significantly impacts performance (priority)pkg: astroRelated to the core `astro` package (scope)pkg: cloudflareRelated to the Cloudflare adapter

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions