atproto-wc
v0.0.1
Published
Framework-agnostic web components for ATProto interactivity. Content via PDS-direct reads, engagement via Constellation backlink index. No AppView dependency.
Downloads
67
Maintainers
Readme
atproto-wc
Custom elements for rendering ATProto content — posts, profiles, feeds, threads, backlinks, arbitrary records. 34 elements, ~12KB gzipped, zero runtime dependencies. Works anywhere custom elements work: plain HTML, Astro, Next, Vite, Svelte, plain ESM.
Quick start
<script type="module" src="https://cdn.jsdelivr.net/npm/atproto-wc/dist/browser.js"></script>
<atproto-post src="https://bsky.app/profile/iammatthias.com/post/3mgkx2wf4pc2w"></atproto-post>
<atproto-profile src="iammatthias.com"></atproto-profile>
<atproto-feed src="iammatthias.com" limit="10"></atproto-feed>Or install from npm:
bun add atproto-wc
# npm i atproto-wcsrc accepts three shapes, auto-detected:
- AT-URI —
at://did:plc:.../app.bsky.feed.post/... - bsky.app URL — parsed into an AT-URI
- Handle — resolved via
/.well-known/atproto-didon the handle's domain. Used by<atproto-profile>,<atproto-feed>,<atproto-followers>, etc.
Data sources
- Records — fetched from the authoring repo's PDS via
com.atproto.repo.getRecordandlistRecords. The PDS is discovered from the DID document (plc.directoryfordid:plc:*,/.well-known/did.jsonfordid:web:*). - Engagement (likes, reposts, replies, quotes, follows, mentions, citations, blocks, list memberships) — fetched from Constellation, a backlink index over the firehose.
No calls to public.api.bsky.app. A practical consequence: elements work against any lexicon. Point <atproto-record-list> at com.example.myschema and it renders. Point <atproto-backlinks> at a custom record and you get every collection+path linking to it.
The things that live in the AppView — moderation labels, algorithmic feeds, the denormalized timeline shapes — are out of scope. For a bsky.app-identical embed, use the official widget.
Registering elements
All at once (auto-registered)
import "atproto-wc/browser";This is what the CDN script tag does. Registers every <atproto-*> element on import.
All at once (manual)
import { defineAll } from "atproto-wc";
defineAll();Same result, but import is side-effect-free until you call defineAll() — friendlier to bundlers that tree-shake.
Only what you need
Every component exports a register* function. Pay only for what you render:
import { registerPost, registerProfile } from "atproto-wc";
registerPost();
registerProfile();Framework integration
Astro
Astro SSRs by default; custom elements need a browser. Render the markup server-side, load the script client-side:
---
// src/pages/profile.astro
const handle = "iammatthias.com";
---
<atproto-profile src={handle}></atproto-profile>
<atproto-feed src={handle} limit="20"></atproto-feed>
<script>
import "atproto-wc/browser";
</script>Next.js (app router)
Mark the importing component as client:
"use client";
import { useEffect } from "react";
export default function PostEmbed({ src }: { src: string }) {
useEffect(() => {
import("atproto-wc/browser");
}, []);
return <atproto-post src={src} />;
}Vite / plain ESM
import { defineAll } from "atproto-wc";
defineAll();Components
34 elements across four tiers. Every src-accepting element parses AT-URIs, bsky.app URLs, and (where applicable) handles. Every Constellation-backed element accepts a constellation="..." attribute to override the endpoint per-element.
Content
| Tag | Purpose |
|---|---|
| <atproto-post> | Post embed — author, facets, images, external cards, quote embeds, video, engagement counts |
| <atproto-profile> | Profile card — avatar, banner, bio, follower/following/post counts |
| <atproto-feed> | Author's posts + reposts, paginated, newest-first |
| <atproto-thread> | Full reply tree for a post, rendered recursively |
| <atproto-comments> | Threaded replies via Constellation |
| <atproto-repo> | Summary card for a repo — identity, PDS, latest commit |
| <atproto-lexicon-viewer> | Any record, any lexicon — JSON tree |
| <atproto-record-list> | Paginated listRecords for an arbitrary collection |
| <atproto-list> | Rendered app.bsky.graph.list with its members |
Backlinks (Constellation)
| Tag | Purpose |
|---|---|
| <atproto-likers> | Avatar grid of accounts that liked a post |
| <atproto-reposters> | Avatar grid of accounts that reposted |
| <atproto-quoters> | Posts that quoted the target (compact cards) |
| <atproto-followers> | Avatar grid of followers |
| <atproto-mutuals> | Intersection — who links both a and b via the same relation |
| <atproto-mentions> | Posts that mention a subject DID |
| <atproto-citations> | Records that cite a target via any subject field |
| <atproto-blockers> | Accounts that have blocked a target DID |
| <atproto-list-memberships> | Lists that contain a given subject |
| <atproto-backlinks> | Every collection+path linking to a target, with counts (lexicon-agnostic) |
| <atproto-like-count> | Live like count for a post |
| <atproto-generic-count> | Count primitive for arbitrary subject + source |
| <atproto-distinct-count> | Unique-actor count for arbitrary subject + source |
Repo state
| Tag | Purpose |
|---|---|
| <atproto-blobs> | Blobs uploaded by a repo, MIME-aware rendering |
| <atproto-latest-commit> | The repo's latest rev + commit CID |
| <atproto-repo-status> | Activation state, handle, PDS health |
| <atproto-verification> | app.bsky.graph.verification status for an account |
| <atproto-gate-badge> | Reply-gate state for a post |
| <atproto-pinned-badge> | Whether a post is the author's pinned post |
Atoms
Primitives composed by the larger elements. Useful on their own when you want, say, a bare display name without the whole profile card.
| Tag | Purpose |
|---|---|
| <atproto-avatar> | Just the avatar image |
| <atproto-display-name> | Just the display name |
| <atproto-handle> | Just the handle (@handle) |
| <atproto-rich-text> | Post text with facet rendering (links, mentions, tags) |
| <atproto-time> | <time> element with relative formatting |
| <atproto-engagement-row> | Counts row (likes, reposts, replies, quotes) |
Shared behavior
Every element:
- Renders a skeleton while loading — no layout shift.
- Shows a retry button on transient failures (network, 5xx). Permanent failures (404, bad URI) don't offer retry.
- Aborts in-flight fetches when disconnected from the DOM.
- Deduplicates identical requests within a 10s window.
- Attaches clickthrough permalinks to
bsky.app. - Themes via CSS custom properties and
::part().
Styling
Three layers, composable:
- CSS custom properties —
--atproto-accent,--atproto-radius,--atproto-font, etc. Set on the element, on an ancestor, or globally. ::part()selectors — target internals (avatars, counts, individual image cells) through the Shadow DOM boundary.- Plain
style=""/class=""on the host — Tailwind-style utilities work via[&::part(...)]arbitrary variants.
/* Theme-wide swap */
atproto-post { --atproto-accent: #ea580c; --atproto-radius: 4px; }
/* Targeted internal overrides */
atproto-post::part(avatar) { border-radius: 4px; }
atproto-post::part(counts) { display: none; }Dark mode is auto-detected via prefers-color-scheme: dark. Override tokens inside your own media query to customize dark values, or set them unconditionally to opt out.
The docs site under site/ has live styling demos and the full token + ::part() reference at /styling.
Constellation endpoint
Defaults to https://constellation.microcosm.blue. Override globally or per-element.
import { setConstellationEndpoint } from "atproto-wc";
setConstellationEndpoint("https://my-constellation.example");<atproto-post src="at://..." constellation="https://my-constellation.example"></atproto-post>Programmatic API
The core resolvers and clients are exported for use outside the components:
import {
parseAtUri,
resolveHandle,
resolvePdsEndpoint,
getRecord,
listRecords,
getBacklinks,
getDistinctDids,
} from "atproto-wc";See src/core/ for the full surface — at-uri, did, pds, constellation, resolve, safe-url, cache.
Dev
bun install
bun run dev # demo server at http://localhost:5173
bun run build # emits dist/ (minified, external sourcemaps)
bun run typecheck
bun run testThe docs site is its own workspace:
cd site
bun run dev # astro dev at http://localhost:4321License
MIT
