22import { FreshContext } from "fresh" ;
33import { Resvg } from "npm:@resvg/resvg-js" ;
44
5+ function wrapAndCenterTitle ( title : string ) : string {
6+ if ( ! title ) return "" ;
7+
8+ const maxCharsPerLine = 30 ;
9+ const baseFontSize = 3472.221 ;
10+ const centerX = 25000 ;
11+ const baseY = 22125.972 ;
12+ const lineHeight = 3800 ;
13+
14+ let fontSize = baseFontSize ;
15+ if ( title . length > 40 ) {
16+ fontSize = baseFontSize * 0.6 ;
17+ } else if ( title . length > 30 ) {
18+ fontSize = baseFontSize * 0.75 ;
19+ }
20+
21+ const words = title . split ( " " ) ;
22+ const lines : string [ ] = [ ] ;
23+ let currentLine = "" ;
24+
25+ for ( const word of words ) {
26+ const testLine = currentLine ? `${ currentLine } ${ word } ` : word ;
27+ if ( testLine . length > maxCharsPerLine && currentLine ) {
28+ lines . push ( currentLine ) ;
29+ currentLine = word ;
30+ } else {
31+ currentLine = testLine ;
32+ }
33+ }
34+ if ( currentLine ) {
35+ lines . push ( currentLine ) ;
36+ }
37+
38+ if ( lines . length === 1 ) {
39+ return `<text x="${ centerX } px" y="${ baseY } px" style="font-family:'JetBrainsMono-Bold', 'JetBrains Mono', monospace;font-weight:700;font-size:${ fontSize } px;fill:#88b2d9;text-anchor:middle;">${
40+ escapeXml ( title )
41+ } </text>`;
42+ }
43+
44+ const adjustedLineHeight = lineHeight * ( fontSize / baseFontSize ) ;
45+ const totalHeight = ( lines . length - 1 ) * adjustedLineHeight ;
46+ const startY = baseY - ( totalHeight / 2 ) ;
47+
48+ const tspans = lines . map ( ( line , i ) => {
49+ const dy = i === 0 ? 0 : adjustedLineHeight ;
50+ return ` <tspan x="${ centerX } px" dy="${ dy } ">${ escapeXml ( line ) } </tspan>` ;
51+ } ) . join ( "\n" ) ;
52+
53+ return `<text x="${ centerX } px" y="${ startY } px" style="font-family:'JetBrainsMono-Bold', 'JetBrains Mono', monospace;font-weight:700;font-size:${ fontSize } px;fill:#88b2d9;text-anchor:middle;">
54+ ${ tspans }
55+ </text>` ;
56+ }
57+
58+ function escapeXml ( text : string ) : string {
59+ return text
60+ . replace ( / & / g, "&" )
61+ . replace ( / < / g, "<" )
62+ . replace ( / > / g, ">" )
63+ . replace ( / " / g, """ )
64+ . replace ( / ' / g, "'" ) ;
65+ }
66+
567export async function handler (
668 ctx : FreshContext ,
769) : Promise < Response > {
@@ -14,8 +76,12 @@ export async function handler(
1476 const coverPath = `${ Deno . cwd ( ) } /static/images/cover.svg` ;
1577 let svgContent = await Deno . readTextFile ( coverPath ) ;
1678
17- // Replace [[INSERT TITLE]] with the actual title (or empty string for homepage)
18- svgContent = svgContent . replace ( "[[INSERT TITLE]]" , title ) ;
79+ // Replace [[INSERT TITLE]] with wrapped and centered title
80+ const titleSvg = wrapAndCenterTitle ( title ) ;
81+ svgContent = svgContent . replace (
82+ / < t e x t x = " 2 5 0 0 0 p x " y = " 2 2 1 2 5 \. 9 7 2 p x " [ ^ > ] * > \[ \[ I N S E R T T I T L E \] \] < \/ t e x t > / ,
83+ titleSvg ,
84+ ) ;
1985
2086 // Convert SVG to PNG using resvg
2187 const resvg = new Resvg ( svgContent , {
0 commit comments