Files
easygoadmin-wiki/EasyGoAdmin_GoFrame_EleVue/.vitepress/config/extract.mjs
T
2026-10-25 17:24:27 +08:00

123 lines
3.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* SEO/GEO 共享工具(纯 ESM,零依赖)。
*
* 同时被两类调用方 import:
* 1. VitePress 配置(.ts,esbuild 打包时内联本文件)
* 2. scripts/generate-llms-txt.mjs(Node 直接运行)
*
* 注意:SITE_URL 是全站唯一 URL 真相源。
* docs 子域当前 TLS 证书为腾讯 CDN 默认证书(*.cdn.myqcloud.com,校验失败),
* 故暂用 http;证书修复后只需改这里一处。
*/
export const SITE_URL = "http://docs.goframe.elevue.easygoadmin.vip";
export const SITE_DESCRIPTION =
"EasyGoAdmin 企业级多租户后台管理基座 - GoFrame v2 + Vue3 + ElementPlus";
/**
* 剥离 H1/标题中的章节编号前缀,用于 <title> 与 JSON-LD headline。
* 例:"5.10 字典模块" → "字典模块";"K. 贡献指南" → "贡献指南"。
* 页内 H1 与侧边栏编号保持不变,仅影响 SEO 输出。
*/
export function stripNumberPrefix(title) {
return String(title)
.replace(/^(?:\d+(?:\.\d+)+|[A-Z]\.)\s+/, "")
.trim();
}
/**
* 从页面 Markdown 提取 meta description:
* 1. 首选 `:::tip xxx` 容器内的首句(141/143 页的标准 opener)
* 2. 回退到 H1 后的第一个普通段落
* 输出压缩为单行、去除行内 Markdown,按中文句末标点截断(≤100 字符)。
*/
export function extractDescription(md) {
let body = String(md).replace(/^---[\s\S]*?\n---\s*/, "");
let text = "";
const tip = body.match(/^:::\s*tip[^\n]*\n([\s\S]*?)\n:::/m);
if (tip) {
text = tip[1];
} else {
const afterH1 = body.match(/^# (?!#)(.+)$/m);
if (afterH1) {
const rest = body.slice((afterH1.index ?? 0) + afterH1[0].length);
const para = rest
.split(/\n{2,}/)
.find(
(p) =>
p.trim() &&
!/^\s*#/.test(p) &&
!/^\s*:::/.test(p) &&
!/^\s*```/.test(p) &&
!/^\s*\|/.test(p),
);
text = para || "";
}
}
text = text
.replace(/\s+/g, " ")
.replace(/`([^`]+)`/g, "$1")
.replace(/\*\*([^*]+)\*\*/g, "$1")
.replace(/\[([^\]]+)\]\([^)]*\)/g, "$1")
.trim();
if (!text) return "";
// 按中文句末标点截取首句,上限 100 字符;截断时避免切碎词
const m = text.match(/^.{1,100}?(?:[。!?!?]|$)/);
let out = m ? m[0] : text.slice(0, 100);
if (out.length < text.length && !/[。!?!?]$/.test(out)) {
out = out.replace(/[^\s,、;:,;:]+$/, "");
}
return out.trim() || text.slice(0, 100);
}
function cleanText(s) {
return s
.replace(/^:::[^\n]*$/gm, "")
.replace(/^```[^\n]*$/gm, "")
.replace(/`([^`]+)`/g, "$1")
.replace(/\*\*([^*]+)\*\*/g, "$1")
.replace(/\[([^\]]+)\]\([^)]*\)/g, "$1")
.replace(/\s+/g, " ")
.trim();
}
/**
* 提取页面中的 Q/A 对(用于 FAQPage JSON-LD),支持两种站内格式:
* A. `### Q:问题` 标题式(如 zh/quickstart/faq.md)
* B. `**Q:问题**` 加粗式(如 zh/appendix/faq.md)
* 返回 [{ q, a }];无匹配返回 []。
*/
export function extractFaqs(md) {
const body = String(md).replace(/^---[\s\S]*?\n---\s*/, "");
let faqs = [];
// 格式 A:按 `### Q:` 切块,答案截断到下一个标题
for (const part of body.split(/(?=^###\s*Q[::])/m)) {
const m = part.match(/^###\s*Q[::]\s*([^\n]+)\n+([\s\S]*)/);
if (!m) continue;
let a = m[2];
const cut = a.search(/^#{1,4}\s/m);
if (cut >= 0) a = a.slice(0, cut);
faqs.push({ q: cleanText(m[1]), a: cleanText(a) });
}
// 格式 B:按 `**Q:` 切块
if (!faqs.length) {
for (const part of body.split(/(?=^\*\*Q[::])/m)) {
const m = part.match(/^\*\*Q[::]\s*([^*]+)\*\*\s*\n+([\s\S]*)/);
if (!m) continue;
let a = m[2];
const cut = a.search(/^(?:#{1,4}\s|\*\*Q)/m);
if (cut >= 0) a = a.slice(0, cut);
faqs.push({ q: cleanText(m[1]), a: cleanText(a) });
}
}
return faqs.filter((f) => f.q && f.a);
}