Writing loaders
@ghostwritr/react-router ships no loader wrappers. You write a standard React Router loader() and call fetchArticles or fetchArticle inside it. Loaders run on the server, so the feed call — and your siteId — stay server-side, and you keep full control of your routing, caching, and error responses.
The site ID
Section titled “The site ID”siteId is the keyless read capability — there is no API key. Keep it in the environment and read it inside the loader (which runs on the server):
GHOSTWRITR_SITE_ID=your-site-idEvery fetcher call takes it as an option. A non-empty string is required; a blank one throws a GhostwritrError with code CONFIG.
The list loader
Section titled “The list loader”fetchArticles({ siteId }) returns every published Article, newest first:
import { fetchArticles } from "@ghostwritr/react-router";import { Link, useLoaderData } from "react-router";
export async function loader() { const articles = await fetchArticles({ siteId: process.env.GHOSTWRITR_SITE_ID! }); return { articles };}
export default function Blog() { const { articles } = useLoaderData<typeof loader>(); return ( <ul> {articles.map((a) => ( <li key={a.id}> <Link to={`/blog/${a.slug}`}>{a.title}</Link> </li> ))} </ul> );}fetchArticles walks every page of the feed (20 articles per page) and de-duplicates by id. A site that hasn’t published yet has no feed and throws GhostwritrError code NOT_FOUND; see Error handling.
The article loader
Section titled “The article loader”fetchArticle({ siteId, slug }) resolves a single article by slug, or returns null when the slug isn’t in the current feed. Translate that null into a real 404 Response so React Router renders your error boundary and sends the right status:
import { fetchArticle, articleMeta } from "@ghostwritr/react-router";import { ArticleContent } from "@ghostwritr/react";import { useLoaderData } from "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" }) : [];
export default function ArticlePage() { const { article } = useLoaderData<typeof loader>(); return ( <article> <h1>{article.title}</h1> <ArticleContent markdown={article.markdown} className="prose" /> </article> );}Freshness and caching
Section titled “Freshness and caching”React Router SSR runs your loader per request, so each render reads the current feed (the manifest is short-cached upstream). If you put a CDN in front of your loaders, point your feed-freshness webhook at a resource route that purges the cache on feed.updated — then new articles appear in seconds instead of waiting on a TTL. See Instant updates.
Overriding the feed origin or fetch
Section titled “Overriding the feed origin or fetch”Both fetchers accept the same FeedFetchOptions: staticBaseUrl to point at a different feed origin, and fetch to supply your own fetch (for caching, retries, or a test stub).
const articles = await fetchArticles({ siteId: process.env.GHOSTWRITR_SITE_ID!, fetch: (url, init) => fetch(url, { ...init, cache: "force-cache" }),});