Files
memind/mindspace-public-route.mjs
john 702871a98c 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>
2026-07-04 00:11:40 +08:00

236 lines
6.9 KiB
JavaScript

import fs from 'node:fs';
import path from 'node:path';
import {
buildMindSpacePublicRoutePath,
resolveMindSpaceUserPublishDir,
} from './mindspace-runtime-config.mjs';
import { PUBLIC_ZONE_DIR, PUBLISH_KEY_UUID } from './user-publish.mjs';
const USERNAME_SLUG = /^[a-z0-9_]{2,32}$/;
const MISPLACED_PUBLIC_HTML_NAME = /^[a-z0-9][a-z0-9._-]{0,127}\.html$/i;
export async function resolveMindSpacePublishOwnerKey(segment, resolveUsernameToUserId) {
const lower = String(segment ?? '').trim().toLowerCase();
if (!lower) return null;
if (PUBLISH_KEY_UUID.test(lower)) return lower;
if (!USERNAME_SLUG.test(lower)) return null;
const resolved = await resolveUsernameToUserId?.(lower);
return resolved ? String(resolved).toLowerCase() : lower;
}
export function decodeMindSpaceRouteSegment(segment) {
try {
return decodeURIComponent(segment);
} catch {
return segment;
}
}
export function resolveMindSpacePublicRouteTarget({ h5Root, ownerKey, parts = [] }) {
const targetDir = resolveMindSpaceUserPublishDir(h5Root, { id: ownerKey });
const resolvedRoot = path.resolve(targetDir);
const resolvedPath = path.resolve(path.join(targetDir, ...parts));
return {
targetDir,
resolvedRoot,
resolvedPath,
insideRoot:
resolvedPath === resolvedRoot || resolvedPath.startsWith(`${resolvedRoot}${path.sep}`),
};
}
export function buildMindSpaceCanonicalRedirect(ownerKey, parts = []) {
return buildMindSpacePublicRoutePath(ownerKey, parts);
}
export function resolveMindSpaceHtmlFallbackRedirect({ ownerKey, targetDir, resolvedRoot, rest = [] }) {
if (rest.length === 1 && String(rest[0]).toLowerCase().endsWith('.html')) {
const publicFallback = path.resolve(targetDir, PUBLIC_ZONE_DIR, rest[0]);
if (
publicFallback.startsWith(`${resolvedRoot}${path.sep}`) &&
fs.existsSync(publicFallback) &&
fs.statSync(publicFallback).isFile()
) {
return buildMindSpacePublicRoutePath(ownerKey, [PUBLIC_ZONE_DIR, rest[0]]);
}
}
return null;
}
export function resolveMindSpaceSimilarHtmlRedirect({ ownerKey, relativePath }) {
if (!relativePath) return null;
return buildMindSpacePublicRoutePath(ownerKey, String(relativePath).split('/'));
}
export async function recoverMisplacedMindSpacePublicHtml({
h5Root,
targetDir,
resolvedRoot,
rest = [],
ensureThumbnail,
logger = console,
} = {}) {
if (rest.length !== 2 || rest[0] !== PUBLIC_ZONE_DIR) return null;
const filename = rest[1];
if (!MISPLACED_PUBLIC_HTML_NAME.test(filename)) return null;
const destination = path.resolve(targetDir, PUBLIC_ZONE_DIR, filename);
if (!destination.startsWith(`${resolvedRoot}${path.sep}`)) return null;
const candidates = [
path.resolve(h5Root, filename),
path.resolve(h5Root, PUBLIC_ZONE_DIR, filename),
];
for (const candidate of candidates) {
if (candidate === destination) continue;
if (!candidate.startsWith(`${path.resolve(h5Root)}${path.sep}`)) continue;
if (!fs.existsSync(candidate) || !fs.statSync(candidate).isFile()) continue;
fs.mkdirSync(path.dirname(destination), { recursive: true });
fs.copyFileSync(candidate, destination);
await ensureThumbnail?.(targetDir, `${PUBLIC_ZONE_DIR}/${filename}`);
logger?.warn?.(
`[MindSpace] recovered misplaced public HTML ${path.relative(h5Root, candidate)} -> ${path.relative(h5Root, destination)}`,
);
return destination;
}
return null;
}
export async function resolveMindSpacePublicRequest({
h5Root,
requestPath = '',
resolveUsernameToUserId,
resolveClosestHtmlRelativePath,
ensureThumbnail,
logger = console,
} = {}) {
const parts = String(requestPath || '')
.split('/')
.filter(Boolean);
if (parts.length < 1) {
return { action: 'not_found', reason: 'missing_path' };
}
const ownerKey = await resolveMindSpacePublishOwnerKey(parts[0], resolveUsernameToUserId);
if (!ownerKey) {
return { action: 'not_found', reason: 'missing_owner' };
}
if (parts[0].toLowerCase() !== ownerKey && parts[0].trim()) {
return {
action: 'redirect',
status: 301,
location: buildMindSpaceCanonicalRedirect(ownerKey, parts.slice(1)),
ownerKey,
};
}
const rest = parts.slice(1).map(decodeMindSpaceRouteSegment);
const routeTarget = resolveMindSpacePublicRouteTarget({
h5Root,
ownerKey,
parts: rest,
});
const { targetDir, resolvedRoot, resolvedPath } = routeTarget;
if (!routeTarget.insideRoot) {
return { action: 'forbidden', reason: 'outside_publish_root', ownerKey };
}
if (!fs.existsSync(targetDir)) {
return { action: 'not_found', reason: 'missing_owner_dir', ownerKey };
}
if (!fs.existsSync(resolvedPath)) {
const publicFallbackRedirect = resolveMindSpaceHtmlFallbackRedirect({
ownerKey,
targetDir,
resolvedRoot,
rest,
});
if (publicFallbackRedirect) {
return {
action: 'redirect',
status: 301,
location: publicFallbackRedirect,
ownerKey,
};
}
if (rest.length === 2 && rest[0] === PUBLIC_ZONE_DIR && rest[1].toLowerCase().endsWith('.html')) {
const similarRelativePath = await resolveClosestHtmlRelativePath?.(
targetDir,
`${PUBLIC_ZONE_DIR}/${rest[1]}`,
);
if (similarRelativePath) {
return {
action: 'redirect',
status: 301,
location: resolveMindSpaceSimilarHtmlRedirect({
ownerKey,
relativePath: similarRelativePath,
}),
ownerKey,
};
}
}
const recoveredPublicHtml = await recoverMisplacedMindSpacePublicHtml({
h5Root,
targetDir,
resolvedRoot,
rest,
ensureThumbnail,
logger,
});
if (recoveredPublicHtml) {
return {
action: 'serve',
filePath: recoveredPublicHtml,
ownerKey,
targetDir,
};
}
// 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 };
}
if (fs.statSync(resolvedPath).isDirectory()) {
const indexPath = path.join(resolvedPath, 'index.html');
if (fs.existsSync(indexPath)) {
return {
action: 'serve',
filePath: indexPath,
ownerKey,
targetDir,
};
}
return { action: 'not_found', reason: 'missing_directory_index', ownerKey };
}
return {
action: 'serve',
filePath: resolvedPath,
ownerKey,
targetDir,
};
}
export const mindspacePublicRouteInternals = {
USERNAME_SLUG,
MISPLACED_PUBLIC_HTML_NAME,
};