diff --git a/bun.lock b/bun.lock index 4170c86..c45f33e 100644 --- a/bun.lock +++ b/bun.lock @@ -1,33 +1,34 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "markdown-gen", "dependencies": { - "@json-schema-tools/traverse": "^1.11.0", - "@open-rpc/examples": "^1.7.2", - "@open-rpc/schema-utils-js": "^2.2.1", - "@open-rpc/spec-types": "^0.0.2", - "mdast": "^3.0.0", - "mdast-util-gfm": "^3.1.0", - "mdast-util-mdx": "^3.0.0", - "mdast-util-to-markdown": "^2.1.2", - "remark-gfm": "^4.0.1", - "remark-stringify": "^11.0.0", - "unified": "^11.0.5", + "@json-schema-tools/traverse": "1.11.0", + "@open-rpc/examples": "1.7.2", + "@open-rpc/schema-utils-js": "2.2.1", + "@open-rpc/spec-types": "0.0.2", + "mdast": "3.0.0", + "mdast-util-gfm": "3.1.0", + "mdast-util-mdx": "3.0.0", + "mdast-util-to-markdown": "2.1.2", + "remark-gfm": "4.0.1", + "remark-stringify": "11.0.0", + "unified": "11.0.5", }, "devDependencies": { - "@changesets/cli": "^2.29.8", - "@eslint/js": "^9.39.2", - "@types/bun": "^1.3.5", + "@changesets/cli": "2.29.8", + "@eslint/js": "9.39.3", + "@types/bun": "1.3.9", "@types/node": "22.13.5", - "@typescript-eslint/eslint-plugin": "^8.50.0", - "@typescript-eslint/parser": "^8.50.0", - "eslint": "^9.39.2", - "eslint-config-prettier": "^10.1.8", - "eslint-plugin-prettier": "^5.5.4", - "globals": "^16.5.0", - "typescript": "~5.9.3", + "@typescript-eslint/eslint-plugin": "8.56.1", + "@typescript-eslint/parser": "8.56.1", + "eslint": "9.39.3", + "eslint-config-prettier": "10.1.8", + "eslint-plugin-prettier": "5.5.5", + "globals": "16.5.0", + "typescript": "5.9.3", }, }, "packages/docusaurus-plugin": { @@ -44,12 +45,12 @@ }, "devDependencies": { "@docusaurus/types": "3.9.2", - "@types/react": "^19.2.4", - "@types/react-dom": "^19.2.3", + "@types/react": "19.2.4", + "@types/react-dom": "19.2.3", }, "peerDependencies": { - "react": "^19.0.0", - "react-dom": "^19.0.0", + "react": "19.2.0", + "react-dom": "19.2.0", }, }, "packages/example-site": { @@ -58,18 +59,18 @@ "dependencies": { "@docusaurus/core": "3.9.2", "@docusaurus/preset-classic": "3.9.2", - "@mdx-js/react": "^3.0.0", + "@mdx-js/react": "3.1.1", "@open-rpc/docusaurus-plugin": "workspace:*", - "clsx": "^2.0.0", - "prism-react-renderer": "^2.3.0", - "react": "^19.0.0", - "react-dom": "^19.0.0", + "clsx": "2.1.1", + "prism-react-renderer": "2.4.1", + "react": "19.2.0", + "react-dom": "19.2.0", }, "devDependencies": { "@docusaurus/module-type-aliases": "3.9.2", "@docusaurus/tsconfig": "3.9.2", "@docusaurus/types": "3.9.2", - "typescript": "~5.6.2", + "typescript": "5.9.3", }, }, "packages/markdown-generator": { @@ -80,22 +81,22 @@ }, "dependencies": { "@open-rpc/schema-utils-js": "2.2.1", - "@types/mdast": "^4.0.4", - "mdast-util-from-markdown": "^2.0.2", - "mdast-util-frontmatter": "^2.0.1", - "mdast-util-gfm": "^3.1.0", - "mdast-util-mdx": "^3.0.0", - "mdast-util-to-markdown": "^2.1.0", - "micromark-extension-gfm": "^3.0.0", + "@types/mdast": "4.0.4", + "mdast-util-from-markdown": "2.0.2", + "mdast-util-frontmatter": "2.0.1", + "mdast-util-gfm": "3.1.0", + "mdast-util-mdx": "3.0.0", + "mdast-util-to-markdown": "2.1.2", + "micromark-extension-gfm": "3.0.0", }, "devDependencies": { - "@types/bun": "^1.3.1", + "@types/bun": "1.3.9", "@types/node": "22.13.5", - "@typescript-eslint/eslint-plugin": "^8.46.2", - "eslint": "^9.38.0", - "eslint-config-prettier": "^10.1.8", - "eslint-plugin-prettier": "^5.5.4", - "typescript": "5.9", + "@typescript-eslint/eslint-plugin": "8.56.1", + "eslint": "9.39.3", + "eslint-config-prettier": "10.1.8", + "eslint-plugin-prettier": "5.5.5", + "typescript": "5.9.3", }, }, }, @@ -2686,8 +2687,6 @@ "null-loader/schema-utils": ["schema-utils@3.3.0", "", { "dependencies": { "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", "ajv-keywords": "^3.5.2" } }, "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg=="], - "open-rpc-markdown-gen-beta-example-site/typescript": ["typescript@5.6.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw=="], - "p-filter/p-map": ["p-map@2.1.0", "", {}, "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw=="], "p-locate/p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], diff --git a/package.json b/package.json index 00c7631..338b1f3 100644 --- a/package.json +++ b/package.json @@ -24,29 +24,29 @@ "ci:publish": "for dir in packages/markdown-generator packages/docusaurus-plugin; do (cd \"$dir\" && bun publish --access public || true); done && changeset tag" }, "devDependencies": { - "@changesets/cli": "^2.29.8", - "@eslint/js": "^9.39.3", - "@types/bun": "^1.3.9", + "@changesets/cli": "2.29.8", + "@eslint/js": "9.39.3", + "@types/bun": "1.3.9", "@types/node": "22.13.5", - "@typescript-eslint/eslint-plugin": "^8.56.1", - "@typescript-eslint/parser": "^8.56.1", - "eslint": "^9.39.3", - "eslint-config-prettier": "^10.1.8", - "eslint-plugin-prettier": "^5.5.5", - "globals": "^16.5.0", - "typescript": "~5.9.3" + "@typescript-eslint/eslint-plugin": "8.56.1", + "@typescript-eslint/parser": "8.56.1", + "eslint": "9.39.3", + "eslint-config-prettier": "10.1.8", + "eslint-plugin-prettier": "5.5.5", + "globals": "16.5.0", + "typescript": "5.9.3" }, "dependencies": { - "@json-schema-tools/traverse": "^1.11.0", - "@open-rpc/examples": "^1.7.2", - "@open-rpc/schema-utils-js": "^2.2.1", - "@open-rpc/spec-types": "^0.0.2", - "mdast": "^3.0.0", - "mdast-util-gfm": "^3.1.0", - "mdast-util-mdx": "^3.0.0", - "mdast-util-to-markdown": "^2.1.2", - "remark-gfm": "^4.0.1", - "remark-stringify": "^11.0.0", - "unified": "^11.0.5" + "@json-schema-tools/traverse": "1.11.0", + "@open-rpc/examples": "1.7.2", + "@open-rpc/schema-utils-js": "2.2.1", + "@open-rpc/spec-types": "0.0.2", + "mdast": "3.0.0", + "mdast-util-gfm": "3.1.0", + "mdast-util-mdx": "3.0.0", + "mdast-util-to-markdown": "2.1.2", + "remark-gfm": "4.0.1", + "remark-stringify": "11.0.0", + "unified": "11.0.5" } -} \ No newline at end of file +} diff --git a/packages/docusaurus-plugin/package.json b/packages/docusaurus-plugin/package.json index b065e4f..fef505e 100644 --- a/packages/docusaurus-plugin/package.json +++ b/packages/docusaurus-plugin/package.json @@ -27,11 +27,11 @@ }, "devDependencies": { "@docusaurus/types": "3.9.2", - "@types/react": "^19.2.4", - "@types/react-dom": "^19.2.3" + "@types/react": "19.2.4", + "@types/react-dom": "19.2.3" }, "peerDependencies": { - "react": "^19.0.0", - "react-dom": "^19.0.0" + "react": "19.2.0", + "react-dom": "19.2.0" } -} \ No newline at end of file +} diff --git a/packages/docusaurus-plugin/src/lib.ts b/packages/docusaurus-plugin/src/lib.ts index 5591680..94c9c6e 100644 --- a/packages/docusaurus-plugin/src/lib.ts +++ b/packages/docusaurus-plugin/src/lib.ts @@ -6,6 +6,7 @@ import { identitySchemaEdits, renderMethodsToMarkdown, renderIndex, + tagPermalinkPrefixForDocsRoute, } from "@open-rpc/markdown-generator"; import type { DereffedMethodObject, @@ -84,7 +85,8 @@ export async function cleanUpExistingDocs(specPath: string, outputDir: string) { (entry) => entry.isFile() && methodFileNamesToGenerate.has(entry.name) === false && - entry.name !== "index.md", + entry.name !== "index.md" && + entry.name !== "index.mdx", ) .map((entry) => `${entry.parentPath}/${entry.name}`); @@ -127,13 +129,21 @@ export async function generateDocs( additionalFrontmatter["slug"] = options.indexSlug; } - const indexContent = renderIndex(doc, "mdx", additionalFrontmatter); + const indexContent = renderIndex( + doc, + "mdx", + additionalFrontmatter, + tagPermalinkPrefixForDocsRoute(options.docsRouteBasePath), + ); const finalIndex = options.showPoweredBy === true ? `${indexContent}\n---\n\n*Powered by [OpenRPC](https://open-rpc.org)*\n` : indexContent; - await fs.writeFile(path.join(outDir, "index.md"), finalIndex, "utf8"); + // Drop any legacy .md index before writing .mdx so Docusaurus doesn't see + // two route candidates with the same slug. + await fs.rm(path.join(outDir, "index.md"), { force: true }); + await fs.writeFile(path.join(outDir, "index.mdx"), finalIndex, "utf8"); } /* ---------- Renderers ---------- */ diff --git a/packages/docusaurus-plugin/src/options.ts b/packages/docusaurus-plugin/src/options.ts index f0f6405..f30fc4e 100644 --- a/packages/docusaurus-plugin/src/options.ts +++ b/packages/docusaurus-plugin/src/options.ts @@ -6,6 +6,8 @@ export type Options = { docOutputPath: string; showPoweredBy: boolean; indexSlug: string | undefined; + /** Docusaurus docs routeBasePath (default preset: "docs"). */ + docsRouteBasePath?: string; }; /** @@ -25,5 +27,6 @@ export function normalizeOptions(options: Options): PluginOptions { showPoweredBy: options.showPoweredBy === undefined ? true : options.showPoweredBy, indexSlug: options.indexSlug === undefined ? undefined : options.indexSlug, + docsRouteBasePath: options.docsRouteBasePath ?? "docs", }; } diff --git a/packages/example-site/docs/api-reference/index.md b/packages/example-site/docs/api-reference/index.mdx similarity index 97% rename from packages/example-site/docs/api-reference/index.md rename to packages/example-site/docs/api-reference/index.mdx index 7b4884d..67c6b09 100644 --- a/packages/example-site/docs/api-reference/index.md +++ b/packages/example-site/docs/api-reference/index.mdx @@ -3,7 +3,6 @@ title: "Ethereum JSON-RPC Specification" description: "A specification of the standard interface for Ethereum clients." - --- # Ethereum JSON-RPC Specification @@ -79,6 +78,10 @@ A specification of the standard interface for Ethereum clients. - [`eth_syncing`](./methods/eth_syncing.mdx) - [`eth_uninstallFilter`](./methods/eth_uninstallFilter.mdx) +import TagsListInline from '@theme/TagsListInline'; + + + --- *Powered by [OpenRPC](https://open-rpc.org)* diff --git a/packages/example-site/docs/api-reference/methods/eth_chainId.mdx b/packages/example-site/docs/api-reference/methods/eth_chainId.mdx index 8e69bd6..bb1040e 100644 --- a/packages/example-site/docs/api-reference/methods/eth_chainId.mdx +++ b/packages/example-site/docs/api-reference/methods/eth_chainId.mdx @@ -3,6 +3,8 @@ title: "eth_chainId" hide_table_of_contents: true description: "" +tags: + - client --- import {TwoColumnLayout, InteractiveRequest, ResponseExample} from '@open-rpc/docusaurus-plugin/components'; diff --git a/packages/example-site/error-group-openrpc.json b/packages/example-site/error-group-openrpc.json index fea5430..5a0618f 100644 --- a/packages/example-site/error-group-openrpc.json +++ b/packages/example-site/error-group-openrpc.json @@ -5598,6 +5598,11 @@ { "name": "eth_chainId", "summary": "Returns the chain ID of the current network.", + "tags": [ + { + "name": "client" + } + ], "params": [], "result": { "name": "Chain ID", diff --git a/packages/example-site/package.json b/packages/example-site/package.json index 28e500d..7528a68 100644 --- a/packages/example-site/package.json +++ b/packages/example-site/package.json @@ -19,17 +19,17 @@ "@open-rpc/docusaurus-plugin": "workspace:*", "@docusaurus/core": "3.9.2", "@docusaurus/preset-classic": "3.9.2", - "@mdx-js/react": "^3.0.0", - "clsx": "^2.0.0", - "prism-react-renderer": "^2.3.0", - "react": "^19.0.0", - "react-dom": "^19.0.0" + "@mdx-js/react": "3.1.1", + "clsx": "2.1.1", + "prism-react-renderer": "2.4.1", + "react": "19.2.0", + "react-dom": "19.2.0" }, "devDependencies": { "@docusaurus/module-type-aliases": "3.9.2", "@docusaurus/tsconfig": "3.9.2", "@docusaurus/types": "3.9.2", - "typescript": "~5.6.2" + "typescript": "5.9.3" }, "browserslist": { "production": [ @@ -46,4 +46,4 @@ "engines": { "node": ">=20.0" } -} \ No newline at end of file +} diff --git a/packages/markdown-generator/README.md b/packages/markdown-generator/README.md index 20ca012..99648cf 100644 --- a/packages/markdown-generator/README.md +++ b/packages/markdown-generator/README.md @@ -30,6 +30,13 @@ Writes markdown files directly to disk for each method, plus an `index.md`. The Generates an index markdown string listing all methods with links. Returns the markdown as a string. +The index intentionally does **not** aggregate method tags into its YAML frontmatter—that previously made the index doc appear under every `/tags/*` listing in Docusaurus. Instead, when at least one method has tags, the index appends an inline tag strip after the methods list: + +- For `"mdx"` output, it emits `` (Docusaurus theme component) so the chips match the standard `tags:` row visually. +- For `"md"` output, it emits a `**Tags:** [name](./tags/) · ...` fallback line. + +When no methods carry tags, no tag block is emitted at all. Tag links must use **absolute** permalinks such as `/docs/tags/` — Docusaurus registers tag list pages at `/tags/`, and relative paths like `../tags/` are resolved by `@docusaurus/Link` to `/tags/` (404). Pass `tagPermalinkPrefixForDocsRoute("docs")` as the fourth argument to `renderIndex` (the Docusaurus plugin does this automatically). Write the index as `.mdx` when using ``. Tag pages list method docs that carry matching `tags:` frontmatter; add `docs/tags.yml` entries if you want tag descriptions on the tag page. + ### `identityEdits` / `identitySchemaEdits` Default edit functions that pass content through unchanged. Use these as a starting point when creating custom edits. diff --git a/packages/markdown-generator/package.json b/packages/markdown-generator/package.json index f5cd16a..2d4e106 100644 --- a/packages/markdown-generator/package.json +++ b/packages/markdown-generator/package.json @@ -30,22 +30,22 @@ "build": "bun build.ts && bun run types" }, "devDependencies": { - "@types/bun": "^1.3.1", + "@types/bun": "1.3.9", "@types/node": "22.13.5", - "typescript": "5.9", - "@typescript-eslint/eslint-plugin": "^8.46.2", - "eslint": "^9.38.0", - "eslint-config-prettier": "^10.1.8", - "eslint-plugin-prettier": "^5.5.4" + "typescript": "5.9.3", + "@typescript-eslint/eslint-plugin": "8.56.1", + "eslint": "9.39.3", + "eslint-config-prettier": "10.1.8", + "eslint-plugin-prettier": "5.5.5" }, "dependencies": { "@open-rpc/schema-utils-js": "2.2.1", - "@types/mdast": "^4.0.4", - "mdast-util-from-markdown": "^2.0.2", - "mdast-util-frontmatter": "^2.0.1", - "mdast-util-gfm": "^3.1.0", - "mdast-util-mdx": "^3.0.0", - "mdast-util-to-markdown": "^2.1.0", - "micromark-extension-gfm": "^3.0.0" + "@types/mdast": "4.0.4", + "mdast-util-from-markdown": "2.0.2", + "mdast-util-frontmatter": "2.0.1", + "mdast-util-gfm": "3.1.0", + "mdast-util-mdx": "3.0.0", + "mdast-util-to-markdown": "2.1.2", + "micromark-extension-gfm": "3.0.0" } -} \ No newline at end of file +} diff --git a/packages/markdown-generator/src/fixtures/comprehensive.test.ts b/packages/markdown-generator/src/fixtures/comprehensive.test.ts index 763eeb8..3701814 100644 --- a/packages/markdown-generator/src/fixtures/comprehensive.test.ts +++ b/packages/markdown-generator/src/fixtures/comprehensive.test.ts @@ -1,6 +1,10 @@ import { describe, it, expect } from "bun:test"; import { renderMethod, identitySchemaEdits, identityEdits } from "../schema"; -import { renderMethodsToMarkdown, renderIndex } from "../lib"; +import { + renderMethodsToMarkdown, + renderIndex, + tagPermalinkPrefixForDocsRoute, +} from "../lib"; import { toMarkdown } from "mdast-util-to-markdown"; import { gfmToMarkdown } from "mdast-util-gfm"; import { mdxToMarkdown } from "mdast-util-mdx"; @@ -113,17 +117,84 @@ describe("renderIndex", () => { ); }); - it("should include frontmatter with tags", () => { + it("should not aggregate method tags into the index frontmatter", () => { const doc = comprehensiveTestDoc as DereffedOpenrpcDocument; const index = renderIndex(doc, "mdx"); - // Frontmatter should exist expect(index.startsWith("---")).toBe(true); - // Should have tags section - expect(index).toContain("tags:"); - // Should include unique tags from methods - expect(index).toContain('"composition"'); - expect(index).toContain('"arrays"'); + const frontmatterEnd = index.indexOf("\n---", 3); + expect(frontmatterEnd).toBeGreaterThan(0); + const frontmatter = index.slice(0, frontmatterEnd); + expect(frontmatter).not.toContain("tags:"); + }); + + it("should render an mdx tag strip in the body when methods have tags", () => { + const doc = comprehensiveTestDoc as DereffedOpenrpcDocument; + const index = renderIndex(doc, "mdx"); + + expect(index).toContain( + "import TagsListInline from '@theme/TagsListInline';", + ); + expect(index).toContain(" { + const doc = comprehensiveTestDoc as DereffedOpenrpcDocument; + const index = renderIndex(doc, "mdx", {}, "/docs/tags/"); + + expect(index).toContain( + '{label: "composition", permalink: "/docs/tags/composition"}', + ); + expect(index).toContain( + '{label: "arrays", permalink: "/docs/tags/arrays"}', + ); + }); + + it("should derive tag permalink prefix from docs routeBasePath", () => { + expect(tagPermalinkPrefixForDocsRoute()).toBe("/docs/tags/"); + expect(tagPermalinkPrefixForDocsRoute("docs")).toBe("/docs/tags/"); + expect(tagPermalinkPrefixForDocsRoute("/docs/")).toBe("/docs/tags/"); + expect(tagPermalinkPrefixForDocsRoute("api")).toBe("/api/tags/"); + expect(tagPermalinkPrefixForDocsRoute("/")).toBe("/tags/"); + expect(tagPermalinkPrefixForDocsRoute("")).toBe("/tags/"); + }); + + it("should render a markdown tag line in the body when output is md", () => { + const doc = comprehensiveTestDoc as DereffedOpenrpcDocument; + const index = renderIndex(doc, "md"); + + expect(index).not.toContain("TagsListInline"); + expect(index).toContain("**Tags:**"); + expect(index).toContain("[composition](./tags/composition)"); + expect(index).toContain("[arrays](./tags/arrays)"); + }); + + it("should omit any tag block when no methods carry tags", () => { + const untaggedDoc: DereffedOpenrpcDocument = { + openrpc: "1.0.0", + info: { title: "Untagged API", version: "1.0.0" }, + methods: [ + { + name: "noop", + params: [], + result: { name: "ok", schema: {} }, + }, + ], + } as DereffedOpenrpcDocument; + + const indexMdx = renderIndex(untaggedDoc, "mdx"); + const indexMd = renderIndex(untaggedDoc, "md"); + + for (const out of [indexMdx, indexMd]) { + expect(out).not.toContain("tags:"); + expect(out).not.toContain("TagsListInline"); + expect(out).not.toContain("**Tags:**"); + expect(out).not.toContain("./tags/"); + } }); it("should list all methods with correct .mdx links", () => { diff --git a/packages/markdown-generator/src/index.ts b/packages/markdown-generator/src/index.ts index 4facd38..c3d9cdc 100644 --- a/packages/markdown-generator/src/index.ts +++ b/packages/markdown-generator/src/index.ts @@ -2,6 +2,7 @@ export { renderMethodsToMarkdown, renderDocumentToMarkdownFiles, renderIndex, + tagPermalinkPrefixForDocsRoute, } from "./lib"; export { identityEdits, diff --git a/packages/markdown-generator/src/lib.ts b/packages/markdown-generator/src/lib.ts index 3603548..fb1e420 100644 --- a/packages/markdown-generator/src/lib.ts +++ b/packages/markdown-generator/src/lib.ts @@ -103,10 +103,26 @@ export async function renderDocumentToMarkdownFiles( ); } +// Build the prefix used for tag links in the index. Docusaurus registers tag +// list pages at `//tags/` (default routeBasePath is +// "docs"). Permalinks must be ABSOLUTE — `@docusaurus/Link` resolves any +// relative path from the site root, so `./tags/x` or `../tags/x` both end up +// at `/tags/x` (404) regardless of the index file's location. +export function tagPermalinkPrefixForDocsRoute( + docsRouteBasePath = "docs", +): string { + const base = docsRouteBasePath.replace(/^\/+|\/+$/g, ""); + return base === "" ? "/tags/" : `/${base}/tags/`; +} + export function renderIndex( doc: DereffedOpenrpcDocument, markdownType: "mdx" | "md", additionalFrontmatter: Record = {}, + // Default emits a relative `./tags/` link that works for plain markdown + // viewers / GitHub but is BROKEN inside Docusaurus. Pass + // `tagPermalinkPrefixForDocsRoute(routeBasePath)` when generating for a site. + tagPermalinkPrefix = "./tags/", ): string { const title = doc.info?.title || "API"; const version = doc.info?.version || ""; @@ -117,11 +133,12 @@ export function renderIndex( const tags = methods .flatMap((m) => m.tags?.map((t) => t?.name) || []) .filter(Boolean); - const uniqueTags = [...new Set(tags)]; - const tagsList = - uniqueTags.length === 0 - ? "" - : `tags:\n${uniqueTags.map((t) => ` - "${t}"`).join("\n")}`; + const uniqueTags = [...new Set(tags)].sort(); + const tagsBlock = renderTagsBlock( + uniqueTags, + markdownType, + tagPermalinkPrefix, + ); const methodsList = methods.length === 0 @@ -149,7 +166,6 @@ export function renderIndex( title: "${title}" description: "${escapeYaml(desc)}" ${frontmatter} -${tagsList} --- # ${title} @@ -159,5 +175,46 @@ ${version ? `Version: \`${version}\`\n` : ""}${desc ? `\n${desc}\n` : ""} ## Methods ${methodsList} +${tagsBlock}`; +} + +// Approximation of `lodash.kebabCase`, which is what Docusaurus uses to derive +// the tag slug from inline frontmatter tags. Matches for ASCII alnum tags; may +// diverge for tags containing digit/underscore boundaries (e.g. "API_v2"). +function slugifyTag(tag: string): string { + return tag + .replace(/([a-z0-9])([A-Z])/g, "$1-$2") + .replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2") + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+|-+$/g, ""); +} + +function renderTagsBlock( + tags: string[], + markdownType: "mdx" | "md", + tagPermalinkPrefix: string, +): string { + if (tags.length === 0) return ""; + + const tagHref = (tag: string) => `${tagPermalinkPrefix}${slugifyTag(tag)}`; + + if (markdownType === "mdx") { + const items = tags + .map( + (t) => + `{label: ${JSON.stringify(t)}, permalink: ${JSON.stringify( + tagHref(t), + )}}`, + ) + .join(", "); + return ` +import TagsListInline from '@theme/TagsListInline'; + + `; + } + + const links = tags.map((t) => `[${t}](${tagHref(t)})`).join(" · "); + return `\n**Tags:** ${links}\n`; }