feat: add raw markdown endpoint and Copy as Markdown button#9
feat: add raw markdown endpoint and Copy as Markdown button#9RanaMoizHaider wants to merge 2 commits intovitodeploy:mainfrom
Conversation
✅ Deploy Preview for vitodeploy ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
There was a problem hiding this comment.
Pull request overview
Adds support for accessing docs as raw Markdown (.md) and introduces a “Copy as Markdown” control in the docs UI, with a build step to emit .md files into the static export output.
Changes:
- Add a build-time script to copy docs from
content/docs/**intoout/docs/**as.md(frontmatter removed). - Add a dev-only raw docs route and a rewrite so
/docs/...*.mdworks in development. - Add a “Copy as Markdown” button to the docs table of contents sidebar.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| scripts/copy-docs-md.ts | New build script to generate .md files into out/docs for static hosting. |
| package.json | Extends build to run the new docs markdown copy script. |
| next.config.mjs | Makes output: "export" prod-only, adds .dev.* routing extensions in dev, and introduces a .md rewrite. |
| components/table-of-contents.tsx | Adds Copy/Check UI and clipboard logic gated by rawContent. |
| app/docs/[...slug]/page.tsx | Passes raw doc content into the TOC so the copy button can copy it. |
| app/docs-raw/[...slug]/route.dev.ts | Adds a dev-only route to serve raw docs content with a Markdown content-type. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| if (headings.length === 0) return null | ||
|
|
There was a problem hiding this comment.
With the new rawContent prop, the “Copy as Markdown” button is part of this component’s output, but if (headings.length === 0) return null makes it impossible to show the button on pages without headings. Consider rendering the copy button even when there are no headings, and only conditionally render the headings list.
| return ( | ||
| <nav aria-label="Table of contents"> | ||
| {rawContent && ( | ||
| <button |
There was a problem hiding this comment.
This <button> doesn’t specify type. If this component is ever rendered inside a <form>, the default type="submit" can cause unintended submissions. Set type="button" to make the click behavior unambiguous.
| <button | |
| <button | |
| type="button" |
| {/* Table of Contents - right sidebar */} | ||
| {headings.length > 0 && ( | ||
| <aside className="sticky top-14 hidden h-[calc(100vh-3.5rem)] w-56 shrink-0 overflow-y-auto py-8 pr-4 xl:block"> | ||
| <TableOfContents headings={headings} /> | ||
| <TableOfContents headings={headings} rawContent={doc.content} /> | ||
| </aside> | ||
| )} |
There was a problem hiding this comment.
TableOfContents is only rendered when headings.length > 0, but the new “Copy as Markdown” UI lives inside that component. This means docs pages without headings can’t show the copy button even though doc.content is available. If the copy feature should work for all docs, render the sidebar (or at least the copy control) regardless of headings length.
| {headings.length > 0 && ( | ||
| <aside className="sticky top-14 hidden h-[calc(100vh-3.5rem)] w-56 shrink-0 overflow-y-auto py-8 pr-4 xl:block"> | ||
| <TableOfContents headings={headings} /> | ||
| <TableOfContents headings={headings} rawContent={doc.content} /> | ||
| </aside> |
There was a problem hiding this comment.
Passing the full doc.content string into a client component means the entire raw doc source is serialized into the RSC payload and sent to the browser on every docs page view (even if the user never clicks copy). To avoid a potentially large page payload, consider fetching the raw markdown on-demand from the new *.md endpoint when the button is clicked instead of embedding it in props.
| import fs from "fs" | ||
| import path from "path" | ||
| import matter from "gray-matter" | ||
| import { VERSIONS, DEFAULT_VERSION, type Version } from "../lib/docs-config" |
There was a problem hiding this comment.
type Version is imported but never used in this script. This will typically fail lint/typecheck with unused-import rules; remove it (or use it) to keep the script clean.
| import { VERSIONS, DEFAULT_VERSION, type Version } from "../lib/docs-config" | |
| import { VERSIONS, DEFAULT_VERSION } from "../lib/docs-config" |
| fs.writeFileSync(outPath, content.trim() + "\n") | ||
| count++ |
There was a problem hiding this comment.
Writing content.trim() will remove leading whitespace/newlines from the document body, which can change Markdown semantics (e.g., leading-indented code blocks) and produce output that doesn't match the source. Prefer preserving the content as-is and only normalize the trailing newline (e.g., ensure it ends with exactly one \n).
| await navigator.clipboard.writeText(rawContent) | ||
| setCopied(true) | ||
| setTimeout(() => setCopied(false), 2000) |
There was a problem hiding this comment.
navigator.clipboard.writeText(...) can throw/reject (permission denied, insecure context, etc.). Right now that would result in an unhandled rejection and copied state never resetting. Wrap this in try/catch (and optionally surface a non-intrusive error state) so the UI doesn’t silently fail.
| await navigator.clipboard.writeText(rawContent) | |
| setCopied(true) | |
| setTimeout(() => setCopied(false), 2000) | |
| if (!navigator?.clipboard?.writeText) { | |
| console.error("Clipboard API is not available in this environment.") | |
| return | |
| } | |
| try { | |
| await navigator.clipboard.writeText(rawContent) | |
| setCopied(true) | |
| setTimeout(() => setCopied(false), 2000) | |
| } catch (error) { | |
| console.error("Failed to copy markdown to clipboard:", error) | |
| } |
| setCopied(true) | ||
| setTimeout(() => setCopied(false), 2000) | ||
| } |
There was a problem hiding this comment.
The setTimeout started after copying is never cleared. If the user navigates away within 2s, this can trigger a state update on an unmounted component. Store the timeout id (e.g., in a ref) and clear it in an effect cleanup (and/or before setting a new timeout).
No description provided.