From cad97b0b269b4e9b3d9135bfef6881db47d1382a Mon Sep 17 00:00:00 2001 From: Jaromir Hamala Date: Mon, 18 May 2026 16:27:06 +0200 Subject: [PATCH 1/2] Advertise .md companion in doc page Inject into every generated doc HTML so HTML-parsing agents can discover the markdown form without sniffing URL patterns. Adds X-Robots-Tag: noindex to .md responses so the raw markdown doesn't show up as duplicate content in Google. Extracts a shared getMarkdownUrl() helper used by both the postBuild plugin and the existing CopyPageButton, so the URL convention has a single source of truth. --- plugins/raw-markdown/index.js | 29 ++++++++++++++++++++++++++++- src/theme/CopyPageButton/index.tsx | 8 +++----- src/utils/markdown-url.js | 16 ++++++++++++++++ static/_headers | 1 + 4 files changed, 48 insertions(+), 6 deletions(-) create mode 100644 src/utils/markdown-url.js diff --git a/plugins/raw-markdown/index.js b/plugins/raw-markdown/index.js index 8f8132e58..1c372e052 100644 --- a/plugins/raw-markdown/index.js +++ b/plugins/raw-markdown/index.js @@ -8,10 +8,11 @@ const { prependFrontmatter, normalizeNewLines, } = require("./convert-components") +const { getMarkdownUrl } = require("../../src/utils/markdown-url") module.exports = () => ({ name: "raw-markdown", - async postBuild({ outDir, plugins }) { + async postBuild({ siteConfig, outDir, plugins }) { const docsPath = path.join(__dirname, "../../documentation") const outputBase = outDir @@ -110,6 +111,32 @@ module.exports = () => ({ fs.writeFileSync(outputFile, processedContent, "utf8") fileCount++ + + // Advertise the .md companion in the rendered HTML's so + // HTML-parsing agents can discover it without sniffing URL patterns. + // URL format matches CopyPageButton via the shared getMarkdownUrl + // helper. Assumes docusaurus.config.js keeps `trailingSlash: true` + // so HTML lands at ${urlPath}/index.html. + const htmlPath = urlPath + ? path.join(outputBase, urlPath, "index.html") + : path.join(outputBase, "index.html") + if (fs.existsSync(htmlPath)) { + const pageUrl = urlPath + ? path.posix.join(siteConfig.baseUrl, urlPath) + : siteConfig.baseUrl + const mdHref = getMarkdownUrl(pageUrl, siteConfig.baseUrl) + const tag = `` + const html = fs.readFileSync(htmlPath, "utf8") + if (!html.includes('type="text/markdown"')) { + const idx = html.lastIndexOf("") + if (idx === -1) { + console.warn(`[raw-markdown] No found in ${htmlPath}; skipping alternate link`) + } else { + const patched = html.slice(0, idx) + tag + html.slice(idx) + fs.writeFileSync(htmlPath, patched, "utf8") + } + } + } } } } diff --git a/src/theme/CopyPageButton/index.tsx b/src/theme/CopyPageButton/index.tsx index 2d19c92ff..ca350bfc6 100644 --- a/src/theme/CopyPageButton/index.tsx +++ b/src/theme/CopyPageButton/index.tsx @@ -6,6 +6,7 @@ import IconCopy from "@theme/Icon/Copy" import IconSuccess from "@theme/Icon/Success" import Chevron from "@theme/Chevron" import { ArrowTopRightOnSquareIcon } from "@heroicons/react/16/solid" +import { getMarkdownUrl } from "../../utils/markdown-url" import styles from "./styles.module.css" @@ -46,13 +47,10 @@ export default function CopyPageButton(): JSX.Element { const { siteConfig } = useDocusaurusContext() - const basePath = siteConfig.baseUrl.replace(/\/$/, "") const pathname = typeof window !== "undefined" - ? window.location.pathname.replace(/\/$/, "") + ? window.location.pathname : "" - const markdownUrl = pathname === basePath - ? basePath + "/index.md" - : pathname + ".md" + const markdownUrl = getMarkdownUrl(pathname, siteConfig.baseUrl) const pageUrl = siteConfig.url + markdownUrl const chatPrompt = `I'd like to ask some questions about ${pageUrl}.\n\n` diff --git a/src/utils/markdown-url.js b/src/utils/markdown-url.js new file mode 100644 index 000000000..011e27926 --- /dev/null +++ b/src/utils/markdown-url.js @@ -0,0 +1,16 @@ +// Shared rule for translating a doc page URL to its `.md` companion. +// Used by: +// - src/theme/CopyPageButton (runtime, via window.location.pathname) +// - plugins/raw-markdown (build time, via siteConfig.baseUrl + urlPath) +// Plain JS (CommonJS) so the Docusaurus plugin can `require()` it directly; +// `allowJs: true` in tsconfig.json lets the TSX side import it with types. + +function getMarkdownUrl(pathname, basePath) { + const normalized = pathname.replace(/\/$/, "") + const normalizedBase = (basePath || "").replace(/\/$/, "") + return normalized === normalizedBase + ? `${normalizedBase}/index.md` + : `${normalized}.md` +} + +module.exports = { getMarkdownUrl } diff --git a/static/_headers b/static/_headers index e4e8d71ac..e7c818a0f 100644 --- a/static/_headers +++ b/static/_headers @@ -12,6 +12,7 @@ Access-Control-Allow-Origin: * Access-Control-Allow-Methods: GET, OPTIONS Cache-Control: public, max-age=60 + X-Robots-Tag: noindex /*/web-console/*.json Access-Control-Allow-Origin: * From b8ef62ba36dcda91ba6a2c5f95359f8d104e84d9 Mon Sep 17 00:00:00 2001 From: emrberk Date: Wed, 20 May 2026 11:13:43 +0300 Subject: [PATCH 2/2] move alternate tag creation to swizzle --- plugins/raw-markdown/index.js | 29 +---------------------------- src/theme/DocItem/Content/index.tsx | 9 +++++++++ 2 files changed, 10 insertions(+), 28 deletions(-) diff --git a/plugins/raw-markdown/index.js b/plugins/raw-markdown/index.js index 1c372e052..8f8132e58 100644 --- a/plugins/raw-markdown/index.js +++ b/plugins/raw-markdown/index.js @@ -8,11 +8,10 @@ const { prependFrontmatter, normalizeNewLines, } = require("./convert-components") -const { getMarkdownUrl } = require("../../src/utils/markdown-url") module.exports = () => ({ name: "raw-markdown", - async postBuild({ siteConfig, outDir, plugins }) { + async postBuild({ outDir, plugins }) { const docsPath = path.join(__dirname, "../../documentation") const outputBase = outDir @@ -111,32 +110,6 @@ module.exports = () => ({ fs.writeFileSync(outputFile, processedContent, "utf8") fileCount++ - - // Advertise the .md companion in the rendered HTML's so - // HTML-parsing agents can discover it without sniffing URL patterns. - // URL format matches CopyPageButton via the shared getMarkdownUrl - // helper. Assumes docusaurus.config.js keeps `trailingSlash: true` - // so HTML lands at ${urlPath}/index.html. - const htmlPath = urlPath - ? path.join(outputBase, urlPath, "index.html") - : path.join(outputBase, "index.html") - if (fs.existsSync(htmlPath)) { - const pageUrl = urlPath - ? path.posix.join(siteConfig.baseUrl, urlPath) - : siteConfig.baseUrl - const mdHref = getMarkdownUrl(pageUrl, siteConfig.baseUrl) - const tag = `` - const html = fs.readFileSync(htmlPath, "utf8") - if (!html.includes('type="text/markdown"')) { - const idx = html.lastIndexOf("") - if (idx === -1) { - console.warn(`[raw-markdown] No found in ${htmlPath}; skipping alternate link`) - } else { - const patched = html.slice(0, idx) + tag + html.slice(idx) - fs.writeFileSync(htmlPath, patched, "utf8") - } - } - } } } } diff --git a/src/theme/DocItem/Content/index.tsx b/src/theme/DocItem/Content/index.tsx index 022dbbf66..1000c3bd1 100644 --- a/src/theme/DocItem/Content/index.tsx +++ b/src/theme/DocItem/Content/index.tsx @@ -1,11 +1,14 @@ import React, { type ReactNode } from "react" import clsx from "clsx" +import Head from "@docusaurus/Head" +import useDocusaurusContext from "@docusaurus/useDocusaurusContext" import { ThemeClassNames } from "@docusaurus/theme-common" import { useDoc } from "@docusaurus/plugin-content-docs/client" import Heading from "@theme/Heading" import MDXContent from "@theme/MDXContent" import type { Props } from "@theme/DocItem/Content" import CopyPageButton from "@theme/CopyPageButton" +import { getMarkdownUrl } from "../../../utils/markdown-url" import styles from "./styles.module.css" @@ -21,8 +24,14 @@ function useSyntheticTitle(): string | null { export default function DocItemContent({ children }: Props): ReactNode { const syntheticTitle = useSyntheticTitle() + const { metadata } = useDoc() + const { siteConfig } = useDocusaurusContext() + const markdownUrl = getMarkdownUrl(metadata.permalink, siteConfig.baseUrl) return (
+ + +
{syntheticTitle && {syntheticTitle}}