diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 8fbda8177..b60df5c54 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -32,6 +32,10 @@ jobs: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: persist-credentials: false + # docs:gather stamps each page with its source file's last commit + # date, which feeds the sitemap's . A shallow clone has a + # single commit, so every page would date to the clone day. + fetch-depth: 0 - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: diff --git a/packages/docs/.gitignore b/packages/docs/.gitignore index c6f7fe146..52e2f6ba2 100644 --- a/packages/docs/.gitignore +++ b/packages/docs/.gitignore @@ -6,10 +6,11 @@ /content # Generated static artifacts (.md siblings, llms.txt, llms-full.txt). -# Keep committed assets (img/) and the placeholder. +# Keep committed assets (img/, robots.txt) and the placeholder. /static/* !/static/.gitkeep !/static/img/ +!/static/robots.txt node_modules *.log diff --git a/packages/docs/docusaurus.config.ts b/packages/docs/docusaurus.config.ts index ee5167bd6..ddc3bd96b 100644 --- a/packages/docs/docusaurus.config.ts +++ b/packages/docs/docusaurus.config.ts @@ -61,8 +61,14 @@ const config: Config = { path: contentPath, routeBasePath: '/', sidebarPath: './sidebars.ts', + showLastUpdateTime: true, }, blog: false, + sitemap: { + lastmod: 'date', + changefreq: null, + priority: null, + }, theme: { customCss: './src/css/custom.css', }, diff --git a/packages/docs/scripts/lib/gather.js b/packages/docs/scripts/lib/gather.js index 1d66aa816..e9a0df374 100644 --- a/packages/docs/scripts/lib/gather.js +++ b/packages/docs/scripts/lib/gather.js @@ -91,12 +91,17 @@ async function readNodeMeta(nodeDir) { return { classType, title, description: extractDescription(text) }; } -/** Extract a first-sentence description from a raw services*.json text string. */ +/** + * Extract a first-sentence description from a raw services*.json text string. + * The JSON `description` is an array of complete lines, so they join with a + * space — joining bare runs them together ("a node.Can be invoked"), which also + * defeats the '. ' sentence split below and returns the whole blob. + */ function extractDescription(text) { const m = /"description"\s*:\s*\[([^\]]*)\]/.exec(text); if (!m) return ''; const parts = m[1].match(/"((?:[^"\\]|\\.)*)"/g) || []; - const full = parts.map((s) => s.slice(1, -1)).join('').trim(); + const full = parts.map((s) => s.slice(1, -1)).join(' ').replace(/\s+/g, ' ').trim(); const dot = full.indexOf('. '); return dot >= 0 ? full.slice(0, dot + 1) : full; } @@ -213,6 +218,62 @@ function frontMatterTitle(content) { return t ? t[2].replace(/^['"]|['"]$/g, '') : null; } +/** + * First-sentence description for a markdown/MDX page, mirroring what Docusaurus + * derives for its meta description. A front-matter `description:` wins when the + * page declares one; otherwise the first prose paragraph is used, skipping MDX + * imports, JSX, headings, code fences, tables and admonitions. Node pages get + * their description from services*.json instead (see extractDescription). + * @param {string} content - raw page content (md/mdx). + * @return {string} description, or '' when the page has no leading prose. + */ +function pageDescription(content) { + const fm = /^---\r?\n([\s\S]*?)\r?\n---/.exec(content); + if (fm) { + const d = /(^|\n)description:[ \t]*(.*)/.exec(fm[1]); + if (d) { + const inline = d[2].trim(); + if (/^[>|][-+\d]*$/.test(inline)) { + // YAML block scalar (`description: >`, `|`, `>-`, …). The text is the + // indented run that follows; the indicator itself is not the value. + const block = []; + for (const line of fm[1].slice(d.index + d[0].length).split(/\r?\n/).slice(1)) { + if (!/^[ \t]+\S/.test(line)) break; + block.push(line.trim()); + } + if (block.length) return block.join(' '); + } else if (inline) { + return inline.replace(/^['"]|['"]$/g, ''); + } + } + } + const body = content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, ''); + const para = []; + let inFence = false; + for (const line of body.split(/\r?\n/)) { + const t = line.trim(); + // Skip fenced blocks wholesale — the fence delimiters AND their contents. + if (/^(```|~~~)/.test(t)) { + if (para.length) break; + inFence = !inFence; + continue; + } + if (inFence) continue; + if (!t) { + if (para.length) break; + continue; + } + if (/^(#{1,6}\s|:::|