Skip to content

feat: allow head route option to accept a static object#6950

Open
WolfieLeader wants to merge 6 commits intoTanStack:mainfrom
WolfieLeader:feat/head-static-object
Open

feat: allow head route option to accept a static object#6950
WolfieLeader wants to merge 6 commits intoTanStack:mainfrom
WolfieLeader:feat/head-static-object

Conversation

@WolfieLeader
Copy link

@WolfieLeader WolfieLeader commented Mar 17, 2026

Summary

  • The head route option now accepts either a function or a plain static object
  • Routes with static head content (favicon, charset, viewport meta) no longer need a function wrapper
  • All existing function-based head usage continues to work unchanged

Before:

head: () => ({
  meta: [{ title: 'My App' }, { charSet: 'UTF-8' }],
  links: [{ rel: 'icon', href: '/favicon.ico' }],
})

After (also valid):

head: {
  meta: [{ title: 'My App' }, { charSet: 'UTF-8' }],
  links: [{ rel: 'icon', href: '/favicon.ico' }],
}

Closes #6949

Changes

  • packages/router-core/src/route.ts — Made head type a union of function and static object
  • packages/router-core/src/load-matches.ts — Added typeof check before calling head
  • packages/router-core/src/ssr/ssr-client.ts — Same typeof check in the SSR hydration path
  • packages/router-core/tests/load.test.ts — Added tests for static object head and mixed hierarchy

Test plan

  • Static object head with meta tags and links
  • Mixed hierarchy: static head on root + function head on child
  • All 1056 existing tests pass with 0 type errors

Summary by CodeRabbit

  • New Features
    • Route head metadata now accepts either a static object or a dynamic function; a new HeadContent type is exported for reuse.
  • Tests
    • Added tests verifying static head objects and mixed static+dynamic head behavior across route hierarchies.
  • Chores
    • Added a changeset declaring a patch release documenting static head object support.

Routes with static head content (e.g. favicon, charset, viewport meta)
no longer need a function wrapper. The head option now accepts either
a function or a plain object.

Closes TanStack#6949
@changeset-bot
Copy link

changeset-bot bot commented Mar 17, 2026

🦋 Changeset detected

Latest commit: f717225

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 27 packages
Name Type
@tanstack/router-core Patch
@tanstack/react-router-devtools Patch
@tanstack/react-router Patch
@tanstack/react-start-client Patch
@tanstack/react-start-server Patch
@tanstack/router-devtools-core Patch
@tanstack/router-generator Patch
@tanstack/router-plugin Patch
@tanstack/solid-router-devtools Patch
@tanstack/solid-router Patch
@tanstack/solid-start-client Patch
@tanstack/solid-start-server Patch
@tanstack/start-client-core Patch
@tanstack/start-plugin-core Patch
@tanstack/start-server-core Patch
@tanstack/start-storage-context Patch
@tanstack/vue-router-devtools Patch
@tanstack/vue-router Patch
@tanstack/vue-start-client Patch
@tanstack/vue-start-server Patch
@tanstack/router-devtools Patch
@tanstack/react-start Patch
@tanstack/router-cli Patch
@tanstack/router-vite-plugin Patch
@tanstack/solid-start Patch
@tanstack/start-static-server-functions Patch
@tanstack/vue-start Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 17, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 32c9b6c1-ac11-41e9-8ad9-a3d03abce15d

📥 Commits

Reviewing files that changed from the base of the PR and between f717225 and 89d8cf1.

📒 Files selected for processing (1)
  • packages/router-core/tests/load.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/router-core/tests/load.test.ts

📝 Walkthrough

Walkthrough

The PR allows the head route option to be either a function or a static object. Types were added/updated, runtime callers now check typeof head === 'function' before invoking, exports updated, and tests and a changeset were added to cover static and mixed usages.

Changes

Cohort / File(s) Summary
Type Definition
packages/router-core/src/route.ts
Added exported HeadContent type and changed UpdatableRouteOptions.head to accept either a function returning HeadContent or a static HeadContent object.
Runtime Execution
packages/router-core/src/load-matches.ts, packages/router-core/src/ssr/ssr-client.ts
Replaced direct optional invocation with typeof === 'function' guards so head is invoked only when it's a function; static objects are used directly.
Exports
packages/router-core/src/index.ts
Exported the new HeadContent type from the package entry point.
Tests
packages/router-core/tests/load.test.ts
Added tests verifying acceptance of a static head object and mixed static/function head across parent/child routes.
Release Note
.changeset/static-head-object.md
Added changeset declaring a patch release noting the feature: allow head to accept a static object.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I nibble on code with a happy little hop,
Static heads now rest — no function on top,
A tiny typeof guard, both forms in tune,
Routes nest and merge beneath the moon,
Hooray — the rabbit hops and hums a new tune! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main feature: allowing the head route option to accept a static object instead of requiring a function wrapper.
Linked Issues check ✅ Passed The PR fully implements all coding requirements from issue #6949: head type is now a union type supporting both function and static object forms, typeof checks guard function invocation in load-matches.ts and ssr-client.ts, and tests validate static and mixed scenarios.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the head option as specified in #6949; scripts and headers are correctly left unchanged, and no unrelated modifications are present.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/router-core/src/route.ts (1)

