Files
memind/mindspace-thumbnail-png.mjs
john 229805a070 Improve WeChat MP replies and ship MindSpace/H5 production updates.
Add WeChat service account routing with sync acks, connectivity tests, and context isolation; document deploy runbooks; and bundle related MindSpace, voice, Plaza, and server gateway changes for production rollout.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-19 23:06:43 +08:00

46 lines
1.8 KiB
JavaScript

// Rasterize a feed thumbnail SVG to PNG so every published page has a guaranteed
// raster og:image (WeChat / browsers don't render SVG cover images).
//
// PNGs are derived lazily from the sibling `<base>.thumbnail.svg` and cached on disk
// as `<base>.thumbnail.png`. Regenerated when the SVG is newer than the cached PNG.
import fs from 'node:fs';
import { Resvg } from '@resvg/resvg-js';
const RENDER_WIDTH = 540; // matches the 540x720 feed card aspect
/** SVG → PNG buffer. White background avoids black fills where clients ignore alpha. */
export function rasterizeThumbnailSvgToPng(svg) {
const resvg = new Resvg(svg, {
background: 'white',
fitTo: { mode: 'width', value: RENDER_WIDTH },
font: { loadSystemFonts: true }, // CJK + serif via the host font stack (PingFang/Georgia on macOS)
});
return resvg.render().asPng();
}
/** Absolute path of the PNG that mirrors a `.thumbnail.svg`. */
export function thumbnailPngPathForSvg(svgAbsPath) {
return svgAbsPath.replace(/\.svg$/i, '.png');
}
/**
* Ensure a `.thumbnail.png` exists and is at least as fresh as its `.thumbnail.svg`.
* @returns {string|null} the PNG path, or null when no source SVG is available.
*/
export function ensureThumbnailPng(svgAbsPath) {
if (!fs.existsSync(svgAbsPath)) return null;
const pngPath = thumbnailPngPathForSvg(svgAbsPath);
try {
const svgStat = fs.statSync(svgAbsPath);
if (fs.existsSync(pngPath) && fs.statSync(pngPath).mtimeMs >= svgStat.mtimeMs) {
return pngPath;
}
const svg = fs.readFileSync(svgAbsPath, 'utf8');
fs.writeFileSync(pngPath, rasterizeThumbnailSvgToPng(svg));
return pngPath;
} catch {
// If rasterization fails, fall back to an existing PNG if present, else give up.
return fs.existsSync(pngPath) ? pngPath : null;
}
}