Files
memind/mindspace-seo-tags.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

136 lines
4.0 KiB
JavaScript

import { extractCoverSignals } from './mindspace-thumbnails.mjs';
import { extractSharePreviewMeta } from './mindspace-og-tags.mjs';
function escapeAttr(value) {
return String(value ?? '')
.replaceAll('&', '&amp;')
.replaceAll('"', '&quot;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;');
}
function hasMeta(html, attr, name) {
return new RegExp(`<meta[^>]+${attr}=["']${name}["']`, 'i').test(String(html));
}
function hasLinkRel(html, rel) {
return new RegExp(`<link[^>]+rel=["'][^"']*\\b${rel}\\b[^"']*["']`, 'i').test(String(html));
}
function appendBeforeHeadClose(html, block) {
const source = String(html ?? '');
if (!block) return source;
if (/<\/head>/i.test(source)) {
return source.replace(/<\/head>/i, `${block}\n</head>`);
}
if (/<head[^>]*>/i.test(source)) {
return source.replace(/(<head[^>]*>)/i, `$1${block}`);
}
return `${block}\n${source}`;
}
function rawMetaContent(html, attr, name) {
const pattern = new RegExp(
`<meta[^>]+${attr}=["']${name}["'][^>]+content=["']([^"']*)["']|<meta[^>]+content=["']([^"']*)["'][^>]+${attr}=["']${name}["']`,
'i',
);
const match = String(html ?? '').match(pattern);
return match?.[1] || match?.[2] || '';
}
export function injectRobotsNoindex(html) {
const source = String(html ?? '');
if (hasMeta(source, 'name', 'robots')) return source;
const tags = [
'<meta name="robots" content="noindex, nofollow">',
'<meta name="googlebot" content="noindex, nofollow">',
]
.map((tag) => ` ${tag}`)
.join('\n');
return appendBeforeHeadClose(source, `\n${tags}\n`);
}
export function resolveCanonicalUrl({
publication = null,
pageUrl = '',
origin = '',
} = {}) {
const fromPublication = String(publication?.publicUrl ?? '').trim();
if (fromPublication) {
if (/^https?:\/\//i.test(fromPublication)) return fromPublication.split('#')[0];
if (fromPublication.startsWith('/') && origin) {
return `${origin}${fromPublication}`.split('#')[0];
}
return fromPublication.split('#')[0];
}
return String(pageUrl ?? '').split('#')[0];
}
export function injectSeoTags(
html,
{
origin = '',
pageUrl = '',
pageDirUrl = '',
publication = null,
canonicalEnabled = true,
htmlFilePath = '',
fileExists = null,
meta = {},
} = {},
) {
const source = String(html ?? '');
const preview = extractSharePreviewMeta(source, {
origin,
pageUrl,
pageDirUrl,
meta,
htmlFilePath,
fileExists,
});
const signals = extractCoverSignals(source, meta);
const tags = [];
const links = [];
if (!hasMeta(source, 'name', 'description') && preview.description) {
tags.push(
`<meta name="description" content="${escapeAttr(preview.description)}">`,
);
}
const keywords = [
signals.tag,
...(Array.isArray(meta.keywords) ? meta.keywords : []),
]
.map((item) => String(item ?? '').trim())
.filter(Boolean);
if (keywords.length > 0 && !hasMeta(source, 'name', 'keywords')) {
tags.push(`<meta name="keywords" content="${escapeAttr(keywords.join(', '))}">`);
}
if (canonicalEnabled) {
const canonicalUrl = resolveCanonicalUrl({ publication, pageUrl, origin });
if (canonicalUrl && !hasLinkRel(source, 'canonical')) {
links.push(`<link rel="canonical" href="${escapeAttr(canonicalUrl)}">`);
}
}
const existingLang = source.match(/<html[^>]*\blang=["']([^"']+)["']/i)?.[1];
if (!existingLang) {
// Leave html lang to author; only add content-language when absent.
if (!hasMeta(source, 'http-equiv', 'content-language')) {
tags.push('<meta http-equiv="content-language" content="zh-CN">');
}
}
const block = [...tags, ...links].map((item) => ` ${item}`).join('\n');
if (!block) return source;
return appendBeforeHeadClose(source, `\n${block}\n`);
}
export function extractSeoDescription(html) {
return (
rawMetaContent(html, 'property', 'og:description') ||
rawMetaContent(html, 'name', 'description') ||
extractCoverSignals(String(html ?? '')).subtitle ||
''
);
}