feat: Link Blog - social link sharing for power browsers #245
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Summary
This PR introduces Link Blogs - a social link-sharing feature designed for "power browsers" who curate and share interesting links. Think del.icio.us meets StumbleUpon, built on ATProto.
Core Value Proposition
Technical Architecture
Why Batched Links, Not Single Links?
Early in development, we considered a simpler model where each link post = one URL. However, this was revised based on how power users actually share links:
The batched model supports 1-50 links per post, with metadata at both the post level AND individual link level.
Data Model Design
ATProto Lexicon:
pub.leaflet.link.postRecord Structure:
Design Decisions:
Separate tags at post and link level: A post about "weekly reading" might have tag
weekly, while individual links have tags likerust,databases. This enables both collection-level and content-level discovery.Optional embed metadata: Reserved for future enhancement (link previews, thumbnails). Not implemented yet to keep scope manageable.
Via attribution: When importing from Bluesky, we preserve the source URIs. This enables:
Max 50 links per post: Prevents abuse while allowing substantial roundups. Most posts will have 1-10 links.
Database Schema
Tables:
Why Two Tables?
get_random_links()operates on link_items directlylink_countin posts table avoids COUNT(*) on every feed renderIndexes:
The GIN index on tags enables efficient
@>(contains) queries for tag filtering.RPC Functions:
get_followed_link_posts(viewer_did, limit, cursor)get_random_links(limit, tag_filter)ORDER BY RANDOM()for true randomnessget_link_posts_by_tag(tag_name, limit, cursor)EXISTSsubquery for efficiencysearch_link_tags(query)Appview Integration
The firehose consumer now indexes
pub.leaflet.link.postrecords:Deletion handling: When a post is deleted, CASCADE on the foreign key automatically removes link_items.
Server Actions
createLinkPost(input)deleteLinkPost(uri)getMyLinkPosts(limit, cursor)getAllLinkPosts(limit, cursor)getFollowedLinkPosts(limit, cursor)getRandomLinks(limit, tag?)getLinkPostsByTag(tag, limit, cursor)getLinkPostsByUser(did, limit, cursor)getUserProfile(did)getMyBskyPostsWithLinks(limit)aggregateBskyLinksToPost(uris, title?, desc?, tags?)Bluesky Import Logic:
UI Components
Feed Component (
LinksFeed.tsx)Feed Modes:
State Management:
randomLinksstate for discover mode (different data shape)useCallbackfor fetch to handle cursor correctlyTag Filter UX:
Post Card (
LinkPostCard.tsx)Displays a batched link post:
Submit Form (
LinkSubmitForm.tsx)Modal form for creating link posts:
Bluesky Import (
BskyImportForm.tsx)Modal for importing from Bluesky:
User Profile (
links/user/[did]/page.tsx)Profile page showing:
Navigation Integration
Added "Links" to the main navigation bar with a link icon.
Trade-offs & Alternatives Considered
1. Single links vs batched posts
2. Tags on posts vs tags on links vs both
3. Denormalized link_items table vs JSONB queries
@>queries work but don't scale4. Random discovery algorithm
ORDER BY RANDOM()5. Bluesky import as batch vs individual posts
Testing Notes
No test framework is configured in this project. Manual testing recommended:
Deployment Checklist
supabase db pushor apply migration manuallynpm run generate-db-types(will error until migration applied)npm run publish-lexicons(optional, for ecosystem interop)Future Enhancements
Files Changed
New Files (12)
lexicons/src/link.ts- ATProto lexicon definitionlexicons/pub/leaflet/link/post.json- Generated lexiconlexicons/api/types/pub/leaflet/link/post.ts- Generated typessupabase/migrations/20251212120000_add_link_posts.sql- Database schemaactions/linkActions.ts- Server actionsapp/(home-pages)/links/page.tsx- Main pageapp/(home-pages)/links/LinksFeed.tsx- Feed componentapp/(home-pages)/links/LinkPostCard.tsx- Post cardapp/(home-pages)/links/LinkSubmitForm.tsx- Submit formapp/(home-pages)/links/BskyImportForm.tsx- Bluesky importapp/(home-pages)/links/user/[did]/page.tsx- User profilecomponents/Icons/LinkSmall.tsx- Nav iconModified Files (4)
lexicons/build.ts- Include link lexiconslexicons/api/index.ts- Export link typeslexicons/api/lexicons.ts- Register lexiconappview/index.ts- Index link postscomponents/ActionBar/Navigation.tsx- Add links navDecision Graph
Development decisions were logged in real-time. View the graph:
https://notactuallytreyanastasio.github.io/leaflet/
18 nodes tracking: goal → observations → decisions → actions → outcomes