Markdown Export

Purpose and Scope

Markdown export gives readers and downstream tools a plain-text version of an OpenWiki page without requiring them to scrape the rendered wiki UI. OpenWiki stores each generated wiki page as markdown, renders that markdown into the public page, and also exposes the same page body as text/markdown when the markdown route can identify a repository and page. This matters for users who want to copy a source-grounded page into an issue, send it to another agent, archive it with a repository snapshot, or inspect the generated citations in their original markdown form.

The export path is intentionally small: it does not regenerate content, reinterpret citations, or rebuild the wiki outline. Instead, it resolves an owner, repository name, and optional wiki slug, loads the already-published wiki artifact through storage, and returns wiki.currentPage.markdown as the response body. The UI copy affordance follows the same model. It receives the page markdown as a prop and writes that exact string to the clipboard, so the exported representation remains aligned with what the wiki renderer uses for display.

Sources: app/api/markdown/route.ts, app/components/copy-markdown-button.tsx, app/components/wiki-markdown.tsx

Relevant Source Files

  • app/api/markdown/route.ts — Implements the markdown GET handler, resolves repository and slug inputs, loads the current wiki page, maps storage failures to HTTP responses, and returns text/markdown with cache and filename headers.
  • app/lib/markdown-route.ts — Defines the MarkdownRoute shape and parses pathname-based markdown routes such as owner/repository and owner/repository/page slug forms that end in .md.
  • app/components/copy-markdown-button.tsx — Provides the client-side copy control, including clipboard API usage, fallback copy behavior, and the temporary Copied! status message.
  • app/components/wiki-markdown.tsx — Renders the same stored markdown for the web UI using react-markdown, GitHub-flavored markdown, syntax highlighting, safe link/image transforms, heading IDs, and HTML skipping.

Route Resolution and HTTP Behavior

The API route is marked force-dynamic, which keeps markdown export tied to the current stored artifact instead of relying on static route generation. On every GET, the handler builds a URL from the request, attempts to parse a markdown-style pathname, and then lets explicit query parameters override or supply the same values. The required inputs are owner and repo; slug is optional and determines whether the route asks storage for a specific wiki page or lets storage select the repository’s current page behavior.

Input handling is deliberately forgiving but bounded. Query string values are trimmed, and pathname segments are decoded through the route parser. If neither the query string nor the parsed route supplies both owner and repository, the handler returns a 400 response with Missing owner or repository.. This makes the failure mode clear for clients building export links, while avoiding an unnecessary storage lookup when the route is not specific enough to identify a repository wiki.

Sources: app/api/markdown/route.ts, app/lib/markdown-route.ts

The route parser recognizes only paths ending in .md. With two decoded path segments, it interprets the first segment as owner and the second as the repository name after stripping the .md suffix. With three decoded path segments, it interprets them as owner, repo, and a slug after stripping .md from the final segment. Any other segment count, empty owner, empty repository, or empty slug returns null, leaving the API handler to rely on query parameters instead.

After input resolution, the handler calls getRepositoryWiki({ name: repo, owner, slug }). Storage configuration errors are converted to 503 with the shared storage configuration message, and unavailable artifacts are also returned as 503 with the artifact error message. A missing wiki or missing currentPage becomes 404 with Markdown page not found.. Other unexpected errors are rethrown, preserving normal framework error handling rather than hiding operational defects behind a generic export response.

Export Contract Reference

The markdown response body is exactly the stored wiki.currentPage.markdown string. The handler sets content-type to text/markdown; charset=utf-8, which lets browsers, command-line clients, and agents treat the payload as markdown rather than HTML. It also sets cache-control to public, max-age=0, must-revalidate, indicating that the response may be cached but should be revalidated before reuse. That matches OpenWiki’s living-documentation model, where the same repository route may be refreshed as upstream source changes.

The content-disposition header is inline, so browsers can display the markdown directly instead of forcing a download. The filename is still supplied for save-as workflows. Filename generation concatenates owner, repository, and the resolved current page slug, replaces characters outside letters, digits, dot, underscore, and hyphen with hyphens, trims leading or trailing hyphens, and appends .md. The slug used for the filename comes from wiki.currentPage.slug, not necessarily the raw request slug, so exported filenames follow the stored page identity.

Sources: app/api/markdown/route.ts

Compact API contract:

