Remix v2 Open Graph Meta Tags: The Updated meta() Export Guide

How to add og:title, og:image, og:description in Remix v2 using the new meta() function — covers route-level, loader data, and nested meta merging.

What changed in Remix v2 meta()

Remix v2 introduced a breaking change to how meta tags work. In Remix v1, you exported a meta function that returned a simple object. In Remix v2, meta() returns an array of objects, where each object can be a { title }, { name, content }, or { property, content } descriptor.

This new format is more explicit, gives you full control over every tag, and solves the merging problem from v1 where parent and child meta were automatically merged in confusing ways.

Static OG tags with the v2 meta() export

For static pages, return all your meta descriptors from the meta export function:

// app/routes/_index.tsx
import type { MetaFunction } from "@remix-run/node";

export const meta: MetaFunction = () => {
  return [
    { title: "My Remix App — Fast Server Rendering" },
    { name: "description", content: "A server-rendered app built with Remix v2." },

    // Open Graph
    { property: "og:type",        content: "website" },
    { property: "og:title",       content: "My Remix App — Fast Server Rendering" },
    { property: "og:description", content: "A server-rendered app built with Remix v2." },
    { property: "og:image",       content: "https://yourdomain.com/og/home.png" },
    { property: "og:url",         content: "https://yourdomain.com/" },

    // Twitter Card
    { name: "twitter:card",        content: "summary_large_image" },
    { name: "twitter:title",       content: "My Remix App — Fast Server Rendering" },
    { name: "twitter:description", content: "A server-rendered app built with Remix v2." },
    { name: "twitter:image",       content: "https://yourdomain.com/og/home.png" },
  ];
};

export default function Index() {
  return <main><h1>Welcome</h1></main>;
}

Each object in the array maps to a single HTML <meta> or <title> element. Remix handles serialization and deduplication automatically.

Dynamic OG tags from loader data

For blog posts, products, or user profile pages, use a loader to fetch data, then access it via the data argument in meta():

// app/routes/blog.$slug.tsx
import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node";
import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";

export async function loader({ params }: LoaderFunctionArgs) {
  const post = await fetchPost(params.slug!);
  if (!post) throw new Response("Not Found", { status: 404 });
  return json(post);
}

export const meta: MetaFunction<typeof loader> = ({ data, params }) => {
  if (!data) {
    return [{ title: "Post Not Found" }];
  }

  const ogImage = data.ogImage
    ?? `https://yourdomain.com/api/og?title=${encodeURIComponent(data.title)}`;
  const canonicalUrl = `https://yourdomain.com/blog/${params.slug}`;

  return [
    { title: `${data.title} | My Blog` },
    { name: "description",         content: data.excerpt },
    { property: "og:type",         content: "article" },
    { property: "og:title",        content: data.title },
    { property: "og:description",  content: data.excerpt },
    { property: "og:image",        content: ogImage },
    { property: "og:url",          content: canonicalUrl },
    { name: "twitter:card",        content: "summary_large_image" },
    { name: "twitter:title",       content: data.title },
    { name: "twitter:image",       content: ogImage },
    { tagName: "link", rel: "canonical", href: canonicalUrl },
  ];
};

export default function BlogPost() {
  const post = useLoaderData<typeof loader>();
  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.excerpt}</p>
    </article>
  );
}

Because Remix runs loader on the server and embeds the data in the HTML during SSR, all OG tags are present in the initial response — no JavaScript execution needed by crawlers.

Nested meta merging in Remix v2

In Remix v2, child routes do not automatically inherit parent meta tags. If you want a child to extend parent defaults, use the matches argument:

// app/routes/blog.$slug.tsx — merge parent meta from root
import type { MetaFunction } from "@remix-run/node";

export const meta: MetaFunction = ({ matches, data }) => {
  // Get root meta tags (site-wide defaults)
  const rootMeta = matches.find((m) => m.id === "root")?.meta ?? [];

  // Filter out tags you want to override
  const baseMeta = rootMeta.filter(
    (m) => !("property" in m && m.property?.startsWith("og:"))
  );

  return [
    ...baseMeta,  // Inherit non-OG root tags
    { title: `${data?.title} | My Blog` },
    { property: "og:title",       content: data?.title ?? "" },
    { property: "og:description", content: data?.excerpt ?? "" },
    { property: "og:image",       content: data?.ogImage ?? "" },
  ];
};

Root layout defaults

Set site-wide OG defaults in app/root.tsx:

// app/root.tsx
export const meta: MetaFunction = () => [
  { property: "og:site_name", content: "My Remix App" },
  { name: "twitter:card",     content: "summary_large_image" },
  { name: "twitter:site",     content: "@yourhandle" },
];

Preview your Remix OG tags

Paste any Remix URL into OGFixer for instant previews on Twitter, LinkedIn, Discord, and Slack.

Related guides