meta
Defines metadata tags for a route, including title, description, Open Graph tags, and other<meta> elements in the document <head>.
Signature
export function meta(args: MetaArgs): MetaDescriptor[]
Arguments passed to the meta function
Show properties
Show properties
Deprecated. Use
loaderData instead.Data returned from this route’s loader or clientLoader
Dynamic route params for the current route
The current location object
Array of parent route matches with their data
The error if one occurred (only available in error boundary)
An array of meta descriptors. Each descriptor becomes a
<meta> or <title> element.Basic Example
import type { MetaFunction } from "react-router";
export const meta: MetaFunction = () => {
return [
{ title: "My App" },
{ name: "description", content: "Welcome to my app" },
];
};
Using Loader Data
export async function loader({ params }: Route.LoaderArgs) {
const product = await fetchProduct(params.productId);
return { product };
}
export const meta: MetaFunction<typeof loader> = ({ loaderData }) => {
return [
{ title: loaderData.product.name },
{ name: "description", content: loaderData.product.description },
];
};
Open Graph Tags
export const meta: MetaFunction<typeof loader> = ({ loaderData }) => {
const { article } = loaderData;
return [
{ title: article.title },
{ property: "og:title", content: article.title },
{ property: "og:description", content: article.summary },
{ property: "og:image", content: article.imageUrl },
{ property: "og:type", content: "article" },
{ property: "og:url", content: `https://example.com/articles/${article.slug}` },
];
};
Twitter Card
export const meta: MetaFunction<typeof loader> = ({ loaderData }) => {
const { post } = loaderData;
return [
{ title: post.title },
{ name: "twitter:card", content: "summary_large_image" },
{ name: "twitter:site", content: "@myapp" },
{ name: "twitter:title", content: post.title },
{ name: "twitter:description", content: post.excerpt },
{ name: "twitter:image", content: post.coverImage },
];
};
Charset and Viewport
export const meta: MetaFunction = () => {
return [
{ charSet: "utf-8" },
{ name: "viewport", content: "width=device-width,initial-scale=1" },
];
};
Dynamic Title with Params
export const meta: MetaFunction<typeof loader> = ({ loaderData, params }) => {
return [
{ title: `${loaderData.user.name} - User Profile` },
{ name: "description", content: `Profile page for ${loaderData.user.name}` },
];
};
Accessing Parent Route Data
import type { MetaFunction } from "react-router";
import type { loader as rootLoader } from "~/root";
export const meta: MetaFunction<
typeof loader,
{ root: typeof rootLoader }
> = ({ loaderData, matches }) => {
// Find parent route data
const rootData = matches.find((match) => match.id === "root")?.loaderData;
return [
{ title: `${loaderData.product.name} - ${rootData.siteName}` },
];
};
Merging Parent Meta
export const meta: MetaFunction<typeof loader> = ({ loaderData, matches }) => {
// Get meta from parent routes
const parentMeta = matches.flatMap((match) => match.meta ?? []);
// Override or add to parent meta
return [
...parentMeta,
{ title: loaderData.product.name },
{ name: "description", content: loaderData.product.description },
];
};
Conditional Meta Based on Location
export const meta: MetaFunction<typeof loader> = ({ loaderData, location }) => {
const isPreview = location.search.includes("preview=true");
return [
{ title: loaderData.page.title },
...(isPreview ? [{ name: "robots", content: "noindex" }] : []),
];
};
JSON-LD Structured Data
export const meta: MetaFunction<typeof loader> = ({ loaderData }) => {
const { product } = loaderData;
return [
{ title: product.name },
{
"script:ld+json": {
"@context": "https://schema.org",
"@type": "Product",
name: product.name,
description: product.description,
image: product.imageUrl,
offers: {
"@type": "Offer",
price: product.price,
priceCurrency: "USD",
},
},
},
];
};
Error Meta
export const meta: MetaFunction = ({ error }) => {
if (error) {
return [
{ title: "Error" },
{ name: "description", content: "An error occurred" },
{ name: "robots", content: "noindex" },
];
}
return [
{ title: "My Page" },
];
};
All Meta Descriptor Types
export const meta: MetaFunction = () => {
return [
// Title
{ title: "Page Title" },
// Charset
{ charSet: "utf-8" },
// Standard meta tags
{ name: "description", content: "Page description" },
{ name: "keywords", content: "react, router" },
{ name: "author", content: "John Doe" },
{ name: "viewport", content: "width=device-width,initial-scale=1" },
{ name: "robots", content: "index,follow" },
// Open Graph
{ property: "og:title", content: "Page Title" },
{ property: "og:description", content: "Description" },
{ property: "og:image", content: "https://example.com/image.jpg" },
// HTTP Equiv
{ httpEquiv: "content-type", content: "text/html; charset=UTF-8" },
{ httpEquiv: "x-ua-compatible", content: "IE=edge" },
// JSON-LD
{ "script:ld+json": { /* structured data */ } },
// Custom tag
{ tagName: "meta", property: "custom", content: "value" },
];
};
SEO Best Practices
export const meta: MetaFunction<typeof loader> = ({ loaderData }) => {
const { article } = loaderData;
const url = `https://example.com/articles/${article.slug}`;
return [
// Essential meta tags
{ title: `${article.title} | My Blog` },
{ name: "description", content: article.summary },
// Open Graph
{ property: "og:type", content: "article" },
{ property: "og:title", content: article.title },
{ property: "og:description", content: article.summary },
{ property: "og:image", content: article.coverImage },
{ property: "og:url", content: url },
// Twitter
{ name: "twitter:card", content: "summary_large_image" },
{ name: "twitter:title", content: article.title },
{ name: "twitter:description", content: article.summary },
{ name: "twitter:image", content: article.coverImage },
// Article meta
{ property: "article:published_time", content: article.publishedAt },
{ property: "article:author", content: article.author.name },
// Structured data
{
"script:ld+json": {
"@context": "https://schema.org",
"@type": "Article",
headline: article.title,
description: article.summary,
image: article.coverImage,
datePublished: article.publishedAt,
author: {
"@type": "Person",
name: article.author.name,
},
},
},
];
};
Best Practices
Use type-safe meta with loader types
Use type-safe meta with loader types
Pass your loader type to MetaFunction for autocomplete:
export async function loader() {
return { product: { name: "Widget", price: 29.99 } };
}
export const meta: MetaFunction<typeof loader> = ({ loaderData }) => {
loaderData.product.name; // ✅ Typed!
return [{ title: loaderData.product.name }];
};
Keep titles under 60 characters
Keep titles under 60 characters
Search engines typically display the first 50-60 characters:
export const meta: MetaFunction<typeof loader> = ({ loaderData }) => {
const title = loaderData.product.name.length > 50
? `${loaderData.product.name.slice(0, 50)}...`
: loaderData.product.name;
return [{ title }];
};
Descriptions should be 150-160 characters
Descriptions should be 150-160 characters
Optimal length for search result snippets:
const truncateDescription = (text: string, maxLength = 155) => {
if (text.length <= maxLength) return text;
return text.slice(0, maxLength).trim() + "...";
};
export const meta: MetaFunction<typeof loader> = ({ loaderData }) => {
return [
{
name: "description",
content: truncateDescription(loaderData.product.description)
},
];
};
Meta tags are merged from leaf to root
Meta tags are merged from leaf to root
Child route meta overrides parent route meta:
// app/root.tsx
export const meta = () => [{ title: "My App" }];
// app/routes/products.$id.tsx
export const meta = ({ loaderData }) => [
// This overrides root title
{ title: `${loaderData.product.name} | My App` }
];
See Also
- links - Define link tags
- loader - Load data for meta
- MetaFunction - Full type reference