fix: restore public page share thumbnails from SVG sidecars

Serve missing *.thumbnail.png requests when the SVG sidecar exists, and schedule async PNG rasterization after public HTML is materialized so WeChat og:image URLs stop 404ing.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-07-04 00:11:40 +08:00
parent 553af7ca7a
commit 702871a98c
5 changed files with 78 additions and 1 deletions
+2
View File
@@ -3,6 +3,7 @@ import path from 'node:path';
import { extractStaticPageLinks } from './mindspace-chat-save.mjs';
import { DOWNLOADABLE_FILE_PATTERN } from './mindspace-html-download-links.mjs';
import { scheduleWorkspaceHtmlThumbnailSidecars } from './mindspace-workspace-thumbnails.mjs';
/**
* Public HTML finish-sync invariants (regression guard — do not simplify away).
@@ -398,6 +399,7 @@ export function materializeMissingPublicHtmlWrites({ messages, publishDir }) {
fs.mkdirSync(path.dirname(destination), { recursive: true });
fs.writeFileSync(destination, artifact.content, 'utf8');
materialized.push(artifact.relativePath);
scheduleWorkspaceHtmlThumbnailSidecars(root, artifact.relativePath);
} catch {
skipped.push(artifact.relativePath);
}
+14
View File
@@ -191,6 +191,20 @@ export async function resolveMindSpacePublicRequest({
};
}
// og:image points at <base>.thumbnail.png while only the SVG sidecar may exist on disk.
// Serve resolves lazily via ensureThumbnailPng in the MindSpace public file handler.
if (/\.thumbnail\.png$/i.test(resolvedPath)) {
const svgSibling = resolvedPath.replace(/\.png$/i, '.svg');
if (fs.existsSync(svgSibling)) {
return {
action: 'serve',
filePath: resolvedPath,
ownerKey,
targetDir,
};
}
}
return { action: 'not_found', reason: 'missing_file', ownerKey };
}
+27
View File
@@ -120,3 +120,30 @@ test('resolveMindSpacePublicRequest coordinates canonical redirect, serve, and n
ownerKey,
});
});
test('resolveMindSpacePublicRequest serves missing thumbnail png when svg sidecar exists', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ms-public-thumb-'));
const ownerKey = '123e4567-e89b-12d3-a456-426614174000';
const targetDir = path.join(root, 'MindSpace', ownerKey);
const publicDir = path.join(targetDir, 'public');
fs.mkdirSync(publicDir, { recursive: true });
fs.writeFileSync(path.join(publicDir, 'report.thumbnail.svg'), '<svg xmlns="http://www.w3.org/2000/svg"></svg>');
const served = await resolveMindSpacePublicRequest({
h5Root: root,
requestPath: `/${ownerKey}/public/report.thumbnail.png`,
});
assert.equal(served.action, 'serve');
assert.equal(served.filePath, path.join(publicDir, 'report.thumbnail.png'));
assert.equal(fs.existsSync(served.filePath), false);
const stillMissing = await resolveMindSpacePublicRequest({
h5Root: root,
requestPath: `/${ownerKey}/public/missing.thumbnail.png`,
});
assert.deepEqual(stillMissing, {
action: 'not_found',
reason: 'missing_file',
ownerKey,
});
});
+22 -1
View File
@@ -1,6 +1,7 @@
import fs from 'node:fs';
import fsPromises from 'node:fs/promises';
import path from 'node:path';
import { ensureThumbnailPng } from './mindspace-thumbnail-png.mjs';
import { ensureHtmlThumbnail } from './mindspace-thumbnails.mjs';
export function workspaceThumbnailRelativePath(htmlRelativePath) {
@@ -15,14 +16,34 @@ export function workspaceThumbnailAbsolutePath(publishDir, htmlRelativePath) {
return path.join(publishDir, workspaceThumbnailRelativePath(htmlRelativePath));
}
function scheduleWorkspaceThumbnailPng(publishDir, htmlRelativePath) {
queueMicrotask(() => {
void Promise.resolve()
.then(() => {
const svgPath = workspaceThumbnailAbsolutePath(publishDir, htmlRelativePath);
if (fs.existsSync(svgPath)) ensureThumbnailPng(svgPath);
})
.catch(() => {});
});
}
/** Fire-and-forget SVG + PNG sidecars after public HTML is written (must not block Finish/chat). */
export function scheduleWorkspaceHtmlThumbnailSidecars(publishDir, htmlRelativePath) {
queueMicrotask(() => {
void ensureWorkspaceHtmlThumbnail(publishDir, htmlRelativePath).catch(() => {});
});
}
export async function ensureWorkspaceHtmlThumbnail(publishDir, htmlRelativePath, html, meta = {}) {
const htmlPath = path.join(publishDir, htmlRelativePath);
const content = html ?? (await fsPromises.readFile(htmlPath, 'utf8'));
const thumbRel = workspaceThumbnailRelativePath(htmlRelativePath);
return ensureHtmlThumbnail(publishDir, thumbRel, content, {
const svg = await ensureHtmlThumbnail(publishDir, thumbRel, content, {
...meta,
contentBaseDir: path.dirname(htmlPath),
});
scheduleWorkspaceThumbnailPng(publishDir, htmlRelativePath);
return svg;
}
export async function readWorkspaceThumbnailIfExists(publishDir, htmlRelativePath) {
+13
View File
@@ -27,3 +27,16 @@ test('ensureWorkspaceHtmlThumbnail writes sidecar on html save', async () => {
assert.match(svg, /运动|普拉提618/i);
await fs.access(path.join(root, 'pilates.thumbnail.svg'));
});
test('ensureWorkspaceHtmlThumbnail schedules raster png sidecar', async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'workspace-thumb-png-'));
const html = `<!doctype html><html><head>
<title>PNG sidecar</title>
<meta name="mindspace-cover" content='{"tag":"测试","accent":"#eeb04e","accent2":"#1a0a2e","subtitle":"副标题"}' />
</head><body></body></html>`;
await fs.writeFile(path.join(root, 'demo.html'), html, 'utf8');
await ensureWorkspaceHtmlThumbnail(root, 'demo.html', html, { title: 'PNG sidecar' });
await new Promise((resolve) => queueMicrotask(resolve));
await new Promise((resolve) => setImmediate(resolve));
await fs.access(path.join(root, 'demo.thumbnail.png'));
});