diff --git a/examples/react/start-convex-trellaux/app/routes/__root.tsx b/examples/react/start-convex-trellaux/app/routes/__root.tsx
index 33080e61db..bf459a6a4c 100644
--- a/examples/react/start-convex-trellaux/app/routes/__root.tsx
+++ b/examples/react/start-convex-trellaux/app/routes/__root.tsx
@@ -4,9 +4,10 @@ import {
Outlet,
createRootRouteWithContext,
useRouterState,
+ HeadContent,
+ Scripts,
} from '@tanstack/react-router'
import { TanStackRouterDevtools } from '@tanstack/router-devtools'
-import { Meta, Scripts } from '@tanstack/start'
import * as React from 'react'
import { Toaster } from 'react-hot-toast'
import type { QueryClient } from '@tanstack/react-query'
@@ -81,7 +82,7 @@ function RootDocument({ children }: { children: React.ReactNode }) {
return (
-
diff --git a/examples/react/start-counter/app/routes/__root.tsx b/examples/react/start-counter/app/routes/__root.tsx
index 326b21dc10..fd039a743a 100644
--- a/examples/react/start-counter/app/routes/__root.tsx
+++ b/examples/react/start-counter/app/routes/__root.tsx
@@ -1,6 +1,10 @@
import * as React from 'react'
-import { Outlet, createRootRoute } from '@tanstack/react-router'
-import { Meta, Scripts } from '@tanstack/start'
+import {
+ HeadContent,
+ Outlet,
+ Scripts,
+ createRootRoute,
+} from '@tanstack/react-router'
export const Route = createRootRoute({
head: () => ({
@@ -32,7 +36,7 @@ function RootDocument({ children }: { children: React.ReactNode }) {
return (
-
+
{children}
diff --git a/examples/react/start-large/app/routes/__root.tsx b/examples/react/start-large/app/routes/__root.tsx
index 4b8e0da32d..831e1af9fb 100644
--- a/examples/react/start-large/app/routes/__root.tsx
+++ b/examples/react/start-large/app/routes/__root.tsx
@@ -1,6 +1,10 @@
// app/routes/__root.tsx
-import { Outlet, createRootRouteWithContext } from '@tanstack/react-router'
-import { Meta, Scripts } from '@tanstack/start'
+import {
+ HeadContent,
+ Outlet,
+ Scripts,
+ createRootRouteWithContext,
+} from '@tanstack/react-router'
import type { QueryClient } from '@tanstack/react-query'
import type { ReactNode } from 'react'
import appCss from '~/styles.css?url'
@@ -40,7 +44,7 @@ function RootDocument({ children }: Readonly<{ children: ReactNode }>) {
return (
-
+
{children}
diff --git a/examples/react/start-supabase-basic/app/routes/__root.tsx b/examples/react/start-supabase-basic/app/routes/__root.tsx
index eddfb638fb..5aabca54f6 100644
--- a/examples/react/start-supabase-basic/app/routes/__root.tsx
+++ b/examples/react/start-supabase-basic/app/routes/__root.tsx
@@ -1,6 +1,12 @@
-import { Link, Outlet, createRootRoute } from '@tanstack/react-router'
+import {
+ HeadContent,
+ Link,
+ Outlet,
+ Scripts,
+ createRootRoute,
+} from '@tanstack/react-router'
import { TanStackRouterDevtools } from '@tanstack/router-devtools'
-import { Meta, Scripts, createServerFn } from '@tanstack/start'
+import { createServerFn } from '@tanstack/start'
import * as React from 'react'
import { DefaultCatchBoundary } from '../components/DefaultCatchBoundary'
import { NotFound } from '../components/NotFound'
@@ -90,7 +96,7 @@ function RootDocument({ children }: { children: React.ReactNode }) {
return (
-
+
diff --git a/examples/react/start-trellaux/app/routes/__root.tsx b/examples/react/start-trellaux/app/routes/__root.tsx
index 33080e61db..0e099bff6f 100644
--- a/examples/react/start-trellaux/app/routes/__root.tsx
+++ b/examples/react/start-trellaux/app/routes/__root.tsx
@@ -1,12 +1,13 @@
import { ReactQueryDevtools } from '@tanstack/react-query-devtools/production'
import {
+ HeadContent,
Link,
Outlet,
+ Scripts,
createRootRouteWithContext,
useRouterState,
} from '@tanstack/react-router'
import { TanStackRouterDevtools } from '@tanstack/router-devtools'
-import { Meta, Scripts } from '@tanstack/start'
import * as React from 'react'
import { Toaster } from 'react-hot-toast'
import type { QueryClient } from '@tanstack/react-query'
@@ -81,7 +82,7 @@ function RootDocument({ children }: { children: React.ReactNode }) {
return (
-
+
diff --git a/packages/create-start/src/modules/core/template/app/routes/__root.tsx b/packages/create-start/src/modules/core/template/app/routes/__root.tsx
index 4483c0873c..046200b232 100644
--- a/packages/create-start/src/modules/core/template/app/routes/__root.tsx
+++ b/packages/create-start/src/modules/core/template/app/routes/__root.tsx
@@ -1,7 +1,11 @@
// @ts-nocheck
-import { Outlet, createRootRoute } from '@tanstack/react-router'
-import { Meta, Scripts } from '@tanstack/start'
+import {
+ HeadContent,
+ Outlet,
+ Scripts,
+ createRootRoute,
+} from '@tanstack/react-router'
import type { ReactNode } from 'react'
export const Route = createRootRoute({
@@ -34,7 +38,7 @@ function RootDocument({ children }: Readonly<{ children: ReactNode }>) {
return (
-
+
{children}
diff --git a/packages/eslint-plugin-router/src/__tests__/create-route-property-order.rule.test.ts b/packages/eslint-plugin-router/src/__tests__/create-route-property-order.rule.test.ts
index 227530ad2a..b3c264e1d1 100644
--- a/packages/eslint-plugin-router/src/__tests__/create-route-property-order.rule.test.ts
+++ b/packages/eslint-plugin-router/src/__tests__/create-route-property-order.rule.test.ts
@@ -175,7 +175,7 @@ invalidTestMatrix.push({
invalidTestMatrix.push({
createRouteFunction: 'createFileRoute',
- properties: { invalid: ['meta', 'loader'], valid: ['loader', 'meta'] },
+ properties: { invalid: ['head', 'loader'], valid: ['loader', 'head'] },
})
const invalidTestCases = invalidTestMatrix.map(
diff --git a/packages/eslint-plugin-router/src/rules/create-route-property-order/constants.ts b/packages/eslint-plugin-router/src/rules/create-route-property-order/constants.ts
index dcead43347..bc5b367485 100644
--- a/packages/eslint-plugin-router/src/rules/create-route-property-order/constants.ts
+++ b/packages/eslint-plugin-router/src/rules/create-route-property-order/constants.ts
@@ -28,8 +28,7 @@ export const sortRules = [
'onEnter',
'onStay',
'onLeave',
- 'meta',
- 'links',
+ 'head',
'scripts',
'headers',
'remountDeps',
diff --git a/packages/start-client/src/Asset.tsx b/packages/react-router/src/Asset.tsx
similarity index 88%
rename from packages/start-client/src/Asset.tsx
rename to packages/react-router/src/Asset.tsx
index a17033765a..f825748fff 100644
--- a/packages/start-client/src/Asset.tsx
+++ b/packages/react-router/src/Asset.tsx
@@ -1,5 +1,4 @@
-/* eslint-disable @eslint-react/dom/no-dangerously-set-innerhtml */
-import type { RouterManagedTag } from '@tanstack/react-router'
+import type { RouterManagedTag } from '@tanstack/router-core'
export function Asset({ tag, attrs, children }: RouterManagedTag): any {
switch (tag) {
diff --git a/packages/react-router/src/HeadContent.tsx b/packages/react-router/src/HeadContent.tsx
new file mode 100644
index 0000000000..638dee8acc
--- /dev/null
+++ b/packages/react-router/src/HeadContent.tsx
@@ -0,0 +1,151 @@
+import * as React from 'react'
+import { Asset } from './Asset'
+import { useRouter } from './useRouter'
+import { useRouterState } from './useRouterState'
+import type { RouterManagedTag } from '@tanstack/router-core'
+
+export const useTags = () => {
+ const router = useRouter()
+
+ const routeMeta = useRouterState({
+ select: (state) => {
+ return state.matches.map((match) => match.meta!).filter(Boolean)
+ },
+ })
+
+ const meta: Array
= React.useMemo(() => {
+ const resultMeta: Array = []
+ const metaByAttribute: Record = {}
+ let title: RouterManagedTag | undefined
+ ;[...routeMeta].reverse().forEach((metas) => {
+ ;[...metas].reverse().forEach((m) => {
+ if (!m) return
+
+ if (m.title) {
+ if (!title) {
+ title = {
+ tag: 'title',
+ children: m.title,
+ }
+ }
+ } else {
+ const attribute = m.name ?? m.property
+ if (attribute) {
+ if (metaByAttribute[attribute]) {
+ return
+ } else {
+ metaByAttribute[attribute] = true
+ }
+ }
+
+ resultMeta.push({
+ tag: 'meta',
+ attrs: {
+ ...m,
+ },
+ })
+ }
+ })
+ })
+
+ if (title) {
+ resultMeta.push(title)
+ }
+
+ resultMeta.reverse()
+
+ return resultMeta
+ }, [routeMeta])
+
+ const links = useRouterState({
+ select: (state) =>
+ state.matches
+ .map((match) => match.links!)
+ .filter(Boolean)
+ .flat(1)
+ .map((link) => ({
+ tag: 'link',
+ attrs: {
+ ...link,
+ },
+ })) as Array,
+ structuralSharing: true as any,
+ })
+
+ const preloadMeta = useRouterState({
+ select: (state) => {
+ const preloadMeta: Array = []
+
+ state.matches
+ .map((match) => router.looseRoutesById[match.routeId]!)
+ .forEach((route) =>
+ router.ssr?.manifest?.routes[route.id]?.preloads
+ ?.filter(Boolean)
+ .forEach((preload) => {
+ preloadMeta.push({
+ tag: 'link',
+ attrs: {
+ rel: 'modulepreload',
+ href: preload,
+ },
+ })
+ }),
+ )
+
+ return preloadMeta
+ },
+ structuralSharing: true as any,
+ })
+
+ const headScripts = useRouterState({
+ select: (state) =>
+ (
+ state.matches
+ .map((match) => match.headScripts!)
+ .flat(1)
+ .filter(Boolean) as Array
+ ).map(({ children, ...script }) => ({
+ tag: 'script',
+ attrs: {
+ ...script,
+ },
+ children,
+ })),
+ structuralSharing: true as any,
+ })
+
+ return uniqBy(
+ [
+ ...meta,
+ ...preloadMeta,
+ ...links,
+ ...headScripts,
+ ] as Array,
+ (d) => {
+ return JSON.stringify(d)
+ },
+ )
+}
+
+/**
+ * @description The `HeadContent` component is used to render meta tags, links, and scripts for the current route.
+ * It should be rendered in the `` of your document.
+ */
+export function HeadContent() {
+ const tags = useTags()
+ return tags.map((tag) => (
+
+ ))
+}
+
+function uniqBy(arr: Array, fn: (item: T) => string) {
+ const seen = new Set()
+ return arr.filter((item) => {
+ const key = fn(item)
+ if (seen.has(key)) {
+ return false
+ }
+ seen.add(key)
+ return true
+ })
+}
diff --git a/packages/react-router/src/Matches.tsx b/packages/react-router/src/Matches.tsx
index 3a68ff8a3c..f827e33831 100644
--- a/packages/react-router/src/Matches.tsx
+++ b/packages/react-router/src/Matches.tsx
@@ -88,6 +88,7 @@ export interface RouteMatch<
meta?: Array
links?: Array
scripts?: Array
+ headScripts?: Array
headers?: Record
globalNotFound?: boolean
staticData: StaticDataRouteOption
diff --git a/packages/react-router/src/Scripts.tsx b/packages/react-router/src/Scripts.tsx
new file mode 100644
index 0000000000..2c28537164
--- /dev/null
+++ b/packages/react-router/src/Scripts.tsx
@@ -0,0 +1,64 @@
+import { Asset } from './Asset'
+import { useRouterState } from './useRouterState'
+import { useRouter } from './useRouter'
+import type { RouterManagedTag } from '@tanstack/router-core'
+
+export const Scripts = () => {
+ const router = useRouter()
+
+ const assetScripts = useRouterState({
+ select: (state) => {
+ const assetScripts: Array = []
+ const manifest = router.ssr?.manifest
+
+ if (!manifest) {
+ return []
+ }
+
+ state.matches
+ .map((match) => router.looseRoutesById[match.routeId]!)
+ .forEach((route) =>
+ manifest.routes[route.id]?.assets
+ ?.filter((d) => d.tag === 'script')
+ .forEach((asset) => {
+ assetScripts.push({
+ tag: 'script',
+ attrs: asset.attrs,
+ children: asset.children,
+ } as any)
+ }),
+ )
+
+ return assetScripts
+ },
+ structuralSharing: true as any,
+ })
+
+ const { scripts } = useRouterState({
+ select: (state) => ({
+ scripts: (
+ state.matches
+ .map((match) => match.scripts!)
+ .flat(1)
+ .filter(Boolean) as Array
+ ).map(({ children, ...script }) => ({
+ tag: 'script',
+ attrs: {
+ ...script,
+ suppressHydrationWarning: true,
+ },
+ children,
+ })),
+ }),
+ })
+
+ const allScripts = [...scripts, ...assetScripts] as Array
+
+ return (
+ <>
+ {allScripts.map((asset, i) => (
+
+ ))}
+ >
+ )
+}
diff --git a/packages/react-router/src/index.tsx b/packages/react-router/src/index.tsx
index 2096e7cebc..1b335cc03c 100644
--- a/packages/react-router/src/index.tsx
+++ b/packages/react-router/src/index.tsx
@@ -356,3 +356,7 @@ export type { NotFoundError } from './not-found'
export * from './typePrimitives'
export { ScriptOnce } from './ScriptOnce'
+
+export { Asset } from './Asset'
+export { HeadContent } from './HeadContent'
+export { Scripts } from './Scripts'
diff --git a/packages/react-router/src/route.ts b/packages/react-router/src/route.ts
index a3753628f7..ffa9d57a41 100644
--- a/packages/react-router/src/route.ts
+++ b/packages/react-router/src/route.ts
@@ -325,6 +325,51 @@ export interface BeforeLoadContextOptions<
>
}
+type AssetFnContextOptions<
+ in out TRouteId,
+ in out TFullPath,
+ in out TParentRoute extends AnyRoute,
+ in out TParams,
+ in out TSearchValidator,
+ in out TLoaderFn,
+ in out TRouterContext,
+ in out TRouteContextFn,
+ in out TBeforeLoadFn,
+ in out TLoaderDeps,
+> = {
+ matches: Array<
+ RouteMatch<
+ TRouteId,
+ TFullPath,
+ ResolveAllParamsFromParent,
+ ResolveFullSearchSchema,
+ ResolveLoaderData,
+ ResolveAllContext<
+ TParentRoute,
+ TRouterContext,
+ TRouteContextFn,
+ TBeforeLoadFn
+ >,
+ TLoaderDeps
+ >
+ >
+ match: RouteMatch<
+ TRouteId,
+ TFullPath,
+ ResolveAllParamsFromParent,
+ ResolveFullSearchSchema,
+ ResolveLoaderData,
+ ResolveAllContext<
+ TParentRoute,
+ TRouterContext,
+ TRouteContextFn,
+ TBeforeLoadFn
+ >,
+ TLoaderDeps
+ >
+ params: ResolveAllParamsFromParent
+ loaderData: ResolveLoaderData
+}
export interface UpdatableRouteOptions<
in out TParentRoute extends AnyRoute,
in out TRouteId,
@@ -427,44 +472,38 @@ export interface UpdatableRouteOptions<
headers?: (ctx: {
loaderData: ResolveLoaderData
}) => Record
- head?: (ctx: {
- matches: Array<
- RouteMatch<
- TRouteId,
- TFullPath,
- ResolveAllParamsFromParent,
- ResolveFullSearchSchema,
- ResolveLoaderData,
- ResolveAllContext<
- TParentRoute,
- TRouterContext,
- TRouteContextFn,
- TBeforeLoadFn
- >,
- TLoaderDeps
- >
- >
- match: RouteMatch<
+ head?: (
+ ctx: AssetFnContextOptions<
TRouteId,
TFullPath,
- ResolveAllParamsFromParent,
- ResolveFullSearchSchema,
- ResolveLoaderData,
- ResolveAllContext<
- TParentRoute,
- TRouterContext,
- TRouteContextFn,
- TBeforeLoadFn
- >,
+ TParentRoute,
+ TParams,
+ TSearchValidator,
+ TLoaderFn,
+ TRouterContext,
+ TRouteContextFn,
+ TBeforeLoadFn,
TLoaderDeps
- >
- params: ResolveAllParamsFromParent
- loaderData: ResolveLoaderData
- }) => {
+ >,
+ ) => {
links?: AnyRouteMatch['links']
- scripts?: AnyRouteMatch['scripts']
+ scripts?: AnyRouteMatch['headScripts']
meta?: AnyRouteMatch['meta']
}
+ scripts?: (
+ ctx: AssetFnContextOptions<
+ TRouteId,
+ TFullPath,
+ TParentRoute,
+ TParams,
+ TSearchValidator,
+ TLoaderFn,
+ TRouterContext,
+ TRouteContextFn,
+ TBeforeLoadFn,
+ TLoaderDeps
+ >,
+ ) => AnyRouteMatch['scripts']
ssr?: boolean
codeSplitGroupings?: Array<
Array<
diff --git a/packages/react-router/src/router.ts b/packages/react-router/src/router.ts
index 401a27bc3f..1786a28ecb 100644
--- a/packages/react-router/src/router.ts
+++ b/packages/react-router/src/router.ts
@@ -1311,6 +1311,7 @@ export class Router<
preload: false,
links: undefined,
scripts: undefined,
+ headScripts: undefined,
meta: undefined,
staticData: route.options.staticData || {},
loadPromise: createControlledPromise(),
@@ -1378,16 +1379,17 @@ export class Router<
match.headers = route.options.headers?.({
loaderData: match.loaderData,
})
- const headFnContent = route.options.head?.({
+ const assetContext = {
matches,
match,
params: match.params,
loaderData: match.loaderData,
- })
-
+ }
+ const headFnContent = route.options.head?.(assetContext)
match.links = headFnContent?.links
- match.scripts = headFnContent?.scripts
+ match.headScripts = headFnContent?.scripts
match.meta = headFnContent?.meta
+ match.scripts = route.options.scripts?.(assetContext)
}
})
@@ -2557,16 +2559,19 @@ export class Router<
await potentialPendingMinPromise()
- const headFnContent = route.options.head?.({
+ const assetContext = {
matches,
match: this.getMatch(matchId)!,
params: this.getMatch(matchId)!.params,
loaderData,
- })
+ }
+ const headFnContent =
+ route.options.head?.(assetContext)
const meta = headFnContent?.meta
const links = headFnContent?.links
- const scripts = headFnContent?.scripts
+ const headScripts = headFnContent?.scripts
+ const scripts = route.options.scripts?.(assetContext)
const headers = route.options.headers?.({
loaderData,
})
@@ -2580,8 +2585,9 @@ export class Router<
loaderData,
meta,
links,
- scripts,
+ headScripts,
headers,
+ scripts,
}))
} catch (e) {
let error = e
diff --git a/packages/start-client/src/tests/index.test.tsx b/packages/react-router/tests/Scripts.test.tsx
similarity index 85%
rename from packages/start-client/src/tests/index.test.tsx
rename to packages/react-router/tests/Scripts.test.tsx
index 871160005e..24805a5079 100644
--- a/packages/start-client/src/tests/index.test.tsx
+++ b/packages/react-router/tests/Scripts.test.tsx
@@ -9,8 +9,8 @@ import {
createRoute,
createRouter,
} from '@tanstack/react-router'
-
-import { Meta, Scripts } from '../index'
+import { Scripts } from '../src/Scripts'
+import { HeadContent } from '../src'
describe('ssr scripts', () => {
test('it works', async () => {
@@ -59,29 +59,19 @@ describe('ssr scripts', () => {
await router.load()
- expect(router.state.matches.map((d) => d.scripts).flat(1)).toEqual([
+ expect(router.state.matches.map((d) => d.headScripts).flat(1)).toEqual([
{ src: 'script.js' },
{ src: 'script2.js' },
{ src: 'script3.js' },
])
-
- const { container } = render()
-
- expect(container.innerHTML).toEqual(
- ``,
- )
})
test('excludes `undefined` script values', async () => {
const rootRoute = createRootRoute({
- head: () => {
- return {
- scripts: [
- { src: 'script.js' },
- undefined, // 'script2.js' opted out by certain conditions, such as `NODE_ENV=production`.
- ],
- }
- },
+ scripts: () => [
+ { src: 'script.js' },
+ undefined, // 'script2.js' opted out by certain conditions, such as `NODE_ENV=production`.
+ ],
component: () => {
return
},
@@ -90,11 +80,7 @@ describe('ssr scripts', () => {
const indexRoute = createRoute({
path: '/',
getParentRoute: () => rootRoute,
- head: () => {
- return {
- scripts: [{ src: 'script3.js' }],
- }
- },
+ scripts: () => [{ src: 'script3.js' }],
})
const router = createRouter({
@@ -122,8 +108,8 @@ describe('ssr scripts', () => {
})
})
-describe('ssr meta', () => {
- test('derives title, dedupes meta, and allows non-loader meta', async () => {
+describe('ssr HeadContent', () => {
+ test('derives title, dedupes meta, and allows non-loader HeadContent', async () => {
const rootRoute = createRootRoute({
loader: () =>
new Promise((r) => setTimeout(r, 1)).then(() => ({
@@ -155,7 +141,7 @@ describe('ssr meta', () => {
}
},
component: () => {
- return
+ return
},
})
diff --git a/packages/react-router/tests/route.test.tsx b/packages/react-router/tests/route.test.tsx
index 91bcb94800..3a7cbee0bc 100644
--- a/packages/react-router/tests/route.test.tsx
+++ b/packages/react-router/tests/route.test.tsx
@@ -292,7 +292,7 @@ describe('route.head', () => {
const indexElem = await screen.findByText('Index')
expect(indexElem).toBeInTheDocument()
- const scriptsState = router.state.matches.map((m) => m.scripts)
+ const scriptsState = router.state.matches.map((m) => m.headScripts)
expect(scriptsState).toEqual([
[{ src: 'root.js' }, { src: 'root2.js' }],
[{ src: 'index.js' }],
@@ -322,7 +322,7 @@ describe('route.head', () => {
const indexElem = await screen.findByText('Index')
expect(indexElem).toBeInTheDocument()
- const scriptsState = router.state.matches.map((m) => m.scripts)
+ const scriptsState = router.state.matches.map((m) => m.headScripts)
expect(scriptsState).toEqual([
[{ src: 'root.js' }, { src: 'root2.js' }],
[{ src: 'index.js' }],
diff --git a/packages/start-client/package.json b/packages/start-client/package.json
index ac11b36155..3147d0f94d 100644
--- a/packages/start-client/package.json
+++ b/packages/start-client/package.json
@@ -27,6 +27,7 @@
"clean": "rimraf ./dist && rimraf ./coverage",
"test": "pnpm test:deps && pnpm test:eslint && pnpm test:types && pnpm test:build && pnpm test:unit",
"test:unit": "vitest",
+ "test:unit:dev": "vitest --watch",
"test:eslint": "eslint ./src",
"test:types": "pnpm run \"/^test:types:ts[0-9]{2}$/\"",
"test:types:ts52": "node ../../node_modules/typescript52/lib/tsc.js",
diff --git a/packages/start-client/src/Meta.tsx b/packages/start-client/src/Meta.tsx
index ebfa96b811..6a91ca59ef 100644
--- a/packages/start-client/src/Meta.tsx
+++ b/packages/start-client/src/Meta.tsx
@@ -1,137 +1,10 @@
-import { useRouter, useRouterState } from '@tanstack/react-router'
-import * as React from 'react'
-import { Asset } from './Asset'
-import type { RouterManagedTag } from '@tanstack/react-router'
+import { HeadContent } from '@tanstack/react-router'
-export const useMeta = () => {
- const router = useRouter()
-
- const routeMeta = useRouterState({
- select: (state) => {
- return state.matches.map((match) => match.meta!).filter(Boolean)
- },
- })
-
- const meta: Array = React.useMemo(() => {
- const resultMeta: Array = []
- const metaByAttribute: Record = {}
- let title: RouterManagedTag | undefined
- ;[...routeMeta].reverse().forEach((metas) => {
- ;[...metas].reverse().forEach((m) => {
- if (!m) return
-
- if (m.title) {
- if (!title) {
- title = {
- tag: 'title',
- children: m.title,
- }
- }
- } else {
- const attribute = m.name ?? m.property
- if (attribute) {
- if (metaByAttribute[attribute]) {
- return
- } else {
- metaByAttribute[attribute] = true
- }
- }
-
- resultMeta.push({
- tag: 'meta',
- attrs: {
- ...m,
- },
- })
- }
- })
- })
-
- if (title) {
- resultMeta.push(title)
- }
-
- resultMeta.reverse()
-
- return resultMeta
- }, [routeMeta])
-
- const links = useRouterState({
- select: (state) =>
- state.matches
- .map((match) => match.links!)
- .filter(Boolean)
- .flat(1)
- .map((link) => ({
- tag: 'link',
- attrs: {
- ...link,
- },
- })) as Array,
- structuralSharing: true as any,
- })
-
- const preloadMeta = useRouterState({
- select: (state) => {
- const preloadMeta: Array = []
-
- state.matches
- .map((match) => router.looseRoutesById[match.routeId]!)
- .forEach((route) =>
- router.ssr?.manifest?.routes[route.id]?.preloads
- ?.filter(Boolean)
- .forEach((preload) => {
- preloadMeta.push({
- tag: 'link',
- attrs: {
- rel: 'modulepreload',
- href: preload,
- },
- })
- }),
- )
-
- return preloadMeta
- },
- structuralSharing: true as any,
- })
-
- return uniqBy(
- [...meta, ...preloadMeta, ...links] as Array,
- (d) => {
- return JSON.stringify(d)
- },
- )
-}
-
-export const useMetaElements = () => {
- const meta = useMeta()
-
- return (
- <>
- {meta.map((asset) => (
-
- ))}
- >
- )
-}
-
-/**
- * @description The `Meta` component is used to render meta tags and links for the current route.
- * It should be rendered in the `` of your document.
- */
export const Meta = () => {
- return <>{useMetaElements()}>
-}
-
-function uniqBy(arr: Array, fn: (item: T) => string) {
- const seen = new Set()
- return arr.filter((item) => {
- const key = fn(item)
- if (seen.has(key)) {
- return false
- }
- seen.add(key)
- return true
- })
+ if (process.env.NODE_ENV === 'development') {
+ console.warn(
+ 'The Meta component is deprecated. Use `HeadContent` from `@tanstack/react-router` instead.',
+ )
+ }
+ return
}
diff --git a/packages/start-client/src/RouterManagedTag.tsx b/packages/start-client/src/RouterManagedTag.tsx
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/packages/start-client/src/Scripts.tsx b/packages/start-client/src/Scripts.tsx
index 9865f1db2b..9db5f5c53e 100644
--- a/packages/start-client/src/Scripts.tsx
+++ b/packages/start-client/src/Scripts.tsx
@@ -1,65 +1,8 @@
-import { useRouter, useRouterState, warning } from '@tanstack/react-router'
-import { Asset } from './Asset'
-import type { RouterManagedTag } from '@tanstack/react-router'
+import { Scripts as RouterScripts } from '@tanstack/react-router'
export const Scripts = () => {
- const router = useRouter()
-
- const assetScripts = useRouterState({
- select: (state) => {
- const assetScripts: Array = []
- const manifest = router.ssr?.manifest
-
- if (!manifest) {
- warning(false, ' found no manifest')
- return []
- }
-
- state.matches
- .map((match) => router.looseRoutesById[match.routeId]!)
- .forEach((route) =>
- manifest.routes[route.id]?.assets
- ?.filter((d) => d.tag === 'script')
- .forEach((asset) => {
- assetScripts.push({
- tag: 'script',
- attrs: asset.attrs,
- children: asset.children,
- } as any)
- }),
- )
-
- return assetScripts
- },
- structuralSharing: true as any,
- })
-
- const { scripts } = useRouterState({
- select: (state) => ({
- scripts: (
- state.matches
- .map((match) => match.scripts!)
- .flat(1)
- .filter(Boolean) as Array
- ).map(({ children, ...script }) => ({
- tag: 'script',
- attrs: {
- ...script,
- suppressHydrationWarning: true,
- },
- children,
- })),
- }),
- })
-
- const allScripts = [...scripts, ...assetScripts] as Array
-
- return (
- <>
- {allScripts.map((asset, i) => (
- // eslint-disable-next-line @eslint-react/no-array-index-key
-
- ))}
- >
- )
+ if (process.env.NODE_ENV === 'development') {
+ console.warn('The Scripts component was moved to `@tanstack/react-router`')
+ }
+ return
}
diff --git a/packages/start-client/src/index.tsx b/packages/start-client/src/index.tsx
index 81c1525605..736b4892bc 100644
--- a/packages/start-client/src/index.tsx
+++ b/packages/start-client/src/index.tsx
@@ -1,5 +1,4 @@
///
-export { Asset } from './Asset'
export {
createIsomorphicFn,
type IsomorphicFn,
diff --git a/packages/start-client/src/ssr-client.tsx b/packages/start-client/src/ssr-client.tsx
index e0103638d7..572082a39b 100644
--- a/packages/start-client/src/ssr-client.tsx
+++ b/packages/start-client/src/ssr-client.tsx
@@ -168,18 +168,20 @@ export function hydrate(router: AnyRouter) {
})
}
- const headFnContent = route.options.head?.({
+ const assetContext = {
matches: router.state.matches,
match,
params: match.params,
loaderData: match.loaderData,
- })
+ }
+ const headFnContent = route.options.head?.(assetContext)
+
+ const scripts = route.options.scripts?.(assetContext)
- Object.assign(match, {
- meta: headFnContent?.meta,
- links: headFnContent?.links,
- scripts: headFnContent?.scripts,
- })
+ match.meta = headFnContent?.meta
+ match.links = headFnContent?.links
+ match.headScripts = headFnContent?.scripts
+ match.scripts = scripts
return match
})
diff --git a/packages/start-router-manifest/src/index.ts b/packages/start-router-manifest/src/index.ts
index 9c03f9ca34..d7df91cb57 100644
--- a/packages/start-router-manifest/src/index.ts
+++ b/packages/start-router-manifest/src/index.ts
@@ -1,6 +1,7 @@
// @ts-expect-error
import tsrGetManifest from 'tsr:routes-manifest'
import { getManifest } from 'vinxi/manifest'
+import { invariant } from '@tanstack/react-router'
import type { Manifest } from '@tanstack/react-router'
function sanitizeBase(base: string) {
@@ -44,14 +45,21 @@ window.__vite_plugin_react_preamble_installed__ = true`,
// Get the entry for the client from vinxi
const vinxiClientManifest = getManifest('client')
+ const importPath =
+ vinxiClientManifest.inputs[vinxiClientManifest.handler]?.output.path
+ if (!importPath) {
+ invariant(importPath, 'Could not find client entry in vinxi manifest')
+ }
+
+ const initScript = `import("${importPath}")`
rootRoute.assets.push({
tag: 'script',
attrs: {
- src: vinxiClientManifest.inputs[vinxiClientManifest.handler]?.output.path,
type: 'module',
suppressHydrationWarning: true,
async: true,
},
+ children: initScript,
})
return routerManifest