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.
Install
Section titled “Install”npm install @scribe-atp/react-router-frameworkSite index route
Section titled “Site index route”Use createSiteLoader to create a loader for a route that displays the full site — groups and article listings.
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> );}Dynamic article route
Section titled “Dynamic article route”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.tsximport { 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.
Open Graph meta tags
Section titled “Open Graph meta tags”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.
TypeScript types
Section titled “TypeScript types”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';