feat(seo-geo): 全页面 SEO/GEO 回填、统一检查与 Plaza 注入
Memind CI / Test, build, and release guards (pull_request) Failing after 4m17s
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>
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Batch backfill mindspace-geo / description / minimal cover for HTML pages.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/backfill-mindspace-geo.mjs [--dry-run] [--no-fix-warnings]
|
||||
* node scripts/backfill-mindspace-geo.mjs --scope all --user <uuid>
|
||||
* node scripts/backfill-mindspace-geo.mjs --file public/hello-john.html
|
||||
*/
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { backfillMindspaceSeoGeoHtml } from '../mindspace-geo-backfill.mjs';
|
||||
import { collectHtmlTargets } from '../mindspace-seo-audit.mjs';
|
||||
|
||||
const repoRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const DEFAULT_PUBLISH_ROOT = 'MindSpace';
|
||||
|
||||
function parseArgs(argv) {
|
||||
let scope = 'all';
|
||||
let root = path.join(repoRoot, DEFAULT_PUBLISH_ROOT);
|
||||
let publicRoot = path.join(repoRoot, 'public');
|
||||
let userId = null;
|
||||
let file = null;
|
||||
let dryRun = false;
|
||||
let forceGeo = false;
|
||||
let fixWarnings = true;
|
||||
|
||||
for (let i = 2; i < argv.length; i += 1) {
|
||||
const arg = argv[i];
|
||||
if (arg === '--dry-run') dryRun = true;
|
||||
else if (arg === '--force-geo') forceGeo = true;
|
||||
else if (arg === '--no-fix-warnings') fixWarnings = false;
|
||||
else if (arg === '--scope' && argv[i + 1]) {
|
||||
scope = argv[i + 1];
|
||||
i += 1;
|
||||
} else if (arg === '--root' && argv[i + 1]) {
|
||||
root = path.resolve(argv[i + 1]);
|
||||
i += 1;
|
||||
} else if (arg === '--public-root' && argv[i + 1]) {
|
||||
publicRoot = path.resolve(argv[i + 1]);
|
||||
i += 1;
|
||||
} else if (arg === '--user' && argv[i + 1]) {
|
||||
userId = argv[i + 1];
|
||||
i += 1;
|
||||
} else if (arg === '--file' && argv[i + 1]) {
|
||||
file = path.resolve(argv[i + 1]);
|
||||
i += 1;
|
||||
} else if (arg === '--help' || arg === '-h') {
|
||||
console.log(`Usage: node scripts/backfill-mindspace-geo.mjs [--scope mindspace|platform|all] [--dry-run] [--force-geo] [--user <uuid>] [--file <html>]`);
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
return { scope, root, publicRoot, userId, file, dryRun, forceGeo, fixWarnings };
|
||||
}
|
||||
|
||||
const { scope, root, publicRoot, userId, file, dryRun, forceGeo, fixWarnings } = parseArgs(process.argv);
|
||||
const targets = collectHtmlTargets({ scope, mindspaceRoot: root, publicRoot, userId, file });
|
||||
|
||||
let changed = 0;
|
||||
let skipped = 0;
|
||||
|
||||
for (const htmlPath of targets) {
|
||||
const before = fs.readFileSync(htmlPath, 'utf8');
|
||||
const after = backfillMindspaceSeoGeoHtml(before, {
|
||||
forceGeo,
|
||||
fixWarnings,
|
||||
htmlFilePath: htmlPath,
|
||||
});
|
||||
if (after === before) {
|
||||
skipped += 1;
|
||||
continue;
|
||||
}
|
||||
changed += 1;
|
||||
if (dryRun) {
|
||||
console.log(`[dry-run] would update ${htmlPath}`);
|
||||
continue;
|
||||
}
|
||||
fs.writeFileSync(htmlPath, after, 'utf8');
|
||||
console.log(`updated ${htmlPath}`);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`${dryRun ? '[dry-run] ' : ''}done: ${changed} updated, ${skipped} unchanged, ${targets.length} scanned (scope=${scope})`,
|
||||
);
|
||||
@@ -33,6 +33,7 @@ const upgraded = {
|
||||
<title>{{PAGE_TITLE}}</title>
|
||||
<meta name="description" content="{{PAGE_DESCRIPTION}}">
|
||||
<meta name="mindspace-cover" content='{{MINDSPACE_COVER_JSON}}'>
|
||||
<meta name="mindspace-geo" content='{{MINDSPACE_GEO_JSON}}'>
|
||||
<style>
|
||||
:root { --accent: {{ACCENT}}; --accent2: {{ACCENT2}}; --ink: #f5f7fa; --muted: #a8b8cc; --bg: #060d18; }
|
||||
* { box-sizing: border-box; }
|
||||
@@ -73,6 +74,7 @@ const upgraded = {
|
||||
<title>{{PAGE_TITLE}}</title>
|
||||
<meta name="description" content="{{PAGE_DESCRIPTION}}">
|
||||
<meta name="mindspace-cover" content='{{MINDSPACE_COVER_JSON}}'>
|
||||
<meta name="mindspace-geo" content='{{MINDSPACE_GEO_JSON}}'>
|
||||
<style>
|
||||
:root { --accent: {{ACCENT}}; --accent2: {{ACCENT2}}; }
|
||||
* { box-sizing: border-box; }
|
||||
@@ -117,6 +119,7 @@ const upgraded = {
|
||||
<title>{{PAGE_TITLE}}</title>
|
||||
<meta name="description" content="{{PAGE_DESCRIPTION}}">
|
||||
<meta name="mindspace-cover" content='{{MINDSPACE_COVER_JSON}}'>
|
||||
<meta name="mindspace-geo" content='{{MINDSPACE_GEO_JSON}}'>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; min-height: 100vh; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", sans-serif; background: radial-gradient(ellipse 120% 80% at 50% -20%, #e8f5ef, #f7f4ed 45%, #f0ebe3 100%); color: #1a2420; }
|
||||
@@ -173,6 +176,7 @@ const upgraded = {
|
||||
<title>{{PAGE_TITLE}}</title>
|
||||
<meta name="description" content="{{PAGE_DESCRIPTION}}">
|
||||
<meta name="mindspace-cover" content='{{MINDSPACE_COVER_JSON}}'>
|
||||
<meta name="mindspace-geo" content='{{MINDSPACE_GEO_JSON}}'>
|
||||
<style>
|
||||
:root { --accent: {{ACCENT}}; --gold: #e8c896; --bg: #120c06; --card: rgba(255,248,240,.04); }
|
||||
* { box-sizing: border-box; }
|
||||
@@ -209,6 +213,7 @@ const upgraded = {
|
||||
<title>{{PAGE_TITLE}}</title>
|
||||
<meta name="description" content="{{PAGE_DESCRIPTION}}">
|
||||
<meta name="mindspace-cover" content='{{MINDSPACE_COVER_JSON}}'>
|
||||
<meta name="mindspace-geo" content='{{MINDSPACE_GEO_JSON}}'>
|
||||
<style>
|
||||
:root { --accent: {{ACCENT}}; --bg: #0a0c10; --card: #141820; --border: #252d3d; }
|
||||
* { box-sizing: border-box; }
|
||||
@@ -251,6 +256,7 @@ const upgraded2 = {
|
||||
<title>{{PAGE_TITLE}}</title>
|
||||
<meta name="description" content="{{PAGE_DESCRIPTION}}">
|
||||
<meta name="mindspace-cover" content='{{MINDSPACE_COVER_JSON}}'>
|
||||
<meta name="mindspace-geo" content='{{MINDSPACE_GEO_JSON}}'>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; min-height: 100vh; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", sans-serif; background: radial-gradient(ellipse 80% 60% at 50% -10%, #3d1f5c, #0f1419 55%); color: #fff; display: grid; place-items: center; padding: 40px 24px; }
|
||||
@@ -282,6 +288,7 @@ const upgraded2 = {
|
||||
<title>{{PAGE_TITLE}}</title>
|
||||
<meta name="description" content="{{PAGE_DESCRIPTION}}">
|
||||
<meta name="mindspace-cover" content='{{MINDSPACE_COVER_JSON}}'>
|
||||
<meta name="mindspace-geo" content='{{MINDSPACE_GEO_JSON}}'>
|
||||
<style>
|
||||
:root { --accent: {{ACCENT}}; }
|
||||
* { box-sizing: border-box; }
|
||||
@@ -313,6 +320,7 @@ const upgraded2 = {
|
||||
<title>{{PAGE_TITLE}}</title>
|
||||
<meta name="description" content="{{PAGE_DESCRIPTION}}">
|
||||
<meta name="mindspace-cover" content='{{MINDSPACE_COVER_JSON}}'>
|
||||
<meta name="mindspace-geo" content='{{MINDSPACE_GEO_JSON}}'>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", sans-serif; background: #f4f7f6; color: #0f2a24; }
|
||||
@@ -349,6 +357,7 @@ const upgraded2 = {
|
||||
<title>{{PAGE_TITLE}}</title>
|
||||
<meta name="description" content="{{PAGE_DESCRIPTION}}">
|
||||
<meta name="mindspace-cover" content='{{MINDSPACE_COVER_JSON}}'>
|
||||
<meta name="mindspace-geo" content='{{MINDSPACE_GEO_JSON}}'>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; font-family: "Songti SC", "Noto Serif SC", Georgia, serif; background: #fafaf8; color: #1c1917; line-height: 1.85; }
|
||||
@@ -380,6 +389,7 @@ const upgraded2 = {
|
||||
<title>{{PAGE_TITLE}}</title>
|
||||
<meta name="description" content="{{PAGE_DESCRIPTION}}">
|
||||
<meta name="mindspace-cover" content='{{MINDSPACE_COVER_JSON}}'>
|
||||
<meta name="mindspace-geo" content='{{MINDSPACE_GEO_JSON}}'>
|
||||
<style>
|
||||
:root { --accent: {{ACCENT}}; }
|
||||
* { box-sizing: border-box; }
|
||||
@@ -417,6 +427,7 @@ const upgraded2 = {
|
||||
<title>{{PAGE_TITLE}}</title>
|
||||
<meta name="description" content="{{PAGE_DESCRIPTION}}">
|
||||
<meta name="mindspace-cover" content='{{MINDSPACE_COVER_JSON}}'>
|
||||
<meta name="mindspace-geo" content='{{MINDSPACE_GEO_JSON}}'>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; min-height: 100vh; font-family: "Songti SC", "Noto Serif SC", serif; background: radial-gradient(ellipse 100% 80% at 50% 0%, #fff5f9, #fdf2f8 50%, #fce7f3); color: #701a56; display: grid; place-items: center; padding: 48px 24px; }
|
||||
@@ -448,6 +459,7 @@ const upgraded2 = {
|
||||
<title>{{PAGE_TITLE}}</title>
|
||||
<meta name="description" content="{{PAGE_DESCRIPTION}}">
|
||||
<meta name="mindspace-cover" content='{{MINDSPACE_COVER_JSON}}'>
|
||||
<meta name="mindspace-geo" content='{{MINDSPACE_GEO_JSON}}'>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", sans-serif; background: #030806; color: #eafff0; }
|
||||
@@ -478,6 +490,7 @@ const upgraded2 = {
|
||||
<title>{{PAGE_TITLE}}</title>
|
||||
<meta name="description" content="{{PAGE_DESCRIPTION}}">
|
||||
<meta name="mindspace-cover" content='{{MINDSPACE_COVER_JSON}}'>
|
||||
<meta name="mindspace-geo" content='{{MINDSPACE_GEO_JSON}}'>
|
||||
<style>
|
||||
:root { --accent: {{ACCENT}}; }
|
||||
* { box-sizing: border-box; }
|
||||
@@ -522,6 +535,7 @@ const newTemplates = {
|
||||
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{PAGE_TITLE}}</title><meta name="description" content="{{PAGE_DESCRIPTION}}">
|
||||
<meta name="mindspace-cover" content='{{MINDSPACE_COVER_JSON}}'>
|
||||
<meta name="mindspace-geo" content='{{MINDSPACE_GEO_JSON}}'>
|
||||
<style>
|
||||
:root { --accent: {{ACCENT}}; --accent2: {{ACCENT2}}; }
|
||||
* { box-sizing: border-box; }
|
||||
@@ -568,6 +582,7 @@ const newTemplates = {
|
||||
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{PAGE_TITLE}}</title><meta name="description" content="{{PAGE_DESCRIPTION}}">
|
||||
<meta name="mindspace-cover" content='{{MINDSPACE_COVER_JSON}}'>
|
||||
<meta name="mindspace-geo" content='{{MINDSPACE_GEO_JSON}}'>
|
||||
<style>
|
||||
:root { --accent: {{ACCENT}}; --accent2: {{ACCENT2}}; }
|
||||
* { box-sizing: border-box; }
|
||||
@@ -612,6 +627,7 @@ const newTemplates = {
|
||||
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{PAGE_TITLE}}</title><meta name="description" content="{{PAGE_DESCRIPTION}}">
|
||||
<meta name="mindspace-cover" content='{{MINDSPACE_COVER_JSON}}'>
|
||||
<meta name="mindspace-geo" content='{{MINDSPACE_GEO_JSON}}'>
|
||||
<style>
|
||||
:root { --accent: {{ACCENT}}; }
|
||||
* { box-sizing: border-box; }
|
||||
@@ -652,6 +668,7 @@ const newTemplates = {
|
||||
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{PAGE_TITLE}}</title><meta name="description" content="{{PAGE_DESCRIPTION}}">
|
||||
<meta name="mindspace-cover" content='{{MINDSPACE_COVER_JSON}}'>
|
||||
<meta name="mindspace-geo" content='{{MINDSPACE_GEO_JSON}}'>
|
||||
<style>
|
||||
:root { --accent: {{ACCENT}}; }
|
||||
* { box-sizing: border-box; }
|
||||
@@ -689,6 +706,7 @@ const newTemplates = {
|
||||
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{PAGE_TITLE}}</title><meta name="description" content="{{PAGE_DESCRIPTION}}">
|
||||
<meta name="mindspace-cover" content='{{MINDSPACE_COVER_JSON}}'>
|
||||
<meta name="mindspace-geo" content='{{MINDSPACE_GEO_JSON}}'>
|
||||
<style>
|
||||
:root { --accent: {{ACCENT}}; --accent2: {{ACCENT2}}; }
|
||||
* { box-sizing: border-box; }
|
||||
@@ -732,6 +750,7 @@ const newTemplates = {
|
||||
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{PAGE_TITLE}}</title><meta name="description" content="{{PAGE_DESCRIPTION}}">
|
||||
<meta name="mindspace-cover" content='{{MINDSPACE_COVER_JSON}}'>
|
||||
<meta name="mindspace-geo" content='{{MINDSPACE_GEO_JSON}}'>
|
||||
<style>
|
||||
:root { --accent: {{ACCENT}}; }
|
||||
* { box-sizing: border-box; }
|
||||
@@ -771,6 +790,7 @@ const newTemplates = {
|
||||
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{PAGE_TITLE}}</title><meta name="description" content="{{PAGE_DESCRIPTION}}">
|
||||
<meta name="mindspace-cover" content='{{MINDSPACE_COVER_JSON}}'>
|
||||
<meta name="mindspace-geo" content='{{MINDSPACE_GEO_JSON}}'>
|
||||
<style>
|
||||
:root { --accent: {{ACCENT}}; --accent2: {{ACCENT2}}; }
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
@@ -1,36 +1,41 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Scan MindSpace public/*.html for mindspace-cover compliance (scheme A gate).
|
||||
* Scan HTML pages for mindspace-cover + SEO/GEO compliance.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/check-mindspace-cover.mjs
|
||||
* node scripts/check-mindspace-cover.mjs --scope all
|
||||
* node scripts/check-mindspace-cover.mjs --user <uuid>
|
||||
* node scripts/check-mindspace-cover.mjs --root /path/to/MindSpace
|
||||
* node scripts/check-mindspace-cover.mjs --file public/thumbnail-demo-samples/vietnam-guide-good.html
|
||||
*/
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { parseMindspaceCoverMeta } from '../mindspace-cover-meta.mjs';
|
||||
import { hasPlatformBrandMarker } from '../mindspace-page-tag.mjs';
|
||||
import {
|
||||
hasMindspaceCoverMeta,
|
||||
hasRasterShareImageHint,
|
||||
hasShareDescription,
|
||||
} from '../wechat/verify/share-preview.mjs';
|
||||
auditHtmlFile,
|
||||
collectHtmlTargets,
|
||||
} from '../mindspace-seo-audit.mjs';
|
||||
|
||||
const repoRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const DEFAULT_PUBLISH_ROOT = 'MindSpace';
|
||||
|
||||
function parseArgs(argv) {
|
||||
let scope = 'all';
|
||||
let root = path.join(repoRoot, DEFAULT_PUBLISH_ROOT);
|
||||
let publicRoot = path.join(repoRoot, 'public');
|
||||
let userId = null;
|
||||
let file = null;
|
||||
let seo = false;
|
||||
let seo = true;
|
||||
for (let i = 2; i < argv.length; i += 1) {
|
||||
if (argv[i] === '--root' && argv[i + 1]) {
|
||||
if (argv[i] === '--scope' && argv[i + 1]) {
|
||||
scope = argv[i + 1];
|
||||
i += 1;
|
||||
} else if (argv[i] === '--root' && argv[i + 1]) {
|
||||
root = path.resolve(argv[i + 1]);
|
||||
i += 1;
|
||||
} else if (argv[i] === '--public-root' && argv[i + 1]) {
|
||||
publicRoot = path.resolve(argv[i + 1]);
|
||||
i += 1;
|
||||
} else if (argv[i] === '--user' && argv[i + 1]) {
|
||||
userId = argv[i + 1];
|
||||
i += 1;
|
||||
@@ -39,146 +44,49 @@ function parseArgs(argv) {
|
||||
i += 1;
|
||||
} else if (argv[i] === '--seo') {
|
||||
seo = true;
|
||||
} else if (argv[i] === '--skip-seo') {
|
||||
seo = false;
|
||||
} else if (argv[i] === '--help' || argv[i] === '-h') {
|
||||
console.log(`Usage: node scripts/check-mindspace-cover.mjs [--root ${DEFAULT_PUBLISH_ROOT}] [--user <uuid>] [--file <html>] [--seo]`);
|
||||
console.log(`Usage: node scripts/check-mindspace-cover.mjs [--scope mindspace|platform|all] [--root ${DEFAULT_PUBLISH_ROOT}] [--user <uuid>] [--file <html>] [--skip-seo]`);
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
return { root, userId, file, seo };
|
||||
return { scope, root, publicRoot, userId, file, seo };
|
||||
}
|
||||
|
||||
function hasCoverImageHint(html) {
|
||||
if (hasRasterShareImageHint(html)) return true;
|
||||
const coverMeta = parseMindspaceCoverMeta(html);
|
||||
const cover = String(coverMeta?.cover ?? coverMeta?.image ?? '').trim();
|
||||
if (!cover) return false;
|
||||
if (/^https?:\/\//i.test(cover)) return true;
|
||||
if (/\.svg(?:[?#]|$)/i.test(cover)) return false;
|
||||
return true;
|
||||
function isPlatformOnlyPath(htmlPath, publicRoot) {
|
||||
const resolvedPublicRoot = path.resolve(publicRoot);
|
||||
const resolved = path.resolve(htmlPath);
|
||||
return resolved.startsWith(`${resolvedPublicRoot}${path.sep}`);
|
||||
}
|
||||
|
||||
function missingLocalCoverPath(htmlPath, coverMeta) {
|
||||
const cover = String(coverMeta?.cover ?? coverMeta?.image ?? '').trim();
|
||||
if (!cover || /^https?:\/\//i.test(cover) || cover.startsWith('data:')) return null;
|
||||
const clean = cover.split('?')[0].split('#')[0].replace(/^\.\//, '');
|
||||
if (!clean || clean.startsWith('/') || clean.includes('\\')) return null;
|
||||
const base = path.dirname(htmlPath);
|
||||
const target = path.resolve(base, clean);
|
||||
if (target !== base && !target.startsWith(`${base}${path.sep}`)) return clean;
|
||||
return fs.existsSync(target) && fs.statSync(target).isFile() ? null : clean;
|
||||
}
|
||||
|
||||
function auditCoverHtml(htmlPath, html) {
|
||||
const issues = [];
|
||||
if (!hasMindspaceCoverMeta(html)) {
|
||||
issues.push({ level: 'error', code: 'missing_mindspace_cover', message: '缺少 <meta name="mindspace-cover">' });
|
||||
}
|
||||
if (!hasShareDescription(html)) {
|
||||
issues.push({ level: 'error', code: 'missing_description', message: '缺少 description / og:description' });
|
||||
}
|
||||
if (!hasPlatformBrandMarker(html)) {
|
||||
issues.push({ level: 'warn', code: 'missing_platform_brand', message: '缺少 data-mindspace-page-tag="platform-brand" 页脚' });
|
||||
}
|
||||
const coverMeta = parseMindspaceCoverMeta(html);
|
||||
if (coverMeta) {
|
||||
if (!String(coverMeta.tag ?? '').trim()) {
|
||||
issues.push({ level: 'warn', code: 'missing_tag', message: 'mindspace-cover 缺少 tag' });
|
||||
}
|
||||
if (!String(coverMeta.subtitle ?? '').trim()) {
|
||||
issues.push({ level: 'warn', code: 'missing_subtitle', message: 'mindspace-cover 缺少 subtitle' });
|
||||
}
|
||||
if (!String(coverMeta.accent ?? '').trim()) {
|
||||
issues.push({ level: 'warn', code: 'missing_accent', message: 'mindspace-cover 缺少 accent' });
|
||||
}
|
||||
const missingCover = missingLocalCoverPath(htmlPath, coverMeta);
|
||||
if (missingCover) {
|
||||
issues.push({
|
||||
level: 'error',
|
||||
code: 'missing_cover_file',
|
||||
message: `mindspace-cover 引用的本地文件不存在:${missingCover}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!hasCoverImageHint(html)) {
|
||||
issues.push({
|
||||
level: 'warn',
|
||||
code: 'missing_cover_image',
|
||||
message: '无 raster 封面图(cover/image/og:image),缩略图会退化为纯色渐变',
|
||||
});
|
||||
}
|
||||
return issues;
|
||||
}
|
||||
|
||||
function auditSeoHtml(html) {
|
||||
const issues = [];
|
||||
const title = String(html).match(/<title[^>]*>([^<]+)<\/title>/i)?.[1]?.replace(/\s+/g, ' ').trim() ?? '';
|
||||
if (!title || title.length < 4) {
|
||||
issues.push({ level: 'warn', code: 'weak_title', message: '缺少或过短的 <title>,SEO/GEO 质量会下降' });
|
||||
} else if (/^(页面|我的页面|未命名|demo|test|MindSpace 页面)$/i.test(title)) {
|
||||
issues.push({ level: 'warn', code: 'generic_title', message: `title 过于泛化:${title}` });
|
||||
}
|
||||
if (!/<h1\b/i.test(html)) {
|
||||
issues.push({ level: 'warn', code: 'missing_h1', message: '缺少 <h1>,搜索引擎与 AI 抽取质量会下降' });
|
||||
}
|
||||
if (!hasShareDescription(html)) {
|
||||
issues.push({ level: 'warn', code: 'seo_missing_description', message: '缺少 description,SEO/GEO 摘要来源不足' });
|
||||
}
|
||||
return issues;
|
||||
}
|
||||
|
||||
function collectHtmlFiles(root, userId) {
|
||||
const files = [];
|
||||
const walk = (dir) => {
|
||||
if (!fs.existsSync(dir)) return;
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
if (entry.name === '.agents' || entry.name === 'node_modules' || entry.name.startsWith('.')) continue;
|
||||
walk(full);
|
||||
continue;
|
||||
}
|
||||
if (!entry.name.endsWith('.html') || entry.name.endsWith('.thumbnail.svg')) continue;
|
||||
files.push(full);
|
||||
}
|
||||
};
|
||||
|
||||
if (userId) {
|
||||
walk(path.join(root, userId, 'public'));
|
||||
return files;
|
||||
}
|
||||
|
||||
if (!fs.existsSync(root)) return files;
|
||||
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
||||
if (!entry.isDirectory() || entry.name === 'wiki' || entry.name.startsWith('.')) continue;
|
||||
walk(path.join(root, entry.name, 'public'));
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
const { root, userId, file, seo } = parseArgs(process.argv);
|
||||
const targets = file ? [file] : collectHtmlFiles(root, userId);
|
||||
const { scope, root, publicRoot, userId, file, seo } = parseArgs(process.argv);
|
||||
const targets = collectHtmlTargets({ scope, mindspaceRoot: root, publicRoot, userId, file });
|
||||
const results = [];
|
||||
|
||||
for (const htmlPath of targets) {
|
||||
const html = fs.readFileSync(htmlPath, 'utf8');
|
||||
const issues = auditCoverHtml(htmlPath, html);
|
||||
if (seo) issues.push(...auditSeoHtml(html));
|
||||
const platformOnly = isPlatformOnlyPath(htmlPath, publicRoot);
|
||||
const issues = auditHtmlFile(htmlPath, html, {
|
||||
seo,
|
||||
requireCoverMeta: !platformOnly,
|
||||
});
|
||||
if (issues.length) {
|
||||
results.push({ htmlPath, issues });
|
||||
}
|
||||
}
|
||||
|
||||
if (results.length === 0) {
|
||||
const scope = file ?? `${root}${userId ? ` (user ${userId})` : ''}`;
|
||||
console.log(`OK: ${targets.length} 个 HTML 页面均符合 mindspace-cover 规范 (${scope})`);
|
||||
const scopeLabel = file ?? `${scope} @ ${root}${userId ? ` (user ${userId})` : ''}`;
|
||||
console.log(`OK: ${targets.length} 个 HTML 页面均符合 mindspace-cover + SEO/GEO 规范 (${scopeLabel})`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
let errors = 0;
|
||||
let warns = 0;
|
||||
console.error(`Found cover issues in ${results.length} / ${targets.length} HTML files:`);
|
||||
console.error(`Found cover/SEO issues in ${results.length} / ${targets.length} HTML files:`);
|
||||
for (const { htmlPath, issues } of results) {
|
||||
const rel = file ? htmlPath : path.relative(root, htmlPath);
|
||||
const rel = file ? htmlPath : path.relative(repoRoot, htmlPath);
|
||||
console.error(`\n${rel}`);
|
||||
for (const issue of issues) {
|
||||
const prefix = issue.level === 'error' ? ' ✗' : ' ⚠';
|
||||
|
||||
Reference in New Issue
Block a user