cloudflare / cloudflare/vinext
App Router: root layout html/body wrapped again by outer SSR, creating nested tags
- Dominant language
- TypeScript
- Stars
- 8.8k
- Forks
- 406
- Avg merge
- 2d 6h
- Merged PRs (30d)
- 120
Description
### Description
When the App Router root layout at `app/[locale]/layout.tsx` returns `......` (as Next.js docs require), the rendered HTML in production ends up with everything nested inside another `` wrapper:
```html
...
...
...actual content...
```
Two ``, three ``, two `` in the final output. Tested on 0.0.43, 0.0.42, 0.0.41, 0.0.40.
### Why this matters
Google Rich Results Test parses the inner `` as a separate document, so any `` placed in the inner `<head>` gets counted twice. My site has a single `LocalBusiness` schema with `aggregateRating` in the root layout. Validator reports "Review has multiple aggregate ratings" because it sees two `LocalBusiness` objects sharing the same `@id`, and marks the rich result as invalid.
### Steps to Reproduce
1. Standard App Router root layout with `<html>` and `<body>` tags
2. Add a `<script type="application/ld+json">` inside `<head>` containing `LocalBusiness` with `aggregateRating`
3. Build and deploy: `npx vinext build && wrangler deploy`
4. Paste the deployed URL into https://search.google.com/test/rich-results
Validator flags "multiple aggregate ratings" and the schema is rejected.
### Quick check on any Vinext site in prod
```bash
curl -s https://yoursite.com | grep -c '<html'
# Expected: 1
# Vinext returns: 2
```
### Workaround I landed on
Removed `<html>`, `<head>`, `<body>` from the root layout, returned a `<>` fragment, let React 19 hoist `<link>`/`<meta>`/`<script>` to the document head. Wrapped the rest in `<div className="min-h-screen flex flex-col">` to replace body-level flex.
Works (Rich Results now validates with 0 errors), but breaks the Next.js App Router contract which explicitly says the root layout must include `<html>` and `<body>`. Will need to revert once Vinext stops double-wrapping.
Happy to put together a minimal repro if it helps narrow this down.
Contributor guide
Research direction
Start with the App Router root layout at app/[locale]/layout.tsx and reproduce the production output with npx vinext build && wrangler deploy. Check why the SSR output adds an outer html/body wrapper when the layout already returns those tags. Done means curl reports one tag and the Rich Results test no longer sees duplicated structured data.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- react, typescript
- Domain
- frontend, web-dev
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100