11import * as React from 'react'
2- import { buildDevStylesUrl , escapeHtml } from '@tanstack/router-core'
32import { Asset } from './Asset'
43import { useRouter } from './useRouter'
5- import { useRouterState } from './useRouterState'
6- import type { RouterManagedTag } from '@tanstack/router-core'
7-
8- /**
9- * Build the list of head/link/meta/script tags to render for active matches.
10- * Used internally by `HeadContent`.
11- */
12- export const useTags = ( ) => {
13- const router = useRouter ( )
14- const nonce = router . options . ssr ?. nonce
15- const routeMeta = useRouterState ( {
16- select : ( state ) => {
17- return state . matches . map ( ( match ) => match . meta ! ) . filter ( Boolean )
18- } ,
19- } )
20-
21- const meta : Array < RouterManagedTag > = React . useMemo ( ( ) => {
22- const resultMeta : Array < RouterManagedTag > = [ ]
23- const metaByAttribute : Record < string , true > = { }
24- let title : RouterManagedTag | undefined
25- for ( let i = routeMeta . length - 1 ; i >= 0 ; i -- ) {
26- const metas = routeMeta [ i ] !
27- for ( let j = metas . length - 1 ; j >= 0 ; j -- ) {
28- const m = metas [ j ]
29- if ( ! m ) continue
30-
31- if ( m . title ) {
32- if ( ! title ) {
33- title = {
34- tag : 'title' ,
35- children : m . title ,
36- }
37- }
38- } else if ( 'script:ld+json' in m ) {
39- // Handle JSON-LD structured data
40- // Content is HTML-escaped to prevent XSS when injected via dangerouslySetInnerHTML
41- try {
42- const json = JSON . stringify ( m [ 'script:ld+json' ] )
43- resultMeta . push ( {
44- tag : 'script' ,
45- attrs : {
46- type : 'application/ld+json' ,
47- } ,
48- children : escapeHtml ( json ) ,
49- } )
50- } catch {
51- // Skip invalid JSON-LD objects
52- }
53- } else {
54- const attribute = m . name ?? m . property
55- if ( attribute ) {
56- if ( metaByAttribute [ attribute ] ) {
57- continue
58- } else {
59- metaByAttribute [ attribute ] = true
60- }
61- }
62-
63- resultMeta . push ( {
64- tag : 'meta' ,
65- attrs : {
66- ...m ,
67- nonce,
68- } ,
69- } )
70- }
71- }
72- }
73-
74- if ( title ) {
75- resultMeta . push ( title )
76- }
77-
78- if ( nonce ) {
79- resultMeta . push ( {
80- tag : 'meta' ,
81- attrs : {
82- property : 'csp-nonce' ,
83- content : nonce ,
84- } ,
85- } )
86- }
87- resultMeta . reverse ( )
88-
89- return resultMeta
90- } , [ routeMeta , nonce ] )
91-
92- const links = useRouterState ( {
93- select : ( state ) => {
94- const constructed = state . matches
95- . map ( ( match ) => match . links ! )
96- . filter ( Boolean )
97- . flat ( 1 )
98- . map ( ( link ) => ( {
99- tag : 'link' ,
100- attrs : {
101- ...link ,
102- nonce,
103- } ,
104- } ) ) satisfies Array < RouterManagedTag >
105-
106- const manifest = router . ssr ?. manifest
107-
108- // These are the assets extracted from the ViteManifest
109- // using the `startManifestPlugin`
110- const assets = state . matches
111- . map ( ( match ) => manifest ?. routes [ match . routeId ] ?. assets ?? [ ] )
112- . filter ( Boolean )
113- . flat ( 1 )
114- . filter ( ( asset ) => asset . tag === 'link' )
115- . map (
116- ( asset ) =>
117- ( {
118- tag : 'link' ,
119- attrs : {
120- ...asset . attrs ,
121- suppressHydrationWarning : true ,
122- nonce,
123- } ,
124- } ) satisfies RouterManagedTag ,
125- )
126-
127- return [ ...constructed , ...assets ]
128- } ,
129- structuralSharing : true as any ,
130- } )
131-
132- const preloadLinks = useRouterState ( {
133- select : ( state ) => {
134- const preloadLinks : Array < RouterManagedTag > = [ ]
135-
136- state . matches
137- . map ( ( match ) => router . looseRoutesById [ match . routeId ] ! )
138- . forEach ( ( route ) =>
139- router . ssr ?. manifest ?. routes [ route . id ] ?. preloads
140- ?. filter ( Boolean )
141- . forEach ( ( preload ) => {
142- preloadLinks . push ( {
143- tag : 'link' ,
144- attrs : {
145- rel : 'modulepreload' ,
146- href : preload ,
147- nonce,
148- } ,
149- } )
150- } ) ,
151- )
152-
153- return preloadLinks
154- } ,
155- structuralSharing : true as any ,
156- } )
157-
158- const styles = useRouterState ( {
159- select : ( state ) =>
160- (
161- state . matches
162- . map ( ( match ) => match . styles ! )
163- . flat ( 1 )
164- . filter ( Boolean ) as Array < RouterManagedTag >
165- ) . map ( ( { children, ...attrs } ) => ( {
166- tag : 'style' ,
167- attrs,
168- children,
169- nonce,
170- } ) ) ,
171- structuralSharing : true as any ,
172- } )
173-
174- const headScripts : Array < RouterManagedTag > = useRouterState ( {
175- select : ( state ) =>
176- (
177- state . matches
178- . map ( ( match ) => match . headScripts ! )
179- . flat ( 1 )
180- . filter ( Boolean ) as Array < RouterManagedTag >
181- ) . map ( ( { children, ...script } ) => ( {
182- tag : 'script' ,
183- attrs : {
184- ...script ,
185- nonce,
186- } ,
187- children,
188- } ) ) ,
189- structuralSharing : true as any ,
190- } )
191-
192- return uniqBy (
193- [
194- ...meta ,
195- ...preloadLinks ,
196- ...links ,
197- ...styles ,
198- ...headScripts ,
199- ] as Array < RouterManagedTag > ,
200- ( d ) => {
201- return JSON . stringify ( d )
202- } ,
203- )
204- }
205-
206- /**
207- * Renders a stylesheet link for dev mode CSS collection.
208- * On the server, renders the full link with route-scoped CSS URL.
209- * On the client, renders the same link to avoid hydration mismatch,
210- * then removes it after hydration since Vite's HMR handles CSS updates.
211- */
212- function DevStylesLink ( ) {
213- const router = useRouter ( )
214- const routeIds = useRouterState ( {
215- select : ( state ) => state . matches . map ( ( match ) => match . routeId ) ,
216- } )
217-
218- React . useEffect ( ( ) => {
219- // After hydration, remove the SSR-rendered dev styles link
220- document
221- . querySelectorAll ( '[data-tanstack-start-dev-styles]' )
222- . forEach ( ( el ) => el . remove ( ) )
223- } , [ ] )
224-
225- const href = buildDevStylesUrl ( router . basepath , routeIds )
226-
227- return (
228- < link
229- rel = "stylesheet"
230- href = { href }
231- data-tanstack-start-dev-styles
232- suppressHydrationWarning
233- />
234- )
235- }
4+ import { useTags } from './headContentUtils'
2365
2376/**
2387 * Render route-managed head tags (title, meta, links, styles, head scripts).
@@ -245,22 +14,9 @@ export function HeadContent() {
24514 const nonce = router . options . ssr ?. nonce
24615 return (
24716 < >
248- { process . env . NODE_ENV !== 'production' && < DevStylesLink /> }
24917 { tags . map ( ( tag ) => (
25018 < Asset { ...tag } key = { `tsr-meta-${ JSON . stringify ( tag ) } ` } nonce = { nonce } />
25119 ) ) }
25220 </ >
25321 )
25422}
255-
256- function uniqBy < T > ( arr : Array < T > , fn : ( item : T ) => string ) {
257- const seen = new Set < string > ( )
258- return arr . filter ( ( item ) => {
259- const key = fn ( item )
260- if ( seen . has ( key ) ) {
261- return false
262- }
263- seen . add ( key )
264- return true
265- } )
266- }
0 commit comments