Skip to content

Commit 768b847

Browse files
committed
feat: blog page og image
1 parent 7e4ed49 commit 768b847

File tree

9 files changed

+221
-15
lines changed

9 files changed

+221
-15
lines changed

.npmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
public-hoist-pattern=@takumi-rs/core-*

netlify.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,6 @@
66
publish = "dist/client"
77

88
[functions]
9+
node_bundler = "esbuild"
910
directory = "netlify/functions"
11+
included_files = ["node_modules/@takumi-rs/core-linux-x64-gnu/**/*.*"]

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"@sentry/vite-plugin": "^2.22.6",
3636
"@tailwindcss/typography": "^0.5.13",
3737
"@tailwindcss/vite": "^4.1.11",
38+
"@takumi-rs/image-response": "^0.62.6",
3839
"@tanstack/pacer": "^0.16.4",
3940
"@tanstack/react-pacer": "^0.17.4",
4041
"@tanstack/react-query": "^5.90.12",

pnpm-lock.yaml

Lines changed: 109 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/og/BlogImage.tsx

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { Post } from 'content-collections'
2+
import { formatAuthors } from '~/utils/blog'
3+
4+
export function BlogImage({
5+
title,
6+
description,
7+
headerImage,
8+
authors,
9+
published,
10+
}: Post) {
11+
return (
12+
<div
13+
tw="w-full h-full bg-cover bg-center bg-no-repeat bg-[#171717]"
14+
style={{
15+
backgroundImage: headerImage
16+
? `url(https://tanstack.com${headerImage})`
17+
: 'none',
18+
}}
19+
>
20+
<div tw="w-full h-full flex-col p-16 text-pretty backdrop-blur-md backdrop-brightness-30">
21+
<img
22+
src="https://tanstack.com/images/logos/splash-dark.png"
23+
tw="w-24"
24+
/>
25+
<div tw="grow" />
26+
<p tw="text-white text-6xl mb-4 font-bold line-clamp-2 text-ellipsis">
27+
{title}
28+
</p>
29+
<p tw="text-white/85 text-4xl font-medium line-clamp-2 text-ellipsis mb-6">
30+
{description}
31+
</p>
32+
<div tw="flex items-center gap-2">
33+
<p tw="text-white/85 text-2xl font-medium uppercase tracking-wider font-mono">
34+
by {formatAuthors(authors)} on {published}
35+
</p>
36+
</div>
37+
</div>
38+
</div>
39+
)
40+
}

src/routeTree.gen.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ import { Route as LibrariesDbVersionIndexRouteImport } from './routes/_libraries
8989
import { Route as LibrariesConfigVersionIndexRouteImport } from './routes/_libraries/config.$version.index'
9090
import { Route as LibrariesAiVersionIndexRouteImport } from './routes/_libraries/ai.$version.index'
9191
import { Route as LibraryIdVersionDocsIndexRouteImport } from './routes/$libraryId/$version.docs.index'
92+
import { Route as ApiOgBlogSplatRouteImport } from './routes/api/og/blog.$'
9293
import { Route as ApiAuthCallbackProviderRouteImport } from './routes/api/auth/callback/$provider'
9394
import { Route as LibraryIdVersionDocsChar123Char125DotmdRouteImport } from './routes/$libraryId/$version.docs.{$}[.]md'
9495
import { Route as LibraryIdVersionDocsContributorsRouteImport } from './routes/$libraryId/$version.docs.contributors'
@@ -513,6 +514,11 @@ const LibraryIdVersionDocsIndexRoute =
513514
path: '/',
514515
getParentRoute: () => LibraryIdVersionDocsRoute,
515516
} as any)
517+
const ApiOgBlogSplatRoute = ApiOgBlogSplatRouteImport.update({
518+
id: '/api/og/blog/$',
519+
path: '/api/og/blog/$',
520+
getParentRoute: () => rootRouteImport,
521+
} as any)
516522
const ApiAuthCallbackProviderRoute = ApiAuthCallbackProviderRouteImport.update({
517523
id: '/api/auth/callback/$provider',
518524
path: '/api/auth/callback/$provider',
@@ -644,6 +650,7 @@ export interface FileRoutesByFullPath {
644650
'/$libraryId/$version/docs/contributors': typeof LibraryIdVersionDocsContributorsRoute
645651
'/$libraryId/$version/docs/{$}.md': typeof LibraryIdVersionDocsChar123Char125DotmdRoute
646652
'/api/auth/callback/$provider': typeof ApiAuthCallbackProviderRoute
653+
'/api/og/blog/$': typeof ApiOgBlogSplatRoute
647654
'/$libraryId/$version/docs/': typeof LibraryIdVersionDocsIndexRoute
648655
'/ai/$version': typeof LibrariesAiVersionIndexRoute
649656
'/config/$version': typeof LibrariesConfigVersionIndexRoute
@@ -730,6 +737,7 @@ export interface FileRoutesByTo {
730737
'/$libraryId/$version/docs/contributors': typeof LibraryIdVersionDocsContributorsRoute
731738
'/$libraryId/$version/docs/{$}.md': typeof LibraryIdVersionDocsChar123Char125DotmdRoute
732739
'/api/auth/callback/$provider': typeof ApiAuthCallbackProviderRoute
740+
'/api/og/blog/$': typeof ApiOgBlogSplatRoute
733741
'/$libraryId/$version/docs': typeof LibraryIdVersionDocsIndexRoute
734742
'/ai/$version': typeof LibrariesAiVersionIndexRoute
735743
'/config/$version': typeof LibrariesConfigVersionIndexRoute
@@ -823,6 +831,7 @@ export interface FileRoutesById {
823831
'/$libraryId/$version/docs/contributors': typeof LibraryIdVersionDocsContributorsRoute
824832
'/$libraryId/$version/docs/{$}.md': typeof LibraryIdVersionDocsChar123Char125DotmdRoute
825833
'/api/auth/callback/$provider': typeof ApiAuthCallbackProviderRoute
834+
'/api/og/blog/$': typeof ApiOgBlogSplatRoute
826835
'/$libraryId/$version/docs/': typeof LibraryIdVersionDocsIndexRoute
827836
'/_libraries/ai/$version/': typeof LibrariesAiVersionIndexRoute
828837
'/_libraries/config/$version/': typeof LibrariesConfigVersionIndexRoute
@@ -916,6 +925,7 @@ export interface FileRouteTypes {
916925
| '/$libraryId/$version/docs/contributors'
917926
| '/$libraryId/$version/docs/{$}.md'
918927
| '/api/auth/callback/$provider'
928+
| '/api/og/blog/$'
919929
| '/$libraryId/$version/docs/'
920930
| '/ai/$version'
921931
| '/config/$version'
@@ -1002,6 +1012,7 @@ export interface FileRouteTypes {
10021012
| '/$libraryId/$version/docs/contributors'
10031013
| '/$libraryId/$version/docs/{$}.md'
10041014
| '/api/auth/callback/$provider'
1015+
| '/api/og/blog/$'
10051016
| '/$libraryId/$version/docs'
10061017
| '/ai/$version'
10071018
| '/config/$version'
@@ -1094,6 +1105,7 @@ export interface FileRouteTypes {
10941105
| '/$libraryId/$version/docs/contributors'
10951106
| '/$libraryId/$version/docs/{$}.md'
10961107
| '/api/auth/callback/$provider'
1108+
| '/api/og/blog/$'
10971109
| '/$libraryId/$version/docs/'
10981110
| '/_libraries/ai/$version/'
10991111
| '/_libraries/config/$version/'
@@ -1137,6 +1149,7 @@ export interface RootRouteChildren {
11371149
StatsNpmPackagesRoute: typeof StatsNpmPackagesRoute
11381150
StatsNpmIndexRoute: typeof StatsNpmIndexRoute
11391151
ApiAuthCallbackProviderRoute: typeof ApiAuthCallbackProviderRoute
1152+
ApiOgBlogSplatRoute: typeof ApiOgBlogSplatRoute
11401153
}
11411154

11421155
declare module '@tanstack/react-router' {
@@ -1701,6 +1714,13 @@ declare module '@tanstack/react-router' {
17011714
preLoaderRoute: typeof LibraryIdVersionDocsIndexRouteImport
17021715
parentRoute: typeof LibraryIdVersionDocsRoute
17031716
}
1717+
'/api/og/blog/$': {
1718+
id: '/api/og/blog/$'
1719+
path: '/api/og/blog/$'
1720+
fullPath: '/api/og/blog/$'
1721+
preLoaderRoute: typeof ApiOgBlogSplatRouteImport
1722+
parentRoute: typeof rootRouteImport
1723+
}
17041724
'/api/auth/callback/$provider': {
17051725
id: '/api/auth/callback/$provider'
17061726
path: '/api/auth/callback/$provider'
@@ -2018,6 +2038,7 @@ const rootRouteChildren: RootRouteChildren = {
20182038
StatsNpmPackagesRoute: StatsNpmPackagesRoute,
20192039
StatsNpmIndexRoute: StatsNpmIndexRoute,
20202040
ApiAuthCallbackProviderRoute: ApiAuthCallbackProviderRoute,
2041+
ApiOgBlogSplatRoute: ApiOgBlogSplatRoute,
20212042
}
20222043
export const routeTree = rootRouteImport
20232044
._addFileChildren(rootRouteChildren)

src/routes/_libraries/blog.$.tsx

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -62,25 +62,14 @@ const fetchBlogPost = createServerFn({ method: 'GET' })
6262
export const Route = createFileRoute('/_libraries/blog/$')({
6363
staleTime: Infinity,
6464
loader: ({ params }) => fetchBlogPost({ data: params._splat }),
65-
head: ({ loaderData }) => {
66-
// Generate optimized social media image URL using Netlify Image CDN
67-
const getSocialImageUrl = (headerImage?: string) => {
68-
if (!headerImage) return undefined
69-
70-
// Use Netlify Image CDN to optimize for social media (1200x630 is the standard for og:image)
71-
const netlifyImageUrl = `https://tanstack.com/.netlify/images?url=${encodeURIComponent(
72-
headerImage,
73-
)}&w=1200&h=630&fit=cover&fm=jpg&q=80`
74-
return netlifyImageUrl
75-
}
76-
65+
head: ({ loaderData, params }) => {
7766
return {
7867
meta: loaderData
7968
? [
8069
...seo({
8170
title: `${loaderData?.title ?? 'Docs'} | TanStack Blog`,
8271
description: loaderData?.description,
83-
image: getSocialImageUrl(loaderData?.headerImage),
72+
image: `/api/og/blog/${params._splat}`,
8473
}),
8574
{
8675
name: 'author',

src/routes/api/og/blog.$.tsx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { createFileRoute, notFound } from '@tanstack/react-router'
2+
import { ImageResponse, ImageResponseOptions } from '@takumi-rs/image-response'
3+
import { BlogImage } from '~/components/og/BlogImage'
4+
import { allPosts } from 'content-collections'
5+
6+
function handleRedirects(docsPath: string) {
7+
if (docsPath.includes('directives-the-new-framework-lock-in')) {
8+
return 'directives-and-the-platform-boundary'
9+
}
10+
11+
return docsPath
12+
}
13+
14+
export const Route = createFileRoute('/api/og/blog/$')({
15+
server: {
16+
handlers: {
17+
GET: async ({ params }) => {
18+
if (!params._splat) {
19+
throw notFound()
20+
}
21+
22+
const slug = handleRedirects(params._splat)
23+
24+
const post = allPosts.find((post) => post.slug === slug)
25+
26+
if (!post) {
27+
throw notFound()
28+
}
29+
30+
return new ImageResponse(<BlogImage {...post} />, {
31+
width: 1200,
32+
height: 630,
33+
format: 'webp',
34+
headers: {
35+
'Cache-Control': 'public, max-age=0, must-revalidate',
36+
'Netlify-CDN-Cache-Control':
37+
'public, max-age=300, durable, stale-while-revalidate=300',
38+
},
39+
})
40+
},
41+
},
42+
},
43+
})

vite.config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ export default defineConfig({
1414
},
1515
ssr: {
1616
external: ['postgres'],
17-
noExternal: ['drizzle-orm'],
17+
noExternal: ['drizzle-orm', '@takumi-rs/image-response', '@takumi-rs/core'],
1818
},
1919
optimizeDeps: {
20-
exclude: ['postgres'],
20+
exclude: ['postgres', '@takumi-rs/core'],
2121
},
2222
build: {
2323
rollupOptions: {

0 commit comments

Comments
 (0)