// Inject Open Graph / Twitter Card tags into published MindSpace HTML at serve time,
// so forwarded links unfurl with a cover image in WeChat / browsers / IM clients.
//
// Source of truth for the cover is the page's own
(mindspace-cover JSON /
// / hero ), reused via extractCoverSignals. Authors who
// already wrote their own og:image are left untouched; missing site_name / description /
// brand icon are still backfilled.
import path from 'node:path';
import { extractCoverSignals } from './mindspace-thumbnails.mjs';
export const PLATFORM_SITE_NAME = 'TKMind 智趣';
export const PLATFORM_BRAND_ICON_PATH = '/brand/tkmind-icon.png';
function rawTitleFromHtml(html) {
// Keep the original verbatim (emoji included) for og:title.
return String(html).match(/]*>([^<]*)<\/title>/i)?.[1]?.trim() ?? '';
}
function escapeAttr(value) {
return String(value)
.replaceAll('&', '&')
.replaceAll('"', '"')
.replaceAll('<', '<')
.replaceAll('>', '>');
}
/**
* Resolve a cover reference (relative path, root-absolute, or full URL) to an
* absolute https URL the unfurling client can fetch.
* @returns {string|null} absolute URL, or null when no usable raster cover exists.
*/
function resolveImageUrl(image, { origin, pageDirUrl }) {
if (!image) return null;
const trimmed = String(image).trim();
if (!trimmed || trimmed.startsWith('data:')) return null;
// og:image must be a raster the client can render; SVG is not honored by WeChat/most unfurlers.
if (/\.svg(?:[?#]|$)/i.test(trimmed)) return null;
if (/^https?:\/\//i.test(trimmed)) return trimmed;
if (trimmed.startsWith('//')) return `https:${trimmed}`;
if (trimmed.startsWith('/')) return `${origin}${trimmed}`;
return `${pageDirUrl}${trimmed}`;
}
/** Relative cover paths must exist beside the HTML file; remote/absolute refs are trusted. */
export function coverImageRefExists(imageRef, htmlFilePath, fileExists = () => false) {
const ref = String(imageRef ?? '').trim();
if (!ref || ref.startsWith('data:')) return false;
if (/^https?:\/\//i.test(ref) || ref.startsWith('//') || ref.startsWith('/')) return true;
if (!htmlFilePath) return true;
return fileExists(path.join(path.dirname(htmlFilePath), ref));
}
function rawMetaContent(html, attr, name) {
const pattern = new RegExp(
`]+${attr}=["']${name}["'][^>]+content=["']([^"']*)["']|]+content=["']([^"']*)["'][^>]+${attr}=["']${name}["']`,
'i',
);
const match = String(html).match(pattern);
return match?.[1] || match?.[2] || '';
}
function hasMeta(html, attr, name) {
return new RegExp(`]+${attr}=["']${name}["']`, 'i').test(String(html));
}
function hasLinkRel(html, rel) {
return new RegExp(`]+rel=["'][^"']*\\b${rel}\\b[^"']*["']`, 'i').test(String(html));
}
function escapeScriptString(value) {
return JSON.stringify(String(value ?? '')).replace(//i.test(source)) {
return source.replace(/<\/head>/i, `${block}\n`);
}
if (/]*>/i.test(source)) {
return source.replace(/(]*>)/i, `$1${block}`);
}
return `${block}\n${source}`;
}
function resolveShareDescription(html, { siteName = PLATFORM_SITE_NAME, meta = {} } = {}) {
return (
rawMetaContent(html, 'property', 'og:description') ||
rawMetaContent(html, 'name', 'description') ||
extractCoverSignals(html, meta).subtitle ||
`${siteName} · 精选作品`
);
}
function resolveShareImageUrl(
html,
{ origin, pageDirUrl, fallbackImageUrl = '', meta = {}, htmlFilePath = '', fileExists = null } = {},
) {
const existing = rawMetaContent(html, 'property', 'og:image');
if (existing) return existing;
const signals = extractCoverSignals(html, meta);
const imageRef = signals.image;
if (imageRef) {
const exists =
typeof fileExists === 'function' ? coverImageRefExists(imageRef, htmlFilePath, fileExists) : true;
if (exists) {
const resolved = resolveImageUrl(imageRef, { origin, pageDirUrl });
if (resolved) return resolved;
}
}
return fallbackImageUrl || '';
}
/**
* Extract the share-card fields WeChat / IM clients consume from HTML.
*/
export function extractSharePreviewMeta(
html,
{
origin = '',
pageUrl = '',
pageDirUrl = '',
fallbackImageUrl = '',
siteName = PLATFORM_SITE_NAME,
brandIconUrl = '',
meta = {},
htmlFilePath = '',
fileExists = null,
} = {},
) {
const source = String(html ?? '');
const title = rawMetaContent(source, 'property', 'og:title') || rawTitleFromHtml(source) || 'MindSpace 页面';
const description = resolveShareDescription(source, { siteName, meta });
const imageUrl = resolveShareImageUrl(source, {
origin,
pageDirUrl,
fallbackImageUrl,
meta,
htmlFilePath,
fileExists,
});
const resolvedSiteName = rawMetaContent(source, 'property', 'og:site_name') || siteName;
const iconUrl = brandIconUrl || (origin ? `${origin}${PLATFORM_BRAND_ICON_PATH}` : PLATFORM_BRAND_ICON_PATH);
return {
title,
description,
imageUrl,
siteName: resolvedSiteName,
iconUrl,
pageUrl,
};
}
/**
* Inject og:/twitter: meta into an HTML document.
* @param {string} html raw page HTML
* @param {{ origin: string, pageUrl: string, pageDirUrl: string, fallbackImageUrl?: string, siteName?: string, brandIconUrl?: string, meta?: object }} ctx
* @returns {string} HTML with tags injected (or unchanged when not applicable)
*/
export function injectOgTags(
html,
{
origin,
pageUrl,
pageDirUrl,
fallbackImageUrl = '',
siteName = PLATFORM_SITE_NAME,
brandIconUrl = '',
meta = {},
htmlFilePath = '',
fileExists = null,
} = {},
) {
const source = String(html);
const preview = extractSharePreviewMeta(source, {
origin,
pageUrl,
pageDirUrl,
fallbackImageUrl,
siteName,
brandIconUrl,
meta,
htmlFilePath,
fileExists,
});
const tags = [];
if (!hasMeta(source, 'property', 'og:type')) {
tags.push('');
}
if (pageUrl && !hasMeta(source, 'property', 'og:url')) {
tags.push(``);
}
if (preview.title && !hasMeta(source, 'property', 'og:title')) {
tags.push(``);
}
if (preview.description && !hasMeta(source, 'property', 'og:description')) {
tags.push(``);
}
if (preview.imageUrl && !hasMeta(source, 'property', 'og:image')) {
tags.push(``);
}
if (preview.siteName && !hasMeta(source, 'property', 'og:site_name')) {
tags.push(``);
}
const twitterCard = preview.imageUrl ? 'summary_large_image' : 'summary';
if (!hasMeta(source, 'name', 'twitter:card')) {
tags.push(``);
}
if (preview.title && !hasMeta(source, 'name', 'twitter:title')) {
tags.push(``);
}
if (preview.description && !hasMeta(source, 'name', 'twitter:description')) {
tags.push(``);
}
if (preview.imageUrl && !hasMeta(source, 'name', 'twitter:image')) {
tags.push(``);
}
const links = [];
if (preview.iconUrl && !hasLinkRel(source, 'icon')) {
links.push(``);
}
if (preview.iconUrl && !hasLinkRel(source, 'apple-touch-icon')) {
links.push(``);
}
const block = [...tags, ...links].map((t) => ` ${t}`).join('\n');
if (!block) return source;
return appendBeforeHeadClose(source, `\n${block}\n`);
}
export function injectWechatShareBridge(
html,
{
pageUrl,
signatureEndpoint = '/auth/wechat/public-js-sdk-signature',
siteName = PLATFORM_SITE_NAME,
} = {},
) {
const source = String(html ?? '');
if (!pageUrl) return source;
if (source.includes('data-tkmind-wechat-share="1"')) return source;
const title = rawMetaContent(source, 'property', 'og:title') || rawTitleFromHtml(source);
const resolvedSiteName = rawMetaContent(source, 'property', 'og:site_name') || siteName;
const description = resolveShareDescription(source, { siteName: resolvedSiteName });
const imageUrl = rawMetaContent(source, 'property', 'og:image') || '';
const script = `
`;
return appendBeforeHeadClose(source, script);
}
/**
* Render a lightweight WeChat-style share card for local preview.
*/
export function renderWechatSharePreviewHtml(preview, { note = '' } = {}) {
const safe = (value) =>
String(value ?? '')
.replaceAll('&', '&')
.replaceAll('<', '<')
.replaceAll('>', '>')
.replaceAll('"', '"');
const title = safe(preview.title || '页面标题');
const description = safe(preview.description || '');
const siteName = safe(preview.siteName || PLATFORM_SITE_NAME);
const imageUrl = safe(preview.imageUrl || '');
const iconUrl = safe(preview.iconUrl || PLATFORM_BRAND_ICON_PATH);
const pageUrl = safe(preview.pageUrl || '');
const noteHtml = note ? `