diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 85cc43527..180940e2e 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' @@ -44,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' @@ -108,6 +108,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', @@ -278,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', @@ -599,6 +599,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 @@ -607,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 @@ -691,13 +691,13 @@ 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 '/brand-guide': typeof LibrariesBrandGuideRoute '/dashboard': typeof LibrariesDashboardRoute '/ethos': typeof LibrariesEthosRoute - '/explore': typeof LibrariesExploreRoute '/feedback-leaderboard': typeof LibrariesFeedbackLeaderboardRoute '/learn': typeof LibrariesLearnRoute '/login': typeof LibrariesLoginRoute @@ -784,6 +784,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 @@ -792,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 @@ -880,6 +880,7 @@ export interface FileRouteTypes { | '/llms.txt' | '/merch' | '/partners-embed' + | '/rss.xml' | '/sponsors-embed' | '/$libraryId/$version' | '/account' @@ -888,7 +889,6 @@ export interface FileRouteTypes { | '/brand-guide' | '/dashboard' | '/ethos' - | '/explore' | '/feedback-leaderboard' | '/learn' | '/login' @@ -972,13 +972,13 @@ export interface FileRouteTypes { | '/llms.txt' | '/merch' | '/partners-embed' + | '/rss.xml' | '/sponsors-embed' | '/$libraryId/$version' | '/ads' | '/brand-guide' | '/dashboard' | '/ethos' - | '/explore' | '/feedback-leaderboard' | '/learn' | '/login' @@ -1064,6 +1064,7 @@ export interface FileRouteTypes { | '/llms.txt' | '/merch' | '/partners-embed' + | '/rss.xml' | '/sponsors-embed' | '/$libraryId/$version' | '/_libraries/account' @@ -1072,7 +1073,6 @@ export interface FileRouteTypes { | '/_libraries/brand-guide' | '/_libraries/dashboard' | '/_libraries/ethos' - | '/_libraries/explore' | '/_libraries/feedback-leaderboard' | '/_libraries/learn' | '/_libraries/login' @@ -1160,6 +1160,7 @@ export interface RootRouteChildren { LlmsDottxtRoute: typeof LlmsDottxtRoute MerchRoute: typeof MerchRoute PartnersEmbedRoute: typeof PartnersEmbedRoute + RssDotxmlRoute: typeof RssDotxmlRoute SponsorsEmbedRoute: typeof SponsorsEmbedRoute ApiUploadthingRoute: typeof ApiUploadthingRoute AuthPopupSuccessRoute: typeof AuthPopupSuccessRoute @@ -1186,6 +1187,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' @@ -1424,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' @@ -1930,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 @@ -1967,7 +1967,6 @@ const LibrariesRouteRouteChildren: LibrariesRouteRouteChildren = { LibrariesBrandGuideRoute: LibrariesBrandGuideRoute, LibrariesDashboardRoute: LibrariesDashboardRoute, LibrariesEthosRoute: LibrariesEthosRoute, - LibrariesExploreRoute: LibrariesExploreRoute, LibrariesFeedbackLeaderboardRoute: LibrariesFeedbackLeaderboardRoute, LibrariesLearnRoute: LibrariesLearnRoute, LibrariesLoginRoute: LibrariesLoginRoute, @@ -2066,6 +2065,7 @@ const rootRouteChildren: RootRouteChildren = { LlmsDottxtRoute: LlmsDottxtRoute, MerchRoute: MerchRoute, PartnersEmbedRoute: PartnersEmbedRoute, + RssDotxmlRoute: RssDotxmlRoute, SponsorsEmbedRoute: SponsorsEmbedRoute, ApiUploadthingRoute: ApiUploadthingRoute, AuthPopupSuccessRoute: AuthPopupSuccessRoute, 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 6d095659e..c9774cd33 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

@@ -88,10 +101,10 @@ function BlogIndex() { {published ? ( ) : null}

diff --git a/src/routes/rss[.]xml.ts b/src/routes/rss[.]xml.ts new file mode 100644 index 000000000..585bd9fdc --- /dev/null +++ b/src/routes/rss[.]xml.ts @@ -0,0 +1,84 @@ +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 + .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 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(content) + }, + }, + }, +}) diff --git a/src/utils/dates.ts b/src/utils/dates.ts index ddb5375e3..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,16 +66,16 @@ export function format(date: Date | number, formatStr: string): string { day: '2-digit', }) - case 'MMMM d, yyyy': - // "April 29, 2023" + case 'MMM d, yyyy': + // "Apr 29, 2023" return d.toLocaleDateString('en-US', { year: 'numeric', - month: 'long', + month: 'short', day: 'numeric', }) - case 'MMM d, yyyy': - // "Apr 29, 2023" + case 'MMM dd, yyyy': + // "Apr 29, 2023" (same as above, just different format string) return d.toLocaleDateString('en-US', { year: 'numeric', month: 'short',