@@ -105,10 +105,20 @@ class FakeAgentRepository {
105105class FakePublicRepository implements PublicRepository {
106106 readonly records : PublicRecordView [ ] ;
107107 readonly announcements : PublicAnnouncementView [ ] ;
108-
109- constructor ( records : PublicRecordView [ ] , announcements : PublicAnnouncementView [ ] = [ ] ) {
108+ readonly topics : PublicDictionaryItem [ ] ;
109+ /** Sitemap sayfalamasını sınamak için: ilk sayfa `hasMore` der. */
110+ readonly feedPageSize : number | null ;
111+ feedCalls = 0 ;
112+
113+ constructor (
114+ records : PublicRecordView [ ] ,
115+ announcements : PublicAnnouncementView [ ] = [ ] ,
116+ options : { topics ?: PublicDictionaryItem [ ] ; feedPageSize ?: number } = { } ,
117+ ) {
110118 this . records = records ;
111119 this . announcements = announcements ;
120+ this . topics = options . topics ?? [ ] ;
121+ this . feedPageSize = options . feedPageSize ?? null ;
112122 }
113123
114124 /* Sahte depo hedef kitleye göre filtrelemez, çünkü filtrenin yeri burası
@@ -120,10 +130,17 @@ class FakePublicRepository implements PublicRepository {
120130 }
121131
122132 async listFeed ( input : Parameters < PublicRepository [ 'listFeed' ] > [ 0 ] ) : Promise < PublicPage > {
123- return {
124- items : this . records . filter ( ( item ) => item . kind === 'post' && ( ! input . agentHandle || item . author . handle === input . agentHandle ) ) ,
125- hasMore : false ,
126- } ;
133+ this . feedCalls += 1 ;
134+ const matching = this . records
135+ . filter ( ( item ) => item . kind === 'post' && ( ! input . agentHandle || item . author . handle === input . agentHandle ) ) ;
136+ if ( this . feedPageSize === null ) return { items : matching , hasMore : false } ;
137+ /* Gerçek sorgu gibi imleçten devam ediyor: imleci taşımayan bir çağıran
138+ * ilk sayfayı sonsuza kadar okur. */
139+ const start = input . cursor
140+ ? matching . findIndex ( ( item ) => item . id === input . cursor ! . id ) + 1
141+ : 0 ;
142+ const items = matching . slice ( start , start + this . feedPageSize ) ;
143+ return { items, hasMore : start + items . length < matching . length } ;
127144 }
128145
129146 async searchRecords ( ) : Promise < PublicPage > {
@@ -162,7 +179,7 @@ class FakePublicRepository implements PublicRepository {
162179 }
163180
164181 async listTopics ( ) : Promise < PublicDictionaryItem [ ] > {
165- return [ ] ;
182+ return this . topics ;
166183 }
167184
168185 async listTopicsPage ( ) : Promise < { items : PublicDictionaryItem [ ] ; hasMore : boolean } > {
@@ -242,6 +259,79 @@ describe('Orbit dynamic public pages', () => {
242259 assert . doesNotMatch ( html , / _ _ O R B I T _ / u) ;
243260 } ) ;
244261
262+ /*
263+ * Sitemap uzun süre derleme anında donuyordu: canlıdaki yedi ajanın dördü,
264+ * D1'e yazılan gönderilerin hiçbiri listede yoktu ve Google yeni içeriği
265+ * yalnız bir bağlantıya rastlarsa görüyordu. Buradaki testler listenin
266+ * canlıdan üretildiğini ve neyi DIŞARIDA bıraktığını koruyor.
267+ */
268+ test ( 'builds the sitemap from D1 agents, records and topics' , async ( ) => {
269+ const first = record ( { id : 'record-1' , slug : 'ilk-kayit' , publishedAt : Date . UTC ( 2026 , 6 , 19 , 10 , 0 ) , updatedAt : Date . UTC ( 2026 , 6 , 20 , 8 , 0 ) } ) ;
270+ const second = record ( { id : 'record-2' , slug : 'ikinci-kayit' , publishedAt : Date . UTC ( 2026 , 7 , 1 , 10 , 0 ) , updatedAt : Date . UTC ( 2026 , 7 , 2 , 9 , 0 ) } ) ;
271+ const repository = new FakePublicRepository ( [ first , second ] , [ ] , {
272+ topics : [ { id : 'topic-orbit' , slug : 'orbit' , name : 'Orbit' , description : '' , accent : '#6f63e8' } ] ,
273+ /* Sayfa başına tek kayıt: imleci taşımayan bir toplayıcı ikinci kaydı
274+ * hiç görmez. */
275+ feedPageSize : 1 ,
276+ } ) ;
277+ const response = await serveDynamicPublicPage (
278+ new Request ( 'https://orbit.example/sitemap.xml' ) ,
279+ assets ,
280+ repository ,
281+ new FakeAgentRepository ( [ agent ( ) , agent ( { id : 'agent-nyx' , handle : 'nyx' } ) ] ) ,
282+ ) ;
283+ assert . ok ( response ) ;
284+ assert . equal ( response . status , 200 ) ;
285+ assert . equal ( response . headers . get ( 'content-type' ) , 'application/xml; charset=utf-8' ) ;
286+ const xml = await response . text ( ) ;
287+ assert . match ( xml , / ^ < \? x m l v e r s i o n = " 1 \. 0 " e n c o d i n g = " U T F - 8 " \? > < u r l s e t x m l n s = " h t t p : \/ \/ w w w \. s i t e m a p s \. o r g \/ s c h e m a s \/ s i t e m a p \/ 0 \. 9 " > / u) ;
288+ assert . match ( xml , / < l o c > h t t p s : \/ \/ o r b i t \. e x a m p l e \/ < \/ l o c > / u) ;
289+ assert . match ( xml , / < l o c > h t t p s : \/ \/ o r b i t \. e x a m p l e \/ a g e n t s \/ g u e s t - m i n d \/ < \/ l o c > / u) ;
290+ assert . match ( xml , / < l o c > h t t p s : \/ \/ o r b i t \. e x a m p l e \/ a g e n t s \/ n y x \/ < \/ l o c > / u) ;
291+ assert . match ( xml , / < l o c > h t t p s : \/ \/ o r b i t \. e x a m p l e \/ t o p i c s \/ o r b i t \/ < \/ l o c > / u) ;
292+ assert . match ( xml , / < l o c > h t t p s : \/ \/ o r b i t \. e x a m p l e \/ p o s t s \/ i l k - k a y i t \/ < \/ l o c > / u) ;
293+ /* Sayfalama: ikinci kayıt yalnız imleç taşındıysa listede olur. */
294+ assert . match ( xml , / < l o c > h t t p s : \/ \/ o r b i t \. e x a m p l e \/ p o s t s \/ i k i n c i - k a y i t \/ < \/ l o c > / u) ;
295+ /* Tazelik sinyali: kaydın kendi güncellenme anı, derleme anı değil. */
296+ assert . match ( xml , / < l o c > h t t p s : \/ \/ o r b i t \. e x a m p l e \/ p o s t s \/ i k i n c i - k a y i t \/ < \/ l o c > < l a s t m o d > 2 0 2 6 - 0 8 - 0 2 T 0 9 : 0 0 : 0 0 \. 0 0 0 Z < \/ l a s t m o d > / u) ;
297+ /* Ana sayfa bir listedir; yaşı içindeki en yeni kaydın yaşıdır. */
298+ assert . match ( xml , / < l o c > h t t p s : \/ \/ o r b i t \. e x a m p l e \/ < \/ l o c > < l a s t m o d > 2 0 2 6 - 0 8 - 0 2 T 0 9 : 0 0 : 0 0 \. 0 0 0 Z < \/ l a s t m o d > / u) ;
299+ } ) ;
300+
301+ test ( 'keeps noindex shells and derived pages out of the sitemap' , async ( ) => {
302+ const response = await serveDynamicPublicPage (
303+ new Request ( 'https://orbit.example/sitemap.xml' ) ,
304+ assets ,
305+ new FakePublicRepository ( [ record ( ) ] ) ,
306+ new FakeAgentRepository ( [ agent ( ) ] ) ,
307+ ) ;
308+ assert . ok ( response ) ;
309+ const xml = await response . text ( ) ;
310+ /* Sitemap "bunu indeksle" demek; sayfanın kendisi `noindex` diyorsa liste
311+ * kendi kendisiyle çelişir ve tarama bütçesi boşa gider. */
312+ for ( const path of [ '/dashboard/' , '/dashboard/platform/' , '/messages/' , '/following/' , '/saved/' , '/search/' ] ) {
313+ assert . doesNotMatch ( xml , new RegExp ( `<loc>https://orbit\\.example${ path } </loc>` , 'u' ) , `${ path } sitemap'te olmamalı` ) ;
314+ }
315+ /* Türev yüzeyler: ajan akışı profilin kopyası, takip listeleri profilden
316+ * türüyor, çalışma kabuğu hiç kamusal değil. */
317+ assert . doesNotMatch ( xml , / < l o c > [ ^ < ] * \/ f e e d \/ / u) ;
318+ assert . doesNotMatch ( xml , / < l o c > [ ^ < ] * \/ t a k i p c i l e r i / u) ;
319+ assert . doesNotMatch ( xml , / < l o c > [ ^ < ] * \/ o r b i t - r u n t i m e \/ / u) ;
320+ } ) ;
321+
322+ test ( 'moves the old build-time sitemap addresses to the live one' , async ( ) => {
323+ for ( const path of [ '/sitemap-index.xml' , '/sitemap-0.xml' ] ) {
324+ const response = await serveDynamicPublicPage (
325+ new Request ( `https://orbit.example${ path } ` ) ,
326+ assets ,
327+ new FakePublicRepository ( [ record ( ) ] ) ,
328+ ) ;
329+ assert . ok ( response ) ;
330+ assert . equal ( response . status , 301 ) ;
331+ assert . equal ( response . headers . get ( 'location' ) , 'https://orbit.example/sitemap.xml' ) ;
332+ }
333+ } ) ;
334+
245335 test ( 'replaces the build-time homepage records with D1 records' , async ( ) => {
246336 const response = await serveDynamicPublicPage (
247337 new Request ( 'https://orbit.example/' ) ,
0 commit comments