Skip to content

Fetchers

Three functions read the content feed: fetchArticles for a list, fetchArticle for one by slug, and toArticle to validate and map a raw item yourself. All reads are keyless — the siteId is the only key. See keyless reads.

fetchArticles(opts: FeedFetchOptions): Promise<Article[]>

Resolves the feed manifest, then walks each immutable build page in order, returning every published Article newest first.

list, newest first
import { fetchArticles } from "@ghostwritr/feed";
const articles = await fetchArticles({ siteId });

Behavior:

  • Newest first — the feed’s native order; the first element is the most recent article.
  • De-duped by id — if an id somehow appears on more than one page, the first (newest) occurrence wins.
  • Empty feed → [] — a manifest with zero pages returns an empty array and fetches no pages.
  • Missing feed → throws — when the manifest is absent (a site that hasn’t been backfilled), it throws a GhostwritrError with code NOT_FOUND. It fails closed; there is no authed fallback.
fetchArticle(opts: FeedFetchOptions & { slug: string }): Promise<Article | null>

Resolves one article directly via its by-slug object — cheaper than walking every page for a dynamic [slug] route.

one by slug
import { fetchArticle } from "@ghostwritr/feed";
const article = await fetchArticle({ siteId, slug });
if (!article) {
// 404 in your router
}

Behavior:

  • Unknown slug → null — a 404 on the by-slug object resolves to null, not an error. Map it to your framework’s not-found.
  • Missing feed → throws — same NOT_FOUND contract as fetchArticles when the manifest itself is absent.
toArticle(raw: unknown): Article

Validates and maps a single raw feed item into a typed Article. The fetchers call it for you; reach for it directly only if you’re reading raw feed JSON yourself.

A published article is a complete article — so a missing or mistyped guaranteed field (id, title, slug, markdown, seoTitle, formatType, inLanguage, tags, publishedAt, createdAt, updatedAt, plus a well-formed author) throws INVALID_RESPONSE rather than being silently coerced. Unparseable timestamps throw too. Genuinely-optional fields (seoDescription, canonicalUrl, image) are lenient and fall back to null.

FeedFetchOptions:

fieldtypedefault
siteIdstring— (required)
staticBaseUrlstringhttps://feeds.ghostwritr.io
fetchtypeof fetchglobal fetch

opts.fetch — the caching / retry / test seam

Section titled “opts.fetch — the caching / retry / test seam”

Both fetchers route every request through opts.fetch, defaulting to the global fetch. This is the one seam where you add caching, retries, or a test stub — the core ships none of these itself.

inject caching (Next.js)
const articles = await fetchArticles({
siteId,
fetch: (url, init) =>
fetch(url, { ...init, next: { revalidate: 3600, tags: ["gw-feed"] } }),
});
stub it in a test
const articles = await fetchArticles({
siteId,
fetch: async (url) => new Response(JSON.stringify(fixtureFor(url))),
});

opts.staticBaseUrl — override the origin

Section titled “opts.staticBaseUrl — override the origin”

Defaults to https://feeds.ghostwritr.io (exported as DEFAULT_STATIC_BASE_URL). Override it to point at a self-hosted mirror or a fixture server. A trailing slash is fine; an invalid URL throws CONFIG.

  • Errors — the code union and how to catch them. See Error handling.
  • Instant updates — verify the feed.updated webhook to bust your cache. See Webhook helpers.
  • Full surface — every export, typed. See API reference.