Skip to content

Commit c30141a

Browse files
committed
feat: add og images for patterns
1 parent ea7e640 commit c30141a

File tree

9 files changed

+227
-8
lines changed

9 files changed

+227
-8
lines changed

app/[lang]/[[...mdxPath]]/page.tsx

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,40 @@
22

33
import { SuggestPattern } from '@/app/_components/suggest-pattern'
44
import { JsonLd, generateArticleSchema } from '@app/_components/json-ld'
5+
import { Metadata } from 'next'
56
import { generateStaticParamsFor, importPage } from 'nextra/pages'
67
import { useMDXComponents } from '../../../mdx-components'
78

89
export const generateStaticParams = generateStaticParamsFor('mdxPath')
910

10-
export async function generateMetadata(props: PageProps) {
11-
const params = await props.params
12-
const { metadata } = await importPage(params.mdxPath, params.lang)
13-
return metadata
11+
export async function generateMetadata(props: PageProps): Promise<Metadata | null> {
12+
try {
13+
const params = await props.params
14+
const { metadata } = await importPage(params.mdxPath || [], params.lang)
15+
16+
// Check if this is the homepage
17+
const isHomepage = !params.mdxPath || params.mdxPath.length === 0
18+
19+
const ogImage = {
20+
url: isHomepage
21+
? '/og/opengraph-image.png'
22+
: `/api/og?title=${encodeURIComponent(metadata.title || '')}`,
23+
width: 1200,
24+
height: 630,
25+
type: 'image/png',
26+
}
27+
28+
return {
29+
...metadata,
30+
openGraph: {
31+
...metadata.openGraph,
32+
images: [ogImage]
33+
}
34+
}
35+
} catch (e) {
36+
console.error('Error generating metadata:', e)
37+
return null
38+
}
1439
}
1540

1641
type PageProps = Readonly<{
@@ -23,7 +48,6 @@ type PageProps = Readonly<{
2348
const Wrapper = useMDXComponents().wrapper
2449

2550
export default async function Page(props: PageProps) {
26-
2751
const params = await props.params
2852
const result = await importPage(params.mdxPath, params.lang)
2953
const { default: MDXContent, toc, metadata } = result

app/[lang]/opengraph-image/route.tsx

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { ImageResponse } from 'next/og'
2+
import { NextRequest } from 'next/server'
3+
4+
export const runtime = 'edge'
5+
export const contentType = 'image/png'
6+
export const size = {
7+
width: 1200,
8+
height: 630,
9+
}
10+
11+
export async function GET(req: NextRequest) {
12+
try {
13+
// Get the language from the URL
14+
const lang = req.nextUrl.pathname.split('/')[1] || 'en'
15+
16+
// Get the base URL from the request
17+
const protocol = req.headers.get('x-forwarded-proto') || 'http'
18+
const host = req.headers.get('host') || 'localhost:3000'
19+
20+
// Use the direct path to the image in the public directory
21+
const imageUrl = `${protocol}://${host}/og/opengraph-image.png`
22+
23+
// Fetch the image
24+
const imageResponse = await fetch(imageUrl)
25+
if (!imageResponse.ok) {
26+
throw new Error(`Failed to fetch image: ${imageResponse.status}`)
27+
}
28+
29+
const imageBuffer = await imageResponse.arrayBuffer()
30+
const base64Image = Buffer.from(imageBuffer).toString('base64')
31+
const dataUrl = `data:${imageResponse.headers.get('content-type') || 'image/png'};base64,${base64Image}`
32+
33+
return new ImageResponse(
34+
(
35+
<div
36+
style={{
37+
height: '100%',
38+
width: '100%',
39+
display: 'flex',
40+
flexDirection: 'column',
41+
alignItems: 'center',
42+
justifyContent: 'flex-end',
43+
position: 'relative',
44+
overflow: 'hidden',
45+
backgroundColor: '#000',
46+
}}
47+
>
48+
<img
49+
src={dataUrl}
50+
alt=""
51+
style={{
52+
position: 'absolute',
53+
top: 0,
54+
left: 0,
55+
width: '100%',
56+
height: '100%',
57+
objectFit: 'cover',
58+
}}
59+
/>
60+
</div>
61+
),
62+
{
63+
...size,
64+
headers: {
65+
'Cache-Control': 'public, max-age=31536000, immutable'
66+
}
67+
}
68+
)
69+
} catch (e) {
70+
console.error('Error generating image:', e)
71+
return new Response(`Failed to generate image: ${e.message}`, {
72+
status: 500,
73+
})
74+
}
75+
}

app/api/og/route.tsx

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { ImageResponse } from 'next/og'
2+
import { NextRequest } from 'next/server'
3+
4+
export const runtime = 'edge'
5+
6+
export async function GET(req: NextRequest) {
7+
try {
8+
const { searchParams } = new URL(req.url)
9+
const title = searchParams.get('title')
10+
11+
// Get the language from the Accept-Language header or default to 'en'
12+
const acceptLanguage = req.headers.get('accept-language') || ''
13+
const lang = acceptLanguage.includes('fr') ? 'fr' : 'en'
14+
15+
// Get the base URL from the request
16+
const protocol = req.headers.get('x-forwarded-proto') || 'http'
17+
const host = req.headers.get('host') || 'localhost:3000'
18+
19+
// Use language-specific path to the image
20+
const imageUrl = `${protocol}://${host}/${lang}/og/opengraph-image.png`
21+
22+
// Fetch the image
23+
const imageResponse = await fetch(imageUrl, {
24+
headers: {
25+
'Cache-Control': 'public, max-age=31536000, immutable'
26+
}
27+
})
28+
29+
if (!imageResponse.ok) {
30+
throw new Error(`Failed to fetch image: ${imageResponse.status}`)
31+
}
32+
33+
const imageBuffer = await imageResponse.arrayBuffer()
34+
const base64Image = Buffer.from(imageBuffer).toString('base64')
35+
const dataUrl = `data:${imageResponse.headers.get('content-type') || 'image/png'};base64,${base64Image}`
36+
37+
return new ImageResponse(
38+
(
39+
<div
40+
style={{
41+
height: '100%',
42+
width: '100%',
43+
display: 'flex',
44+
flexDirection: 'column',
45+
alignItems: 'center',
46+
justifyContent: 'flex-end',
47+
position: 'relative',
48+
overflow: 'hidden',
49+
backgroundColor: '#000',
50+
}}
51+
>
52+
<img
53+
src={dataUrl}
54+
alt=""
55+
style={{
56+
position: 'absolute',
57+
top: 0,
58+
left: 0,
59+
width: '100%',
60+
height: '100%',
61+
objectFit: 'cover',
62+
}}
63+
/>
64+
{title && (
65+
<div
66+
style={{
67+
position: 'relative',
68+
fontSize: 45,
69+
fontWeight: 'normal',
70+
color: 'white',
71+
textAlign: 'center',
72+
maxWidth: '80%',
73+
marginBottom: 150,
74+
}}
75+
>
76+
{title}
77+
</div>
78+
)}
79+
</div>
80+
),
81+
{
82+
width: 1200,
83+
height: 630,
84+
headers: {
85+
'Cache-Control': 'public, max-age=31536000, immutable'
86+
}
87+
}
88+
)
89+
} catch (e) {
90+
console.error('Error generating image:', e)
91+
return new Response(`Failed to generate image: ${e.message}`, {
92+
status: 500,
93+
})
94+
}
95+
}
96+

app/metadata.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,15 @@ export const metadataSEO: Metadata = {
2323
openGraph: {
2424
siteName: "UX Patterns for Devs",
2525
type: "website",
26-
locale: "en_US"
26+
locale: "en_US",
27+
title: "UX Patterns for Devs",
28+
description: "UX Patterns for Devs is a collection of UX Patterns for Devs to use in their projects.",
29+
images: [{
30+
url: '/og/opengraph-image.png',
31+
width: 1200,
32+
height: 630,
33+
type: 'image/png',
34+
}]
2735
},
2836
twitter: {
2937
card: 'summary_large_image',

app/twitter-image.png

-13.7 KB
Binary file not shown.

middleware.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@ export { middleware } from 'nextra/locales'
33
export const config = {
44
// Matcher ignoring `/_next/` and `/api/`
55
matcher: [
6-
'/((?!api/mdx|api/patterns/random|_next/static|_next/image|favicon.ico|robots.txt|opengraph-image.png|twitter-image.png|sitemap.xml|apple-icon.png|manifest|_pagefind).*)'
6+
'/((?!api/mdx|api/patterns/random|api/og|_next/static|_next/image|favicon.ico|robots.txt|og/opengraph-image.png|twitter-image|sitemap.xml|apple-icon.png|manifest|_pagefind).*)'
77
]
88
}

next.config.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,29 @@ const nextConfig = withBundleAnalyzer(
2323
locales: ['en'],
2424
defaultLocale: 'en'
2525
},
26+
images: {
27+
remotePatterns: [
28+
{
29+
protocol: 'https',
30+
hostname: '**',
31+
},
32+
],
33+
},
2634
redirects: async () => [
2735
{
2836
source: '/patterns',
2937
destination: '/patterns/getting-started',
3038
statusCode: 302
3139
}
32-
]
40+
],
41+
async rewrites() {
42+
return [
43+
{
44+
source: '/og/:slug',
45+
destination: '/api/og?title=:slug',
46+
},
47+
]
48+
},
3349
})
3450
)
3551

public/en/og/opengraph-image.png

37.1 KB
Loading

public/og/opengraph-image.png

-22.4 KB
Loading

0 commit comments

Comments
 (0)