Skip to content

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.

Terminal window
npm install @scribe-atp/social

Requires React 18 or later.

The components need AT URIs — stable identifiers that point to specific AT Protocol records:

  • LikeButton and ShareButton both need the document URI: the AT URI of the site.standard.document article record.
  • LikeButton, ShareButton, and SubscribeButton all need the publication URI: the AT URI of the site.standard.publication site 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.tsx
import { 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 };
}

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" />
)}
  1. A 480×640 popup opens at social.scribe-atp.app showing the article title (or site name for Subscribe).
  2. The reader authenticates with their Bluesky account via AT Protocol OAuth.
  3. On success, the service writes the relevant AT Protocol record to their repository, then notifies the opener page via postMessage.
  4. The button updates to its confirmed state and — for Like and Subscribe — the state is saved to localStorage.

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>

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.

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!")}
/>

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 loader
export 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}
/>

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.
  • localStorage is 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.

State is persisted in localStorage under:

  • scribe:recommended:{documentUri}"1" when liked
  • scribe: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.