API reference

@scribe-atp/react-router-framework

Functions

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.

ParameterTypeDescription
authorstringAuthor handle or DID
publicationUrlstringThe site's canonical HTTPS URL, e.g. "https://alice.bsky.social"
deps.fetchSitefunctionOptional. Override the fetch function — useful for testing
// app/routes/blog.tsx
export const loader = createSiteLoader('alice.bsky.social', 'https://alice.bsky.social');

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.

ParameterTypeDescription
authorstringAuthor handle or DID
publicationUrlstringThe site's canonical HTTPS URL, e.g. "https://alice.bsky.social"
slugParamstringOptional. URL param name to read the slug from. Defaults to "articleSlug"
deps.fetchArticleBySlugfunctionOptional. Override the fetch function — useful for testing
// app/routes/blog.$articleSlug.tsx
export 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

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.

ParameterTypeDescription
authorstringAuthor handle or DID
publicationUrlstringThe site's canonical HTTPS URL
deps.resolvePublicationUrifunctionOptional. Override the resolve function — useful for testing
// app/routes/well-known.ts
export const loader = createWellKnownLoader('alice.bsky.social', 'https://alice.bsky.social');

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.

ParameterTypeDescription
articleArticleThe full article object
siteSiteThe 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

function siteMeta(site: Site): MetaDescriptor[]

Returns MetaDescriptor[] for a site index or group page.

ParameterTypeDescription
siteSiteThe site object

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 });
  });
});

Types

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.

Settings

Appearance