1386-1411: LGTM! Backwards-compatible union type for static head support.

The type correctly allows both function and static object forms while maintaining the same shape. Existing function-based head definitions continue to work.

Consider extracting the duplicated object shape to a named type to reduce repetition:

♻️ Optional: Extract shared head content type
+type HeadContent = {
+  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<{
-        links?: AnyRouteMatch['links']
-        scripts?: AnyRouteMatch['headScripts']
-        meta?: AnyRouteMatch['meta']
-        styles?: AnyRouteMatch['styles']
-      }>)
-    | {
-        links?: AnyRouteMatch['links']
-        scripts?: AnyRouteMatch['headScripts']
-        meta?: AnyRouteMatch['meta']
-        styles?: AnyRouteMatch['styles']
-      }
+      ) => Awaitable<HeadContent>)
+    | HeadContent
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/router-core/src/route.ts` around lines 1386 - 1411, The union type
for the head property duplicates the object shape; extract that repeated shape
into a named type (e.g., HeadContent) and replace the inline object in the union
with that type and AssetFnContextOptions<...> returning Awaitable<HeadContent>;
update references in this declaration (the head property, AssetFnContextOptions
generic usage, and AnyRouteMatch usages) so the union becomes either (ctx =>
Awaitable<HeadContent>) | HeadContent to remove duplication and improve
readability.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/router-core/src/route.ts`:
- Around line 1386-1411: The union type for the head property duplicates the
object shape; extract that repeated shape into a named type (e.g., HeadContent)
and replace the inline object in the union with that type and
AssetFnContextOptions<...> returning Awaitable<HeadContent>; update references
in this declaration (the head property, AssetFnContextOptions generic usage, and
AnyRouteMatch usages) so the union becomes either (ctx =>
Awaitable<HeadContent>) | HeadContent to remove duplication and improve
readability.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d62e5aab-f9e1-4b95-aaff-483530149f6f

📥 Commits

Reviewing files that changed from the base of the PR and between 13ef4c2 and be93850.

📒 Files selected for processing (4)
  • packages/router-core/src/load-matches.ts
  • packages/router-core/src/route.ts
  • packages/router-core/src/ssr/ssr-client.ts
  • packages/router-core/tests/load.test.ts

@github-actions
Copy link
Contributor

github-actions bot commented Mar 17, 2026

Bundle Size Benchmarks

  • Commit: 3bc18d462345
  • Measured at: 2026-03-17T15:01:23.028Z
  • Baseline source: history:d6371529b5ab
  • Dashboard: bundle-size history
Scenario Current (gzip) Delta vs baseline Raw Brotli Trend
react-router.minimal 87.62 KiB +12 B (+0.01%) 276.11 KiB 76.21 KiB ▁▁▁▁▁▁▁▁████
react-router.full 90.65 KiB +9 B (+0.01%) 286.33 KiB 78.67 KiB ▁▁▁▁▁▁▁▁████
solid-router.minimal 37.15 KiB +16 B (+0.04%) 111.42 KiB 33.36 KiB ▁▁▁▁▁▁▁▁████
solid-router.full 41.37 KiB +15 B (+0.04%) 124.22 KiB 37.09 KiB ▁▁▁▁▁▁▁▁████
vue-router.minimal 53.00 KiB +12 B (+0.02%) 151.46 KiB 47.65 KiB ▁▁▁▁▁▁▁▁████
vue-router.full 57.70 KiB +11 B (+0.02%) 166.38 KiB 51.75 KiB ▁▁▁▁▁▁▁▁████
react-start.minimal 102.01 KiB +23 B (+0.02%) 324.17 KiB 88.15 KiB ▁▁▁▁▁▁▁▁████
react-start.full 105.36 KiB +21 B (+0.02%) 333.98 KiB 91.10 KiB ▁▁▁▁▁▁▁▁████
solid-start.minimal 51.26 KiB +22 B (+0.04%) 157.91 KiB 45.18 KiB ▁▁▁▁▁▁▁▁████
solid-start.full 56.55 KiB +21 B (+0.04%) 173.41 KiB 49.82 KiB ▁▁▁▁▁▁▁▁████

Trend sparkline is historical gzip bytes ending with this PR measurement; lower is better.

@nx-cloud
Copy link

nx-cloud bot commented Mar 17, 2026

View your CI Pipeline Execution ↗ for commit 89d8cf1

Command Status Duration Result
nx affected --targets=test:eslint,test:unit,tes... ⛔ Cancelled 19s View ↗

☁️ Nx Cloud last updated this comment at 2026-03-17 14:59:06 UTC

@WolfieLeader
Copy link
Author

@lachlancollins Thank's for the help!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: allow head route option to accept a static object

2 participants