Skip to content

The article shape

Every read across every SDK returns the same Article. The type is hand-authored and stable, and it reflects a single rule: a published article is a complete article. Guaranteed fields are always present; only genuinely-optional fields are nullable.

interface Article {
id: string;
title: string; // your <h1>
slug: string; // your route param
markdown: string; // the body, as Markdown
seoTitle: string; // SEO <title>; falls back to title
seoDescription: string | null;
canonicalUrl: string | null;
formatType: string; // schema.org type hint, e.g. "Article"
inLanguage: string; // BCP-47, e.g. "en"
tags: string[]; // possibly empty
image: ArticleImage | null;
author: Author; // always present
publishedAt: string; // ISO-8601
createdAt: string; // ISO-8601
updatedAt: string; // ISO-8601
}
FieldTypeNullableNotes
idstringnoStable identifier; use as a React key.
titlestringnoThe on-page title (your <h1>).
slugstringnoURL-safe; your route param.
markdownstringnoThe article body as Markdown. Render with <ArticleContent>.
seoTitlestringnoThe SEO <title>. Server-side falls back to title, so it’s always set.
seoDescriptionstring | nullyesMeta description, or null — let the search engine derive one.
canonicalUrlstring | nullyesAbsolute canonical URL, or null if the site has no URL set.
formatTypestringnoschema.org type hint (e.g. "Article").
inLanguagestringnoBCP-47 tag for <html lang> / inLanguage.
tagsstring[]noShort topical tags; the array may be empty.
imageArticleImage | nullyesThe cover image, or null when none was generated.
authorAuthornoThe byline — always present.
publishedAtstringnoISO-8601 publish timestamp.
createdAtstringnoISO-8601 row-created timestamp.
updatedAtstringnoISO-8601 row-updated timestamp.

The cover is genuinely optional — image is null when an article has no generated cover.

interface ArticleImage {
url: string; // ready-to-use responsive URL (the default/large width)
alt: string; // may be "" for a decorative cover
width: number | null; // set <img width/height> from these to prevent layout shift
height: number | null;
srcset: string | null; // a responsive srcset string, or null
}
FieldTypeNullableNotes
urlstringnoWhen image is present, url is always present.
altstringnoPresent, but may be an empty string.
widthnumber | nullyesMaster width; pair with height to reserve space.
heightnumber | nullyesMaster height.
srcsetstring | nullyesDrop straight into <img srcset> when set.

The byline is always present. kind is what selects the structured-data type — see SEO & JSON-LD.

interface Author {
name: string;
slug: string;
bio: string | null;
avatarUrl: string | null;
jobTitle: string | null;
sameAs: string[]; // profile/social URLs → Person.sameAs
kind: "real" | "persona"; // "real" → Person, "persona" → Organization
}
FieldTypeNullableNotes
namestringnoDisplay name.
slugstringnoURL-safe author identifier.
biostring | nullyesShort biography, or null.
avatarUrlstring | nullyesAvatar image URL, or null.
jobTitlestring | nullyesRole/title (e.g. "Senior Engineer"), or null.
sameAsstring[]noProfile/social URLs; may be empty.
kind"real" | "persona"noDrives Person vs Organization JSON-LD.

The split is deliberate and load-bearing — you can rely on it in TypeScript without optional chaining where the contract guarantees presence.

  • Guaranteed (never null): markdown, seoTitle, publishedAt, and author — these have server-side fallbacks, so the feed never ships them empty.
  • Genuinely optional (null when absent): seoDescription, canonicalUrl, and image.

A published article that is missing a guaranteed field is a contract violation, not a null — the SDK throws a GhostwritrError with code INVALID_RESPONSE rather than coercing it. See Error handling.