API reference

@scribe-atp/core

Functions

fetchSite

function fetchSite(
  author: string,
  publicationUrl: string,
  signal?: AbortSignal
): Promise<Site>

Fetches a site record from the author's PDS. Resolves the author handle to a DID, discovers the PDS, and returns the full Site with embedded group and article metadata.

ParameterTypeDescription
authorstringAuthor handle (alice.bsky.social) or DID (did:plc:…)
publicationUrlstringThe site's canonical HTTPS URL, e.g. "https://alice.bsky.social"
signalAbortSignalOptional. Cancel the request when the signal fires

fetchArticleBySlug

function fetchArticleBySlug(
  author: string,
  publicationUrl: string,
  articleSlug: string,
  signal?: AbortSignal
): Promise<ArticleResult>

Fetches a full article record and its AT URI in a single call. Resolves the site record first (using the publication URL cache if already fetched), then locates the article ref by slug and fetches the full article.

ParameterTypeDescription
authorstringAuthor handle or DID
publicationUrlstringThe site's canonical HTTPS URL, e.g. "https://alice.bsky.social"
articleSlugstringThe article's rkey / slug
signalAbortSignalOptional. Cancel the request when the signal fires

Returns { article: Article, uri: string }. The uri is the full AT URI of the site.standard.document record — pass it to @scribe-atp/social's LikeButton component.

This is the preferred function for article pages in server-rendered frameworks. Use fetchArticle only when you don't need the AT URI.


fetchArticle

function fetchArticle(
  author: string,
  articleSlug: string,
  signal?: AbortSignal
): Promise<Article>

Fetches a single article record (including full HTML content) directly by slug. Does not return the AT URI.

ParameterTypeDescription
authorstringAuthor handle or DID
articleSlugstringThe article's rkey / slug
signalAbortSignalOptional. Cancel the request when the signal fires

fetchProfile

function fetchProfile(
  handleOrDid: string,
  signal?: AbortSignal
): Promise<Profile>

Fetches a Bluesky profile — display name, avatar, bio, and related profile data — for a handle or DID. Calls the public, unauthenticated app.bsky.actor.getProfile endpoint directly; unlike fetchSite and fetchArticle, no PDS resolution step is needed since Bluesky's AppView resolves either a handle or a DID itself.

ParameterTypeDescription
handleOrDidstringBluesky handle (alice.bsky.social) or DID (did:plc:…)
signalAbortSignalOptional. Cancel the request when the signal fires

Use this to resolve the profile of an ArticleContributor for display — e.g. rendering an author byline card per Writer-role contributor on an article, falling back to the site owner's own profile when there are none.

const writers = article.contributors?.filter((c) => c.role === "Writer") ?? [];
const profiles = writers.length > 0
  ? await Promise.all(writers.map((w) => fetchProfile(w.did, signal)))
  : [await fetchProfile(SITE_AUTHOR, signal)];

resolvePublicationUri

function resolvePublicationUri(
  author: string,
  publicationUrl: string,
  signal?: AbortSignal
): Promise<string>

Resolves the AT URI of a site record given the author handle and publication URL. Results are cached for the lifetime of the module — repeated calls with the same arguments make no additional network requests.

ParameterTypeDescription
authorstringAuthor handle or DID
publicationUrlstringThe site's canonical HTTPS URL
signalAbortSignalOptional. Cancel the request when the signal fires

Returns a string AT URI, e.g. "at://did:plc:.../site.standard.publication/3mp4nd46xwr2h". Pass this to @scribe-atp/social's SubscribeButton component.


withRetry

function withRetry<T>(
  fn: () => Promise<T>,
  options?: {
    attempts?: number;
    delaysMs?: number[];
    signal?: AbortSignal;
  }
): Promise<T>

Retries a failing async call with backoff. Generic — wraps fetchSite, fetchArticle, fetchArticleBySlug, resolvePublicationUri, or any other async function.

