Skip to content

Add Copy as Markdown actions to each page - #70

Merged
xhan97 merged 1 commit into
mainfrom
docs/copy-page-button
Aug 15, 2026
Merged

Add Copy as Markdown actions to each page#70
xhan97 merged 1 commit into
mainfrom
docs/copy-page-button

Conversation

@xhan97

@xhan97 xhan97 commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator

Builds the equivalent of what camoufox.com offers, on top of the .md files from #69.

What camoufox actually does

It runs Retype, which renders a <doc-page-actions> component and exposes each page as Markdown:

<link rel="alternate" type="text/markdown" href="https://camoufox.com/python/usage.md">

Material has no equivalent. squidfunk/mkdocs-material#8382 asked for exactly this and was closed as a duplicate in August 2025, with the maintainer putting it on the backlog pending the end of feature freeze. So it is built here.

The feature

Two controls at the top right of every page:

  • Copy as Markdown — fetches the page's .md and writes it to the clipboard, with the label switching to "Copied" for two seconds
  • View Markdown — opens the raw file

#69 supplies the files; this makes them reachable without knowing the URL scheme.

Design notes

The URL is derived in JavaScript, not the template. use_directory_urls is false, so pages are page.html and their Markdown is page.md — only the extension differs. Deriving it at runtime keeps the template free of path logic.

The controls start hidden. A HEAD request confirms the file exists before they are revealed, so they never appear on a page the plugin does not cover. Every page currently has one, so this is defensive — but it means adding a page outside the llmstxt sections degrades quietly instead of shipping a broken button.

Binding goes through Material's document$ observable, with a DOMContentLoaded fallback. navigation.instant is not enabled today; if it is enabled later, this keeps working rather than silently binding once.

Verification

Built the site and drove it in headless Chrome against a local server.

On a normal page the hidden attribute is dropped and the link resolves:

<div class="ik-page-actions" data-ik-page-actions="">
<a class="ik-page-actions__button" data-ik-view="" target="_blank" rel="noopener"
   href="/api/anomaly/inne.md">

With the .md removed from the built site, the controls stay hidden:

<div class="ik-page-actions" data-ik-page-actions="" hidden="">

URL derivation checked across page shapes — API, notebook, quick-start, releases, and the site root all resolve to a file served with HTTP 200. The button renders on all 26 content pages; Material's 404.html does not use this template block, so it is excluded naturally.

One thing not verified end to end: the clipboard write itself. navigator.clipboard needs a secure context and a permission grant that headless Chrome does not give by default. The fetch path either side of it is confirmed, and the failure branch sets a "Copy failed" label rather than doing nothing.

Adjacent fix

custom_dir: docs/overrides points inside docs/, so mkdocs was also copying the theme template into the site as a page — visitors hitting /overrides/main.html got raw Jinja. This predates the change; exclude_docs: overrides/ stops it, and it is fixed here because it is the same file.

Possible follow-up

Sites like Anthropic's and Mintlify's add "Open in ChatGPT" / "Open in Claude" entries alongside the copy button, which deep-link to those services with the page URL prefilled. Left out — it adds external service links, which is a product decision rather than a technical one.

The llmstxt plugin already writes a .md counterpart for every page, but
reaching it meant knowing the URL. This surfaces it: a "Copy as Markdown"
button that fetches the file and puts it on the clipboard, and a "View
Markdown" link beside it. Retype ships this as doc-page-actions; Material
does not, and the request for it (squidfunk/mkdocs-material#8382) sits on
their backlog, so it is built here.

The target URL is derived in JavaScript from the current path rather than
rendered into the template, since use_directory_urls is false and it is
only the extension that differs.

The controls start hidden and are revealed only after a HEAD request
confirms the file exists, so they never appear on a page the plugin does
not cover. Verified by removing a .md from the built site and loading the
page in headless Chrome: the container keeps its hidden attribute. On a
normal page the attribute is dropped and the link resolves correctly.

Binding goes through Material's document$ observable, with a
DOMContentLoaded fallback, so enabling navigation.instant later will not
silently break it.

Also excludes overrides/ from the docs collection. custom_dir points
inside docs/, so mkdocs was copying the theme template into the site as a
page and serving raw Jinja at /overrides/main.html. That predates this
change; it is fixed here because it is the same file.
@xhan97
xhan97 merged commit abb4c1e into main Aug 15, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant