55 * LICENSE file in the root directory of this source tree.
66 */
77
8+ import fs from 'fs' ;
9+ import path from 'path' ;
810import { ImageResponse } from 'next/og' ;
9- import type { NextRequest } from 'next/server ' ;
11+ import type { NextApiRequest , NextApiResponse } from 'next' ;
1012
11- export const config = {
12- runtime : 'edge' ,
13- } ;
13+ // Satori can't read woff2, so the site fonts are also vendored as
14+ // TTF in public/fonts (converted from the woff2 files there).
15+ const fontsDir = path . join ( process . cwd ( ) , 'public' , 'fonts' ) ;
16+ const bold = fs . readFileSync (
17+ path . join ( fontsDir , 'Optimistic_Display_W_Bd.ttf' )
18+ ) ;
19+ const medium = fs . readFileSync (
20+ path . join ( fontsDir , 'Optimistic_Display_W_Md.ttf' )
21+ ) ;
1422
15- const SECTION_LABELS : Record < string , string > = {
16- learn : 'Learn React' ,
17- reference : 'API Reference' ,
18- community : 'Community' ,
19- blog : 'Blog' ,
20- } ;
23+ export default async function handler (
24+ req : NextApiRequest ,
25+ res : NextApiResponse
26+ ) {
27+ const title = String ( req . query . title ?? 'react.dev' ) . slice ( 0 , 80 ) ;
28+ const pagePath = String ( req . query . path ?? '' ) . slice ( 0 , 120 ) ;
29+ const footer = ( 'react.dev' + pagePath ) . toUpperCase ( ) ;
2130
22- // Satori doesn't support woff2, so the site fonts can't be reused here.
23- // Source Code Pro matches the code-styled titles used across the docs.
24- const fontPromise = fetch (
25- 'https://raw.githubusercontent.com/adobe-fonts/source-code-pro/release/TTF/SourceCodePro-Semibold.ttf'
26- ) . then ( ( res ) => res . arrayBuffer ( ) ) ;
27-
28- export default async function handler ( request : NextRequest ) {
29- const { searchParams} = new URL ( request . url ) ;
30- const title = searchParams . get ( 'title' ) ?. slice ( 0 , 80 ) ?? 'react.dev' ;
31- const section = searchParams . get ( 'section' ) ?? '' ;
32- const label = SECTION_LABELS [ section ] ?? 'react.dev' ;
33- const fontData = await fontPromise ;
34-
35- return new ImageResponse (
31+ const image = new ImageResponse (
3632 (
3733 < div
3834 style = { {
3935 width : '100%' ,
4036 height : '100%' ,
4137 display : 'flex' ,
4238 flexDirection : 'column' ,
43- justifyContent : 'center' ,
44- padding : '80px' ,
39+ padding : '72px 80px' ,
4540 backgroundColor : '#23272f' ,
41+ backgroundImage :
42+ 'radial-gradient(circle at 25% 30%, #343a46 0%, #23272f 55%)' ,
4643 } } >
4744 < div
4845 style = { {
4946 display : 'flex' ,
5047 alignItems : 'center' ,
5148 gap : '20px' ,
5249 } } >
53- < svg
54- width = "72"
55- height = "72"
56- viewBox = "-11.5 -10.23 23 20.46"
57- fill = "none" >
58- < circle cx = "0" cy = "0" r = "2.05" fill = "#58c4dc" />
50+ < svg width = "80" height = "72" viewBox = "-10.5 -9.45 21 18.9" fill = "none" >
51+ < circle cx = "0" cy = "0" r = "2" fill = "#58c4dc" />
5952 < g stroke = "#58c4dc" strokeWidth = "1" fill = "none" >
60- < ellipse rx = "11 " ry = "4.2 " />
61- < ellipse rx = "11 " ry = "4.2 " transform = "rotate(60)" />
62- < ellipse rx = "11 " ry = "4.2 " transform = "rotate(120)" />
53+ < ellipse rx = "10 " ry = "4.5 " />
54+ < ellipse rx = "10 " ry = "4.5 " transform = "rotate(60)" />
55+ < ellipse rx = "10 " ry = "4.5 " transform = "rotate(120)" />
6356 </ g >
6457 </ svg >
6558 < div
6659 style = { {
67- fontSize : 36 ,
68- color : '#99a1b3' ,
69- textTransform : 'uppercase' ,
70- letterSpacing : '0.1em' ,
60+ fontSize : 48 ,
61+ fontFamily : 'Optimistic Display Bold' ,
62+ color : '#f6f7f9' ,
7163 } } >
72- { label }
64+ React
7365 </ div >
7466 </ div >
7567 < div
7668 style = { {
77- marginTop : 48 ,
78- fontSize : title . length > 24 ? 64 : 96 ,
69+ marginTop : 'auto' ,
70+ marginBottom : 'auto' ,
71+ fontSize : title . length > 24 ? 72 : 96 ,
72+ fontFamily : 'Optimistic Display Bold' ,
7973 color : '#f6f7f9' ,
8074 lineHeight : 1.1 ,
8175 wordBreak : 'break-word' ,
@@ -84,11 +78,12 @@ export default async function handler(request: NextRequest) {
8478 </ div >
8579 < div
8680 style = { {
87- marginTop : 'auto' ,
88- fontSize : 32 ,
81+ fontSize : 28 ,
82+ fontFamily : 'Optimistic Display Medium' ,
8983 color : '#99a1b3' ,
84+ letterSpacing : '0.08em' ,
9085 } } >
91- react.dev
86+ { footer }
9287 </ div >
9388 </ div >
9489 ) ,
@@ -97,12 +92,23 @@ export default async function handler(request: NextRequest) {
9792 height : 630 ,
9893 fonts : [
9994 {
100- name : 'Source Code Pro ' ,
101- data : fontData ,
95+ name : 'Optimistic Display Bold ' ,
96+ data : bold ,
10297 style : 'normal' ,
103- weight : 600 ,
98+ weight : 700 ,
99+ } ,
100+ {
101+ name : 'Optimistic Display Medium' ,
102+ data : medium ,
103+ style : 'normal' ,
104+ weight : 500 ,
104105 } ,
105106 ] ,
106107 }
107108 ) ;
109+
110+ const buffer = Buffer . from ( await image . arrayBuffer ( ) ) ;
111+ res . setHeader ( 'Content-Type' , 'image/png' ) ;
112+ res . setHeader ( 'Cache-Control' , 'public, max-age=31536000, immutable' ) ;
113+ res . end ( buffer ) ;
108114}
0 commit comments