@scribe-atp/react-router-framework
Functions
Section titled “Functions”createSiteLoader
Section titled “createSiteLoader”function createSiteLoader( author: string, publicationUrl: string, deps?: { fetchSite?: typeof fetchSite }): (args: LoaderFunctionArgs) => Promise<Site>Returns a React Router loader function that fetches a site. The loader passes request.signal automatically so the fetch is cancelled if the user navigates away.
| Parameter | Type | Description |
|---|---|---|
author |
string |
Author handle or DID |
publicationUrl |
string |
The site’s canonical HTTPS URL, e.g. "https://alice.bsky.social" |
deps.fetchSite |
function |
Optional. Override the fetch function — useful for testing |
export const loader = createSiteLoader('alice.bsky.social', 'https://alice.bsky.social');createArticleRouteLoader
Section titled “createArticleRouteLoader”function createArticleRouteLoader( author: string, publicationUrl: string, slugParam?: string, deps?: { fetchArticleBySlug?: typeof fetchArticleBySlug }): (args: LoaderFunctionArgs) => Promise<ArticleWithUri>Returns a React Router loader function that reads an article slug from URL params, fetches the full article and its AT URI, and returns ArticleWithUri. The loader passes request.signal automatically.
| Parameter | Type | Description |
|---|---|---|
author |
string |
Author handle or DID |
publicationUrl |
string |
The site’s canonical HTTPS URL, e.g. "https://alice.bsky.social" |
slugParam |
string |
Optional. URL param name to read the slug from. Defaults to "articleSlug" |
deps.fetchArticleBySlug |
function |
Optional. Override the fetch function — useful for testing |
// app/routes/blog.$articleSlug.tsxexport const loader = createArticleRouteLoader('alice.bsky.social', 'https://alice.bsky.social');
// custom param name:export const loader = createArticleRouteLoader('alice.bsky.social', 'https://alice.bsky.social', 'slug');The returned loader throws a 404 Response if the slug param is missing, and an error if the article is not found.
createWellKnownLoader
Section titled “createWellKnownLoader”function createWellKnownLoader( author: string, publicationUrl: string, deps?: { resolvePublicationUri?: typeof resolvePublicationUri }): (args: LoaderFunctionArgs) => Promise<Response>Returns a React Router loader function that responds with the author’s publication AT URI as plain text. Used to serve the /.well-known/ endpoint required for AT Protocol client discovery.
| Parameter | Type | Description |
|---|---|---|
author |
string |
Author handle or DID |
publicationUrl |
string |
The site’s canonical HTTPS URL |
deps.resolvePublicationUri |
function |
Optional. Override the resolve function — useful for testing |
export const loader = createWellKnownLoader('alice.bsky.social', 'https://alice.bsky.social');articleMeta
Section titled “articleMeta”function articleMeta(article: Article, site: Site): MetaDescriptor[]Returns a MetaDescriptor[] array ready to spread into a React Router v7/v8 meta function. Produces Open Graph and Twitter Card tags for rich link previews.
| Parameter | Type | Description |
|---|---|---|
article |
Article |
The full article object |
site |
Site |
The site object |
import { articleMeta } from '@scribe-atp/react-router-framework';import type { Route } from './+types/Article';
export function meta({ loaderData }: Route.MetaArgs) { if (!loaderData) return [{ title: 'My Blog' }]; return [ ...articleMeta(loaderData.article, loaderData.site), { title: `${loaderData.article.title} — My Blog` }, ];}The spread comes first so that your explicit { title } at the end takes precedence in the browser tab, while all OG/Twitter tags are populated automatically. See the Open Graph meta tags guide for the full pattern.
siteMeta
Section titled “siteMeta”function siteMeta(site: Site): MetaDescriptor[]Returns MetaDescriptor[] for a site index or group page.
| Parameter | Type | Description |
|---|---|---|
site |
Site |
The site object |
Testing
Section titled “Testing”All three factory functions accept an optional deps parameter for dependency injection. Pass mock functions directly — no vi.mock needed.
import { describe, it, expect, vi } from 'vitest';import { createSiteLoader, createArticleRouteLoader, createWellKnownLoader } from '@scribe-atp/react-router-framework';
describe('blog loaders', () => { it('fetches the site', async () => { const fetchSite = vi.fn().mockResolvedValueOnce(mockSite); const loader = createSiteLoader('alice.bsky.social', 'https://alice.bsky.social', { fetchSite }); const result = await loader({ request: new Request('https://example.com'), params: {}, context: {} }); expect(result).toEqual(mockSite); expect(fetchSite).toHaveBeenCalled(); });
it('fetches an article by slug', async () => { const fetchArticleBySlug = vi.fn().mockResolvedValueOnce({ article: mockArticle, uri: mockUri }); const loader = createArticleRouteLoader('alice.bsky.social', 'https://alice.bsky.social', 'articleSlug', { fetchArticleBySlug }); const result = await loader({ request: new Request('https://example.com'), params: { articleSlug: 'hello' }, context: {} }); expect(result).toEqual({ ...mockArticle, documentUri: mockUri }); });});ArticleWithUri
Section titled “ArticleWithUri”The return type of createArticleRouteLoader. Extends Article with the AT URI of the article record.
interface ArticleWithUri extends Article { documentUri: string; // AT URI, e.g. "at://did:plc:…/site.standard.document/3abc123"}Pass documentUri to @scribe-atp/social’s LikeButton component.
All types from @scribe-atp/core are also re-exported:
import type { Site, Article, ArticleRef, SiteGroup, ArticleWithUri } from '@scribe-atp/react-router-framework';See the core reference for full type definitions.