Viewing docs for an older release. View latest
meta (v2)
On this page

meta (v2)

You can opt-in to the new meta API with a future flag in Remix config.

/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
  future: {
    v2_meta: true,
  },
};

The meta export allows you to add metadata HTML tags for every route in your app. These tags are important for things like search engine optimization (SEO) and browser directives for determining certain behaviors. They can also be used by social media sites to display rich previews of your app.

The meta function should return an array of V2_MetaDescriptor objects. These objects map one-to-one with HTML tags. So this meta function:

export const meta: V2_MetaFunction = () => {
  return [
    { title: "Very cool app | Remix" },
    {
      property: "og:title",
      content: "Very cool app",
    },
    {
      name: "description",
      content: "This app is the best",
    },
  ];
};

produces this HTML:

<title>Very cool app | Remix</title>
<meta property="og:title" content="Very cool app" />;
<meta name="description" content="This app is the best" />

By default, meta descriptors will render a <meta> tag in most cases. The two exceptions are:

  • { title } renders a <title> tag
  • { "script:ld+json" } renders a <script type="application/ld+json"> tag, and its value should be a serializable object that is stringified and injected into the tag.
export const meta: V2_MetaFunction = () => {
  return [
    {
      "script:ld+json": {
        "@context": "https://schema.org",
        "@type": "Organization",
        name: "Remix",
        url: "https://remix.run",
      },
    },
  ];
};

A meta descriptor can also render a <link> tag by setting the tagName property to "link". This is useful for <link> tags associated with SEO like canonical URLs. For asset links like stylesheets and favicons, you should use the links export instead.

export const meta: V2_MetaFunction = () => {
  return [
    {
      tagName: "link",
      rel: "canonical",
      href: "https://remix.run",
    },
  ];
};

meta Function Parameters

location

This is the current router Location object. This is useful for generating tags for routes at specific paths or query parameters.

export const meta: V2_MetaFunction = ({ location }) => {
  const searchQuery = new URLSearchParams(
    location.search
  ).get("q");
  return [{ title: `Search results for "${searchQuery}"` }];
};

matches

This is an array of the current route matches. You have access to many things, particularly the meta from the parent matches and data.

The interface for matches is similar to the return value of useMatches, but each match will include the output of its meta function. This is useful for merging metadata across the route hierarchy.

data

This is the data from your route's loader.

export async function loader({ params }: LoaderArgs) {
  return json({
    task: await getTask(params.projectId, params.taskId),
  });
}

export const meta: V2_MetaFunction<typeof loader> = ({
  data,
}) => {
  return [{ title: data.task.name }];
};

params

The route's URL params. See Dynamic Segments in the Routing Guide.

Accessing Data from Parent Route Loaders

In addition to the current route's data, often you'll want to access data from a route higher up in the route hierarchy. You can look it up by its route ID in matches.

import type { loader as projectDetailsLoader } from "../../../$pid";

export async function loader({ params }: LoaderArgs) {
  return json({ task: await getTask(params.tid) });
}

export const meta: V2_MetaFunction<
  typeof loader,
  { "routes/project/$pid": typeof projectDetailsLoader }
> = ({ data, matches }) => {
  const project = matches.find(
    (match) => match.id === "routes/project/$pid"
  ).project;
  const task = data.task;
  return [{ title: `${project.name}: ${task.name}` }];
};

Gotchas with meta and Nested Routes

Because multiple nested routes render at the same time, there is some merging that needs to happen to determine the meta tags that ultimately render. Remix gives you complete control over this merge because there is no obvious default.

Remix will take the last matching route with a meta export and use that. This allows you to override things like title, remove things like og:image that the parent route added, or keep everything from the parent and add new meta for the child route.

This can get quite tricky when you're new.

Consider a route like /projects/123, there are likely three matching routes: root.tsx, projects.tsx, and projects/$id.tsx. All three may export meta descriptors.

export const meta: V2_MetaFunction = () => {
  return [
    {
      name: "viewport",
      content: "width=device-width,initial-scale=1",
    },
    { title: "New Remix App" },
  ];
};
export const meta: V2_MetaFunction = () => {
  return [{ title: "Projects" }];
};
export const meta: V2_MetaFunction<typeof loader> = ({
  data,
}) => {
  return [{ title: data.project.name }];
};

With this code, we will lose the viewport meta tag at /projects and /projects/123 because only the last meta is used and the code doesn't merge with the parent.

Global meta

Nearly every app will have global meta like the viewport and charSet. We recommend using normal <meta> tags inside of the root route instead of the meta export so you simply don't have to deal with merging:

import {
  Meta,
  Links,
  Scripts,
  Outlet,
} from "@remix-run/react";

export default function Root() {
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta
          name="viewport"
          content="width=device-width,initial-scale=1"
        />
        <Meta />
        <Links />
      </head>
      <body>
        <Outlet />
        <Scripts />
      </body>
    </html>
  );
}

Avoid meta in Parent Routes

You can also avoid the merge problem by simply not exporting meta that you want to override from parent routes. Instead of defining meta on the parent route, use the index route. This way you can avoid complex merge logic for things like the title. Otherwise you will need to find the parent title descriptor and replace it with the child's title. It's much easier to simply not need to override by using index routes.

Merging with Parent meta

Usually you only need to add meta to what the parent has already defined. You can merge parent meta with the spread operator and the matches arg:

export const meta: V2_MetaFunction = ({ matches }) => {
  const parentMeta = matches.flatMap(
    (match) => match.meta ?? []
  );
  return [...parentMeta, { title: "Projects" }];
};

Note that this will not override something like title. This is only additive. If the inherited route meta includes a title tag, you can override with Array.prototype.filter:

export const meta: V2_MetaFunction = ({ matches }) => {
  const parentMeta = matches
    .flatMap((match) => match.meta ?? [])
    .filter((meta) => !("title" in meta));
  return [...parentMeta, { title: "Projects" }];
};

meta Merging helper

If you can't avoid the merge problem with global meta or index routes, we've created a helper that you can put in your app that can override and append to parent meta easily.

Docs and examples licensed under MIT