Skip to content

@scribe-atp/core

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.

Parameter Type Description
author string Author handle (alice.bsky.social) or DID (did:plc:…)
publicationUrl string The site’s canonical HTTPS URL, e.g. "https://alice.bsky.social"
signal AbortSignal Optional. Cancel the request when the signal fires

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.

Parameter Type Description
author string Author handle or DID
publicationUrl string The site’s canonical HTTPS URL, e.g. "https://alice.bsky.social"
articleSlug string The article’s rkey / slug
signal AbortSignal Optional. 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.


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.

Parameter Type Description
author string Author handle or DID
articleSlug string The article’s rkey / slug
signal AbortSignal Optional. Cancel the request when the signal fires

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.

Parameter Type Description
handleOrDid string Bluesky handle (alice.bsky.social) or DID (did:plc:…)
signal AbortSignal Optional. 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)];

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.

Parameter Type Description
author string Author handle or DID
publicationUrl string The site’s canonical HTTPS URL
signal AbortSignal Optional. 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.


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.

Parameter Type Description
fn () => Promise<T> The call to retry, e.g. () => fetchSite(author, publicationUrl, signal)
options.attempts number Total attempts including the first. Default 5
options.delaysMs number[] Delay in ms before each retry. Default [300, 600, 1200, 2400]
options.signal AbortSignal Optional. 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.


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.

Parameter Type Description
author string Author handle (alice.bsky.social) or DID (did:plc:…)
signal AbortSignal Optional. 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.


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.

Parameter Type Description
author string Author handle or DID
signal AbortSignal Optional. Cancel the request when the signal fires

Call fetchArticle to retrieve the full content for any article.


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.


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"

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

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

const allArticles = flattenArticles(site.groups);

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

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

FeedOptions

Property Type Required Description
baseUrl string Yes Your site’s origin, e.g. "https://alice.example.com"
feedUrl string No Canonical URL of the feed — used for the <atom:link> self-reference
language string No RSS <language> tag. Default: "en"
limit number No Maximum number of items to include

See the RSS feeds guide for usage examples.


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

Property Type Required Description
baseUrl string Yes Your 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.


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.

Parameter Type Description
article Article The full article object, as returned by fetchArticleBySlug or fetchArticle
site Site The 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.


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.

Parameter Type Description
site Site The site object

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"

function buildSiteUrl(site: Site): string

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

Parameter Type Description
site Site The site object
buildSiteUrl(site);
// → "https://alice.bsky.social/blog"

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.

Parameter Type Description
article Article The full article object
site Site The 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)) }}
/>

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).

Parameter Type Description
site Site The site object

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.

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.

Parameter Type Description
agent AtpAgentLike An 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
params CrossPostParams See 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 — Scribe CMS already calls the equivalent logic server-side when an author uses its own “Share to Bluesky” action. Most SDK consumers (sites that only display Scribe 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: "..." }

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.

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.

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.


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
}
interface SiteGroup {
slug: string;
title: string;
articles: 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
}

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
}

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 Scribe CMS’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
}

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;
}
interface ProfileAssociated {
lists?: number;
feedgens?: number;
starterPacks?: number;
labeler?: boolean;
}
interface ProfileVerification {
verifiedStatus: string;
trustedVerifierStatus: string;
verifications: Array<{
issuer: string;
uri: string;
isValid: boolean;
createdAt: string;
}>;
}
interface ProfileStatus {
status: string;
expiresAt?: string;
isActive?: boolean;
isDisabled?: boolean;
}

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.

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"
}

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.

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[];
};

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
}

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