1
1
import fs from "fs" ;
2
2
import path from "path" ;
3
3
import { createContentLoader } from "vitepress" ;
4
+ import sharp from "sharp" ;
4
5
5
6
// Colors
6
7
const COLORS = {
@@ -24,13 +25,67 @@ const FONT_FACE = {
24
25
SECONDARY : "Roboto" ,
25
26
} ;
26
27
27
- // Image paths
28
+ // Image paths relative to project root
28
29
const IMAGES = {
29
- BACKGROUND : "../ open-graph-background.png",
30
- LOGO : "../ logo.png",
30
+ BACKGROUND : path . resolve ( process . cwd ( ) , "docs/public/images/ open-graph-background.png") ,
31
+ LOGO : path . resolve ( process . cwd ( ) , "docs/public/images/ logo.png") ,
31
32
} ;
32
33
33
- function buildOgImage ( { title, summary, pageUrl } ) {
34
+ // Function to convert image to base64
35
+ async function imageToBase64 ( imagePath ) {
36
+ try {
37
+ const imageBuffer = await fs . promises . readFile ( imagePath ) ;
38
+ return `data:image/png;base64,${ imageBuffer . toString ( 'base64' ) } ` ;
39
+ } catch ( error ) {
40
+ console . error ( `Error loading image ${ imagePath } :` , error ) ;
41
+ return null ;
42
+ }
43
+ }
44
+
45
+ // Text wrapping function
46
+ function wrapText ( text , maxWidth , fontSize ) {
47
+ const avgCharWidth = fontSize * 0.6 ;
48
+ const charsPerLine = Math . floor ( maxWidth / avgCharWidth ) ;
49
+ const words = text . split ( ' ' ) ;
50
+ const lines = [ ] ;
51
+ let currentLine = words [ 0 ] ;
52
+
53
+ for ( let i = 1 ; i < words . length ; i ++ ) {
54
+ if ( currentLine . length + words [ i ] . length + 1 <= charsPerLine ) {
55
+ currentLine += ' ' + words [ i ] ;
56
+ } else {
57
+ lines . push ( currentLine ) ;
58
+ currentLine = words [ i ] ;
59
+ }
60
+ }
61
+ lines . push ( currentLine ) ;
62
+
63
+ return lines ;
64
+ }
65
+
66
+ async function buildOgImage ( { title, summary, pageUrl } ) {
67
+ // Load images as base64
68
+ const backgroundBase64 = await imageToBase64 ( IMAGES . BACKGROUND ) ;
69
+ const logoBase64 = await imageToBase64 ( IMAGES . LOGO ) ;
70
+
71
+ if ( ! backgroundBase64 || ! logoBase64 ) {
72
+ throw new Error ( 'Failed to load required images' ) ;
73
+ }
74
+
75
+ // Calculate wrapped text
76
+ const titleLines = wrapText ( title , 1000 , parseInt ( FONT_SIZE . TITLE ) ) ;
77
+ const summaryLines = wrapText ( summary , 1100 , parseInt ( FONT_SIZE . SUMMARY ) ) ;
78
+
79
+ // Generate title tspans with 1.5 line height
80
+ const titleTspans = titleLines . map ( ( line , index ) =>
81
+ `<tspan x="50" dy="${ index === 0 ? '0' : '1.5em' } ">${ line } </tspan>`
82
+ ) . join ( '' ) ;
83
+
84
+ // Generate summary tspans with 1.5 line height
85
+ const summaryTspans = summaryLines . map ( ( line , index ) =>
86
+ `<tspan x="50" dy="${ index === 0 ? '0' : '1.5em' } ">${ line } </tspan>`
87
+ ) . join ( '' ) ;
88
+
34
89
return `
35
90
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 630">
36
91
<defs>
@@ -40,7 +95,7 @@ function buildOgImage({ title, summary, pageUrl }) {
40
95
</defs>
41
96
42
97
<image
43
- href="${ IMAGES . BACKGROUND } "
98
+ href="${ backgroundBase64 } "
44
99
width="1200"
45
100
height="630"
46
101
clip-path="url(#clip)"
@@ -55,7 +110,7 @@ function buildOgImage({ title, summary, pageUrl }) {
55
110
56
111
<g>
57
112
<image
58
- href="${ IMAGES . LOGO } "
113
+ href="${ logoBase64 } "
59
114
x="50"
60
115
y="55"
61
116
width="117"
@@ -83,14 +138,16 @@ function buildOgImage({ title, summary, pageUrl }) {
83
138
font-family="${ FONT_FACE . PRIMARY } "
84
139
font-weight="bold"
85
140
fill="${ COLORS . TEXT_PRIMARY } "
86
- >${ title } </text>
141
+ >${ titleTspans } </text>
142
+
87
143
<text
88
144
x="50"
89
145
y="400"
90
146
font-size="${ FONT_SIZE . SUMMARY } "
91
147
font-family="${ FONT_FACE . SECONDARY } "
92
148
fill="${ COLORS . TEXT_PRIMARY } "
93
- >${ summary } </text>
149
+ >${ summaryTspans } </text>
150
+
94
151
<text
95
152
x="50"
96
153
y="550"
@@ -103,6 +160,18 @@ function buildOgImage({ title, summary, pageUrl }) {
103
160
` ;
104
161
}
105
162
163
+ async function convertSvgToPng ( svgBuffer , outputPath ) {
164
+ try {
165
+ await sharp ( Buffer . from ( svgBuffer ) )
166
+ . png ( )
167
+ . toFile ( outputPath ) ;
168
+ return true ;
169
+ } catch ( error ) {
170
+ console . error ( 'Error converting SVG to PNG:' , error ) ;
171
+ return false ;
172
+ }
173
+ }
174
+
106
175
export const generateOgImages = async ( config ) => {
107
176
try {
108
177
// Get the output directory from VitePress config
@@ -119,7 +188,7 @@ export const generateOgImages = async (config) => {
119
188
try {
120
189
const relativePath = file . url . replace ( / ^ \/ / , "" ) + ".md" ;
121
190
122
- const svg = buildOgImage ( {
191
+ const svg = await buildOgImage ( {
123
192
title : file . frontmatter ?. title || "Shoko" ,
124
193
summary : file . frontmatter ?. description || "" ,
125
194
pageUrl : `https://docs.shokoanime.com${ file . url } ` ,
@@ -131,8 +200,11 @@ export const generateOgImages = async (config) => {
131
200
. replace ( / \s + / g, "-" )
132
201
. toLowerCase ( ) ;
133
202
134
- fs . writeFileSync ( path . join ( outputDir , `${ filename } .svg` ) , svg ) ;
135
- console . log ( `Generated OG image for ${ filename } ` ) ;
203
+ // Convert directly to PNG without saving SVG
204
+ const pngPath = path . join ( outputDir , `${ filename } .png` ) ;
205
+ await convertSvgToPng ( svg , pngPath ) ;
206
+
207
+ console . log ( `Generated PNG OG image for ${ filename } ` ) ;
136
208
} catch ( fileError ) {
137
209
console . error ( `Error processing file ${ file . url } :` , fileError ) ;
138
210
}
0 commit comments