Skip to content

feat(stream): split template at <!--app-html--> / <!--ssr-outlet--> marker#753

Closed
harlan-zw wants to merge 1 commit into
mainfrom
feat/streaming-ssr-outlet-marker
Closed

feat(stream): split template at <!--app-html--> / <!--ssr-outlet--> marker#753
harlan-zw wants to merge 1 commit into
mainfrom
feat/streaming-ssr-outlet-marker

Conversation

@harlan-zw

@harlan-zw harlan-zw commented Apr 23, 2026

Copy link
Copy Markdown
Collaborator

🔗 Linked issue

N/A — surfaced while making the Vue streaming example use the canonical Vite outlet template.

❓ Type of change

  • 📖 Documentation
  • 🐞 Bug fix
  • 👌 Enhancement
  • ✨ New feature
  • 🧹 Chore
  • ⚠️ Breaking change

📚 Description

`prepareStreamingTemplate` only split at the body tag boundary, so a canonical Vite SSR template like `<div id="app">` emitted the app stream as a sibling of `<div id="app">` and the client couldn't hydrate `#app` without a container mismatch. When the body interior contains `` or ``, we now split at that marker so the app stream lands inside the container. Templates without a marker keep current behavior.

Summary by CodeRabbit

  • Bug Fixes

    • Improved streaming SSR template handling to properly detect and position outlet markers, ensuring correct content placement between shell and streamed sections in Vite SSR configurations.
  • Tests

    • Added comprehensive test coverage for streaming SSR template scenarios with various outlet marker patterns and fallback behavior to ensure streaming reliability.

`prepareStreamingTemplate` currently splits a template only at the body
tag boundary, so a template like
`<body><div id="app"><!--app-html--></div></body>` emits the shell up to
`<body>` and then streams the app content as a sibling of `<div id="app">`.
That produces a server-rendered DOM the client can't hydrate at `#app`
without a container mismatch.

If the body interior contains the canonical Vite SSR outlet marker
(`<!--app-html-->` or `<!--ssr-outlet-->`), split there instead: everything
before the marker is appended to the shell, everything after becomes the
new body interior. Templates without a marker retain current behavior.
@coderabbitai

coderabbitai Bot commented Apr 23, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This change modifies the streaming template preparation logic to detect and handle Vite SSR outlet comment markers (<!--app-html--> and <!--ssr-outlet-->), conditionally splitting the HTML template so the marked outlet section is excluded from the shell and reserved for the streamed app output.

Changes

Cohort / File(s) Summary
Streaming Server Implementation
packages/unhead/src/stream/server.ts
Added detection of canonical Vite SSR outlet comment markers. Modified template splitting logic in prepareStreamingTemplate to conditionally relocate the segment before the outlet marker into the shell, while keeping the remainder (starting at/after the marker) for the end output that follows the streamed app.
Streaming Tests
packages/unhead/test/streaming/streaming.test.ts
Added three new unit tests for prepareStreamingTemplate coverage. Tests verify that when outlet markers (<!--app-html--> or <!--ssr-outlet-->) are present, the template splits correctly so markers don't appear in the generated shell but continue into the generated end. Includes fallback behavior test using <body> boundary when markers are absent.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

A rabbit hops through streaming time,
Finding outlets, marker signs,
Splitting shells with care and grace,
Apps now stream in their rightful place! 🐇✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: detecting and splitting templates at Vite SSR outlet markers, which is the core feature implemented.
Description check ✅ Passed The description includes all required template sections with substantive content: linked issue context, type of change selected, and a detailed explanation of the problem and solution.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/streaming-ssr-outlet-marker

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

Copy link
Copy Markdown
Contributor

Bundle Size Analysis

Bundle Size Gzipped
Client (Minimal) 10.7 kB 4.4 kB
Server (Minimal) 10.6 kB 4.3 kB
Vue Client (Minimal) 11.8 kB 4.9 kB
Vue Server (Minimal) 11.6 kB 4.7 kB

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/unhead/test/streaming/streaming.test.ts (1)

425-455: Tighten assertions around the final assembled HTML.

These tests cover the split, but they don’t currently prove the streamed app lands inside the outlet wrapper or that the marker is absent from end. Adding round-trip assertions would pin the hydration contract more directly.

🧪 Suggested test assertions
       expect(shell).toContain('<title>Outlet Test</title>')
       expect(shell).toContain('<div id="app">')
       expect(shell).not.toContain('<!--app-html-->')
+      expect(end).not.toContain('<!--app-html-->')
       expect(end).toContain('</div></body></html>')
+      expect(`${shell}<span>app</span>${end}`).toContain('<div id="app"><span>app</span></div>')
@@
       expect(shell).toContain('<div id="root">')
       expect(shell).not.toContain('<!--ssr-outlet-->')
+      expect(end).not.toContain('<!--ssr-outlet-->')
       expect(end).toContain('</div></body></html>')
+      expect(`${shell}<span>app</span>${end}`).toContain('<div id="root"><span>app</span></div>')
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/unhead/test/streaming/streaming.test.ts` around lines 425 - 455, The
tests currently only assert split points but don't verify the final assembled
HTML nor that the streamed app content actually lands inside the outlet wrapper;
update the three specs using createStreamableHead and prepareStreamingTemplate
to perform a round-trip assertion: simulate the streamed app payload (e.g. an
app fragment string), concatenate shell + app fragment + end, then assert the
concatenated output contains the outlet wrapper with the app fragment inside,
and assert the original outlet marker (<!--app-html--> or <!--ssr-outlet-->) is
not present in shell, end, or the final concatenation; reference the
prepareStreamingTemplate result variables shell and end and the
createStreamableHead head usage when adding these assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/unhead/test/streaming/streaming.test.ts`:
- Around line 425-455: The tests currently only assert split points but don't
verify the final assembled HTML nor that the streamed app content actually lands
inside the outlet wrapper; update the three specs using createStreamableHead and
prepareStreamingTemplate to perform a round-trip assertion: simulate the
streamed app payload (e.g. an app fragment string), concatenate shell + app
fragment + end, then assert the concatenated output contains the outlet wrapper
with the app fragment inside, and assert the original outlet marker
(<!--app-html--> or <!--ssr-outlet-->) is not present in shell, end, or the
final concatenation; reference the prepareStreamingTemplate result variables
shell and end and the createStreamableHead head usage when adding these
assertions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e2f2d7cc-6ed2-4e74-8c06-24db12f2a182

📥 Commits

Reviewing files that changed from the base of the PR and between ea54e36 and 45ae3df.

📒 Files selected for processing (2)
  • packages/unhead/src/stream/server.ts
  • packages/unhead/test/streaming/streaming.test.ts

@harlan-zw

Copy link
Copy Markdown
Collaborator Author

Superseded by #752, which landed the same marker-splitting logic in prepareStreamingTemplate plus equivalent test coverage.

@harlan-zw harlan-zw closed this Apr 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant