app-template/packages/seo-geo/src/llms.ts

191 lines
5.5 KiB
TypeScript
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.

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<LlmsSiteConfig["pages"]>, siteUrl: string): List {
return {
type: "list",
ordered: false,
spread: false,
children: pages.map((p): ListItem => {
const url = absoluteUrl(siteUrl, p.path);
const children: NonNullable<ListItem["children"]> = [
{
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<CitableSection & { path?: string }>;
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);
}