Skip to content

Feat/reads article improvements#273

Merged
spe1020 merged 5 commits intomainfrom
feat/reads-article-improvements
Mar 26, 2026
Merged

Feat/reads article improvements#273
spe1020 merged 5 commits intomainfrom
feat/reads-article-improvements

Conversation

@spe1020
Copy link
Copy Markdown
Contributor

@spe1020 spe1020 commented Mar 26, 2026

No description provided.

spe1020 and others added 3 commits March 25, 2026 18:34
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>
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages bot commented Mar 26, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
❌ Deployment failed
View logs
frontend 1d5f96e Mar 26 2026, 02:29 AM

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/articleStore to 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' }
];

Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
let sortOptions: { value: SortOption; label: string }[] = baseSortOptions;

Copilot uses AI. Check for mistakes.
Comment on lines +288 to +293
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);
}
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +303 to +308
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);
}
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +91 to +95
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);
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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
);

Copilot uses AI. Check for mistakes.
$: if (isSignedIn && selectedSort === 'newest') {
selectedSort = 'foryou';
}

Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
// When user signs out, reset "For You" back to default "newest"
$: if (!isSignedIn && selectedSort === 'foryou') {
selectedSort = 'newest';
}

Copilot uses AI. Check for mistakes.
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages bot commented Mar 26, 2026

Deploying frontend with  Cloudflare Pages  Cloudflare Pages

Latest commit: 1d5f96e
Status:⚡️  Build in progress...

View logs

@spe1020 spe1020 marked this pull request as ready for review March 26, 2026 02:18
- 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>
@spe1020 spe1020 merged commit 0934826 into main Mar 26, 2026
1 of 3 checks passed
@spe1020 spe1020 deleted the feat/reads-article-improvements branch March 26, 2026 02:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants