I’ve been experimenting with llms.txt for Dash apps and wanted to share the approach, because I think the common pattern — a static text file that unloads your entire codebase to any scraper — misses most of what’s possible.
Full walkthrough (architecture, the gate concept, and the CI/CD side for the new components) is here:
Here’s the thing that started it: a rendered Dash layout is effectively invisible to an agent fetching the page. All that beautiful interactivity arrives as JavaScript the agent never executes. Most people treat that as a bug. I started treating it as a design feature: humans and agents want different documents anyway, so serve each one the version built for them.
The core idea: one URL, two honest renders
Every docs page on my sites serves a machine-readable twin at /<page>/llms.txt, and the same URL content-negotiates:
- An agent (or anything sending
Accept: text/markdown) gets clean markdown — the page’s prose plus the complete example source code, directives expanded, nothing truncated. - A browser gets a rendered viewer of that same markdown, with navigation, a network directory, and an embedded Claude instance — so the “machine view” is a first-class page, not a dead
.txt.
Try it yourself in ten seconds:
# what an agent sees
curl -H "Accept: text/markdown" https://leaflet.2plot.dev/llms.txt
# what you see — same URL in a browser
https://leaflet.2plot.dev/llms.txt
And the site-level index at 2plot.dev/llms.txt maps every page plus the rest of the network, so an agent can walk the whole thing from one entry point.
It’s a network, not a site
The layer above is running across a federated set of docs apps — one hub, one satellite per component library, each on its own subdomain, all publishing the same llms.txt contract and cross-linking each other in a shared directory. An agent that lands on any one of them can discover all of them.
That network is also where the component news is. Everything below is live right now:
| Library | What it is | Docs | PyPI |
|---|---|---|---|
| dash-pannellum 2.0 | 360° panoramas, virtual tours, multi-res tiles and 360° video (WebGL, no plugins) | pannellum.2plot.dev | pip install dash-pannellum |
| flexlayout-dash 2.0 | Docking window/tab layouts (drag, split, pop-out) | flexlayout.2plot.dev | pip install flexlayout-dash |
| dash-mui-charts | 13 MUI X chart & tree components — Line, Bar, Candlestick, Pie, Scatter, Composite, Heatmap, Sparkline, LiveTrading, TreeView ×3, TimeClock | muicharts.2plot.dev | pip install dash-mui-charts |
| dash-mui-scheduler | MUI X Scheduler — calendars, resource timelines, event scheduling | muischeduler.2plot.dev | pip install dash-mui-scheduler |
| dash-flows | Node-graph editors built on React Flow | flows.2plot.dev | pip install dash-flows |
| dash-emoji-mart | Slack-style emoji picker | emojimart.2plot.dev | pip install dash-emoji-mart |
| dash-email | Compose emails as Dash components, send via Resend | email.2plot.dev | pip install dash-email |
| dash-leaflet2 | Leaflet 2 maps | leaflet.2plot.dev | docs preview — PyPI soon |
| dash-improve-my-llms | The package powering this whole llms.txt layer | llms.2plot.dev | pip install dash-improve-my-llms |
The full catalogue (10 maintained libraries 8 waiting to migrate) lives at 2plot.dev/pip, and the whole docs stack is a reusable template — boilerplate.2plot.dev is the actual boilerplate app, deployed and eating its own dog food.
The parts I think are actually interesting
Docs that update themselves. The docs are dynamic and driven by the same apps that manage the components. The hub publishes a bulletin feed, and every satellite folds it into its llms.txt surfaces within 15 minutes — so when a dependency pin changes or a breaking release lands, the alert goes into the document agents read, instead of the llms.txt quietly going stale.
An authentication gate as a feature, not a wall. Pages have visibility tiers. Public tiers serve everyone. Deeper tiers serve a gate document to anonymous agents — but a signed-in user’s AI assistant can present a short-lived agent key (a signed URL parameter, capped at 30 days, revoked on logout) and get the full prose. Why a key and not a cookie? Because your assistant fetches without your cookies — a URL is the only credential that survives the copy-paste into an AI tool. The keys authorize documents only, never pages or mutations, and satellites hold no key material — they verify against the hub.
Honest bot taxonomy. Search and answer-engine crawlers (including OAI-SearchBot and Claude’s fetchers) get the markdown; scrapers that ignore robots.txt get nothing useful, because the interactive layer was never in the HTML to begin with.
The ops side. Every satellite ships on the same standard: secretless CI (the suite proves the app fails closed with zero env), a container-boot gate (build the real image, boot it, curl /healthz — because green tests from a working tree prove nothing about the image), a post-deploy battery against the live domain, and tag-driven PyPI publishing via OIDC trusted publishing, so there’s no API token stored anywhere.
Where this fits
This pairs with Plotly’s own work extending the MCP/callback story for agentic access — Dash 4.3+ ships an MCP server, and these docs apps expose their pages as MCP resources too. The llms.txt layer felt like the most neglected piece of that story: MCP answers “how does an agent call my app,” this answers “how does an agent read my docs.”
Happy to answer questions on any of it — the markdown/JS dual-render setup, the agent-key design, or any of the components. If you want this for your own component library, start from the boilerplate above; it’s the exact template every satellite in the table was built from.

