Skip to content

SEO meta (articleMeta)

articleMeta(article, opts?) turns an Article into the MetaDescriptor[] that a React Router route’s meta export returns. One call emits the <title>, meta description, Open Graph + Twitter cards, the canonical <link>, and schema.org Article JSON-LD — all driven by the feed’s guarantees. See SEO & JSON-LD for the why.

Return it from the article route’s meta export. When the loader threw a 404, data is undefined, so guard and return []:

app/routes/blog.$slug.tsx
import { fetchArticle, articleMeta } from "@ghostwritr/react-router";
import type { Route } from "./+types/blog.$slug";
export async function loader({ params }: Route.LoaderArgs) {
const article = await fetchArticle({
siteId: process.env.GHOSTWRITR_SITE_ID!,
slug: params.slug!,
});
if (!article) throw new Response("Not found", { status: 404 });
return { article };
}
export const meta: Route.MetaFunction = ({ data }) =>
data ? articleMeta(data.article, { siteName: "Your Brand" }) : [];

ArticleMetaOptions is optional — every field within it is too.

OptionTypeEffect
urlstringAbsolute URL of this page. Sets <link rel="canonical"> and og:url. Falls back to article.canonicalUrl. Pass your own when self-hosting under a different domain.
siteNamestringSets og:site_name and becomes the JSON-LD publisher (an Organization).
twitterSitestringSets twitter:site (your default @handle).

From a typical article, articleMeta produces these descriptors:

  • { title: article.seoTitle } — the <title>. seoTitle always falls back to title upstream, so it’s never empty.
  • { name: "description", content: article.seoDescription } — only when seoDescription is set.
  • og:type = "article", og:title = seoTitle, and og:description (when present).
  • og:url (when a canonical resolves), og:site_name (when siteName is given), og:image (when the article has an image).
  • article:published_time = publishedAt, article:modified_time = updatedAt, and one article:tag per entry in tags.
  • twitter:card = "summary_large_image" when the article has an image, otherwise "summary"; plus twitter:site when twitterSite is given.
  • { tagName: "link", rel: "canonical", href } — when a canonical resolves.
  • { "script:ld+json": ... } — the schema.org Article JSON-LD (below).

The injected Article JSON-LD carries headline, description (when set), url + mainEntityOfPage (when a canonical resolves), image (when present), datePublished, dateModified, inLanguage, the author, and publisher (when siteName is given).

The author shape follows author.kind:

  • "real" → a Person with name, plus jobTitle and sameAs when present.
  • "persona" → an Organization byline (just name). Ghost Writr never fabricates a human for a brand voice — see SEO & JSON-LD.