Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions packages/react-router/src/Matches.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,24 @@ declare module '@tanstack/router-core' {
export interface RouteMatchExtensions {
meta?: Array<React.JSX.IntrinsicElements['meta'] | undefined>
links?: Array<React.JSX.IntrinsicElements['link'] | undefined>
scripts?: Array<React.JSX.IntrinsicElements['script'] | undefined>
styles?: Array<React.JSX.IntrinsicElements['style'] | undefined>
headScripts?: Array<React.JSX.IntrinsicElements['script'] | undefined>
scripts?: Array<
| (React.JSX.IntrinsicElements['script'] & {
children?: string | null | boolean | number | undefined
})
| undefined
>
styles?: Array<
| (React.JSX.IntrinsicElements['style'] & {
children?: string | null | boolean | number | undefined
})
| undefined
>
headScripts?: Array<
| (React.JSX.IntrinsicElements['script'] & {
children?: string | null | boolean | number | undefined
})
| undefined
>
}
}

Expand Down
287 changes: 167 additions & 120 deletions packages/react-router/src/headContentUtils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,25 @@ export const useTags = () => {
const nonce = router.options.ssr?.nonce
const routeMeta = useRouterState({
select: (state) => {
return state.matches.map((match) => match.meta!).filter(Boolean)
const result = []
for (const match of state.matches) {
const meta = match.meta
if (!meta) continue
result.push(meta)
}
return result
},
structuralSharing: true as any,
})

const meta: Array<RouterManagedTag> = React.useMemo(() => {
const resultMeta: Array<RouterManagedTag> = []
const metaByAttribute: Record<string, true> = {}
// Process routeMeta into separate arrays for each tag type
const { title, ldJsonScripts, metaTags } = React.useMemo(() => {
const ldJsonScripts: Array<RouterManagedTag> = []
const seenLdJson = new Set<string>()
const metaTags: Array<RouterManagedTag> = []
const seenMeta = new Set<string>()
let title: RouterManagedTag | undefined

for (let i = routeMeta.length - 1; i >= 0; i--) {
const metas = routeMeta[i]!
for (let j = metas.length - 1; j >= 0; j--) {
Expand All @@ -38,8 +49,11 @@ export const useTags = () => {
// Handle JSON-LD structured data
// Content is HTML-escaped to prevent XSS when injected via dangerouslySetInnerHTML
try {
// Deduplicate by JSON content before creating the object
const json = JSON.stringify(m['script:ld+json'])
resultMeta.push({
if (seenLdJson.has(json)) continue
seenLdJson.add(json)
ldJsonScripts.push({
tag: 'script',
attrs: {
type: 'application/ld+json',
Expand All @@ -50,16 +64,14 @@ export const useTags = () => {
// Skip invalid JSON-LD objects
}
} else {
const attribute = m.name ?? m.property
if (attribute) {
if (metaByAttribute[attribute]) {
continue
} else {
metaByAttribute[attribute] = true
}
// Deduplicate
const key = `${m.name ?? m.property}\0${m.content}\0${m.media}`
if (key) {
if (seenMeta.has(key)) continue
seenMeta.add(key)
}

resultMeta.push({
metaTags.push({
tag: 'meta',
attrs: {
...m,
Expand All @@ -70,148 +82,183 @@ export const useTags = () => {
}
}

if (title) {
resultMeta.push(title)
}

if (nonce) {
resultMeta.push({
// Add CSP nonce meta tag if present
if (nonce && !seenMeta.has('csp-nonce')) {
metaTags.push({
tag: 'meta',
attrs: {
property: 'csp-nonce',
content: nonce,
},
})
}
resultMeta.reverse()

return resultMeta
// Reverse to restore original order (we iterated backwards for deduplication)
ldJsonScripts.reverse()
metaTags.reverse()

return { title, ldJsonScripts, metaTags }
}, [routeMeta, nonce])

const links = useRouterState({
select: (state) => {
const constructed = state.matches
.map((match) => match.links!)
.filter(Boolean)
.flat(1)
.map((link) => ({
tag: 'link',
attrs: {
...link,
nonce,
},
})) satisfies Array<RouterManagedTag>

const result: Array<RouterManagedTag> = []
const seen = new Set<string>()
const manifest = router.ssr?.manifest

// These are the assets extracted from the ViteManifest
// using the `startManifestPlugin`
const assets = state.matches
.map((match) => manifest?.routes[match.routeId]?.assets ?? [])
.filter(Boolean)
.flat(1)
.filter((asset) => asset.tag === 'link')
.map(
(asset) =>
({
for (const match of state.matches) {
// Process constructed links from match.links
const matchLinks = match.links
if (matchLinks) {
for (const link of matchLinks) {
if (!link) continue
// Deduplicate
const key = `${link.rel}\0${link.href}\0${link.media}\0${link.type}\0${link.as}`
if (seen.has(key)) continue
seen.add(key)
result.push({
tag: 'link',
attrs: {
...link,
nonce,
},
})
}
}

// Process assets from manifest
const assets = manifest?.routes[match.routeId]?.assets
if (assets) {
for (const asset of assets) {
if (asset.tag !== 'link') continue
const attrs = asset.attrs
if (!attrs) continue
// Deduplicate
const key = `${attrs.rel}\0${attrs.href}\0${attrs.media}\0${attrs.type}\0${attrs.as}`
if (seen.has(key)) continue
seen.add(key)
result.push({
tag: 'link',
attrs: {
...asset.attrs,
...attrs,
suppressHydrationWarning: true,
nonce,
},
}) satisfies RouterManagedTag,
)
})
}
}
}

return [...constructed, ...assets]
return result
},
structuralSharing: true as any,
})

const preloadLinks = useRouterState({
select: (state) => {
const preloadLinks: Array<RouterManagedTag> = []

state.matches
.map((match) => router.looseRoutesById[match.routeId]!)
.forEach((route) =>
router.ssr?.manifest?.routes[route.id]?.preloads
?.filter(Boolean)
.forEach((preload) => {
preloadLinks.push({
tag: 'link',
attrs: {
rel: 'modulepreload',
href: preload,
nonce,
},
})
}),
)

return preloadLinks
const result: Array<RouterManagedTag> = []
const seen = new Set<string>()
const manifest = router.ssr?.manifest

for (const match of state.matches) {
const route = router.looseRoutesById[match.routeId]
if (!route) continue

const preloads = manifest?.routes[route.id]?.preloads
if (!preloads) continue

for (const preload of preloads) {
if (!preload) continue
// Deduplicate by href before creating the object
if (seen.has(preload)) continue
seen.add(preload)
result.push({
tag: 'link',
attrs: {
rel: 'modulepreload',
href: preload,
nonce,
},
})
}
}

return result
},
structuralSharing: true as any,
})

const styles = useRouterState({
select: (state) =>
(
state.matches
.map((match) => match.styles!)
.flat(1)
.filter(Boolean) as Array<RouterManagedTag>
).map(({ children, ...attrs }) => ({
tag: 'style',
attrs: {
...attrs,
nonce,
},
children,
})),
structuralSharing: true as any,
})
select: (state) => {
const result: Array<RouterManagedTag> = []
const seen = new Set<string>()

const headScripts: Array<RouterManagedTag> = useRouterState({
select: (state) =>
(
state.matches
.map((match) => match.headScripts!)
.flat(1)
.filter(Boolean) as Array<RouterManagedTag>
).map(({ children, ...script }) => ({
tag: 'script',
attrs: {
...script,
nonce,
},
children,
})),
for (const match of state.matches) {
const matchStyles = match.styles
if (!matchStyles) continue

for (const style of matchStyles) {
if (!style) continue
// Deduplicate
const { children, ...attrs } = style
const key = `${attrs.media}\0${children}`
if (seen.has(key)) continue
seen.add(key)
result.push({
tag: 'style',
attrs: {
...attrs,
nonce,
},
children: children as string | undefined,
})
}
}

return result
},
structuralSharing: true as any,
})

return uniqBy(
[
...meta,
...preloadLinks,
...links,
...styles,
...headScripts,
] as Array<RouterManagedTag>,
(d) => {
return JSON.stringify(d)
},
)
}
const headScripts = useRouterState({
select: (state) => {
const result: Array<RouterManagedTag> = []
const seen = new Set<string>()

export function uniqBy<T>(arr: Array<T>, fn: (item: T) => string) {
const seen = new Set<string>()
return arr.filter((item) => {
const key = fn(item)
if (seen.has(key)) {
return false
}
seen.add(key)
return true
for (const match of state.matches) {
const matchScripts = match.headScripts
if (!matchScripts) continue

for (const script of matchScripts) {
if (!script) continue
// Deduplicate
const { children, ...attrs } = script
const key = `${attrs.src}\0${attrs.type}\0${children}`
if (seen.has(key)) continue
seen.add(key)
result.push({
tag: 'script',
attrs: {
...attrs,
nonce,
},
children: children as string | undefined,
})
}
}

return result
},
structuralSharing: true as any,
})

return [
...(title ? [title] : []),
...metaTags,
...ldJsonScripts,
...preloadLinks,
...links,
...styles,
...headScripts,
] as Array<RouterManagedTag>
}
Loading
Loading