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.
| 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 |
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.
| 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.
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.
| 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 |
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.
| 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)];
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.
| 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.
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.
| 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.
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.
| 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.
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.
| 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.
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
| 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.
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
| 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.
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.
| 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.
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.
| Parameter | Type | Description |
|---|---|---|
site | Site | The 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.
| Parameter | Type | Description |
|---|---|---|
site | Site | The 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.
| 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)) }}
/>
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).
| Parameter | Type | Description |
|---|---|---|
site | Site | The 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.
| 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 — 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;
}