@@ -18,7 +18,8 @@ import {
1818
1919const cfRanker : Ranker = new CollaborativeFilterRanker ( )
2020const graphRanker : Ranker = new GraphRanker ( )
21- const popularityRanker : Ranker = new PopularityRanker ( )
21+ // Concrete type (not Ranker): its rank() takes an extra cold-start language arg.
22+ const popularityRanker = new PopularityRanker ( )
2223
2324// Compute-on-request with a per-(feed, viewer) Redis cache of the ranked list.
2425// The cached list is an IMMUTABLE snapshot: a seen-aware order is baked in once
@@ -32,6 +33,10 @@ export const handler = async (
3233 params : QueryParams ,
3334 viewerDid : string | null ,
3435 feed : FeedDef ,
36+ // Normalized primary language subtags from the viewer's Accept-Language,
37+ // in preference order; [] when the header is absent. Used only to bias the
38+ // cold-start feed (see computeRanked).
39+ viewerLangs : string [ ] ,
3540) => {
3641 const cacheKey = rankedListKey ( feed . rkey , viewerDid )
3742 const offset = parseCursor ( params . cursor )
@@ -41,7 +46,7 @@ export const handler = async (
4146 // On a cache miss, import the viewer's like history so the first request is
4247 // already personalized (runs at most once per backfill TTL).
4348 if ( viewerDid ) await ensureViewerBackfilled ( ctx , viewerDid )
44- const scored = await computeRanked ( ctx , viewerDid , feed . content )
49+ const scored = await computeRanked ( ctx , viewerDid , feed . content , viewerLangs )
4550 // Bake the seen-aware order in now so every page is a plain offset slice.
4651 ranked = await orderBySeen ( ctx , viewerDid , scored )
4752 await cacheRankedList (
@@ -91,13 +96,20 @@ const computeRanked = async (
9196 ctx : AppContext ,
9297 viewerDid : string | null ,
9398 content : ContentFilter ,
99+ viewerLangs : string [ ] ,
94100) : Promise < string [ ] > => {
95101 // Personalized first; fall back to popularity for anonymous / no-history
96102 // viewers and while the graph is still building.
97103 const engine = ctx . cfg . rankerEngine === 'graph' ? graphRanker : cfRanker
98104 const personalized = await engine . rank ( ctx , viewerDid , content )
99105 if ( personalized . length > 0 ) return personalized
100- return popularityRanker . rank ( ctx , viewerDid , content )
106+ // Bias the cold-start feed by the viewer's Accept-Language — but only for
107+ // authenticated viewers, whose ranked list is cached per-DID. Anonymous
108+ // viewers share one cache entry (viewerDid = null), so applying a per-request
109+ // header there would let one viewer's language poison every other viewer's
110+ // shared list; they stay global.
111+ const langs = viewerDid ? viewerLangs : [ ]
112+ return popularityRanker . rank ( ctx , viewerDid , content , langs )
101113}
102114
103115const parseCursor = ( cursor ?: string ) : number => {
0 commit comments