Peekly

Open Graph tags in Remix

A route's meta function returns the descriptors Remix turns into meta tags. Render the Meta component inside the root head, or the document response has none of them. Deploy it, then paste the public URL into the checker above.

Free. No signup. Public pages only.

Add the tags

  • The meta export. Remix v2's meta docs say the function returns an array of descriptors, one object per tag. A title property becomes the title element. property becomes a meta property attribute, which is what og: tags use. name becomes a meta name attribute, which is what twitter:card uses. content is the attribute value. tagName set to link, with rel canonical, is how the same export prints the canonical URL. Put an absolute https URL in og:image. A path with no host is still a path in the HTML. summary_large_image is the large card on X. The v1 meta export was an object that merged by key. v2 does not do that merge for you. The How to Add Open Graph Tags page is the properties these descriptors emit. The Next.js Open Graph page is the same fields as a metadata export.
    import type { MetaFunction } from "@remix-run/node";
    
    export const meta: MetaFunction = () => {
      return [
        { title: "Post title" },
        { name: "description", content: "One or two sentences." },
        { property: "og:title", content: "Post title" },
        { property: "og:description", content: "One or two sentences." },
        { property: "og:url", content: "https://example.com/post" },
        { property: "og:type", content: "article" },
        { property: "og:image", content: "https://example.com/og.png" },
        { property: "og:image:width", content: "1200" },
        { property: "og:image:height", content: "630" },
        { name: "twitter:card", content: "summary_large_image" },
        { name: "twitter:image", content: "https://example.com/og.png" },
        { tagName: "link", rel: "canonical", href: "https://example.com/post" },
      ];
    };
  • Meta in the root head. The descriptors are rendered by the Meta component. Remix's docs put it in app/root.tsx, inside head, next to Links. Charset and the viewport tag are written as ordinary meta elements in that file, outside the meta export, so a child route cannot drop them by returning its own array. When Meta is missing from that document, the response has no og: tags, including on a route whose meta function is correct. A meta tag created in a component after hydration is not in the document request. That request is rendered on the server, and that HTML is what a crawler fetches. View source on the document response, not the head after the browser has navigated.
    import { Links, Meta, Outlet, Scripts } from "@remix-run/react";
    
    export default function App() {
      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>
      );
    }
  • A child route replaces the parent array. Remix uses the meta export from the last matching route that has one. A parent that sets og:image and a child that returns only a title leaves that child URL with no og:image. The merge the docs show is matches.flatMap of each match.meta. Spreading that array keeps the parent tags. It does not replace a title or an og:title already in the list, so the HTML can contain two of them. Filter the parent descriptors you are about to set again, then append the child's. The Open Graph protocol gives the first tag preference, and this checker reads the first og:title, og:description, and og:image. Two og:image tags means the card can show the parent's image. data on the meta arguments is the value the route's loader returned. When the loader throws, data is missing, so return the parent tags rather than reading fields off undefined. Build the image URL from the request origin. A root path in the loader is still a root path in the tag.
    import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node";
    import { json } from "@remix-run/node";
    
    const replaced = new Set([
      "og:title",
      "og:description",
      "og:url",
      "og:image",
      "twitter:card",
      "twitter:image",
    ]);
    
    export async function loader({ request }: LoaderFunctionArgs) {
      const origin = new URL(request.url).origin;
      return json({
        title: "Post title",
        description: "One or two sentences.",
        image: new URL("/og.png", origin).href,
      });
    }
    
    export const meta: MetaFunction<typeof loader> = ({ data, matches }) => {
      const parentMeta = matches
        .flatMap((match) => match.meta ?? [])
        .filter((meta) => !("title" in meta))
        .filter((meta) => {
          const property = "property" in meta ? meta.property : undefined;
          const name = "name" in meta ? meta.name : undefined;
          if (property && replaced.has(property)) return false;
          if (name && replaced.has(name)) return false;
          return true;
        });
      if (!data) return [...parentMeta, { title: "Not found" }];
      return [
        ...parentMeta,
        { title: data.title },
        { property: "og:title", content: data.title },
        { property: "og:description", content: data.description },
        { property: "og:image", content: data.image },
        { name: "twitter:card", content: "summary_large_image" },
        { name: "twitter:image", content: data.image },
      ];
    };
  • Check the document response. Paste the public URL. Peekly fetches the HTML and grades og:title, og:description, og:image, and twitter:card. It does not run the Remix router in a browser. When the result is missing tags that the meta export returns, Meta is absent from the head that was served, or a parent export is the last one and the child never merged. The Open Graph Checker is that grade. The Twitter Card Validator is the card. The Astro Open Graph, Nuxt Open Graph, and SvelteKit Open Graph pages are the same tags in those frameworks. The Open Graph Image Size page is the file the width and height descriptors should describe.

Tags covered: og:title · og:description · og:image · og:url · og:type · twitter:card · twitter:image

More checkers

Twitter Card ValidatorOpen Graph CheckerLinkedIn Link Preview CheckerDiscord Embed PreviewWhatsApp Link Preview CheckerSlack Link Preview CheckeriMessage Link Preview CheckerFacebook Link Preview CheckerOpen Graph Image SizeTelegram Link Preview CheckerReddit Link Preview CheckerNext.js Open GraphRefresh a Link Preview CacheBluesky Link Preview CheckerMastodon Link Preview CheckerMicrosoft Teams Link Preview CheckerFix a Broken Open Graph PreviewInstagram Link Preview CheckerPinterest Link Preview CheckerTikTok Link Preview CheckerYouTube Link Preview CheckerThreads Link Preview CheckerSnapchat Link Preview CheckerHow to Add Open Graph TagsOpen Graph Image Not UpdatingDiscord Link PreviewX (Twitter) Link PreviewWordPress Open GraphShopify Open GraphAstro Open GraphNuxt Open GraphSvelteKit Open GraphAll platforms

What each platform reads

  • X / Twittertwitter:card, twitter:* → og:* fallback1200×630 (min 300×157), < 5 MB
  • Facebookog:title, og:description, og:image1200×630, 1.91:1
  • LinkedInog:title, og:image (og: only)1200×627, 1.91:1
  • Discordog:site_name, og:*, theme-color, twitter:cardLarge image needs summary_large_image
  • Slackog:site_name, og:title, og:description, og:image1200×630
  • WhatsAppog:title, og:description, og:imageKeep < 300 KB

Guidance based on each platform's public documentation and observed behavior. Platforms change their rules without notice.

FAQ

+Is Peekly free?

Yes. Checking a single URL is free forever — no signup and no limit on one-off checks. Batch watching and monitoring may arrive later as a paid Pro plan; the free single-URL tool stays.

+Why does my link show no image when I share it?

Usually one of four things: there's no og:image tag, the image URL is relative instead of absolute, the image is behind a login, firewall or bot block, or the file is too big or in an unsupported format like SVG. Peekly checks all four and tells you which one it is.

+What size should my og:image be?

1200×630 pixels (a 1.91:1 ratio) as a PNG or JPG under about 300 KB. That size works for large cards on X, Facebook, LinkedIn, Discord, Slack and WhatsApp.

+Do I need twitter: tags if I already have Open Graph tags?

X reads og:title, og:description and og:image as fallbacks, but you still need twitter:card set to summary_large_image if you want the big image card. Without it, X shows a small thumbnail.

+I fixed my tags but the old preview still shows. Why?

Platforms cache link previews, sometimes for days. Facebook and LinkedIn let you force a refresh with their own debugging tools. On X, Slack and Discord the cache usually expires on its own, or you can add a query string such as ?v=2 to the URL.

+Do you store the pages I check?

No. We fetch the page once to read its <head>, show you the result, and don't save the page or the result. We only fetch public http(s) URLs, and private or internal addresses are blocked.

+Are the previews pixel-perfect?

They're close approximations of each platform's current card layout, built from the same tags the platforms read. Platforms change their designs from time to time, so treat them as a very good guide rather than a screenshot.

$9

One time

A written fix for the batch.

Up to 25 URLs. We check og:image, Twitter cards, size, redirects, and cache, then send copy-paste meta tags within 48 hours. The free single-URL check stays free.

Get the fix report — $9