From a4586e5fdac165f5e3953fbc79f2321d16d5cc79 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Mon, 9 Feb 2026 14:32:43 +0400 Subject: [PATCH 01/28] add inngest function to track bsky post likes --- app/api/inngest/client.ts | 6 +++ app/api/inngest/functions/sync_bsky_likes.ts | 41 +++++++++++++++++++ app/api/inngest/route.tsx | 2 + app/api/rpc/[command]/get_publication_data.ts | 1 + .../dashboard/PublishedPostsLists.tsx | 1 + appview/index.ts | 18 ++++++++ drizzle/schema.ts | 3 +- supabase/database.types.ts | 3 ++ .../20260209000000_add_bsky_like_count.sql | 1 + 9 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 app/api/inngest/functions/sync_bsky_likes.ts create mode 100644 supabase/migrations/20260209000000_add_bsky_like_count.sql diff --git a/app/api/inngest/client.ts b/app/api/inngest/client.ts index 4dceb80f..f648b39f 100644 --- a/app/api/inngest/client.ts +++ b/app/api/inngest/client.ts @@ -51,6 +51,12 @@ export type Events = { documentUris?: string[]; }; }; + "appview/sync-bsky-likes": { + data: { + document_uri: string; + bsky_post_uri: string; + }; + }; "user/write-records-to-pds": { data: { did: string; diff --git a/app/api/inngest/functions/sync_bsky_likes.ts b/app/api/inngest/functions/sync_bsky_likes.ts new file mode 100644 index 00000000..9b67aab8 --- /dev/null +++ b/app/api/inngest/functions/sync_bsky_likes.ts @@ -0,0 +1,41 @@ +import { inngest } from "../client"; +import { supabaseServerClient } from "supabase/serverClient"; +import { AtpAgent } from "@atproto/api"; + +const TOTAL_ITERATIONS = 144; // 36 hours at 15-minute intervals + +export const sync_bsky_likes = inngest.createFunction( + { + id: "sync_bsky_likes", + idempotency: "event.data.bsky_post_uri", + }, + { event: "appview/sync-bsky-likes" }, + async ({ event, step }) => { + const { document_uri, bsky_post_uri } = event.data; + + const agent = new AtpAgent({ service: "https://public.api.bsky.app" }); + + const fetchAndUpdate = async () => { + const res = await agent.app.bsky.feed.getPosts({ + uris: [bsky_post_uri], + }); + const post = res.data.posts[0]; + if (!post) return 0; + const likeCount = post.likeCount ?? 0; + await supabaseServerClient + .from("documents") + .update({ bsky_like_count: likeCount }) + .eq("uri", document_uri); + return likeCount; + }; + + let likeCount = await step.run("sync-0", fetchAndUpdate); + + for (let i = 1; i < TOTAL_ITERATIONS; i++) { + await step.sleep(`wait-${i}`, "15m"); + likeCount = await step.run(`sync-${i}`, fetchAndUpdate); + } + + return { likeCount }; + }, +); diff --git a/app/api/inngest/route.tsx b/app/api/inngest/route.tsx index ce86472d..ea0f2f9e 100644 --- a/app/api/inngest/route.tsx +++ b/app/api/inngest/route.tsx @@ -13,6 +13,7 @@ import { check_oauth_session, } from "./functions/cleanup_expired_oauth_sessions"; import { write_records_to_pds } from "./functions/write_records_to_pds"; +import { sync_bsky_likes } from "./functions/sync_bsky_likes"; export const { GET, POST, PUT } = serve({ client: inngest, @@ -28,5 +29,6 @@ export const { GET, POST, PUT } = serve({ cleanup_expired_oauth_sessions, check_oauth_session, write_records_to_pds, + sync_bsky_likes, ], }); diff --git a/app/api/rpc/[command]/get_publication_data.ts b/app/api/rpc/[command]/get_publication_data.ts index 09cc91f4..9fe23804 100644 --- a/app/api/rpc/[command]/get_publication_data.ts +++ b/app/api/rpc/[command]/get_publication_data.ts @@ -86,6 +86,7 @@ export const get_publication_data = makeRoute({ indexed_at: dip.documents.indexed_at, sort_date: dip.documents.sort_date, data: dip.documents.data, + bsky_like_count: dip.documents.bsky_like_count, commentsCount: dip.documents.comments_on_documents[0]?.count || 0, mentionsCount: dip.documents.document_mentions_in_bsky[0]?.count || 0, recommendsCount: diff --git a/app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx b/app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx index a7e4c630..29b4b8a8 100644 --- a/app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx +++ b/app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx @@ -112,6 +112,7 @@ function PublishedPostItem(props: { indexed_at: doc.indexed_at, sort_date: doc.sort_date, data: doc.data, + bsky_like_count: doc.bsky_like_count ?? 0, }, }, ], diff --git a/appview/index.ts b/appview/index.ts index 139ea9fd..7319f549 100644 --- a/appview/index.ts +++ b/appview/index.ts @@ -109,6 +109,15 @@ async function handleEvent(evt: Event) { data: record.value as Json, }); if (docResult.error) console.log(docResult.error); + if (record.value.postRef?.uri) { + await inngest.send({ + name: "appview/sync-bsky-likes", + data: { + document_uri: evt.uri.toString(), + bsky_post_uri: record.value.postRef.uri, + }, + }); + } if (record.value.publication) { let publicationURI = new AtUri(record.value.publication); @@ -269,6 +278,15 @@ async function handleEvent(evt: Event) { data: record.value as Json, }); if (docResult.error) console.log(docResult.error); + if (record.value.bskyPostRef?.uri) { + await inngest.send({ + name: "appview/sync-bsky-likes", + data: { + document_uri: evt.uri.toString(), + bsky_post_uri: record.value.bskyPostRef.uri, + }, + }); + } // site.standard.document uses "site" field to reference the publication // For documents in publications, site is an AT-URI (at://did:plc:xxx/site.standard.publication/rkey) diff --git a/drizzle/schema.ts b/drizzle/schema.ts index 65cd9add..05b5b5ad 100644 --- a/drizzle/schema.ts +++ b/drizzle/schema.ts @@ -1,4 +1,4 @@ -import { pgTable, pgEnum, text, jsonb, foreignKey, timestamp, boolean, uuid, index, bigint, unique, uniqueIndex, smallint, primaryKey } from "drizzle-orm/pg-core" +import { pgTable, pgEnum, text, jsonb, foreignKey, timestamp, boolean, uuid, index, bigint, unique, uniqueIndex, smallint, primaryKey, integer } from "drizzle-orm/pg-core" import { sql } from "drizzle-orm" export const aal_level = pgEnum("aal_level", ['aal1', 'aal2', 'aal3']) @@ -225,6 +225,7 @@ export const documents = pgTable("documents", { uri: text("uri").primaryKey().notNull(), data: jsonb("data").notNull(), indexed_at: timestamp("indexed_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), + bsky_like_count: integer("bsky_like_count").default(0).notNull(), }); export const atp_poll_votes = pgTable("atp_poll_votes", { diff --git a/supabase/database.types.ts b/supabase/database.types.ts index 6153540b..1eb727c2 100644 --- a/supabase/database.types.ts +++ b/supabase/database.types.ts @@ -335,17 +335,20 @@ export type Database = { } documents: { Row: { + bsky_like_count: number data: Json indexed_at: string sort_date: string uri: string } Insert: { + bsky_like_count?: number data: Json indexed_at?: string uri: string } Update: { + bsky_like_count?: number data?: Json indexed_at?: string uri?: string diff --git a/supabase/migrations/20260209000000_add_bsky_like_count.sql b/supabase/migrations/20260209000000_add_bsky_like_count.sql new file mode 100644 index 00000000..676476b0 --- /dev/null +++ b/supabase/migrations/20260209000000_add_bsky_like_count.sql @@ -0,0 +1 @@ +ALTER TABLE documents ADD COLUMN bsky_like_count integer NOT NULL DEFAULT 0; -- 2.51.2 From 89a5e06cfb4576b98ad25c74688e7e1d99cdd0fb Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Mon, 9 Feb 2026 15:35:45 +0400 Subject: [PATCH 02/28] don't sync bsky likes of brid.gy posts --- app/api/inngest/functions/sync_bsky_likes.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/app/api/inngest/functions/sync_bsky_likes.ts b/app/api/inngest/functions/sync_bsky_likes.ts index 9b67aab8..69dfe3c6 100644 --- a/app/api/inngest/functions/sync_bsky_likes.ts +++ b/app/api/inngest/functions/sync_bsky_likes.ts @@ -1,6 +1,7 @@ import { inngest } from "../client"; import { supabaseServerClient } from "supabase/serverClient"; -import { AtpAgent } from "@atproto/api"; +import { AtpAgent, AtUri } from "@atproto/api"; +import { idResolver } from "app/(home-pages)/reader/idResolver"; const TOTAL_ITERATIONS = 144; // 36 hours at 15-minute intervals @@ -13,6 +14,19 @@ export const sync_bsky_likes = inngest.createFunction( async ({ event, step }) => { const { document_uri, bsky_post_uri } = event.data; + const isBridgy = await step.run("check-bridgy", async () => { + const did = new AtUri(bsky_post_uri).host; + const doc = await idResolver.did.resolve(did); + const handle = doc?.alsoKnownAs + ?.find((a) => a.startsWith("at://")) + ?.replace("at://", ""); + return handle?.includes("brid.gy") ?? false; + }); + + if (isBridgy) { + return { skipped: true, reason: "brid.gy post" }; + } + const agent = new AtpAgent({ service: "https://public.api.bsky.app" }); const fetchAndUpdate = async () => { -- 2.51.2 From 764c811b667edc30b90e41f16a35ed4f7cc65a77 Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Mon, 9 Feb 2026 18:47:35 +0100 Subject: [PATCH 03/28] fix: copy post link button --- components/InteractionsPreview.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/InteractionsPreview.tsx b/components/InteractionsPreview.tsx index 9f393f86..930e2b36 100644 --- a/components/InteractionsPreview.tsx +++ b/components/InteractionsPreview.tsx @@ -77,7 +77,7 @@ export const InteractionPreview = (props: { let mouseY = e.clientY; if (!props.postUrl) return; - navigator.clipboard.writeText(`leaflet.pub${props.postUrl}`); + navigator.clipboard.writeText(props.postUrl); smoker({ text: Copied Link!, -- 2.51.2 From 1971c0e399e1e670b34b3c4781ca93756fefc80a Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Mon, 9 Feb 2026 22:34:43 +0400 Subject: [PATCH 04/28] add bsky auth view all permissions --- app/api/oauth/[route]/oauth-metadata.ts | 2 +- app/api/oauth/[route]/route.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/api/oauth/[route]/oauth-metadata.ts b/app/api/oauth/[route]/oauth-metadata.ts index 32ed3b35..e29dcb9b 100644 --- a/app/api/oauth/[route]/oauth-metadata.ts +++ b/app/api/oauth/[route]/oauth-metadata.ts @@ -8,7 +8,7 @@ const hostname = : "https://leaflet.pub"; const scope = - "atproto transition:generic transition:email include:pub.leaflet.authFullPermissions include:site.standard.authFull include:app.bsky.authCreatePosts blob:*/*"; + "atproto transition:generic transition:email include:pub.leaflet.authFullPermissions include:site.standard.authFull include:app.bsky.authCreatePosts include:app.bsky.authViewAll blob:*/*"; const localconfig: OAuthClientMetadataInput = { client_id: `http://localhost/?redirect_uri=${encodeURI(`http://127.0.0.1:3000/api/oauth/callback`)}&scope=${encodeURIComponent(scope)}`, client_name: `Leaflet`, diff --git a/app/api/oauth/[route]/route.ts b/app/api/oauth/[route]/route.ts index 108e032b..38f8ebee 100644 --- a/app/api/oauth/[route]/route.ts +++ b/app/api/oauth/[route]/route.ts @@ -43,7 +43,7 @@ export async function GET( const url = await client.authorize(handle || "https://bsky.social", { scope: - "atproto transition:email include:pub.leaflet.authFullPermissions include:site.standard.authFull include:app.bsky.authCreatePosts blob:*/*", + "atproto transition:email include:pub.leaflet.authFullPermissions include:site.standard.authFull include:app.bsky.authCreatePosts include:app.bsky.authViewAll blob:*/*", signal: ac.signal, state: JSON.stringify(state), }); -- 2.51.2 From 7bb38cc61bc41c0f391e4cd98909fcff608cb059 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Mon, 9 Feb 2026 23:28:35 +0400 Subject: [PATCH 05/28] add aud to bsky view permissions --- app/api/oauth/[route]/oauth-metadata.ts | 2 +- app/api/oauth/[route]/route.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/api/oauth/[route]/oauth-metadata.ts b/app/api/oauth/[route]/oauth-metadata.ts index e29dcb9b..6af54950 100644 --- a/app/api/oauth/[route]/oauth-metadata.ts +++ b/app/api/oauth/[route]/oauth-metadata.ts @@ -8,7 +8,7 @@ const hostname = : "https://leaflet.pub"; const scope = - "atproto transition:generic transition:email include:pub.leaflet.authFullPermissions include:site.standard.authFull include:app.bsky.authCreatePosts include:app.bsky.authViewAll blob:*/*"; + "atproto transition:generic transition:email include:pub.leaflet.authFullPermissions include:site.standard.authFull include:app.bsky.authCreatePosts include:app.bsky.authViewAll?aud=did:web:api.bsky.app%23bsky_appview blob:*/*"; const localconfig: OAuthClientMetadataInput = { client_id: `http://localhost/?redirect_uri=${encodeURI(`http://127.0.0.1:3000/api/oauth/callback`)}&scope=${encodeURIComponent(scope)}`, client_name: `Leaflet`, diff --git a/app/api/oauth/[route]/route.ts b/app/api/oauth/[route]/route.ts index 38f8ebee..9c7eb139 100644 --- a/app/api/oauth/[route]/route.ts +++ b/app/api/oauth/[route]/route.ts @@ -43,7 +43,7 @@ export async function GET( const url = await client.authorize(handle || "https://bsky.social", { scope: - "atproto transition:email include:pub.leaflet.authFullPermissions include:site.standard.authFull include:app.bsky.authCreatePosts include:app.bsky.authViewAll blob:*/*", + "atproto transition:email include:pub.leaflet.authFullPermissions include:site.standard.authFull include:app.bsky.authCreatePosts include:app.bsky.authViewAll?aud=did:web:api.bsky.app%23bsky_appview blob:*/*", signal: ac.signal, state: JSON.stringify(state), }); -- 2.51.2 From c81be9ad3fd9e3c74966ec1d88667b89b6c6e20f Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Tue, 10 Feb 2026 00:16:55 +0400 Subject: [PATCH 06/28] add recommend notification --- .../notifications/NotificationList.tsx | 4 ++ .../notifications/RecommendNotification.tsx | 48 ++++++++++++++ .../[rkey]/Interactions/recommendAction.ts | 21 +++++++ src/notifications.ts | 63 +++++++++++++++++-- 4 files changed, 132 insertions(+), 4 deletions(-) create mode 100644 app/(home-pages)/notifications/RecommendNotification.tsx diff --git a/app/(home-pages)/notifications/NotificationList.tsx b/app/(home-pages)/notifications/NotificationList.tsx index a0ec12ee..30852227 100644 --- a/app/(home-pages)/notifications/NotificationList.tsx +++ b/app/(home-pages)/notifications/NotificationList.tsx @@ -11,6 +11,7 @@ import { QuoteNotification } from "./QuoteNotification"; import { BskyPostEmbedNotification } from "./BskyPostEmbedNotification"; import { MentionNotification } from "./MentionNotification"; import { CommentMentionNotification } from "./CommentMentionNotification"; +import { RecommendNotification } from "./RecommendNotification"; export function NotificationList({ notifications, @@ -58,6 +59,9 @@ export function NotificationList({ if (n.type === "comment_mention") { return ; } + if (n.type === "recommend") { + return ; + } })} diff --git a/app/(home-pages)/notifications/RecommendNotification.tsx b/app/(home-pages)/notifications/RecommendNotification.tsx new file mode 100644 index 00000000..3abb49d3 --- /dev/null +++ b/app/(home-pages)/notifications/RecommendNotification.tsx @@ -0,0 +1,48 @@ +import { ContentLayout, Notification } from "./Notification"; +import { HydratedRecommendNotification } from "src/notifications"; +import { blobRefToSrc } from "src/utils/blobRefToSrc"; +import { AppBskyActorProfile } from "lexicons/api"; +import { Avatar } from "components/Avatar"; +import { AtUri } from "@atproto/api"; +import { RecommendTinyFilled } from "components/Icons/RecommendTiny"; + +export const RecommendNotification = ( + props: HydratedRecommendNotification, +) => { + const profileRecord = props.recommendData?.identities?.bsky_profiles + ?.record as AppBskyActorProfile.Record; + const displayName = + profileRecord?.displayName || + props.recommendData?.identities?.bsky_profiles?.handle || + "Someone"; + const docRecord = props.normalizedDocument; + const pubRecord = props.normalizedPublication; + const avatarSrc = + profileRecord?.avatar?.ref && + blobRefToSrc( + profileRecord.avatar.ref, + props.recommendData?.recommender_did || "", + ); + + if (!docRecord) return null; + + const docUri = new AtUri(props.document.uri); + const rkey = docUri.rkey; + const did = docUri.host; + + const href = pubRecord ? `${pubRecord.url}/${rkey}` : `/p/${did}/${rkey}`; + + return ( + } + actionText={<>{displayName} recommended your post} + content={ + + {null} + + } + /> + ); +}; diff --git a/app/lish/[did]/[publication]/[rkey]/Interactions/recommendAction.ts b/app/lish/[did]/[publication]/[rkey]/Interactions/recommendAction.ts index d0295f26..c219720c 100644 --- a/app/lish/[did]/[publication]/[rkey]/Interactions/recommendAction.ts +++ b/app/lish/[did]/[publication]/[rkey]/Interactions/recommendAction.ts @@ -7,6 +7,11 @@ import { TID } from "@atproto/common"; import { AtUri, Un$Typed } from "@atproto/api"; import { supabaseServerClient } from "supabase/serverClient"; import { Json } from "supabase/database.types"; +import { v7 } from "uuid"; +import { + Notification, + pingIdentityToUpdateNotification, +} from "src/notifications"; type RecommendResult = | { success: true; uri: string } @@ -68,6 +73,22 @@ export async function recommendAction(args: { }); console.log(res); + // Notify the document owner + let documentOwner = new AtUri(args.document).host; + if (documentOwner !== credentialSession.did) { + let notification: Notification = { + id: v7(), + recipient: documentOwner, + data: { + type: "recommend", + document_uri: args.document, + recommend_uri: uri.toString(), + }, + }; + await supabaseServerClient.from("notifications").insert(notification); + await pingIdentityToUpdateNotification(documentOwner); + } + return { success: true, uri: uri.toString(), diff --git a/src/notifications.ts b/src/notifications.ts index f4353d2b..be5ae4bb 100644 --- a/src/notifications.ts +++ b/src/notifications.ts @@ -27,7 +27,8 @@ export type NotificationData = | { type: "mention"; document_uri: string; mention_type: "document"; mentioned_uri: string } | { type: "comment_mention"; comment_uri: string; mention_type: "did" } | { type: "comment_mention"; comment_uri: string; mention_type: "publication"; mentioned_uri: string } - | { type: "comment_mention"; comment_uri: string; mention_type: "document"; mentioned_uri: string }; + | { type: "comment_mention"; comment_uri: string; mention_type: "document"; mentioned_uri: string } + | { type: "recommend"; document_uri: string; recommend_uri: string }; export type HydratedNotification = | HydratedCommentNotification @@ -35,22 +36,24 @@ export type HydratedNotification = | HydratedQuoteNotification | HydratedBskyPostEmbedNotification | HydratedMentionNotification - | HydratedCommentMentionNotification; + | HydratedCommentMentionNotification + | HydratedRecommendNotification; export async function hydrateNotifications( notifications: NotificationRow[], ): Promise> { // Call all hydrators in parallel - const [commentNotifications, subscribeNotifications, quoteNotifications, bskyPostEmbedNotifications, mentionNotifications, commentMentionNotifications] = await Promise.all([ + const [commentNotifications, subscribeNotifications, quoteNotifications, bskyPostEmbedNotifications, mentionNotifications, commentMentionNotifications, recommendNotifications] = await Promise.all([ hydrateCommentNotifications(notifications), hydrateSubscribeNotifications(notifications), hydrateQuoteNotifications(notifications), hydrateBskyPostEmbedNotifications(notifications), hydrateMentionNotifications(notifications), hydrateCommentMentionNotifications(notifications), + hydrateRecommendNotifications(notifications), ]); // Combine all hydrated notifications - const allHydrated = [...commentNotifications, ...subscribeNotifications, ...quoteNotifications, ...bskyPostEmbedNotifications, ...mentionNotifications, ...commentMentionNotifications]; + const allHydrated = [...commentNotifications, ...subscribeNotifications, ...quoteNotifications, ...bskyPostEmbedNotifications, ...mentionNotifications, ...commentMentionNotifications, ...recommendNotifications]; // Sort by created_at to maintain order allHydrated.sort( @@ -519,6 +522,58 @@ async function hydrateCommentMentionNotifications(notifications: NotificationRow .filter((n) => n !== null); } +export type HydratedRecommendNotification = Awaited< + ReturnType +>[0]; + +async function hydrateRecommendNotifications(notifications: NotificationRow[]) { + const recommendNotifications = notifications.filter( + (n): n is NotificationRow & { data: ExtractNotificationType<"recommend"> } => + (n.data as NotificationData)?.type === "recommend", + ); + + if (recommendNotifications.length === 0) { + return []; + } + + // Fetch recommend data from the database + const recommendUris = recommendNotifications.map((n) => n.data.recommend_uri); + const documentUris = recommendNotifications.map((n) => n.data.document_uri); + + const [{ data: recommends }, { data: documents }] = await Promise.all([ + supabaseServerClient + .from("recommends_on_documents") + .select("*, identities(bsky_profiles(*))") + .in("uri", recommendUris), + supabaseServerClient + .from("documents") + .select("*, documents_in_publications(publications(*))") + .in("uri", documentUris), + ]); + + return recommendNotifications + .map((notification) => { + const recommendData = recommends?.find((r) => r.uri === notification.data.recommend_uri); + const document = documents?.find((d) => d.uri === notification.data.document_uri); + if (!recommendData || !document) return null; + return { + id: notification.id, + recipient: notification.recipient, + created_at: notification.created_at, + type: "recommend" as const, + recommend_uri: notification.data.recommend_uri, + document_uri: notification.data.document_uri, + recommendData, + document, + normalizedDocument: normalizeDocumentRecord(document.data, document.uri), + normalizedPublication: normalizePublicationRecord( + document.documents_in_publications[0]?.publications?.record, + ), + }; + }) + .filter((n) => n !== null); +} + export async function pingIdentityToUpdateNotification(did: string) { let channel = supabaseServerClient.channel(`identity.atp_did:${did}`); await channel.send({ -- 2.51.2 From 856e0c8fa0829bf8b382775d1483f96233cc758e Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Tue, 10 Feb 2026 13:01:14 +0400 Subject: [PATCH 07/28] fix post links to standard site blogs --- .../BskyPostEmbedNotification.tsx | 7 +-- .../CommentMentionNotification.tsx | 10 ++--- .../notifications/CommentNotication.tsx | 10 ++--- .../notifications/MentionNotification.tsx | 9 +--- .../notifications/QuoteNotification.tsx | 8 +--- .../notifications/RecommendNotification.tsx | 7 +-- .../notifications/ReplyNotification.tsx | 10 ++--- .../actions/ShareOptions/index.tsx | 16 ++++--- .../[command]/search_publication_documents.ts | 12 ++--- .../[publication]/[rkey]/getPostPageData.ts | 12 ++--- .../dashboard/PublishedPostsLists.tsx | 7 +-- app/lish/[did]/[publication]/generateFeed.ts | 6 ++- app/lish/[did]/[publication]/page.tsx | 7 +-- app/lish/createPub/getPublicationURL.ts | 45 +++++++++++++++++++ components/PageSWRDataProvider.tsx | 28 +++++++++--- components/PostListing.tsx | 5 +-- 16 files changed, 123 insertions(+), 76 deletions(-) diff --git a/app/(home-pages)/notifications/BskyPostEmbedNotification.tsx b/app/(home-pages)/notifications/BskyPostEmbedNotification.tsx index 2b433771..66cf0018 100644 --- a/app/(home-pages)/notifications/BskyPostEmbedNotification.tsx +++ b/app/(home-pages)/notifications/BskyPostEmbedNotification.tsx @@ -2,6 +2,7 @@ import { BlueskyTiny } from "components/Icons/BlueskyTiny"; import { ContentLayout, Notification } from "./Notification"; import { HydratedBskyPostEmbedNotification } from "src/notifications"; import { AtUri } from "@atproto/api"; +import { getDocumentURL } from "app/lish/createPub/getPublicationURL"; export const BskyPostEmbedNotification = ( props: HydratedBskyPostEmbedNotification, @@ -11,11 +12,7 @@ export const BskyPostEmbedNotification = ( if (!docRecord) return null; - const docUri = new AtUri(props.document.uri); - const rkey = docUri.rkey; - const did = docUri.host; - - const href = pubRecord ? `${pubRecord.url}/${rkey}` : `/p/${did}/${rkey}`; + const href = getDocumentURL(docRecord, props.document.uri, pubRecord); const embedder = props.documentCreatorHandle ? `@${props.documentCreatorHandle}` diff --git a/app/(home-pages)/notifications/CommentMentionNotification.tsx b/app/(home-pages)/notifications/CommentMentionNotification.tsx index 10917d61..3bf1e63d 100644 --- a/app/(home-pages)/notifications/CommentMentionNotification.tsx +++ b/app/(home-pages)/notifications/CommentMentionNotification.tsx @@ -8,6 +8,7 @@ import { Notification, } from "./Notification"; import { AtUri } from "@atproto/api"; +import { getDocumentURL } from "app/lish/createPub/getPublicationURL"; export const CommentMentionNotification = ( props: HydratedCommentMentionNotification, @@ -19,13 +20,10 @@ export const CommentMentionNotification = ( const profileRecord = props.commentData.bsky_profiles ?.record as AppBskyActorProfile.Record; const pubRecord = props.normalizedPublication; - const docUri = new AtUri(props.commentData.documents?.uri!); - const rkey = docUri.rkey; - const did = docUri.host; - const href = pubRecord - ? `${pubRecord.url}/${rkey}?interactionDrawer=comments` - : `/p/${did}/${rkey}?interactionDrawer=comments`; + const href = + getDocumentURL(docRecord, props.commentData.documents?.uri!, pubRecord) + + "?interactionDrawer=comments"; const commenter = props.commenterHandle ? `@${props.commenterHandle}` diff --git a/app/(home-pages)/notifications/CommentNotication.tsx b/app/(home-pages)/notifications/CommentNotication.tsx index 500d2783..fde0e160 100644 --- a/app/(home-pages)/notifications/CommentNotication.tsx +++ b/app/(home-pages)/notifications/CommentNotication.tsx @@ -10,6 +10,7 @@ import { Notification, } from "./Notification"; import { AtUri } from "@atproto/api"; +import { getDocumentURL } from "app/lish/createPub/getPublicationURL"; export const CommentNotification = (props: HydratedCommentNotification) => { const docRecord = props.normalizedDocument; @@ -24,13 +25,10 @@ export const CommentNotification = (props: HydratedCommentNotification) => { props.commentData.bsky_profiles?.handle || "Someone"; const pubRecord = props.normalizedPublication; - const docUri = new AtUri(props.commentData.documents?.uri!); - const rkey = docUri.rkey; - const did = docUri.host; - const href = pubRecord - ? `${pubRecord.url}/${rkey}?interactionDrawer=comments` - : `/p/${did}/${rkey}?interactionDrawer=comments`; + const href = + getDocumentURL(docRecord, props.commentData.documents?.uri!, pubRecord) + + "?interactionDrawer=comments"; return ( { const docRecord = props.normalizedDocument; @@ -9,13 +10,7 @@ export const MentionNotification = (props: HydratedMentionNotification) => { if (!docRecord) return null; - const docUri = new AtUri(props.document.uri); - const rkey = docUri.rkey; - const did = docUri.host; - - const href = pubRecord - ? `${pubRecord.url}/${rkey}` - : `/p/${did}/${rkey}`; + const href = getDocumentURL(docRecord, props.document.uri, pubRecord); let actionText: React.ReactNode; let mentionedItemName: string | undefined; diff --git a/app/(home-pages)/notifications/QuoteNotification.tsx b/app/(home-pages)/notifications/QuoteNotification.tsx index d6849205..6c7b3f16 100644 --- a/app/(home-pages)/notifications/QuoteNotification.tsx +++ b/app/(home-pages)/notifications/QuoteNotification.tsx @@ -3,6 +3,7 @@ import { ContentLayout, Notification } from "./Notification"; import { HydratedQuoteNotification } from "src/notifications"; import { AtUri } from "@atproto/api"; import { Avatar } from "components/Avatar"; +import { getDocumentURL } from "app/lish/createPub/getPublicationURL"; export const QuoteNotification = (props: HydratedQuoteNotification) => { const postView = props.bskyPost.post_view as any; @@ -13,14 +14,9 @@ export const QuoteNotification = (props: HydratedQuoteNotification) => { if (!docRecord) return null; - const docUri = new AtUri(props.document.uri); - const rkey = docUri.rkey; - const did = docUri.host; const postText = postView.record?.text || ""; - const href = pubRecord - ? `${pubRecord.url}/${rkey}` - : `/p/${did}/${rkey}`; + const href = getDocumentURL(docRecord, props.document.uri, pubRecord); return ( { const docRecord = props.normalizedDocument; @@ -32,14 +33,11 @@ export const ReplyNotification = (props: HydratedCommentNotification) => { props.parentData?.bsky_profiles?.handle || "Someone"; - const docUri = new AtUri(props.commentData.documents?.uri!); - const rkey = docUri.rkey; - const did = docUri.host; const pubRecord = props.normalizedPublication; - const href = pubRecord - ? `${pubRecord.url}/${rkey}?interactionDrawer=comments` - : `/p/${did}/${rkey}?interactionDrawer=comments`; + const href = + getDocumentURL(docRecord, props.commentData.documents?.uri!, pubRecord) + + "?interactionDrawer=comments"; return ( (null); useEffect(() => { diff --git a/app/api/rpc/[command]/search_publication_documents.ts b/app/api/rpc/[command]/search_publication_documents.ts index a58f6867..a52be5f6 100644 --- a/app/api/rpc/[command]/search_publication_documents.ts +++ b/app/api/rpc/[command]/search_publication_documents.ts @@ -2,7 +2,8 @@ import { AtUri } from "@atproto/api"; import { z } from "zod"; import { makeRoute } from "../lib"; import type { Env } from "./route"; -import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; +import { getDocumentURL } from "app/lish/createPub/getPublicationURL"; +import { normalizeDocumentRecord } from "src/utils/normalizeRecords"; export type SearchPublicationDocumentsReturnType = Awaited< ReturnType<(typeof search_publication_documents)["handler"]> @@ -37,13 +38,14 @@ export const search_publication_documents = makeRoute({ } const result = documents.map((d) => { - const docUri = new AtUri(d.documents.uri); - const pubUrl = getPublicationURL(d.publications); + const normalizedDoc = normalizeDocumentRecord(d.documents.data, d.documents.uri); return { uri: d.documents.uri, - title: (d.documents.data as { title?: string })?.title || "Untitled", - url: `${pubUrl}/${docUri.rkey}`, + title: normalizedDoc?.title || (d.documents.data as { title?: string })?.title || "Untitled", + url: normalizedDoc + ? getDocumentURL(normalizedDoc, d.documents.uri, d.publications) + : `${d.documents.uri}`, }; }); diff --git a/app/lish/[did]/[publication]/[rkey]/getPostPageData.ts b/app/lish/[did]/[publication]/[rkey]/getPostPageData.ts index 7f16418a..49eb500b 100644 --- a/app/lish/[did]/[publication]/[rkey]/getPostPageData.ts +++ b/app/lish/[did]/[publication]/[rkey]/getPostPageData.ts @@ -8,6 +8,7 @@ import { } from "src/utils/normalizeRecords"; import { PubLeafletPublication, SiteStandardPublication } from "lexicons/api"; import { documentUriFilter } from "src/utils/uriHelpers"; +import { getDocumentURL } from "app/lish/createPub/getPublicationURL"; export async function getPostPageData(did: string, rkey: string) { let { data: documents } = await supabaseServerClient @@ -46,11 +47,12 @@ export async function getPostPageData(did: string, rkey: string) { ); // Fetch constellation backlinks for mentions - let aturi = new AtUri(document.uri); - const postUrl = normalizedPublication - ? `${normalizedPublication.url}/${aturi.rkey}` - : `https://leaflet.pub/p/${aturi.host}/${aturi.rkey}`; - const constellationBacklinks = await getConstellationBacklinks(postUrl); + const postUrl = getDocumentURL(normalizedDocument, document.uri, normalizedPublication); + // Constellation needs an absolute URL + const absolutePostUrl = postUrl.startsWith("/") + ? `https://leaflet.pub${postUrl}` + : postUrl; + const constellationBacklinks = await getConstellationBacklinks(absolutePostUrl); // Deduplicate constellation backlinks (same post could appear in both links and embeds) const uniqueBacklinks = Array.from( diff --git a/app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx b/app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx index 29b4b8a8..a3e25db9 100644 --- a/app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx +++ b/app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx @@ -9,7 +9,7 @@ import { } from "./PublicationSWRProvider"; import { Fragment } from "react"; import { useParams } from "next/navigation"; -import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; +import { getPublicationURL, getDocumentURL } from "app/lish/createPub/getPublicationURL"; import { SpeedyLink } from "components/SpeedyLink"; import { InteractionPreview } from "components/InteractionsPreview"; import { useLocalizedDate } from "src/hooks/useLocalizedDate"; @@ -71,6 +71,7 @@ function PublishedPostItem(props: { const leaflet = publication.leaflets_in_publications.find( (l) => l.doc === doc.uri, ); + const docUrl = getDocumentURL(doc.record, doc.uri, publication); return ( @@ -87,7 +88,7 @@ function PublishedPostItem(props: {

{doc.record.title} @@ -144,7 +145,7 @@ function PublishedPostItem(props: { showComments={pubRecord?.preferences?.showComments !== false} showMentions={pubRecord?.preferences?.showMentions !== false} showRecommends={pubRecord?.preferences?.showRecommends !== false} - postUrl={`${getPublicationURL(publication)}/${uri.rkey}`} + postUrl={docUrl} /> diff --git a/app/lish/[did]/[publication]/generateFeed.ts b/app/lish/[did]/[publication]/generateFeed.ts index e12aa75f..383f9461 100644 --- a/app/lish/[did]/[publication]/generateFeed.ts +++ b/app/lish/[did]/[publication]/generateFeed.ts @@ -11,6 +11,7 @@ import { hasLeafletContent, } from "src/utils/normalizeRecords"; import { publicationNameOrUriFilter } from "src/utils/uriHelpers"; +import { getDocumentURL } from "app/lish/createPub/getPublicationURL"; export async function generateFeed( did: string, @@ -84,12 +85,13 @@ export async function generateFeed( } } + const docUrl = getDocumentURL(record, doc.documents.uri, pubRecord); feed.addItem({ title: record.title, description: record.description, date: record.publishedAt ? new Date(record.publishedAt) : new Date(), - id: `${pubRecord.url}/${rkey}`, - link: `${pubRecord.url}/${rkey}`, + id: docUrl, + link: docUrl, content: chunks.join(""), }); }), diff --git a/app/lish/[did]/[publication]/page.tsx b/app/lish/[did]/[publication]/page.tsx index 457f38b2..6d6e2e31 100644 --- a/app/lish/[did]/[publication]/page.tsx +++ b/app/lish/[did]/[publication]/page.tsx @@ -1,6 +1,6 @@ import { supabaseServerClient } from "supabase/serverClient"; import { AtUri } from "@atproto/syntax"; -import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; +import { getPublicationURL, getDocumentURL } from "app/lish/createPub/getPublicationURL"; import { BskyAgent } from "@atproto/api"; import { publicationNameOrUriFilter } from "src/utils/uriHelpers"; import { SubscribeWithBluesky } from "app/lish/Subscribe"; @@ -135,11 +135,12 @@ export default async function Publication(props: { doc.documents.recommends_on_documents?.[0]?.count || 0; let tags = doc_record.tags || []; + const docUrl = getDocumentURL(doc_record, doc.documents.uri, publication); return (

{doc_record.title}

@@ -168,7 +169,7 @@ export default async function Publication(props: { recommendsCount={recommends} documentUri={doc.documents.uri} tags={tags} - postUrl={`${getPublicationURL(publication)}/${uri.rkey}`} + postUrl={docUrl} showComments={ record?.preferences?.showComments !== false } diff --git a/app/lish/createPub/getPublicationURL.ts b/app/lish/createPub/getPublicationURL.ts index 613bef1b..3f864599 100644 --- a/app/lish/createPub/getPublicationURL.ts +++ b/app/lish/createPub/getPublicationURL.ts @@ -5,6 +5,8 @@ import { Json } from "supabase/database.types"; import { normalizePublicationRecord, isLeafletPublication, + hasLeafletContent, + type NormalizedDocument, type NormalizedPublication, } from "src/utils/normalizeRecords"; @@ -44,3 +46,46 @@ export function getBasePublicationURL(pub: PublicationInput): string { const name = aturi.rkey || normalized?.name; return `/lish/${aturi.host}/${encodeURIComponent(name || "")}`; } + +/** + * Gets the full URL for a document. + * Always appends the document's path property. + * For non-leaflet documents (content.$type !== "pub.leaflet.content"), + * always uses the full publication site URL, not internal /lish/ URLs. + */ +export function getDocumentURL( + doc: NormalizedDocument, + docUri: string, + publication?: PublicationInput | NormalizedPublication | null, +): string { + const path = doc.path || "/" + new AtUri(docUri).rkey; + const aturi = new AtUri(docUri); + + const isNormalized = + !!publication && + (publication as NormalizedPublication).$type === "site.standard.publication"; + const normPub = isNormalized + ? (publication as NormalizedPublication) + : publication + ? normalizePublicationRecord((publication as PublicationInput).record) + : null; + const pubInput = isNormalized ? null : (publication as PublicationInput | null); + + // Non-leaflet documents always use the full publication site URL + if (doc.content && !hasLeafletContent(doc) && normPub?.url) { + return normPub.url + path; + } + + // For leaflet documents, use getPublicationURL (may return /lish/ internal paths) + if (pubInput) { + return getPublicationURL(pubInput) + path; + } + + // When we only have a normalized publication, use its URL directly + if (normPub?.url) { + return normPub.url + path; + } + + // Standalone document fallback + return `/p/${aturi.host}${path}`; +} diff --git a/components/PageSWRDataProvider.tsx b/components/PageSWRDataProvider.tsx index 227c17e9..189a4e43 100644 --- a/components/PageSWRDataProvider.tsx +++ b/components/PageSWRDataProvider.tsx @@ -8,7 +8,7 @@ import { getPollData } from "actions/pollActions"; import type { GetLeafletDataReturnType } from "app/api/rpc/[command]/get_leaflet_data"; import { createContext, useContext, useMemo } from "react"; import { getPublicationMetadataFromLeafletData } from "src/utils/getPublicationMetadataFromLeafletData"; -import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; +import { getPublicationURL, getDocumentURL } from "app/lish/createPub/getPublicationURL"; import { AtUri } from "@atproto/syntax"; import { normalizeDocumentRecord, @@ -119,13 +119,27 @@ export function useLeafletPublicationStatus() { // Compute the full post URL for sharing let postShareLink: string | undefined; if (publishedInPublication?.publications && publishedInPublication.documents) { - // Published in a publication - use publication URL + document rkey - const docUri = new AtUri(publishedInPublication.documents.uri); - postShareLink = `${getPublicationURL(publishedInPublication.publications)}/${docUri.rkey}`; + const normalizedDoc = normalizeDocumentRecord( + publishedInPublication.documents.data, + publishedInPublication.documents.uri, + ); + if (normalizedDoc) { + postShareLink = getDocumentURL( + normalizedDoc, + publishedInPublication.documents.uri, + publishedInPublication.publications, + ); + } } else if (publishedStandalone?.document) { - // Standalone published post - use /p/{did}/{rkey} format - const docUri = new AtUri(publishedStandalone.document); - postShareLink = `/p/${docUri.host}/${docUri.rkey}`; + const normalizedDoc = publishedStandalone.documents + ? normalizeDocumentRecord(publishedStandalone.documents.data, publishedStandalone.document) + : null; + if (normalizedDoc) { + postShareLink = getDocumentURL(normalizedDoc, publishedStandalone.document); + } else { + const docUri = new AtUri(publishedStandalone.document); + postShareLink = `/p/${docUri.host}/${docUri.rkey}`; + } } return { diff --git a/components/PostListing.tsx b/components/PostListing.tsx index 32bd9bd4..cee5cd5a 100644 --- a/components/PostListing.tsx +++ b/components/PostListing.tsx @@ -18,6 +18,7 @@ import Link from "next/link"; import { InteractionPreview } from "./InteractionsPreview"; import { useLocalizedDate } from "src/hooks/useLocalizedDate"; import { mergePreferences } from "src/utils/mergePreferences"; +import { getDocumentURL } from "app/lish/createPub/getPublicationURL"; export const PostListing = (props: Post) => { let pubRecord = props.publication?.pubRecord as @@ -60,9 +61,7 @@ export const PostListing = (props: Post) => { let tags = (postRecord?.tags as string[] | undefined) || []; // For standalone posts, link directly to the document - let postHref = props.publication - ? `${props.publication.href}/${postUri.rkey}` - : `/p/${postUri.host}/${postUri.rkey}`; + let postHref = getDocumentURL(postRecord, props.documents.uri, pubRecord); return ( -- 2.51.2 From e023d527a637797a9daf04cc1e2df77e2f8a9bf1 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Tue, 10 Feb 2026 13:23:51 +0400 Subject: [PATCH 08/28] don't getpreferences on subscribe --- app/lish/subscribeToPublication.ts | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/app/lish/subscribeToPublication.ts b/app/lish/subscribeToPublication.ts index e321361d..ed712efe 100644 --- a/app/lish/subscribeToPublication.ts +++ b/app/lish/subscribeToPublication.ts @@ -3,10 +3,7 @@ import { AtpBaseClient } from "lexicons/api"; import { AppBskyActorDefs, Agent as BskyAgent } from "@atproto/api"; import { getIdentityData } from "actions/getIdentityData"; -import { - restoreOAuthSession, - OAuthSessionError, -} from "src/atproto-oauth"; +import { restoreOAuthSession, OAuthSessionError } from "src/atproto-oauth"; import { TID } from "@atproto/common"; import { supabaseServerClient } from "supabase/serverClient"; import { revalidatePath } from "next/cache"; @@ -79,8 +76,7 @@ export async function subscribeToPublication( } let bsky = new BskyAgent(credentialSession); - let [prefs, profile, resolveDid] = await Promise.all([ - bsky.app.bsky.actor.getPreferences(), + let [profile, resolveDid] = await Promise.all([ bsky.app.bsky.actor.profile .get({ repo: credentialSession.did!, @@ -96,13 +92,10 @@ export async function subscribeToPublication( handle: resolveDid?.alsoKnownAs?.[0]?.slice(5), }); } - let savedFeeds = prefs.data.preferences.find( - (pref) => pref.$type === "app.bsky.actor.defs#savedFeedsPrefV2", - ) as AppBskyActorDefs.SavedFeedsPrefV2; revalidatePath("/lish/[did]/[publication]", "layout"); return { success: true, - hasFeed: !!savedFeeds.items.find((feed) => feed.value === leafletFeedURI), + hasFeed: true, }; } @@ -111,7 +104,7 @@ type UnsubscribeResult = | { success: false; error: OAuthSessionError }; export async function unsubscribeToPublication( - publication: string + publication: string, ): Promise { let identity = await getIdentityData(); if (!identity || !identity.atp_did) { @@ -144,8 +137,12 @@ export async function unsubscribeToPublication( // Delete from both collections (old and new schema) - one or both may exist let rkey = new AtUri(existingSubscription.uri).rkey; await Promise.all([ - agent.pub.leaflet.graph.subscription.delete({ repo: credentialSession.did!, rkey }).catch(() => {}), - agent.site.standard.graph.subscription.delete({ repo: credentialSession.did!, rkey }).catch(() => {}), + agent.pub.leaflet.graph.subscription + .delete({ repo: credentialSession.did!, rkey }) + .catch(() => {}), + agent.site.standard.graph.subscription + .delete({ repo: credentialSession.did!, rkey }) + .catch(() => {}), ]); await supabaseServerClient -- 2.51.2 From 3ccb429f1b08799df585438be427195cf975325e Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Tue, 10 Feb 2026 17:04:12 +0400 Subject: [PATCH 09/28] add recommend column to documents --- .../20260210000000_add_recommend_count.sql | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 supabase/migrations/20260210000000_add_recommend_count.sql diff --git a/supabase/migrations/20260210000000_add_recommend_count.sql b/supabase/migrations/20260210000000_add_recommend_count.sql new file mode 100644 index 00000000..f0dd1160 --- /dev/null +++ b/supabase/migrations/20260210000000_add_recommend_count.sql @@ -0,0 +1,23 @@ +ALTER TABLE documents ADD COLUMN recommend_count integer NOT NULL DEFAULT 0; + +UPDATE documents d +SET recommend_count = ( + SELECT COUNT(*) FROM recommends_on_documents r WHERE r.document = d.uri +); + +CREATE OR REPLACE FUNCTION update_recommend_count() RETURNS trigger AS $$ +BEGIN + IF TG_OP = 'INSERT' THEN + UPDATE documents SET recommend_count = recommend_count + 1 + WHERE uri = NEW.document; + ELSIF TG_OP = 'DELETE' THEN + UPDATE documents SET recommend_count = recommend_count - 1 + WHERE uri = OLD.document; + END IF; + RETURN NULL; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER trg_recommend_count +AFTER INSERT OR DELETE ON recommends_on_documents +FOR EACH ROW EXECUTE FUNCTION update_recommend_count(); -- 2.51.2 From 1021011dc090d8a4b74cad59342326afa77445f1 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Tue, 10 Feb 2026 21:44:21 +0400 Subject: [PATCH 10/28] check permissions on retractions --- src/replicache/cachedServerMutationContext.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/replicache/cachedServerMutationContext.ts b/src/replicache/cachedServerMutationContext.ts index 322f773e..cad2eb64 100644 --- a/src/replicache/cachedServerMutationContext.ts +++ b/src/replicache/cachedServerMutationContext.ts @@ -155,6 +155,20 @@ export function cachedServerMutationContext( }); }, async retractFact(factID) { + let cachedFact = writeCache.find( + (f) => f.type === "put" && f.fact.id === factID, + ); + let entity: string | undefined; + if (cachedFact && cachedFact.type === "put") { + entity = cachedFact.fact.entity; + } else { + let [row] = await tx + .select({ entity: facts.entity }) + .from(facts) + .where(driz.eq(facts.id, factID)); + entity = row?.entity; + } + if (!entity || !(await this.checkPermission(entity))) return; writeCache = writeCache.filter((f) => f.fact.id !== factID); writeCache.push({ type: "del", fact: { id: factID } }); }, -- 2.51.2 From 27b896242aa84b1d3de24c74cafc1ca77691b6dc Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Tue, 10 Feb 2026 21:50:45 +0400 Subject: [PATCH 11/28] don't show block options if no write permissions --- components/Blocks/Block.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/components/Blocks/Block.tsx b/components/Blocks/Block.tsx index dbf1e61c..33ff933a 100644 --- a/components/Blocks/Block.tsx +++ b/components/Blocks/Block.tsx @@ -419,6 +419,7 @@ const NonTextBlockOptions = (props: { if (focusedEntity?.entityType === "page") return; if (isMultiselected) return; + if (!entity_set.permissions.write) return null; return (
Date: Tue, 10 Feb 2026 22:14:23 +0400 Subject: [PATCH 12/28] handle pasting bluesky posts --- components/Blocks/TextBlock/useHandlePaste.ts | 21 ++++++++++++++++++- src/utils/getBlocksAsHTML.tsx | 11 +++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/components/Blocks/TextBlock/useHandlePaste.ts b/components/Blocks/TextBlock/useHandlePaste.ts index 1e50bc5d..3e165a67 100644 --- a/components/Blocks/TextBlock/useHandlePaste.ts +++ b/components/Blocks/TextBlock/useHandlePaste.ts @@ -396,6 +396,24 @@ const createBlockFromHTML = ( ]); } + if (child.tagName === "DIV" && child.getAttribute("data-bluesky-post")) { + let postData = child.getAttribute("data-bluesky-post"); + if (postData) { + rep.mutate.assertFact([ + { + entity: entityID, + attribute: "block/type", + data: { type: "block-type-union", value: "bluesky-post" }, + }, + { + entity: entityID, + attribute: "block/bluesky-post", + data: { type: "bluesky-post", value: JSON.parse(postData) }, + }, + ]); + } + } + if (child.tagName === "DIV" && child.getAttribute("data-entityid")) { let oldEntityID = child.getAttribute("data-entityid") as string; let factsData = child.getAttribute("data-facts"); @@ -593,7 +611,8 @@ function flattenHTMLToTextBlocks(element: HTMLElement): HTMLElement[] { "HR", ].includes(elementNode.tagName) || elementNode.getAttribute("data-entityid") || - elementNode.getAttribute("data-tex") + elementNode.getAttribute("data-tex") || + elementNode.getAttribute("data-bluesky-post") ) { htmlBlocks.push(elementNode); } else { diff --git a/src/utils/getBlocksAsHTML.tsx b/src/utils/getBlocksAsHTML.tsx index 98bf380a..d9ac5b73 100644 --- a/src/utils/getBlocksAsHTML.tsx +++ b/src/utils/getBlocksAsHTML.tsx @@ -79,7 +79,16 @@ const BlockTypeToHTML: { mailbox: async () => null, poll: async () => null, embed: async () => null, - "bluesky-post": async () => null, + "bluesky-post": async (b, tx) => { + let [post] = await scanIndex(tx).eav(b.value, "block/bluesky-post"); + if (!post) return null; + return ( +
+ ); + }, math: async (b, tx, a) => { let [math] = await scanIndex(tx).eav(b.value, "block/math"); const html = Katex.renderToString(math?.data.value || "", { -- 2.51.2 From f94d0da04728872b981230b270a447ba7d1b1fd3 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Wed, 11 Feb 2026 10:51:04 +0400 Subject: [PATCH 13/28] add indexed col to documents and update inngest --- app/api/inngest/client.ts | 4 +-- ...sky_likes.ts => sync_document_metadata.ts} | 30 +++++++++++------ app/api/inngest/route.tsx | 4 +-- appview/index.ts | 32 ++++++++----------- supabase/database.types.ts | 8 +++++ ...0_add_indexed_column_and_ranking_index.sql | 6 ++++ 6 files changed, 52 insertions(+), 32 deletions(-) rename app/api/inngest/functions/{sync_bsky_likes.ts => sync_document_metadata.ts} (62%) create mode 100644 supabase/migrations/20260210100000_add_indexed_column_and_ranking_index.sql diff --git a/app/api/inngest/client.ts b/app/api/inngest/client.ts index f648b39f..ffce505c 100644 --- a/app/api/inngest/client.ts +++ b/app/api/inngest/client.ts @@ -51,10 +51,10 @@ export type Events = { documentUris?: string[]; }; }; - "appview/sync-bsky-likes": { + "appview/sync-document-metadata": { data: { document_uri: string; - bsky_post_uri: string; + bsky_post_uri?: string; }; }; "user/write-records-to-pds": { diff --git a/app/api/inngest/functions/sync_bsky_likes.ts b/app/api/inngest/functions/sync_document_metadata.ts similarity index 62% rename from app/api/inngest/functions/sync_bsky_likes.ts rename to app/api/inngest/functions/sync_document_metadata.ts index 69dfe3c6..d2836a2a 100644 --- a/app/api/inngest/functions/sync_bsky_likes.ts +++ b/app/api/inngest/functions/sync_document_metadata.ts @@ -5,26 +5,36 @@ import { idResolver } from "app/(home-pages)/reader/idResolver"; const TOTAL_ITERATIONS = 144; // 36 hours at 15-minute intervals -export const sync_bsky_likes = inngest.createFunction( +export const sync_document_metadata = inngest.createFunction( { - id: "sync_bsky_likes", - idempotency: "event.data.bsky_post_uri", + id: "sync_document_metadata", + idempotency: "event.data.document_uri", }, - { event: "appview/sync-bsky-likes" }, + { event: "appview/sync-document-metadata" }, async ({ event, step }) => { const { document_uri, bsky_post_uri } = event.data; - const isBridgy = await step.run("check-bridgy", async () => { - const did = new AtUri(bsky_post_uri).host; + const did = new AtUri(document_uri).host; + + const handleResult = await step.run("resolve-handle", async () => { const doc = await idResolver.did.resolve(did); const handle = doc?.alsoKnownAs ?.find((a) => a.startsWith("at://")) ?.replace("at://", ""); - return handle?.includes("brid.gy") ?? false; + return { handle: handle ?? null, isBridgy: handle?.includes("brid.gy") ?? false }; }); - if (isBridgy) { - return { skipped: true, reason: "brid.gy post" }; + if (handleResult.isBridgy) { + await step.run("set-unindexed", async () => { + await supabaseServerClient + .from("documents") + .update({ indexed: false }) + .eq("uri", document_uri); + }); + } + + if (!bsky_post_uri || handleResult.isBridgy) { + return { handle: handleResult.handle }; } const agent = new AtpAgent({ service: "https://public.api.bsky.app" }); @@ -50,6 +60,6 @@ export const sync_bsky_likes = inngest.createFunction( likeCount = await step.run(`sync-${i}`, fetchAndUpdate); } - return { likeCount }; + return { likeCount, handle: handleResult.handle }; }, ); diff --git a/app/api/inngest/route.tsx b/app/api/inngest/route.tsx index ea0f2f9e..a74f1d98 100644 --- a/app/api/inngest/route.tsx +++ b/app/api/inngest/route.tsx @@ -13,7 +13,7 @@ import { check_oauth_session, } from "./functions/cleanup_expired_oauth_sessions"; import { write_records_to_pds } from "./functions/write_records_to_pds"; -import { sync_bsky_likes } from "./functions/sync_bsky_likes"; +import { sync_document_metadata } from "./functions/sync_document_metadata"; export const { GET, POST, PUT } = serve({ client: inngest, @@ -29,6 +29,6 @@ export const { GET, POST, PUT } = serve({ cleanup_expired_oauth_sessions, check_oauth_session, write_records_to_pds, - sync_bsky_likes, + sync_document_metadata, ], }); diff --git a/appview/index.ts b/appview/index.ts index 7319f549..31ead5fb 100644 --- a/appview/index.ts +++ b/appview/index.ts @@ -109,15 +109,13 @@ async function handleEvent(evt: Event) { data: record.value as Json, }); if (docResult.error) console.log(docResult.error); - if (record.value.postRef?.uri) { - await inngest.send({ - name: "appview/sync-bsky-likes", - data: { - document_uri: evt.uri.toString(), - bsky_post_uri: record.value.postRef.uri, - }, - }); - } + await inngest.send({ + name: "appview/sync-document-metadata", + data: { + document_uri: evt.uri.toString(), + bsky_post_uri: record.value.postRef?.uri, + }, + }); if (record.value.publication) { let publicationURI = new AtUri(record.value.publication); @@ -278,15 +276,13 @@ async function handleEvent(evt: Event) { data: record.value as Json, }); if (docResult.error) console.log(docResult.error); - if (record.value.bskyPostRef?.uri) { - await inngest.send({ - name: "appview/sync-bsky-likes", - data: { - document_uri: evt.uri.toString(), - bsky_post_uri: record.value.bskyPostRef.uri, - }, - }); - } + await inngest.send({ + name: "appview/sync-document-metadata", + data: { + document_uri: evt.uri.toString(), + bsky_post_uri: record.value.bskyPostRef?.uri, + }, + }); // site.standard.document uses "site" field to reference the publication // For documents in publications, site is an AT-URI (at://did:plc:xxx/site.standard.publication/rkey) diff --git a/supabase/database.types.ts b/supabase/database.types.ts index 1eb727c2..c4f53bde 100644 --- a/supabase/database.types.ts +++ b/supabase/database.types.ts @@ -337,20 +337,28 @@ export type Database = { Row: { bsky_like_count: number data: Json + indexed: boolean indexed_at: string + recommend_count: number sort_date: string uri: string } Insert: { bsky_like_count?: number data: Json + indexed?: boolean indexed_at?: string + recommend_count?: number + sort_date?: string uri: string } Update: { bsky_like_count?: number data?: Json + indexed?: boolean indexed_at?: string + recommend_count?: number + sort_date?: string uri?: string } Relationships: [] diff --git a/supabase/migrations/20260210100000_add_indexed_column_and_ranking_index.sql b/supabase/migrations/20260210100000_add_indexed_column_and_ranking_index.sql new file mode 100644 index 00000000..562f3b18 --- /dev/null +++ b/supabase/migrations/20260210100000_add_indexed_column_and_ranking_index.sql @@ -0,0 +1,6 @@ +ALTER TABLE documents ADD COLUMN indexed boolean NOT NULL DEFAULT true; + +CREATE INDEX idx_documents_ranking +ON documents (sort_date DESC) +INCLUDE (uri, bsky_like_count, recommend_count) +WHERE indexed = true; -- 2.51.2 From 1a7a36b5ac5ea04aca0c4a58f47a3fdbd7e23ff9 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Wed, 11 Feb 2026 11:06:46 +0400 Subject: [PATCH 14/28] fix type error --- app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx b/app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx index a3e25db9..1de03e91 100644 --- a/app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx +++ b/app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx @@ -114,6 +114,8 @@ function PublishedPostItem(props: { sort_date: doc.sort_date, data: doc.data, bsky_like_count: doc.bsky_like_count ?? 0, + indexed: true, + recommend_count: doc.recommendsCount ?? 0, }, }, ], -- 2.51.2 From 1af1ce6f916bd0192b814f11864f1216ed0ac649 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Wed, 11 Feb 2026 13:21:18 +0400 Subject: [PATCH 15/28] filter out posts from feeds --- feeds/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/feeds/index.ts b/feeds/index.ts index 279f5d7c..a30e229f 100644 --- a/feeds/index.ts +++ b/feeds/index.ts @@ -115,6 +115,7 @@ app.get("/xrpc/app.bsky.feed.getFeedSkeleton", async (c) => { ); } query = query + .eq("indexed", true) .or("data->postRef.not.is.null,data->bskyPostRef.not.is.null") .order("sort_date", { ascending: false }) .order("uri", { ascending: false }) -- 2.51.2 From 44da33e46d65a5604a97cb35fc95c436fbd18cf4 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Wed, 11 Feb 2026 14:13:31 +0400 Subject: [PATCH 16/28] explicitly set if documents are indexed after checking pds host --- .../functions/sync_document_metadata.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/app/api/inngest/functions/sync_document_metadata.ts b/app/api/inngest/functions/sync_document_metadata.ts index d2836a2a..35acb38d 100644 --- a/app/api/inngest/functions/sync_document_metadata.ts +++ b/app/api/inngest/functions/sync_document_metadata.ts @@ -21,17 +21,18 @@ export const sync_document_metadata = inngest.createFunction( const handle = doc?.alsoKnownAs ?.find((a) => a.startsWith("at://")) ?.replace("at://", ""); - return { handle: handle ?? null, isBridgy: handle?.includes("brid.gy") ?? false }; + const isBridgy = !!doc?.service?.find( + (s) => s.serviceEndpoint === "https://atproto.brid.gy", + ); + return { handle: handle ?? null, isBridgy }; }); - if (handleResult.isBridgy) { - await step.run("set-unindexed", async () => { - await supabaseServerClient - .from("documents") - .update({ indexed: false }) - .eq("uri", document_uri); - }); - } + await step.run("set-indexed", async () => { + await supabaseServerClient + .from("documents") + .update({ indexed: !handleResult.isBridgy }) + .eq("uri", document_uri); + }); if (!bsky_post_uri || handleResult.isBridgy) { return { handle: handleResult.handle }; -- 2.51.2 From fc660a92f04333949277b5e541815c4c75b312cc Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Wed, 11 Feb 2026 14:23:42 +0400 Subject: [PATCH 17/28] indexed false by default --- appview/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/appview/index.ts b/appview/index.ts index 31ead5fb..6c4c1548 100644 --- a/appview/index.ts +++ b/appview/index.ts @@ -107,6 +107,7 @@ async function handleEvent(evt: Event) { let docResult = await supabase.from("documents").upsert({ uri: evt.uri.toString(), data: record.value as Json, + indexed: false, }); if (docResult.error) console.log(docResult.error); await inngest.send({ @@ -274,6 +275,7 @@ async function handleEvent(evt: Event) { let docResult = await supabase.from("documents").upsert({ uri: evt.uri.toString(), data: record.value as Json, + indexed: false, }); if (docResult.error) console.log(docResult.error); await inngest.send({ -- 2.51.2 From ffb44ee2c5658377096de934ac383cc0a5beb553 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Wed, 11 Feb 2026 16:17:12 +0400 Subject: [PATCH 18/28] return if no handle in sync document metadata --- app/api/inngest/functions/sync_document_metadata.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/api/inngest/functions/sync_document_metadata.ts b/app/api/inngest/functions/sync_document_metadata.ts index 35acb38d..cab26e98 100644 --- a/app/api/inngest/functions/sync_document_metadata.ts +++ b/app/api/inngest/functions/sync_document_metadata.ts @@ -21,11 +21,13 @@ export const sync_document_metadata = inngest.createFunction( const handle = doc?.alsoKnownAs ?.find((a) => a.startsWith("at://")) ?.replace("at://", ""); + if (!doc) return null; const isBridgy = !!doc?.service?.find( (s) => s.serviceEndpoint === "https://atproto.brid.gy", ); return { handle: handle ?? null, isBridgy }; }); + if (!handleResult) return { error: "No Handle" }; await step.run("set-indexed", async () => { await supabaseServerClient -- 2.51.2 From 9c5c5c6b9ff8bc85f808e031cdaf2ed437dde7b5 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Wed, 11 Feb 2026 16:17:42 +0400 Subject: [PATCH 19/28] add doc to inngest return --- app/api/inngest/functions/sync_document_metadata.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/inngest/functions/sync_document_metadata.ts b/app/api/inngest/functions/sync_document_metadata.ts index cab26e98..79b95de7 100644 --- a/app/api/inngest/functions/sync_document_metadata.ts +++ b/app/api/inngest/functions/sync_document_metadata.ts @@ -25,7 +25,7 @@ export const sync_document_metadata = inngest.createFunction( const isBridgy = !!doc?.service?.find( (s) => s.serviceEndpoint === "https://atproto.brid.gy", ); - return { handle: handle ?? null, isBridgy }; + return { handle: handle ?? null, isBridgy, doc }; }); if (!handleResult) return { error: "No Handle" }; -- 2.51.2 From 5ba035764042b0475f7785df35a3e040e18f7519 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Wed, 11 Feb 2026 16:23:40 +0400 Subject: [PATCH 20/28] handle trailing brid.gy / --- app/api/inngest/functions/sync_document_metadata.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/api/inngest/functions/sync_document_metadata.ts b/app/api/inngest/functions/sync_document_metadata.ts index 79b95de7..8dc0c775 100644 --- a/app/api/inngest/functions/sync_document_metadata.ts +++ b/app/api/inngest/functions/sync_document_metadata.ts @@ -23,7 +23,9 @@ export const sync_document_metadata = inngest.createFunction( ?.replace("at://", ""); if (!doc) return null; const isBridgy = !!doc?.service?.find( - (s) => s.serviceEndpoint === "https://atproto.brid.gy", + (s) => + typeof s.serviceEndpoint === "string" && + s.serviceEndpoint.includes("atproto.brid.gy"), ); return { handle: handle ?? null, isBridgy, doc }; }); -- 2.51.2 From b48c51c8497284b859304447d1fc761a9745685a Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Wed, 11 Feb 2026 21:28:08 +0400 Subject: [PATCH 21/28] sort rss feeds properly --- app/lish/[did]/[publication]/generateFeed.ts | 82 +++++++++++--------- 1 file changed, 46 insertions(+), 36 deletions(-) diff --git a/app/lish/[did]/[publication]/generateFeed.ts b/app/lish/[did]/[publication]/generateFeed.ts index 383f9461..e5b6de11 100644 --- a/app/lish/[did]/[publication]/generateFeed.ts +++ b/app/lish/[did]/[publication]/generateFeed.ts @@ -54,47 +54,57 @@ export async function generateFeed( }); await Promise.all( - publication.documents_in_publications.map(async (doc) => { - if (!doc.documents) return; - const record = normalizeDocumentRecord( - doc.documents?.data, - doc.documents?.uri, - ); - const uri = new AtUri(doc.documents?.uri); - const rkey = uri.rkey; - if (!record) return; + publication.documents_in_publications + .sort((a, b) => { + const dateA = a.documents?.sort_date + ? new Date(a.documents.sort_date).getTime() + : 0; + const dateB = b.documents?.sort_date + ? new Date(b.documents.sort_date).getTime() + : 0; + return dateB - dateA; // Sort in descending order (newest first) + }) + .map(async (doc) => { + if (!doc.documents) return; + const record = normalizeDocumentRecord( + doc.documents?.data, + doc.documents?.uri, + ); + const uri = new AtUri(doc.documents?.uri); + const rkey = uri.rkey; + if (!record) return; - let blocks: PubLeafletPagesLinearDocument.Block[] = []; - if (hasLeafletContent(record) && record.content.pages[0]) { - const firstPage = record.content.pages[0]; - if (PubLeafletPagesLinearDocument.isMain(firstPage)) { - blocks = firstPage.blocks || []; + let blocks: PubLeafletPagesLinearDocument.Block[] = []; + if (hasLeafletContent(record) && record.content.pages[0]) { + const firstPage = record.content.pages[0]; + if (PubLeafletPagesLinearDocument.isMain(firstPage)) { + blocks = firstPage.blocks || []; + } } - } - const stream = await renderToReadableStream( - createElement(StaticPostContent, { blocks, did: uri.host }), - ); - const reader = stream.getReader(); - const chunks = []; + const stream = await renderToReadableStream( + createElement(StaticPostContent, { blocks, did: uri.host }), + ); + const reader = stream.getReader(); + const chunks = []; - let done, value; - while (!done) { - ({ done, value } = await reader.read()); - if (value) { - chunks.push(new TextDecoder().decode(value)); + let done, value; + while (!done) { + ({ done, value } = await reader.read()); + if (value) { + chunks.push(new TextDecoder().decode(value)); + } } - } - const docUrl = getDocumentURL(record, doc.documents.uri, pubRecord); - feed.addItem({ - title: record.title, - description: record.description, - date: record.publishedAt ? new Date(record.publishedAt) : new Date(), - id: docUrl, - link: docUrl, - content: chunks.join(""), - }); - }), + const docUrl = getDocumentURL(record, doc.documents.uri, pubRecord); + feed.addItem({ + title: record.title, + description: record.description, + date: record.publishedAt ? new Date(record.publishedAt) : new Date(), + id: docUrl, + link: docUrl, + content: chunks.join(""), + }); + }), ); return feed; -- 2.51.2 From 5aaf7df819a607c99f64d18a973f0beaac855a52 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Wed, 11 Feb 2026 21:34:10 +0400 Subject: [PATCH 22/28] fix feed ordering --- app/lish/[did]/[publication]/generateFeed.ts | 97 ++++++++++---------- 1 file changed, 47 insertions(+), 50 deletions(-) diff --git a/app/lish/[did]/[publication]/generateFeed.ts b/app/lish/[did]/[publication]/generateFeed.ts index e5b6de11..94227ea0 100644 --- a/app/lish/[did]/[publication]/generateFeed.ts +++ b/app/lish/[did]/[publication]/generateFeed.ts @@ -53,59 +53,56 @@ export async function generateFeed( }, }); - await Promise.all( - publication.documents_in_publications - .sort((a, b) => { - const dateA = a.documents?.sort_date - ? new Date(a.documents.sort_date).getTime() - : 0; - const dateB = b.documents?.sort_date - ? new Date(b.documents.sort_date).getTime() - : 0; - return dateB - dateA; // Sort in descending order (newest first) - }) - .map(async (doc) => { - if (!doc.documents) return; - const record = normalizeDocumentRecord( - doc.documents?.data, - doc.documents?.uri, - ); - const uri = new AtUri(doc.documents?.uri); - const rkey = uri.rkey; - if (!record) return; + let docs = publication.documents_in_publications.sort((a, b) => { + const dateA = a.documents?.sort_date + ? new Date(a.documents.sort_date).getTime() + : 0; + const dateB = b.documents?.sort_date + ? new Date(b.documents.sort_date).getTime() + : 0; + return dateB - dateA; // Sort in descending order (newest first) + }); + for (const doc of docs) { + if (!doc.documents) continue; + const record = normalizeDocumentRecord( + doc.documents?.data, + doc.documents?.uri, + ); + const uri = new AtUri(doc.documents?.uri); + const rkey = uri.rkey; + if (!record) continue; - let blocks: PubLeafletPagesLinearDocument.Block[] = []; - if (hasLeafletContent(record) && record.content.pages[0]) { - const firstPage = record.content.pages[0]; - if (PubLeafletPagesLinearDocument.isMain(firstPage)) { - blocks = firstPage.blocks || []; - } - } - const stream = await renderToReadableStream( - createElement(StaticPostContent, { blocks, did: uri.host }), - ); - const reader = stream.getReader(); - const chunks = []; + let blocks: PubLeafletPagesLinearDocument.Block[] = []; + if (hasLeafletContent(record) && record.content.pages[0]) { + const firstPage = record.content.pages[0]; + if (PubLeafletPagesLinearDocument.isMain(firstPage)) { + blocks = firstPage.blocks || []; + } + } + const stream = await renderToReadableStream( + createElement(StaticPostContent, { blocks, did: uri.host }), + ); + const reader = stream.getReader(); + const chunks = []; - let done, value; - while (!done) { - ({ done, value } = await reader.read()); - if (value) { - chunks.push(new TextDecoder().decode(value)); - } - } + let done, value; + while (!done) { + ({ done, value } = await reader.read()); + if (value) { + chunks.push(new TextDecoder().decode(value)); + } + } - const docUrl = getDocumentURL(record, doc.documents.uri, pubRecord); - feed.addItem({ - title: record.title, - description: record.description, - date: record.publishedAt ? new Date(record.publishedAt) : new Date(), - id: docUrl, - link: docUrl, - content: chunks.join(""), - }); - }), - ); + const docUrl = getDocumentURL(record, doc.documents.uri, pubRecord); + feed.addItem({ + title: record.title, + description: record.description, + date: record.publishedAt ? new Date(record.publishedAt) : new Date(), + id: docUrl, + link: docUrl, + content: chunks.join(""), + }); + } return feed; } -- 2.51.2 From 5a7a4d3269a06b0b9b8607d161f3c7b0c1d091c7 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Wed, 11 Feb 2026 21:46:37 +0400 Subject: [PATCH 23/28] check if doc path has leading / and add if needed --- app/lish/createPub/getPublicationURL.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/lish/createPub/getPublicationURL.ts b/app/lish/createPub/getPublicationURL.ts index 3f864599..cfb39d3f 100644 --- a/app/lish/createPub/getPublicationURL.ts +++ b/app/lish/createPub/getPublicationURL.ts @@ -58,18 +58,22 @@ export function getDocumentURL( docUri: string, publication?: PublicationInput | NormalizedPublication | null, ): string { - const path = doc.path || "/" + new AtUri(docUri).rkey; + let path = doc.path || "/" + new AtUri(docUri).rkey; + if (path[0] !== "/") path = "/" + path; const aturi = new AtUri(docUri); const isNormalized = !!publication && - (publication as NormalizedPublication).$type === "site.standard.publication"; + (publication as NormalizedPublication).$type === + "site.standard.publication"; const normPub = isNormalized ? (publication as NormalizedPublication) : publication ? normalizePublicationRecord((publication as PublicationInput).record) : null; - const pubInput = isNormalized ? null : (publication as PublicationInput | null); + const pubInput = isNormalized + ? null + : (publication as PublicationInput | null); // Non-leaflet documents always use the full publication site URL if (doc.content && !hasLeafletContent(doc) && normPub?.url) { -- 2.51.2 From c460c609d3e2c17146302eb3c21ecae0fe29d9ae Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Wed, 11 Feb 2026 21:56:40 +0400 Subject: [PATCH 24/28] log when setting indexed --- app/api/inngest/functions/sync_document_metadata.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/inngest/functions/sync_document_metadata.ts b/app/api/inngest/functions/sync_document_metadata.ts index 8dc0c775..1c091192 100644 --- a/app/api/inngest/functions/sync_document_metadata.ts +++ b/app/api/inngest/functions/sync_document_metadata.ts @@ -32,7 +32,7 @@ export const sync_document_metadata = inngest.createFunction( if (!handleResult) return { error: "No Handle" }; await step.run("set-indexed", async () => { - await supabaseServerClient + return await supabaseServerClient .from("documents") .update({ indexed: !handleResult.isBridgy }) .eq("uri", document_uri); -- 2.51.2 From 67ea66e7a3ba57fef2364710a5ae0042ad071ebe Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Wed, 11 Feb 2026 22:02:54 +0400 Subject: [PATCH 25/28] log data from update indexed step --- app/api/inngest/functions/sync_document_metadata.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/api/inngest/functions/sync_document_metadata.ts b/app/api/inngest/functions/sync_document_metadata.ts index 1c091192..ee4e6b86 100644 --- a/app/api/inngest/functions/sync_document_metadata.ts +++ b/app/api/inngest/functions/sync_document_metadata.ts @@ -35,7 +35,8 @@ export const sync_document_metadata = inngest.createFunction( return await supabaseServerClient .from("documents") .update({ indexed: !handleResult.isBridgy }) - .eq("uri", document_uri); + .eq("uri", document_uri) + .select(); }); if (!bsky_post_uri || handleResult.isBridgy) { -- 2.51.2 From 471ed160881f9cf827b374fb511f6fd3c71c21e2 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Wed, 11 Feb 2026 22:48:22 +0400 Subject: [PATCH 26/28] default to indexed false and manually set on leaflet publish --- actions/publishToPublication.ts | 1 + appview/index.ts | 2 -- supabase/migrations/20260211000000_indexed_default_false.sql | 1 + 3 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 supabase/migrations/20260211000000_indexed_default_false.sql diff --git a/actions/publishToPublication.ts b/actions/publishToPublication.ts index a1a93592..70aa3d9d 100644 --- a/actions/publishToPublication.ts +++ b/actions/publishToPublication.ts @@ -300,6 +300,7 @@ export async function publishToPublication({ await supabaseServerClient.from("documents").upsert({ uri: result.uri, data: record as unknown as Json, + indexed: true, }); if (publication_uri) { diff --git a/appview/index.ts b/appview/index.ts index 6c4c1548..31ead5fb 100644 --- a/appview/index.ts +++ b/appview/index.ts @@ -107,7 +107,6 @@ async function handleEvent(evt: Event) { let docResult = await supabase.from("documents").upsert({ uri: evt.uri.toString(), data: record.value as Json, - indexed: false, }); if (docResult.error) console.log(docResult.error); await inngest.send({ @@ -275,7 +274,6 @@ async function handleEvent(evt: Event) { let docResult = await supabase.from("documents").upsert({ uri: evt.uri.toString(), data: record.value as Json, - indexed: false, }); if (docResult.error) console.log(docResult.error); await inngest.send({ diff --git a/supabase/migrations/20260211000000_indexed_default_false.sql b/supabase/migrations/20260211000000_indexed_default_false.sql new file mode 100644 index 00000000..09031c3a --- /dev/null +++ b/supabase/migrations/20260211000000_indexed_default_false.sql @@ -0,0 +1 @@ +ALTER TABLE documents ALTER COLUMN indexed SET DEFAULT false; -- 2.51.2 From 5c1da06c43b099a4836bef01e460585552edc851 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Fri, 13 Feb 2026 09:59:56 +0400 Subject: [PATCH 27/28] remove commit claude local settings --- .claude/settings.local.json | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 .claude/settings.local.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index 9371392c..00000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "permissions": { - "allow": [ - "mcp__acp__Edit", - "mcp__acp__Write", - "mcp__acp__Bash", - "mcp__primitive__say_hello", - "mcp__primitive__pending_delegations", - "mcp__primitive__claim_delegation", - "mcp__primitive__tasks_update", - "mcp__primitive__contexts_update", - "mcp__primitive__contexts_list" - ] - } -} -- 2.51.2 From 19c7b0af3767686a8d25be6956d1037dfec698d2 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Fri, 13 Feb 2026 10:09:37 +0400 Subject: [PATCH 28/28] prevent description from being overridden when blank --- app/lish/createPub/updatePublication.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/lish/createPub/updatePublication.ts b/app/lish/createPub/updatePublication.ts index 350b3f88..77e1a191 100644 --- a/app/lish/createPub/updatePublication.ts +++ b/app/lish/createPub/updatePublication.ts @@ -275,7 +275,7 @@ export async function updatePublication({ return buildRecord(normalizedPub, existingBasePath, publicationType, { name, - description, + ...(description !== undefined && { description }), icon: iconBlob, preferences, }); -- 2.51.2