Files
memind/mindspace-seo-tags.mjs
john acca846856
Memind CI / Test, build, and release guards (pull_request) Failing after 4m17s
feat(seo-geo): 全页面 SEO/GEO 回填、统一检查与 Plaza 注入
引入批量回填与审计工具链,强制 mindspace-geo 元数据,扩展 sitemap/llms 收录 /learn,并为 Plaza 壳页注入 canonical/OG/JSON-LD;本地 593 页已通过 check:mindspace-cover。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-16 09:36:01 +08:00

138 lines
4.2 KiB
JavaScript

import { resolveGeoKeywords } from './mindspace-geo-meta.mjs';
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 = resolveGeoKeywords(source);
for (const item of Array.isArray(meta.keywords) ? meta.keywords : []) {
const token = String(item ?? '').trim();
if (token && !keywords.includes(token)) keywords.push(token);
}
if (signals.tag && !keywords.includes(signals.tag)) keywords.unshift(signals.tag);
const keywordLine = keywords.slice(0, 12);
if (keywordLine.length > 0 && !hasMeta(source, 'name', 'keywords')) {
tags.push(`<meta name="keywords" content="${escapeAttr(keywordLine.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 ||
''
);
}