import type { Blockquote, Heading, List, ListItem, Paragraph, Root, RootContent, ThematicBreak, } from "mdast"; import { toMarkdown } from "mdast-util-to-markdown"; import { absoluteUrl } from "./url.js"; import type { CitableSection, FaqItem, JsonLdNode, LlmsSiteConfig, SiteSeoConfig, } from "./types.js"; import { faqJsonLd } from "./jsonld.js"; function heading(depth: 1 | 2 | 3, value: string): Heading { return { type: "heading", depth, children: [{ type: "text", value }] }; } function paragraph(value: string): Paragraph { return { type: "paragraph", children: [{ type: "text", value }] }; } function bullets(items: string[]): List { return { type: "list", ordered: false, spread: false, children: items.map( (item): ListItem => ({ type: "listItem", spread: false, children: [paragraph(item)], }), ), }; } function quote(value: string): Blockquote { return { type: "blockquote", children: [paragraph(value)] }; } function hr(): ThematicBreak { return { type: "thematicBreak" }; } function linkList(pages: NonNullable, siteUrl: string): List { return { type: "list", ordered: false, spread: false, children: pages.map((p): ListItem => { const url = absoluteUrl(siteUrl, p.path); const children: NonNullable = [ { type: "paragraph", children: [ { type: "link", url, children: [{ type: "text", value: p.title }] }, ...(p.description ? [{ type: "text" as const, value: ` — ${p.description}` }] : []), ], }, ]; return { type: "listItem", spread: false, children }; }), }; } function render(children: RootContent[]): string { const tree: Root = { type: "root", children }; return toMarkdown(tree, { bullet: "-", rule: "-", ruleRepetition: 3, fences: true }); } /** * 生成 llms.txt(GEO):面向 LLM 的站点地图式说明。 * 规格参考:https://llmstxt.org(约定式,非正式标准)。 */ export function buildLlmsTxt(site: SiteSeoConfig): string { const cfg: LlmsSiteConfig = site.llms ?? { summary: site.description }; const children: RootContent[] = [heading(1, site.name), quote(cfg.summary)]; if (cfg.details?.length) { for (const d of cfg.details) children.push(paragraph(d)); } if (cfg.pages?.length) { children.push(heading(2, "页面"), linkList(cfg.pages, site.siteUrl)); } if (cfg.discouraged?.length) { children.push( heading(2, "可选"), paragraph("以下内容通常无需纳入回答依据:"), bullets(cfg.discouraged), ); } children.push( hr(), paragraph(`站点: ${absoluteUrl(site.siteUrl, "/")}`), paragraph(`完整内容索引: ${absoluteUrl(site.siteUrl, "/llms-full.txt")}`), ); return render(children); } /** * 生成 llms-full.txt:把可引用章节 + FAQ 拼成整站长文, * 供需要全文上下文的抓取方一次拉取。 */ export function buildLlmsFullTxt( site: SiteSeoConfig, input: { title?: string; sections?: Array; faqs?: FaqItem[]; extraMarkdown?: string; }, ): string { const children: RootContent[] = [ heading(1, input.title ?? site.name), paragraph(site.description), ]; if (input.sections?.length) { for (const s of input.sections) { children.push(heading(2, s.heading), paragraph(s.summary)); if (s.points?.length) children.push(bullets(s.points)); if (s.path) { children.push(paragraph(`来源: ${absoluteUrl(site.siteUrl, s.path)}`)); } } } if (input.faqs?.length) { children.push(heading(2, "常见问题")); for (const f of input.faqs) { children.push(heading(3, f.question), paragraph(f.answer)); } } if (input.extraMarkdown) { children.push(paragraph(input.extraMarkdown.trim())); } children.push( hr(), paragraph(`由 ${site.name} 提供 · ${absoluteUrl(site.siteUrl, "/llms.txt")}`), ); return render(children); } /** FAQ 同时产出 JSON-LD(SEO)与 Markdown 片段(GEO)。 */ export function faqToJsonLdAndMarkdown(faqs: FaqItem[]): { jsonLd: JsonLdNode; markdown: string; } { const children: RootContent[] = []; for (const f of faqs) { children.push( { type: "paragraph", children: [ { type: "strong", children: [{ type: "text", value: `Q: ${f.question}` }], }, ], }, paragraph(f.answer), ); } return { jsonLd: faqJsonLd(faqs), markdown: render(children), }; } /** 将可引用章节渲染为紧凑 Markdown(嵌入文章底部利于摘录)。 */ export function citableSectionMarkdown(section: CitableSection): string { const children: RootContent[] = [heading(2, section.heading), paragraph(section.summary)]; if (section.points?.length) children.push(bullets(section.points)); return render(children); }