React Router v7 quickstart
@ghostwritr/react-router is a keyless feed client for React Router v7 (the Remix successor). You call its fetchers inside your own server loaders and return the typed Article; articleMeta builds your route’s SEO meta. No API key — your siteId is the only key.
-
Install the SDK (and the renderer used below):
npm install @ghostwritr/react-router @ghostwritr/reactpnpm add @ghostwritr/react-router @ghostwritr/reactyarn add @ghostwritr/react-router @ghostwritr/reactbun add @ghostwritr/react-router @ghostwritr/react -
Add your site ID to your environment. It’s the only key you need — see Your site ID.
.env GHOSTWRITR_SITE_ID=your-site-id -
Build the index route. Its
loaderruns on the server, so calling the feed there keeps yoursiteIdserver-side:app/routes/blog._index.tsx 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>);} -
Build the article route.
fetchArticlereturnsnullwhen the slug isn’t in the feed — turn that into a real 404Responseso React Router renders your error boundary: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 };}export default function ArticlePage() {const { article } = useLoaderData<typeof loader>();return (<article><h1>{article.title}</h1><ArticleContent markdown={article.markdown} className="prose" /></article>);} -
Add the
metaexport to the article route. One call returns<title>, description, Open Graph + Twitter cards, the canonical link, and schema.orgArticleJSON-LD. When the loader threw a 404,datais undefined — return[]:app/routes/blog.$slug.tsx export const meta: Route.MetaFunction = ({ data }) =>data ? articleMeta(data.article, { siteName: "Your Brand" }) : [];
That’s a complete, server-rendered blog reading from the keyless feed. Start your dev server and open /blog.
What to reach for next
Section titled “What to reach for next”- Loader patterns — list vs. article loaders, reading
siteIdfrom the environment, and why there’s no loader wrapper. See Writing loaders. - SEO meta — everything
articleMetaemits and how to override the canonical URL. See articleMeta. - Publish instantly — verify the signed
feed.updatedwebhook in a resource route to purge a CDN cache. See Feed-freshness webhook and Instant updates. - Error handling —
nullvs. throw, and the typedGhostwritrErrorcodes. See Error handling.