ParameterTypeDescription
fn() => Promise<T>The call to retry, e.g. () => fetchSite(author, publicationUrl, signal)
options.attemptsnumberTotal attempts including the first. Default 5
options.delaysMsnumber[]Delay in ms before each retry. Default [300, 600, 1200, 2400]
options.signalAbortSignalOptional. Stops retrying immediately once aborted

Never retries NotFoundError or an aborted signal. Retries everything else, including plain Errors. Opt-in only — see Errors and retries for the full pattern, including pairing this with a loading state in server-rendered frameworks.


listSites

function listSites(
  author: string,
  signal?: AbortSignal
): Promise<SiteRecord[]>

Returns all site records for the given author. Calls com.atproto.repo.listRecords for the site.standard.publication collection and follows cursor pagination automatically.

ParameterTypeDescription
authorstringAuthor handle (alice.bsky.social) or DID (did:plc:…)
signalAbortSignalOptional. Cancel the request when the signal fires

Each SiteRecord is a Site with an additional uri field (the full AT URI of the record). Use slugFromUri(record.uri) to extract the rkey when you need to construct URLs.


listArticles

function listArticles(
  author: string,
  signal?: AbortSignal
): Promise<ArticleRef[]>

Returns all published article records for the given author as lightweight ArticleRef objects (no content field). Calls com.atproto.repo.listRecords for the site.standard.document collection and follows cursor pagination automatically.

ParameterTypeDescription
authorstringAuthor handle or DID
signalAbortSignalOptional. Cancel the request when the signal fires

Call fetchArticle to retrieve the full content for any article.


toSlug

function toSlug(domain: string): string

Derives a slug from a domain name by replacing . with - and removing non-alphanumeric characters.

toSlug('norobots.blog')        // → "norobots-blog"
toSlug('anthonycregan.co.uk')  // → "anthonycregan-co-uk"

toSlug is no longer needed for SDK calls — fetchSite, fetchArticleBySlug, and resolvePublicationUri now take a full HTTPS URL instead of a slug. It remains exported for any other use where slug-style strings are useful.


slugFromUri

function slugFromUri(uri: string): string

Extracts the rkey (slug) from an AT URI.

slugFromUri('at://did:plc:abc/site.standard.document/my-post') // → "my-post"

flattenArticles

function flattenArticles(
  groups: Array<{ articles: ArticleRef[] }>
): ArticleRef[]

Flattens all ArticleRef objects from all groups into a single ordered array.

const allArticles = flattenArticles(site.groups);

generateFeed

function generateFeed(site: Site, options: FeedOptions): string

Returns a complete RSS 2.0 XML string for all published articles in the site.

FeedOptions

PropertyTypeRequiredDescription
baseUrlstringYesYour site's origin, e.g. "https://alice.example.com"
feedUrlstringNoCanonical URL of the feed — used for the <atom:link> self-reference
languagestringNoRSS <language> tag. Default: "en"
limitnumberNoMaximum number of items to include

See the RSS feeds guide for usage examples.


getSitemapEntries

function getSitemapEntries(
  site: Site,
  options: GetSitemapEntriesOptions
): SitemapEntry[]

Returns an array of sitemap entries for all published articles (plus the site root and group index pages).

GetSitemapEntriesOptions

PropertyTypeRequiredDescription
baseUrlstringYesYour site's origin, e.g. "https://alice.example.com"

SitemapEntry

interface SitemapEntry {
  url: string;
  lastmod?: string; // ISO 8601 date, e.g. "2024-03-15"
}

See the Sitemaps guide for usage examples.


generateArticleMeta

function generateArticleMeta(article: Article, site: Site): ScribeMetaTag[]

Returns a framework-neutral array of meta tag descriptors for an article page. Covers og:type, og:title, og:url, og:site_name, og:description, og:image, twitter:card, twitter:title, twitter:description, and twitter:image.

