diff --git a/e2e/react-start/encoding/.gitignore b/e2e/react-start/encoding/.gitignore
new file mode 100644
index 00000000000..a79d5cf1299
--- /dev/null
+++ b/e2e/react-start/encoding/.gitignore
@@ -0,0 +1,20 @@
+node_modules
+package-lock.json
+yarn.lock
+
+.DS_Store
+.cache
+.env
+.vercel
+.output
+
+/build/
+/api/
+/server/build
+/public/build
+# Sentry Config File
+.env.sentry-build-plugin
+/test-results/
+/playwright-report/
+/blob-report/
+/playwright/.cache/
diff --git a/e2e/react-start/encoding/.prettierignore b/e2e/react-start/encoding/.prettierignore
new file mode 100644
index 00000000000..2be5eaa6ece
--- /dev/null
+++ b/e2e/react-start/encoding/.prettierignore
@@ -0,0 +1,4 @@
+**/build
+**/public
+pnpm-lock.yaml
+routeTree.gen.ts
\ No newline at end of file
diff --git a/e2e/react-start/encoding/package.json b/e2e/react-start/encoding/package.json
new file mode 100644
index 00000000000..acfc1a2346a
--- /dev/null
+++ b/e2e/react-start/encoding/package.json
@@ -0,0 +1,38 @@
+{
+ "name": "tanstack-react-start-e2e-encoding",
+ "private": true,
+ "sideEffects": false,
+ "type": "module",
+ "scripts": {
+ "dev": "vite dev --port 3000",
+ "dev:e2e": "vite dev",
+ "build": "vite build && tsc --noEmit",
+ "start": "node .output/server/index.mjs",
+ "test:e2e": "rm -rf port*.txt; playwright test --project=chromium"
+ },
+ "dependencies": {
+ "@tanstack/react-router": "workspace:^",
+ "@tanstack/react-router-devtools": "workspace:^",
+ "@tanstack/react-start": "workspace:^",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0",
+ "redaxios": "^0.5.1",
+ "tailwind-merge": "^2.6.0",
+ "vite": "6.3.5",
+ "zod": "^3.24.2"
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.50.1",
+ "@tanstack/router-e2e-utils": "workspace:^",
+ "@types/node": "^22.10.2",
+ "@types/react": "^19.0.8",
+ "@types/react-dom": "^19.0.3",
+ "@vitejs/plugin-react": "^4.3.4",
+ "autoprefixer": "^10.4.20",
+ "combinate": "^1.1.11",
+ "postcss": "^8.5.1",
+ "tailwindcss": "^3.4.17",
+ "typescript": "^5.7.2",
+ "vite-tsconfig-paths": "^5.1.4"
+ }
+}
diff --git a/e2e/react-start/encoding/playwright.config.ts b/e2e/react-start/encoding/playwright.config.ts
new file mode 100644
index 00000000000..b0c365f8bd1
--- /dev/null
+++ b/e2e/react-start/encoding/playwright.config.ts
@@ -0,0 +1,42 @@
+import { defineConfig, devices } from '@playwright/test'
+import {
+ getDummyServerPort,
+ getTestServerPort,
+} from '@tanstack/router-e2e-utils'
+import packageJson from './package.json' with { type: 'json' }
+
+const PORT = await getTestServerPort(packageJson.name)
+const EXTERNAL_PORT = await getDummyServerPort(packageJson.name)
+const baseURL = `http://localhost:${PORT}`
+
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+export default defineConfig({
+ testDir: './tests',
+ workers: 1,
+
+ reporter: [['line']],
+
+ globalSetup: './tests/setup/global.setup.ts',
+ globalTeardown: './tests/setup/global.teardown.ts',
+
+ use: {
+ /* Base URL to use in actions like `await page.goto('/')`. */
+ baseURL,
+ },
+
+ webServer: {
+ command: `VITE_NODE_ENV="test" VITE_EXTERNAL_PORT=${EXTERNAL_PORT} pnpm build && VITE_NODE_ENV="test" VITE_EXTERNAL_PORT=${EXTERNAL_PORT} VITE_SERVER_PORT=${PORT} PORT=${PORT} pnpm start`,
+ url: baseURL,
+ reuseExistingServer: !process.env.CI,
+ stdout: 'pipe',
+ },
+
+ projects: [
+ {
+ name: 'chromium',
+ use: { ...devices['Desktop Chrome'] },
+ },
+ ],
+})
diff --git a/e2e/react-start/encoding/postcss.config.mjs b/e2e/react-start/encoding/postcss.config.mjs
new file mode 100644
index 00000000000..2e7af2b7f1a
--- /dev/null
+++ b/e2e/react-start/encoding/postcss.config.mjs
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/e2e/react-start/encoding/src/client.tsx b/e2e/react-start/encoding/src/client.tsx
new file mode 100644
index 00000000000..5fcd1c5f19d
--- /dev/null
+++ b/e2e/react-start/encoding/src/client.tsx
@@ -0,0 +1,19 @@
+// DO NOT DELETE THIS FILE!!!
+// This file is a good smoke test to make sure the custom client entry is working
+import { StrictMode, startTransition } from 'react'
+import { hydrateRoot } from 'react-dom/client'
+import { StartClient } from '@tanstack/react-start'
+import { createRouter } from './router'
+
+console.log("[client-entry]: using custom client entry in 'src/client.tsx'")
+
+const router = createRouter()
+
+startTransition(() => {
+ hydrateRoot(
+ document,
+
+
+ ,
+ )
+})
diff --git a/e2e/react-start/encoding/src/components/CustomMessage.tsx b/e2e/react-start/encoding/src/components/CustomMessage.tsx
new file mode 100644
index 00000000000..d00e4eac60b
--- /dev/null
+++ b/e2e/react-start/encoding/src/components/CustomMessage.tsx
@@ -0,0 +1,10 @@
+import * as React from 'react'
+
+export function CustomMessage({ message }: { message: string }) {
+ return (
+
+
This is a custom message:
+
{message}
+
+ )
+}
diff --git a/e2e/react-start/encoding/src/components/DefaultCatchBoundary.tsx b/e2e/react-start/encoding/src/components/DefaultCatchBoundary.tsx
new file mode 100644
index 00000000000..15f316681cc
--- /dev/null
+++ b/e2e/react-start/encoding/src/components/DefaultCatchBoundary.tsx
@@ -0,0 +1,53 @@
+import {
+ ErrorComponent,
+ Link,
+ rootRouteId,
+ useMatch,
+ useRouter,
+} from '@tanstack/react-router'
+import type { ErrorComponentProps } from '@tanstack/react-router'
+
+export function DefaultCatchBoundary({ error }: ErrorComponentProps) {
+ const router = useRouter()
+ const isRoot = useMatch({
+ strict: false,
+ select: (state) => state.id === rootRouteId,
+ })
+
+ console.error(error)
+
+ return (
+
+
+
+
+ {isRoot ? (
+
+ Home
+
+ ) : (
+ {
+ e.preventDefault()
+ window.history.back()
+ }}
+ >
+ Go Back
+
+ )}
+
+
+ )
+}
diff --git a/e2e/react-start/encoding/src/components/NotFound.tsx b/e2e/react-start/encoding/src/components/NotFound.tsx
new file mode 100644
index 00000000000..af4e0e74946
--- /dev/null
+++ b/e2e/react-start/encoding/src/components/NotFound.tsx
@@ -0,0 +1,25 @@
+import { Link } from '@tanstack/react-router'
+
+export function NotFound({ children }: { children?: any }) {
+ return (
+
+
+ {children ||
The page you are looking for does not exist.
}
+
+
+
+
+ Start Over
+
+
+
+ )
+}
diff --git a/e2e/react-start/encoding/src/routeTree.gen.ts b/e2e/react-start/encoding/src/routeTree.gen.ts
new file mode 100644
index 00000000000..2ca196a7ca8
--- /dev/null
+++ b/e2e/react-start/encoding/src/routeTree.gen.ts
@@ -0,0 +1,153 @@
+/* eslint-disable */
+
+// @ts-nocheck
+
+// noinspection JSUnusedGlobalSymbols
+
+// This file was automatically generated by TanStack Router.
+// You should NOT make any changes in this file as it will be overwritten.
+// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
+
+import type { CreateFileRoute, FileRoutesByPath } from '@tanstack/react-router'
+import type {
+ CreateServerFileRoute,
+ ServerFileRoutesByPath,
+} from '@tanstack/react-start/server'
+
+import { Route as rootRouteImport } from './routes/__root'
+import { Route as NoCharsetRouteImport } from './routes/no-charset'
+import { Route as CharsetRouteImport } from './routes/charset'
+import { Route as IndexRouteImport } from './routes/index'
+
+const NoCharsetRoute = NoCharsetRouteImport.update({
+ id: '/no-charset',
+ path: '/no-charset',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const CharsetRoute = CharsetRouteImport.update({
+ id: '/charset',
+ path: '/charset',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const IndexRoute = IndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => rootRouteImport,
+} as any)
+
+export interface FileRoutesByFullPath {
+ '/': typeof IndexRoute
+ '/charset': typeof CharsetRoute
+ '/no-charset': typeof NoCharsetRoute
+}
+export interface FileRoutesByTo {
+ '/': typeof IndexRoute
+ '/charset': typeof CharsetRoute
+ '/no-charset': typeof NoCharsetRoute
+}
+export interface FileRoutesById {
+ __root__: typeof rootRouteImport
+ '/': typeof IndexRoute
+ '/charset': typeof CharsetRoute
+ '/no-charset': typeof NoCharsetRoute
+}
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths: '/' | '/charset' | '/no-charset'
+ fileRoutesByTo: FileRoutesByTo
+ to: '/' | '/charset' | '/no-charset'
+ id: '__root__' | '/' | '/charset' | '/no-charset'
+ fileRoutesById: FileRoutesById
+}
+export interface RootRouteChildren {
+ IndexRoute: typeof IndexRoute
+ CharsetRoute: typeof CharsetRoute
+ NoCharsetRoute: typeof NoCharsetRoute
+}
+
+declare module '@tanstack/react-router' {
+ interface FileRoutesByPath {
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/charset': {
+ id: '/charset'
+ path: '/charset'
+ fullPath: '/charset'
+ preLoaderRoute: typeof CharsetRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/no-charset': {
+ id: '/no-charset'
+ path: '/no-charset'
+ fullPath: '/no-charset'
+ preLoaderRoute: typeof NoCharsetRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ }
+}
+
+declare module './routes/index' {
+ const createFileRoute: CreateFileRoute<
+ '/',
+ FileRoutesByPath['/']['parentRoute'],
+ FileRoutesByPath['/']['id'],
+ FileRoutesByPath['/']['path'],
+ FileRoutesByPath['/']['fullPath']
+ >
+
+ const createServerFileRoute: CreateServerFileRoute<
+ ServerFileRoutesByPath['/']['parentRoute'],
+ ServerFileRoutesByPath['/']['id'],
+ ServerFileRoutesByPath['/']['path'],
+ ServerFileRoutesByPath['/']['fullPath'],
+ unknown
+ >
+}
+declare module './routes/charset' {
+ const createFileRoute: CreateFileRoute<
+ '/charset',
+ FileRoutesByPath['/charset']['parentRoute'],
+ FileRoutesByPath['/charset']['id'],
+ FileRoutesByPath['/charset']['path'],
+ FileRoutesByPath['/charset']['fullPath']
+ >
+
+ const createServerFileRoute: CreateServerFileRoute<
+ ServerFileRoutesByPath['/charset']['parentRoute'],
+ ServerFileRoutesByPath['/charset']['id'],
+ ServerFileRoutesByPath['/charset']['path'],
+ ServerFileRoutesByPath['/charset']['fullPath'],
+ unknown
+ >
+}
+declare module './routes/no-charset' {
+ const createFileRoute: CreateFileRoute<
+ '/no-charset',
+ FileRoutesByPath['/no-charset']['parentRoute'],
+ FileRoutesByPath['/no-charset']['id'],
+ FileRoutesByPath['/no-charset']['path'],
+ FileRoutesByPath['/no-charset']['fullPath']
+ >
+
+ const createServerFileRoute: CreateServerFileRoute<
+ ServerFileRoutesByPath['/no-charset']['parentRoute'],
+ ServerFileRoutesByPath['/no-charset']['id'],
+ ServerFileRoutesByPath['/no-charset']['path'],
+ ServerFileRoutesByPath['/no-charset']['fullPath'],
+ unknown
+ >
+}
+
+const rootRouteChildren: RootRouteChildren = {
+ IndexRoute: IndexRoute,
+ CharsetRoute: CharsetRoute,
+ NoCharsetRoute: NoCharsetRoute,
+}
+export const routeTree = rootRouteImport
+ ._addFileChildren(rootRouteChildren)
+ ._addFileTypes()
diff --git a/e2e/react-start/encoding/src/router.tsx b/e2e/react-start/encoding/src/router.tsx
new file mode 100644
index 00000000000..c76eb0210cc
--- /dev/null
+++ b/e2e/react-start/encoding/src/router.tsx
@@ -0,0 +1,22 @@
+import { createRouter as createTanStackRouter } from '@tanstack/react-router'
+import { routeTree } from './routeTree.gen'
+import { DefaultCatchBoundary } from './components/DefaultCatchBoundary'
+import { NotFound } from './components/NotFound'
+
+export function createRouter() {
+ const router = createTanStackRouter({
+ routeTree,
+ defaultPreload: 'intent',
+ defaultErrorComponent: DefaultCatchBoundary,
+ defaultNotFoundComponent: () => ,
+ scrollRestoration: true,
+ })
+
+ return router
+}
+
+declare module '@tanstack/react-router' {
+ interface Register {
+ router: ReturnType
+ }
+}
diff --git a/e2e/react-start/encoding/src/routes/__root.tsx b/e2e/react-start/encoding/src/routes/__root.tsx
new file mode 100644
index 00000000000..388c3cbfe6d
--- /dev/null
+++ b/e2e/react-start/encoding/src/routes/__root.tsx
@@ -0,0 +1,64 @@
+///
+import * as React from 'react'
+import {
+ HeadContent,
+ Outlet,
+ Scripts,
+ createRootRoute,
+} from '@tanstack/react-router'
+
+import { DefaultCatchBoundary } from '~/components/DefaultCatchBoundary'
+import { NotFound } from '~/components/NotFound'
+
+export const Route = createRootRoute({
+ head: () => ({
+ meta: [
+ {
+ name: 'viewport',
+ content: 'width=device-width, initial-scale=1',
+ },
+ ],
+ }),
+ errorComponent: (props) => {
+ return (
+
+
+
+ )
+ },
+ notFoundComponent: () => ,
+ component: RootComponent,
+})
+
+function RootComponent() {
+ return (
+
+
+
+ )
+}
+
+const RouterDevtools =
+ process.env.NODE_ENV === 'production'
+ ? () => null // Render nothing in production
+ : React.lazy(() =>
+ // Lazy load in development
+ import('@tanstack/react-router-devtools').then((res) => ({
+ default: res.TanStackRouterDevtools,
+ })),
+ )
+
+function RootDocument({ children }: { children: React.ReactNode }) {
+ return (
+
+
+
+
+
+ {children}
+
+
+
+
+ )
+}
diff --git a/e2e/react-start/encoding/src/routes/charset.tsx b/e2e/react-start/encoding/src/routes/charset.tsx
new file mode 100644
index 00000000000..b522ef192ad
--- /dev/null
+++ b/e2e/react-start/encoding/src/routes/charset.tsx
@@ -0,0 +1,14 @@
+export const Route = createFileRoute({
+ head: () => ({
+ meta: [
+ {
+ charSet: 'utf-8',
+ },
+ ],
+ }),
+ component: Page,
+})
+
+function Page() {
+ return Charset
+}
diff --git a/e2e/react-start/encoding/src/routes/index.tsx b/e2e/react-start/encoding/src/routes/index.tsx
new file mode 100644
index 00000000000..b522ef192ad
--- /dev/null
+++ b/e2e/react-start/encoding/src/routes/index.tsx
@@ -0,0 +1,14 @@
+export const Route = createFileRoute({
+ head: () => ({
+ meta: [
+ {
+ charSet: 'utf-8',
+ },
+ ],
+ }),
+ component: Page,
+})
+
+function Page() {
+ return Charset
+}
diff --git a/e2e/react-start/encoding/src/routes/no-charset.tsx b/e2e/react-start/encoding/src/routes/no-charset.tsx
new file mode 100644
index 00000000000..4ef0458325a
--- /dev/null
+++ b/e2e/react-start/encoding/src/routes/no-charset.tsx
@@ -0,0 +1,7 @@
+export const Route = createFileRoute({
+ component: Page,
+})
+
+function Page() {
+ return No charset
+}
diff --git a/e2e/react-start/encoding/src/server.ts b/e2e/react-start/encoding/src/server.ts
new file mode 100644
index 00000000000..77cb4a13169
--- /dev/null
+++ b/e2e/react-start/encoding/src/server.ts
@@ -0,0 +1,13 @@
+// DO NOT DELETE THIS FILE!!!
+// This file is a good smoke test to make sure the custom server entry is working
+import {
+ createStartHandler,
+ defaultStreamHandler,
+} from '@tanstack/react-start/server'
+import { createRouter } from './router'
+
+console.log("[server-entry]: using custom server entry in 'src/server.ts'")
+
+export default createStartHandler({
+ createRouter,
+})(defaultStreamHandler)
diff --git a/e2e/react-start/encoding/tailwind.config.mjs b/e2e/react-start/encoding/tailwind.config.mjs
new file mode 100644
index 00000000000..e49f4eb776e
--- /dev/null
+++ b/e2e/react-start/encoding/tailwind.config.mjs
@@ -0,0 +1,4 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: ['./src/**/*.{js,jsx,ts,tsx}'],
+}
diff --git a/e2e/react-start/encoding/tests/charset-encoding.spec.ts b/e2e/react-start/encoding/tests/charset-encoding.spec.ts
new file mode 100644
index 00000000000..8b949e4697d
--- /dev/null
+++ b/e2e/react-start/encoding/tests/charset-encoding.spec.ts
@@ -0,0 +1,52 @@
+import { expect } from '@playwright/test'
+import { test } from './fixture'
+
+test.describe('Encoding', () => {
+ test('asserts dehydration script is injected before when no charset is present', async ({
+ page,
+ }) => {
+ // In this test setup, the no-charset route does not have a charset meta tag.
+ const response = await page.goto('/no-charset')
+ const html = await response?.text()
+
+ expect(html).toBeDefined()
+ if (!html) return
+
+ // Case-insensitive search for charset meta, TSR script, and closing head tag
+ const htmlLower = html.toLowerCase()
+ const charsetIndex = htmlLower.search(/')
+
+ // Charset should NOT exist, but the TSR script and head tag should
+ expect(charsetIndex).toBe(-1)
+ expect(tsrScriptIndex).toBeGreaterThan(-1)
+ expect(headEndIndex).toBeGreaterThan(-1)
+
+ // In the fallback case, the TSR dehydration script should appear BEFORE the closing tag.
+ expect(tsrScriptIndex).toBeLessThan(headEndIndex)
+ })
+
+ test('asserts charset meta tag appears before dehydration script when present on a route', async ({
+ page,
+ }) => {
+ // This route specifically adds a charset meta tag.
+ const response = await page.goto('/charset')
+ const html = await response?.text()
+
+ expect(html).toBeDefined()
+ if (!html) return
+
+ // Case-insensitive search for charset meta and TSR script
+ const htmlLower = html.toLowerCase()
+ const charsetIndex = htmlLower.search(/
+}
+export const test = base.extend({
+ whitelistErrors: [[], { option: true }],
+ page: async ({ page, whitelistErrors }, use) => {
+ const errorMessages: Array = []
+ page.on('console', (m) => {
+ if (m.type() === 'error') {
+ const text = m.text()
+ for (const whitelistError of whitelistErrors) {
+ if (
+ (typeof whitelistError === 'string' &&
+ text.includes(whitelistError)) ||
+ (whitelistError instanceof RegExp && whitelistError.test(text))
+ ) {
+ return
+ }
+ }
+ errorMessages.push(text)
+ }
+ })
+ await use(page)
+ expect(errorMessages).toEqual([])
+ },
+})
diff --git a/e2e/react-start/encoding/tests/setup/global.setup.ts b/e2e/react-start/encoding/tests/setup/global.setup.ts
new file mode 100644
index 00000000000..3593d10ab90
--- /dev/null
+++ b/e2e/react-start/encoding/tests/setup/global.setup.ts
@@ -0,0 +1,6 @@
+import { e2eStartDummyServer } from '@tanstack/router-e2e-utils'
+import packageJson from '../../package.json' with { type: 'json' }
+
+export default async function setup() {
+ await e2eStartDummyServer(packageJson.name)
+}
diff --git a/e2e/react-start/encoding/tests/setup/global.teardown.ts b/e2e/react-start/encoding/tests/setup/global.teardown.ts
new file mode 100644
index 00000000000..62fd79911cc
--- /dev/null
+++ b/e2e/react-start/encoding/tests/setup/global.teardown.ts
@@ -0,0 +1,6 @@
+import { e2eStopDummyServer } from '@tanstack/router-e2e-utils'
+import packageJson from '../../package.json' with { type: 'json' }
+
+export default async function teardown() {
+ await e2eStopDummyServer(packageJson.name)
+}
diff --git a/e2e/react-start/encoding/tsconfig.json b/e2e/react-start/encoding/tsconfig.json
new file mode 100644
index 00000000000..b3a2d67dfa6
--- /dev/null
+++ b/e2e/react-start/encoding/tsconfig.json
@@ -0,0 +1,22 @@
+{
+ "include": ["**/*.ts", "**/*.tsx", "public/script*.js"],
+ "compilerOptions": {
+ "strict": true,
+ "esModuleInterop": true,
+ "jsx": "react-jsx",
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "lib": ["DOM", "DOM.Iterable", "ES2022"],
+ "isolatedModules": true,
+ "resolveJsonModule": true,
+ "skipLibCheck": true,
+ "target": "ES2022",
+ "allowJs": true,
+ "forceConsistentCasingInFileNames": true,
+ "baseUrl": ".",
+ "paths": {
+ "~/*": ["./src/*"]
+ },
+ "noEmit": true
+ }
+}
diff --git a/e2e/react-start/encoding/vite.config.ts b/e2e/react-start/encoding/vite.config.ts
new file mode 100644
index 00000000000..54430698260
--- /dev/null
+++ b/e2e/react-start/encoding/vite.config.ts
@@ -0,0 +1,15 @@
+import { defineConfig } from 'vite'
+import tsConfigPaths from 'vite-tsconfig-paths'
+import { tanstackStart } from '@tanstack/react-start/plugin/vite'
+
+export default defineConfig({
+ server: {
+ port: 3000,
+ },
+ plugins: [
+ tsConfigPaths({
+ projects: ['./tsconfig.json'],
+ }),
+ tanstackStart({ tsr: { verboseFileRoutes: false } }),
+ ],
+})
diff --git a/packages/router-core/src/ssr/transformStreamWithRouter.ts b/packages/router-core/src/ssr/transformStreamWithRouter.ts
index 54c0197bb58..2383fbbb966 100644
--- a/packages/router-core/src/ssr/transformStreamWithRouter.ts
+++ b/packages/router-core/src/ssr/transformStreamWithRouter.ts
@@ -24,6 +24,8 @@ const patternBodyStart = /()/
const patternHtmlEnd = /(<\/html>)/
const patternHeadStart = /()/
+const patternHeadEnd = /(<\/head>)/
+const patternCharset = /(\s]+\2.*?>)/i
// regex pattern for matching closing tags
const patternClosingTag = /(<\/[a-zA-Z][\w:.-]*?>)/g
@@ -98,6 +100,7 @@ export function transformStreamWithRouter(
let pendingClosingTags = ''
let bodyStarted = false as boolean
let headStarted = false as boolean
+ let headScriptInjected = false as boolean
let leftover = ''
let leftoverHtml = ''
@@ -181,18 +184,35 @@ export function transformStreamWithRouter(
}
}
- if (!headStarted) {
- const headStartMatch = chunkString.match(patternHeadStart)
- if (headStartMatch) {
- headStarted = true
- const index = headStartMatch.index!
- const headTag = headStartMatch[0]
- const remaining = chunkString.slice(index + headTag.length)
- finalPassThrough.write(
- chunkString.slice(0, index) + headTag + getBufferedRouterStream(),
- )
- // make sure to only write `remaining` until the next closing tag
- chunkString = remaining
+ if (!headScriptInjected) {
+ if (!headStarted) {
+ const headStartMatch = chunkString.match(patternHeadStart)
+ if (headStartMatch) {
+ headStarted = true
+ }
+ }
+
+ if (headStarted) {
+ const charsetMatch = chunkString.match(patternCharset)
+
+ if (charsetMatch) {
+ headScriptInjected = true
+ const index = charsetMatch.index! + charsetMatch[0]!.length
+ finalPassThrough.write(
+ chunkString.slice(0, index) + getBufferedRouterStream(),
+ )
+ chunkString = chunkString.slice(index)
+ } else {
+ const headEndMatch = chunkString.match(patternHeadEnd)
+ if (headEndMatch) {
+ headScriptInjected = true
+ const index = headEndMatch.index!
+ finalPassThrough.write(
+ chunkString.slice(0, index) + getBufferedRouterStream(),
+ )
+ chunkString = chunkString.slice(index)
+ }
+ }
}
}