How to set html tag title from a route #1056
Replies: 18 comments 35 replies
-
Another option that I am considering is to extract the |
Beta Was this translation helpful? Give feedback.
-
Following this diccussion, I've seen https://tanstack.com/router/v1/docs/guide/router-context#processing-accumulated-route-context but not sure how to adjust Vite config to be able to have the |
Beta Was this translation helpful? Give feedback.
-
I’m working on a first class solution for title tags.
…On Jan 23, 2024 at 6:36 PM -0700, Dominic Garms ***@***.***>, wrote:
Following this diccussion, I've seen https://tanstack.com/router/v1/docs/guide/router-context#processing-accumulated-route-context but not sure how to adjust Vite config to be able to have the <head/> section inside of __root.tsx
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.Message ID: ***@***.***>
|
Beta Was this translation helpful? Give feedback.
-
I'd be keen for a first-class solution too. I had to adapt this solution from my previous React Router version... For the moment I have a The It's not perfect and there's actually no type-safety between my route data and the meta data argument, but it does the job pretty well - I just have to be very mindful of the loader data and manually provide the types to |
Beta Was this translation helpful? Give feedback.
-
@tannerlinsley any update when a first class solution might land, we're evaluating swapping to TanStack Router and this is one of the core pieces we're considering, especially when combined with Streaming responses. I noted the code/examples cover the Meta data, (except I couldn't find an example which loads in data for the tags), and wondered if this was the final API, or if it was a matter of just adding docs? |
Beta Was this translation helpful? Give feedback.
-
@tannerlinsley thanks for the wonderful library, looking forward to use the title tag feature in TanStack Router |
Beta Was this translation helpful? Give feedback.
-
For now, here's an example using accumulated route context: interface RootContext {
getTitle?: () => string | Promise<string>;
}
function Root() {
const matches = useMatches();
useEffect(() => {
// Set document title based on lowest matching route with a title
const breadcrumbPromises = [...matches]
.reverse()
.map((match, index) => {
if (!('getTitle' in match.routeContext)) {
if (index === 0 && import.meta.env.DEV) {
// eslint-disable-next-line no-console
console.warn('no getTitle', match.pathname, match);
}
return undefined;
}
const { routeContext } = match;
return routeContext.getTitle();
})
.filter(Boolean);
void Promise.all(breadcrumbPromises).then((titles) => {
document.title = titles.join(' · ');
return titles;
});
}, [matches]);
return <Outlet />;
}
export const Route = createRootRouteWithContext<RootContext>()({
beforeLoad: () => ({ getTitle: () => 'App Name' }),
component: Root,
}); // In another route file:
export const Route = createFileRoute('/test/')({
beforeLoad: () => ({ getTitle: () => 'Test' }),
component: () => <Test />,
}); This results in title like:
And supports further nesting. |
Beta Was this translation helpful? Give feedback.
-
I am now relying on react-helmet-async for head markup in my fastrat React + Fastify starter kit. |
Beta Was this translation helpful? Give feedback.
-
I've opted for a much simpler solution relying on the const TITLE = 'Your Company';
export const Route = createRootRouteWithContext<RootRouteContext>()({
meta: () => [
{
title: TITLE,
},
],
component: RootComponent,
});
function Meta({ children }: { children: ReactNode }) {
const matches = useMatches();
const meta = matches.at(-1)?.meta?.find((meta) => meta.title);
useEffect(() => {
document.title = [meta?.title, TITLE].filter(Boolean).join(' · ');
}, [meta]);
return children;
}
function RootComponent() {
return (
<Meta>
<Outlet />
</Meta>
);
} |
Beta Was this translation helpful? Give feedback.
-
To build on @Rendez solution, I wrote a simple hook that gathers up all the titles, and exposes them via prop. You can easily combine this with the import { useMatches } from "@tanstack/react-router";
import {
createContext,
PropsWithChildren,
useContext,
useEffect,
useState,
} from "react";
const BreadcrumbContext = createContext<
| {
breadcrumbs: string[];
getBreadcrumbString: (separator?: string) => string;
}
| undefined
>(undefined);
export const BreadcrumbProvider = ({ children }: PropsWithChildren) => {
const [breadcrumbs, setBreadcrumbs] = useState<string[]>([]);
const matches = useMatches();
useEffect(() => {
const meta = matches
.map((x) => x.meta?.find((meta) => meta.title)?.title)
.filter((x) => x !== undefined);
setBreadcrumbs(meta);
}, [matches]);
const getBreadcrumbString = (separator = " > ") => {
if (!breadcrumbs) return "";
return breadcrumbs?.join(separator);
};
return (
<BreadcrumbContext.Provider value={{ breadcrumbs, getBreadcrumbString }}>
{children}
</BreadcrumbContext.Provider>
);
};
export const useBreadcrumbs = () => {
const breadcrumbs = useContext(BreadcrumbContext);
if (!breadcrumbs)
throw new Error("useBreadcrumbs must be used within BreadcrumbProvider");
return breadcrumbs;
};
/// access breadcrumb data from any where via
/// const { breadcrumbs, getBreadcrumbString } = useBreadcrumbs(); Adjust your root module to be similar to the following: function RootComponent() {
return (
<BreadcrumbProvider>
<Meta>
<Outlet />
</Meta>
</BreadcrumbProvider>
);
} |
Beta Was this translation helpful? Give feedback.
-
hey @tannerlinsley , I’ve seen the
but I don't see my page title changing... is this API still experimental? |
Beta Was this translation helpful? Give feedback.
-
Has this been fixed ? I just tried setting a meta attribute inside createFileRoute and it seemd to work!
|
Beta Was this translation helpful? Give feedback.
-
This is what I currently use and works well. You can add more features to it. import { useEffect } from "react";
interface HelmetProps {
title?: string;
description?: string;
keywords?: string;
}
const Helmet: React.FC<HelmetProps> = ({ title, description, keywords }) => {
useEffect(() => {
// Set document title if provided
if (title) {
document.title = title;
}
// Set meta description if provided
if (description) {
let metaDescription = document.querySelector('meta[name="description"]');
if (!metaDescription) {
metaDescription = document.createElement("meta");
(metaDescription as HTMLMetaElement).name = "description";
document.head.appendChild(metaDescription);
}
(metaDescription as HTMLMetaElement).content = description;
}
// Set meta keywords if provided
if (keywords) {
let metaKeywords = document.querySelector('meta[name="keywords"]');
if (!metaKeywords) {
metaKeywords = document.createElement("meta");
(metaKeywords as HTMLMetaElement).name = "keywords";
document.head.appendChild(metaKeywords);
}
(metaKeywords as HTMLMetaElement).content = keywords;
}
}, [title, description, keywords]);
return null; // This component does not render any visible UI
};
export { Helmet }; |
Beta Was this translation helpful? Give feedback.
-
The docs are slightly outdated currently, but they describe a new way to set the export const Route = createRootRoute()({
head: () => ({
title: 'My App',
}) but the actual syntax is export const Route = createRootRoute()({
head: (ctx) => {
return {
meta: [
{
title: "My App",
},
],
};
},
}) You also need to render a |
Beta Was this translation helpful? Give feedback.
-
The way I have done suggested by @BrendanC23:
• Do it once, In the Root Route file: export const Route = createRootRoute({
component: () => (
<>
<HeadContent />
<RootComponent />
</>
),
head: () => ({
meta: [
{
title: "My App",
},
{ name: "description", content: "Learn more about MyApp" },
],
}),
}); then, import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/")({
component: HomeComponent,
head: (ctx) => {
return {
meta: [
{
title: "Index Page",
},
{
name: "description",
content: "Super Content",
},
],
};
},
});
function HomeComponent() {
return <h1>Hello Index</h1>;
} |
Beta Was this translation helpful? Give feedback.
-
For SPAs, you can render the import { createPortal } from "react-dom";
export const Route = createRootRoute({
component: () => (
<>
{createPortal(<HeadContent />, document.querySelector("head")!)}
<Outlet />
</>
),
head: () => ({
meta: [
{ title: "My App" },
{ name: "description", content: "Learn more about MyApp" },
],
}),
}); |
Beta Was this translation helpful? Give feedback.
-
Not an answer (more like a followup question), is there a way to dynamically set the title based on an async request response for SSR? Currently it seems to be possible to set html tags based per route but I'd like to send a separate network request to (another lol) server to get information to render the body (which can be done with |
Beta Was this translation helpful? Give feedback.
-
I couldn't find a solution that I could use without "as unknown as" export const Route = createFileRoute("/_protected/publications/$id/")({
component: PublicationView,
head: (ctx) => {
const publication =
ctx.loaderData as unknown as PublicationRetrieveSchemaReadable
return {
meta: [
{
title: `Publications / ${publication?.title}`,
},
],
}
},
loader: async ({ params, context }) => {
const publication = await context.queryClient.ensureQueryData(
publicationApiGetPublicationOptions({
path: { publication_id: params.id },
}),
)
return publication
},
}) |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
I can't see this from the examples, I am looking for a way to set HTML page title from a route definition.
This is challenging because the
<head />
element is defined in the root component__root.tsx
and other routes don't have access to it.At the moment I am relying on rendering an hidden HTML element that I copy to the page head, before sending the code to the browser.
Which works well, until I want to find a way to apply the streaming technique and then I am lost!
Beta Was this translation helpful? Give feedback.
All reactions