Files
memind/mindspace-geo-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

158 lines
4.1 KiB
JavaScript

import { extractSharePreviewMeta, PLATFORM_SITE_NAME } from './mindspace-og-tags.mjs';
import {
parseMindspaceGeoMeta,
resolveGeoFaq,
} from './mindspace-geo-meta.mjs';
import { extractSeoDescription } from './mindspace-seo-tags.mjs';
function escapeAttr(value) {
return String(value ?? '')
.replaceAll('&', '&amp;')
.replaceAll('"', '&quot;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;');
}
function rawTitleFromHtml(html) {
return String(html ?? '').match(/<title[^>]*>([^<]*)<\/title>/i)?.[1]?.trim() ?? '';
}
function rawH1FromHtml(html) {
const match = String(html ?? '').match(/<h1[^>]*>([\s\S]*?)<\/h1>/i);
if (!match) return '';
return match[1].replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim();
}
function hasJsonLd(html) {
return /<script[^>]+type=["']application\/ld\+json["']/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 buildArticleJsonLd({
title,
description,
imageUrl,
pageUrl,
siteName = PLATFORM_SITE_NAME,
faq = [],
}) {
const graph = {
'@context': 'https://schema.org',
'@type': 'Article',
headline: title,
description,
inLanguage: 'zh-CN',
isAccessibleForFree: true,
publisher: {
'@type': 'Organization',
name: siteName,
},
};
if (pageUrl) graph.mainEntityOfPage = pageUrl;
if (imageUrl) graph.image = [imageUrl];
if (Array.isArray(faq) && faq.length > 0) {
graph.hasPart = {
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: faq
.map((item) => ({
'@type': 'Question',
name: String(item?.q ?? item?.question ?? '').trim(),
acceptedAnswer: {
'@type': 'Answer',
text: String(item?.a ?? item?.answer ?? '').trim(),
},
}))
.filter((item) => item.name && item.acceptedAnswer.text),
};
}
return graph;
}
export function buildGeoStructuredData(
html,
{
origin = '',
pageUrl = '',
pageDirUrl = '',
publication = null,
siteName = PLATFORM_SITE_NAME,
htmlFilePath = '',
fileExists = null,
meta = {},
} = {},
) {
const source = String(html ?? '');
const authorMeta = parseMindspaceGeoMeta(source);
const preview = extractSharePreviewMeta(source, {
origin,
pageUrl,
pageDirUrl,
siteName,
meta,
htmlFilePath,
fileExists,
});
const title =
rawMetaOgTitle(source) ||
rawTitleFromHtml(source) ||
rawH1FromHtml(source) ||
'MindSpace 页面';
const description =
String(authorMeta.summary ?? '').trim() ||
extractSeoDescription(source) ||
preview.description ||
'';
const canonical =
String(publication?.publicUrl ?? pageUrl ?? '').trim() || pageUrl;
const absoluteUrl = canonical.startsWith('http')
? canonical
: origin && canonical.startsWith('/')
? `${origin}${canonical}`
: pageUrl;
return buildArticleJsonLd({
title,
description,
imageUrl: preview.imageUrl,
pageUrl: absoluteUrl,
siteName: preview.siteName || siteName,
faq: resolveGeoFaq(source, authorMeta),
});
}
function rawMetaOgTitle(html) {
return (
String(html ?? '').match(
/<meta[^>]+property=["']og:title["'][^>]+content=["']([^"']+)["']/i,
)?.[1] ||
String(html ?? '').match(
/<meta[^>]+content=["']([^"']+)["'][^>]+property=["']og:title["']/i,
)?.[1] ||
''
);
}
export function injectGeoTags(
html,
ctx = {},
) {
const sourceHtml = String(ctx.sourceHtml ?? html ?? '');
const targetHtml = String(html ?? '');
if (hasJsonLd(targetHtml)) return targetHtml;
const payload = buildGeoStructuredData(sourceHtml, ctx);
const serialized = JSON.stringify(payload).replace(/</g, '\\u003c');
const block = `\n <script type="application/ld+json">${serialized}</script>\n`;
return appendBeforeHeadClose(targetHtml, block);
}