acca846856
Memind CI / Test, build, and release guards (pull_request) Failing after 4m17s
引入批量回填与审计工具链,强制 mindspace-geo 元数据,扩展 sitemap/llms 收录 /learn,并为 Plaza 壳页注入 canonical/OG/JSON-LD;本地 593 页已通过 check:mindspace-cover。 Co-authored-by: Cursor <cursoragent@cursor.com>
138 lines
4.2 KiB
JavaScript
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('&', '&')
|
|
.replaceAll('"', '"')
|
|
.replaceAll('<', '<')
|
|
.replaceAll('>', '>');
|
|
}
|
|
|
|
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 ||
|
|
''
|
|
);
|
|
}
|