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
5 changes: 5 additions & 0 deletions .changeset/static-head-object.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/router-core': patch
---

feat: allow head route option to accept a static object
1 change: 1 addition & 0 deletions packages/router-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ export type {
FileBaseRouteOptions,
BaseRouteOptions,
UpdatableRouteOptions,
HeadContent,
LoaderStaleReloadMode,
RouteLoaderFn,
RouteLoaderEntry,
Expand Down
4 changes: 3 additions & 1 deletion packages/router-core/src/load-matches.ts
Original file line number Diff line number Diff line change
Expand Up @@ -581,7 +581,9 @@ const executeHead = (
}

return Promise.all([
route.options.head?.(assetContext),
typeof route.options.head === 'function'
? route.options.head(assetContext)
: route.options.head,
route.options.scripts?.(assetContext),
route.options.headers?.(assetContext),
]).then(([headFnContent, scripts, headers]) => {
Expand Down
42 changes: 23 additions & 19 deletions packages/router-core/src/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1177,6 +1177,13 @@ export interface BeforeLoadContextOptions<
>
}

export type HeadContent = {
links?: AnyRouteMatch['links']
scripts?: AnyRouteMatch['headScripts']
meta?: AnyRouteMatch['meta']
styles?: AnyRouteMatch['styles']
}

type AssetFnContextOptions<
in out TRouteId,
in out TFullPath,
Expand Down Expand Up @@ -1383,25 +1390,22 @@ export interface UpdatableRouteOptions<
TLoaderDeps
>,
) => Awaitable<Record<string, string> | undefined>
head?: (
ctx: AssetFnContextOptions<
TRouteId,
TFullPath,
TParentRoute,
TParams,
TSearchValidator,
TLoaderFn,
TRouterContext,
TRouteContextFn,
TBeforeLoadFn,
TLoaderDeps
>,
) => Awaitable<{
links?: AnyRouteMatch['links']
scripts?: AnyRouteMatch['headScripts']
meta?: AnyRouteMatch['meta']
styles?: AnyRouteMatch['styles']
}>
head?:
| ((
ctx: AssetFnContextOptions<
TRouteId,
TFullPath,
TParentRoute,
TParams,
TSearchValidator,
TLoaderFn,
TRouterContext,
TRouteContextFn,
TBeforeLoadFn,
TLoaderDeps
>,
) => Awaitable<HeadContent>)
| HeadContent
scripts?: (
ctx: AssetFnContextOptions<
TRouteId,
Expand Down
5 changes: 4 additions & 1 deletion packages/router-core/src/ssr/ssr-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,10 @@ export async function hydrate(router: AnyRouter): Promise<any> {
params: match.params,
loaderData: match.loaderData,
}
const headFnContent = await route.options.head?.(assetContext)
const headFnContent =
typeof route.options.head === 'function'
? await route.options.head(assetContext)
: route.options.head

const scripts = await route.options.scripts?.(assetContext)

Expand Down
57 changes: 57 additions & 0 deletions packages/router-core/tests/load.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1851,6 +1851,63 @@ describe('head execution', () => {
expect(rootMatch?.error).toBeUndefined()
})
})

test('accepts a static object for head instead of a function', async () => {
const staticHead = {
meta: [{ title: 'Static Title' }],
links: [{ rel: 'icon', href: '/favicon.ico' }],
}

const rootRoute = new BaseRootRoute({
head: staticHead,
})
const testRoute = new BaseRoute({
getParentRoute: () => rootRoute,
path: '/test',
})
const routeTree = rootRoute.addChildren([testRoute])
const router = new RouterCore({
routeTree,
history: createMemoryHistory({ initialEntries: ['/test'] }),
})

await router.load()

const match = router.state.matches.find((m) => m.routeId === rootRoute.id)
expect(match?.meta).toEqual([{ title: 'Static Title' }])
expect(match?.links).toEqual([{ rel: 'icon', href: '/favicon.ico' }])
})

test('static head object and function head work together in route hierarchy', async () => {
const rootRoute = new BaseRootRoute({
head: {
meta: [{ charSet: 'UTF-8' }],
},
})
const childRoute = new BaseRoute({
getParentRoute: () => rootRoute,
path: '/child',
head: () => ({
meta: [{ title: 'Child Page' }],
}),
})
const routeTree = rootRoute.addChildren([childRoute])
const router = new RouterCore({
routeTree,
history: createMemoryHistory({ initialEntries: ['/child'] }),
})

await router.load()

const rootMatch = router.state.matches.find(
(m) => m.routeId === rootRoute.id,
)
const childMatch = router.state.matches.find(
(m) => m.routeId === childRoute.id,
)
expect(rootMatch?.meta).toEqual([{ charSet: 'UTF-8' }])
expect(childMatch?.meta).toEqual([{ title: 'Child Page' }])
})
})

describe('params.parse notFound', () => {
Expand Down