Skip to content

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
  • fetchArticles / fetchArticle — the keyless feed fetchers, for use inside your route loaders. fetchArticles returns every published article newest-first; fetchArticle returns one by slug, or null.
  • articleMeta(article, opts?) — turns an article into the MetaDescriptor[] your route’s meta export returns: <title>, description, Open Graph + Twitter cards, <link rel="canonical">, and schema.org Article JSON-LD.
  • verifyFeedSignature + FEED_SIGNATURE_HEADER — to verify the signed feed.updated webhook when you cache the feed at a CDN. See Instant updates.
  • Types + GhostwritrError — re-exported from the shared @ghostwritr/feed core, so the contract is identical across the Next / Astro / React Router SDKs.

A list loader and an article loader with SEO meta — both server-side:

app/routes/blog._index.tsx
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>
);
}
app/routes/blog.$slug.tsx
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-LD
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>
);
}

That is a complete, server-rendered blog reading from the keyless feed.