Skip to content

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.

  1. Install the SDK (and the renderer used below):

    npm install @ghostwritr/react-router @ghostwritr/react
  2. 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
  3. Build the index route. Its loader runs on the server, so calling the feed there keeps your siteId server-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>
    );
    }
  4. Build the article route. fetchArticle returns null when the slug isn’t in the feed — turn that into a real 404 Response so 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>
    );
    }
  5. Add the meta export to the article route. One call returns <title>, description, Open Graph + Twitter cards, the canonical link, and schema.org Article JSON-LD. When the loader threw a 404, data is 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.

  • Loader patterns — list vs. article loaders, reading siteId from the environment, and why there’s no loader wrapper. See Writing loaders.
  • SEO meta — everything articleMeta emits and how to override the canonical URL. See articleMeta.
  • Publish instantly — verify the signed feed.updated webhook in a resource route to purge a CDN cache. See Feed-freshness webhook and Instant updates.
  • Error handlingnull vs. throw, and the typed GhostwritrError codes. See Error handling.