diff --git a/app/test/bsky-embed/page.tsx b/app/test/bsky-embed/page.tsx index 41f3dd95..e8db2345 100644 --- a/app/test/bsky-embed/page.tsx +++ b/app/test/bsky-embed/page.tsx @@ -47,6 +47,42 @@ const external = (opts: { thumb?: boolean } = {}) => }, }); +const standardSite = (opts: { thumb?: boolean; icon?: boolean } = {}) => + e({ + $type: "app.bsky.embed.external#view", + external: { + uri: "https://leaflet.pub/p/how-we-built-the-thing", + title: "How we built the thing: a deep dive into the internals", + description: + "An article published on a standard.site publication, with an associated document ref, author profile, and publication source.", + thumb: opts.thumb ? img("standardsite", 1200, 630) : undefined, + createdAt: "2026-07-10T00:00:00.000Z", + source: { + uri: "https://leaflet.pub/alice", + title: "Alice's Newsletter", + icon: opts.icon === false ? undefined : img("pubicon", 100, 100), + }, + associatedRefs: [ + { + uri: "at://did:plc:examplealice/site.standard.document/doc123", + cid: "bafydoc", + }, + { + uri: "at://did:plc:examplealice/site.standard.publication/pub123", + cid: "bafypub", + }, + ], + associatedProfiles: [ + { + did: "did:plc:examplealice", + handle: "alice.bsky.social", + displayName: "Alice Example", + avatar: img("alice", 100, 100), + }, + ], + }, + }); + const viewRecord = (extra: Record = {}) => ({ $type: "app.bsky.embed.record#viewRecord", uri: "at://did:plc:examplealice/app.bsky.feed.post/abc123", @@ -92,6 +128,19 @@ const VARIANTS: Variant[] = [ { label: "Image ×5 (overflow +1)", content: images(5) }, { label: "External (with thumb)", content: external({ thumb: true }) }, { label: "External (no thumb)", content: external() }, + { + label: "standard.site post (thumb + pub footer)", + content: standardSite({ thumb: true }), + note: "Footer shows publication icon + name + author, links to source.uri.", + }, + { + label: "standard.site post (no thumb)", + content: standardSite(), + }, + { + label: "standard.site post (no pub icon)", + content: standardSite({ thumb: true, icon: false }), + }, { label: "Quote — short", content: quote(), diff --git a/components/ActionBar/Publications.tsx b/components/ActionBar/Publications.tsx index 2f4d0772..6d4e6d95 100644 --- a/components/ActionBar/Publications.tsx +++ b/components/ActionBar/Publications.tsx @@ -194,9 +194,11 @@ export const PubIcon = (props: { /> ) : ( -
+
{props.pubName ? props.pubName.slice(0, 1).toUpperCase() : "P"}
diff --git a/components/Blocks/BlueskyPostBlock/BskyEmbed.tsx b/components/Blocks/BlueskyPostBlock/BskyEmbed.tsx index e74ef2cc..5b8209a7 100644 --- a/components/Blocks/BlueskyPostBlock/BskyEmbed.tsx +++ b/components/Blocks/BlueskyPostBlock/BskyEmbed.tsx @@ -13,7 +13,9 @@ import { } from "@atproto/api"; import { useMemo } from "react"; import { Avatar } from "components/Avatar"; +import { LocalizedDate } from "app/(app)/lish/[did]/[publication]/LocalizedDate"; import { BlueskyVideoPlayer } from "./BlueskyVideoPlayer"; +import { PubIcon } from "components/ActionBar/Publications"; // A fork of bluesky's embed renderer, matching its layout while using our // colors, fonts, and shared components (Avatar, BlueskyVideoPlayer). Context @@ -379,6 +381,16 @@ function ExternalEmbed({ className?: string; }) { if (labelInfo) return {labelInfo}; + const external = content.external as StandardSiteExternal; + if (external.source && isStandardSiteEmbed(external)) { + return ( + + ); + } return ( + atCollection(ref.uri).startsWith("site.standard.document"), + ) + .map((ref) => atHost(ref.uri)) + .find(Boolean); + const author = authorDid + ? external.associatedProfiles?.find((p) => p.did === authorDid) + : undefined; + + return ( +
+ + {external.thumb && !compact && ( + {external.title} + )} +
+

+ {external.title} +

+ {external.description && ( +

+ {external.description} +

+ )} + {external.createdAt && ( +

+ +

+ )} +
+ +
+ + + +
+ + {source.title} + + {author?.handle && ( + + by @{author.handle} + + )} +
+ + + Subscribe + +
+
+ ); +} + function GenericWithImageEmbed({ title, subtitle, @@ -596,6 +702,43 @@ function getRkey(uri: string): string { return new AtUri(uri).rkey; } +// standard.site (leaflet.pub etc.) augments the external embed with refs to the +// underlying site.standard.* records, the author profiles, and the publication. +// These aren't in the published @atproto/api types yet, so widen locally. +type StandardSiteExternal = AppBskyEmbedExternal.ViewExternal & { + associatedRefs?: { uri: string; cid?: string }[]; + associatedProfiles?: { + did: string; + handle: string; + displayName?: string; + avatar?: string; + }[]; + source?: { uri?: string; title?: string; icon?: string }; + createdAt?: string; +}; + +function atCollection(uri: string): string { + try { + return new AtUri(uri).collection; + } catch { + return ""; + } +} + +function atHost(uri: string): string { + try { + return new AtUri(uri).host; + } catch { + return ""; + } +} + +function isStandardSiteEmbed(external: StandardSiteExternal): boolean { + return !!external.associatedRefs?.some((ref) => + atCollection(ref.uri).startsWith("site.standard."), + ); +} + function clamp(num: number, min: number, max: number) { return Math.max(min, Math.min(num, max)); }