From 9493bb0ccf6e1c86e29088891422443067a44931 Mon Sep 17 00:00:00 2001 From: Lucas Motta Date: Wed, 4 Feb 2026 11:36:35 -0300 Subject: [PATCH 01/15] Add .md extension and Accept header support for markdown - Add rewrite rule in next.config.js for *.md -> /api/markdown/* - Create middleware to handle Accept: text/markdown header - Create API route that fetches rendered HTML and converts to markdown - Use unified ecosystem (rehype-parse, rehype-remark, remark-gfm) - Extract main content via [data-algolia-page-scope] selector - Tables, code blocks, and links properly converted to GFM markdown --- middleware.ts | 47 ++++ next-env.d.ts | 3 +- next.config.js | 9 + package.json | 4 + pages/api/markdown/[...path].ts | 208 ++++++++++++++++++ pnpm-lock.yaml | 371 ++++++++++++++++++++++++++++++++ 6 files changed, 640 insertions(+), 2 deletions(-) create mode 100644 middleware.ts create mode 100644 pages/api/markdown/[...path].ts diff --git a/middleware.ts b/middleware.ts new file mode 100644 index 00000000..905d9765 --- /dev/null +++ b/middleware.ts @@ -0,0 +1,47 @@ +import { NextResponse } from "next/server"; +import type { NextRequest } from "next/server"; + +export function middleware(request: NextRequest) { + const { pathname } = request.nextUrl; + + // Check if the request accepts markdown + const acceptHeader = request.headers.get("accept") || ""; + const wantsMarkdown = + acceptHeader.includes("text/markdown") || + acceptHeader.includes("text/x-markdown"); + + // If the client accepts markdown and this is a docs page, rewrite to markdown API + if (wantsMarkdown && isDocsPage(pathname)) { + const url = request.nextUrl.clone(); + url.pathname = `/api/markdown${pathname}`; + return NextResponse.rewrite(url); + } + + return NextResponse.next(); +} + +/** + * Check if a path is a documentation page that can be served as markdown + */ +function isDocsPage(pathname: string): boolean { + // Match documentation paths + const docPatterns = [ + /^\/primitives\/docs\//, + /^\/themes\/docs\//, + /^\/colors\/docs\//, + /^\/blog\//, + ]; + + return docPatterns.some((pattern) => pattern.test(pathname)); +} + +// Configure which paths the middleware runs on +export const config = { + matcher: [ + // Match all docs paths + "/primitives/docs/:path*", + "/themes/docs/:path*", + "/colors/docs/:path*", + "/blog/:path*", + ], +}; diff --git a/next-env.d.ts b/next-env.d.ts index 725dd6f2..a4a7b3f5 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,6 +1,5 @@ /// /// -/// // NOTE: This file should not be edited -// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/next.config.js b/next.config.js index 43c2db29..5e821a94 100644 --- a/next.config.js +++ b/next.config.js @@ -13,6 +13,15 @@ module.exports = { }, // Next.js config + async rewrites() { + return [ + { + source: "/:path*.md", + destination: "/api/markdown/:path*", + }, + ]; + }, + async redirects() { return [ { diff --git a/package.json b/package.json index 230af4f4..f16a06a0 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "eslint-config-next": "^14.2.6", "glob": "^10", "gray-matter": "^4.0.2", + "hast-util-select": "^6.0.4", "hast-util-to-html": "^9.0.1", "hast-util-to-string": "^3.0.0", "lodash.debounce": "^4.0.8", @@ -52,7 +53,10 @@ "refractor": "^3.3.1", "rehype": "^11.0.0", "rehype-parse": "^9.0.0", + "rehype-remark": "^10.0.1", + "remark-gfm": "^4.0.1", "remark-slug": "^6.0.0", + "remark-stringify": "^11.0.0", "scroll-into-view-if-needed": "^3.1.0", "smoothscroll-polyfill": "^0.4.4", "tinycolor2": "^1.6.0", diff --git a/pages/api/markdown/[...path].ts b/pages/api/markdown/[...path].ts new file mode 100644 index 00000000..f7d6390a --- /dev/null +++ b/pages/api/markdown/[...path].ts @@ -0,0 +1,208 @@ +import type { NextApiRequest, NextApiResponse } from "next"; +import { unified } from "unified"; +import rehypeParse from "rehype-parse"; +import rehypeRemark from "rehype-remark"; +import remarkStringify from "remark-stringify"; +import remarkGfm from "remark-gfm"; +import { select, selectAll } from "hast-util-select"; +import type { Root, Element } from "hast"; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse, +) { + if (req.method !== "GET") { + return res.status(405).json({ error: "Method not allowed" }); + } + + const pathSegments = req.query.path as string[]; + if (!pathSegments || pathSegments.length === 0) { + return res.status(400).json({ error: "Invalid path" }); + } + + const pagePath = "/" + pathSegments.join("/"); + + try { + // Determine the base URL for fetching + // In development, use the request host; in production, use the configured URL + const protocol = req.headers["x-forwarded-proto"] || "http"; + const host = req.headers.host; + const baseUrl = `${protocol}://${host}`; + + // Fetch the rendered HTML page + const htmlResponse = await fetch(`${baseUrl}${pagePath}`, { + headers: { + // Pass along cookies for any auth + cookie: req.headers.cookie || "", + }, + }); + + if (!htmlResponse.ok) { + return res.status(404).json({ error: "Page not found" }); + } + + const html = await htmlResponse.text(); + + // Convert HTML to Markdown + const markdown = await convertHtmlToMarkdown(html); + + // Set appropriate headers + res.setHeader("Content-Type", "text/markdown; charset=utf-8"); + res.setHeader("X-Content-Type-Options", "nosniff"); + res.setHeader("Cache-Control", "public, max-age=3600, s-maxage=86400"); + + return res.status(200).send(markdown); + } catch (error) { + console.error("Error converting to markdown:", error); + return res + .status(500) + .json({ error: "Failed to convert page to markdown" }); + } +} + +async function convertHtmlToMarkdown(html: string): Promise { + const result = await unified() + .use(rehypeParse) + .use(extractMainContent) + .use(cleanupHtml) + .use(rehypeRemark) + .use(remarkGfm) + .use(remarkStringify, { + bullet: "-", + emphasis: "_", + strong: "**", + fence: "`", + fences: true, + listItemIndent: "one", + }) + .process(html); + + return String(result); +} + +/** + * Extract the main content area from the page + */ +function extractMainContent() { + return (tree: Root) => { + // Try to find the main content area marked with data-algolia-page-scope + const mainContent = select("[data-algolia-page-scope]", tree) as + | Element + | undefined; + + if (mainContent) { + // Return a new root with just the main content + return { + type: "root" as const, + children: [mainContent], + }; + } + + // Fallback: return the original tree + return tree; + }; +} + +/** + * Clean up the HTML before conversion + * - Remove elements we don't want in the markdown + * - Fix code block language annotations + */ +function cleanupHtml() { + return (tree: Root) => { + // Remove elements marked as excluded from indexing + const excluded = selectAll("[data-algolia-exclude]", tree); + for (const node of excluded) { + removeNode(tree, node); + } + + // Remove navigation and footer elements + const navFooterSelectors = ["nav", "footer"]; + for (const selector of navFooterSelectors) { + const elements = selectAll(selector, tree); + for (const node of elements) { + removeNode(tree, node); + } + } + + // Remove buttons that are NOT inside tables (preserve table buttons for accessibility info) + const buttons = selectAll("button", tree) as Element[]; + for (const button of buttons) { + // Check if this button is inside a table + if (!isInsideTable(tree, button)) { + removeNode(tree, button); + } + } + + // Process code blocks - extract language from class + const codeBlocks = selectAll("pre code", tree) as Element[]; + for (const code of codeBlocks) { + if (code.properties?.className) { + const classes = Array.isArray(code.properties.className) + ? code.properties.className + : [code.properties.className]; + + for (const cls of classes) { + if (typeof cls === "string" && cls.startsWith("language-")) { + const lang = cls.replace("language-", ""); + code.properties.dataLanguage = lang; + } + } + } + } + + return tree; + }; +} + +/** + * Check if an element is inside a table + */ +function isInsideTable(tree: Root | Element, target: Element): boolean { + const tables = selectAll("table", tree) as Element[]; + for (const table of tables) { + if (containsNode(table, target)) { + return true; + } + } + return false; +} + +/** + * Check if a node contains another node + */ +function containsNode(parent: Element, target: Element): boolean { + if (parent === target) return true; + if (parent.children) { + for (const child of parent.children) { + if (child.type === "element" && containsNode(child as Element, target)) { + return true; + } + } + } + return false; +} + +/** + * Remove a node from the tree + */ +function removeNode(tree: Root | Element, nodeToRemove: Element): void { + const visit = (node: Root | Element): boolean => { + if ("children" in node && Array.isArray(node.children)) { + const index = node.children.indexOf(nodeToRemove as any); + if (index !== -1) { + node.children.splice(index, 1); + return true; + } + for (const child of node.children) { + if (child.type === "element" || child.type === "root") { + if (visit(child as Element)) { + return true; + } + } + } + } + return false; + }; + visit(tree); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 744eca43..c547d227 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -56,6 +56,9 @@ importers: gray-matter: specifier: ^4.0.2 version: 4.0.3 + hast-util-select: + specifier: ^6.0.4 + version: 6.0.4 hast-util-to-html: specifier: ^9.0.1 version: 9.0.3 @@ -110,9 +113,18 @@ importers: rehype-parse: specifier: ^9.0.0 version: 9.0.1 + rehype-remark: + specifier: ^10.0.1 + version: 10.0.1 + remark-gfm: + specifier: ^4.0.1 + version: 4.0.1 remark-slug: specifier: ^6.0.0 version: 6.1.0 + remark-stringify: + specifier: ^11.0.0 + version: 11.0.0 scroll-into-view-if-needed: specifier: ^3.1.0 version: 3.1.0 @@ -1537,6 +1549,9 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + bcp-47-match@2.0.3: + resolution: {integrity: sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ==} + bezier-easing@2.1.0: resolution: {integrity: sha512-gbIqZ/eslnUFC1tjEvtz0sgx+xTK20wDnYMIA27VA04R7w6xxXQPZDbibjA9DTWZRA2CXtwHykkVzlCaAJAZig==} @@ -1550,6 +1565,9 @@ packages: bluebird@3.7.2: resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + boxen@1.3.0: resolution: {integrity: sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw==} engines: {node: '>=4'} @@ -1766,6 +1784,9 @@ packages: resolution: {integrity: sha512-GsVpkFPlycH7/fRR7Dhcmnoii54gV1nz7y4CWyeFS14N+JVBBhY+r8amRHE4BwSYal7BPTDp8isvAlCxyFt3Hg==} engines: {node: '>=4'} + css-selector-parser@3.3.0: + resolution: {integrity: sha512-Y2asgMGFqJKF4fq4xHDSlFYIkeVfRsm69lQC1q9kbEsH5XtnINTMrweLkjYMeaUgiXBy/uvKeO/a1JHTNnmB2g==} + csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} @@ -1856,6 +1877,10 @@ packages: devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + direction@2.0.1: + resolution: {integrity: sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA==} + hasBin: true + doctrine@2.1.0: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} @@ -2379,6 +2404,9 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hast-util-embedded@3.0.0: + resolution: {integrity: sha512-naH8sld4Pe2ep03qqULEtvYr7EjrLK2QHY8KJR6RJkTUjPGObe1vnx585uzem2hGra+s1q08DZZpfgDVYRbaXA==} + hast-util-from-html@2.0.3: resolution: {integrity: sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==} @@ -2388,15 +2416,33 @@ packages: hast-util-from-parse5@8.0.1: resolution: {integrity: sha512-Er/Iixbc7IEa7r/XLtuG52zoqn/b3Xng/w6aZQ0xGVxzhw5xUFxcRqdPzP6yFi/4HBYRaifaI5fQ1RH8n0ZeOQ==} + hast-util-has-property@3.0.0: + resolution: {integrity: sha512-MNilsvEKLFpV604hwfhVStK0usFY/QmM5zX16bo7EjnAEGofr5YyI37kzopBlZJkHD4t887i+q/C8/tr5Q94cA==} + + hast-util-is-body-ok-link@3.0.1: + resolution: {integrity: sha512-0qpnzOBLztXHbHQenVB8uNuxTnm/QBFUOmdOSsEn7GnBtyY07+ENTWVFBAnXd/zEgd9/SUG3lRY7hSIBWRgGpQ==} + hast-util-is-element@1.1.0: resolution: {integrity: sha512-oUmNua0bFbdrD/ELDSSEadRVtWZOf3iF6Lbv81naqsIV99RnSCieTbWuWCY8BAeEfKJTKl0gRdokv+dELutHGQ==} + hast-util-is-element@3.0.0: + resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==} + + hast-util-minify-whitespace@1.0.1: + resolution: {integrity: sha512-L96fPOVpnclQE0xzdWb/D12VT5FabA7SnZOUMtL1DbXmYiHJMXZvFkIZfiMmTCNJHUeO2K9UYNXoVyfz+QHuOw==} + hast-util-parse-selector@2.2.5: resolution: {integrity: sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==} hast-util-parse-selector@4.0.0: resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} + hast-util-phrasing@3.0.1: + resolution: {integrity: sha512-6h60VfI3uBQUxHqTyMymMZnEbNl1XmEGtOxxKYL7stY2o601COo62AWAYBQR9lZbYXYSBoxag8UpPRXK+9fqSQ==} + + hast-util-select@6.0.4: + resolution: {integrity: sha512-RqGS1ZgI0MwxLaKLDxjprynNzINEkRHY2i8ln4DDjgv9ZhcYVIHN9rlpiYsqtFwrgpYU361SyWDQcGNIBVu3lw==} + hast-util-to-estree@3.1.0: resolution: {integrity: sha512-lfX5g6hqVh9kjS/B9E2gSkvHH4SZNiQFiqWS0x9fENzEl+8W12RqdRxX6d/Cwxi30tPQs3bIO+aolQJNp1bIyw==} @@ -2409,9 +2455,15 @@ packages: hast-util-to-jsx-runtime@2.3.0: resolution: {integrity: sha512-H/y0+IWPdsLLS738P8tDnrQ8Z+dj12zQQ6WC11TIM21C8WFVoIxcqWXf2H3hiTVZjF1AWqoimGwrTWecWrnmRQ==} + hast-util-to-mdast@10.1.2: + resolution: {integrity: sha512-FiCRI7NmOvM4y+f5w32jPRzcxDIz+PUqDwEqn1A+1q2cdp3B8Gx7aVrXORdOKjMNDQsD1ogOr896+0jJHW1EFQ==} + hast-util-to-string@3.0.1: resolution: {integrity: sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==} + hast-util-to-text@4.0.2: + resolution: {integrity: sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==} + hast-util-whitespace@1.0.4: resolution: {integrity: sha512-I5GTdSfhYfAPNztx2xJRQpG8cuDSNt599/7YUn7Gx/WxNMsG+a835k97TDkFgk123cwjfwINaZknkKkphx/f2A==} @@ -2857,12 +2909,36 @@ packages: resolution: {integrity: sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==} engines: {node: '>=16'} + markdown-table@3.0.4: + resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + + mdast-util-find-and-replace@3.0.2: + resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} + mdast-util-from-markdown@2.0.1: resolution: {integrity: sha512-aJEUyzZ6TzlsX2s5B4Of7lN7EQtAxvtradMMglCQDyaTFgse6CmtmdJ15ElnVRlCg1vpNyVtbem0PWzlNieZsA==} mdast-util-frontmatter@2.0.1: resolution: {integrity: sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA==} + mdast-util-gfm-autolink-literal@2.0.1: + resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} + + mdast-util-gfm-footnote@2.1.0: + resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==} + + mdast-util-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} + + mdast-util-gfm-table@2.0.0: + resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + + mdast-util-gfm-task-list-item@2.0.0: + resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + + mdast-util-gfm@3.1.0: + resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} + mdast-util-mdx-expression@2.0.1: resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} @@ -2909,6 +2985,27 @@ packages: micromark-extension-frontmatter@2.0.0: resolution: {integrity: sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg==} + micromark-extension-gfm-autolink-literal@2.1.0: + resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} + + micromark-extension-gfm-footnote@2.1.0: + resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} + + micromark-extension-gfm-strikethrough@2.1.0: + resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} + + micromark-extension-gfm-table@2.1.1: + resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==} + + micromark-extension-gfm-tagfilter@2.0.0: + resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} + + micromark-extension-gfm-task-list-item@2.1.0: + resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} + + micromark-extension-gfm@3.0.0: + resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + micromark-extension-mdx-expression@3.0.0: resolution: {integrity: sha512-sI0nwhUDz97xyzqJAbHQhp5TfaxEvZZZ2JDqUo+7NvyIYG6BZ5CPPqj2ogUoPJlmXHBnyZUzISg9+oUmU6tUjQ==} @@ -3124,6 +3221,9 @@ packages: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -3368,6 +3468,9 @@ packages: property-information@6.5.0: resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==} + property-information@7.1.0: + resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + protoduck@4.0.0: resolution: {integrity: sha512-9sxuz0YTU/68O98xuDn8NBxTVH9EuMhrBTxZdiBL0/qxRmWhB/5a8MagAebDa+98vluAZTs8kMZibCdezbRCeQ==} @@ -3476,12 +3579,18 @@ packages: resolution: {integrity: sha512-ZbgR5aZEdf4UKZVBPYIgaglBmSF2Hi94s2PcIHhRGFjKYu+chjJdYfHn4rt3hB6eCKLJ8giVIIfgMa1ehDfZKA==} engines: {node: '>=0.10.0'} + rehype-minify-whitespace@6.0.2: + resolution: {integrity: sha512-Zk0pyQ06A3Lyxhe9vGtOtzz3Z0+qZ5+7icZ/PL/2x1SHPbKao5oB/g/rlc6BCTajqBb33JcOe71Ye1oFsuYbnw==} + rehype-parse@7.0.1: resolution: {integrity: sha512-fOiR9a9xH+Le19i4fGzIEowAbwG7idy2Jzs4mOrFWBSJ0sNUgy0ev871dwWnbOo371SjgjG4pwzrbgSVrKxecw==} rehype-parse@9.0.1: resolution: {integrity: sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==} + rehype-remark@10.0.1: + resolution: {integrity: sha512-EmDndlb5NVwXGfUa4c9GPK+lXeItTilLhE6ADSaQuHr4JUlKw9MidzGzx4HpqZrNCt6vnHmEifXQiiA+CEnjYQ==} + rehype-stringify@8.0.0: resolution: {integrity: sha512-VkIs18G0pj2xklyllrPSvdShAV36Ff3yE5PUO9u36f6+2qJFnn22Z5gKwBOwgXviux4UC7K+/j13AnZfPICi/g==} @@ -3491,6 +3600,9 @@ packages: remark-frontmatter@5.0.0: resolution: {integrity: sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ==} + remark-gfm@4.0.1: + resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} + remark-mdx-frontmatter@4.0.0: resolution: {integrity: sha512-PZzAiDGOEfv1Ua7exQ8S5kKxkD8CDaSb4nM+1Mprs6u8dyvQifakh+kCj6NovfGXW+bTvrhjaR3srzjS2qJHKg==} @@ -3506,6 +3618,9 @@ packages: remark-slug@6.1.0: resolution: {integrity: sha512-oGCxDF9deA8phWvxFuyr3oSJsdyUAxMFbA0mZ7Y1Sas+emILtO+e5WutF9564gDsEN4IXaQXm5pFo6MLH+YmwQ==} + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + remote-origin-url@0.4.0: resolution: {integrity: sha512-HYhdsT2pNd0LP4Osb0vtQ1iassxIc3Yk1oze7j8dMJFciMkW8e0rdg9E/mOunqtSVHSzvMfwLDIYzPnEDmpk6Q==} engines: {node: '>= 0.8.0'} @@ -3895,6 +4010,9 @@ packages: trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + trim-trailing-lines@2.1.0: + resolution: {integrity: sha512-5UR5Biq4VlVOtzqkm2AZlgvSlDJtME46uV0br0gENbwN4l5+mMKT4b9gJKqWtuL2zAIqajGJGuvbCbcAJUZqBg==} + trough@1.0.5: resolution: {integrity: sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==} @@ -3976,6 +4094,9 @@ packages: unist-builder@4.0.0: resolution: {integrity: sha512-wmRFnH+BLpZnTKpc5L7O67Kac89s9HMrtELpnNaE6TAobq5DTZZs5YaTQfAZBA9bFPECx2uVAPO31c+GVug8mg==} + unist-util-find-after@5.0.0: + resolution: {integrity: sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==} + unist-util-is@4.1.0: resolution: {integrity: sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==} @@ -5634,6 +5755,8 @@ snapshots: balanced-match@1.0.2: {} + bcp-47-match@2.0.3: {} + bezier-easing@2.1.0: {} binaryextensions@2.3.0: {} @@ -5645,6 +5768,8 @@ snapshots: bluebird@3.7.2: {} + boolbase@1.0.0: {} + boxen@1.3.0: dependencies: ansi-align: 2.0.0 @@ -5902,6 +6027,8 @@ snapshots: crypto-random-string@1.0.0: {} + css-selector-parser@3.3.0: {} + csstype@3.1.3: {} cwd@0.9.1: @@ -5998,6 +6125,8 @@ snapshots: dependencies: dequal: 2.0.3 + direction@2.0.1: {} + doctrine@2.1.0: dependencies: esutils: 2.0.3 @@ -6754,6 +6883,11 @@ snapshots: dependencies: function-bind: 1.1.2 + hast-util-embedded@3.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-is-element: 3.0.0 + hast-util-from-html@2.0.3: dependencies: '@types/hast': 3.0.4 @@ -6783,14 +6917,60 @@ snapshots: vfile-location: 5.0.3 web-namespaces: 2.0.1 + hast-util-has-property@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hast-util-is-body-ok-link@3.0.1: + dependencies: + '@types/hast': 3.0.4 + hast-util-is-element@1.1.0: {} + hast-util-is-element@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hast-util-minify-whitespace@1.0.1: + dependencies: + '@types/hast': 3.0.4 + hast-util-embedded: 3.0.0 + hast-util-is-element: 3.0.0 + hast-util-whitespace: 3.0.0 + unist-util-is: 6.0.0 + hast-util-parse-selector@2.2.5: {} hast-util-parse-selector@4.0.0: dependencies: '@types/hast': 3.0.4 + hast-util-phrasing@3.0.1: + dependencies: + '@types/hast': 3.0.4 + hast-util-embedded: 3.0.0 + hast-util-has-property: 3.0.0 + hast-util-is-body-ok-link: 3.0.1 + hast-util-is-element: 3.0.0 + + hast-util-select@6.0.4: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + bcp-47-match: 2.0.3 + comma-separated-tokens: 2.0.3 + css-selector-parser: 3.3.0 + devlop: 1.1.0 + direction: 2.0.1 + hast-util-has-property: 3.0.0 + hast-util-to-string: 3.0.1 + hast-util-whitespace: 3.0.0 + nth-check: 2.1.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + unist-util-visit: 5.0.0 + zwitch: 2.0.4 + hast-util-to-estree@3.1.0: dependencies: '@types/estree': 1.0.6 @@ -6859,10 +7039,34 @@ snapshots: transitivePeerDependencies: - supports-color + hast-util-to-mdast@10.1.2: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.2.0 + hast-util-phrasing: 3.0.1 + hast-util-to-html: 9.0.3 + hast-util-to-text: 4.0.2 + hast-util-whitespace: 3.0.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-hast: 13.2.0 + mdast-util-to-string: 4.0.0 + rehype-minify-whitespace: 6.0.2 + trim-trailing-lines: 2.1.0 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + hast-util-to-string@3.0.1: dependencies: '@types/hast': 3.0.4 + hast-util-to-text@4.0.2: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + hast-util-is-element: 3.0.0 + unist-util-find-after: 5.0.0 + hast-util-whitespace@1.0.4: {} hast-util-whitespace@3.0.0: @@ -7289,6 +7493,15 @@ snapshots: markdown-extensions@2.0.0: {} + markdown-table@3.0.4: {} + + mdast-util-find-and-replace@3.0.2: + dependencies: + '@types/mdast': 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + mdast-util-from-markdown@2.0.1: dependencies: '@types/mdast': 4.0.4 @@ -7317,6 +7530,63 @@ snapshots: transitivePeerDependencies: - supports-color + mdast-util-gfm-autolink-literal@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-find-and-replace: 3.0.2 + micromark-util-character: 2.1.0 + + mdast-util-gfm-footnote@2.1.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.1 + mdast-util-to-markdown: 2.1.0 + micromark-util-normalize-identifier: 2.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-strikethrough@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.1 + mdast-util-to-markdown: 2.1.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-table@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + markdown-table: 3.0.4 + mdast-util-from-markdown: 2.0.1 + mdast-util-to-markdown: 2.1.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-task-list-item@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.1 + mdast-util-to-markdown: 2.1.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm@3.1.0: + dependencies: + mdast-util-from-markdown: 2.0.1 + mdast-util-gfm-autolink-literal: 2.0.1 + mdast-util-gfm-footnote: 2.1.0 + mdast-util-gfm-strikethrough: 2.0.0 + mdast-util-gfm-table: 2.0.0 + mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-to-markdown: 2.1.0 + transitivePeerDependencies: + - supports-color + mdast-util-mdx-expression@2.0.1: dependencies: '@types/estree-jsx': 1.0.5 @@ -7445,6 +7715,64 @@ snapshots: micromark-util-symbol: 2.0.0 micromark-util-types: 2.0.0 + micromark-extension-gfm-autolink-literal@2.1.0: + dependencies: + micromark-util-character: 2.1.0 + micromark-util-sanitize-uri: 2.0.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-extension-gfm-footnote@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-core-commonmark: 2.0.1 + micromark-factory-space: 2.0.0 + micromark-util-character: 2.1.0 + micromark-util-normalize-identifier: 2.0.0 + micromark-util-sanitize-uri: 2.0.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-extension-gfm-strikethrough@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.0 + micromark-util-classify-character: 2.0.0 + micromark-util-resolve-all: 2.0.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-extension-gfm-table@2.1.1: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.0 + micromark-util-character: 2.1.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-extension-gfm-tagfilter@2.0.0: + dependencies: + micromark-util-types: 2.0.0 + + micromark-extension-gfm-task-list-item@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.0 + micromark-util-character: 2.1.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-extension-gfm@3.0.0: + dependencies: + micromark-extension-gfm-autolink-literal: 2.1.0 + micromark-extension-gfm-footnote: 2.1.0 + micromark-extension-gfm-strikethrough: 2.1.0 + micromark-extension-gfm-table: 2.1.1 + micromark-extension-gfm-tagfilter: 2.0.0 + micromark-extension-gfm-task-list-item: 2.1.0 + micromark-util-combine-extensions: 2.0.0 + micromark-util-types: 2.0.0 + micromark-extension-mdx-expression@3.0.0: dependencies: '@types/estree': 1.0.6 @@ -7791,6 +8119,10 @@ snapshots: dependencies: path-key: 3.1.1 + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + object-assign@4.1.1: {} object-inspect@1.13.2: {} @@ -8068,6 +8400,8 @@ snapshots: property-information@6.5.0: {} + property-information@7.1.0: {} + protoduck@4.0.0: dependencies: genfun: 4.0.1 @@ -8251,6 +8585,11 @@ snapshots: dependencies: rc: 1.2.8 + rehype-minify-whitespace@6.0.2: + dependencies: + '@types/hast': 3.0.4 + hast-util-minify-whitespace: 1.0.1 + rehype-parse@7.0.1: dependencies: hast-util-from-parse5: 6.0.1 @@ -8262,6 +8601,14 @@ snapshots: hast-util-from-html: 2.0.3 unified: 11.0.5 + rehype-remark@10.0.1: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + hast-util-to-mdast: 10.1.2 + unified: 11.0.5 + vfile: 6.0.3 + rehype-stringify@8.0.0: dependencies: hast-util-to-html: 7.1.3 @@ -8281,6 +8628,17 @@ snapshots: transitivePeerDependencies: - supports-color + remark-gfm@4.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-gfm: 3.1.0 + micromark-extension-gfm: 3.0.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + remark-mdx-frontmatter@4.0.0: dependencies: '@types/mdast': 4.0.4 @@ -8320,6 +8678,12 @@ snapshots: mdast-util-to-string: 1.1.0 unist-util-visit: 2.0.3 + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-markdown: 2.1.0 + unified: 11.0.5 + remote-origin-url@0.4.0: dependencies: parse-git-config: 0.2.0 @@ -8722,6 +9086,8 @@ snapshots: trim-lines@3.0.1: {} + trim-trailing-lines@2.1.0: {} + trough@1.0.5: {} trough@2.2.0: {} @@ -8830,6 +9196,11 @@ snapshots: dependencies: '@types/unist': 3.0.3 + unist-util-find-after@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + unist-util-is@4.1.0: {} unist-util-is@6.0.0: From 604514926674b05f1c4b87ec05e901e1d2c6b4ee Mon Sep 17 00:00:00 2001 From: Lucas Motta Date: Wed, 4 Feb 2026 11:41:54 -0300 Subject: [PATCH 02/15] Fix hidden labels and strong serialization error - Remove [data-algolia-lvl0] hidden elements (e.g., 'Components', 'Guides') - Fix remarkStringify strong option: use '*' instead of '**' (library doubles it) --- pages/api/markdown/[...path].ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pages/api/markdown/[...path].ts b/pages/api/markdown/[...path].ts index f7d6390a..526b63fe 100644 --- a/pages/api/markdown/[...path].ts +++ b/pages/api/markdown/[...path].ts @@ -70,7 +70,7 @@ async function convertHtmlToMarkdown(html: string): Promise { .use(remarkStringify, { bullet: "-", emphasis: "_", - strong: "**", + strong: "*", fence: "`", fences: true, listItemIndent: "one", @@ -116,6 +116,12 @@ function cleanupHtml() { removeNode(tree, node); } + // Remove hidden category labels (e.g., "Components", "Guides") + const hiddenLabels = selectAll("[data-algolia-lvl0]", tree); + for (const node of hiddenLabels) { + removeNode(tree, node); + } + // Remove navigation and footer elements const navFooterSelectors = ["nav", "footer"]; for (const selector of navFooterSelectors) { From 604e2287f524d4be4c9152307bc1bc379bcaf7f9 Mon Sep 17 00:00:00 2001 From: Lucas Motta Date: Wed, 4 Feb 2026 11:54:07 -0300 Subject: [PATCH 03/15] filter out live preview elements and add markdown formatting --- package.json | 1 + pages/api/markdown/[...path].ts | 12 ++++- pnpm-lock.yaml | 91 +++++++++++++++++++++++++++++++++ 3 files changed, 103 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index f16a06a0..4d0cfa99 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "@types/unist": "^3.0.3", "autoprefixer": "^10.4.19", "husky": "^9.1.6", + "oxfmt": "0.28.0", "prettier": "^3.3.3", "pretty-quick": "^4.0.0", "sucrase": "3.29.0", diff --git a/pages/api/markdown/[...path].ts b/pages/api/markdown/[...path].ts index 526b63fe..498bb991 100644 --- a/pages/api/markdown/[...path].ts +++ b/pages/api/markdown/[...path].ts @@ -1,4 +1,5 @@ import type { NextApiRequest, NextApiResponse } from "next"; +import { format } from "oxfmt"; import { unified } from "unified"; import rehypeParse from "rehype-parse"; import rehypeRemark from "rehype-remark"; @@ -46,12 +47,15 @@ export default async function handler( // Convert HTML to Markdown const markdown = await convertHtmlToMarkdown(html); + // Format markdown + const { code: formattedMarkdown } = await format("file.md", markdown); + // Set appropriate headers res.setHeader("Content-Type", "text/markdown; charset=utf-8"); res.setHeader("X-Content-Type-Options", "nosniff"); res.setHeader("Cache-Control", "public, max-age=3600, s-maxage=86400"); - return res.status(200).send(markdown); + return res.status(200).send(formattedMarkdown); } catch (error) { console.error("Error converting to markdown:", error); return res @@ -122,6 +126,12 @@ function cleanupHtml() { removeNode(tree, node); } + // Remove live preview elements (interactive demos) + const livePreviews = selectAll("[data-live-preview='true']", tree); + for (const node of livePreviews) { + removeNode(tree, node); + } + // Remove navigation and footer elements const navFooterSelectors = ["nav", "footer"]; for (const selector of navFooterSelectors) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c547d227..ec49bedb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -177,6 +177,9 @@ importers: husky: specifier: ^9.1.6 version: 9.1.6 + oxfmt: + specifier: 0.28.0 + version: 0.28.0 prettier: specifier: ^3.3.3 version: 3.3.3 @@ -535,6 +538,46 @@ packages: resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} engines: {node: '>=12.4.0'} + '@oxfmt/darwin-arm64@0.28.0': + resolution: {integrity: sha512-jmUfF7cNJPw57bEK7sMIqrYRgn4LH428tSgtgLTCtjuGuu1ShREyrkeB7y8HtkXRfhBs4lVY+HMLhqElJvZ6ww==} + cpu: [arm64] + os: [darwin] + + '@oxfmt/darwin-x64@0.28.0': + resolution: {integrity: sha512-S6vlV8S7jbjzJOSjfVg2CimUC0r7/aHDLdUm/3+/B/SU/s1jV7ivqWkMv1/8EB43d1BBwT9JQ60ZMTkBqeXSFA==} + cpu: [x64] + os: [darwin] + + '@oxfmt/linux-arm64-gnu@0.28.0': + resolution: {integrity: sha512-TfJkMZjePbLiskmxFXVAbGI/OZtD+y+fwS0wyW8O6DWG0ARTf0AipY9zGwGoOdpFuXOJceXvN4SHGLbYNDMY4Q==} + cpu: [arm64] + os: [linux] + + '@oxfmt/linux-arm64-musl@0.28.0': + resolution: {integrity: sha512-7fyQUdW203v4WWGr1T3jwTz4L7KX9y5DeATryQ6fLT6QQp9GEuct8/k0lYhd+ys42iTV/IkJF20e3YkfSOOILg==} + cpu: [arm64] + os: [linux] + + '@oxfmt/linux-x64-gnu@0.28.0': + resolution: {integrity: sha512-sRKqAvEonuz0qr1X1ncUZceOBJerKzkO2gZIZmosvy/JmqyffpIFL3OE2tqacFkeDhrC+dNYQpusO8zsfHo3pw==} + cpu: [x64] + os: [linux] + + '@oxfmt/linux-x64-musl@0.28.0': + resolution: {integrity: sha512-fW6czbXutX/tdQe8j4nSIgkUox9RXqjyxwyWXUDItpoDkoXllq17qbD7GVc0whrEhYQC6hFE1UEAcDypLJoSzw==} + cpu: [x64] + os: [linux] + + '@oxfmt/win32-arm64@0.28.0': + resolution: {integrity: sha512-D/HDeQBAQRjTbD9OLV6kRDcStrIfO+JsUODDCdGmhRfNX8LPCx95GpfyybpZfn3wVF8Jq/yjPXV1xLkQ+s7RcA==} + cpu: [arm64] + os: [win32] + + '@oxfmt/win32-x64@0.28.0': + resolution: {integrity: sha512-4+S2j4OxOIyo8dz5osm5dZuL0yVmxXvtmNdHB5xyGwAWVvyWNvf7tCaQD7w2fdSsAXQLOvK7KFQrHFe33nJUCA==} + cpu: [x64] + os: [win32] + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -3295,6 +3338,11 @@ packages: resolution: {integrity: sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==} deprecated: This package is no longer supported. + oxfmt@0.28.0: + resolution: {integrity: sha512-3+hhBqPE6Kp22KfJmnstrZbl+KdOVSEu1V0ABaFIg1rYLtrMgrupx9znnHgHLqKxAVHebjTdiCJDk30CXOt6cw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + p-finally@1.0.0: resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} engines: {node: '>=4'} @@ -3993,6 +4041,10 @@ packages: tinycolor2@1.6.0: resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} + tinypool@2.1.0: + resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==} + engines: {node: ^20.0.0 || >=22.0.0} + tmp@0.0.33: resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} engines: {node: '>=0.6.0'} @@ -4633,6 +4685,30 @@ snapshots: '@nolyfill/is-core-module@1.0.39': {} + '@oxfmt/darwin-arm64@0.28.0': + optional: true + + '@oxfmt/darwin-x64@0.28.0': + optional: true + + '@oxfmt/linux-arm64-gnu@0.28.0': + optional: true + + '@oxfmt/linux-arm64-musl@0.28.0': + optional: true + + '@oxfmt/linux-x64-gnu@0.28.0': + optional: true + + '@oxfmt/linux-x64-musl@0.28.0': + optional: true + + '@oxfmt/win32-arm64@0.28.0': + optional: true + + '@oxfmt/win32-x64@0.28.0': + optional: true + '@pkgjs/parseargs@0.11.0': optional: true @@ -8207,6 +8283,19 @@ snapshots: os-homedir: 1.0.2 os-tmpdir: 1.0.2 + oxfmt@0.28.0: + dependencies: + tinypool: 2.1.0 + optionalDependencies: + '@oxfmt/darwin-arm64': 0.28.0 + '@oxfmt/darwin-x64': 0.28.0 + '@oxfmt/linux-arm64-gnu': 0.28.0 + '@oxfmt/linux-arm64-musl': 0.28.0 + '@oxfmt/linux-x64-gnu': 0.28.0 + '@oxfmt/linux-x64-musl': 0.28.0 + '@oxfmt/win32-arm64': 0.28.0 + '@oxfmt/win32-x64': 0.28.0 + p-finally@1.0.0: {} p-limit@3.1.0: @@ -9072,6 +9161,8 @@ snapshots: tinycolor2@1.6.0: {} + tinypool@2.1.0: {} + tmp@0.0.33: dependencies: os-tmpdir: 1.0.2 From 045095bda1db6efb71c40e181e31458cbdbf9828 Mon Sep 17 00:00:00 2001 From: Lucas Motta Date: Wed, 4 Feb 2026 11:54:14 -0300 Subject: [PATCH 04/15] add data-live-preview attribute to live preview component --- components/CodeBlock.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/CodeBlock.tsx b/components/CodeBlock.tsx index 40264679..56284fd1 100644 --- a/components/CodeBlock.tsx +++ b/components/CodeBlock.tsx @@ -48,7 +48,7 @@ const LivePreview = React.forwardRef( forwardedRef, ) { return ( - + Date: Wed, 4 Feb 2026 11:58:52 -0300 Subject: [PATCH 05/15] filter out visually-hidden accessibility spans --- pages/api/markdown/[...path].ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/pages/api/markdown/[...path].ts b/pages/api/markdown/[...path].ts index 498bb991..93d24de5 100644 --- a/pages/api/markdown/[...path].ts +++ b/pages/api/markdown/[...path].ts @@ -132,6 +132,20 @@ function cleanupHtml() { removeNode(tree, node); } + // Remove visually-hidden elements (AccessibleIcon, screen-reader-only spans) + // These use clip:rect(0, 0, 0, 0) or similar visually-hidden CSS patterns + const allSpans = selectAll("span", tree) as Element[]; + for (const span of allSpans) { + const style = span.properties?.style; + if ( + typeof style === "string" && + (style.includes("clip:rect(0, 0, 0, 0)") || + style.includes("clip: rect(0, 0, 0, 0)")) + ) { + removeNode(tree, span); + } + } + // Remove navigation and footer elements const navFooterSelectors = ["nav", "footer"]; for (const selector of navFooterSelectors) { From ecce5641edae147e0c41f473102ee39ad0c11de5 Mon Sep 17 00:00:00 2001 From: Lucas Motta Date: Wed, 4 Feb 2026 12:01:29 -0300 Subject: [PATCH 06/15] use data-md-exclude convention for filtering visually-hidden elements --- components/PropsTable.tsx | 24 +++++++++++++++--------- pages/api/markdown/[...path].ts | 16 ++++------------ 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/components/PropsTable.tsx b/components/PropsTable.tsx index 05a08d27..ddf162f7 100644 --- a/components/PropsTable.tsx +++ b/components/PropsTable.tsx @@ -9,9 +9,9 @@ import { Flex, Inset, ScrollArea, + VisuallyHidden, } from "@radix-ui/themes"; import { InfoCircledIcon, DividerHorizontalIcon } from "@radix-ui/react-icons"; -import { AccessibleIcon } from "radix-ui"; export type PropDef = { name: string; @@ -74,9 +74,10 @@ export function PropsTable({ - - - + - - - + ) : ( - + <> + + No default value + + )} diff --git a/pages/api/markdown/[...path].ts b/pages/api/markdown/[...path].ts index 93d24de5..bf7b6a03 100644 --- a/pages/api/markdown/[...path].ts +++ b/pages/api/markdown/[...path].ts @@ -132,18 +132,10 @@ function cleanupHtml() { removeNode(tree, node); } - // Remove visually-hidden elements (AccessibleIcon, screen-reader-only spans) - // These use clip:rect(0, 0, 0, 0) or similar visually-hidden CSS patterns - const allSpans = selectAll("span", tree) as Element[]; - for (const span of allSpans) { - const style = span.properties?.style; - if ( - typeof style === "string" && - (style.includes("clip:rect(0, 0, 0, 0)") || - style.includes("clip: rect(0, 0, 0, 0)")) - ) { - removeNode(tree, span); - } + // Remove elements marked for markdown exclusion + const mdExcluded = selectAll("[data-md-exclude]", tree); + for (const node of mdExcluded) { + removeNode(tree, node); } // Remove navigation and footer elements From 46d74427ed72a41371dd64516cbffd1869fd8407 Mon Sep 17 00:00:00 2001 From: Lucas Motta Date: Wed, 4 Feb 2026 12:03:04 -0300 Subject: [PATCH 07/15] consolidate to single data-md-exclude attribute for markdown filtering --- components/CodeBlock.tsx | 2 +- pages/api/markdown/[...path].ts | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/components/CodeBlock.tsx b/components/CodeBlock.tsx index 56284fd1..3662bba8 100644 --- a/components/CodeBlock.tsx +++ b/components/CodeBlock.tsx @@ -48,7 +48,7 @@ const LivePreview = React.forwardRef( forwardedRef, ) { return ( - + Date: Wed, 4 Feb 2026 12:43:00 -0300 Subject: [PATCH 08/15] Add hast type --- package.json | 1 + pages/api/markdown/[...path].ts | 2 +- pnpm-lock.yaml | 3 +++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 4d0cfa99..61b3e672 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "usehooks-ts": "^3.0.1" }, "devDependencies": { + "@types/hast": "3.0.4", "@types/lodash.debounce": "^4.0.6", "@types/node": "20.12.10", "@types/react": "^18.3.4", diff --git a/pages/api/markdown/[...path].ts b/pages/api/markdown/[...path].ts index 74464158..912ae9b0 100644 --- a/pages/api/markdown/[...path].ts +++ b/pages/api/markdown/[...path].ts @@ -211,7 +211,7 @@ function removeNode(tree: Root | Element, nodeToRemove: Element): void { return true; } for (const child of node.children) { - if (child.type === "element" || child.type === "root") { + if (child.type === "element") { if (visit(child as Element)) { return true; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ec49bedb..7c5c90b2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -147,6 +147,9 @@ importers: specifier: ^3.0.1 version: 3.1.0(react@18.3.1) devDependencies: + '@types/hast': + specifier: 3.0.4 + version: 3.0.4 '@types/lodash.debounce': specifier: ^4.0.6 version: 4.0.9 From 7aad0143fc78dacdfc5d90f36fc01d126158d4aa Mon Sep 17 00:00:00 2001 From: Lucas Motta Date: Wed, 4 Feb 2026 12:52:38 -0300 Subject: [PATCH 09/15] add 'View as markdown' link to documentation pages --- components/Highlights.tsx | 60 +++++++++++++++++-------- pages/themes/docs/components/[slug].tsx | 11 ++++- 2 files changed, 51 insertions(+), 20 deletions(-) diff --git a/components/Highlights.tsx b/components/Highlights.tsx index 273dbc70..b70c73c4 100644 --- a/components/Highlights.tsx +++ b/components/Highlights.tsx @@ -9,7 +9,11 @@ import { Link, Select, } from "@radix-ui/themes"; -import { ArrowTopRightIcon, CheckIcon } from "@radix-ui/react-icons"; +import { + ArrowTopRightIcon, + CheckIcon, + FileTextIcon, +} from "@radix-ui/react-icons"; import { useRouter } from "next/router"; import { VisuallyHidden } from "radix-ui"; import { FrontmatterContext } from "./MDXComponents"; @@ -35,24 +39,28 @@ export function Highlights({ features }: { features: React.ReactNode[] }) { {features.map( (feature, i) => feature != null && ( - - - - - - {feature} - + +
  • + + + + + {feature} + +
  • ), )} @@ -157,6 +165,20 @@ export function Highlights({ features }: { features: React.ReactNode[] }) {
    )} + + + + + View as Markdown + + +
    diff --git a/pages/themes/docs/components/[slug].tsx b/pages/themes/docs/components/[slug].tsx index 88e55b8a..0fdb9d37 100644 --- a/pages/themes/docs/components/[slug].tsx +++ b/pages/themes/docs/components/[slug].tsx @@ -1,13 +1,14 @@ import * as React from "react"; import { getMDXComponent } from "mdx-bundler/client"; import NextLink from "next/link"; +import { useRouter } from "next/router"; import { Box, Flex, Link, Text, Heading } from "@radix-ui/themes"; import { TitleAndMetaTags } from "@components/TitleAndMetaTags"; import { MDXProvider } from "@components/MDXComponents"; import { ThemesMDXComponents } from "@components/ThemesMDXComponents"; import { getAllFrontmatter, getMdxBySlug } from "@utils/mdx"; import { QuickNav } from "@components/QuickNav"; -import { ArrowTopRightIcon } from "@radix-ui/react-icons"; +import { ArrowTopRightIcon, FileTextIcon } from "@radix-ui/react-icons"; import type { Frontmatter } from "types/frontmatter"; import { GetStaticPropsContext } from "next"; @@ -20,6 +21,7 @@ type Doc = { export default function GuidesDoc({ frontmatter, code }: Doc) { const Component = React.useMemo(() => getMDXComponent(code), [code]); const componentName = frontmatter.metaTitle.replace(/\s+/g, ""); + const router = useRouter(); return ( <> @@ -48,6 +50,7 @@ export default function GuidesDoc({ frontmatter, code }: Doc) { + + + View as Markdown + + + {hasPlaygroundExample(frontmatter.slug) && ( Date: Wed, 4 Feb 2026 13:02:23 -0300 Subject: [PATCH 10/15] add component index pages for primitives and themes --- data/primitives/docs/components/index.mdx | 41 ++++++++++++ data/themes/docs/components/index.mdx | 81 +++++++++++++++++++++++ 2 files changed, 122 insertions(+) create mode 100644 data/primitives/docs/components/index.mdx create mode 100644 data/themes/docs/components/index.mdx diff --git a/data/primitives/docs/components/index.mdx b/data/primitives/docs/components/index.mdx new file mode 100644 index 00000000..9fb216eb --- /dev/null +++ b/data/primitives/docs/components/index.mdx @@ -0,0 +1,41 @@ +--- +metaTitle: Components +metaDescription: Unstyled, accessible UI primitives for building high-quality design systems and web apps. +--- + +# Components + +Radix Primitives is a low-level UI component library with a focus on accessibility, customization and developer experience. You can use these components either as the base layer of your design system, or adopt them incrementally. + +## Available Components + +- [Accordion](/primitives/docs/components/accordion) - A vertically stacked set of interactive headings that each reveal an associated section of content. +- [Alert Dialog](/primitives/docs/components/alert-dialog) - A modal dialog that interrupts the user with important content and expects a response. +- [Aspect Ratio](/primitives/docs/components/aspect-ratio) - Displays content within a desired ratio. +- [Avatar](/primitives/docs/components/avatar) - An image element with a fallback for representing the user. +- [Checkbox](/primitives/docs/components/checkbox) - A control that allows the user to toggle between checked and not checked. +- [Collapsible](/primitives/docs/components/collapsible) - An interactive component which expands/collapses a panel. +- [Context Menu](/primitives/docs/components/context-menu) - Displays a menu located at the pointer, triggered by a right-click or a long-press. +- [Dialog](/primitives/docs/components/dialog) - A window overlaid on either the primary window or another dialog window, rendering the content underneath inert. +- [Dropdown Menu](/primitives/docs/components/dropdown-menu) - Displays a menu to the user, such as a set of actions or functions, triggered by a button. +- [Form](/primitives/docs/components/form) - Collect information from your users using validation rules. +- [Hover Card](/primitives/docs/components/hover-card) - For sighted users to preview content available behind a link. +- [Label](/primitives/docs/components/label) - Renders an accessible label associated with controls. +- [Menubar](/primitives/docs/components/menubar) - A visually persistent menu common in desktop applications that provides quick access to a consistent set of commands. +- [Navigation Menu](/primitives/docs/components/navigation-menu) - A collection of links for navigating websites. +- [One Time Password Field](/primitives/docs/components/one-time-password-field) - A control that allows users to input a one-time password. +- [Password Toggle Field](/primitives/docs/components/password-toggle-field) - A control that allows users to input a password with visibility toggle. +- [Popover](/primitives/docs/components/popover) - Displays rich content in a portal, triggered by a button. +- [Progress](/primitives/docs/components/progress) - Displays an indicator showing the completion progress of a task, typically displayed as a progress bar. +- [Radio Group](/primitives/docs/components/radio-group) - A set of checkable buttons, known as radio buttons, where no more than one of the buttons can be checked at a time. +- [Scroll Area](/primitives/docs/components/scroll-area) - Augments native scroll functionality for custom, cross-browser styling. +- [Select](/primitives/docs/components/select) - Displays a list of options for the user to pick from, triggered by a button. +- [Separator](/primitives/docs/components/separator) - Visually or semantically separates content. +- [Slider](/primitives/docs/components/slider) - An input where the user selects a value from within a given range. +- [Switch](/primitives/docs/components/switch) - A control that allows the user to toggle between checked and not checked. +- [Tabs](/primitives/docs/components/tabs) - A set of layered sections of content, known as tab panels, that are displayed one at a time. +- [Toast](/primitives/docs/components/toast) - A succinct message that is displayed temporarily. +- [Toggle](/primitives/docs/components/toggle) - A two-state button that can be either on or off. +- [Toggle Group](/primitives/docs/components/toggle-group) - A set of two-state buttons that can be toggled on or off. +- [Toolbar](/primitives/docs/components/toolbar) - A container for grouping a set of controls, such as buttons, toggle groups or dropdown menus. +- [Tooltip](/primitives/docs/components/tooltip) - A popup that displays information related to an element when the element receives keyboard focus or the mouse hovers over it. diff --git a/data/themes/docs/components/index.mdx b/data/themes/docs/components/index.mdx new file mode 100644 index 00000000..3b479e1d --- /dev/null +++ b/data/themes/docs/components/index.mdx @@ -0,0 +1,81 @@ +--- +metaTitle: Components +metaDescription: A comprehensive library of React components for building beautiful, accessible user interfaces. +sourcePath: components +--- + +# Components + +Radix Themes provides a comprehensive set of pre-styled, accessible React components that work out of the box. All components support theming, dark mode, and are built with accessibility in mind. + +## Available Components + +### Layout +- [Box](/themes/docs/components/box) - A fundamental layout building block. +- [Flex](/themes/docs/components/flex) - A flexbox container for arranging elements. +- [Grid](/themes/docs/components/grid) - A grid container for two-dimensional layouts. +- [Container](/themes/docs/components/container) - Constrains content to a maximum width. +- [Section](/themes/docs/components/section) - A semantic section with consistent spacing. + +### Typography +- [Text](/themes/docs/components/text) - Renders text with consistent styling. +- [Heading](/themes/docs/components/heading) - Semantic heading elements with styling. +- [Blockquote](/themes/docs/components/blockquote) - A styled blockquote element. +- [Code](/themes/docs/components/code) - Inline code styling. +- [Em](/themes/docs/components/em) - Emphasized text. +- [Kbd](/themes/docs/components/kbd) - Keyboard input styling. +- [Link](/themes/docs/components/link) - Styled anchor element. +- [Quote](/themes/docs/components/quote) - An inline quote element. +- [Strong](/themes/docs/components/strong) - Strong importance text. + +### Forms +- [Button](/themes/docs/components/button) - Trigger an action or event. +- [Checkbox](/themes/docs/components/checkbox) - A control to toggle between checked states. +- [Checkbox Cards](/themes/docs/components/checkbox-cards) - Checkbox presented as selectable cards. +- [Checkbox Group](/themes/docs/components/checkbox-group) - A group of related checkboxes. +- [Icon Button](/themes/docs/components/icon-button) - A button containing only an icon. +- [Radio](/themes/docs/components/radio) - A single radio button. +- [Radio Cards](/themes/docs/components/radio-cards) - Radio buttons presented as selectable cards. +- [Radio Group](/themes/docs/components/radio-group) - A set of radio buttons. +- [Segmented Control](/themes/docs/components/segmented-control) - A set of mutually exclusive buttons. +- [Select](/themes/docs/components/select) - A dropdown list of options. +- [Slider](/themes/docs/components/slider) - An input for selecting a value from a range. +- [Switch](/themes/docs/components/switch) - A toggle control for binary options. +- [Text Area](/themes/docs/components/text-area) - A multi-line text input. +- [Text Field](/themes/docs/components/text-field) - A single-line text input. + +### Display +- [Avatar](/themes/docs/components/avatar) - An image element with fallback for representing users. +- [Badge](/themes/docs/components/badge) - A small status indicator. +- [Callout](/themes/docs/components/callout) - A highlighted message or tip. +- [Card](/themes/docs/components/card) - A container for related content. +- [Data List](/themes/docs/components/data-list) - A list of key-value pairs. +- [Inset](/themes/docs/components/inset) - Allows content to span the full width of its container. +- [Separator](/themes/docs/components/separator) - Visually separates content. +- [Skeleton](/themes/docs/components/skeleton) - A placeholder for loading content. +- [Spinner](/themes/docs/components/spinner) - A loading indicator. +- [Table](/themes/docs/components/table) - A structured data table. + +### Overlays +- [Alert Dialog](/themes/docs/components/alert-dialog) - A modal dialog for important content. +- [Context Menu](/themes/docs/components/context-menu) - A menu triggered by right-click. +- [Dialog](/themes/docs/components/dialog) - A window overlaid on the primary window. +- [Dropdown Menu](/themes/docs/components/dropdown-menu) - A menu triggered by a button. +- [Hover Card](/themes/docs/components/hover-card) - Preview content behind a link. +- [Popover](/themes/docs/components/popover) - Rich content in a portal. +- [Tooltip](/themes/docs/components/tooltip) - Information displayed on hover or focus. + +### Navigation +- [Tab Nav](/themes/docs/components/tab-nav) - Navigation styled as tabs. +- [Tabs](/themes/docs/components/tabs) - Layered sections of content. + +### Utilities +- [Accessible Icon](/themes/docs/components/accessible-icon) - Makes icons accessible to screen readers. +- [Aspect Ratio](/themes/docs/components/aspect-ratio) - Displays content within a desired ratio. +- [Portal](/themes/docs/components/portal) - Renders content in a different part of the DOM. +- [Progress](/themes/docs/components/progress) - Displays task completion progress. +- [Reset](/themes/docs/components/reset) - Removes default browser styles. +- [Scroll Area](/themes/docs/components/scroll-area) - Custom, cross-browser scrolling. +- [Slot](/themes/docs/components/slot) - Merges props onto its immediate child. +- [Theme](/themes/docs/components/theme) - Provides theming context. +- [Visually Hidden](/themes/docs/components/visually-hidden) - Hides content visually but keeps it accessible. From 3b0ec943815868b6dbcef322092cc51350c4732a Mon Sep 17 00:00:00 2001 From: Lucas Motta Date: Wed, 4 Feb 2026 13:03:54 -0300 Subject: [PATCH 11/15] add index pages for /primitives/docs/components and /themes/docs/components --- pages/primitives/docs/components/index.tsx | 44 +++++++++++++++++++++ pages/themes/docs/components/index.tsx | 45 ++++++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 pages/primitives/docs/components/index.tsx create mode 100644 pages/themes/docs/components/index.tsx diff --git a/pages/primitives/docs/components/index.tsx b/pages/primitives/docs/components/index.tsx new file mode 100644 index 00000000..f0324317 --- /dev/null +++ b/pages/primitives/docs/components/index.tsx @@ -0,0 +1,44 @@ +import * as React from "react"; +import { getMDXComponent } from "mdx-bundler/client"; +import { TitleAndMetaTags } from "@components/TitleAndMetaTags"; +import { MDXProvider, components } from "@components/MDXComponents"; +import { QuickNav } from "@components/QuickNav"; +import { getMdxBySlug } from "@utils/mdx"; +import type { Frontmatter } from "types/frontmatter"; + +type Doc = { + frontmatter: Frontmatter; + code: string; +}; + +export default function ComponentsIndex({ frontmatter, code }: Doc) { + const Component = React.useMemo(() => getMDXComponent(code), [code]); + + return ( + <> +
    + Components +
    + + + + + + + + + + ); +} + +export async function getStaticProps() { + const { frontmatter, code } = await getMdxBySlug( + "primitives/docs/components/", + "index", + ); + return { props: { frontmatter, code } }; +} diff --git a/pages/themes/docs/components/index.tsx b/pages/themes/docs/components/index.tsx new file mode 100644 index 00000000..da18e4fd --- /dev/null +++ b/pages/themes/docs/components/index.tsx @@ -0,0 +1,45 @@ +import * as React from "react"; +import { getMDXComponent } from "mdx-bundler/client"; +import { TitleAndMetaTags } from "@components/TitleAndMetaTags"; +import { MDXProvider } from "@components/MDXComponents"; +import { ThemesMDXComponents } from "@components/ThemesMDXComponents"; +import { QuickNav } from "@components/QuickNav"; +import { getMdxBySlug } from "@utils/mdx"; +import type { Frontmatter } from "types/frontmatter"; + +type Doc = { + frontmatter: Frontmatter; + code: string; +}; + +export default function ComponentsIndex({ frontmatter, code }: Doc) { + const Component = React.useMemo(() => getMDXComponent(code), [code]); + + return ( + <> +
    + Components +
    + + + + + + + + + + ); +} + +export async function getStaticProps() { + const { frontmatter, code } = await getMdxBySlug( + "themes/docs/components/", + "index", + ); + return { props: { frontmatter, code } }; +} From 3da656e29ba59ffb47e09722d7b7694cc9478172 Mon Sep 17 00:00:00 2001 From: Lucas Motta Date: Wed, 4 Feb 2026 13:05:49 -0300 Subject: [PATCH 12/15] generate component index pages dynamically from frontmatter --- data/primitives/docs/components/index.mdx | 41 ----------- data/themes/docs/components/index.mdx | 81 ---------------------- pages/primitives/docs/components/index.tsx | 69 ++++++++++++------ pages/themes/docs/components/index.tsx | 70 +++++++++++++------ 4 files changed, 98 insertions(+), 163 deletions(-) delete mode 100644 data/primitives/docs/components/index.mdx delete mode 100644 data/themes/docs/components/index.mdx diff --git a/data/primitives/docs/components/index.mdx b/data/primitives/docs/components/index.mdx deleted file mode 100644 index 9fb216eb..00000000 --- a/data/primitives/docs/components/index.mdx +++ /dev/null @@ -1,41 +0,0 @@ ---- -metaTitle: Components -metaDescription: Unstyled, accessible UI primitives for building high-quality design systems and web apps. ---- - -# Components - -Radix Primitives is a low-level UI component library with a focus on accessibility, customization and developer experience. You can use these components either as the base layer of your design system, or adopt them incrementally. - -## Available Components - -- [Accordion](/primitives/docs/components/accordion) - A vertically stacked set of interactive headings that each reveal an associated section of content. -- [Alert Dialog](/primitives/docs/components/alert-dialog) - A modal dialog that interrupts the user with important content and expects a response. -- [Aspect Ratio](/primitives/docs/components/aspect-ratio) - Displays content within a desired ratio. -- [Avatar](/primitives/docs/components/avatar) - An image element with a fallback for representing the user. -- [Checkbox](/primitives/docs/components/checkbox) - A control that allows the user to toggle between checked and not checked. -- [Collapsible](/primitives/docs/components/collapsible) - An interactive component which expands/collapses a panel. -- [Context Menu](/primitives/docs/components/context-menu) - Displays a menu located at the pointer, triggered by a right-click or a long-press. -- [Dialog](/primitives/docs/components/dialog) - A window overlaid on either the primary window or another dialog window, rendering the content underneath inert. -- [Dropdown Menu](/primitives/docs/components/dropdown-menu) - Displays a menu to the user, such as a set of actions or functions, triggered by a button. -- [Form](/primitives/docs/components/form) - Collect information from your users using validation rules. -- [Hover Card](/primitives/docs/components/hover-card) - For sighted users to preview content available behind a link. -- [Label](/primitives/docs/components/label) - Renders an accessible label associated with controls. -- [Menubar](/primitives/docs/components/menubar) - A visually persistent menu common in desktop applications that provides quick access to a consistent set of commands. -- [Navigation Menu](/primitives/docs/components/navigation-menu) - A collection of links for navigating websites. -- [One Time Password Field](/primitives/docs/components/one-time-password-field) - A control that allows users to input a one-time password. -- [Password Toggle Field](/primitives/docs/components/password-toggle-field) - A control that allows users to input a password with visibility toggle. -- [Popover](/primitives/docs/components/popover) - Displays rich content in a portal, triggered by a button. -- [Progress](/primitives/docs/components/progress) - Displays an indicator showing the completion progress of a task, typically displayed as a progress bar. -- [Radio Group](/primitives/docs/components/radio-group) - A set of checkable buttons, known as radio buttons, where no more than one of the buttons can be checked at a time. -- [Scroll Area](/primitives/docs/components/scroll-area) - Augments native scroll functionality for custom, cross-browser styling. -- [Select](/primitives/docs/components/select) - Displays a list of options for the user to pick from, triggered by a button. -- [Separator](/primitives/docs/components/separator) - Visually or semantically separates content. -- [Slider](/primitives/docs/components/slider) - An input where the user selects a value from within a given range. -- [Switch](/primitives/docs/components/switch) - A control that allows the user to toggle between checked and not checked. -- [Tabs](/primitives/docs/components/tabs) - A set of layered sections of content, known as tab panels, that are displayed one at a time. -- [Toast](/primitives/docs/components/toast) - A succinct message that is displayed temporarily. -- [Toggle](/primitives/docs/components/toggle) - A two-state button that can be either on or off. -- [Toggle Group](/primitives/docs/components/toggle-group) - A set of two-state buttons that can be toggled on or off. -- [Toolbar](/primitives/docs/components/toolbar) - A container for grouping a set of controls, such as buttons, toggle groups or dropdown menus. -- [Tooltip](/primitives/docs/components/tooltip) - A popup that displays information related to an element when the element receives keyboard focus or the mouse hovers over it. diff --git a/data/themes/docs/components/index.mdx b/data/themes/docs/components/index.mdx deleted file mode 100644 index 3b479e1d..00000000 --- a/data/themes/docs/components/index.mdx +++ /dev/null @@ -1,81 +0,0 @@ ---- -metaTitle: Components -metaDescription: A comprehensive library of React components for building beautiful, accessible user interfaces. -sourcePath: components ---- - -# Components - -Radix Themes provides a comprehensive set of pre-styled, accessible React components that work out of the box. All components support theming, dark mode, and are built with accessibility in mind. - -## Available Components - -### Layout -- [Box](/themes/docs/components/box) - A fundamental layout building block. -- [Flex](/themes/docs/components/flex) - A flexbox container for arranging elements. -- [Grid](/themes/docs/components/grid) - A grid container for two-dimensional layouts. -- [Container](/themes/docs/components/container) - Constrains content to a maximum width. -- [Section](/themes/docs/components/section) - A semantic section with consistent spacing. - -### Typography -- [Text](/themes/docs/components/text) - Renders text with consistent styling. -- [Heading](/themes/docs/components/heading) - Semantic heading elements with styling. -- [Blockquote](/themes/docs/components/blockquote) - A styled blockquote element. -- [Code](/themes/docs/components/code) - Inline code styling. -- [Em](/themes/docs/components/em) - Emphasized text. -- [Kbd](/themes/docs/components/kbd) - Keyboard input styling. -- [Link](/themes/docs/components/link) - Styled anchor element. -- [Quote](/themes/docs/components/quote) - An inline quote element. -- [Strong](/themes/docs/components/strong) - Strong importance text. - -### Forms -- [Button](/themes/docs/components/button) - Trigger an action or event. -- [Checkbox](/themes/docs/components/checkbox) - A control to toggle between checked states. -- [Checkbox Cards](/themes/docs/components/checkbox-cards) - Checkbox presented as selectable cards. -- [Checkbox Group](/themes/docs/components/checkbox-group) - A group of related checkboxes. -- [Icon Button](/themes/docs/components/icon-button) - A button containing only an icon. -- [Radio](/themes/docs/components/radio) - A single radio button. -- [Radio Cards](/themes/docs/components/radio-cards) - Radio buttons presented as selectable cards. -- [Radio Group](/themes/docs/components/radio-group) - A set of radio buttons. -- [Segmented Control](/themes/docs/components/segmented-control) - A set of mutually exclusive buttons. -- [Select](/themes/docs/components/select) - A dropdown list of options. -- [Slider](/themes/docs/components/slider) - An input for selecting a value from a range. -- [Switch](/themes/docs/components/switch) - A toggle control for binary options. -- [Text Area](/themes/docs/components/text-area) - A multi-line text input. -- [Text Field](/themes/docs/components/text-field) - A single-line text input. - -### Display -- [Avatar](/themes/docs/components/avatar) - An image element with fallback for representing users. -- [Badge](/themes/docs/components/badge) - A small status indicator. -- [Callout](/themes/docs/components/callout) - A highlighted message or tip. -- [Card](/themes/docs/components/card) - A container for related content. -- [Data List](/themes/docs/components/data-list) - A list of key-value pairs. -- [Inset](/themes/docs/components/inset) - Allows content to span the full width of its container. -- [Separator](/themes/docs/components/separator) - Visually separates content. -- [Skeleton](/themes/docs/components/skeleton) - A placeholder for loading content. -- [Spinner](/themes/docs/components/spinner) - A loading indicator. -- [Table](/themes/docs/components/table) - A structured data table. - -### Overlays -- [Alert Dialog](/themes/docs/components/alert-dialog) - A modal dialog for important content. -- [Context Menu](/themes/docs/components/context-menu) - A menu triggered by right-click. -- [Dialog](/themes/docs/components/dialog) - A window overlaid on the primary window. -- [Dropdown Menu](/themes/docs/components/dropdown-menu) - A menu triggered by a button. -- [Hover Card](/themes/docs/components/hover-card) - Preview content behind a link. -- [Popover](/themes/docs/components/popover) - Rich content in a portal. -- [Tooltip](/themes/docs/components/tooltip) - Information displayed on hover or focus. - -### Navigation -- [Tab Nav](/themes/docs/components/tab-nav) - Navigation styled as tabs. -- [Tabs](/themes/docs/components/tabs) - Layered sections of content. - -### Utilities -- [Accessible Icon](/themes/docs/components/accessible-icon) - Makes icons accessible to screen readers. -- [Aspect Ratio](/themes/docs/components/aspect-ratio) - Displays content within a desired ratio. -- [Portal](/themes/docs/components/portal) - Renders content in a different part of the DOM. -- [Progress](/themes/docs/components/progress) - Displays task completion progress. -- [Reset](/themes/docs/components/reset) - Removes default browser styles. -- [Scroll Area](/themes/docs/components/scroll-area) - Custom, cross-browser scrolling. -- [Slot](/themes/docs/components/slot) - Merges props onto its immediate child. -- [Theme](/themes/docs/components/theme) - Provides theming context. -- [Visually Hidden](/themes/docs/components/visually-hidden) - Hides content visually but keeps it accessible. diff --git a/pages/primitives/docs/components/index.tsx b/pages/primitives/docs/components/index.tsx index f0324317..df3bc423 100644 --- a/pages/primitives/docs/components/index.tsx +++ b/pages/primitives/docs/components/index.tsx @@ -1,19 +1,21 @@ import * as React from "react"; -import { getMDXComponent } from "mdx-bundler/client"; +import NextLink from "next/link"; +import { Heading, Text, Box, Flex, Link } from "@radix-ui/themes"; import { TitleAndMetaTags } from "@components/TitleAndMetaTags"; -import { MDXProvider, components } from "@components/MDXComponents"; -import { QuickNav } from "@components/QuickNav"; -import { getMdxBySlug } from "@utils/mdx"; +import { getAllFrontmatter } from "@utils/mdx"; import type { Frontmatter } from "types/frontmatter"; -type Doc = { - frontmatter: Frontmatter; - code: string; +type ComponentItem = { + slug: string; + title: string; + description: string; }; -export default function ComponentsIndex({ frontmatter, code }: Doc) { - const Component = React.useMemo(() => getMDXComponent(code), [code]); +type Props = { + components: ComponentItem[]; +}; +export default function ComponentsIndex({ components }: Props) { return ( <>
    @@ -21,24 +23,51 @@ export default function ComponentsIndex({ frontmatter, code }: Doc) {
    - - - + + Components + + + + + Unstyled, accessible UI primitives for building high-quality design + systems and web apps. + + - + + {components.map((component) => ( + + + + {component.title} + + + + {component.description} + + + ))} + ); } export async function getStaticProps() { - const { frontmatter, code } = await getMdxBySlug( - "primitives/docs/components/", - "index", - ); - return { props: { frontmatter, code } }; + const frontmatters = getAllFrontmatter("primitives/docs/components"); + + const components = frontmatters + .filter((fm) => !fm.slug.endsWith("/index")) + .map((fm) => ({ + slug: fm.slug, + title: fm.metaTitle, + description: fm.metaDescription, + })) + .sort((a, b) => a.title.localeCompare(b.title)); + + return { props: { components } }; } diff --git a/pages/themes/docs/components/index.tsx b/pages/themes/docs/components/index.tsx index da18e4fd..b8e74338 100644 --- a/pages/themes/docs/components/index.tsx +++ b/pages/themes/docs/components/index.tsx @@ -1,20 +1,21 @@ import * as React from "react"; -import { getMDXComponent } from "mdx-bundler/client"; +import NextLink from "next/link"; +import { Heading, Text, Box, Flex, Link } from "@radix-ui/themes"; import { TitleAndMetaTags } from "@components/TitleAndMetaTags"; -import { MDXProvider } from "@components/MDXComponents"; -import { ThemesMDXComponents } from "@components/ThemesMDXComponents"; -import { QuickNav } from "@components/QuickNav"; -import { getMdxBySlug } from "@utils/mdx"; +import { getAllFrontmatter } from "@utils/mdx"; import type { Frontmatter } from "types/frontmatter"; -type Doc = { - frontmatter: Frontmatter; - code: string; +type ComponentItem = { + slug: string; + title: string; + description: string; }; -export default function ComponentsIndex({ frontmatter, code }: Doc) { - const Component = React.useMemo(() => getMDXComponent(code), [code]); +type Props = { + components: ComponentItem[]; +}; +export default function ComponentsIndex({ components }: Props) { return ( <>
    @@ -22,24 +23,51 @@ export default function ComponentsIndex({ frontmatter, code }: Doc) {
    - - - + + Components + + + + + A comprehensive library of React components for building beautiful, + accessible user interfaces. + + - + + {components.map((component) => ( + + + + {component.title} + + + + {component.description} + + + ))} + ); } export async function getStaticProps() { - const { frontmatter, code } = await getMdxBySlug( - "themes/docs/components/", - "index", - ); - return { props: { frontmatter, code } }; + const frontmatters = getAllFrontmatter("themes/docs/components"); + + const components = frontmatters + .filter((fm) => !fm.slug.endsWith("/index")) + .map((fm) => ({ + slug: fm.slug, + title: fm.metaTitle, + description: fm.metaDescription, + })) + .sort((a, b) => a.title.localeCompare(b.title)); + + return { props: { components } }; } From 88d6564e5b5fad3095cb13913fd97e1ce59ce90c Mon Sep 17 00:00:00 2001 From: Lucas Motta Date: Wed, 4 Feb 2026 13:16:31 -0300 Subject: [PATCH 13/15] Use lists for components display --- pages/primitives/docs/components/index.tsx | 30 ++++++++++++---------- pages/themes/docs/components/index.tsx | 30 ++++++++++++---------- 2 files changed, 34 insertions(+), 26 deletions(-) diff --git a/pages/primitives/docs/components/index.tsx b/pages/primitives/docs/components/index.tsx index df3bc423..7d45cde2 100644 --- a/pages/primitives/docs/components/index.tsx +++ b/pages/primitives/docs/components/index.tsx @@ -39,19 +39,23 @@ export default function ComponentsIndex({ components }: Props) { - - {components.map((component) => ( - - - - {component.title} - - - - {component.description} - - - ))} + +
      + {components.map((component) => ( + +
    • + + + {component.title} + + + + {component.description} + +
    • +
      + ))} +
    ); diff --git a/pages/themes/docs/components/index.tsx b/pages/themes/docs/components/index.tsx index b8e74338..134221d2 100644 --- a/pages/themes/docs/components/index.tsx +++ b/pages/themes/docs/components/index.tsx @@ -39,19 +39,23 @@ export default function ComponentsIndex({ components }: Props) { - - {components.map((component) => ( - - - - {component.title} - - - - {component.description} - - - ))} + +
      + {components.map((component) => ( + +
    • + + + {component.title} + + + + {component.description} + +
    • +
      + ))} +
    ); From f7061aa4c70ac322847c0d4cebc285fea659adad Mon Sep 17 00:00:00 2001 From: Lucas Motta Date: Wed, 4 Feb 2026 15:53:39 -0300 Subject: [PATCH 14/15] PR feedback --- components/PropsTable.tsx | 42 +++++++++++++++++++-------------- pages/api/markdown/[...path].ts | 5 ---- 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/components/PropsTable.tsx b/components/PropsTable.tsx index ddf162f7..5f46e6cf 100644 --- a/components/PropsTable.tsx +++ b/components/PropsTable.tsx @@ -9,9 +9,9 @@ import { Flex, Inset, ScrollArea, - VisuallyHidden, } from "@radix-ui/themes"; import { InfoCircledIcon, DividerHorizontalIcon } from "@radix-ui/react-icons"; +import { AccessibleIcon } from "radix-ui"; export type PropDef = { name: string; @@ -73,11 +73,15 @@ export function PropsTable({ {description && ( - - - - ) : ( <> -