// 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.
import { extractCoverSignals } from './mindspace-thumbnails.mjs';
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}`;
}
/**
* Inject og:/twitter: meta into an HTML document.
* @param {string} html raw page HTML
* @param {{ origin: string, pageUrl: string, pageDirUrl: string, fallbackImageUrl?: string }} ctx
* origin: https://host pageUrl: canonical page URL pageDirUrl: page directory URL (trailing '/')
* fallbackImageUrl: absolute raster URL to use when the page has no cover of its own
* @returns {string} HTML with tags injected (or unchanged when not applicable)
*/
export function injectOgTags(html, { origin, pageUrl, pageDirUrl, fallbackImageUrl = '' }) {
const source = String(html);
// Respect a page that already declares its own Open Graph image.
if (/]+property=["']og:image["']/i.test(source)) return source;
const signals = extractCoverSignals(source);
const title = rawTitleFromHtml(source) || signals.title;
const description = signals.subtitle || '';
const imageUrl = resolveImageUrl(signals.image, { origin, pageDirUrl }) || fallbackImageUrl || null;
const tags = [
'',
pageUrl ? `` : '',
title ? `` : '',
description ? `` : '',
imageUrl ? `` : '',
``,
title ? `` : '',
description ? `` : '',
imageUrl ? `` : '',
].filter(Boolean);
const block = `\n${tags.map((t) => ` ${t}`).join('\n')}\n`;
if (/<\/head>/i.test(source)) {
return source.replace(/<\/head>/i, `${block}`);
}
if (/]*>/i.test(source)) {
return source.replace(/(]*>)/i, `$1${block}`);
}
return source;
}