1- import { Feed , type Author as FeedAuthor , type Item } from "feed" ;
1+ import { Feed , type Item } from "feed" ;
22import { sanityFetch } from "@/sanity/lib/live" ;
33import type { LivePerspective } from "next-sanity/live" ;
44import type { RssQueryResult } from "@/sanity/types" ;
@@ -12,24 +12,11 @@ const site = productionDomain
1212 : "https://codingcat.dev" ;
1313
1414/**
15- * `sanityFetch` calls `cacheTag()` internally, which is only allowed inside a
16- * `'use cache'` function — so the data fetch must live in its own cached helper.
17- * Route handlers hardcode `stega: false`; only `perspective` is resolved .
15+ * Feeds only ever surface the most recent entries. Pulling the entire archive
16+ * ( the previous `10000`) meant every cache miss fetched and serialized every
17+ * post/podcast — expensive on both the Sanity side and in `toHTML` .
1818 */
19- async function fetchRssData (
20- query : typeof rssQuery | typeof rssPodcastQuery ,
21- queryParams : { type : string ; skip : string ; limit : number ; offset : number } ,
22- perspective : LivePerspective ,
23- ) : Promise < RssQueryResult > {
24- "use cache" ;
25- const { data } = await sanityFetch ( {
26- query,
27- params : queryParams ,
28- perspective,
29- stega : false ,
30- } ) ;
31- return data as RssQueryResult ;
32- }
19+ const DEFAULT_FEED_LIMIT = 50 ;
3320
3421/** Map Sanity _type to the URL path segment used on the site */
3522function typePath ( type : string ) : string {
@@ -43,33 +30,40 @@ function typePath(type: string): string {
4330 }
4431}
4532
46- export async function buildFeed ( params : {
47- type : string ;
48- skip ?: string ;
49- limit ?: number ;
50- offset ?: number ;
51- perspective : LivePerspective ;
52- } ) {
53- const isPodcast = params . type === "podcast" ;
54- const query = isPodcast ? rssPodcastQuery : rssQuery ;
55-
56- const data = await fetchRssData (
33+ /**
34+ * Fetches feed data. Called only from within a `'use cache'` boundary so the
35+ * sync tags `sanityFetch` attaches propagate to the surrounding cache entry —
36+ * the serialized feed below is then cached and revalidated by Sanity Live when
37+ * the underlying content changes, instead of being re-serialized per request.
38+ */
39+ async function fetchFeedData (
40+ type : string ,
41+ perspective : LivePerspective ,
42+ ) : Promise < RssQueryResult > {
43+ const query = type === "podcast" ? rssPodcastQuery : rssQuery ;
44+ const { data } = await sanityFetch ( {
5745 query,
58- {
59- type : params . type ,
60- skip : params . skip || "none" ,
61- limit : params . limit || 10000 ,
62- offset : params . offset || 0 ,
46+ params : {
47+ type,
48+ skip : "none" ,
49+ limit : DEFAULT_FEED_LIMIT ,
50+ offset : 0 ,
6351 } ,
64- params . perspective ,
65- ) ;
52+ perspective,
53+ stega : false ,
54+ } ) ;
55+ return data as RssQueryResult ;
56+ }
6657
67- const feedPath = typePath ( params . type ) ;
58+ /** Build a `feed` library `Feed` from already-fetched data (no I/O). */
59+ function createFeed ( data : RssQueryResult , type : string ) : Feed {
60+ const feedPath = typePath ( type ) ;
6861 const currentYear = new Date ( ) . getFullYear ( ) ;
62+ const isPodcast = type === "podcast" ;
6963
7064 const feed = new Feed ( {
71- title : `CodingCat.dev - ${ params . type } feed` ,
72- description : `CodingCat.dev - ${ params . type } feed` ,
65+ title : `CodingCat.dev - ${ type } feed` ,
66+ description : `CodingCat.dev - ${ type } feed` ,
7367 id : `${ site } ` ,
7468 link : `${ site } /${ feedPath } ` ,
7569 language : "en" ,
@@ -142,40 +136,56 @@ export async function buildFeed(params: {
142136}
143137
144138/**
145- * Build a podcast-specific RSS feed with iTunes namespace tags.
146- * Returns raw XML string with proper iTunes/podcast namespace support.
139+ * Cached RSS 2.0 string for the `feed`-library feeds (blog).
140+ * The `sanityFetch` runs inside this `'use cache'` scope so the rendered XML is
141+ * cached and tagged for on-demand revalidation via Sanity Live.
147142 */
148- export async function buildPodcastFeed ( params : {
149- skip ?: string ;
150- limit ?: number ;
151- offset ?: number ;
143+ export async function getFeedRss2 ( params : {
144+ type : string ;
152145 perspective : LivePerspective ;
153146} ) : Promise < string > {
154- const data = await fetchRssData (
155- rssPodcastQuery ,
156- {
157- type : "podcast" ,
158- skip : params . skip || "none" ,
159- limit : params . limit || 10000 ,
160- offset : params . offset || 0 ,
161- } ,
162- params . perspective ,
163- ) ;
147+ "use cache" ;
148+ const data = await fetchFeedData ( params . type , params . perspective ) ;
149+ return createFeed ( data , params . type ) . rss2 ( ) ;
150+ }
151+
152+ /** Cached JSON Feed string for the `feed`-library feeds (blog + podcast). */
153+ export async function getFeedJson ( params : {
154+ type : string ;
155+ perspective : LivePerspective ;
156+ } ) : Promise < string > {
157+ "use cache" ;
158+ const data = await fetchFeedData ( params . type , params . perspective ) ;
159+ return createFeed ( data , params . type ) . json1 ( ) ;
160+ }
161+
162+ /**
163+ * Cached podcast RSS string with the full iTunes namespace. Manually serialized
164+ * XML, produced inside `'use cache'` so it isn't rebuilt on every request.
165+ */
166+ export async function getPodcastFeedXml ( params : {
167+ perspective : LivePerspective ;
168+ } ) : Promise < string > {
169+ "use cache" ;
170+ const data = await fetchFeedData ( "podcast" , params . perspective ) ;
171+ return buildPodcastXml ( data ) ;
172+ }
164173
174+ /** Serialize podcast data into RSS 2.0 XML with iTunes namespace tags. */
175+ function buildPodcastXml ( data : RssQueryResult ) : string {
165176 const currentYear = new Date ( ) . getFullYear ( ) ;
166177 const feedUrl = `${ site } /podcasts/rss.xml` ;
167178 const feedImage = `${ site } /icon.svg` ;
168179
169- // Build RSS 2.0 XML with iTunes namespace manually for full podcast support
170180 const items = data
171181 . map ( ( item ) => {
172182 const imageUrl =
173- urlForImage ( item . coverImage ) ?. width ( 1400 ) . height ( 1400 ) . url ( ) || feedImage ;
183+ urlForImage ( item . coverImage ) ?. width ( 1400 ) . height ( 1400 ) . url ( ) ||
184+ feedImage ;
174185 const pubDate = item . date
175186 ? new Date ( item . date ) . toUTCString ( )
176187 : new Date ( ) . toUTCString ( ) ;
177188 const link = `${ site } /${ item . _type } /${ item . slug } ` ;
178- const description = escapeXml ( item . excerpt || "" ) ;
179189 const title = escapeXml ( item . title || "" ) ;
180190
181191 let enclosureXml = "" ;
0 commit comments