Files
memind/mindspace-public-delivery.mjs
T
john fde6503bdf
Memind CI / Test, build, and release guards (push) Has been cancelled
feat(mindspace): add SEO/GEO delivery, page template catalog, and admin hooks
Enable optional SEO/GEO injection and discovery routes for confirmed public pages while keeping private pages noindex. Add premium page template skills, portal catalog API, template shop UI, and Baidu push gated by memind_adm config.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-10 08:06:12 +08:00

145 lines
4.6 KiB
JavaScript

import crypto from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';
import { injectMindSpacePageDataContext } from './mindspace-public-page-context.mjs';
import { preparePublishedPlatformBrand } from './mindspace-page-tag.mjs';
import { injectPublicImageRetryScript } from './mindspace-public-image-retry.mjs';
import { applyWechatSurveyCompat } from './mindspace-page-data-wechat-survey-compat.mjs';
import { stripPublicationHtmlCspMeta } from './plaza-embed.mjs';
import {
decorateMindSpaceSeoGeoHtml,
} from './mindspace-seo-geo-delivery.mjs';
const INLINE_SCRIPT_PATTERN = /<script\b(?![^>]*\bsrc\b)[^>]*>([\s\S]*?)<\/script>/gi;
export function collectInlineScriptHashes(html) {
const hashes = [];
const seen = new Set();
for (const match of String(html ?? '').matchAll(INLINE_SCRIPT_PATTERN)) {
const hash = crypto.createHash('sha256').update(match[1] ?? '').digest('base64');
if (seen.has(hash)) continue;
seen.add(hash);
hashes.push(hash);
}
return hashes;
}
export async function handleMindSpaceLongImageDownload({
query,
filePath,
pageUrl = null,
res,
isLongImageDownloadRequest,
longImagePathForHtml,
renderLongImage,
} = {}) {
if (!isLongImageDownloadRequest?.(query)) return false;
const longImagePath = longImagePathForHtml(filePath);
try {
await renderLongImage(pageUrl
? { url: pageUrl, outputPath: longImagePath }
: { htmlPath: filePath, outputPath: longImagePath });
res.set('Cache-Control', 'no-store');
res.download(longImagePath, path.basename(longImagePath), (err) => {
if (err && !res.headersSent) res.status(404).json({ message: '长图文件不存在' });
});
} catch (error) {
res
.status(500)
.type('text/plain; charset=utf-8')
.send(`长图生成失败:${error?.message || '未知错误'}`);
}
return true;
}
export function decorateMindSpacePublishedHtml({
html,
embed = false,
isOwner = true,
pageDataContext = null,
context,
htmlFilePath = '',
fileExists = fs.existsSync,
userAgent = '',
preparePublicationHtmlForEmbed,
injectOgTags,
injectWechatShareBridge,
injectPublicFileShareButton,
publishedPageCsp,
isWechatUserAgent,
seoGeoConfig = null,
publicationSnapshot = null,
} = {}) {
// Preview HTML carries a restrictive inline CSP (often script-src 'none').
// Published delivery sets the authoritative CSP response header below; keep
// the preview meta out of the delivered document so Page Data and other
// same-origin scripts are governed by that header instead of being blocked
// by a stale preview policy.
let nextHtml = stripPublicationHtmlCspMeta(html);
let allowEmbedFrame = false;
if (embed) {
nextHtml = preparePublicationHtmlForEmbed(nextHtml);
allowEmbedFrame = true;
}
try {
nextHtml = preparePublishedPlatformBrand(nextHtml);
nextHtml = injectOgTags(nextHtml, {
origin: context.origin,
pageUrl: context.pageUrl,
pageDirUrl: context.pageDirUrl,
fallbackImageUrl: context.fallbackImageUrl,
htmlFilePath,
fileExists,
});
const wechatShare = !embed && isWechatUserAgent(userAgent || '');
if (wechatShare) {
nextHtml = injectWechatShareBridge(nextHtml, { pageUrl: context.pageUrl });
}
const shareInjection = !embed
? injectPublicFileShareButton(nextHtml, { isOwner })
: { html: nextHtml, scriptHashes: [] };
nextHtml = shareInjection.html;
// Retries transient asset-download failures client-side before a page's own onerror
// permanently swaps the <img> for a placeholder. See mindspace-public-image-retry.mjs.
if (!embed) {
nextHtml = injectPublicImageRetryScript(nextHtml).html;
}
if (pageDataContext?.pageId) {
nextHtml = injectMindSpacePageDataContext(nextHtml, pageDataContext);
}
if (!embed && isWechatUserAgent(userAgent || '')) {
nextHtml = applyWechatSurveyCompat(nextHtml).html;
}
const seoGeoDecorated = decorateMindSpaceSeoGeoHtml(nextHtml, {
seoGeoConfig,
publicationSnapshot,
pageDataContext,
embed,
context,
htmlFilePath,
fileExists,
});
nextHtml = seoGeoDecorated.html;
const scriptHashes = collectInlineScriptHashes(nextHtml);
return {
html: nextHtml,
csp: publishedPageCsp(nextHtml, {
embed,
wechatShare,
scriptHashes,
}),
allowEmbedFrame,
robotsHeader: seoGeoDecorated.robotsHeader,
};
} catch {
return {
html: nextHtml,
csp: publishedPageCsp(nextHtml, { embed }),
allowEmbedFrame,
robotsHeader: null,
};
}
}