Skip to content

Commit d17bbc7

Browse files
perf: stop shipping full blog post content to every client bundle (#1025)
1 parent 8e8af15 commit d17bbc7

12 files changed

Lines changed: 195 additions & 96 deletions

File tree

src/components/BlogCard.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
formatAuthors,
66
formatPublishedDate,
77
getBlogLibraries,
8-
} from '~/utils/blog'
8+
} from '~/utils/blog-format'
99
import { getOptimizedImageUrl } from '~/utils/optimizedImage'
1010

1111
export type BlogCardPost = {

src/components/RecentPostsWidget.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Link } from '@tanstack/react-router'
22
import { useQuery } from '@tanstack/react-query'
33
import { fetchRecentPosts, type RecentPost } from '~/utils/blog.functions'
4-
import { formatPublishedDate } from '~/utils/blog'
4+
import { formatPublishedDate } from '~/utils/blog-format'
55

66
type RecentPostsWidgetProps = {
77
posts?: ReadonlyArray<RecentPost>

src/components/home/HomeSocialProofSection.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { ArrowRight } from 'lucide-react'
55
import { Card } from '~/components/Card'
66
import { PartnersGrid } from '~/components/PartnersGrid'
77
import { Button } from '~/ui'
8-
import { formatAuthors, formatPublishedDate } from '~/utils/blog'
8+
import { formatAuthors, formatPublishedDate } from '~/utils/blog-format'
99
import type { RecentPost } from '~/utils/blog.functions'
1010

1111
type HomeSocialProofSectionProps = {

src/components/stack/CategoryArticle.tsx

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@ import {
2222
} from 'lucide-react'
2323

2424
import { LibraryWordmark } from '~/components/LibraryWordmark'
25-
import type { LibrarySlim } from '~/libraries'
26-
import { formatPublishedDate, getPostsForLibrary } from '~/utils/blog'
25+
import type { LibraryId, LibrarySlim } from '~/libraries'
26+
import { formatPublishedDate } from '~/utils/blog-format'
27+
import type { RelatedPost as RelatedPostData } from '~/utils/blog.functions'
2728
import {
2829
categoryMeta,
2930
getCategoryLibraries,
@@ -44,10 +45,16 @@ const libraryLinkClassName =
4445
const staticPanelClassName =
4546
'rounded-lg border border-zinc-200 bg-white p-4 dark:border-zinc-800 dark:bg-zinc-950'
4647

47-
export function CategoryArticle({ slug }: { slug: CategorySlug }) {
48+
export function CategoryArticle({
49+
slug,
50+
relatedPosts: relatedPostsData,
51+
}: {
52+
slug: CategorySlug
53+
relatedPosts: Array<RelatedPostData>
54+
}) {
4855
const meta = categoryMeta[slug]
4956
const libraries = getCategoryLibraries(slug)
50-
const relatedPosts = getRelatedPosts(libraries)
57+
const relatedPosts = reconstructRelatedPosts(libraries, relatedPostsData)
5158

5259
return (
5360
<div className="bg-white text-zinc-950 dark:bg-zinc-950 dark:text-zinc-50">
@@ -75,10 +82,23 @@ export function CategoryArticle({ slug }: { slug: CategorySlug }) {
7582
)
7683
}
7784

78-
function getRelatedPosts(libraries: Array<LibrarySlim>) {
79-
return libraries
80-
.flatMap((lib) => getPostsForLibrary(lib.id).map((post) => ({ post, lib })))
81-
.slice(0, 4)
85+
/**
86+
* Reconstructs {post, lib} pairs from the server-provided, already-ordered
87+
* and already-sliced related-posts data, using the in-memory `libraries`
88+
* array (pure, client-safe) rather than sending non-serializable LibrarySlim
89+
* objects (e.g. `handleRedirects`) over the server-fn RPC boundary.
90+
*/
91+
function reconstructRelatedPosts(
92+
libraries: Array<LibrarySlim>,
93+
data: Array<RelatedPostData>,
94+
): Array<RelatedPost> {
95+
const libraryById = new Map<LibraryId, LibrarySlim>(
96+
libraries.map((lib) => [lib.id, lib]),
97+
)
98+
return data.flatMap(({ libraryId, post }) => {
99+
const lib = libraryById.get(libraryId)
100+
return lib ? [{ post, lib }] : []
101+
})
82102
}
83103

84104
function Breadcrumb({ categoryName }: { categoryName: string }) {

src/routes/_library/$libraryId/$version.docs.blog.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,18 @@ import { DocTitle } from '~/components/DocTitle'
77
import { BlogCard } from '~/components/BlogCard'
88
import { BlogAuthorFilter } from '~/components/BlogAuthorFilter'
99
import { getLibrary, type LibraryId } from '~/libraries'
10-
import { getDistinctAuthors, getPostsForLibrary } from '~/utils/blog'
10+
import { fetchPostsForLibrary } from '~/utils/blog.functions'
11+
import { getDistinctAuthors } from '~/utils/blog-format'
1112

1213
const searchSchema = v.object({
1314
author: v.fallback(v.optional(v.string()), undefined),
1415
})
1516

1617
export const Route = createFileRoute('/_library/$libraryId/$version/docs/blog')(
1718
{
19+
staleTime: Infinity,
1820
validateSearch: searchSchema,
21+
loader: ({ params }) => fetchPostsForLibrary({ data: params.libraryId }),
1922
component: RouteComponent,
2023
},
2124
)
@@ -26,7 +29,7 @@ function RouteComponent() {
2629
const navigate = Route.useNavigate()
2730
const library = getLibrary(libraryId as LibraryId)
2831

29-
const posts = getPostsForLibrary(libraryId as LibraryId)
32+
const posts = Route.useLoaderData()
3033
const authors = getDistinctAuthors(posts)
3134

3235
const filteredPosts = author

src/routes/blog.$.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { createFileRoute } from '@tanstack/react-router'
22
import { seo } from '~/utils/seo'
33
import { PostNotFound } from './blog'
4-
import { formatAuthors } from '~/utils/blog'
4+
import { formatAuthors } from '~/utils/blog-format'
55
import * as React from 'react'
66
import { MarkdownContent } from '~/components/markdown'
77
import { Card } from '~/components/Card'

src/routes/blog.index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import { Link, createFileRoute } from '@tanstack/react-router'
22
import * as v from 'valibot'
33
import { BlogCard, type BlogCardPost } from '~/components/BlogCard'
44
import { BlogAuthorFilter } from '~/components/BlogAuthorFilter'
5-
import { getDistinctAuthors, getPublishedPosts } from '~/utils/blog'
5+
import { getPublishedPosts } from '~/utils/blog'
6+
import { getDistinctAuthors } from '~/utils/blog-format'
67

78
import { Footer } from '~/components/Footer'
89
import { PostNotFound } from './blog'

src/routes/rss[.]xml.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
import { createFileRoute } from '@tanstack/react-router'
22
import { setResponseHeader } from '@tanstack/react-start/server'
3-
import {
4-
getPublishedPosts,
5-
formatAuthors,
6-
publishedDateToUTCString,
7-
} from '~/utils/blog'
3+
import { getPublishedPosts } from '~/utils/blog'
4+
import { formatAuthors, publishedDateToUTCString } from '~/utils/blog-format'
85

96
function escapeXml(unsafe: string): string {
107
return unsafe

src/routes/stack.$category.tsx

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,33 @@ import { CategoryArticle } from '~/components/stack/CategoryArticle'
44
import {
55
categoryMeta,
66
categorySlugs,
7+
getCategoryLibraries,
78
type CategorySlug,
89
} from '~/components/stack/stack-categories'
10+
import { fetchRelatedPostsForLibraries } from '~/utils/blog.functions'
911
import { seo } from '~/utils/seo'
1012

1113
function isCategorySlug(value: string): value is CategorySlug {
1214
return (categorySlugs as readonly string[]).includes(value)
1315
}
1416

1517
export const Route = createFileRoute('/stack/$category')({
16-
loader: ({ params }) => {
18+
staleTime: Infinity,
19+
loader: async ({ params }) => {
1720
if (!isCategorySlug(params.category)) {
1821
throw notFound()
1922
}
20-
return { category: params.category, meta: categoryMeta[params.category] }
23+
24+
const libraries = getCategoryLibraries(params.category)
25+
const relatedPosts = await fetchRelatedPostsForLibraries({
26+
data: libraries.map((lib) => lib.id),
27+
})
28+
29+
return {
30+
category: params.category,
31+
meta: categoryMeta[params.category],
32+
relatedPosts,
33+
}
2134
},
2235
head: ({ loaderData }) => ({
2336
meta: seo({
@@ -31,6 +44,6 @@ export const Route = createFileRoute('/stack/$category')({
3144
})
3245

3346
function StackCategoryPage() {
34-
const { category } = Route.useLoaderData()
35-
return <CategoryArticle slug={category} />
47+
const { category, relatedPosts } = Route.useLoaderData()
48+
return <CategoryArticle slug={category} relatedPosts={relatedPosts} />
3649
}

src/utils/blog-format.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { findLibrary, type LibrarySlim } from '~/libraries'
2+
3+
const listJoiner = new Intl.ListFormat('en-US', {
4+
style: 'long',
5+
type: 'conjunction',
6+
})
7+
8+
export function formatAuthors(authors: Array<string>) {
9+
if (!authors.length) {
10+
return 'TanStack'
11+
}
12+
13+
return listJoiner.format(authors)
14+
}
15+
16+
function getUtcDateString(date = new Date()) {
17+
return date.toISOString().slice(0, 10)
18+
}
19+
20+
function parsePublishedDate(published: string) {
21+
const [year, month, day] = published.split('-').map(Number)
22+
23+
return new Date(Date.UTC(year, month - 1, day, 12))
24+
}
25+
26+
export function formatPublishedDate(published: string) {
27+
return parsePublishedDate(published).toLocaleDateString('en-US', {
28+
timeZone: 'UTC',
29+
year: 'numeric',
30+
month: 'short',
31+
day: 'numeric',
32+
})
33+
}
34+
35+
export function isPublishedDateReleased(published: string, now = new Date()) {
36+
return published <= getUtcDateString(now)
37+
}
38+
39+
export function publishedDateToUTCString(published: string) {
40+
return parsePublishedDate(published).toUTCString()
41+
}
42+
43+
function isLibrarySlim(
44+
library: LibrarySlim | undefined,
45+
): library is LibrarySlim {
46+
return library !== undefined
47+
}
48+
49+
export function getBlogLibraries(library: string | undefined): LibrarySlim[] {
50+
if (!library) {
51+
return []
52+
}
53+
54+
return library
55+
.split(',')
56+
.map((libraryId) => findLibrary(libraryId.trim()))
57+
.filter(isLibrarySlim)
58+
}
59+
60+
export function getDistinctAuthors(
61+
posts: ReadonlyArray<{ authors: string[] }>,
62+
): string[] {
63+
const authors = new Set<string>()
64+
for (const post of posts) {
65+
for (const author of post.authors) {
66+
authors.add(author)
67+
}
68+
}
69+
return [...authors].sort((a, b) => a.localeCompare(b))
70+
}

0 commit comments

Comments
 (0)