@@ -3,86 +3,83 @@ import { createRequire } from 'node:module';
3
3
// biome-ignore lint/nursery/noRestrictedImports: ignore
4
4
import { resolve } from 'node:path' ;
5
5
6
- // We can't import sharp normally because it's a CJS thing and those don't seems to work well with Astro, Vite, everyone
7
6
const require = createRequire ( import . meta. url ) ;
8
7
// ts-expect-error TS80005
9
8
const sharp = require ( 'sharp' ) ;
10
9
11
10
const template = readFileSync ( resolve ( 'src/assets/og-template.svg' ) , 'utf-8' ) ;
12
11
13
- export function breakText ( str : string , maxLines : number , maxLineLen : number ) {
14
- const segmenterTitle = new Intl . Segmenter ( 'en-US' , { granularity : 'word' } ) ;
15
- const segments = segmenterTitle . segment ( str ) ;
16
-
17
- const linesOut = [ '' ] ;
18
- let lineNo = 0 ;
19
- let offsetInLine = 0 ;
20
- for ( const word of Array . from ( segments ) ) {
21
- if ( offsetInLine + word . segment . length >= maxLineLen ) {
22
- lineNo ++ ;
23
- offsetInLine = 0 ;
24
- linesOut . push ( '' ) ;
25
- }
26
-
27
- if ( lineNo >= maxLines ) {
28
- return linesOut . slice ( 0 , maxLines ) ;
29
- }
30
-
31
- linesOut [ lineNo ] += word . segment . replace ( / & / g, '&' ) . replace ( / < / g, '<' ) . replace ( / > / g, '>' ) ;
32
- offsetInLine += word . segment . length ;
33
- }
34
-
35
- return linesOut ;
36
- }
37
-
38
12
const getPages = async ( ) => {
39
13
const data = import . meta. glob ( [ '/src/content/**/*.{md,mdx}' ] , { eager : true } ) ;
40
14
const pages : Record < string , unknown > = { } ;
41
15
for ( const [ filePath , page ] of Object . entries ( data ) ) {
42
16
const imagePath = filePath . replace ( / ^ \/ s r c \/ c o n t e n t \/ / , '' ) . replace ( / ( \/ i n d e x ) ? \. ( m d | m d x ) $ / , '.webp' ) ;
43
17
pages [ imagePath ] = page ;
44
18
}
45
-
46
- pages [ 'docs/sponsors.webp' ] = {
47
- frontmatter : { title : 'Become a sponsor!' , description : 'Become a sponsor of Knip' } ,
48
- } ;
49
-
50
- pages [ 'docs/playground.webp' ] = {
51
- frontmatter : { title : 'Playground' , description : 'Try Knip in your browser' } ,
52
- } ;
53
-
54
19
return pages ;
55
20
} ;
56
21
57
- const renderSVG = ( { title } : { title : string ; description : string [ ] } ) => {
58
- const titleText = breakText ( title , 2 , 45 )
59
- . map ( ( text , i , texts ) => {
60
- const m = ( texts . length === 1 ? 0 : - 75 ) / 2 ;
61
- const y = ( 1000 + m + 150 * i ) / 2 ;
62
- const s = ( texts . length === 1 ? 150 : 96 ) / 2 ;
63
- return `<text font-size="${ s } " xml:space="preserve" fill="#fff"><tspan x="75" y="${ y } ">${ text } </tspan></text>` ;
64
- } )
65
- . join ( '\n' ) ;
22
+ const renderSVG = ( { title } : { title : string } ) => {
23
+ const lines = balanceText ( title , 30 ) ;
24
+
25
+ const titleText = `
26
+ <text
27
+ text-anchor="start"
28
+ text-rendering="optimizeLegibility"
29
+ font-size="${ lines . length === 1 ? 80 : 64 } "
30
+ fill="#fff"
31
+ x="75"
32
+ y="500"
33
+ >
34
+ ${ lines . map ( ( line , i ) => `<tspan x="75" dy="${ i === 0 ? '0' : '1.2em' } " >${ encodeXML ( line ) } </tspan>` ) . join ( '' ) }
35
+ </text>
36
+ ` ;
66
37
67
38
return template . replace ( '<!-- titleText -->' , titleText ) ;
68
39
} ;
69
40
41
+ function encodeXML ( text : string ) : string {
42
+ return text
43
+ . replace ( / & / g, '&' )
44
+ . replace ( / < / g, '<' )
45
+ . replace ( / > / g, '>' )
46
+ . replace ( / " / g, '"' )
47
+ . replace ( / ' / g, ''' ) ;
48
+ }
49
+
50
+ function balanceText ( text : string , maxLen : number ) : string [ ] {
51
+ const words = text . split ( ' ' ) ;
52
+ if ( words . join ( ' ' ) . length <= maxLen ) return [ text ] ;
53
+
54
+ let bestSplit = 0 ;
55
+ let bestDiff = Number . POSITIVE_INFINITY ;
56
+
57
+ for ( let i = 0 ; i < words . length - 1 ; i ++ ) {
58
+ const line1 = words . slice ( 0 , i + 1 ) . join ( ' ' ) ;
59
+ const line2 = words . slice ( i + 1 ) . join ( ' ' ) ;
60
+ const diff = Math . abs ( line1 . length - line2 . length ) ;
61
+ if ( diff < bestDiff ) {
62
+ bestDiff = diff ;
63
+ bestSplit = i + 1 ;
64
+ }
65
+ }
66
+
67
+ return [ words . slice ( 0 , bestSplit ) . join ( ' ' ) , words . slice ( bestSplit ) . join ( ' ' ) ] ;
68
+ }
69
+
70
70
export const GET = async ( { params } : { params : { route : string } } ) => {
71
71
const pages = await getPages ( ) ;
72
72
const pageEntry = pages [ params . route ] ;
73
73
if ( ! pageEntry ) return new Response ( 'Page not found' , { status : 404 } ) ;
74
74
75
- const svgBuffer = Buffer . from (
76
- renderSVG ( {
77
- // @ts -expect-error TODO type properly
78
- title : pageEntry . frontmatter . hero ?. tagline ?? pageEntry . frontmatter . title ,
79
- // @ts -expect-error TODO type properly
80
- description : pageEntry . frontmatter . description ,
81
- } )
82
- ) ;
83
- const body = await sharp ( svgBuffer ) . resize ( 1200 , 630 ) . png ( ) . toBuffer ( ) ;
75
+ // @ts -expect-error TODO type properly
76
+ const title = pageEntry . frontmatter . hero ?. tagline ?? pageEntry . frontmatter . title ;
77
+ const svgBuffer = Buffer . from ( renderSVG ( { title } ) ) ;
78
+ const body = await sharp ( svgBuffer ) . resize ( 1200 , 630 ) . webp ( { lossless : true } ) . toBuffer ( ) ;
79
+
84
80
return new Response ( body , {
85
81
headers : {
82
+ 'Content-Type' : 'image/webp' ,
86
83
'Cache-Control' : 'no-cache, no-store, must-revalidate' ,
87
84
Pragma : 'no-cache' ,
88
85
Expires : '0' ,
0 commit comments