diff --git a/mindspace-public-finish-sync.mjs b/mindspace-public-finish-sync.mjs
index d0d970b..d9792d8 100644
--- a/mindspace-public-finish-sync.mjs
+++ b/mindspace-public-finish-sync.mjs
@@ -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);
}
diff --git a/mindspace-public-route.mjs b/mindspace-public-route.mjs
index 7025b55..4be24d4 100644
--- a/mindspace-public-route.mjs
+++ b/mindspace-public-route.mjs
@@ -191,6 +191,20 @@ export async function resolveMindSpacePublicRequest({
};
}
+ // og:image points at .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 };
}
diff --git a/mindspace-public-route.test.mjs b/mindspace-public-route.test.mjs
index af1f143..05853aa 100644
--- a/mindspace-public-route.test.mjs
+++ b/mindspace-public-route.test.mjs
@@ -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'), '');
+
+ 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,
+ });
+});
diff --git a/mindspace-workspace-thumbnails.mjs b/mindspace-workspace-thumbnails.mjs
index 25abd75..59bc396 100644
--- a/mindspace-workspace-thumbnails.mjs
+++ b/mindspace-workspace-thumbnails.mjs
@@ -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) {
diff --git a/mindspace-workspace-thumbnails.test.mjs b/mindspace-workspace-thumbnails.test.mjs
index 7cd0b99..efbd417 100644
--- a/mindspace-workspace-thumbnails.test.mjs
+++ b/mindspace-workspace-thumbnails.test.mjs
@@ -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 = `
+ PNG sidecar
+
+ `;
+ 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'));
+});