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
Section titled “fetchArticles”fetchArticles(opts: FeedFetchOptions): Promise<Article[]>Resolves the feed manifest, then walks each immutable build page in order, returning every published Article 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
GhostwritrErrorwith codeNOT_FOUND. It fails closed; there is no authed fallback.
fetchArticle
Section titled “fetchArticle”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.
import { fetchArticle } from "@ghostwritr/feed";
const article = await fetchArticle({ siteId, slug });if (!article) { // 404 in your router}Behavior:
- Unknown slug →
null— a404on theby-slugobject resolves tonull, not an error. Map it to your framework’s not-found. - Missing feed → throws — same
NOT_FOUNDcontract asfetchArticleswhen the manifest itself is absent.
toArticle
Section titled “toArticle”toArticle(raw: unknown): ArticleValidates 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.
Options
Section titled “Options”FeedFetchOptions:
| field | type | default |
|---|---|---|
siteId | string | — (required) |
staticBaseUrl | string | https://feeds.ghostwritr.io |
fetch | typeof fetch | global 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.
const articles = await fetchArticles({ siteId, fetch: (url, init) => fetch(url, { ...init, next: { revalidate: 3600, tags: ["gw-feed"] } }),});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.
What to reach for next
Section titled “What to reach for next”- Errors — the code union and how to catch them. See Error handling.
- Instant updates — verify the
feed.updatedwebhook to bust your cache. See Webhook helpers. - Full surface — every export, typed. See API reference.