my website at ewancroft.uk
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix(og): add debugging info

Ewan Croft 374124df 9f066f64

+113 -44
+74 -30
src/lib/server/ogImage.ts
··· 1 - import fs from 'fs/promises'; 2 - import path from 'path'; 3 1 import satori from 'satori'; 4 2 import { Resvg } from '@resvg/resvg-js'; 3 + import { dev } from '$app/environment'; 5 4 6 - // Font paths (adjust if needed) 7 - const fontRegularPath = path.resolve('static/fonts/ArrowType-Recursive-1.085/Recursive_Desktop/separate_statics/TTF/RecursiveSansCslSt-Regular.ttf'); 8 - const fontBoldPath = path.resolve('static/fonts/ArrowType-Recursive-1.085/Recursive_Desktop/separate_statics/TTF/RecursiveSansCslSt-Bold.ttf'); 9 - const fontItalicPath = path.resolve('static/fonts/ArrowType-Recursive-1.085/Recursive_Desktop/separate_statics/TTF/RecursiveSansCslSt-Italic.ttf'); 5 + // Font URLs for production (served from static folder) 6 + const FONT_BASE_URL = '/fonts/ArrowType-Recursive-1.085/Recursive_Desktop/separate_statics/TTF'; 7 + const FONT_FILES = { 8 + regular: 'RecursiveSansCslSt-Regular.ttf', 9 + bold: 'RecursiveSansCslSt-Bold.ttf', 10 + italic: 'RecursiveSansCslSt-Italic.ttf' 11 + }; 10 12 11 13 // Preload fonts (cache in memory for performance) 12 14 let fontCache: { regular?: Buffer; bold?: Buffer; italic?: Buffer } = {}; 13 - async function loadFonts() { 14 - if (!fontCache.regular) fontCache.regular = await fs.readFile(fontRegularPath); 15 - if (!fontCache.bold) fontCache.bold = await fs.readFile(fontBoldPath); 16 - if (!fontCache.italic) fontCache.italic = await fs.readFile(fontItalicPath); 17 - // Defensive: throw if any font is missing 18 - if (!fontCache.regular || !fontCache.bold || !fontCache.italic) { 19 - throw new Error('Failed to load all required font files for OG image'); 15 + 16 + async function loadSingleFont(fileName: string, baseUrl?: string): Promise<Buffer> { 17 + try { 18 + if (dev) { 19 + // In development, try to load from filesystem first 20 + const fs = await import('fs/promises'); 21 + const path = await import('path'); 22 + const fontPath = path.resolve(`static${FONT_BASE_URL}/${fileName}`); 23 + console.log(`Loading font from filesystem: ${fontPath}`); 24 + return await fs.readFile(fontPath); 25 + } else { 26 + // In production, fetch from the served static files 27 + const fontUrl = `${baseUrl || ''}${FONT_BASE_URL}/${fileName}`; 28 + console.log(`Fetching font from URL: ${fontUrl}`); 29 + 30 + const response = await fetch(fontUrl); 31 + if (!response.ok) { 32 + throw new Error(`Failed to fetch font: ${response.status} ${response.statusText}`); 33 + } 34 + 35 + const arrayBuffer = await response.arrayBuffer(); 36 + return Buffer.from(arrayBuffer); 37 + } 38 + } catch (error) { 39 + console.error(`Failed to load font ${fileName}:`, error); 40 + throw error; 20 41 } 21 - return fontCache as { regular: Buffer; bold: Buffer; italic: Buffer }; 42 + } 43 + 44 + async function loadFonts(baseUrl?: string) { 45 + try { 46 + // Load fonts if not already cached 47 + if (!fontCache.regular) { 48 + fontCache.regular = await loadSingleFont(FONT_FILES.regular, baseUrl); 49 + } 50 + if (!fontCache.bold) { 51 + fontCache.bold = await loadSingleFont(FONT_FILES.bold, baseUrl); 52 + } 53 + if (!fontCache.italic) { 54 + fontCache.italic = await loadSingleFont(FONT_FILES.italic, baseUrl); 55 + } 56 + 57 + // Defensive: throw if any font is missing 58 + if (!fontCache.regular || !fontCache.bold || !fontCache.italic) { 59 + throw new Error('Failed to load all required font files for OG image'); 60 + } 61 + 62 + return fontCache as { regular: Buffer; bold: Buffer; italic: Buffer }; 63 + } catch (error) { 64 + console.error('Font loading error:', error); 65 + throw error; 66 + } 22 67 } 23 68 24 69 export interface OgImageOptions { ··· 40 85 * Calculate optimal font size based on text length and available space 41 86 */ 42 87 function calculateTitleFontSize(title: string, maxWidth: number = 1000): number { 43 - const baseSize = 64; // Increased from 56px 44 - const charThreshold = 45; // Slightly lower threshold 45 - const minSize = 36; // Increased minimum size 88 + const baseSize = 64; 89 + const charThreshold = 45; 90 + const minSize = 36; 46 91 47 92 if (title.length <= charThreshold) { 48 93 return baseSize; 49 94 } 50 95 51 - // Scale down based on length 52 96 const scaleFactor = Math.max(0.5, 1 - ((title.length - charThreshold) * 0.012)); 53 97 return Math.max(minSize, Math.floor(baseSize * scaleFactor)); 54 98 } ··· 57 101 * Calculate optimal subtitle font size 58 102 */ 59 103 function calculateSubtitleFontSize(subtitle: string, titleSize: number): number { 60 - const baseRatio = 0.57; // subtitle is ~57% of title size 104 + const baseRatio = 0.57; 61 105 const charThreshold = 80; 62 106 const minSize = 20; 63 107 ··· 82 126 totalContentHeight: number; 83 127 } { 84 128 const titleFontSize = calculateTitleFontSize(options.title); 85 - const titleLines = Math.ceil(options.title.length / 40); // Adjusted for larger font 86 - const titleHeight = titleLines * titleFontSize * 1.25 + 32; // line-height + margin 129 + const titleLines = Math.ceil(options.title.length / 40); 130 + const titleHeight = titleLines * titleFontSize * 1.25 + 32; 87 131 88 132 let subtitleHeight = 0; 89 133 if (options.subtitle) { ··· 94 138 95 139 let metaHeight = 0; 96 140 if (options.metaLine || (options.extraMeta && options.extraMeta.length > 0)) { 97 - metaHeight = 32 + 24; // Adjusted for larger meta text 141 + metaHeight = 32 + 24; 98 142 } 99 143 100 - let authorHeight = options.author ? 120 : 28; // author section or decorative line 144 + let authorHeight = options.author ? 120 : 28; 101 145 102 146 return { 103 147 titleHeight, ··· 111 155 /** 112 156 * Generates a PNG buffer for an OG image with unified styling. 113 157 * @param options OgImageOptions 158 + * @param baseUrl Base URL for fetching fonts in production (e.g., 'https://ewancroft.uk') 114 159 * @returns Buffer (PNG) 115 160 */ 116 - export async function generateOgImage(options: OgImageOptions): Promise<Buffer> { 117 - const fonts = await loadFonts(); 161 + export async function generateOgImage(options: OgImageOptions, baseUrl?: string): Promise<Buffer> { 162 + const fonts = await loadFonts(baseUrl); 118 163 119 164 // Calculate optimal sizing 120 165 const titleFontSize = calculateTitleFontSize(options.title); ··· 122 167 123 168 // Estimate if content will fit and adjust if needed 124 169 const contentEstimate = estimateContentHeight(options); 125 - const availableHeight = 630 - 96; // Total height minus padding 170 + const availableHeight = 630 - 96; 126 171 127 172 let titleMarginBottom = 32; 128 173 let subtitleMarginBottom = 24; 129 174 let metaMarginBottom = 24; 130 175 131 - // If content is too tall, reduce margins more aggressively 132 176 if (contentEstimate.totalContentHeight > availableHeight) { 133 177 const compressionRatio = availableHeight / contentEstimate.totalContentHeight; 134 178 titleMarginBottom = Math.max(8, Math.floor(titleMarginBottom * compressionRatio)); ··· 155 199 display: 'flex', 156 200 alignItems: 'center', 157 201 justifyContent: 'center', 158 - textWrap: 'balance', // Better text wrapping 202 + textWrap: 'balance', 159 203 hyphens: 'auto', 160 204 paddingLeft: '48px', 161 205 paddingRight: '48px', ··· 192 236 type: 'div', 193 237 props: { 194 238 style: { 195 - fontSize: '24px', // Increased from 20px 196 - opacity: 0.75, // Slightly more visible 239 + fontSize: '24px', 240 + opacity: 0.75, 197 241 margin: `0 0 ${metaMarginBottom}px 0`, 198 242 display: 'flex', 199 243 flexDirection: 'row',
+7 -3
src/routes/api/og/blog.png/+server.ts
··· 1 1 import { generateOgImage } from '$lib/server/ogImage'; 2 + import { dev } from '$app/environment'; 2 3 3 - export const GET = async () => { 4 + export const GET = async ({ url }) => { 5 + const baseUrl = dev ? url.origin : 'https://ewancroft.uk'; 6 + 4 7 const pngBuffer = await generateOgImage({ 5 8 title: "Blog – Ewan's Corner", 6 9 subtitle: 'Personal blog, coding, technology, and life', 7 - }); 10 + }, baseUrl); 11 + 8 12 return new Response(new Uint8Array(pngBuffer), { 9 13 headers: { 10 14 'Content-Type': 'image/png', 11 15 'Cache-Control': 'public, max-age=86400', 12 16 }, 13 17 }); 14 - }; 18 + };
+10 -1
src/routes/api/og/blog/[rkey].png/+server.ts
··· 1 1 import { loadAllPosts } from '$lib/services/blogService'; 2 2 import { formatDate } from '$lib/utils/formatters'; 3 3 import { generateOgImage } from '$lib/server/ogImage'; 4 + import { dev } from '$app/environment'; 5 + 4 6 export const GET = async (event) => { 5 7 const rkey = event.params.rkey; 8 + const baseUrl = dev ? event.url.origin : 'https://ewancroft.uk'; 9 + 6 10 if (!rkey) { 7 11 return new Response('Missing rkey', { status: 400 }); 8 12 } 13 + 9 14 const { getPost, profile } = await loadAllPosts(event.fetch); 10 15 const post = getPost(rkey); 16 + 11 17 if (!post) { 12 18 return new Response('Post not found', { status: 404 }); 13 19 } 20 + 14 21 const wordCount = post.wordCount || 0; 15 22 const readingTime = Math.ceil(wordCount / 200); 16 23 const date = post.createdAt ? new Date(post.createdAt) : new Date(); 17 24 const dateString = formatDate ? formatDate(date) : date.toLocaleDateString('en-GB', { year: 'numeric', month: 'short', day: 'numeric' }); 18 25 const metaLine = `${readingTime} min read • ${wordCount} words`; 26 + 19 27 const pngBuffer = await generateOgImage({ 20 28 title: post.title, 21 29 subtitle: undefined, ··· 26 34 handle: profile.handle, 27 35 avatar: profile.avatar, 28 36 }, 29 - }); 37 + }, baseUrl); 38 + 30 39 return new Response(new Uint8Array(pngBuffer), { 31 40 headers: { 32 41 'Content-Type': 'image/png',
+7 -3
src/routes/api/og/main.png/+server.ts
··· 1 1 import { generateOgImage } from '$lib/server/ogImage'; 2 + import { dev } from '$app/environment'; 2 3 3 - export const GET = async () => { 4 + export const GET = async ({ url }) => { 5 + const baseUrl = dev ? url.origin : 'https://ewancroft.uk'; 6 + 4 7 const pngBuffer = await generateOgImage({ 5 8 title: "Ewan's Corner", 6 9 subtitle: 'personal site, blog, and digital garden', 7 - }); 10 + }, baseUrl); 11 + 8 12 return new Response(new Uint8Array(pngBuffer), { 9 13 headers: { 10 14 'Content-Type': 'image/png', 11 15 'Cache-Control': 'public, max-age=86400', 12 16 }, 13 17 }); 14 - }; 18 + };
+8 -4
src/routes/api/og/now.png/+server.ts
··· 1 1 import { generateOgImage } from '$lib/server/ogImage'; 2 + import { dev } from '$app/environment'; 2 3 3 - export const GET = async () => { 4 + export const GET = async ({ url }) => { 5 + const baseUrl = dev ? url.origin : 'https://ewancroft.uk'; 6 + 4 7 const pngBuffer = await generateOgImage({ 5 - title: 'What I’m Doing Now', 8 + title: 'What I\'m Doing Now', 6 9 subtitle: 'My current focus, projects, and life updates', 7 - }); 10 + }, baseUrl); 11 + 8 12 return new Response(new Uint8Array(pngBuffer), { 9 13 headers: { 10 14 'Content-Type': 'image/png', 11 15 'Cache-Control': 'public, max-age=86400', 12 16 }, 13 17 }); 14 - }; 18 + };
+7 -3
src/routes/api/og/site/meta.png/+server.ts
··· 1 1 import { generateOgImage } from '$lib/server/ogImage'; 2 + import { dev } from '$app/environment'; 2 3 3 - export const GET = async () => { 4 + export const GET = async ({ url }) => { 5 + const baseUrl = dev ? url.origin : 'https://ewancroft.uk'; 6 + 4 7 const pngBuffer = await generateOgImage({ 5 8 title: 'Site Meta', 6 9 subtitle: 'About this site, colophon, and technical details', 7 - }); 10 + }, baseUrl); 11 + 8 12 return new Response(new Uint8Array(pngBuffer), { 9 13 headers: { 10 14 'Content-Type': 'image/png', 11 15 'Cache-Control': 'public, max-age=86400', 12 16 }, 13 17 }); 14 - }; 18 + };