Skip to content

SEO & JSON-LD

Every field you need for on-page SEO and structured data ships inside the Article. There is nothing to compute and nothing to fetch separately — map the fields to meta tags and JSON-LD.

FieldUse it forNotes
seoTitle<title>Always present — falls back to title server-side. Use it directly.
seoDescription<meta name="description">string | null. When null, omit the tag and let the search engine derive one.
canonicalUrl<link rel="canonical">string | null. null only when the site has no URL set.
inLanguage<html lang> / JSON-LD inLanguageBCP-47 tag, e.g. "en".
formatTypeJSON-LD @type hintschema.org type, e.g. "Article".
tagskeywords / topical metadataMay be empty.

Because seoTitle is guaranteed and the two nullable fields are explicitly typed, you can emit clean head tags without guesswork:

const head = {
title: article.seoTitle,
description: article.seoDescription ?? undefined, // omit when null
canonical: article.canonicalUrl ?? undefined,
lang: article.inLanguage,
};

When present, image is everything you need for an Open Graph card and a layout-shift-free render in one object.

interface ArticleImage {
url: string; // og:image and your <img src>
alt: string; // <img alt>
width: number | null; // set <img width/height> from these
height: number | null;
srcset: string | null; // <img srcset> for responsive sources
}
  • Use image.url for og:image (and twitter:image).
  • Set <img width> / <img height> from image.width / image.height so the browser reserves space and you avoid cumulative layout shift.
  • Drop image.srcset straight into <img srcset> when it’s set.

image is null when the article has no generated cover — fall back to a site-level default OG image, or omit the card.

The author’s kind is the switch between a Person and an Organization in your structured data.

author.kindJSON-LD author @typeWhen
"real"PersonA real human byline. sameAs carries their profile URLs.
"persona"OrganizationA brand byline rather than an individual.
function authorLd(author: Article["author"]) {
if (author.kind === "real") {
return {
"@type": "Person",
name: author.name,
jobTitle: author.jobTitle ?? undefined,
image: author.avatarUrl ?? undefined,
description: author.bio ?? undefined,
sameAs: author.sameAs.length ? author.sameAs : undefined,
};
}
return {
"@type": "Organization",
name: author.name,
sameAs: author.sameAs.length ? author.sameAs : undefined,
};
}

A complete Article JSON-LD node then assembles from the same fields:

const articleLd = {
"@context": "https://schema.org",
"@type": article.formatType, // e.g. "Article"
headline: article.title,
inLanguage: article.inLanguage,
datePublished: article.publishedAt,
dateModified: article.updatedAt,
description: article.seoDescription ?? undefined,
image: article.image?.url,
mainEntityOfPage: article.canonicalUrl ?? undefined,
author: authorLd(article.author),
};