签入版本
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* 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.fastapi.naivevue.djangoadmin.cn";
|
||||
|
||||
export const SITE_DESCRIPTION =
|
||||
"DjangoAdmin 企业级中台解决方案 - FastAPI + NaiveUI 前后端分离版本";
|
||||
|
||||
/**
|
||||
* 剥离 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) {
|
||||
const src = String(md);
|
||||
const fm = src.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
||||
const body = fm ? src.slice(fm[0].length) : src;
|
||||
|
||||
let text = "";
|
||||
// frontmatter description 显式声明优先(无 H1 页面的补齐手段)
|
||||
const fmDesc = fm && fm[1].match(/^description:\s*(.+)$/m);
|
||||
if (fmDesc) {
|
||||
text = fmDesc[1].trim().replace(/^["']|["']$/g, "");
|
||||
}
|
||||
const tip = !text && body.match(/^:::\s*tip[^\n]*\n([\s\S]*?)\n:::/m);
|
||||
if (tip) {
|
||||
text = tip[1];
|
||||
}
|
||||
if (!text) {
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user