LaralCN-UI docs for agents: llms.txt and a Copy Page button
Pasting a documentation page into Claude Code means pasting a sidebar, a theme toggle and a footer. The docs site now serves a second channel: an /llms.txt index and a .md twin of every component page, generated from the same registry the HTML reads. Plus the route that quietly ate its own suffix.
I work on other Laravel projects with Claude Code open, and I kept doing the same clumsy thing: opening a LaralCN-UI component page, selecting the source, and pasting it into the agent along with whatever markup came with it. What the agent got was a sidebar, a theme toggle, a breadcrumb and a footer wrapped around forty lines of Blade.
The docs site now serves a second channel. Every page has a Markdown twin, and there is an /llms.txt index that points at all of them.
https://laralcn-ui.abdulkadersafi.com/llms.txt
https://laralcn-ui.abdulkadersafi.com/components.md
https://laralcn-ui.abdulkadersafi.com/components/button.md
https://laralcn-ui.abdulkadersafi.com/blocks/navbar-01.md
https://laralcn-ui.abdulkadersafi.com/theming.md
https://laralcn-ui.abdulkadersafi.com/llms-full.txt
The header of every component, block and index page has a Copy Page button next to the theme switch. It fetches that page's .md and puts it on your clipboard.
One generator, off the registry
The site already had a rule it has followed since the first release: it never reimplements a component. Registry reads registry/ off disk, and the Blade pages render the real file and print the real source, so what you copy from the site is what php artisan ui:add writes into your project.
The Markdown channel is a second reader on that same class, not a second copy of the docs. DocsMarkdown takes the registry entry and emits frontmatter, the install command, the dependency list, the usage snippet, the demo source, the full contents of every file, and the theme tokens the component reads. Nothing is transcribed by hand, so nothing can go stale.
A component page comes out like this:
---
title: "button"
description: "Clickable button with variant and size options."
category: "Forms"
---
# button
## Installation
php artisan ui:add button
## Dependencies
- composer: `gehrisandro/tailwind-merge-laravel`
## Usage / Example / Source / Theme tokens
The frontmatter values go through json_encode rather than being wrapped in quotes by hand. Several component descriptions contain a colon, and a bare colon in an unquoted YAML value is a parse error waiting for the one component that trips it.
There is no props table, which was a deliberate omission rather than a shortcut. The full Blade file is already in the page, and the first thing in every file is its @props block with real defaults. A generated table would be a second, lossier statement of something the source states exactly.
The route that ate its own suffix
/components/{name} was already a route. Adding /components/{name}.md produced a page that resolved fine and then, on some requests, did not.
Laravel compiles both to a regex where {name} is [^/]+, and [^/]+ will happily match button.md. Two routes match the same URL, and the first one registered wins. Registered in the obvious order, the HTML route claims /components/button.md and looks up a component literally named button.md, which does not exist.
The fix is one line of ordering, with the reason written down next to it:
// Markdown channel. These come before the HTML routes they shadow, otherwise
// `/components/{name}` swallows `button.md` as a component name.
Route::get("/components/{name}.md", [DocsController::class, "showMarkdown"]);
With the .md route first, the greedy [^/]+ matches button.md, fails the literal \.md that follows, backtracks to button, and the suffix lands where it belongs. The only thing this makes unrepresentable is a component whose name ends in .md.
Two smaller decisions
Serve it as text/plain. Marking the response text/markdown is the honest content type and the wrong one, because browsers download it instead of showing it. The .md link next to the Copy Page button is there so you can read the thing, so it goes out as plain text.
Fail loudly, in a useful direction. The Copy Page handler fetches the twin and writes it to the clipboard, and both of those can be refused. Rather than swallow the error and leave a button that appears to do nothing, the catch opens the Markdown in a new tab, which is what you wanted anyway:
try {
const response = await fetch(button.dataset.copyUrl);
if (!response.ok) throw new Error(response.status);
await navigator.clipboard.writeText(await response.text());
button.dataset.copied = 'true';
setTimeout(() => delete button.dataset.copied, 1500);
} catch {
window.open(button.dataset.copyUrl, '_blank', 'noopener');
}
The button reuses the data-copied convention the code blocks already use, so the label swap to "Copied!" is CSS that was on the page before I got there: group-data-[copied]:hidden.
Wiring it into every page was one attribute, because an anonymous Blade component receives its attributes as variables. <x-layouts.app :md-url="route('docs.show.md', $entry['name'])"> becomes $mdUrl in the layout, and the header renders the button only when it is set. Pages with no twin do not grow a button that lies.
What llms.txt actually says
The index is not just a list of links. Reading source teaches an agent what a component looks like, not how the system expects to be used, so the file states the five rules up front:
- Components are anonymous Blade components under
resources/views/components/ui/, used as<x-ui.button>, with multi-file components namespaced as<x-ui.pagination.item>. - Variants and sizes are always the props
variantandsize, resolved with an inlinematch()in the file itself. - Anything you pass in
classwins, because every component merges base, variant and consumer classes throughTailwindMerge. - The only external symbol any component file uses is
TailwindMerge, so one copied file works standalone. - Colours come from theme tokens. Tailwind v4, CSS-first, no
tailwind.config.js.
Then 35 components and 7 blocks, each linked to its .md with its description.
/llms-full.txt is the whole thing concatenated, source included, at 246KB. It exists because the shape of the request is usually "here is the library, now build me a settings page", and that is one paste.
The two guide pages, Getting Started and Plain Blade, have no Markdown twin. They are prose I wrote rather than data the registry holds, so a twin would be a second copy that drifts from the first. What they teach is in llms.txt directly instead.
Where it is
35 components and 7 blocks, Tailwind v4 only, MIT. Docs at laralcn-ui.abdulkadersafi.com, code on GitHub.
If you use LaralCN-UI with an agent, /llms.txt is the file to hand it.
Building scalable systems and developer-first tools. Lead Software Engineer at DSRPT.
Frequently asked
-
A plain Markdown file at the root of a site that tells a language model what the site is and where its content lives, in the way robots.txt tells a crawler what it may fetch. It is a convention rather than a standard: a short description of the project, then a linked list of pages. For a component library the useful part is that each link points at a Markdown version of the page rather than the HTML, so an agent reading it gets source and prose without the site chrome around them.
-
Because text/markdown makes browsers download the file rather than display it. The .md link sits next to a Copy Page button so a human can read the page's Markdown before handing it to an agent, and a download interrupts that. Agents fetching the URL do not care either way, so text/plain is the content type that serves both.
-
Laravel compiles a route parameter to [^/]+ by default, and that pattern matches button.md as happily as it matches button. Both routes match the same URL, and Laravel dispatches to whichever was registered first. With the HTML route first it looked up a component literally named button.md and 404'd. Registering the .md route first fixes it: the greedy match takes button.md, fails the literal .md that follows, and backtracks to button.
-
It cannot, because it is not a separate copy. The Markdown generator reads the same Registry class the HTML docs read, which reads the registry directory the CLI installs from. A component page's source section is the file contents, fetched at request time. If a component changes, both the HTML page and the .md change with it and nobody has to remember to update a doc.