fde6503bdf
Memind CI / Test, build, and release guards (push) Has been cancelled
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>
169 lines
4.5 KiB
JavaScript
169 lines
4.5 KiB
JavaScript
import { extractCoverSignals } from './mindspace-thumbnails.mjs';
|
|
import { extractSharePreviewMeta, PLATFORM_SITE_NAME } from './mindspace-og-tags.mjs';
|
|
import { extractSeoDescription } from './mindspace-seo-tags.mjs';
|
|
|
|
function escapeAttr(value) {
|
|
return String(value ?? '')
|
|
.replaceAll('&', '&')
|
|
.replaceAll('"', '"')
|
|
.replaceAll('<', '<')
|
|
.replaceAll('>', '>');
|
|
}
|
|
|
|
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 parseMindspaceGeoMeta(html) {
|
|
const tag = String(html ?? '').match(/<meta[^>]*name=["']mindspace-geo["'][^>]*>/i)?.[0];
|
|
if (!tag) return {};
|
|
const contentMatch =
|
|
tag.match(/content=(["'])([\s\S]*?)\1/i) ?? tag.match(/content=["']([^"']+)["']/i);
|
|
const raw = contentMatch?.[2] ?? contentMatch?.[1];
|
|
if (!raw) return {};
|
|
try {
|
|
return JSON.parse(raw.replaceAll('"', '"'));
|
|
} catch {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
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: authorMeta.faq,
|
|
});
|
|
}
|
|
|
|
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);
|
|
}
|