Conversation
Article discovery: - Extend lookback from 90 days to 365 days for deeper article pool - Dual fetch strategy: food-hashtag fetch (primary, for cover + Food/Farming) plus general fetch (secondary, for All/Bitcoin/Nostr/etc categories) - Remove Food Only toggle — cover is always food-editorial, feed shows all topics via category pills Filtering improvements (learnings from Highlighter): - Use NDKArticle typed wrapper for cleaner title/image/summary extraction - Add pubkey blacklist in quality filters for known spammers - Enforce image requirement for cover articles (hard filter) - Require real images for feed articles (no placeholder gradients in reads) - Remove 5-tag limit on getArticleTags — all tags available for category matching - Filter broken image URLs (Ooops patterns, placeholder domains) - Add preview/full article deduplication via 'full' tag - Add "test"/"testing" as forbidden words in article titles - Lower "All" tab min read time from 3min to 1min Ranking: - New "For You" sort option boosting followed authors with weekly decay - Defaults to "For You" when signed in, "Newest" when signed out - getFollowedPubkeys() helper in followListCache for the ranking function Shared article store: - New articleStore.ts with writable stores for articles and cover - Reads page syncs articles to shared store reactively - LongformFoodFeed on explore page consumes shared store, falls back to own food-hashtag fetch if store is empty - Both pages feed the same pool — visit either and both benefit Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Run general (no-hashtag) article fetch even when cache is fresh so non-food categories populate - Hydrate reads page from shared articleStore on load for cross-page caching between /reads and /explore - Optimize shared store sync to only add newly appended articles - Make selectedSort reactive to login state transitions - Separate follow pubkey set from profile cache to prevent non-followed pubkeys from skewing "For You" ranking - Scan all image regex matches in extractImage (not just first) - Fix misleading IndexedDB comment in articleStore - Remove unused imports in LongformFoodFeed Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Broader article and feed discovery by including antiprimal.net in the fallback, discovery, and article outbox relay pools. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ❌ Deployment failed View logs |
frontend | 1d5f96e | Mar 26 2026, 02:29 AM |
There was a problem hiding this comment.
Pull request overview
This PR improves the “Reads” longform article experience by expanding article discovery (food + general topics), adding a shared in-memory article cache for cross-page reuse, and introducing a “For You” sort driven by the user’s follow list.
Changes:
- Add a shared
$lib/articleStoreto hydrate/reuse articles (and cover curation) across Reads/Explore components. - Expand fetching to include a parallel all-topics article fetch (no hashtag filter) while keeping the cover curated from food/editorial articles.
- Enhance ranking/filtering and presentation: “For You” sort option, better image validation/placeholder rejection, preview/full dedupe, spam filtering tweaks, and broader relay/time-window discovery.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| src/routes/reads/+page.svelte | Loads follow list for “For You”, hydrates/syncs with shared store, adds parallel general-articles fetch, removes food-only toggle UI. |
| src/routes/community/+page.svelte | Minor formatting-only change. |
| src/lib/followListCache.ts | Tracks an authoritative follow pubkey set from kind:3 and exports getFollowedPubkeys(). |
| src/lib/articleUtils.ts | Adds image URL validation, adjusts tag handling/quality filters, adds preview/full dedupe, adds foryou sort, and uses NDKArticle for typed access. |
| src/lib/articleStore.ts | New shared Svelte stores for articles + curated cover, with helpers to add/clear/refresh. |
| src/lib/articleOutbox.ts | Adds wss://antiprimal.net and expands default discovery window to 365 days. |
| src/components/table/FilterBar.svelte | Adds conditional “For You” sort option based on a new showForYou prop. |
| src/components/table/FeedSection.svelte | Removes foodOnly prop, adds “For You” sort integration, preview dedupe, and filters out image-less articles. |
| src/components/LongformFoodFeed.svelte | Refactors to use shared store + articleUtils pipeline; pushes fetched articles into shared cache. |
| src/components/FoodstrFeedOptimized.svelte | Adds antiprimal.net to relay pools and minor formatting tweaks. |
| src/components/ArticleCard.svelte | Treats tiny loaded images as broken (likely placeholders/error pages). |
| package.json | Version bump. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| { value: 'longest', label: 'Longest' }, | ||
| { value: 'shortest', label: 'Shortest' } | ||
| ]; | ||
|
|
There was a problem hiding this comment.
sortOptions is assigned in a reactive statement but never declared (let sortOptions = ...). In Svelte + TypeScript this will fail compilation (and sortOptions.find(...) will also be unsafe). Declare sortOptions with the appropriate type and initialize it (e.g., to baseSortOptions) before the $: assignment.
| let sortOptions: { value: SortOption; label: string }[] = baseSortOptions; |
src/routes/reads/+page.svelte
Outdated
| const articleData = eventToArticleData(event, true); | ||
| if (articleData) { | ||
| articles = [...articles, articleData] | ||
| .filter((a, i, arr) => arr.findIndex((x) => x.id === a.id) === i) | ||
| .sort((a, b) => b.publishedAt - a.publishedAt); | ||
| } |
There was a problem hiding this comment.
In fetchGeneralArticles, the articles = [...articles, articleData].filter(findIndex...).sort(...) path is doing an O(n^2) dedupe pass and a full re-sort on every event, but duplicates are already prevented via seenEventIds. Consider removing the redundant filter(...) (and ideally batching/appending then sorting once) to avoid unnecessary allocations and quadratic work during streaming fetches.
src/routes/reads/+page.svelte
Outdated
| const articleData = eventToArticleData(event, true); | ||
| if (articleData) { | ||
| articles = [...articles, articleData] | ||
| .filter((a, i, arr) => arr.findIndex((x) => x.id === a.id) === i) | ||
| .sort((a, b) => b.publishedAt - a.publishedAt); | ||
| } |
There was a problem hiding this comment.
Same issue in the batch-processing loop: it repeats the per-item [...articles, articleData].filter(findIndex...).sort(...) pattern even though seenEventIds already enforces uniqueness. This is quadratic and does a full sort on every addition; prefer pushing into a local array (or using a Map keyed by id) and sorting once after the loop.
| const articleData = eventToArticleData(event, true); | ||
| if (articleData && isValidLongformArticle(event)) { | ||
| localArticles = [...localArticles, articleData] | ||
| .filter((a, i, arr) => arr.findIndex((x) => x.id === a.id) === i) | ||
| .sort((a, b) => b.publishedAt - a.publishedAt); |
There was a problem hiding this comment.
eventToArticleData(event, true) is executed before checking isValidLongformArticle(event), so you pay the full parsing/preview/tag work for non-food events and then throw it away. Also, the subsequent .filter(findIndex...) dedupe is redundant because seenEventIds already prevents duplicates. Check isValidLongformArticle first and avoid the extra dedupe/sort work per event.
| const articleData = eventToArticleData(event, true); | |
| if (articleData && isValidLongformArticle(event)) { | |
| localArticles = [...localArticles, articleData] | |
| .filter((a, i, arr) => arr.findIndex((x) => x.id === a.id) === i) | |
| .sort((a, b) => b.publishedAt - a.publishedAt); | |
| if (!isValidLongformArticle(event)) return; | |
| const articleData = eventToArticleData(event, true); | |
| if (articleData) { | |
| localArticles = [...localArticles, articleData].sort( | |
| (a, b) => b.publishedAt - a.publishedAt | |
| ); |
| $: if (isSignedIn && selectedSort === 'newest') { | ||
| selectedSort = 'foryou'; | ||
| } | ||
|
|
There was a problem hiding this comment.
selectedSort can remain 'foryou' when isSignedIn becomes false (there’s only a sign-in transition). In that state FilterBar hides the “For You” option (showForYou={false}) so sortOptions.find(... ) returns undefined and the UI shows an empty label. Add a reactive branch to reset selectedSort to 'newest' on sign-out (or always include the current selection in the options).
| // When user signs out, reset "For You" back to default "newest" | |
| $: if (!isSignedIn && selectedSort === 'foryou') { | |
| selectedSort = 'newest'; | |
| } |
- Declare sortOptions with explicit type before reactive assignment to fix Svelte+TS compilation - Reset selectedSort to 'newest' on sign-out so FilterBar doesn't show empty label when 'foryou' is hidden - Replace O(n^2) per-event dedupe+sort in fetchGeneralArticles with batch collect then single merge+sort (seenEventIds already guards uniqueness) - Check isValidLongformArticle before eventToArticleData in LongformFoodFeed to skip parsing non-food events; drop redundant findIndex dedupe since seenEventIds handles it Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
No description provided.