Skip to content

Commit 30dc101

Browse files
committed
2 parents c95391e + 5be252a commit 30dc101

File tree

5 files changed

+136
-30
lines changed

5 files changed

+136
-30
lines changed

src/routeTree.gen.ts

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import { Route as rootRouteImport } from './routes/__root'
1212
import { Route as SponsorsEmbedRouteImport } from './routes/sponsors-embed'
13+
import { Route as RssDotxmlRouteImport } from './routes/rss[.]xml'
1314
import { Route as PartnersEmbedRouteImport } from './routes/partners-embed'
1415
import { Route as MerchRouteImport } from './routes/merch'
1516
import { Route as LlmsDottxtRouteImport } from './routes/llms[.]txt'
@@ -44,7 +45,6 @@ import { Route as LibrariesMaintainersRouteImport } from './routes/_libraries/ma
4445
import { Route as LibrariesLoginRouteImport } from './routes/_libraries/login'
4546
import { Route as LibrariesLearnRouteImport } from './routes/_libraries/learn'
4647
import { Route as LibrariesFeedbackLeaderboardRouteImport } from './routes/_libraries/feedback-leaderboard'
47-
import { Route as LibrariesExploreRouteImport } from './routes/_libraries/explore'
4848
import { Route as LibrariesEthosRouteImport } from './routes/_libraries/ethos'
4949
import { Route as LibrariesDashboardRouteImport } from './routes/_libraries/dashboard'
5050
import { Route as LibrariesBrandGuideRouteImport } from './routes/_libraries/brand-guide'
@@ -108,6 +108,11 @@ const SponsorsEmbedRoute = SponsorsEmbedRouteImport.update({
108108
path: '/sponsors-embed',
109109
getParentRoute: () => rootRouteImport,
110110
} as any)
111+
const RssDotxmlRoute = RssDotxmlRouteImport.update({
112+
id: '/rss.xml',
113+
path: '/rss.xml',
114+
getParentRoute: () => rootRouteImport,
115+
} as any)
111116
const PartnersEmbedRoute = PartnersEmbedRouteImport.update({
112117
id: '/partners-embed',
113118
path: '/partners-embed',
@@ -278,11 +283,6 @@ const LibrariesFeedbackLeaderboardRoute =
278283
path: '/feedback-leaderboard',
279284
getParentRoute: () => LibrariesRouteRoute,
280285
} as any)
281-
const LibrariesExploreRoute = LibrariesExploreRouteImport.update({
282-
id: '/explore',
283-
path: '/explore',
284-
getParentRoute: () => LibrariesRouteRoute,
285-
} as any)
286286
const LibrariesEthosRoute = LibrariesEthosRouteImport.update({
287287
id: '/ethos',
288288
path: '/ethos',
@@ -599,6 +599,7 @@ export interface FileRoutesByFullPath {
599599
'/llms.txt': typeof LlmsDottxtRoute
600600
'/merch': typeof MerchRoute
601601
'/partners-embed': typeof PartnersEmbedRoute
602+
'/rss.xml': typeof RssDotxmlRoute
602603
'/sponsors-embed': typeof SponsorsEmbedRoute
603604
'/$libraryId/$version': typeof LibraryIdVersionRouteWithChildren
604605
'/account': typeof LibrariesAccountRouteWithChildren
@@ -607,7 +608,6 @@ export interface FileRoutesByFullPath {
607608
'/brand-guide': typeof LibrariesBrandGuideRoute
608609
'/dashboard': typeof LibrariesDashboardRoute
609610
'/ethos': typeof LibrariesEthosRoute
610-
'/explore': typeof LibrariesExploreRoute
611611
'/feedback-leaderboard': typeof LibrariesFeedbackLeaderboardRoute
612612
'/learn': typeof LibrariesLearnRoute
613613
'/login': typeof LibrariesLoginRoute
@@ -691,13 +691,13 @@ export interface FileRoutesByTo {
691691
'/llms.txt': typeof LlmsDottxtRoute
692692
'/merch': typeof MerchRoute
693693
'/partners-embed': typeof PartnersEmbedRoute
694+
'/rss.xml': typeof RssDotxmlRoute
694695
'/sponsors-embed': typeof SponsorsEmbedRoute
695696
'/$libraryId/$version': typeof LibraryIdVersionRouteWithChildren
696697
'/ads': typeof LibrariesAdsRoute
697698
'/brand-guide': typeof LibrariesBrandGuideRoute
698699
'/dashboard': typeof LibrariesDashboardRoute
699700
'/ethos': typeof LibrariesEthosRoute
700-
'/explore': typeof LibrariesExploreRoute
701701
'/feedback-leaderboard': typeof LibrariesFeedbackLeaderboardRoute
702702
'/learn': typeof LibrariesLearnRoute
703703
'/login': typeof LibrariesLoginRoute
@@ -784,6 +784,7 @@ export interface FileRoutesById {
784784
'/llms.txt': typeof LlmsDottxtRoute
785785
'/merch': typeof MerchRoute
786786
'/partners-embed': typeof PartnersEmbedRoute
787+
'/rss.xml': typeof RssDotxmlRoute
787788
'/sponsors-embed': typeof SponsorsEmbedRoute
788789
'/$libraryId/$version': typeof LibraryIdVersionRouteWithChildren
789790
'/_libraries/account': typeof LibrariesAccountRouteWithChildren
@@ -792,7 +793,6 @@ export interface FileRoutesById {
792793
'/_libraries/brand-guide': typeof LibrariesBrandGuideRoute
793794
'/_libraries/dashboard': typeof LibrariesDashboardRoute
794795
'/_libraries/ethos': typeof LibrariesEthosRoute
795-
'/_libraries/explore': typeof LibrariesExploreRoute
796796
'/_libraries/feedback-leaderboard': typeof LibrariesFeedbackLeaderboardRoute
797797
'/_libraries/learn': typeof LibrariesLearnRoute
798798
'/_libraries/login': typeof LibrariesLoginRoute
@@ -880,6 +880,7 @@ export interface FileRouteTypes {
880880
| '/llms.txt'
881881
| '/merch'
882882
| '/partners-embed'
883+
| '/rss.xml'
883884
| '/sponsors-embed'
884885
| '/$libraryId/$version'
885886
| '/account'
@@ -888,7 +889,6 @@ export interface FileRouteTypes {
888889
| '/brand-guide'
889890
| '/dashboard'
890891
| '/ethos'
891-
| '/explore'
892892
| '/feedback-leaderboard'
893893
| '/learn'
894894
| '/login'
@@ -972,13 +972,13 @@ export interface FileRouteTypes {
972972
| '/llms.txt'
973973
| '/merch'
974974
| '/partners-embed'
975+
| '/rss.xml'
975976
| '/sponsors-embed'
976977
| '/$libraryId/$version'
977978
| '/ads'
978979
| '/brand-guide'
979980
| '/dashboard'
980981
| '/ethos'
981-
| '/explore'
982982
| '/feedback-leaderboard'
983983
| '/learn'
984984
| '/login'
@@ -1064,6 +1064,7 @@ export interface FileRouteTypes {
10641064
| '/llms.txt'
10651065
| '/merch'
10661066
| '/partners-embed'
1067+
| '/rss.xml'
10671068
| '/sponsors-embed'
10681069
| '/$libraryId/$version'
10691070
| '/_libraries/account'
@@ -1072,7 +1073,6 @@ export interface FileRouteTypes {
10721073
| '/_libraries/brand-guide'
10731074
| '/_libraries/dashboard'
10741075
| '/_libraries/ethos'
1075-
| '/_libraries/explore'
10761076
| '/_libraries/feedback-leaderboard'
10771077
| '/_libraries/learn'
10781078
| '/_libraries/login'
@@ -1160,6 +1160,7 @@ export interface RootRouteChildren {
11601160
LlmsDottxtRoute: typeof LlmsDottxtRoute
11611161
MerchRoute: typeof MerchRoute
11621162
PartnersEmbedRoute: typeof PartnersEmbedRoute
1163+
RssDotxmlRoute: typeof RssDotxmlRoute
11631164
SponsorsEmbedRoute: typeof SponsorsEmbedRoute
11641165
ApiUploadthingRoute: typeof ApiUploadthingRoute
11651166
AuthPopupSuccessRoute: typeof AuthPopupSuccessRoute
@@ -1186,6 +1187,13 @@ declare module '@tanstack/react-router' {
11861187
preLoaderRoute: typeof SponsorsEmbedRouteImport
11871188
parentRoute: typeof rootRouteImport
11881189
}
1190+
'/rss.xml': {
1191+
id: '/rss.xml'
1192+
path: '/rss.xml'
1193+
fullPath: '/rss.xml'
1194+
preLoaderRoute: typeof RssDotxmlRouteImport
1195+
parentRoute: typeof rootRouteImport
1196+
}
11891197
'/partners-embed': {
11901198
id: '/partners-embed'
11911199
path: '/partners-embed'
@@ -1424,13 +1432,6 @@ declare module '@tanstack/react-router' {
14241432
preLoaderRoute: typeof LibrariesFeedbackLeaderboardRouteImport
14251433
parentRoute: typeof LibrariesRouteRoute
14261434
}
1427-
'/_libraries/explore': {
1428-
id: '/_libraries/explore'
1429-
path: '/explore'
1430-
fullPath: '/explore'
1431-
preLoaderRoute: typeof LibrariesExploreRouteImport
1432-
parentRoute: typeof LibrariesRouteRoute
1433-
}
14341435
'/_libraries/ethos': {
14351436
id: '/_libraries/ethos'
14361437
path: '/ethos'
@@ -1930,7 +1931,6 @@ interface LibrariesRouteRouteChildren {
19301931
LibrariesBrandGuideRoute: typeof LibrariesBrandGuideRoute
19311932
LibrariesDashboardRoute: typeof LibrariesDashboardRoute
19321933
LibrariesEthosRoute: typeof LibrariesEthosRoute
1933-
LibrariesExploreRoute: typeof LibrariesExploreRoute
19341934
LibrariesFeedbackLeaderboardRoute: typeof LibrariesFeedbackLeaderboardRoute
19351935
LibrariesLearnRoute: typeof LibrariesLearnRoute
19361936
LibrariesLoginRoute: typeof LibrariesLoginRoute
@@ -1967,7 +1967,6 @@ const LibrariesRouteRouteChildren: LibrariesRouteRouteChildren = {
19671967
LibrariesBrandGuideRoute: LibrariesBrandGuideRoute,
19681968
LibrariesDashboardRoute: LibrariesDashboardRoute,
19691969
LibrariesEthosRoute: LibrariesEthosRoute,
1970-
LibrariesExploreRoute: LibrariesExploreRoute,
19711970
LibrariesFeedbackLeaderboardRoute: LibrariesFeedbackLeaderboardRoute,
19721971
LibrariesLearnRoute: LibrariesLearnRoute,
19731972
LibrariesLoginRoute: LibrariesLoginRoute,
@@ -2066,6 +2065,7 @@ const rootRouteChildren: RootRouteChildren = {
20662065
LlmsDottxtRoute: LlmsDottxtRoute,
20672066
MerchRoute: MerchRoute,
20682067
PartnersEmbedRoute: PartnersEmbedRoute,
2068+
RssDotxmlRoute: RssDotxmlRoute,
20692069
SponsorsEmbedRoute: SponsorsEmbedRoute,
20702070
ApiUploadthingRoute: ApiUploadthingRoute,
20712071
AuthPopupSuccessRoute: AuthPopupSuccessRoute,

src/routes/_libraries/blog.$.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ function BlogPost() {
101101

102102
const blogContent = `<small>_by ${formatAuthors(authors)} on ${format(
103103
new Date(published || 0),
104-
'MMM dd, yyyy',
104+
'MMMM d, yyyy',
105105
)}._</small>
106106
107107
${content}`

src/routes/_libraries/blog.index.tsx

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { Footer } from '~/components/Footer'
77
import { PostNotFound } from './blog'
88
import { createServerFn } from '@tanstack/react-start'
99
import { setResponseHeaders } from '@tanstack/react-start/server'
10+
import { RssIcon } from 'lucide-react'
1011

1112
type BlogFrontMatter = {
1213
slug: string
@@ -65,7 +66,19 @@ function BlogIndex() {
6566
<div className="flex flex-col max-w-full min-h-screen gap-12 p-4 md:p-8 pb-0">
6667
<div className="flex-1 space-y-12 w-full max-w-4xl mx-auto">
6768
<header className="">
68-
<h1 className="text-3xl font-black">Blog</h1>
69+
<div className="flex gap-3 items-baseline">
70+
<h1 className="text-3xl font-black">Blog</h1>
71+
<a
72+
href="/rss.xml"
73+
target="_blank"
74+
rel="noreferrer"
75+
className="text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 transition-colors text-xl"
76+
title="RSS Feed"
77+
>
78+
<RssIcon />
79+
</a>
80+
</div>
81+
6982
<p className="text-lg mt-4 text-gray-600 dark:text-gray-400">
7083
The latest news and blog posts from TanStack
7184
</p>
@@ -88,10 +101,10 @@ function BlogIndex() {
88101
{published ? (
89102
<time
90103
dateTime={published}
91-
title={format(new Date(published), 'MMM dd, yyyy')}
104+
title={format(new Date(published), 'MMM d, yyyy')}
92105
>
93106
{' '}
94-
on {format(new Date(published), 'MMM dd, yyyy')}
107+
on {format(new Date(published), 'MMM d, yyyy')}
95108
</time>
96109
) : null}
97110
</p>

src/routes/rss[.]xml.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { createFileRoute } from '@tanstack/react-router'
2+
import { setResponseHeader } from '@tanstack/react-start/server'
3+
import { getPublishedPosts, formatAuthors } from '~/utils/blog'
4+
5+
function escapeXml(unsafe: string): string {
6+
return unsafe
7+
.replace(/&/g, '&amp;')
8+
.replace(/</g, '&lt;')
9+
.replace(/>/g, '&gt;')
10+
.replace(/"/g, '&quot;')
11+
.replace(/'/g, '&apos;')
12+
}
13+
14+
function generateRSSFeed() {
15+
const posts = getPublishedPosts().slice(0, 50) // Most recent 50 posts
16+
const siteUrl = 'https://tanstack.com'
17+
const buildDate = new Date().toUTCString()
18+
19+
const rssItems = posts
20+
.map((post) => {
21+
const postUrl = `${siteUrl}/blog/${post.slug}`
22+
const pubDate = new Date(post.published).toUTCString()
23+
const author = formatAuthors(post.authors)
24+
25+
// Use excerpt if available, otherwise try to get first paragraph from content
26+
let description = post.excerpt || ''
27+
if (!description && post.content) {
28+
// Extract first paragraph after frontmatter
29+
const contentWithoutFrontmatter = post.content
30+
.replace(/^---[\s\S]*?---/, '')
31+
.trim()
32+
const firstParagraph = contentWithoutFrontmatter.split('\n\n')[0]
33+
description = firstParagraph.replace(/!\[[^\]]*\]\([^)]*\)/g, '') // Remove images
34+
}
35+
36+
return `
37+
<item>
38+
<title>${escapeXml(post.title)}</title>
39+
<link>${escapeXml(postUrl)}</link>
40+
<guid isPermaLink="true">${escapeXml(postUrl)}</guid>
41+
<pubDate>${pubDate}</pubDate>
42+
<author>${escapeXml(author)}</author>
43+
<description>${escapeXml(description)}</description>
44+
${post.headerImage ? `<enclosure url="${escapeXml(siteUrl + post.headerImage)}" type="image/png" />` : ''}
45+
</item>`
46+
})
47+
.join('')
48+
49+
return `<?xml version="1.0" encoding="UTF-8"?>
50+
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
51+
<channel>
52+
<title>TanStack Blog</title>
53+
<link>${siteUrl}/blog</link>
54+
<description>The latest news and updates from TanStack</description>
55+
<language>en-us</language>
56+
<lastBuildDate>${buildDate}</lastBuildDate>
57+
<atom:link href="${siteUrl}/rss.xml" rel="self" type="application/rss+xml" />
58+
${rssItems}
59+
</channel>
60+
</rss>`
61+
}
62+
63+
export const Route = createFileRoute('/rss.xml')({
64+
// @ts-ignore server property not in route types yet
65+
server: {
66+
handlers: {
67+
GET: async () => {
68+
const content = generateRSSFeed()
69+
70+
setResponseHeader('Content-Type', 'application/xml; charset=utf-8')
71+
setResponseHeader(
72+
'Cache-Control',
73+
'public, max-age=300, must-revalidate',
74+
)
75+
setResponseHeader(
76+
'CDN-Cache-Control',
77+
'max-age=3600, stale-while-revalidate=3600',
78+
)
79+
80+
return new Response(content)
81+
},
82+
},
83+
},
84+
})

src/utils/dates.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export function format(date: Date | number, formatStr: string): string {
4242
// Common format patterns
4343
switch (formatStr) {
4444
case 'PPP':
45+
case 'MMMM d, yyyy':
4546
// "April 29, 2023"
4647
return d.toLocaleDateString('en-US', {
4748
year: 'numeric',
@@ -65,22 +66,30 @@ export function format(date: Date | number, formatStr: string): string {
6566
day: '2-digit',
6667
})
6768

68-
case 'MMMM d, yyyy':
69-
// "April 29, 2023"
69+
case 'MMM d, yyyy':
70+
// "Apr 29, 2023"
7071
return d.toLocaleDateString('en-US', {
7172
year: 'numeric',
72-
month: 'long',
73+
month: 'short',
7374
day: 'numeric',
7475
})
7576

76-
case 'MMM d, yyyy':
77-
// "Apr 29, 2023"
77+
case 'MMM dd, yyyy':
78+
// "Apr 29, 2023" (same as above, just different format string)
7879
return d.toLocaleDateString('en-US', {
7980
year: 'numeric',
8081
month: 'short',
8182
day: 'numeric',
8283
})
8384

85+
case 'MMMM d, yyyy':
86+
// "April 29, 2023"
87+
return d.toLocaleDateString('en-US', {
88+
year: 'numeric',
89+
month: 'long',
90+
day: 'numeric',
91+
})
92+
8493
case 'yyyy-MM-dd':
8594
// "2023-04-29"
8695
return d.toISOString().split('T')[0]

0 commit comments

Comments
 (0)