Social interactions
@scribe-atp/social exports LikeButton, ShareButton, and SubscribeButton — React components that let readers interact with Scribe content on the AT Protocol using their Bluesky account. No server-side setup is required; the components handle the OAuth flow via a popup.
Install
Section titled “Install”npm install @scribe-atp/socialRequires React 18 or later.
Getting the AT URIs
Section titled “Getting the AT URIs”The components need AT URIs — stable identifiers that point to specific AT Protocol records:
LikeButtonandShareButtonboth need the document URI: the AT URI of thesite.standard.documentarticle record.LikeButton,ShareButton, andSubscribeButtonall need the publication URI: the AT URI of thesite.standard.publicationsite record.
Your article page loader should fetch both. The exact approach depends on your framework:
createArticleRouteLoader returns documentUri alongside the article. Fetch the site separately to get the publication URI:
// app/routes/blog.$articleSlug.tsximport { createArticleRouteLoader } from '@scribe-atp/react-router-framework';import { fetchSite } from '@scribe-atp/core';import type { LoaderFunctionArgs } from 'react-router';
export async function loader({ request, params }: LoaderFunctionArgs) { const [articleData, site] = await Promise.all([ createArticleRouteLoader('alice.bsky.social', 'https://alice.bsky.social')({ request, params }), fetchSite('alice.bsky.social', 'https://alice.bsky.social', request.signal), ]); return { ...articleData, publicationUri: site.uri };}import { fetchArticleBySlug, fetchSite } from '@scribe-atp/core';
const [{ article, uri: documentUri }, site] = await Promise.all([ fetchArticleBySlug('alice.bsky.social', 'https://alice.bsky.social', params.slug), fetchSite('alice.bsky.social', 'https://alice.bsky.social'),]);
// documentUri → LikeButton, ShareButton// site.uri → LikeButton, ShareButton, SubscribeButtonimport { fetchArticleBySlug, fetchSite } from '@scribe-atp/core';
const route = useRoute();const [{ article, uri: documentUri }, site] = await Promise.all([ fetchArticleBySlug('alice.bsky.social', 'https://alice.bsky.social', route.params.slug as string), fetchSite('alice.bsky.social', 'https://alice.bsky.social'),]);Rendering the buttons
Section titled “Rendering the buttons”Once you have the URIs, render the components in your article template:
import { LikeButton, ShareButton, SubscribeButton } from '@scribe-atp/social';
{documentUri && ( <LikeButton documentUri={documentUri} publicationUri={publicationUri} title={article.title} />)}{documentUri && ( <ShareButton documentUri={documentUri} publicationUri={publicationUri} title={article.title} />)}{publicationUri && ( <SubscribeButton publicationUri={publicationUri} title="My Site" />)}What happens when clicked
Section titled “What happens when clicked”- A
480×640popup opens atsocial.scribe-atp.appshowing the article title (or site name for Subscribe). - The reader authenticates with their Bluesky account via AT Protocol OAuth.
- On success, the service writes the relevant AT Protocol record to their repository, then notifies the opener page via
postMessage. - The button updates to its confirmed state and — for Like and Subscribe — the state is saved to
localStorage.
Customising the label
Section titled “Customising the label”All three buttons accept a children prop to replace the default label text. Pass a render prop to access the button’s internal state — useful when you want different copy before and after the action:
<LikeButton documentUri={documentUri} publicationUri={publicationUri} title={article.title}> {(isLiked) => (isLiked ? "Loved it ✓" : "Did you enjoy this?")}</LikeButton>
<ShareButton documentUri={documentUri} publicationUri={publicationUri} title={article.title}> {(isShared) => (isShared ? "Shared! ✓" : "Share on Bluesky")}</ShareButton>
<SubscribeButton publicationUri={publicationUri} title="My Site"> {(isSubscribed) => (isSubscribed ? "Following ✓" : "Follow this site")}</SubscribeButton>Or pass a static node if the label doesn’t need to change:
<LikeButton documentUri={documentUri} publicationUri={publicationUri} title={article.title}> ♥ Recommend</LikeButton>Styling
Section titled “Styling”Each button renders a plain <button> element with a base CSS class (scribe-atp-like-button, scribe-atp-share-button, scribe-atp-subscribe-button). Pass a className prop to append your own class alongside the base:
<LikeButton documentUri={documentUri} publicationUri={publicationUri} title={article.title} className="btn btn-outline"/>The base class is always present, so you can also target the components globally in your stylesheet:
.scribe-atp-like-button,.scribe-atp-share-button,.scribe-atp-subscribe-button { /* shared button styles */}
.scribe-atp-like-button[aria-pressed="true"],.scribe-atp-subscribe-button[aria-pressed="true"] { /* confirmed / already-actioned state */}
.scribe-atp-share-button:disabled { /* briefly disabled during the 3-second confirmed window */}See the @scribe-atp/styles package for a ready-made stylesheet if you don’t want to write your own.
Reacting to success
Section titled “Reacting to success”All three buttons accept an onSuccess callback fired once the action completes. Use it to show a toast, fire an analytics event, or update surrounding UI:
<LikeButton documentUri={documentUri} publicationUri={publicationUri} title={article.title} onSuccess={() => toast("Thanks for the like!")}/>
<SubscribeButton publicationUri={publicationUri} title="My Site" onSuccess={() => analytics.track("subscribe")}/>
<ShareButton documentUri={documentUri} publicationUri={publicationUri} title={article.title} onSuccess={() => toast("Article shared to Bluesky!")}/>SSR / avoiding flash of unconfirmed state
Section titled “SSR / avoiding flash of unconfirmed state”On SSR frameworks, LikeButton and SubscribeButton initialise in their unconfirmed state because localStorage is unavailable at render time. The useEffect that reads localStorage runs after hydration, which can produce a brief flash of the wrong state for returning readers.
Pass defaultLiked / defaultSubscribed to set the initial state at render time and skip the localStorage read on mount:
// app/routes/blog.$slug.tsx — React Router v7 loaderexport async function loader({ request, params }: LoaderFunctionArgs) { const cookies = parseCookies(request.headers.get("Cookie") ?? ""); return { ...articleData, defaultLiked: cookies[`scribe:recommended:${documentUri}`] === "1", defaultSubscribed: cookies[`scribe:subscribed:${publicationUri}`] === "1", };}
// In your article component:<LikeButton documentUri={documentUri} publicationUri={publicationUri} title={article.title} defaultLiked={defaultLiked}/><SubscribeButton publicationUri={publicationUri} title="My Site" defaultSubscribed={defaultSubscribed}/>Unsubscribing
Section titled “Unsubscribing”SubscribeButton is a toggle. When the reader is already subscribed, clicking the button opens an unsubscribe confirmation popup rather than the subscribe flow. The popup asks “Are you sure you want to unsubscribe from Site?” and requires the reader to confirm before deleting their subscription record.
After a confirmed unsubscribe:
- The subscription AT Protocol record is deleted from the reader’s repository.
localStorageis cleared for the publication.- The button returns to its unsubscribed state.
No extra props are required. The button detects subscribed state via localStorage and routes the click to the appropriate popup automatically.
Liked / subscribed state
Section titled “Liked / subscribed state”State is persisted in localStorage under:
scribe:recommended:{documentUri}—"1"when likedscribe:subscribed:{publicationUri}—"1"when subscribed
The components read this on mount, so a returning reader will see the confirmed state immediately without needing to re-authenticate.
The storage utilities are also exported directly if you need to read or set state programmatically:
import { isRecommended, isSubscribed, clearSubscribed } from '@scribe-atp/social';
if (isRecommended(documentUri)) { // already liked}
// Remove a subscription from localStorage (e.g. after a server-side unsubscribe)clearSubscribed(publicationUri);See the API reference for the full function signatures.