Remix Meta Tags


Ryan Chenkie

cover image

December 31, 2021

With everyone raving about Remix, I wanted to give it a try myself. My site was in need of an upgrade and Remix seemed like it would be a good fit. So far so good!

For my first post, I needed to set some meta tags for things like the post title, Twitter card image, description, etc. These are set on meta tags in the document head.

I wasn't initially aware of the "Remix way" of doing this was. My naive approach was to try to set reset items in the head of my blog/$slug.tsx component.

export default function PostSlug() {
  const post = useLoaderData();

  return (
        <title>My 2021 Year in Review | Ryan Chenkie</title>
        <meta name="twitter:card" content="summary_large_image" />
        <meta property="og:description" content="..." />
        <meta property="og:image" content="..." />

This is, of course, not the way to do it. Doing it this way will nest a head element within the body which is not only incorrent syntactically, it also causes issues with the way Remix renders. I was getting some weird double-rendering of my content happening that I traced back to the above approach.

Use the Remix MetaFunction

The correct way to handle meta tags in Remix is to use the meta function. Remix provides a TypeScript type of MetaFunction for it.

This function needs to be exported alongside the component with the page you're targeting. In my case, this is my dynamic blog/$slug.tsx route.

import type { MetaFunction } from "remix";

export const meta: MetaFunction = () => {
  return {
    title: "Ryan's Blog",
    description: "This is my blog",
    "twitter:card": "summary_large_image",
    "og:description": "...",
    "og:image": "...",

Getting Data in the MetaFunction

The biggest question I had when I realized I needed the meta function was "how do I make it dynamic?" I need to be able to pass it the title, description, etc of the current post.

It turns out as long as we're loading our data the Remix-prescribed way, it's fairly simple. The data returned from our loader function is available as a param in the meta function for us to use.

import type { LoaderFunction, MetaFunction } from "remix";

export const loader: LoaderFunction = async ({ params }) => {
  // return all the stuff needed for the post including
  // title, description, html, and more
  return getPost(params.slug);

export const meta: MetaFunction = ({ data }) => {
  return {
    title: `${data.title} | Ryan Chenkie`,
    description: data.description,
    "twitter:card": "summary_large_image",
    "og:description": data.description,
    "og:image": data.coverImage,

Other MetaFunction Params

There are other params we have access to in the meta function that might be useful.

Straight from the Remix docs:

meta function is passed an object that has following data:

  • data is whatever exported by loader function
  • location is a window.location-like object that has some data about the current route
  • params is an object containing route params
  • parentsData is a hashmap of all the data exported by loader functions of current route and all of its parents

Wrapping Up

I'm gaining more intuition about how Remix works but this was intially not very intuitive for me. Now that I know how it works, it's great and quite flexible.

Thanks for reading. Follow me on Twitter and say hi 👋