229805a070
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>
46 lines
1.8 KiB
JavaScript
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;
|
|
}
|
|
}
|