1
+ import { CustomizationHeaderPreset } from '@gitbook/api' ;
1
2
import { redirect } from 'next/navigation' ;
2
3
import { ImageResponse } from 'next/og' ;
3
4
import { NextRequest } from 'next/server' ;
5
+ import colorContrast from 'postcss-color-contrast/js' ;
4
6
import React from 'react' ;
5
7
8
+ import { absoluteHref } from '@/lib/links' ;
9
+ import { tcls } from '@/lib/tailwind' ;
6
10
import { getContentTitle } from '@/lib/utils' ;
7
11
8
12
import { PageIdParams , fetchPageData } from '../../../../fetch' ;
@@ -14,40 +18,206 @@ export const runtime = 'edge';
14
18
*/
15
19
export async function GET ( req : NextRequest , { params } : { params : PageIdParams } ) {
16
20
const { space, page, customization, site } = await fetchPageData ( params ) ;
17
- const url = new URL ( space . urls . published ?? space . urls . app ) ;
18
21
19
22
if ( customization . socialPreview . url ) {
20
23
// If user configured a custom social preview, we redirect to it.
21
24
redirect ( customization . socialPreview . url ) ;
22
25
}
23
26
27
+ // TODO: Support all fonts available in GitBook
28
+ // Right now this is impossible since next/font/google does not expose the cached font file
29
+ // Another option would be to use the Satori prop `loadAdditionalAsset` [example](https://github.com/vercel/satori/blob/main/playground/pages/index.tsx),
30
+ // but this prop isn't (yet) exposed through `ImageResponse`.
31
+ const interRegular = await fetch (
32
+ new URL ( '../../../../../../fonts/Inter/Inter-Regular.ttf' , import . meta. url ) ,
33
+ ) . then ( ( res ) => res . arrayBuffer ( ) ) ;
34
+ const interBold = await fetch (
35
+ new URL ( '../../../../../../fonts/Inter/Inter-Bold.ttf' , import . meta. url ) ,
36
+ ) . then ( ( res ) => res . arrayBuffer ( ) ) ;
37
+
38
+ const theme = customization . themes . default ;
39
+ const useLightTheme = theme === 'light' ;
40
+
41
+ // We have no access to CSS variables, so we'll have to hardcode some values
42
+ const baseColors = {
43
+ light : '#ffffff' ,
44
+ dark : '#111827' ,
45
+ } ;
46
+
47
+ let colors = {
48
+ background : baseColors [ theme ] ,
49
+ gradient : customization . styling . primaryColor [ theme ] ,
50
+ title : customization . styling . primaryColor [ theme ] ,
51
+ body : baseColors [ useLightTheme ? 'dark' : 'light' ] , // Invert text on background
52
+ } ;
53
+
54
+ const gridWhite = absoluteHref ( '~gitbook/static/images/ogimage-grid-white.png' , true ) ;
55
+ const gridBlack = absoluteHref ( '~gitbook/static/images/ogimage-grid-black.png' , true ) ;
56
+
57
+ let gridAsset = useLightTheme ? gridBlack : gridWhite ;
58
+
59
+ switch ( customization . header . preset ) {
60
+ case CustomizationHeaderPreset . Custom :
61
+ colors = {
62
+ background : customization . header . backgroundColor ?. [ theme ] || colors . background ,
63
+ gradient : customization . header . linkColor ?. [ theme ] || colors . gradient ,
64
+ title : customization . header . linkColor ?. [ theme ] || colors . title ,
65
+ body : colorContrast (
66
+ customization . header . backgroundColor ?. [ theme ] || colors . background ,
67
+ [ baseColors . light , baseColors . dark ] ,
68
+ ) ,
69
+ } ;
70
+ gridAsset = colors . body == baseColors . light ? gridWhite : gridBlack ;
71
+ break ;
72
+
73
+ case CustomizationHeaderPreset . Bold :
74
+ colors = {
75
+ background : customization . styling . primaryColor [ theme ] ,
76
+ gradient : colorContrast ( customization . styling . primaryColor [ theme ] , [
77
+ baseColors . light ,
78
+ baseColors . dark ,
79
+ ] ) ,
80
+ title : colorContrast ( customization . styling . primaryColor [ theme ] , [
81
+ baseColors . light ,
82
+ baseColors . dark ,
83
+ ] ) ,
84
+ body : colorContrast ( customization . styling . primaryColor [ theme ] , [
85
+ baseColors . light ,
86
+ baseColors . dark ,
87
+ ] ) ,
88
+ } ;
89
+ gridAsset = colors . body == baseColors . light ? gridWhite : gridBlack ;
90
+ break ;
91
+ }
92
+
93
+ const favicon = function ( ) {
94
+ if ( 'icon' in customization . favicon )
95
+ return (
96
+ < img
97
+ src = { customization . favicon . icon [ theme ] }
98
+ width = { 40 }
99
+ height = { 40 }
100
+ tw = { tcls ( 'mr-4' ) }
101
+ alt = "Icon"
102
+ />
103
+ ) ;
104
+ if ( 'emoji' in customization . favicon )
105
+ return (
106
+ < span tw = { tcls ( 'text-4xl' , 'mr-4' ) } >
107
+ { String . fromCodePoint ( parseInt ( '0x' + customization . favicon . emoji ) ) }
108
+ </ span >
109
+ ) ;
110
+ return (
111
+ < img
112
+ src = { absoluteHref (
113
+ `~gitbook/icon?size=medium&theme=${ customization . themes . default } ` ,
114
+ true ,
115
+ ) }
116
+ alt = "Icon"
117
+ width = { 40 }
118
+ height = { 40 }
119
+ tw = { tcls ( 'mr-4' ) }
120
+ />
121
+ ) ;
122
+ } ;
123
+
24
124
return new ImageResponse (
25
125
(
26
126
< div
27
- tw = "bg-gray-50 py-16 px-14"
28
- style = { {
29
- height : '100%' ,
30
- width : '100%' ,
31
- display : 'flex' ,
32
- flexDirection : 'column' ,
33
- } }
127
+ tw = { tcls (
128
+ 'justify-between' ,
129
+ 'p-20' ,
130
+ 'relative' ,
131
+ 'w-full' ,
132
+ 'h-full' ,
133
+ 'flex' ,
134
+ 'flex-col' ,
135
+ `bg-[${ colors . background } ]` ,
136
+ `text-[${ colors . body } ]` ,
137
+ ) }
34
138
>
35
- < h2 tw = "text-7xl font-bold tracking-tight text-left" >
36
- { getContentTitle ( space , customization , site ?? null ) }
37
- </ h2 >
38
- < div tw = "flex flex-1" >
39
- < p tw = "text-4xl" > { page ? page . title : 'Not found' } </ p >
40
- </ div >
41
- < div tw = "flex" >
42
- < p tw = "text-4xl" >
43
- { url . hostname + ( url . pathname . length > 1 ? url . pathname : '' ) }
44
- </ p >
139
+ { /* Gradient */ }
140
+ < div
141
+ tw = { tcls ( 'absolute' , 'inset-0' ) }
142
+ style = { {
143
+ backgroundImage : `radial-gradient(ellipse 100% 100% at top right , ${ colors . gradient } , ${ colors . gradient } 00)` ,
144
+ opacity : 0.5 ,
145
+ } }
146
+ > </ div >
147
+
148
+ { /* Grid */ }
149
+ < img
150
+ tw = { tcls ( 'absolute' , 'inset-0' , 'w-[100vw]' , 'h-[100vh]' ) }
151
+ src = { gridAsset }
152
+ alt = "Grid"
153
+ />
154
+
155
+ { /* Logo */ }
156
+ { customization . header . logo ? (
157
+ < img
158
+ alt = "Logo"
159
+ height = { 60 }
160
+ src = {
161
+ useLightTheme
162
+ ? customization . header . logo . light
163
+ : customization . header . logo . dark
164
+ }
165
+ />
166
+ ) : (
167
+ < div tw = { tcls ( 'flex' ) } >
168
+ { favicon ( ) }
169
+ < h3 tw = { tcls ( 'text-4xl' , 'my-0' ) } >
170
+ { getContentTitle ( space , customization , site ?? null ) }
171
+ </ h3 >
172
+ </ div >
173
+ ) }
174
+
175
+ { /* Title and description */ }
176
+ < div tw = { tcls ( 'flex' , 'flex-col' ) } >
177
+ < h1
178
+ tw = { tcls (
179
+ 'text-8xl' ,
180
+ 'my-0' ,
181
+ 'tracking-tight' ,
182
+ 'leading-none' ,
183
+ 'text-left' ,
184
+ `text-[${ colors . title } ]` ,
185
+ 'font-bold' ,
186
+ ) }
187
+ >
188
+ { page
189
+ ? page . title . length > 64
190
+ ? page . title . slice ( 0 , 64 ) + '...'
191
+ : page . title
192
+ : 'Not found' }
193
+ </ h1 >
194
+ { page ?. description && page ?. title . length <= 64 ? (
195
+ < h2 tw = { tcls ( 'text-4xl' , 'mb-0' , 'mt-8' , 'w-[75%]' , 'font-normal' ) } >
196
+ { page . description . length > 164
197
+ ? page . description . slice ( 0 , 164 ) + '...'
198
+ : page . description }
199
+ </ h2 >
200
+ ) : null }
45
201
</ div >
46
202
</ div >
47
203
) ,
48
204
{
49
205
width : 1200 ,
50
206
height : 630 ,
207
+ fonts : [
208
+ {
209
+ name : 'Inter' ,
210
+ data : interRegular ,
211
+ weight : 400 ,
212
+ style : 'normal' ,
213
+ } ,
214
+ {
215
+ name : 'Inter' ,
216
+ data : interBold ,
217
+ weight : 700 ,
218
+ style : 'normal' ,
219
+ } ,
220
+ ] ,
51
221
} ,
52
222
) ;
53
223
}
0 commit comments