From 6ae3bd48400a9886769310c6286c5b8f2f7ce311 Mon Sep 17 00:00:00 2001 From: Sarah Gerrard Date: Tue, 6 Jan 2026 23:40:54 +0000 Subject: [PATCH 1/6] wip --- src/routes/rss.xml.ts | 71 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 src/routes/rss.xml.ts diff --git a/src/routes/rss.xml.ts b/src/routes/rss.xml.ts new file mode 100644 index 000000000..05219ff58 --- /dev/null +++ b/src/routes/rss.xml.ts @@ -0,0 +1,71 @@ +import { createAPIFileRoute } from '@tanstack/react-start/api' +import { getPublishedPosts, formatAuthors } from '~/utils/blog' + +function escapeXml(unsafe: string): string { + return unsafe + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') +} + +function generateRSSFeed() { + const posts = getPublishedPosts().slice(0, 50) // Most recent 50 posts + const siteUrl = 'https://tanstack.com' + const buildDate = new Date().toUTCString() + + const rssItems = posts + .map((post) => { + const postUrl = `${siteUrl}/blog/${post.slug}` + const pubDate = new Date(post.published).toUTCString() + const author = formatAuthors(post.authors) + + // Use excerpt if available, otherwise try to get first paragraph from content + let description = post.excerpt || '' + if (!description && post.content) { + // Extract first paragraph after frontmatter + const contentWithoutFrontmatter = post.content.replace(/^---[\s\S]*?---/, '').trim() + const firstParagraph = contentWithoutFrontmatter.split('\n\n')[0] + description = firstParagraph.replace(/!\[[^\]]*\]\([^)]*\)/g, '') // Remove images + } + + return ` + + ${escapeXml(post.title)} + ${escapeXml(postUrl)} + ${escapeXml(postUrl)} + ${pubDate} + ${escapeXml(author)} + ${escapeXml(description)} + ${post.headerImage ? `` : ''} + ` + }) + .join('') + + return ` + + + TanStack Blog + ${siteUrl}/blog + The latest news and updates from TanStack + en-us + ${buildDate} + + ${rssItems} + +` +} + +export const APIRoute = createAPIFileRoute('/rss.xml')({ + GET: async () => { + const rss = generateRSSFeed() + + return new Response(rss, { + headers: { + 'Content-Type': 'application/xml; charset=utf-8', + 'Cache-Control': 'public, max-age=300, s-maxage=3600', + }, + }) + }, +}) From 00f0380f855b18132b52c215e46ac26684e1b052 Mon Sep 17 00:00:00 2001 From: ladybluenotes Date: Tue, 6 Jan 2026 16:14:22 -0800 Subject: [PATCH 2/6] wip --- src/routeTree.gen.ts | 21 +++++++++++++++ src/routes/{rss.xml.ts => rss[.]xml.ts} | 35 +++++++++++++++++-------- 2 files changed, 45 insertions(+), 11 deletions(-) rename src/routes/{rss.xml.ts => rss[.]xml.ts} (71%) diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 36339e493..d68989061 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -10,6 +10,7 @@ import { Route as rootRouteImport } from './routes/__root' import { Route as SponsorsEmbedRouteImport } from './routes/sponsors-embed' +import { Route as RssDotxmlRouteImport } from './routes/rss[.]xml' import { Route as PartnersEmbedRouteImport } from './routes/partners-embed' import { Route as MerchRouteImport } from './routes/merch' import { Route as LlmsDottxtRouteImport } from './routes/llms[.]txt' @@ -106,6 +107,11 @@ const SponsorsEmbedRoute = SponsorsEmbedRouteImport.update({ path: '/sponsors-embed', getParentRoute: () => rootRouteImport, } as any) +const RssDotxmlRoute = RssDotxmlRouteImport.update({ + id: '/rss.xml', + path: '/rss.xml', + getParentRoute: () => rootRouteImport, +} as any) const PartnersEmbedRoute = PartnersEmbedRouteImport.update({ id: '/partners-embed', path: '/partners-embed', @@ -587,6 +593,7 @@ export interface FileRoutesByFullPath { '/llms.txt': typeof LlmsDottxtRoute '/merch': typeof MerchRoute '/partners-embed': typeof PartnersEmbedRoute + '/rss.xml': typeof RssDotxmlRoute '/sponsors-embed': typeof SponsorsEmbedRoute '/$libraryId/$version': typeof LibraryIdVersionRouteWithChildren '/account': typeof LibrariesAccountRouteWithChildren @@ -677,6 +684,7 @@ export interface FileRoutesByTo { '/llms.txt': typeof LlmsDottxtRoute '/merch': typeof MerchRoute '/partners-embed': typeof PartnersEmbedRoute + '/rss.xml': typeof RssDotxmlRoute '/sponsors-embed': typeof SponsorsEmbedRoute '/$libraryId/$version': typeof LibraryIdVersionRouteWithChildren '/ads': typeof LibrariesAdsRoute @@ -768,6 +776,7 @@ export interface FileRoutesById { '/llms.txt': typeof LlmsDottxtRoute '/merch': typeof MerchRoute '/partners-embed': typeof PartnersEmbedRoute + '/rss.xml': typeof RssDotxmlRoute '/sponsors-embed': typeof SponsorsEmbedRoute '/$libraryId/$version': typeof LibraryIdVersionRouteWithChildren '/_libraries/account': typeof LibrariesAccountRouteWithChildren @@ -862,6 +871,7 @@ export interface FileRouteTypes { | '/llms.txt' | '/merch' | '/partners-embed' + | '/rss.xml' | '/sponsors-embed' | '/$libraryId/$version' | '/account' @@ -952,6 +962,7 @@ export interface FileRouteTypes { | '/llms.txt' | '/merch' | '/partners-embed' + | '/rss.xml' | '/sponsors-embed' | '/$libraryId/$version' | '/ads' @@ -1042,6 +1053,7 @@ export interface FileRouteTypes { | '/llms.txt' | '/merch' | '/partners-embed' + | '/rss.xml' | '/sponsors-embed' | '/$libraryId/$version' | '/_libraries/account' @@ -1136,6 +1148,7 @@ export interface RootRouteChildren { LlmsDottxtRoute: typeof LlmsDottxtRoute MerchRoute: typeof MerchRoute PartnersEmbedRoute: typeof PartnersEmbedRoute + RssDotxmlRoute: typeof RssDotxmlRoute SponsorsEmbedRoute: typeof SponsorsEmbedRoute ApiUploadthingRoute: typeof ApiUploadthingRoute AuthSignoutRoute: typeof AuthSignoutRoute @@ -1161,6 +1174,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof SponsorsEmbedRouteImport parentRoute: typeof rootRouteImport } + '/rss.xml': { + id: '/rss.xml' + path: '/rss.xml' + fullPath: '/rss.xml' + preLoaderRoute: typeof RssDotxmlRouteImport + parentRoute: typeof rootRouteImport + } '/partners-embed': { id: '/partners-embed' path: '/partners-embed' @@ -2025,6 +2045,7 @@ const rootRouteChildren: RootRouteChildren = { LlmsDottxtRoute: LlmsDottxtRoute, MerchRoute: MerchRoute, PartnersEmbedRoute: PartnersEmbedRoute, + RssDotxmlRoute: RssDotxmlRoute, SponsorsEmbedRoute: SponsorsEmbedRoute, ApiUploadthingRoute: ApiUploadthingRoute, AuthSignoutRoute: AuthSignoutRoute, diff --git a/src/routes/rss.xml.ts b/src/routes/rss[.]xml.ts similarity index 71% rename from src/routes/rss.xml.ts rename to src/routes/rss[.]xml.ts index 05219ff58..585bd9fdc 100644 --- a/src/routes/rss.xml.ts +++ b/src/routes/rss[.]xml.ts @@ -1,4 +1,5 @@ -import { createAPIFileRoute } from '@tanstack/react-start/api' +import { createFileRoute } from '@tanstack/react-router' +import { setResponseHeader } from '@tanstack/react-start/server' import { getPublishedPosts, formatAuthors } from '~/utils/blog' function escapeXml(unsafe: string): string { @@ -20,12 +21,14 @@ function generateRSSFeed() { const postUrl = `${siteUrl}/blog/${post.slug}` const pubDate = new Date(post.published).toUTCString() const author = formatAuthors(post.authors) - + // Use excerpt if available, otherwise try to get first paragraph from content let description = post.excerpt || '' if (!description && post.content) { // Extract first paragraph after frontmatter - const contentWithoutFrontmatter = post.content.replace(/^---[\s\S]*?---/, '').trim() + const contentWithoutFrontmatter = post.content + .replace(/^---[\s\S]*?---/, '') + .trim() const firstParagraph = contentWithoutFrontmatter.split('\n\n')[0] description = firstParagraph.replace(/!\[[^\]]*\]\([^)]*\)/g, '') // Remove images } @@ -57,15 +60,25 @@ function generateRSSFeed() { ` } -export const APIRoute = createAPIFileRoute('/rss.xml')({ - GET: async () => { - const rss = generateRSSFeed() +export const Route = createFileRoute('/rss.xml')({ + // @ts-ignore server property not in route types yet + server: { + handlers: { + GET: async () => { + const content = generateRSSFeed() + + setResponseHeader('Content-Type', 'application/xml; charset=utf-8') + setResponseHeader( + 'Cache-Control', + 'public, max-age=300, must-revalidate', + ) + setResponseHeader( + 'CDN-Cache-Control', + 'max-age=3600, stale-while-revalidate=3600', + ) - return new Response(rss, { - headers: { - 'Content-Type': 'application/xml; charset=utf-8', - 'Cache-Control': 'public, max-age=300, s-maxage=3600', + return new Response(content) }, - }) + }, }, }) From 249e2ee75acd15c2b9f06b7da6e2c608c4816623 Mon Sep 17 00:00:00 2001 From: ladybluenotes Date: Wed, 7 Jan 2026 11:43:05 -0800 Subject: [PATCH 3/6] feat: add RSS feed link to blog header and improve RSS feed generation --- src/routeTree.gen.ts | 21 --------------------- src/routes/_libraries/blog.index.tsx | 15 ++++++++++++++- src/routes/rss[.]xml.ts | 11 ++++++----- 3 files changed, 20 insertions(+), 27 deletions(-) diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 699078687..180940e2e 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -45,7 +45,6 @@ import { Route as LibrariesMaintainersRouteImport } from './routes/_libraries/ma import { Route as LibrariesLoginRouteImport } from './routes/_libraries/login' import { Route as LibrariesLearnRouteImport } from './routes/_libraries/learn' import { Route as LibrariesFeedbackLeaderboardRouteImport } from './routes/_libraries/feedback-leaderboard' -import { Route as LibrariesExploreRouteImport } from './routes/_libraries/explore' import { Route as LibrariesEthosRouteImport } from './routes/_libraries/ethos' import { Route as LibrariesDashboardRouteImport } from './routes/_libraries/dashboard' import { Route as LibrariesBrandGuideRouteImport } from './routes/_libraries/brand-guide' @@ -284,11 +283,6 @@ const LibrariesFeedbackLeaderboardRoute = path: '/feedback-leaderboard', getParentRoute: () => LibrariesRouteRoute, } as any) -const LibrariesExploreRoute = LibrariesExploreRouteImport.update({ - id: '/explore', - path: '/explore', - getParentRoute: () => LibrariesRouteRoute, -} as any) const LibrariesEthosRoute = LibrariesEthosRouteImport.update({ id: '/ethos', path: '/ethos', @@ -614,7 +608,6 @@ export interface FileRoutesByFullPath { '/brand-guide': typeof LibrariesBrandGuideRoute '/dashboard': typeof LibrariesDashboardRoute '/ethos': typeof LibrariesEthosRoute - '/explore': typeof LibrariesExploreRoute '/feedback-leaderboard': typeof LibrariesFeedbackLeaderboardRoute '/learn': typeof LibrariesLearnRoute '/login': typeof LibrariesLoginRoute @@ -705,7 +698,6 @@ export interface FileRoutesByTo { '/brand-guide': typeof LibrariesBrandGuideRoute '/dashboard': typeof LibrariesDashboardRoute '/ethos': typeof LibrariesEthosRoute - '/explore': typeof LibrariesExploreRoute '/feedback-leaderboard': typeof LibrariesFeedbackLeaderboardRoute '/learn': typeof LibrariesLearnRoute '/login': typeof LibrariesLoginRoute @@ -801,7 +793,6 @@ export interface FileRoutesById { '/_libraries/brand-guide': typeof LibrariesBrandGuideRoute '/_libraries/dashboard': typeof LibrariesDashboardRoute '/_libraries/ethos': typeof LibrariesEthosRoute - '/_libraries/explore': typeof LibrariesExploreRoute '/_libraries/feedback-leaderboard': typeof LibrariesFeedbackLeaderboardRoute '/_libraries/learn': typeof LibrariesLearnRoute '/_libraries/login': typeof LibrariesLoginRoute @@ -898,7 +889,6 @@ export interface FileRouteTypes { | '/brand-guide' | '/dashboard' | '/ethos' - | '/explore' | '/feedback-leaderboard' | '/learn' | '/login' @@ -989,7 +979,6 @@ export interface FileRouteTypes { | '/brand-guide' | '/dashboard' | '/ethos' - | '/explore' | '/feedback-leaderboard' | '/learn' | '/login' @@ -1084,7 +1073,6 @@ export interface FileRouteTypes { | '/_libraries/brand-guide' | '/_libraries/dashboard' | '/_libraries/ethos' - | '/_libraries/explore' | '/_libraries/feedback-leaderboard' | '/_libraries/learn' | '/_libraries/login' @@ -1444,13 +1432,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LibrariesFeedbackLeaderboardRouteImport parentRoute: typeof LibrariesRouteRoute } - '/_libraries/explore': { - id: '/_libraries/explore' - path: '/explore' - fullPath: '/explore' - preLoaderRoute: typeof LibrariesExploreRouteImport - parentRoute: typeof LibrariesRouteRoute - } '/_libraries/ethos': { id: '/_libraries/ethos' path: '/ethos' @@ -1950,7 +1931,6 @@ interface LibrariesRouteRouteChildren { LibrariesBrandGuideRoute: typeof LibrariesBrandGuideRoute LibrariesDashboardRoute: typeof LibrariesDashboardRoute LibrariesEthosRoute: typeof LibrariesEthosRoute - LibrariesExploreRoute: typeof LibrariesExploreRoute LibrariesFeedbackLeaderboardRoute: typeof LibrariesFeedbackLeaderboardRoute LibrariesLearnRoute: typeof LibrariesLearnRoute LibrariesLoginRoute: typeof LibrariesLoginRoute @@ -1987,7 +1967,6 @@ const LibrariesRouteRouteChildren: LibrariesRouteRouteChildren = { LibrariesBrandGuideRoute: LibrariesBrandGuideRoute, LibrariesDashboardRoute: LibrariesDashboardRoute, LibrariesEthosRoute: LibrariesEthosRoute, - LibrariesExploreRoute: LibrariesExploreRoute, LibrariesFeedbackLeaderboardRoute: LibrariesFeedbackLeaderboardRoute, LibrariesLearnRoute: LibrariesLearnRoute, LibrariesLoginRoute: LibrariesLoginRoute, diff --git a/src/routes/_libraries/blog.index.tsx b/src/routes/_libraries/blog.index.tsx index 6d095659e..ae1a38cba 100644 --- a/src/routes/_libraries/blog.index.tsx +++ b/src/routes/_libraries/blog.index.tsx @@ -7,6 +7,7 @@ import { Footer } from '~/components/Footer' import { PostNotFound } from './blog' import { createServerFn } from '@tanstack/react-start' import { setResponseHeaders } from '@tanstack/react-start/server' +import { RssIcon } from 'lucide-react' type BlogFrontMatter = { slug: string @@ -65,7 +66,19 @@ function BlogIndex() {
-

Blog

+
+

Blog

+ + + +
+

The latest news and blog posts from TanStack

diff --git a/src/routes/rss[.]xml.ts b/src/routes/rss[.]xml.ts index 585bd9fdc..f59637b70 100644 --- a/src/routes/rss[.]xml.ts +++ b/src/routes/rss[.]xml.ts @@ -1,6 +1,5 @@ import { createFileRoute } from '@tanstack/react-router' import { setResponseHeader } from '@tanstack/react-start/server' -import { getPublishedPosts, formatAuthors } from '~/utils/blog' function escapeXml(unsafe: string): string { return unsafe @@ -11,7 +10,9 @@ function escapeXml(unsafe: string): string { .replace(/'/g, ''') } -function generateRSSFeed() { +async function generateRSSFeed() { + const { getPublishedPosts, formatAuthors } = await import('~/utils/blog') + const posts = getPublishedPosts().slice(0, 50) // Most recent 50 posts const siteUrl = 'https://tanstack.com' const buildDate = new Date().toUTCString() @@ -30,7 +31,7 @@ function generateRSSFeed() { .replace(/^---[\s\S]*?---/, '') .trim() const firstParagraph = contentWithoutFrontmatter.split('\n\n')[0] - description = firstParagraph.replace(/!\[[^\]]*\]\([^)]*\)/g, '') // Remove images + description = firstParagraph.replace(/!\[[^\]]*]\([^)]*\)/g, '') // Remove images } return ` @@ -61,11 +62,11 @@ function generateRSSFeed() { } export const Route = createFileRoute('/rss.xml')({ - // @ts-ignore server property not in route types yet + // @ts-expect-error server property not in route types yet server: { handlers: { GET: async () => { - const content = generateRSSFeed() + const content = await generateRSSFeed() setResponseHeader('Content-Type', 'application/xml; charset=utf-8') setResponseHeader( From 2a61a991bbaa89c35e02fa51c0d7239118b0636e Mon Sep 17 00:00:00 2001 From: Vlad Rafeev Date: Wed, 7 Jan 2026 19:48:34 +0000 Subject: [PATCH 4/6] fix: improve blog date formatting (#630) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Blog post: use full month name format (January 2, 2026) - Posts list: use short month format without leading zero (Jan 2, 2026) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Sonnet 4.5 --- src/routes/_libraries/blog.$.tsx | 2 +- src/routes/_libraries/blog.index.tsx | 4 ++-- src/utils/dates.ts | 8 ++++++++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/routes/_libraries/blog.$.tsx b/src/routes/_libraries/blog.$.tsx index 5922289d4..45d69d6ce 100644 --- a/src/routes/_libraries/blog.$.tsx +++ b/src/routes/_libraries/blog.$.tsx @@ -101,7 +101,7 @@ function BlogPost() { const blogContent = `_by ${formatAuthors(authors)} on ${format( new Date(published || 0), - 'MMM dd, yyyy', + 'MMMM d, yyyy', )}._ ${content}` diff --git a/src/routes/_libraries/blog.index.tsx b/src/routes/_libraries/blog.index.tsx index ae1a38cba..c9774cd33 100644 --- a/src/routes/_libraries/blog.index.tsx +++ b/src/routes/_libraries/blog.index.tsx @@ -101,10 +101,10 @@ function BlogIndex() { {published ? ( ) : null}

diff --git a/src/utils/dates.ts b/src/utils/dates.ts index ddb5375e3..502c5135a 100644 --- a/src/utils/dates.ts +++ b/src/utils/dates.ts @@ -81,6 +81,14 @@ export function format(date: Date | number, formatStr: string): string { day: 'numeric', }) + case 'MMMM d, yyyy': + // "April 29, 2023" + return d.toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + }) + case 'yyyy-MM-dd': // "2023-04-29" return d.toISOString().split('T')[0] From a35ccb6446f20c9302e0543ec2a6074e0a45d340 Mon Sep 17 00:00:00 2001 From: ladybluenotes Date: Tue, 6 Jan 2026 16:14:22 -0800 Subject: [PATCH 5/6] wip --- src/routes/rss[.]xml.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/routes/rss[.]xml.ts b/src/routes/rss[.]xml.ts index f59637b70..585bd9fdc 100644 --- a/src/routes/rss[.]xml.ts +++ b/src/routes/rss[.]xml.ts @@ -1,5 +1,6 @@ import { createFileRoute } from '@tanstack/react-router' import { setResponseHeader } from '@tanstack/react-start/server' +import { getPublishedPosts, formatAuthors } from '~/utils/blog' function escapeXml(unsafe: string): string { return unsafe @@ -10,9 +11,7 @@ function escapeXml(unsafe: string): string { .replace(/'/g, ''') } -async function generateRSSFeed() { - const { getPublishedPosts, formatAuthors } = await import('~/utils/blog') - +function generateRSSFeed() { const posts = getPublishedPosts().slice(0, 50) // Most recent 50 posts const siteUrl = 'https://tanstack.com' const buildDate = new Date().toUTCString() @@ -31,7 +30,7 @@ async function generateRSSFeed() { .replace(/^---[\s\S]*?---/, '') .trim() const firstParagraph = contentWithoutFrontmatter.split('\n\n')[0] - description = firstParagraph.replace(/!\[[^\]]*]\([^)]*\)/g, '') // Remove images + description = firstParagraph.replace(/!\[[^\]]*\]\([^)]*\)/g, '') // Remove images } return ` @@ -62,11 +61,11 @@ async function generateRSSFeed() { } export const Route = createFileRoute('/rss.xml')({ - // @ts-expect-error server property not in route types yet + // @ts-ignore server property not in route types yet server: { handlers: { GET: async () => { - const content = await generateRSSFeed() + const content = generateRSSFeed() setResponseHeader('Content-Type', 'application/xml; charset=utf-8') setResponseHeader( From 6d2618f0a13d69c504c73db05e551eb81155ed99 Mon Sep 17 00:00:00 2001 From: ladybluenotes Date: Wed, 7 Jan 2026 12:00:26 -0800 Subject: [PATCH 6/6] feat: add RSS feed link to blog header and improve RSS feed generation --- src/utils/dates.ts | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/utils/dates.ts b/src/utils/dates.ts index 502c5135a..2f1dd78e1 100644 --- a/src/utils/dates.ts +++ b/src/utils/dates.ts @@ -42,6 +42,7 @@ export function format(date: Date | number, formatStr: string): string { // Common format patterns switch (formatStr) { case 'PPP': + case 'MMMM d, yyyy': // "April 29, 2023" return d.toLocaleDateString('en-US', { year: 'numeric', @@ -65,14 +66,6 @@ export function format(date: Date | number, formatStr: string): string { day: '2-digit', }) - case 'MMMM d, yyyy': - // "April 29, 2023" - return d.toLocaleDateString('en-US', { - year: 'numeric', - month: 'long', - day: 'numeric', - }) - case 'MMM d, yyyy': // "Apr 29, 2023" return d.toLocaleDateString('en-US', { @@ -81,11 +74,11 @@ export function format(date: Date | number, formatStr: string): string { day: 'numeric', }) - case 'MMMM d, yyyy': - // "April 29, 2023" + case 'MMM dd, yyyy': + // "Apr 29, 2023" (same as above, just different format string) return d.toLocaleDateString('en-US', { year: 'numeric', - month: 'long', + month: 'short', day: 'numeric', })