Overview
The Resolid Framework provides utilities for working with React Router, including meta tag merging for hierarchical page metadata.
Merges meta tags from parent routes with child routes, eliminating duplicates and combining titles.
function mergeMeta(
metaFn: (arg: MetaArgs) => MetaDescriptor[],
titleJoin?: string
): (arg: MetaArgs) => MetaDescriptor[]
metaFn
(arg: MetaArgs) => MetaDescriptor[]
required
Function that returns meta descriptors for the current route
String to join multiple title segments
return
(arg: MetaArgs) => MetaDescriptor[]
A function that merges meta tags from all matched routes
How It Works
The mergeMeta function:
- Collects meta tags from all parent routes
- Merges them with the current route’s meta tags
- Removes duplicates (matching by
name, property, or title)
- Combines all title segments into a single title tag
- Returns the merged meta descriptors
Basic Usage
import { mergeMeta } from '@resolid/dev/router';
import type { Route } from 'react-router';
export default {
meta: mergeMeta(() => [
{ title: 'Home' },
{ name: 'description', content: 'Welcome to our site' }
])
} satisfies Route.MetaFunction;
Hierarchical Titles
When routes are nested, titles are automatically combined:
// Root route: app/root.tsx
export const meta = mergeMeta(() => [
{ title: 'My App' }
]);
// Parent route: app/routes/blog.tsx
export const meta = mergeMeta(() => [
{ title: 'Blog' }
]);
// Child route: app/routes/blog.post.$id.tsx
export const meta = mergeMeta(({ data }) => [
{ title: data.post.title }
]);
// Result for child route:
// <title>Understanding React - Blog - My App</title>
Custom Title Separator
Change the title separator:
export const meta = mergeMeta(
() => [
{ title: 'Products' }
],
' | ' // Use pipe instead of dash
);
// Result: Products | My App
Child route meta tags override parent meta tags with the same name or property:
// Root route
export const meta = mergeMeta(() => [
{ name: 'description', content: 'Default description' },
{ property: 'og:image', content: '/default-image.jpg' }
]);
// Child route
export const meta = mergeMeta(() => [
{ name: 'description', content: 'Specific page description' },
{ property: 'og:image', content: '/page-image.jpg' }
]);
// Result uses child route values:
// <meta name="description" content="Specific page description" />
// <meta property="og:image" content="/page-image.jpg" />
Use loader data to generate dynamic meta tags:
import { mergeMeta } from '@resolid/dev/router';
import type { Route } from 'react-router';
export const meta = mergeMeta(({ data }) => [
{ title: data.post.title },
{ name: 'description', content: data.post.excerpt },
{ property: 'og:title', content: data.post.title },
{ property: 'og:description', content: data.post.excerpt },
{ property: 'og:image', content: data.post.coverImage },
{ property: 'og:type', content: 'article' },
{ property: 'article:published_time', content: data.post.publishedAt },
{ property: 'article:author', content: data.post.author.name }
]) satisfies Route.MetaFunction;
Route Matches
Access parent route data through matches:
export const meta = mergeMeta(({ data, matches }) => {
// Get parent route data
const parentData = matches[matches.length - 2]?.data;
return [
{ title: `${data.product.name} - ${parentData?.category.name}` },
{ name: 'description', content: data.product.description }
];
});
Type Definitions
interface MetaArgs<
Loader extends LoaderFunction | unknown = unknown,
MatchLoaders extends Record<string, LoaderFunction> | unknown = unknown
> {
data: Loader extends LoaderFunction ? SerializeFrom<Loader> : unknown;
params: Params;
location: Location;
matches: RouteMatch<MatchLoaders>[];
}
type MetaDescriptor =
| { charSet: 'utf-8' }
| { title: string }
| { name: string; content: string }
| { property: string; content: string }
| { httpEquiv: string; content: string }
| { 'script:ld+json': object }
| { tagName: string; [key: string]: string };
Complete Example
// app/root.tsx
import { mergeMeta } from '@resolid/dev/router';
import type { Route } from 'react-router';
export const meta = mergeMeta(() => [
{ charSet: 'utf-8' },
{ title: 'My App' },
{ name: 'viewport', content: 'width=device-width,initial-scale=1' },
{ name: 'description', content: 'Welcome to My App' },
{ property: 'og:site_name', content: 'My App' }
]) satisfies Route.MetaFunction;
// app/routes/blog.tsx
export const meta = mergeMeta(() => [
{ title: 'Blog' },
{ name: 'description', content: 'Read our latest articles' },
{ property: 'og:type', content: 'website' }
]) satisfies Route.MetaFunction;
// app/routes/blog.post.$id.tsx
export const loader = async ({ params }) => {
const post = await getPost(params.id);
return { post };
};
export const meta = mergeMeta(({ data }) => [
{ title: data.post.title },
{ name: 'description', content: data.post.excerpt },
{ property: 'og:title', content: data.post.title },
{ property: 'og:description', content: data.post.excerpt },
{ property: 'og:image', content: data.post.coverImage },
{ property: 'og:type', content: 'article' },
{ property: 'article:published_time', content: data.post.publishedAt },
{ property: 'article:author', content: data.post.author.name },
{
'script:ld+json': {
'@context': 'https://schema.org',
'@type': 'Article',
headline: data.post.title,
description: data.post.excerpt,
image: data.post.coverImage,
datePublished: data.post.publishedAt,
author: {
'@type': 'Person',
name: data.post.author.name
}
}
}
]) satisfies Route.MetaFunction;
// Resulting HTML for blog post:
// <meta charset="utf-8" />
// <title>Understanding React - Blog - My App</title>
// <meta name="viewport" content="width=device-width,initial-scale=1" />
// <meta name="description" content="Learn the fundamentals..." />
// <meta property="og:site_name" content="My App" />
// <meta property="og:title" content="Understanding React" />
// <meta property="og:description" content="Learn the fundamentals..." />
// <meta property="og:image" content="/posts/react-cover.jpg" />
// <meta property="og:type" content="article" />
// <meta property="article:published_time" content="2024-01-15" />
// <meta property="article:author" content="John Doe" />
// <script type="application/ld+json">{...}</script>
Best Practices
-
Use mergeMeta consistently: Apply
mergeMeta to all routes for consistent meta tag handling
-
Define base meta in root: Set default meta tags in the root route
-
Override selectively: Only override specific meta tags in child routes
-
Include OpenGraph tags: Add OpenGraph tags for better social sharing
-
Add structured data: Use
script:ld+json for SEO-friendly structured data
-
Keep titles concise: Combine titles should stay under 60 characters
-
Unique descriptions: Provide unique descriptions for each page
Source Code
Location: packages/dev/src/router/utils/merge-meta.ts