import * as React from 'react' import { useStore } from '@tanstack/react-store' import { deepEqual, escapeHtml, getAssetCrossOrigin, resolveManifestAssetLink, } from '@tanstack/router-core' import { isServer } from '@tanstack/router-core/isServer' import { useRouter } from './useRouter' import type { AssetCrossOriginConfig, RouterManagedTag, } from '@tanstack/router-core' function buildTagsFromMatches( router: ReturnType, nonce: string | undefined, matches: Array, assetCrossOrigin?: AssetCrossOriginConfig, ): Array { const routeMeta = matches.map((match) => match.meta!).filter(Boolean) const resultMeta: Array = [] const metaByAttribute: Record = {} 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--) { const m = metas[j] if (!m) continue if (m.title) { if (!title) { title = { tag: 'title', children: m.title, } } } else if ('script:ld+json' in m) { try { const json = JSON.stringify(m['script:ld+json']) resultMeta.push({ tag: 'script', attrs: { type: 'application/ld+json', }, children: escapeHtml(json), }) } catch { // Skip invalid JSON-LD objects } } else { const attribute = m.name ?? m.property if (attribute) { if (metaByAttribute[attribute]) { continue } else { metaByAttribute[attribute] = true } } resultMeta.push({ tag: 'meta', attrs: { ...m, nonce, }, }) } } } if (title) { resultMeta.push(title) } if (nonce) { resultMeta.push({ tag: 'meta', attrs: { property: 'csp-nonce', content: nonce, }, }) } resultMeta.reverse() const constructedLinks = matches .map((match) => match.links!) .filter(Boolean) .flat(1) .map((link) => ({ tag: 'link', attrs: { ...link, nonce, }, })) satisfies Array const manifest = router.ssr?.manifest const assetLinks = matches .map((match) => manifest?.routes[match.routeId]?.assets ?? []) .filter(Boolean) .flat(1) .filter((asset) => asset.tag === 'link') .map( (asset) => ({ tag: 'link', attrs: { ...asset.attrs, crossOrigin: getAssetCrossOrigin(assetCrossOrigin, 'stylesheet') ?? asset.attrs?.crossOrigin, suppressHydrationWarning: true, nonce, }, }) satisfies RouterManagedTag, ) const preloadLinks: Array = [] matches .map((match) => router.looseRoutesById[match.routeId]!) .forEach((route) => router.ssr?.manifest?.routes[route.id]?.preloads ?.filter(Boolean) .forEach((preload) => { const preloadLink = resolveManifestAssetLink(preload) preloadLinks.push({ tag: 'link', attrs: { rel: 'modulepreload', href: preloadLink.href, crossOrigin: getAssetCrossOrigin(assetCrossOrigin, 'modulepreload') ?? preloadLink.crossOrigin, nonce, }, }) }), ) const styles = ( matches .map((match) => match.styles!) .flat(1) .filter(Boolean) as Array ).map(({ children, ...attrs }) => ({ tag: 'style', attrs: { ...attrs, nonce, }, children, })) const headScripts = ( matches .map((match) => match.headScripts!) .flat(1) .filter(Boolean) as Array ).map(({ children, ...script }) => ({ tag: 'script', attrs: { ...script, nonce, }, children, })) return uniqBy( [ ...resultMeta, ...preloadLinks, ...constructedLinks, ...assetLinks, ...styles, ...headScripts, ] as Array, (d) => JSON.stringify(d), ) } /** * Build the list of head/link/meta/script tags to render for active matches. * Used internally by `HeadContent`. */ export const useTags = (assetCrossOrigin?: AssetCrossOriginConfig) => { const router = useRouter() const nonce = router.options.ssr?.nonce if (isServer ?? router.isServer) { return buildTagsFromMatches( router, nonce, router.stores.matches.get(), assetCrossOrigin, ) } // eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static const routeMeta = useStore( router.stores.matches, (matches) => { return matches.map((match) => match.meta!).filter(Boolean) }, deepEqual, ) // eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static const meta: Array = React.useMemo(() => { const resultMeta: Array = [] const metaByAttribute: Record = {} 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--) { const m = metas[j] if (!m) continue if (m.title) { if (!title) { title = { tag: 'title', children: m.title, } } } else if ('script:ld+json' in m) { // Handle JSON-LD structured data // Content is HTML-escaped to prevent XSS when injected via dangerouslySetInnerHTML try { const json = JSON.stringify(m['script:ld+json']) resultMeta.push({ tag: 'script', attrs: { type: 'application/ld+json', }, children: escapeHtml(json), }) } catch { // Skip invalid JSON-LD objects } } else { const attribute = m.name ?? m.property if (attribute) { if (metaByAttribute[attribute]) { continue } else { metaByAttribute[attribute] = true } } resultMeta.push({ tag: 'meta', attrs: { ...m, nonce, }, }) } } } if (title) { resultMeta.push(title) } if (nonce) { resultMeta.push({ tag: 'meta', attrs: { property: 'csp-nonce', content: nonce, }, }) } resultMeta.reverse() return resultMeta }, [routeMeta, nonce]) // eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static const links = useStore( router.stores.matches, (matches) => { const constructed = matches .map((match) => match.links!) .filter(Boolean) .flat(1) .map((link) => ({ tag: 'link', attrs: { ...link, nonce, }, })) satisfies Array const manifest = router.ssr?.manifest // These are the assets extracted from the ViteManifest // using the `startManifestPlugin` const assets = matches .map((match) => manifest?.routes[match.routeId]?.assets ?? []) .filter(Boolean) .flat(1) .filter((asset) => asset.tag === 'link') .map( (asset) => ({ tag: 'link', attrs: { ...asset.attrs, crossOrigin: getAssetCrossOrigin(assetCrossOrigin, 'stylesheet') ?? asset.attrs?.crossOrigin, suppressHydrationWarning: true, nonce, }, }) satisfies RouterManagedTag, ) return [...constructed, ...assets] }, deepEqual, ) // eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static const preloadLinks = useStore( router.stores.matches, (matches) => { const preloadLinks: Array = [] matches .map((match) => router.looseRoutesById[match.routeId]!) .forEach((route) => router.ssr?.manifest?.routes[route.id]?.preloads ?.filter(Boolean) .forEach((preload) => { const preloadLink = resolveManifestAssetLink(preload) preloadLinks.push({ tag: 'link', attrs: { rel: 'modulepreload', href: preloadLink.href, crossOrigin: getAssetCrossOrigin(assetCrossOrigin, 'modulepreload') ?? preloadLink.crossOrigin, nonce, }, }) }), ) return preloadLinks }, deepEqual, ) // eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static const styles = useStore( router.stores.matches, (matches) => ( matches .map((match) => match.styles!) .flat(1) .filter(Boolean) as Array ).map(({ children, ...attrs }) => ({ tag: 'style', attrs: { ...attrs, nonce, }, children, })), deepEqual, ) // eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static const headScripts: Array = useStore( router.stores.matches, (matches) => ( matches .map((match) => match.headScripts!) .flat(1) .filter(Boolean) as Array ).map(({ children, ...script }) => ({ tag: 'script', attrs: { ...script, nonce, }, children, })), deepEqual, ) return uniqBy( [ ...meta, ...preloadLinks, ...links, ...styles, ...headScripts, ] as Array, (d) => { return JSON.stringify(d) }, ) } export 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 }) }