Skip to content

Commit af3d010

Browse files
committed
feat: align text for generated image
1 parent 2f3568f commit af3d010

3 files changed

Lines changed: 74 additions & 7 deletions

File tree

routes/og.ts

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,68 @@
22
import { FreshContext } from "fresh";
33
import { 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, "&amp;")
61+
.replace(/</g, "&lt;")
62+
.replace(/>/g, "&gt;")
63+
.replace(/"/g, "&quot;")
64+
.replace(/'/g, "&apos;");
65+
}
66+
567
export 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+
/<text x="25000px" y="22125\.972px"[^>]*>\[\[INSERT TITLE\]\]<\/text>/,
83+
titleSvg,
84+
);
1985

2086
// Convert SVG to PNG using resvg
2187
const resvg = new Resvg(svgContent, {

static/images/cover.svg

Lines changed: 1 addition & 1 deletion
Loading

static/sw.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// deno-lint-ignore-file no-unused-vars
2-
const CACHE_VERSION = "v29"; // Increment this when you have new features
2+
const CACHE_VERSION = "v30"; // Increment this when you have new features
33
// const STATIC_CACHE = `andromeda-static-${CACHE_VERSION}`;
44
const DYNAMIC_CACHE = `andromeda-dynamic-${CACHE_VERSION}`;
55

@@ -376,9 +376,10 @@ async function cacheIfAllowed(
376376
) {
377377
try {
378378
if (!response || !response.ok) return;
379-
const ct = (response.headers &&
380-
response.headers.get &&
381-
response.headers.get("content-type")) ||
379+
const ct =
380+
(response.headers &&
381+
response.headers.get &&
382+
response.headers.get("content-type")) ||
382383
"";
383384
if (
384385
ct.startsWith("image/") ||

0 commit comments

Comments
 (0)