SurfaceContractBehavior
GET in app/api/markdown/route.tsReads owner, repo, and optional slug from query parameters or a parsed markdown pathnameReturns stored page markdown or an HTTP error response
parseMarkdownRoute(pathname)Accepts a pathname ending in .mdReturns { owner, repo }, { owner, repo, slug }, or null
getMarkdownFilename(input)Accepts owner, repository, and slug stringsProduces a sanitized .md filename for content-disposition
CopyMarkdownButton({ markdown })Accepts a markdown string propCopies the exact string to the user clipboard
WikiMarkdown({ markdown, owner, repoName, commitSha })Accepts stored markdown plus repository contextRenders markdown safely for the wiki page UI

Example request shapes supported by the handler’s input model include query-driven calls such as /api/markdown?owner=vercel&repo=next.js&slug=overview. When a request is routed to the same handler with a markdown pathname, the parser can also understand two-segment and three-segment .md forms such as /vercel/next.js.md or /vercel/next.js/overview.md. In both cases, the final response depends on the stored wiki artifact for the resolved repository and page.

Copy UI and Clipboard Flow

The copy button is a client component because it uses browser clipboard APIs and local React state. It accepts one prop, markdown, and does not fetch content itself. When clicked, it calls an internal copyMarkdown function, awaits clipboard writing, sets copied to true, and schedules the state to reset after 1600 milliseconds. The rendered label is announced through an aria-live span, changing from Copy markdown to Copied!, which gives assistive technologies a clear status update after the action completes.

Sources: app/components/copy-markdown-button.tsx

Clipboard writing first uses navigator.clipboard.writeText when available. That is the modern browser path and preserves the markdown string without constructing a visible DOM control. If that API is unavailable, the component creates a hidden read-only textarea, inserts the markdown, selects it, and invokes document.execCommand("copy"). The textarea is removed in a finally block, and a failed legacy copy command throws an error instead of silently reporting success. A cleanup effect clears any pending timeout when the component unmounts.

This design keeps markdown export user-driven in the UI. The button does not know about repository ownership, slugs, filenames, storage, or routing. Those responsibilities stay in the route and page data-loading layers. As a result, the same component can be reused anywhere a rendered page already has access to the markdown string, and tests or future UI placements only need to verify that the correct markdown prop is passed in.

Relationship to Rendered Wiki Markdown

The exported markdown and the rendered wiki page share the same source text, but they are consumed differently. WikiMarkdown turns the markdown into React elements with MarkdownAsync, enables GitHub-flavored markdown through remark-gfm, and applies syntax highlighting through rehype-pretty-code using GitHub light and dark themes. It also wraps the output in the shared markdown class name so generated pages have consistent typography and spacing across the wiki experience.

Sources: app/components/wiki-markdown.tsx

Rendering adds safety and navigation behavior that is not part of the raw export. Links are transformed before being rendered; unsafe or unsupported links can be dropped by returning only their children. External HTTP(S) links receive target="_blank" and rel="noreferrer". Headings at levels two and three get deterministic IDs derived from their text, with counters for duplicate headings. Images are restricted: HTTPS images are allowed only from raw.githubusercontent.com, HTTP and data URLs are rejected, and relative paths are normalized and validated before rendering.

The renderer also sets skipHtml: true, which means embedded raw HTML in the markdown is not rendered into the page. That is a UI safety decision, not an export mutation. The markdown route still serves the stored markdown body as text, while the renderer decides how to display it safely in a browser. Developers adding new markdown features should therefore consider both surfaces: whether the raw markdown contract should remain unchanged, and whether the rendered UI needs additional transforms or sanitization.

Implementation Notes and Next Steps

When adding a new export link or copy control, pass the stored page markdown directly rather than reconstructing markdown from rendered DOM. DOM-to-markdown conversion would lose source-grounded details, citation formatting, and code fences that are already present in the generated artifact. Prefer the route for programmatic access and the copy button for interactive access, because each path already handles its own operational concerns: HTTP errors and response headers in the route, and clipboard compatibility plus status feedback in the client component.

If you change route parsing, keep the query-parameter fallback in mind. Existing clients can call the API by supplying owner, repo, and optional slug, while pathname parsing supports cleaner markdown-style URLs when routing reaches the same handler. If you change rendering behavior, verify that the stored markdown remains valid for export and that the UI still rejects unsafe links, unsafe images, and raw HTML. Related pages to read next are wiki-routing-and-pages for public page routes and storage-setup for the artifact layer that provides currentPage.markdown.