Skip to content

React Router v7

@scribe-atp/react-router-framework provides loader factories for React Router v7 framework mode. The factories wire up request.signal automatically so fetches are cancelled if the user navigates away before the response arrives.

Terminal window
npm install @scribe-atp/react-router-framework

Use createSiteLoader to create a loader for a route that displays the full site — groups and article listings.

app/routes/blog.tsx
import { createSiteLoader } from '@scribe-atp/react-router-framework';
import { useLoaderData } from 'react-router';
export const loader = createSiteLoader('alice.bsky.social', 'https://alice.bsky.social');
export default function Blog() {
const site = useLoaderData<typeof loader>();
return (
<main>
<h1>{site.title}</h1>
{site.groups.map((group) => (
<section key={group.slug}>
<h2>{group.title}</h2>
<ul>
{group.articles.map((article) => (
<li key={article.uri}>
<a href={`/blog/${group.slug}/${article.slug}`}>{article.title}</a>
</li>
))}
</ul>
</section>
))}
</main>
);
}

Use createArticleRouteLoader for routes where the slug comes from URL params. It fetches the article and its AT URI and returns ArticleWithUri{ ...article, documentUri }.

// app/routes/blog.$group.$articleSlug.tsx
import { createArticleRouteLoader } from '@scribe-atp/react-router-framework';
import { useLoaderData } from 'react-router';
export const loader = createArticleRouteLoader('alice.bsky.social', 'https://alice.bsky.social');
export default function Article() {
const { title, content, documentUri } = useLoaderData<typeof loader>();
return (
<article>
<h1>{title}</h1>
<div dangerouslySetInnerHTML={{ __html: content }} />
</article>
);
}

Pass documentUri to @scribe-atp/social’s LikeButton if you want social interactions on the article page.

Use articleMeta to populate Open Graph and Twitter Card tags for rich link previews. Your loader must also fetch the site object:

import { createArticleRouteLoader, articleMeta } from '@scribe-atp/react-router-framework';
import { fetchSite } from '@scribe-atp/core';
import type { Route } from './+types/Article';
export async function loader({ request, params }: Route.LoaderArgs) {
const [articleData, site] = await Promise.all([
createArticleRouteLoader('alice.bsky.social', 'https://alice.bsky.social')({ request, params }),
fetchSite('alice.bsky.social', 'https://alice.bsky.social', request.signal),
]);
return { ...articleData, publicationUri: site.uri, site };
}
export function meta({ loaderData }: Route.MetaArgs) {
if (!loaderData) return [{ title: 'My Blog' }];
return [
...articleMeta(loaderData, loaderData.site),
{ title: `${loaderData.title} | My Blog` },
];
}

See the Open Graph meta tags guide for more detail.

All types from @scribe-atp/core are re-exported from @scribe-atp/react-router-framework, plus ArticleWithUri:

import type { Site, Article, ArticleRef, SiteGroup, ArticleWithUri } from '@scribe-atp/react-router-framework';