React Router — overview
@ghostwritr/react-router pulls your published articles into React Router v7 (the Remix successor) — you call the fetchers inside your own server loaders and get a one-call meta export for SEO + JSON-LD. It runs anywhere your loaders run: Node 20+ or an edge runtime.
It is keyless: your siteId is the read capability, there is no API key. Reads come straight from the static feed at feeds.ghostwritr.io/{siteId}/.
npm install @ghostwritr/react-router pnpm add @ghostwritr/react-router yarn add @ghostwritr/react-router bun add @ghostwritr/react-router What it gives you
Section titled “What it gives you”fetchArticles/fetchArticle— the keyless feed fetchers, for use inside your routeloaders.fetchArticlesreturns every published article newest-first;fetchArticlereturns one by slug, ornull.articleMeta(article, opts?)— turns an article into theMetaDescriptor[]your route’smetaexport returns:<title>, description, Open Graph + Twitter cards,<link rel="canonical">, and schema.orgArticleJSON-LD.verifyFeedSignature+FEED_SIGNATURE_HEADER— to verify the signedfeed.updatedwebhook when you cache the feed at a CDN. See Instant updates.- Types +
GhostwritrError— re-exported from the shared@ghostwritr/feedcore, so the contract is identical across the Next / Astro / React Router SDKs.
The 30-second example
Section titled “The 30-second example”A list loader and an article loader with SEO meta — both server-side:
import { fetchArticles } from "@ghostwritr/react-router";import { Link, useLoaderData } from "react-router";
export async function loader() { return { articles: await fetchArticles({ siteId: process.env.GHOSTWRITR_SITE_ID! }) };}
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> );}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 };}
// <title>, description, OG + Twitter, canonical, and schema.org Article JSON-LDexport 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> );}That is a complete, server-rendered blog reading from the keyless feed.
What to reach for next
Section titled “What to reach for next” Quickstart Wire the two routes end to end, step by step.
Writing loaders List and article loader patterns; siteId from the environment.
articleMeta Everything the SEO meta export emits, and how to override it.
API reference Every export, signature, and type.