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:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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'));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user