ParameterTypeDescription
articleArticleThe full article object, as returned by fetchArticleBySlug or fetchArticle
siteSiteThe site object, used for og:site_name and canonical URL derivation

Pass the output to a framework adapter (articleMeta in @scribe-atp/react-router-framework, articleMetadata in @scribe-atp/next, articleSeoMeta in @scribe-atp/nuxt) or convert to your framework's format manually.

Also includes a { "script:ld+json": ... } tag generated internally via generateArticleJsonLd — you get JSON-LD structured data for free without calling it yourself. Use generateArticleJsonLd directly only if your framework's metadata API has no room for a raw <script> tag (e.g. Next.js's Metadata object) — see below.

See the Open Graph meta tags guide for framework-specific usage.


generateSiteMeta

function generateSiteMeta(site: Site): ScribeMetaTag[]

Returns meta tag descriptors for a site index or group page — covers title, og:type (website), and optionally description and splash image.

ParameterTypeDescription
siteSiteThe site object

buildCanonicalUrl

function buildCanonicalUrl(article: Article, site: Site): string

Derives the fully-qualified canonical URL for an article. Uses article.canonicalUrl if set; otherwise combines site.url (the site's domain, e.g. "myblog.com"), site.urlPrefix, and article.path to produce an https:// URL.

buildCanonicalUrl(article, site);
// → "https://alice.bsky.social/blog/my-first-post"

buildSiteUrl

function buildSiteUrl(site: Site): string

Derives the fully-qualified URL for a site's index page from site.url and site.urlPrefix.

ParameterTypeDescription
siteSiteThe site object
buildSiteUrl(site);
// → "https://alice.bsky.social/blog"

generateArticleJsonLd

function generateArticleJsonLd(article: Article, site: Site): JsonLdObject

Returns a schema.org BlogPosting object as a plain, JSON-serializable value — headline, publish/modified dates, author, publisher, and (when present) description, image, and keywords. generateArticleMeta already calls this internally and includes the result as a script:ld+json tag, so most consumers never need to call it directly.

ParameterTypeDescription
articleArticleThe full article object
siteSiteThe site object

Call this directly when your framework's metadata API has no place for a raw <script> tag and you need to render the JSON-LD <script type="application/ld+json"> element yourself. @scribe-atp/next's adapter needs this for exactly that reason — but note that generateArticleJsonLd isn't currently re-exported from @scribe-atp/next itself, so import it from @scribe-atp/core directly even in a Next.js project:

import { generateArticleJsonLd } from '@scribe-atp/core';

// in your page component
<script
  type="application/ld+json"
  dangerouslySetInnerHTML={{ __html: JSON.stringify(generateArticleJsonLd(article, site)) }}
/>

generateSiteJsonLd

function generateSiteJsonLd(site: Site): JsonLdObject

Returns a schema.org WebSite object — name, URL, and description if present. Same standalone-use case as generateArticleJsonLd above; not called automatically by generateSiteMeta's WebSite type (only articles get automatic JSON-LD via generateArticleMeta).

ParameterTypeDescription
siteSiteThe site object

Writing content

Every function above is a read — no authentication required. crossPostToBluesky is the one exception: it writes a record to the AT Protocol, so it needs an authenticated agent.

crossPostToBluesky

function crossPostToBluesky(
  agent: AtpAgentLike,
  params: CrossPostParams
): Promise<StrongRef>

Creates an app.bsky.feed.post with a rich link-card embed pointing at the article, plus associatedRefs linking the post back to the underlying site.standard.document and site.standard.publication records. This is what produces the Bluesky embed card and lets the bskyPostRef field be populated on the article record.

ParameterTypeDescription
agentAtpAgentLikeAn authenticated AT Protocol agent exposing com.atproto.repo.createRecord — e.g. an @atproto/api Agent instance with a valid OAuth session for the author's own account
paramsCrossPostParamsSee below

Returns a StrongRef ({ uri, cid }) pointing at the newly created post — write this to the article's bskyPostRef field yourself if you want readers of your own tooling to see the cross-post link.

This is a low-level primitive for building your own publishing tool against the SDK — SkyScribe already calls the equivalent logic server-side when an author uses its own "Share to Bluesky" action. Most SDK consumers (sites that only display published content) will never call this.

import { crossPostToBluesky } from '@scribe-atp/core';

const ref = await crossPostToBluesky(agent, {
  did: authorDid,
  documentUri: article.uri,
  documentCid: article.cid,
  publicationUri: site.uri,
  publicationCid: site.cid,
  canonicalUrl: buildCanonicalUrl(article, site),
  title: article.title,
  text: `New post: ${article.title}`,
  description: article.description,
});
// ref → { uri: "at://did:plc:.../app.bsky.feed.post/3mp...", cid: "..." }

Errors

NotFoundError

class NotFoundError extends Error {}

Thrown when a fetch succeeded but the site, publication, or article genuinely doesn't exist. Retrying will not help — treat it the same as an HTTP 404.

PdsFetchError

class PdsFetchError extends Error {
  constructor(message: string, options?: { cause?: unknown });
}

Thrown when a request reached the PDS but it responded with a non-ok HTTP status. The service is up — this specific operation failed. Safe to retry — see withRetry and the Errors and retries guide.

PdsUnreachableError

class PdsUnreachableError extends PdsFetchError {
  constructor(message: string, options?: { cause?: unknown });
}

Thrown when a request never got a response at all — DNS failure, connection refused, timeout. Extends PdsFetchError, so an instanceof PdsFetchError check matches this too; check instanceof PdsUnreachableError first if your UI distinguishes "this record had trouble loading" from "the service is down." Safe to retry.


Types

Site

interface Site {
  title: string;
  url: string;          // Domain without protocol, e.g. "alice.bsky.social"
  urlPrefix: string;    // Path prefix, e.g. "blog" — empty string if content is at root
  description?: string;
  splashImageUrl?: string;
  logoImageUrl?: string;
  groups: SiteGroup[];
  ungroupedArticles: ArticleRef[]; // legacy — always empty for current content, see Core Concepts
}

SiteGroup

interface SiteGroup {
  slug: string;
  title: string;
  articles: ArticleRef[];
}

ArticleRef

A lightweight snapshot of article metadata, embedded in the site record to avoid N+1 fetch patterns. Does not include content.

interface ArticleRef {
  uri: string;            // AT URI, e.g. "at://did:plc:…/site.standard.document/my-post"
  title: string;
  slug?: string;          // Article slug / rkey
  splashImageUrl: string | null;
  description?: string | null;
  tags?: string[];
  createdAt: string;      // ISO 8601
  publishedAt?: string;   // ISO 8601
  updatedAt?: string;     // ISO 8601
}

Article

The full article record, including HTML content. Returned by fetchArticle.

interface Article {
  title: string;
  content: string;         // Sanitised HTML — safe to render directly
  textContent?: string;    // Plain-text version of content (HTML tags stripped)
  path: string;            // e.g. "/creative-writing/my-post"
  site: string;            // AT URI of the publication, e.g. "at://did:plc:…/site.standard.publication/3abc"
  canonicalUrl?: string;   // Fully-qualified article URL, e.g. "https://myblog.com/blog/my-article"
  coverImageUrl?: string;  // URL of the cover/splash image
  description?: string;
  tags?: string[];
  contributors?: ArticleContributor[];
  bskyPostRef?: { uri: string; cid: string };  // Bluesky post reference if the article was cross-posted
  createdAt?: string;      // ISO 8601
  publishedAt: string;     // ISO 8601
  updatedAt: string;       // ISO 8601
}

ArticleContributor

Additional credited contributors on an article's byline — added manually per-article from the article editor (e.g. crediting an editor or illustrator alongside the author). Distinct from SkyScribe's separate site-level Contributor feature (an author invited to submit articles to someone else's site) — this array is not populated by that flow. An empty array is written on first publish if no contributors were added.

interface ArticleContributor {
  did: string;           // AT Protocol DID of the contributor
  role?: string;         // e.g. "author", "editor", "illustrator"
  displayName?: string;  // Human-readable name
}

Profile

Returned by fetchProfile. Mirrors Bluesky's app.bsky.actor.defs#profileViewDetailed shape.

interface Profile {
  did: string;
  handle: string;
  displayName?: string;
  description?: string;
  avatar?: string;
  banner?: string;
  pronouns?: string;
  website?: string;
  followersCount?: number;
  followsCount?: number;
  postsCount?: number;
  createdAt?: string;               // ISO 8601
  associated?: ProfileAssociated;
  pinnedPost?: { uri: string; cid: string };
  verification?: ProfileVerification;
  status?: ProfileStatus;
}

ProfileAssociated

interface ProfileAssociated {
  lists?: number;
  feedgens?: number;
  starterPacks?: number;
  labeler?: boolean;
}

ProfileVerification

interface ProfileVerification {
  verifiedStatus: string;
  trustedVerifierStatus: string;
  verifications: Array<{
    issuer: string;
    uri: string;
    isValid: boolean;
    createdAt: string;
  }>;
}

ProfileStatus

interface ProfileStatus {
  status: string;
  expiresAt?: string;
  isActive?: boolean;
  isDisabled?: boolean;
}

SiteRecord

A Site with the AT URI of its underlying record included. Returned by listSites.

interface SiteRecord extends Site {
  uri: string;  // AT URI, e.g. "at://did:plc:…/site.standard.publication/3mp4nd46xwr2h"
}

Use slugFromUri(record.uri) to extract the rkey when constructing URLs.

ArticleResult

Returned by fetchArticleBySlug. Contains the full article and its AT URI.

interface ArticleResult {
  article: Article;
  uri: string;  // AT URI, e.g. "at://did:plc:…/site.standard.document/3abc123"
}

ScribeMetaTag

The output element type of generateArticleMeta and generateSiteMeta. A union of five tag shapes:

type ScribeMetaTag =
  | { title: string }
  | { name: string; content: string }
  | { property: string; content: string }
  | { tagName: "link"; rel: string; href: string }
  | { "script:ld+json": JsonLdObject };

Framework adapters convert this to their own meta format. Consume it directly only if you're writing your own adapter or using a framework not covered by the existing packages. The script:ld+json variant is how generateArticleMeta delivers the JSON-LD structured data described under generateArticleJsonLd above.

JsonLdObject

A plain, JSON-serializable object — matches the shape consuming frameworks' own JSON-LD types expect (e.g. React Router's LdJsonObject) without this package depending on any framework's types directly.

type JsonLdObject = {
  [key: string]: string | number | boolean | null | JsonLdObject | JsonLdObject[];
};

CrossPostParams

Input to crossPostToBluesky.

interface CrossPostParams {
  did: string;              // The author's own DID — the repo the post is written to
  documentUri: string;      // AT URI of the site.standard.document being cross-posted
  documentCid: string;
  publicationUri: string;   // AT URI of the owning site.standard.publication
  publicationCid: string;
  canonicalUrl: string;     // The article's canonical URL — becomes the link-card target
  title: string;
  text: string;             // The post's own text content
  description?: string;
  thumbBlob?: unknown;      // A pre-uploaded blob ref for the link-card thumbnail, if any
}

StrongRef

An AT Protocol content-addressed reference — a URI plus the CID pinning it to a specific version of the record. Returned by crossPostToBluesky; also the shape of Article.bskyPostRef.

interface StrongRef {
  uri: string;
  cid: string;
}

Settings

Appearance