API reference
Framework-agnostic functions and types. Every other package is built on these. Install with npm install @skyscribe-sdk/core.
fetchSitefunction fetchSite(
author: string,
publicationUrl: string,
signal?: AbortSignal
): Promise<Site>
Fetches a Site from the author's PDS: resolves the author to a DID, finds their PDS, looks up the Site record whose URL matches publicationUrl, and returns the Site with its Groups and ArticleRefs. Throws NotFoundError if no Site matches.
| 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 |
fetchArticleBySlugfunction fetchArticleBySlug(
author: string,
publicationUrl: string,
articleSlug: string,
signal?: AbortSignal
): Promise<ArticleResult>
Fetches a full article and its AT URI by the article's slug. Fetches the Site first, finds the ArticleRef with that slug, then fetches the article from the account the ref points to (which can be a contributor's, not the site owner's). Throws NotFoundError if no published article has that slug.
| 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 slug, as in its URL. (For very old articles whose rkey was their slug, the rkey also matches.) |
signal | AbortSignal | Optional. Cancel the request when the signal fires |
Returns { article: Article, uri: string }. uri is the full AT URI of the site.standard.document record: pass it to @skyscribe-sdk/social's buttons.
This is the function to use for article pages, which usually have the slug from the URL.
fetchArticlefunction fetchArticle(
author: string,
rkey: string,
signal?: AbortSignal
): Promise<Article>
Fetches a single article record, including its full HTML content, by its record key (rkey). Doesn't return the AT URI.
| Parameter | Type | Description |
|---|---|---|
author | string | Handle or DID of the account the article is stored in |
rkey | string | The article's record key. Get it from an ArticleRef with slugFromUri(ref.uri) |
Careful
Articles published with current tooling have opaque rkeys like 3mp47vvkh342n, so passing a human-readable slug only finds very old articles. Use fetchArticleBySlug when you have a slug. When you have an ArticleRef, take the account from its AT URI too (ref.uri.split('/')[2]), since an article can live in a contributor's account.
| signal | AbortSignal | Optional. Cancel the request when the signal fires |
fetchProfilefunction fetchProfile(
handleOrDid: string,
signal?: AbortSignal
): Promise<Profile>
Fetches a Bluesky profile (display name, avatar, bio and related data) for a handle or DID. It calls Bluesky's public app.bsky.actor.getProfile endpoint, which accepts either, so there's no PDS lookup.
| 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 it to show contributors, for example a byline card for each Writer contributor on an article, falling back to the site owner's 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)];
resolvePublicationUrifunction resolvePublicationUri(
author: string,
publicationUrl: string,
signal?: AbortSignal
): Promise<string>
Returns the AT URI of the Site record for the author and URL, without fetching the whole Site's content. Results are cached in memory, so repeated calls with the same arguments make no further 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 an AT URI, e.g. "at://did:plc:.../site.standard.publication/3mp4nd46xwr2h". Pass it to @skyscribe-sdk/social's buttons as publicationUri. (A fetched Site has the same value as site.uri.)
withRetryfunction withRetry<T>(
fn: () => Promise<T>,
options?: {
attempts?: number;
delaysMs?: number[];
signal?: AbortSignal;
}
): Promise<T>
Retries a failing async call with backoff. It's generic: it wraps any of the fetch functions, 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. Nothing retries unless you use this: see Errors and retries for the full pattern.
listSitesfunction listSites(
author: string,
signal?: AbortSignal
): Promise<SiteRecord[]>
Returns every Site (publication record) in the author's account, following pagination automatically. Records without a scribe object (made by other site.standard tools) are skipped.
| Parameter | Type | Description |
|---|---|---|
author | string | Author handle (alice.bsky.social) or DID (did:plc:…) |
signal | AbortSignal | Optional. Cancel the request when the signal fires |
SiteRecord is the same shape as Site.
listArticlesfunction listArticles(
author: string,
signal?: AbortSignal
): Promise<ArticleRef[]>
Returns every article record in the author's account, drafts included, as lightweight ArticleRef objects without content. Follows pagination automatically.
| Parameter | Type | Description |
|---|---|---|
author | string | Author handle or DID |
signal | AbortSignal | Optional. Cancel the request when the signal fires |
Each ref's slug comes from the last segment of the article's path; drafts, which have no path yet, get their rkey instead. splashImageUrl holds the article's cover image. To fetch an article's full content, pass its account and rkey (from ref.uri) to fetchArticle.
Note
Use this to list everything an author has written, drafts included. To list what's published on a website, use fetchSite and its Groups, which have the authors' ordering.
toSlugfunction toSlug(domain: string): string
Turns a domain name into a slug by replacing . with - and removing other characters that aren't lowercase letters, digits or hyphens.
toSlug('norobots.blog') // → "norobots-blog"
toSlug('anthonycregan.co.uk') // → "anthonycregan-co-uk"
No SDK function needs it any more: they all take the site's full URL. It's still exported for other uses.
slugFromUrifunction slugFromUri(uri: string): string
Returns the last segment of an AT URI: the record key (rkey). Despite the name, for current articles that's an opaque TID, not the human-readable slug.
slugFromUri('at://did:plc:abc/site.standard.document/3mp47vvkh342n') // → "3mp47vvkh342n"
flattenArticlesfunction flattenArticles(
groups: Array<{ articles: ArticleRef[] }>
): ArticleRef[]
Flattens the ArticleRefs from all groups into one array, in Group order.
const allArticles = flattenArticles(site.groups);
generateFeedfunction generateFeed(site: Site, options: FeedOptions): string
Returns a complete RSS 2.0 XML string for the Site's published articles, in Group order.
FeedOptions
| Property | Type | Required | Description |
|---|---|---|---|
baseUrl | string | Yes | Your site's origin, e.g. "https://alice.example.com" |
feedUrl | string | No | The feed's own URL, used for the <atom:link rel="self"> |
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.
getSitemapEntriesfunction getSitemapEntries(
site: Site,
options: GetSitemapEntriesOptions
): SitemapEntry[]
Returns sitemap entries for the home page, the path-prefix root (if any), each Group page and each published article.
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; // date only, e.g. "2024-03-15" (articles only, from updatedAt)
}
See the Sitemaps guide for usage examples.
generateArticleMetafunction generateArticleMeta(article: Article, site: Site): ScribeMetaTag[]
Returns a framework-neutral array of tag descriptors for an article page: the <title>, a canonical <link>, description, og:type, og:title, og:url, og:site_name, og:description, og:image (the cover image), twitter:card, twitter:title, twitter:description, twitter:image, and JSON-LD structured data.
| Parameter | Type | Description |
|---|---|---|
article | Article | The full article object, as returned by fetchArticleBySlug or fetchArticle |
site | Site | The site, used for og:site_name, the title and the publisher in the JSON-LD |
The framework packages have their own equivalents in their framework's format (articleMeta in @skyscribe-sdk/react-router-framework, articleMetadata in @skyscribe-sdk/next, articleSeoMeta in @skyscribe-sdk/nuxt). Use this one directly for other frameworks, converting each descriptor to your framework's format.
The structured data comes as a { "script:ld+json": ... } descriptor built with generateArticleJsonLd, so you don't need to call that yourself unless your framework's metadata API has no room for a <script> tag (see below).
See the meta tags guide for framework-specific usage.
generateSiteMetafunction generateSiteMeta(site: Site): ScribeMetaTag[]
Returns tag descriptors for a site's index or group pages: the title, a canonical <link>, og:type (website), og:title, og:url, og:site_name and Twitter Card tags, plus the description and splash image when set, and JSON-LD structured data from generateSiteJsonLd.
| Parameter | Type | Description |
|---|---|---|
site | Site | The site object |
buildCanonicalUrlfunction buildCanonicalUrl(article: Article, site: Site): string
Returns an article's full canonical URL: the canonicalUrl SkyScribe stored when the article was published. For very old records without one, it builds it from site.url and article.path, adding site.urlPrefix only if the path doesn't already start with it.
buildCanonicalUrl(article, site);
// → "https://alice.bsky.social/blog/essays/my-first-post"
buildSiteUrlfunction buildSiteUrl(site: Site): string
Returns the full URL of 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"
generateArticleJsonLdfunction generateArticleJsonLd(article: Article, site: Site): JsonLdObject
Returns a schema.org BlogPosting as a plain, JSON-serialisable object: headline, publish and modified dates, author (the first contributor's name, or the site's title), publisher, and the description, cover image and keywords when present. generateArticleMeta already includes it, so you rarely need to call it directly.
| Parameter | Type | Description |
|---|---|---|
article | Article | The full article object |
site | Site | The site object |
Call it directly when your framework's metadata API has no place for a <script> tag, so you render <script type="application/ld+json"> yourself. Next.js's Metadata object is one example. It isn't re-exported from @skyscribe-sdk/next, so import it from @skyscribe-sdk/core even in a Next.js project:
import { generateArticleJsonLd } from '@skyscribe-sdk/core';
// in your page component
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(generateArticleJsonLd(article, site)) }}
/>
generateSiteJsonLdfunction generateSiteJsonLd(site: Site): JsonLdObject
Returns a schema.org WebSite object: name, URL, and the description if present. generateSiteMeta already includes it; call it directly for the same reason as generateArticleJsonLd above.
| Parameter | Type | Description |
|---|---|---|
site | Site | The site object |
Every function above only reads, and needs no login. crossPostToBluesky is the one exception: it writes a record, so it needs an authenticated agent.
crossPostToBlueskyfunction crossPostToBluesky(
agent: AtpAgentLike,
params: CrossPostParams
): Promise<StrongRef>
Creates a Bluesky post (app.bsky.feed.post) with a link card pointing at the article, plus associatedRefs linking the post back to its site.standard.document and site.standard.publication records. The result is what goes in an article's bskyPostRef.
| Parameter | Type | Description |
|---|---|---|
agent | AtpAgentLike | An authenticated AT Protocol agent with com.atproto.repo.createRecord, e.g. an @atproto/api Agent with an OAuth session for the author's own account |
params | CrossPostParams | See below |
Returns a StrongRef ({ uri, cid }) for the new post. Write it to the document's bskyPostRef field yourself, so websites can link to the post.
This is a low-level building block for your own publishing tool. SkyScribe does the same thing when an author uses Share to Bluesky, so websites that only display content never need it. The document and publication URIs and CIDs come from your tool's own createRecord / getRecord responses: the SDK's Article and Site types don't carry CIDs.
import { crossPostToBluesky, buildCanonicalUrl } from '@skyscribe-sdk/core';
const ref = await crossPostToBluesky(agent, {
did: authorDid,
documentUri,
documentCid,
publicationUri,
publicationCid,
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: "..." }
NotFoundErrorclass NotFoundError extends Error {}
Thrown when the request worked but the site or article doesn't exist. Retrying won't help: treat it like an HTTP 404.
PdsFetchErrorclass PdsFetchError extends Error {
constructor(message: string, options?: { cause?: unknown });
}
Thrown when the PDS responded with an error status. The service is up; this operation failed. Safe to retry: see withRetry and Errors and retries.
PdsUnreachableErrorclass PdsUnreachableError extends PdsFetchError {
constructor(message: string, options?: { cause?: unknown });
}
Thrown when a request never got a response: DNS failure, connection refused, timeout. It extends PdsFetchError, so instanceof PdsFetchError matches it too; check instanceof PdsUnreachableError first if your UI tells "this record had trouble loading" apart from "the service is down". Safe to retry.
Siteinterface Site {
uri: string; // AT URI of the publication record
title: string;
url: string; // Domain without protocol, e.g. "alice.bsky.social"
urlPrefix: string; // Path prefix, e.g. "blog", or "" if content is at the root
description?: string;
splashImageUrl?: string;
logoImageUrl?: string;
groups: SiteGroup[];
ungroupedArticles: ArticleRef[]; // legacy: empty for current content (see Concepts)
}
SiteGroupinterface SiteGroup {
slug: string;
title: string;
articles: ArticleRef[];
}
ArticleRefA lightweight snapshot of an article's metadata, stored in the Site record so you can list articles without fetching each one. Doesn't include content.
interface ArticleRef {
uri: string; // AT URI, e.g. "at://did:plc:…/site.standard.document/3mp47vvkh342n"
title: string;
slug?: string; // Human-readable slug, as in the article's URL
splashImageUrl: string | null;
description?: string | null;
tags?: string[];
contributors?: ArticleContributor[];
createdAt: string; // ISO 8601
publishedAt?: string; // ISO 8601
updatedAt?: string; // ISO 8601
}
ArticleThe full article, including its HTML content. Returned by fetchArticle and fetchArticleBySlug.
interface Article {
title: string;
content: string; // HTML, as stored. Sanitise it if you render accounts you don't control
textContent?: string; // Plain-text version of content (HTML tags stripped)
path: string; // e.g. "/blog/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 image
description?: string;
tags?: string[];
contributors?: ArticleContributor[];
bskyPostRef?: { uri: string; cid: string }; // set if the article was shared to Bluesky
createdAt?: string; // ISO 8601
publishedAt: string; // ISO 8601
updatedAt: string; // ISO 8601
}
ArticleContributorPeople credited on an article's byline, such as a co-writer, editor or illustrator, added by the author. SkyScribe calls them Contributors. They're not the same as SkyScribe's Collaborators, who are people invited to work on a Publication.
interface ArticleContributor {
did: string; // AT Protocol DID of the contributor
role?: string; // e.g. "author", "editor", "illustrator"
displayName?: string; // Human-readable name
}
ProfileReturned 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;
}
ProfileAssociatedinterface ProfileAssociated {
lists?: number;
feedgens?: number;
starterPacks?: number;
labeler?: boolean;
}
ProfileVerificationinterface ProfileVerification {
verifiedStatus: string;
trustedVerifierStatus: string;
verifications: Array<{
issuer: string;
uri: string;
isValid: boolean;
createdAt: string;
}>;
}
ProfileStatusinterface ProfileStatus {
status: string;
expiresAt?: string;
isActive?: boolean;
isDisabled?: boolean;
}
SiteRecordReturned by listSites. The same shape as Site, kept as a separate name for compatibility.
interface SiteRecord extends Site {}
ArticleResultReturned 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"
}
ScribeMetaTagThe element type of generateArticleMeta and generateSiteMeta's output. One of five shapes:
type ScribeMetaTag =
| { title: string }
| { name: string; content: string }
| { property: string; content: string }
| { tagName: "link"; rel: string; href: string }
| { "script:ld+json": JsonLdObject };
You only handle it directly for frameworks without an SDK package. The script:ld+json shape carries the structured data from generateArticleJsonLd.
JsonLdObjectA plain, JSON-serialisable object, compatible with frameworks' own JSON-LD types (such as React Router's LdJsonObject).
type JsonLdObject = {
[key: string]: string | number | boolean | null | JsonLdObject | JsonLdObject[];
};
CrossPostParamsInput to crossPostToBluesky.
interface CrossPostParams {
did: string; // The author's own DID: the account 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: the link card's 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
}
StrongRefAn AT Protocol reference to one specific version of a record: its URI plus the CID of that version. Returned by crossPostToBluesky, and the shape of Article.bskyPostRef.
interface StrongRef {
uri: string;
cid: string;
}