All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Sync item 18 — the 1.6.41 remainder, nineteen files as one contract.
.. exec::reached only the browser. A markdown2dash directive that renders Dash components puts its output in the React tree alone, while the machine lane, the prerender and the crawler HTML are built from the markdown SOURCE with the directive line stripped. Four pages published prose that referred to a component the reader could not see —/showcase/robots-sandbox/llms.txtread "Move the switches. The document on the right is generated by…" and then stopped. The directive now expands into the module's source through the same fence-aware pass.. source::uses: one parse, two consumers.- The battery probed a path that does not exist and never probed one
that does.
network_smoke.HIDDEN_DOC_PATHSlisted/admin/llms.txt(a redirect, not a page) and omitted/admin/traffic/llms.txt, added with the ledger round. It is now pinned against the page registry. - A bare test client sends
Werkzeug/x.y, which dimll ≥2.8 puts on the crawler lane, somark_hiddenpages 404 and every-page-200 loops go red at a floor bump.tests/test_agent_key_route.pynow names the browser lane and keeps the internal token.
- A skip link as the first tab stop on every page, and prop tables that scroll inside their own box.
nav:frontmatter for short sidebar labels (no page needs one here — the longest name is 18 characters)./changelogcarries a real<lastmod>, derived from the newest dated release heading. The sitemap honesty pin learns that second source rather than being loosened.tests/test_exec_lane_parity.py— content pins, derived from the docs tree, that MUTATION-CHECK themselves. The expansion was disabled and every content pin confirmed red before being restored.- CD fails fast when superseded (a later commit already serving, via
the compare API) instead of timing out, and refuses to push when a
release/*branch would makereleasean unusable ref. GITHUB_URLmust resolve on the wire — a wrong repo slug is a live 404 that no amount of "profile vs repo" framing catches.
header.pyandtest_nav_contract.pyare byte-copies again. Both seams this fork reported at 1.6.38 as template-side were closed at 1.6.41 (LOGO_ASSETet al., and a registry-derived aside pin), soDIVERGENCES.md8 is retired. The byte-identity evidence recorded against 519d496 was stale and is superseded by a re-measurement against 4ac02e0./apiremains unregistered —dash_improve_my_llmsships no component metadata, so all four empty-/apimechanisms are moot here.
Sync items 16 and 17 — the navigation contract and the battery's lane.
/changelog— this file as a Timeline, linked under Home in the sidebar and from the footer, and served as its ownllms.txt.- A footer — © Pip Install Python LLC, the owner's GitHub profile, Discord and YouTube, every icon labelled.
- An Admin section in the sidebar, visible to the OWNER. Both admin
pages (
/admin/control-board,/admin/traffic) were previously hidden from everyone including the owner by a blanket/admin/*path filter; they are now listed foris_admin_user()and for nobody else — the anonymous tree carries no/admin/href at all. - An
Other Appsmenu in the top bar, built fromnetwork_directory.PRIMARYrather than a hand-typed list. lib/aside.py— the aside column collapses on pages with no.. toc::, so/changelogand/render full width.
- The sidebar is generated from frontmatter, not from three
hand-typed link clusters in
components/navbar.py. Every page already declaredcategory:; the navbar simply ignored it. Each page now also declaresorder:./audiences/web-crawlersis listed once — it appeared twice, as "Web Crawlers" under This package and as "A · What the crawler sees" under Showcase. - The sidebar's
Pip ComponentsandOther Apps I've builtsections are gone — the network is listed once, in the top bar's menu.Resourcesis third-party only:dmc. Nocommunity.plotly.com, no2plot.dev. pages/home.pyrenders through markdown2dash, notdcc.Markdown, so home's headings, tables and code fences match every docs page./admin/trafficusesdmc.DatePickerInputinstead ofdcc.Dropdown, and gains a People section — the day's human hits, visitors, sessions and median session — above the crawler tables, with the line that humans never enter the read ledger.- Mobile fit: code blocks inside a List item, Blockquote or Timeline
scroll in their own box instead of widening the document; the rules are
in
main.cssagainst public Mantine class names, never per page. /apiis NOT registered here, and that is a measurement.dash_improve_my_llmsships no component metadata — nometadata.json, no bundled JS, no generated component classes (checked on 2.8.0). It is a library of routes, middleware and config objects, so there are no props to tabulate.API_PACKAGESis[].- The live battery's default User-Agent names the browser lane
(item 17). At dimll ≥2.8 a UA with no browser engine token is
crawler-lane, so
scripts/network_smoke.py's bare internal token made every default-UA check read the prerendered crawler document. A Chrome token now leads, the internal token follows it (substring match, so internal-traffic exclusion still holds), andCRAWLER_UAis untouched.
- Admin URLs no longer leak into the machine corpus. Five pages
hyperlinked
/admin/control-boardin their prose, which put an admin path in/llms.txt. Found by item 16's rewrittentests/test_excluded_links_hidden.py. The prose keeps the words and drops the link — the URL is useless to anyone who cannot authenticate, and publishing it invited crawlers to an admin path.
Round 3.4, the posture flip — consumed as sync item 15, with boilerplate.2plot.dev as the other canary.
- The AI-training wall is retired (owner decision, 2026-08-30).
run.pysetsRobotsConfig(block_ai_training=False). It stood while a refused read was the only thing this site could say about a training crawler; since the 2.8.0 ledger round every read is RECORDED and reconcilable — vendor, tier, verdict, bytes, verified — and the hub's Ledger tab agrees with this host's own count (llms 52 = 52). A wall that refuses the read also refuses the evidence. Per-vendor block/meter through the callablevendor_policyseam is the instrument now: aimed, live, revocable per request, which a blanket bucket never was. Claude-User, Claude-SearchBot, ChatGPT-User, OAI-SearchBot and PerplexityBot were never in the training bucket and are unaffected. - In-process, both real UAs (ClaudeBot, GPTBot), before → after:
/403 → 200 (13,778 B crawler document) ·/llms.txt200 → 200 ·/healthz403 → 200. On the wire at 14:09Z the pre-flip triple was identical to the in-process one, which means every 403 this host serves is the APP's — no separate edge wall has been observed here. - Three robots.txt fingerprint checks became posture checks
(
tests/test_llms_routes.py,scripts/network_smoke.py,scripts/smoke_live.py).ClaudeBot -> Disallow: /was asserted as proof the running artifact was the intended package; it was really a statement of policy, and policy changed. The vendor SPLIT still fingerprints the package (OAI-SearchBot, Claude-User, Claude-SearchBot allAllow: /), and ClaudeBot/GPTBot/CCBot are now asserted NOT to carryDisallow: /. tests/test_vendor_policy.py's default-posture pin now asserts 200 where it asserted 403, andtests/test_showcase.pyasserts the panel shows the served document rather than a 403 — the showcase exists to show the truth about this host, so it moves with the posture.- On the wire after deploy (build 625c91c, 22:05Z), both real UAs:
/200 (14,133 B crawler document) ·/llms.txt200 (13,801 B) ·/healthz200, and/robots.txtcarryingAllow: /for ClaudeBot, GPTBot and CCBot. No edge wall appeared when the app's came down — every 403 this host ever served was its own, and the owner has since confirmed no Cloudflare rule exists on the zone. TheDIVERGENCES.mdposture fence is re-dated to200/200/200.
Consumed SYNC-1.6.22-1.6.35 items 12 and 13 at
dash-documentation-boilerplate 1.6.35 (4c63992). The ledger round and
the release-branch round. Both items are contract-class: ported into this
fork's shape, not byte-copied. Per-item dispositions are in the sync report.
/admin/traffic— this host's own crawler ledger, behind the control board's exact gate (fails CLOSED without Clerk, for the same reason). Vendor × day, vendor → tier for a picked day, top paths per vendor, and the v3 headline numbers for the same day so the two accountings can be read side by side. Plain tables, no charts, no interval callback: a 14 × 40 table of strings is about a millisecond, five charts were ten seconds (fleet fact 18).- The read table.
dash-improve-my-llms2.8.0 emits one event per corpus document it serves (on_document_read) and does no I/O with it;run.pyregistersAnalyticsTracker.record_readonce, which keeps the row as areadstable in the SAME analytics file — same buffer, lock, flush cadence and retention asvisits, withclient_ipdropped unlessANALYTICS_KEEP_CLIENT_IP=1.readsis JOINED by the rollup, never summed intohuman_hits/bot_hits/pages. - Rollup v4, additive and present only on a day with reads:
vendors[](one row per vendor key × verified × policy, with per-tier counts and bytes, null key kept as the unverifiable bulk) andreads. Every v3 key is byte-identical; the reporter POSTs whatdaily_rollupreturns and is unchanged. - CD promotes
main→release..github/workflows/cd.yml'sdeployjob now fast-forwardsreleaseto the run's own sha after the matrix is green, and Render watchesrelease(render.yaml).needs: [test]is the whole gate, soreleasecannot receive an uncertified commit by construction.tests/test_cd_promotes_release.pypins the structure. - A
posturefence inDIVERGENCES.md— measured, not intended:ai_bots,healthz: full,runtime: python,deploy: release-branch.
- The first promoted run went red on its build-match wait, and that was
the owner step outstanding, not a defect.
5c73a53reachedmainat 17:43:00Z andreleaseat 17:45:24Z; at 18:22:44Z/healthzstill servedae1dce6. This service is not Blueprint-managed, sorender.yaml'sbranch:is documentation and the dashboard Branch field is the switch — item 13 says exactly this in its notes. The owner switched it at ~18:00Z and the next run (33281935425,0081f65) went fully green in five minutes: promote, wait, and verify including its/healthz build == github.shastep.main == release == wire. - On the same run, item 13's
verifyfix proved itself: the job was skipped rather than run. Under the previousalways() && != 'cancelled' && != 'skipped'gate it would have run after the failed deploy and reported GREEN againstae1dce6— the previous build. That is the defect the gate change exists to prevent, observed here on the first run that could exhibit it.
human_hitsDROPS andbot_hitsRISES from this release. UA-less and library clients (httpx,Go-http-client,node-fetch, an empty User-Agent) move from the human lane to the crawler lane, becauseclassify()puts them there and this app no longer disagrees with it. The hub's day-over-day view will show a step on the adoption date. That is the number becoming true, not a regression.- There is ONE classifier.
lib/analytics_tracker.pydelegatedis_botanddetect_bot_typetodash_improve_my_llms.classify()and now ends with ZERO User-Agent strings. Its own lists had filed ClaudeBot — Anthropic's TRAINING crawler — under "search", still named the retiredanthropic-ai/claude-webtokens, and knew nothing ofbytespiderorClaude-User. The names and signatures are kept for callers. Crawler rows gainvendor_key,vendor_class,verified,lane; human rows are byte-identical to before, and theINTERNAL_UA_TOKENdrop still happens FIRST. - The floor moves to
dash-improve-my-llms>=2.8.0in every encoding:requirements.txt(four lines),run.py'sLLMS_PKG_FLOOR, and CI's install line plus both version asserts. Not a degradable feature —lib/analytics_trackerimportsclassifyand_ledger.EVENT_FIELDSat module scope. verifyruns only onneeds.deploy.result == 'success'. The oldalways() && != 'cancelled' && != 'skipped'admittedfailure, so a failed promote could still be followed by a green verify of the PREVIOUS build. Verify's first step now also asserts/healthzbuild == this run's sha itself. This fork'sSITE_URLguard (DIVERGENCES.md 4) is kept as a second conjunct.
- The Render deploy-hook secret, and every trace of its name from
cd.yml. A push tomainis no longer a deploy; it is a candidate.
tests/test_proxy_scheme.pysendsBROWSER_UA. At the 2.8.0 floor an absent User-Agent is the crawler lane, so the UA-less probe received the crawler document — which carries notwitter:url— and failed on "no tag" without saying anything about the forwarded scheme. Either lane can be the one you did not mean to test.
Kit adoption — consumed SYNC-1.6.10-1.6.16, SYNC-1.6.17-1.6.21 and
SYNC-1.6.22-1.6.29 at dash-documentation-boilerplate 1.6.29 (5589318).
This host was a live kit-lineage site that had never been on the fleet
roster; it joins with this release. Per-item dispositions are in the sync
report; the deliberate differences are now recorded in DIVERGENCES.md.
- The
.claude/development kit —CLAUDE.md(this site's own guide above the network's behavioral contract and verification traps, both ported verbatim),settings.jsonpointed at THIS host, and the three shipped skills (wire-verify,sync-template,report) byte-verbatim..gitignoremoves from a blanket.claude/ignore to the template's ALLOW-LIST form, which is what keeps credentials under.claude/structurally uncommittable, and gains the session-document block. DIVERGENCES.md— six recorded divergences with reasons, plus the machine-readablebyte-ownedfence (empty: this fork makes no byte-level claim on anysync-verbatimpath, and drift is never fenced).tests/test_claude_kit.py,tests/test_auth_demos.py— byte-verbatim kit cargo.tests/test_python_version.py— the one-fleet-Python agreement pins.pythonon/healthz, all three backends, plus the battery'spython_matches_declaredcheck: the serving interpreter is now on the wire, so a stale image can be contradicted from outside.- The configured-auth branch is certified. Two pins render
lib.auth.register()with a FAKE non-empty Clerk config — the branch a zero-secret suite had never executed, including thepk_livesatellite-mode auto-enable that can only exist in production. - CI asserts Docker's own health verdict (
docker inspect .State.Health.Status), failing onnone: the external curl proves the app answers, never the HEALTHCHECK instruction itself.
- One fleet Python: 3.14, in every encoding at once —
Dockerfile(python:3.14-slim, a MINOR tag: the old3.11.8patch pin could never receive a 3.11.x security release), the CI matrix main, the lint and pip-audit jobs,cd.yml's verify job, andrender.yaml'sPYTHON_VERSION(fullX.Y.Z, as Render's native runtime requires). The window legs are 3.13 and 3.12. Dockerfilehonors$PORT— shell-formCMDwith the default at the point of use (${PORT:-8550}), and the HEALTHCHECK probes the same variable. Exec-formCMDnever expands env, so the old form hardcoded the port whatever the platform asked for.render.yamlnames THIS host. Every identity field still saidboilerplate— service name, domain,APP_BASE_URL,SATELLITE_APP_KEY,AD_APP_ID— inherited at fork time and never corrected, while production servedllms.2plot.devwithapp: "llms".POLICY_STORE_FILEis now declared on the mounted disk beside the other two stores.- CD is sized for the worst build: the build-match wait runs 100 × 15s
under a 30-minute job timeout (a floor bump busts the pip cache by design,
so this pipeline's most important deploy is also its slowest), a hookless
deploy emits
::warningrather than a quiet notice, and the verify job now stands down on askippeddeploy as well as acancelledone. .github/dependabot.yml— the 1.6.24 rewrite: the pip ecosystem is removed entirely. On range requirements dependabot can only propose FLOOR RAISES, so the old allow-list group structurally produced the very PR class it existed to suppress. Floors move through sync specs, every encoding at once. Security updates ride GitHub's separate channel.scripts/smoke_live.pyis the template's current file: the auth POST now carries the same SSL context asfetch(without it every POST died in the macOS handshake and read as missing auth wiring), andwake()tolerates a legacyfetchstub. A source pin intests/test_auth_wiring.pyholds the SSL half — no wired test can, they all monkeypatchpost.- The gate card promises only what ships — "and the AI assistant" is gone from the demo-card copy; nothing here wires one.
lib/auth_demos.pypoints at a demo this site can render. The inherited/examples/visualizationentry named a page and a module that exist on no fork; every gate card rendered demo-less and silent since fork time. This site's hero is/showcase/robots-sandbox.
- The vestigial Node layer (template issue #12, CVE-2026-1615, removed
upstream in 1.6.9 and carried here with this Dockerfile sync).
package.json/package-lock.jsonwere dash-mantine-components' component-build toolchain, inherited through the fork lineage and used by nothing in this repo — no webpack config, nosrc/ts, no CI job, no served asset — while the image apt-installed nodejs+npm andnpm installed a known-vulnerablejsonpath@1.1.1into every production build.
This repository forked here. Everything below this entry is the history of
dash-documentation-boilerplate, the template this site was forked from at
1.6.7 — kept because the machinery is inherited and its reasoning still
applies. Everything from here up is llms-2plot-dev, the documentation site
for dash-improve-my-llms and the 2plot network's owner-control bench.
- The site's own content. Three audience pages (
/audiences/mcp-clients,/audiences/web-crawlers,/audiences/llm-context— URLs preserved byte for byte from the retiring service), a five-page Reference section, and three showcases that run the package's own pure handlers in-process. lib/policy_store.py— the writable layer. Flock-guarded JSON, validated on write, atomic on replace, fail-open on read, re-stat'd on every call. Reachesdash-improve-my-llms2.7.0 through its callable seams, so a control-board toggle lands on the next request in every worker with no restart.- The control board's country guardrail — a click-to-select world map over the inherited page-visibility board, with the admin gate re-checked server-side in the write callback.
BUGS-2.7.0.md— the pre-release soak that gates the package's tag.
- Identity, on every surface: brand, description, origin, favicons from the hook mark, header logo and wordmark, the GitHub link, the social-card object, and the JSON-LD blocks.
- The template's documentation is DELETED, not hidden (owner decision).
excluded_linkshid the eleven tutorial pages from the sidebar but left them insitemap.xml,/llms.txt,/llms-full.txtand the MCP resource set — so this host would have published the boilerplate's documentation as its own. This overrides the migration kickoff's "NEVER deleted (wave-sync purity)" rule: template syncs touchingdocs/now need resolving by hand, and that cost was accepted for a site that stands on its own. - Sixteen
301redirects for retired and deleted URLs.
requirements.txtstill floors atdash-improve-my-llms>=2.6.1. 2.7.0 is unpublished, so every 2.7.0 call site sits behind theLLMS_HAS_27capability probe inrun.pyand the app boots on either release.
- Auth-wiring guards, both halves (the flexlayout finding):
dash-clerk-auth wires either side of
Dash(...)—register()is the UI half,configure_app(app)the server half (/api/auth/*routes + per-request identity). Flexlayout's batch-2 pass shipped the first call without the second: components rendered and ClerkJS reported signed-in while every server render read signed-out — the control board served the owner the sign-in card forever,POST /api/auth/sessionanswered 405 through Dash's GET-only page catch-all, and sign-out never revoked. Invisible to every suite, because Clerk is off in test environments andconfigure_appno-ops without keys. Two guards now, one per environment:tests/test_auth_wiring.pypins structurally (AST) that run.py calls BOTH halves;scripts/smoke_live.pygains an "Auth wiring" block that POSTs both endpoints on the live host (registered = 2xx/4xx; unregistered = 404/405), gated on the package's inline bootstrap being present in the served shell so clerk-off hosts skip rather than fail. Measured baselines: boilerplate answers 401/200, flexlayout answered 405/405. Note: the battery's POST probes need real egress — sandboxed environments that allow only GET report transport-0.
- dimll floor 2.6.0 → 2.6.1 (requirements incl. the commented
backend extras, run.py's boot floor + its message, and the test —
the floor lives in more than one place; all moved together). 2.6.1
makes the universal prerender VISIBLE to non-JS consumers: below it
the injected block carries a literal
hiddenattribute, so every visibility-respecting reader (html-to-text extractors, arguably crawler content-weighting) saw only "Loading..." — the outside-audit finding of 2026-08-22, diagnosed live across six hosts and fixed at the package. The generic-UA prerender test now asserts the fixed shape: div withouthidden, plus the marked synchronous hide script that keeps JS browsers flash-free (React's mount wipes the pair, so nothing changes for humans). The fleet inherits 2.6.1 on each host's next deploy with no requirements edit; this release is the reference host's own pickup plus the floor that makes the guarantee permanent.
Batch-1 closeout: the wave's other three hosts (emojimart, modelviewer, excalidraw) shipped dark, and four of their findings trace to this template. All four are fixed at the source so batch 2 and every future fork inherit the fix instead of rediscovering it.
- Runtime-imports guard (
tests/test_runtime_imports.py, the modelviewer finding): a fork died in production on a function-localimport PILthat every dev machine happened to satisfy — suite green, boots locally, dies in a clean image, and one docs example took all ten pages down because Dash imports every page at construction. The test AST-walks every runtime module and asserts each absolute import resolves in the environment CI installs (requirements.txt and nothing else); nesting is deliberately ignored because it does not predict boot-fatality. The optional-backend exemption (fastapi/quart select by env) is earned by two companion tests: the extras must stay documented as commented requirements lines, and the carrier modules must never be hoisted to run.py's unconditional top level. A third companion pins that runtime code never imports build-timescripts/. - CSS hygiene guard (
tests/test_css_hygiene.py, the excalidraw finding, landed at the source): fails on any hashed.m_*Mantine selector inassets/*.css. Three forks have paid for this class — leaflet's floating drawer, emojimart's 63vh drawer, and excalidraw inheriting two dead-or-harmful hashed rules from this template. - modelviewer + excalidraw joined the canonical network directory
(
lib/network_directory.py): both were deliberately absent until they deployed; both are live and build-identity-verified as of 2026-08-21/22. The fleet re-copy carries the entries everywhere. - Markdown tables scroll in their own box (
table.m2d-table, GitHub's recipe: content-width, capped at the container, scrollable past it — the excalidraw finding): a<table>is min-content sized, so one wide prop table dragged an entire page 105px sideways at 414px. A no-op for tables that already fit; covers kwargs prop tables too, since markdown2dash stamps the class on every table.
- The three hashed-selector fossils removed from
assets/main.css(dmc-docs fork era, present since the initial commit):.m_46b77525put an!importantmargin on every Input wrapper in every docs example;.m_5caae85bwas dead in DMC 2.7 and 2.8;.m_9cdde9arestated Mantine's own aside declarations around one intentful pixel — the TOC's 15px breathing gap, which moved to the staticaside.mantine-AppShell-asiderule. scripts/make_favicons.pynow flattens the apple-touch icon onto opaque white (the emojimart finding): iOS composites the icon's alpha onto its own background — black on some surfaces, white on others — so every fork that ran this script shipped an icon that renders differently everywhere it appears. Every other size keeps its transparency. The template's ownapple-touch-icon.pngis regenerated (the other seven files regenerated byte-identical, confirming provenance), and a header-level PNG colour-type test pins opacity without needing Pillow in CI.- The header wordmark now hides below
xswith the accessible name preserved — the pattern both modelviewer and excalidraw needed and implemented divergently.visibleFromkeeps the node in the DOM (the typing animation still finds it) butdisplay:noneDOES remove it from the accessibility tree, so the home link now carries a permanentaria-labeland the logo img is explicitly decorative (alt=""). Without the label, phones would get a home link with no name at all — the modelviewer defect, which excalidraw's pass reasoned incorrectly about and likely still ships.
Two fleet-class fixes surfaced by the wave's first pair, landed at the source so the other eighteen forks inherit them.
- CD now verifies the artifact it shipped, not "whatever is live"
(the muicharts finding): with
RENDER_DEPLOY_HOOK_URLunset, the old workflow skipped the wait and ran the live battery seconds after the push — against the previous release, every run, invisibly./healthznow reports the running instance's commit (RENDER_GIT_COMMIT, optional field — the fleet probe contract is unchanged), and the CD wait holds until it matches the run's SHA, falling back once (with a warning) on builds predating the field. - The byte-copy identity trap (the pannellum finding): the
reporter must stay byte-identical across forks, so its fallback
app key says "boilerplate" everywhere — while a fork's other modules
default to the fork's own key.
run.pynow claims the identity viaos.environ.setdefault("SATELLITE_APP_KEY", ...)before any hub-facing import — the marked FORK POINT; forks change that one string and keep the reporter byte-identical. A real env value always wins.
- Vendored dash-clerk-auth 1.0.4 → 1.0.5 (sha256
a2f9062e…b74f3, full provenance in requirements.txt). Fixes the return-trip stale gate the owner observed live: landing back on an auth-gated page after signing in on the primary showed the gate card until a manual refresh, because the first server render precedes__dca_identityminting. 1.0.5 syncs the session and reloads once, with a sessionStorage no-loop marker shared by both reconciliation paths. The provenance rule is now general: only the recorded sha admits a tarball — stale early builds have bitten on both of the last two releases and are indistinguishable by name, size, or date.
The pre-wave hygiene pass, from the four-repo review.
- Date-skew corrections (leaflet handoff §8): seven committed
provenance stamps read
2026-08-22for events whose verified date is2026-08-21(git author dates corroborate) — CHANGELOG headers 1.5.3–1.6.1,components/header.py,lib/ad_client.py,requirements.txt. All corrected; the three release commit SUBJECTS carrying the wrong date are immutable and stand corrected by this entry. A date nobody can trace is worse than no date. docs/authentication/authentication.mdnow documents the control-board override layer (override → frontmatter →PAGE_DEFAULT_TIER, hub ceiling on top) instead of contradicting shipped behavior;lastmodbumped accordingly.run.py's floor failure message now names what a 2.5.x actually loses first — silently swallowedlastmod, the lying sitemap — matching the comment that raised the floor.lib/auth.py's signout-shim docstring caught up with reality (upstream fix shipped in 1.0.3/1.0.4; the shim is a deliberate duplicate until the fleet-wide retirement pass).
- README caught up three releases: dimll floor 2.5.1 → 2.6.0 in five places, a new Access Control & Live Page Management section (control board, admin allowlist, gate teasers), the mobile-drawer standard under UI/UX, and the admin env vars in Configuration.
.env.examplegains the admin surface (ADMIN_EMAILS,ADMIN_USER_IDS,ALLOW_UNGATED_ADMIN) — the gate for the 1.6.0 headline feature was previously undiscoverable from the env template..claude/CLAUDE.mdCustomization Points now lists the control board, the override store, and the auth-demo teasers.
- Accessibility + agentic-browsing names on the header's icon controls
(hamburger, theme toggle, GitHub link —
create_linknow requires a label), and the network-ad image reserves a square box viaaspect-ratioso the aside no longer layout-shifts when the creative loads. All three were Lighthouse findings on the pilot host measured against template code — every fork inherits the fix.
Every fork gets its own live control board — the leaflet pilot's proven UX, ported with its scar tissue included.
/admin/control-board(pages/control_board.py): flip any docs page between public / auth / admin / hidden and toggle its llms.txt exposure, live — changes apply on the next render, no restart. Gated by the ADMIN_EMAILS/ADMIN_USER_IDS allowlist + owner; fails CLOSED without Clerk (ALLOW_UNGATED_ADMIN=1for local work), and the write callback re-checks the gate server-side (pattern-matching callbacks stay callable by anyone who can POST). The board stays OUT of both tier ledgers — its machine surfaces are silenced package-side viamark_hidden()(sitemap, llms.txt, MCP, prerender, crawler HTML all treat it as absent) soaccess.gating_configured()stays False on all-public forks and the hot path stays check-free.lib/page_visibility.py— the override store, with both fleet lessons built in: mtime-throttled cross-worker reload (a toggle lands on every gunicorn worker within ~1s — the pilot's coin-flip defect) and loud persistence guards (boot warns whenPAGE_VISIBILITY_FILEis unset OR points under /var/ without a real mount — the twice-observed silent-reset-per-deploy class).- Override-first resolution in
lib/access.py: board override → frontmatter → env default, with the hub ceiling still applied on top (an override can loosen a local declaration, never a network restriction).pages/markdown.pyregisters every docs page on both ledgers from the one declared value. - The sign-in card's live-demo teaser now ships ARMED: DEMOS carries a
working entry (
/examples/visualization→ the theme-aware chart), so gating that page shows "Live demo — try it" above "Authentication required — You're looking at a live preview of {page}. Create a free account to unlock the full documentation — every interactive example, the complete API reference, and the AI assistant." render.yaml+.env.example:PAGE_VISIBILITY_FILEon the /var/data disk, with the blueprint-vs-dashboard drift warning inline. 14 new tests (tests/test_control_board.py).
- Navigation order: "Other Apps I've built" now sits above "Resources", and "Resources" is the LAST section — own-work ranks above third-party links, and the only section that navigates away from the network closes the list.
- Vendored dash-clerk-auth 1.0.3 → 1.0.4 (sha256
7a7c333a…cf701a, recorded in full in requirements.txt with the stale-first-build warning). What 1.0.4 fixes, from the live network certification: the FastAPI auth endpoints were never callable (un-annotated request param → required query field → 422 on every POST — inert on this Flask host, fatal on fastapi ones), and the ghost-cookie fresh-load case — a page loading with ClerkJS signed-out while the server still held the identity now reconciles with a signout POST + single reload, which is the cross-host sign-out path no click shim can cover.revokeServerSessionalso verifies its response now. The 1.5.1 shim remains an idempotent duplicate; retirement is one clean release cycle after the fleet is on >=1.0.4.
- Vendored dash-clerk-auth 1.0.2 → 1.0.3 (sha256
2c6b40f4…da1944, recorded in full in requirements.txt — the tarball IS the release; there is no PyPI for this package). 1.0.3 fixes sign-out revocation package-side (both entry points + the signed-in→signed-out listener transition, so sign-outs propagate across tabs and hosts), replaces the DiceBear default avatar with an inline SVG data URI (no third party in the UI path), and discards non-absolutesatellite_sign_in_redirectvalues loudly. 1.5.1's app-side signout shim is idempotent alongside it and retires next release. - Provenance caveat recorded in requirements.txt: vendor from the hook
repo's
dist/artifact ONLY — itsmaincurrently holds a broken build (boot-time collection error on Python 3.10/3.11) until the import-fix PR lands; verify the sha before re-vendoring.
Pilot-week hotfix: Sign Out that actually signs out, and an honest floor comment.
- Sign Out now revokes the server session. dash-clerk-auth 1.0.2's
logout runs
window.Clerk.signOut()client-side and reloads — but the server keeps trusting the signed__dca_identitycookie (max-agesession_lifetime_days, default 7 days) and the Flask session it minted at sign-in, so a signed-out browser kept rendering every auth-gated page; on a shared computer the next person inherited the previous user's access. The package ships the endpoint that fixes this (POST /api/auth/signout) but nothing ever called it. Newlib/auth.py:_install_signout_delegation()— a capture-phase delegate on the logout menu item (the sign-in delegation's proven pattern) — owns the click and sequencesClerk.signOut()FIRST (so the slow path can't re-verify__sessionand re-mint), then the server signout, then the reload, awaited so the reload never races the cookie clears. The package-side fix ships in dash-clerk-auth 1.0.3; this delegate is idempotent alongside it and retires a release after the fleet vendors>=1.0.3.
- Floor-comment honesty (
run.py,pages/markdown.py): 1.5.0's claim that passinglastmod="TypeErrors on anything older" was false — measured on 2.5.1 by the pip-docs+ stage-4 session, the signature is(path, name=None, description=None, llms_doc=None, **kwargs), so older packages accept the date and silently ignore it. The 2.6.0 floor stays load-bearing, but for honesty (below it, every stamped date is swallowed and the sitemap goes back to swearing everything changed at build time), not crash avoidance.
The reference host proves dimll 2.6.0 (stage 2 of the network rollout
order). The floor is load-bearing: pages/markdown.py passes lastmod=
unconditionally, which TypeErrors on anything older.
dash-improve-my-llms[flask]>=2.6.0(was 2.5.1), andLLMS_PKG_FLOOR = (2, 6, 0). What arrives: icon autodiscovery, truthful sitemap<lastmod>, JSON-LDpublisher.logo, and the llms.txt viewer banner de-dup (package-side, free).- Every docs page's frontmatter now declares
lastmod:with its REAL git last-commit date (2025-11-09 through 2026-08-19 — eleven pages, zero invented dates). TheMetamodel gains the field with a YAML-date-to-ISO validator;register_page_metadatapasses it through; unset pages omit the tag — truth or silence. Deliberately not scripted from file mtimes, which reset on every Docker build and would re-invent the daily-lie sitemap 2.6.0 exists to end. configure_seo(icons=)'s.icoentry moved to theassets/favicon/favicon.icocopy (byte-identical to the root one index.html links) so the declared list is SET-equal to what 2.6.0's discovery finds.
tests/test_seo_icons.py: discovery-vs-declaration set-agreement (the proof the fleet can rely on discovery alone once its pixels are right — order-inequality is not a failure, per the release notes) and sitemap-honesty pins (every emitted<lastmod>traceable to a frontmatter declaration; the undeclared home page carries none).
dash-clerk-authis now installed by requirements.txt (from the vendored tarball) rather than riding the image uninstalled. 1.4.0 shipped the whole sign-in surface — avatar, gate cards, delegation — but the deployed reference site could not render any of it because the package it wires was never onsys.path. Runtime posture is unchanged: with noCLERK_*keys the site is exactly as public as before, so forks inherit the capability, never a login wall. Alongside it, the fleet security floors are now asserted rather than merely permitted:clerk-backend-api>=7.0.0,<8andcryptography>=50.0.0(the four-advisory baseline dash-clerk-auth 1.0.1 widened its cap for).
The interactive gate and the real-time half of the fleet's analytics land on the template. Humans meet a sign-in card on gated pages while agents keep reading the machine surfaces through the data window — the two lanes split onto separate axes, each flipped per host by one env var. The satellite reporter grows a presence beacon so the hub board can show "active right now" without waiting for a rollup. On THIS host the gate ships dark twice over: every tier is public, and dash-clerk-auth is deliberately not in requirements.txt (the vendored tarball exists for the docs' optional-auth install command) — the presence beacon is what this deploy turns on.
- The interactive gate (
lib/gate_layouts.py): every markdown docs page renders through a per-request verdict — sign-in card at HTTP 200 (with an optional live teaser demo vialib/auth_demos.py, table empty in the template), forbidden and 404 cards, the content on allow. The verdict is the newaccess.resolve_page_access(): docs fall open without Clerk, admin fails closed, and?key=never unlocks a browser layout. The gate switch isPAGE_DEFAULT_TIER=authper deployment;/,/getting-startedand the corpus pseudo-paths are pinned public so no env flip can gate the funnel. Card buttons rideassets/auth_gate.js/.css(satellite mode navigates to the primary with?returnTo=; local dev opens the Clerk modal). - The second tier axis,
llms_public(frontmatter, orLLMS_PUBLIC_DEFAULT, default open): a gated page's machine twin —/<page>/llms.txt, crawler HTML, the prerender — stays public while the interactive page is gated. That split is the data-window posture, and the later agent flip isLLMS_PUBLIC_DEFAULT=0, env only. The exemption never applies to a hub-imposed tier: a satellite's env default cannot loosen what the network restricted. GET /api/agent-key(lib/agent_key.py, all three backends): turns the browser's Clerk session into the hub-minted?key=that the "Copy for LLM" button (assets/llms_copy.js) now appends, so a copied URL keeps working inside an assistant that has no cookie. 204 for anonymous / Clerk-off / hub-down;Cache-Control: private, no-storealways; the token is read from the__sessioncookie, never the query.- The presence beacon (
lib/satellite_reporter.py): a second, fail-silent daemon thread POSTs{app, active}to the hub's/api/satellite/activeevery 60s (SATELLITE_PRESENCE_INTERVAL_S, floor 30,0disables) — distinct human visitors inside the session window, the same derivation as the hub's own count. Display-only and ephemeral hub-side; the daily rollup stays the sole source of the daily numbers. A hub that predates the endpoint 404s harmlessly. - Clerk avatar in the header (
components/header.py::create_clerk_avatar), rendered only when Clerk is configured.
render.yaml: rollup cadenceSATELLITE_REPORT_INTERVAL_S=900(the fleet is on paid instances and the hub board now reads near-real-time), the full Clerk satellite env block, and the two gate knobs — remembering that env/plan changes apply on Blueprint sync, not git push.lib/auth.py: the hand-rolled 0.9.0/0.9.1 satellite fixups are retired — both are upstream in the vendored dash-clerk-auth 1.0.2. What remains is capture-phase delegation (_install_satellite_signin_delegation, back-ported from the leaflet pilot 2026-08-19): late-rendered#clerk-login-buttons get exactly one handler, preferringbuildSatelliteRedirect()with?returnTo=, falling back toredirectToSignInon origin+pathname so stale__clerk_*params never ride into the next sign-in.- The corpus pseudo-paths (
/llms-small.txt,/llms-full.txt) registerpublicexplicitly instead of falling through the tier default, soPAGE_DEFAULT_TIERcan never gate them;/likewise (it registers via pages/home.py, which no frontmatter ever tiers). - Vendored
dash_clerk_auth0.9.1 → 1.0.2 (the clerk-backend-api<8cap for thecryptography>=50floor, plus the avatar session fix).
- The peer-host key-leak test judges parsed origins, not substrings —
bare-host matching flags a site's own links whenever a peer host is a
substring of its own (
2plot.dev⊂leaflet.2plot.dev; found by the leaflet pilot, this repo was saved only by its hostname). The invariant stated properly: any URL carrying a key must be same-origin. lib/agent_key.pyrecords why it must not usefrom __future__ import annotations: PEP 563 turns the FastAPIRequestannotation into a string resolved against module globals, where the locally imported class does not exist — the parameter silently becomes a required query field and the route 422s.
Instrument first: the 402 groundwork lands on the template. The network's
metered lane is gated on ~30 days of crawl data (owner decision 2026-08-10);
this release is what makes that data exist and stay true on every satellite
forked from here — machine-surface demand reported per document, counted
once, tested, and tierable per deployment. No payment code ships here.
Rollout plan: kickoff/KICKOFF-x402-instrumentation-rollout.md (local).
- The daily rollup now reports the machine surfaces (the network's
v3 analytics fields): unique bot visitors per day (
bot_visitors, a daily distinct count), and llms.txt / robots / sitemap / page.json rows inpageswith a per-row bot split — mirroring the hub's own self-report semantics exactly. These fetches were always recorded; they were only hidden from the report. A day with only machine-surface fetches is now reported instead of skipped — crawlers hammering llms.txt with zero human visits is exactly the signal the hub's day-pass board exists to see. - The machine-surface rollup is tested (
tests/test_traffic_rollup.py) — 15 hand-checkable cases pinning the partition (every path is a page visit or a machine-surface hit, never both), the machine-only-day report, the per-row bot split, and the distinctbot_visitorscount. This data is the evidence base for the network's 402 pricing decision; untested measurement code deciding a revenue model was the wrong risk to carry. - Tier registrations for the corpus documents. Every satellite built
from this template now declares access tiers for
/llms-small.txtand/llms-full.txt(served by dash-improve-my-llms ≥ 2.4.0; inert on older versions):LLMS_SMALL_TIER/LLMS_FULL_TIERenv vars set them locally (unset = public; documented in.env.exampleand visible inrender.yamlso every fork sees the knob), and the hub's page-tier ceilings can tighten either network-wide with no redeploy here. The dependency-floor message notes the 2.4.0 requirement for the tier documents. - Generic version placeholder
{{VERSION:<distribution>}}(newlib/versions.py, used by both markdown loaders). Prose may now state the installed version of any package — not just dash-improve-my-llms — so every satellite can write{{VERSION:<its-pypi-name>}}for the library it documents and a package upgrade propagates to the browser page, the copy button,/llms.txtand every/<page>/llms.txton the next deploy, with no prose edit.{{DIMLL_VERSION}}remains as a legacy alias. Fenced code blocks and inline code spans are left verbatim (the network-standard page shows the syntax in a fence), and a placeholder naming an uninstalled distribution fails the boot instead of leaking. The identity tests now also sweep for bold version claims next to any PyPI link, not only dash-improve-my-llms's.
- Machine-surface fetches were double-counted.
_SKIPexcluded/llms.txt,/robotsand/sitemapfrom page visits by substring — but/llms.txtdoes not substring-match/llms-small.txt, so the tier documents andpage.jsontwins landed in BOTHload_visitsandload_agent_hits, inflatinghuman_hits/bot_hits/pagesfor exactly the surfaces the 402 board prices._SKIPnow names all three. The hub'straffic_insights._SKIPhas the same gap (its comment claims the exclusion; its tuple doesn't deliver it) — port this fix there before the data window opens. - Dash-built components rendered empty props tables. The
numpy-docstring branch in
lib/directives/kwargs.py(for dash-mantine-components' hand-written docs) shadowed the base markdown2dash parser for theKeyword arguments:format that dash-generate-components emits — the format of every component a library satellite documents — so their.. kwargs::tables rendered silently empty. Found on muicharts'/api; pannellum's likely affected too. The directive now falls back to the base parser for that shape.
The post-deploy battery is the fleet's deploy gate: cd.yml runs it against
the live host after every merge and its exit code decides whether the run
goes green. Its fetch was a single urlopen — no retry, no wake-up — while
most of the fleet sits on Render tiers where a cold start or a dropped
connection is routine. Measured on dash-flows-upgraded: two runs minutes
apart against the same host, FAIL canonical on /interactions then
ok canonical on /interactions. A misdiagnosed failure is worse than a slow
one; it sends you to look at canonical tags that were correct all along.
Both fixes already existed in-fleet and never met (blueprint LESSONS §21 states the rule outright):
- A wake-up loop before the first check.
/healthzis polled up to 24 times, 10s apart — deliberately wider than §21's "12×5s is plenty", because a free-tier cold start routinely takes 60–90s and the window only costs time when the host is actually down. Awake meansok: true, not any 200: Render's loading page and a CDN error page can both be 200s. A host that never wakes is ONE failure ("nothing else was tested"), not a cascade of forty per-check failures that all mean the same thing. - A retry ladder inside
fetch— the shapescripts/network_smoke.pyalready had, and that leaflet's copy of this very script grew without the fix ever flowing back to the canonical here. Transport errors and 5xx retry with backoff; 2xx/3xx/4xx return immediately, because a 404 is a verdict and retrying it only slows the battery. Retries print to the CD log — a green run that shows retries is a host worth watching.
Proven live before shipping, twice over: on the first run after the change,
flows' llms.txt dropped the connection mid-body (IncompleteRead) and passed
on retry — the exact flake that triggered this fix — and a run against
email.2plot.dev saw its OWN pages do the same on two fatal checks that
would have turned that deploy red.
Tunables (env, so satellites stretch them without editing the file):
SMOKE_WAKE_ATTEMPTS, SMOKE_WAKE_INTERVAL_S, SMOKE_FETCH_RETRIES. Exit
semantics unchanged; no check weakened, removed, or reordered. The file
remains the canonical copy — satellites take it verbatim on their next touch.
NETWORK_BULLETIN_URL has been set in production, pointing at a hub endpoint
that works, against code that never read it. The wiring sat commented out
in run.py under a note saying "2plot.dev does not serve
/api/network/bulletin yet". The hub started serving it; the comment did not
change.
Nothing failed. configure_bulletin is opt-in, so an unwired app makes no
request at all and the viewer header renders perfectly well on the package's
built-in tips and an "No announcements." empty state. The only symptom was an
announcement that never appeared — which nobody goes looking for.
Now lib/bulletin.py, shaped like lib/proxy.py and lib/access.py: a
configure() that returns whether it wired, and a boot line that says which
of the two states the process is in. No commented-out code to go stale, and
tests/test_bulletin.py::test_run_py_wires_it_rather_than_leaving_it_commented_out
fails the moment someone comments it out again — commented wiring cannot
define the name it asserts on.
Two details worth keeping:
app_idcomes fromSATELLITE_APP_KEY, reused fromlib.satellite_reporter.app_key()rather than hard-coded. The hub scopes announcements by?app=and uses it to see which satellites actually render the bulletin, so a fork left announcing itself asboilerplatewould receive this template's news and be miscounted. One notion of "which satellite am I", not two that can disagree.- The TTL is floored at 60s. It is configurable via
NETWORK_BULLETIN_TTL_S, and a small value would refetch on nearly every llms.txt view; junk falls back to the default rather than raising at boot.
Verified end to end against the live hub: the rendered header carries the hub's own tip wording ("Append /llms.txt to any URL") rather than the package's default ("Append /llms.txt to any page URL"), and the current announcement.
One thing that cost time and is worth recording: on macOS the package's
bulletin client fails with CERTIFICATE_VERIFY_FAILED, because it uses a bare
urlopen with no CA bundle and the system Python has no OS trust-store
integration. That is a local-development artifact only — Linux containers have
a working store — but locally it looks exactly like a broken fetch. Run with
SSL_CERT_FILE=$(python -c "import certifi;print(certifi.where())") to tell
the two apart. scripts/smoke_live.py and scripts/audit_links.py already
carry their own certifi context for the same reason.
AD_APP_ID now defaults to boilerplate, not
dash-documentation-boilerplate. Four modules present an identity to the hub
— lib/ad_client.py, lib/satellite_reporter.py, lib/hub_client.py and
lib/bulletin.py — each with its own fallback, and the ad client was the odd
one out. The visible cost was a column on /admin/ad-board that did not line
up with /traffic; the invisible one is that hub_client.app_id() falls back
to AD_APP_ID when SATELLITE_APP_KEY is unset, so a deployment that set the
long name for ads alone was silently presenting it as its hub identity too.
lib/satellite_reporter.app_key() still refuses to chain to AD_APP_ID. The
two agreeing here is a convenience, not a contract — leaflet.2plot.dev runs
AD_APP_ID=dash-leaflet2 against directory key leaflet, and setting one for
ads must never re-key a satellite's analytics series.
render.yaml now sets AD_APP_ID explicitly rather than leaning on the code
default, so the deployed value is visible in the blueprint.
This splits ad history. The ad server keys impressions and clicks by
app, so anything already logged under dash-documentation-boilerplate stays
there — worth a look at /admin/ad-board on 2plot.dev before assuming the
numbers reset.
The repo had none, so every configurable was discoverable only by reading
lib/. Each block states what turns ON when set and what the app does when
it is not — because almost every one of these fails silently rather than
loudly: no APP_BASE_URL deindexes a fork, no CROSS_APP_WEBHOOK_SECRET
means the hub simply never charts this app, no NETWORK_BULLETIN_URL renders
a header that looks complete.
Not gitignored (the pattern is .env, exactly), and .dockerignore already
whitelists it against the .env* exclusion added in 1.2.2.
render.yaml gains NETWORK_BULLETIN_URL so the deployment documents itself
rather than depending on someone remembering to set it in the dashboard.
The social card, finished. 1.2.2 closed three of four defects and left this one open because the artwork did not exist. It does now.
Renders the 1200×630 card: the artwork composited onto a frame carrying the
brand, tagline and domain, using the manifest's own background_color and
theme_color so the card, the browser chrome and the install splash cannot
disagree. Output lands in build/social-cards/<domain>.png, which is
gitignored.
A TEMPLATE FILE, and that is the point — pass --brand/--tagline/--domain
and every satellite is framed identically, instead of each card being made by
hand once and drifting. Three details that are not incidental: the artwork's
alpha bounding box is cropped before fitting (assets/ddb.png carries ~66px
of transparent margin that would otherwise be centred as if it were image); a
brand too long for two lines shrinks once rather than colliding with the
domain strip; and fonts resolve from a candidate list (macOS, then
Debian/Ubuntu) rather than being bundled, because shipping a licensed TTF in
a template every satellite forks is a question best not answered.
Pillow stays out of requirements.txt. Nothing at runtime renders images,
and a docs site should not carry an image library into production for a
script run by hand every few months.
1200×630 = 1.91:1, the Open Graph documented ideal, which also degrades
cleanly into Twitter's 2:1 summary_large_image slot. Deliberately not
leaflet's 1280×515 (2.49:1), which is wider than both and gets cropped on
each — and what sits at that URL today is the 2plot wordmark rather than a
per-site card at all. This is the shape for the network to converge on.
was: https://boilerplate.2plot.dev/assets/ddb.png 784×741 (1.06:1)
now: https://cdn.2plot.ai/github_assets/boilerplate.2plot.dev.png 1200×630
The old image's declared dimensions were honest, so nothing was broken — it
was simply near-square, and summary_large_image letterboxed it into a wide
slot with bars either side.
Moving it off the app is the network rule and it is about cold starts, not tidiness: a card the app serves is fetched by the scraper at unfurl time, and on a cold free-tier container that request lands mid-wake and times out. The preview renders blank once, and the platform caches the miss — so the first person to share the link poisons it for everyone. The CDN has no cold start.
og:image:secure_url and og:image:type join the auxiliaries in
templates/index.html, matching what leaflet carries. Both are tags Dash does
not emit, which is the only reason they belong in the template.
The card's dimensions are now declared in three places: lib/constants.py,
templates/index.html, and the CDN object itself. test_social_card.py pins
the first two against each other, but nothing offline can look at the third —
so replacing the uploaded file with a differently-shaped one would leave every
test green while the platform reserves the wrong box and crops into it.
scripts/smoke_live.py now fetches the real file after every deploy and reads
its actual pixel dimensions out of the PNG's IHDR chunk, checking them against
the declared tags, plus the ratio, plus that og:image is neither empty nor
app-served. Two tests prove the check fires rather than merely existing:
test_a_reshaped_card_on_the_cdn_fails_the_deploy and
test_an_empty_og_image_fails_the_deploy.
That second case is not hypothetical — it is 2plot.dev's live state today, and
the reason kickoff/ now holds a handoff for it.
fetch() in that script changed from errors="replace" to
errors="surrogateescape" to make this possible. "replace" substitutes
U+FFFD for every invalid byte and is one-way, so the PNG header was gone
before it could be read; surrogateescape round-trips exactly and behaves
identically for text.
test_smoke_script_rejects_a_peer_serving_its_spa_shell and
test_a_dead_peer_is_reported_but_does_not_fail_the_deploy stubbed every
off-host URL, which now included the CDN-hosted card and failed the
(correctly fatal) card checks. The card is off-host but it is this
deployment's own responsibility, not a peer's — the distinction 1.2.2 drew
between "this host is fatal, somebody else's host is a warning" holds, the
stubs just needed to respect it.
build/ because the card is published to the CDN and never committed or
served. kickoff/ because handoff notes start a session in another repo: a
task list for 2plot.dev has no business in the template's checkout, and every
satellite forking this repo would inherit a to-do that was never theirs.
Finishing 1.2.1, and the three things it exposed. 1.2.1 shipped the right template and half the change. Everything below was measured against the live site rather than a local boot, because the local/deployed gap is precisely what hid the first defect for a day.
assets/favicon/ (the whole icon set plus site.webmanifest) and
tests/test_social_card.py were sitting UNTRACKED. The committed template
pointed at /assets/favicon/…, the deploy builds from git, so production
404'd the manifest, the apple-touch-icon and every PNG icon link — the entire
installable-app surface — while every local boot looked perfect because the
files were on disk. Nothing in the app reported it; git status was the only
place it appeared. Measured on the live site:
/assets/favicon/site.webmanifest 404
/assets/favicon/apple-touch-icon.png 404
/assets/favicon/favicon-32x32.png 404
The guard test was untracked too, so the one thing that would have caught this
had never run in CI either. Both are now tracked, and
test_every_asset_the_template_references_resolves widens the check from
"the manifest icons resolve" to "every /assets/… the template
references resolves" — because the failure was never about icons, it was
about a template referencing a file the repository does not have. A checkout
is what CI tests, so it fails there the moment something is not committed.
The manifest's contents needed no change; they were already correct.
PAGE_TITLE_PREFIX still read "Dash Pip Components | ", inherited from the
upstream this template was forked from and never changed. That is not only a
browser-tab string: Dash passes each page's title straight into og:title and
twitter:title (dash/_pages.py:_page_meta_tags), so every unfurl of
boilerplate.2plot.dev advertised a different site, while <title>,
og:site_name and the /llms.txt H1 all correctly said this one.
Now f"{SITE_SHORT_NAME} | ", matching the network convention the other
satellites already use (dash-leaflet2 | , Dash Email | ) and derived from
the brand rather than retyped, so the two cannot drift.
tests/test_site_identity.py pins the prefix, the derivation, the rendered
og:title/twitter:title, and sweeps the identity surfaces for any surviving
mention of the old brand.
Nobody sees their own share cards, which is the whole reason this needed a test rather than a look at the page.
Dash builds that tag from request.url, and on Flask request.url comes from
wsgi.url_scheme. Requests arrive over Cloudflare → Render → gunicorn and the
last hop is plaintext, so production told every social scraper
http://boilerplate.2plot.dev/. og:url looked fine throughout because the
template hard-codes it.
gunicorn does try to fix this — it rewrites the scheme from
X-Forwarded-Proto, but only when the immediate peer is in
forwarded_allow_ips, which defaults to 127.0.0.1. Reading the header
ourselves one layer above gunicorn sidesteps the question entirely:
HTTP_X_FORWARDED_PROTO is in the environ either way.
Notes on the implementation, all of them load-bearing:
- Only the scheme is taken. Host is not rewritten from
X-Forwarded-Host;BASE_URLis already this project's single source of truth for the public origin, and a second header-derived notion of "what host am I" is how a fork ends up serving two. - The FIRST entry of the header wins. Proxies append, as with
X-Forwarded-For, so the last entry is the hop nearest the app — the plaintext one being seen past. Reading from the wrong end reinstates the bug and still passes a single-proxy test, so there is a test for it. TRUST_PROXY_HEADERS=0turns it off. This trusts a header from whoever connected, which is correct behind Render (it overwrites the header on every inbound request) and wrong for an app exposed directly, where a client could forge it.- The server object is wrapped, never rebound —
app.serverstays the Flask/FastAPI/Quart instance that gunicorn imports asrun:serverand thatrun.pyhangsbefore_requestoff. All three backends are handled.
The sibling leaflet.2plot.dev already serves https in the same tag from an
identical Cloudflare/Render/gunicorn stack with no proxy configuration of its
own; the difference we could observe is that it deploys as a Docker service
rather than a native one, which would plausibly put the proxy on loopback and
satisfy gunicorn's default. That is inference — Render's internal topology is
not visible to us — and the fix deliberately does not depend on which
explanation is true.
Ported from leaflet.2plot.dev and adapted: that site hard-codes a static
canonical and this one does not (dash-improve-my-llms injects a per-page one),
so this version only ever corrects tags that exist and never creates one.
Three tags go stale after the first client-side route change, each for a
different reason: og:url is static in the template, twitter:url is
server-rendered from the entry request, and the injected canonical is right on
arrival and wrong thereafter. Dash routes through history.pushState, which
fires no event, so the tags advertise the landing URL for the rest of the
session. The origin is read from the existing og:url tag rather than
hard-coded a second time.
This helps Google, which runs JS. It cannot help social scrapers, which do not — which is why the scheme half had to be fixed server-side.
It counted the substring rel="canonical", and the new sync script's selector
(link[rel="canonical"]) is not a canonical tag. Same lesson as the
dv-banner chrome check it sits beside: match the markup, not the words, so a
file may legitimately discuss what it is being checked for.
og:image remains /assets/ddb.png, 784×741, served by the app. The declared
dimensions match the file honestly, so nothing is broken, but it misses two
network rules: cards belong on cdn.2plot.ai so a cold free-tier container
cannot blank a preview, and summary_large_image wants roughly 1.91:1
(leaflet's is 1280×515). https://cdn.2plot.ai/github_assets/boilerplate.2plot.dev.png
does not exist yet, and pointing og:image at a 404 is strictly worse than
the present state, so this waits on the asset. When it lands, the change is
OG_IMAGE_URL plus the width/height constants, plus the og:image:secure_url
and og:image:type tags leaflet carries.
The social card and the installable app — the two surfaces that live
entirely outside the app, and so fail where nobody is looking. Found while
rolling the standard onto leaflet.2plot.dev, which inherited the same shapes
from this template. Satellites copy tests/test_social_card.py verbatim.
- Two
og:imagetags per page, and the wrong one won.templates/index.htmldeclaredog:image/twitter:imagestatically while Dash also emits both per page. With noimage_url=passed, Dash inferred an image from the assets folder, foundassets/logo.svg, and emitted it alongside the static tag. Every major scraper rejects SVG, and the inferred tag came last — so the card described so carefully in the template lost to an image nothing can render.lib/constants.OG_IMAGE_URLis now passed toregister_page, and the template keeps only the auxiliaries Dash omits. - The same duplication across nine other tags —
description,og:type,og:title,og:description,twitter:card,twitter:url,twitter:title,twitter:description,twitter:imagewere all declared statically and emitted by Dash. The static copies described the site where Dash's describe the page, so the duplicate was both redundant and the less accurate of the two.test_no_meta_tag_dash_emits_is_also_declared_staticallypins the rule. - The home page published an empty
description.pages/home.pynever passed one, so Dash emitteddescription,og:descriptionandtwitter:descriptionascontent=""on the most-linked page on the site. - The web app manifest was inert, and named the wrong product. Its link and
the
apple-touch-iconwere commented out behind a note saying the files were missing — a note that outlived their arrival inassets/favicon/— and the commented hrefs pointed at/assets/, one level above where they live. The manifest itself still read "Dash Email — Email components for Plotly Dash", copied in from another repo; that string is what an installed app would have shown on the home screen. Fixed, linked, and itstheme_colorreconciled with thetheme-colormeta tag.
tests/test_social_card.py— a template file. Asserts the image is declared exactly once, is absolute, is not an SVG and resolves; that the manifest is linked, served, correctly named and has resolving icons; and thattemplates/index.htmlis still wired in, since it looks removable ( dash-improve-my-llms appears to cover OpenGraph) and is not — its injection runs only on the prerender path, which social scrapers do not take.lib/constants.OG_IMAGE_URL/_WIDTH/_HEIGHT/_ALT— the per-site values a fork changes.
The 2plot network standard, landed on the template.
2plot.ai (the network root) and 2plot.dev (the section hub) shipped this
first; satellites are next, and this repo is the one they fork. So the point
of this release is not that boilerplate.2plot.dev complies — it is that the
files a satellite copies verbatim now carry the standard with them. The new
Network Standard page is the
per-site checklist.
The three obligations below share a shape, and it is worth naming: every failure they prevent is silent. Nothing errors, no dashboard turns red, and the damage accumulates for months. That is why each one is now pinned by a test rather than by a convention.
One constant, "Dash Documentation Boilerplate — the 2plot network's template", now reaches every surface that states what this site is:
Dash(title=), register_page_metadata(path="/", name=…), the first line of
pages/home.md, and templates/index.html (og:site_name, og:title,
twitter:title, the schema.org SoftwareApplication.name, the <noscript>
heading).
What this fixes is not cosmetic. dash-improve-my-llms resolves the
/llms.txt H1 and the llms viewer's brand chip through
resolve_site_title(home_page_name, app.title), and given nothing useful it
publishes what it finds. On this host that was the Dash() constructor's
default title: every agent that fetched boilerplate.2plot.dev/llms.txt cold
was told the site is called "Dash". The page rendered perfectly the whole
time. 2.3.4 fixed half of it — generic candidates (Home, Index, Dash)
are now skipped rather than served — but a package cannot invent a name; the
other half is stating one.
Naming rules, from the standard: the brand says what the site is; the
package name (dash-documentation-boilerplate) belongs in the description;
"Pip Install Python" is the byline and never the site name.
tests/test_site_identity.py pins all of it, including the direction that is
easy to lose — that SITE_BRAND is not itself one of the generic values the
package skips.
The point of truth is 2plot.ai's satellite-analytics
document, "Internal traffic": any
request whose User-Agent contains 2plot-internal is network machinery
talking to itself and is counted nowhere.
Inbound. lib/analytics_tracker.track_visit drops token-carrying requests
at write time, before detect_device_type. The ordering is the whole
point: a health sweep and a CI battery both look like bots, so classified
first they land in bot_hits and get reported to the hub as crawler interest
in these docs. /healthz and /health stopped being stored at all —
lib/traffic_rollup already filtered them on the way out, but a row that
exists and must be discounted is still a row somebody has to know about.
Outbound — the half that was missing here. Every call this host makes to
another network host now sends INTERNAL_UA:
lib/ad_client.py→2plot.dev, once per docs page view;lib/satellite_reporter.py→2plot.ai, hourly;lib/hub_client.py→2plot.dev, per agent-key verify and tier fetch;scripts/network_smoke.py,scripts/smoke_live.py,scripts/audit_links.py.
The ad client is the one that mattered. All of these were arriving as
python-requests/2.x, which matches the hub's own bot patterns — so this
satellite's readers were inflating 2plot.dev's bot_hits, once per page view,
and had been for as long as the ad slot has existed. The battery scripts keep
their Googlebot and Chrome tokens and append the internal one: the target
still exercises exactly the path under test, it just knows the caller is
machinery. The click beacon is the deliberate exception — a browser cannot set
a User-Agent, and a click is a real person.
tests/test_internal_traffic.py proves the exclusion reaches the numbers the
hub actually charts (human_hits / bot_hits in daily_rollup), proves the
positive case still counts (a rule that drops everything would satisfy the
negative assertions), and asserts the outbound header on all three clients and
all three scripts.
The same named checks against the CI container, against production after a
deploy, and in-process from tests/test_network_smoke.py, so a failure reads
identically wherever it happens. It proves identity (the /llms.txt H1 is the
brand, verbatim), the deployed artifact (the robots.txt crawler split, which
is the only fingerprint visible from outside — pip metadata is not), that no
owner-only surface leaks, that a crawler gets prose and not the JavaScript
stub, and that agents and browsers get different content types under a
Vary: Accept.
The in-process seat is not redundant: a script that only ever runs in CI and after a deploy is exactly the code that rots, where a typo turns a check into a silent pass. That test also breaks a check on purpose and requires the battery to report it.
.github/workflows/ci.yml is now a template file in its own right:
least-privilege permissions: contents: read, timeout-minutes on every job
(the default is six hours, which is how one hung curl burns a day of runner
minutes), docker/setup-buildx-action with a type=gha cache, and version
fingerprints asserted inside the built image rather than in the runner.
The container is booted and probed by the battery before anything is allowed
to merge. cd.yml runs the battery against the live host before
smoke_live.py.
tests/conftest.py now boots the app secretless, the way CI's container does:
every CLERK_*, CROSS_APP_WEBHOOK_SECRET and SESSION_SECRET is pinned to
"" before run.py is imported, because load_dotenv() runs during that
import and a developer's local .env would otherwise flip the app into a
configured posture and quietly invalidate every fail-closed assertion in
tests/test_access.py. The analytics ledger moves to a temp dir in the same
block — the suite had been appending its own hits to the repo's checked-out
visitor_analytics.json.
Added .github/dependabot.yml with a dash-network group (a package release
lands as one reviewable PR per repo, not five) and an advisory pip-audit
job.
dash-improve-my-llms>= 2.3.4 (from 2.3.2). The network standard;run.py's startup floor and CI's in-image fingerprint both assert it. There is no vendored copy of this package anywhere in the repo — the stale comments inDockerfile,render.yamlandREADME.mdthat still described one are gone.vendor/holdsdash_clerk_authalone.gunicorn>= 23.0.0 (from 21.2.0). 21.x carried two HTTP request-smuggling CVEs (CVE-2024-6827, CVE-2024-1135), both fixed in 23.0.markdown2dash0.1.2 declaresgunicorn>=21.2.0,<22.0.0— a markdown parser pinning a WSGI server — which pip cannot reconcile with that floor, so markdown2dash is installed with--no-depsand its real dependencies (docutils,jsonpath,mistune) are listed inrequirements.txtinstead. Every install path does the same two commands:requirements.txt,scripts/dev.sh, theDockerfile,render.yaml'sbuildCommand, CI, and the README. CI's in-image assert is what keeps the dodge honest.
Found by booting the image locally as part of verifying this release: the
Dockerfile ends in COPY . ., so a developer's .env was being baked into
the production image. The container died at boot with Could not import dash.backends._fastapi — the local file said DASH_BACKEND=fastapi and the
image has no FastAPI extra. It never appeared in CI, where the checkout has no
.env, which is precisely what made it worth a file rather than a lesson: the
same COPY would carry real Clerk keys and the webhook secret into an image
layer on any machine that has them. The ledger, session store, virtualenv and
node_modules are excluded too. docs/**/*.md deliberately is not —
those files are the app.
1.1.0 was declared in README.md and lib/constants.APP_VERSION but never
cut here; everything previously sitting under [Unreleased] ships as part of
1.2.0. templates/index.html's softwareVersion and APP_VERSION now agree,
which tests/test_config.py asserts.
Previously unreleased, now shipping as part of 1.2.0 — three threads of work:
the CI/CD system, network analytics reporting, and the upgrade to
dash-improve-my-llms 2.2.0.
2.1.0 was assigned during that package's development and never published, so there is no 2.1.0 anywhere and 2.0.0 upgrades straight to 2.2.0. Work described here as "2.1-era" in earlier drafts shipped as part of 2.2.0.
The four-host verification gate passed, dash-improve-my-llms published, and
this repo switched from the vendored sdist to the PyPI pin
(dash-improve-my-llms[flask]>=2.3.2) — the Phase-5 step the vendor block
always anticipated. vendor/dash_improve_my_llms-*.tar.gz is gone; CI's ASGI
legs and the Dockerfile install from PyPI too. vendor/ still carries
dash_clerk_auth (not on PyPI, deliberately outside requirements.txt).
The floor resolves to 2.3.3, which recategorises the Anthropic crawlers:
ClaudeBot — the actual training crawler — moves to Disallow, while the
user-triggered and search fetchers Claude-User / Claude-SearchBot are
allowed, matching the intent the OAI-SearchBot fix established for OpenAI.
It also strips unexpanded directive lines from resolved prose. The artifact
fingerprint in tests/test_llms_routes.py and scripts/smoke_live.py now
asserts the full crawler split, so a host running a stale build fails its
post-deploy battery by name.
Verifying that fingerprint exposed a real misconfiguration:
run.py set block_ai_training=False, so the training bucket was never
emitted and every training crawler was silently allowed — the opposite of the
"blocks AI training, allows AI search" policy this project documents, and it
would have made 2.3.3's ClaudeBot recategorisation invisible on this host.
Now block_ai_training=True, matching the documented policy and the rest of
the network.
Deployment prep for boilerplate.2plot.dev (rollout step 4; the hub's auth
endpoints are now live in production).
dash-improve-my-llms2.3.0 → 2.3.2 (vendored). The vendored 2.3.0 was a pre-fix build whose robots.txt disallowed OAI-SearchBot — ChatGPT search's crawler, exactly the audience these surfaces exist for. 2.3.2 allows it.User-agent: OAI-SearchBot→Allow: /in a live host's/robots.txtis the fingerprint that it runs the fixed artifact (pip metadata is invisible from outside);test_robots_artifact_fingerprintnow asserts it locally so a vendored regression fails CI, not production. 2.3.1 was assigned during development and never published.dash-clerk-auth0.9.0 → 0.9.1 (vendored, built from the Dash-Clerk-Auth-Hook working tree). 0.9.0 ships a bug hitting every Clerk satellite forked from this template: clerk-js v5 auto-instantiates from the script tag'sdata-*attributes and reads the instance domain, so on a satellite the user button never mounts (dead avatar) while server-side session verification keeps working. 0.9.1 emitsdata-clerk-domain="<satellite_domain>"on the tag whenis_satellite=True. This app runs no Clerk by design — the bump is for the template's sake.lib/auth.py's fixup #1 guards on the attribute's absence, so it degrades to a no-op under 0.9.1 and stays for forks still on 0.9.0.lib/hub_client.pyaligned with the hub's real contract. Two functions predated the hub going live.current_key()now sends{"token": <Clerk session token>, "app": ...}— the hub 401s any caller-asserted identity (user_idin the payload is the forgery path) and verifies the token against Clerk's JWKS, minting atscope=auth, never admin. Call it on copy-button click, never on page render;Nonedegrades to copying the plain URL.hub_tiers()is no longer a stub: signed POST/api/page-tiers{"app": ...}→{"tiers": {path: tier}, "ttl": s}, cached for the returned TTL with failures cached 60s — so a down hub costs one timeout per window, not one per request, and resolves to the local tier, which the ceiling rule guarantees can never loosen anything.verify()already matched the hub and is untouched.
lib/network_directory.py— the peer/affiliated/external directory, defined once here and copied verbatim into every satellite. Publishes<link rel="related">tags, a## Networksection in/llms.txt, and followed links in the prerendered body, so an agent landing on one satellite can enumerate the rest. Filters the app's own URL out ofpeers.- Wordmark —
"2"+ morse(plot) +"ai", drawn as columns of dots and dashes in the header of the renderedllms.txtview. No period glyph: the morse block already separates the halves, and a literal.beside it reads as punctuation dropped into a graphic. The renderer turns a suffix ending iniinto an upward flourish, so"ai"draws asaplus that mark, with the real domain inlabelfor screen readers and the SVG<title>. It lives in the shared module rather than per-app, which is what keeps one mark across the network instead of twelve near-identical ones. - Page
llms.txtdocuments are no longer dead ends. Each now opens with the site index, the network index one level up the hub chain (2plot.dev, correct for a*.2plot.devsubdomain), and the sitemap. These documents are usually read in isolation — pasted into a chat, handed to an agent — and an agent fetches a URL rather than crawling from one, so previously its exploration simply stopped there. - The same URL content-negotiates. Agents, crawlers and curl get the
Markdown byte for byte; browsers get it rendered behind a header carrying
the network identity.
?raw=1and?format=htmloverride, both variants sendVary: Accept, and the rendered view isnoindexso it never competes with the page it documents. Verified identical on Flask, FastAPI and Quart. docs/networks/networks.md— the guide for satellite authors: the three tiers, why per-host SEO can't express any of this, the wordmark and bulletin conventions, the one-URL-two-audiences contract, and the verification commands.- Network bulletin left deliberately unwired.
configure_bulletin()sits commented next toadd_llms_routeswith a pointer to the contract.2plot.devdoes not serve/api/network/bulletinyet, and pointing at a dead endpoint gains nothing: the client degrades silently and the header renders fine without it — the "Tips for getting started" and "What's new" panels use the package's built-in defaults, which a bulletin only overrides.
Opt-in, and off in a default clone. This is the template every *.2plot.dev
subdomain is forked from, so the goal was a pattern good enough to copy rather
than a one-off. Requires dash-improve-my-llms 2.3.0 (configure_access,
configure_viewer_identity) and the vendored dash-clerk-auth 0.9.0, which is
deliberately not on the active requirements line — a default install should
not pull in an auth stack the site does not use.
lib/auth.py— adapted from2plot_leaflet/lib/auth.py, the implementation already sharing authenticated state across2plot.ai→2plot.dev→leaflet.2plot.devin production. Keeps both satellite fixups fordash-clerk-auth0.9.0 (clerk-js readsdomainas a constructor option fromdata-clerk-domain, and a satellite mustredirectToSignIn()rather than open a modal that 403s), thepk_liveauto-enable so production cannot silently boot in primary mode,DISABLE_CLERK=1, and call-time env reads. Changed for the template: the satellite domain derives fromAPP_BASE_URL, which every deployment must set anyway — one variable rather than two, and one fewer way to announce another site's domain to Clerk.lib/page_tiers.py—public < auth < admin < hidden, declared in markdown frontmatter (tier: admin) because this template is already frontmatter-driven and marking one page should not require a control board. Two rules: everything excepthiddenfalls open when Clerk is unavailable (documentation must not brick over a missing credential), andeffective_tier = more_restrictive(local, hub)so a satellite may restrict further but never loosen.lib/hub_client.py— the client for the hub's/api/agent-key/currentand/api/agent-key/verify. Authenticates the caller with the network's existingCROSS_APP_WEBHOOK_SECRETHMAC scheme, the onelib/satellite_reporteralready uses: it authenticates who is asking and derives nothing, which is what keeps "satellites hold no key material" true while still keeping the verify endpoint from being an open key-guessing oracle. Verdicts cached on a SHA-256 fingerprint of the key rather than the key, because that cache is process memory a debugger or error reporter can dump.allowcached 900s,deny60s — a brief hub outage must not gate readers who were fine a minute ago, while a revoked key should stop working promptly.lib/access.py— the policy, and its ordering is the design: tier → local Clerk session → hub, only for?key=. A signed-in visitor resolves entirely on this host, so the hub being down gates nothing for them; only the agent path, which arrives with no cookie, needs the hub at all. Reversing it would couple every satellite's availability to one host for no benefit. Kept out ofrun.pyso satellites inherit one file.docs/authentication/— three layers, so a reader stops at the one they need: the default (nothing to do), a standalone site with its own Clerk, and joining or running a network. Names the two traps: the Clerk token'siatis the token's age, not the sign-in's, so wiring it renders a clock that resets every minute; and identity must never travel in the bulletin, which is TTL-cached and shared across every satellite.handoff/— kickoff prompts for the two repos this unblocks: an addendum pairing with thepip-docs+hub brief, carrying the request shapes and cache TTLs the client already sends, and a per-subdomain port guide.tests/test_access.py— 17 tests against a fake hub. The two that justify the design: signed-in browser with the hub unreachable still resolves toallow, and a valid key with the hub down degrades togatedrather than 500 or prose. One asserts the ordering rather than the outcome — a signed-in reader must trigger zero hub calls, since "allowed" could otherwise come from a hub that happened to agree.
Inert until a tier says otherwise. With the wiring in place, no Clerk keys, and every page public, all 43 surfaces are byte-identical to the build before any of it existed — measured, with a control run to strip out the per-request ids Dash puts in page HTML.
Vendored, as before; 2.3.0 is additive and opt-in. Verified as a no-op on the
surfaces that matter: every Markdown document, the root index, sitemap.xml,
robots.txt and the crawler HTML are byte-identical. The HTML viewer variants
grow by 192 bytes each — three CSS rules for the identity block that ship
whether or not identity is configured. Behaviourally a no-op; not literally
byte-identical everywhere, which is worth stating precisely since this baseline
is what a later regression gets attributed to.
-
.github/workflows/ci.yml— flake8 (blocking), then the full test suite across a matrix of Python version × backend × Dash version: Flask, FastAPI and Quart on Python 3.12, Python 3.11 and 3.13 on Flask, and the bottom of the~=4.4.1range pinned explicitly on Flask and FastAPI so a 4.4.0-only regression cannot hide behind pip resolving to 4.4.1. Asserts the resolved Dash anddash-improve-my-llmsversions before running anything, boots the app under gunicorn (a page can render under a test client and still fail under a real WSGI worker), and builds and probes the Docker image. -
.github/workflows/cd.yml— runs CI, POSTs theRENDER_DEPLOY_HOOK_URLsecret, waits for the new instance to be sustainably healthy (Render swaps instances rather than restarting in place, so a single 200 from/healthzproves nothing), then verifies the live site. Skips the deploy step when the secret is absent instead of failing, so a fork isn't red on day one. -
tests/— a pytest suite that bootsrun.pyitself rather than a test app.conftest.pynormalises the three backends' test clients behind one synchronous.get(), including driving Quart's async client from a fixture-owned event loop. Covers page registration and reachability, stub bodies, rendered prose, canonical tags, sitemap/robots/llms.txt, content negotiation in both directions, the navigation block, the banner and its panels, the network directory and wordmark, docs frontmatter and directive targets, heading anchors, and theBASE_URLguard. -
scripts/smoke_live.py— post-deploy checks against a live satellite, standard library only. Covers the failures that are silent in production: a canonical on the wrong host, a page serving the JavaScript stub, viewer chrome leaking into an agent's Markdown, a missingVary: Accept, a missing network directory, and dead peerllms.txtlinks. Run in CD and by hand (python scripts/smoke_live.py https://emojimart.2plot.dev), and itself tested against the in-process app so a typo can't turn every live check into a silent pass. -
scripts/dev.sh— starts the development server with this project's interpreter, resolved from the script's own location rather than from an IDE setting orPATH. -
scripts/audit_links.py— walks every page'sllms.txt, extracts every link, resolves internal paths in-process and checks the rest over the network. A dead link in anllms.txtis worse than one on a page: the agent holding that document has no navigation to fall back on and no way to tell a typo from a host that is down.Classified rather than lumped together, because the classes want different responses:
internalis a real defect,self-hostis correct once deployed,networkis a peer awaiting the rollout,unpushedis a file that exists locally and 404s only until the branch is pushed, andexternalis someone else's problem to route around. Code spans and fenced blocks are skipped — a URL inside backticks renders as<code>, not<a>— and a transport failure is retried once, because an audit that cries wolf gets ignored. -
LICENSE— the MIT text the README badge,pages/home.mdand the Schema.org block have all claimed since 0.1.0 without the file ever existing. -
render.yaml— Render Blueprint forboilerplate.2plot.dev: gunicorn,/healthzhealth check, custom domain, and a persistent disk for the analytics ledger (on an ephemeral filesystem a mid-day deploy wipes it and the next hourly report overwrites the day's real total). -
.flake8,pytest.ini.
lib/satellite_reporter.py— hourly signed rollup POSTed tohttps://2plot.ai/api/satellite/traffic, so a deployed docs site shows up on the hub's owner-only/trafficdashboard. HMAC-SHA256 over"{timestamp}." + bodywithCROSS_APP_WEBHOOK_SECRET, matching the network's existing webhook scheme. Off by default: no secret, no reporting. Re-posts yesterday during the first hours of a new day so the final hits of a day aren't left out, and uses a lease file so only one web worker reports per interval instead of every worker racing.python -m lib.satellite_reporter --dry-runprints the payload without sending it.lib/traffic_rollup.py— derives the reported numbers (human_hits,bot_hits,visitors,sessions,median_session_s, top pages, countries) using the hub's own definitions, so this app's figures are comparable with every other app on the chart. Infrastructure paths (/healthz,/llms.txt,/robots.txt,/sitemap.xml, assets, Dash internals) are excluded from the report but stay in the local ledger.lib/health.py—/healthzon Flask and Quart, matching the endpoint the FastAPI build already declared. The hub's hourly sweep probes it for up/down + latency, which previously only worked on one of the three backends.- Quart now tracks visitors too; previously only Flask and FastAPI did.
-
dash-improve-my-llms2.0.0 → 2.2.0, installed fromvendor/until it is published to PyPI. App 1 of 4 in a staged rollout, first because every satellite documentation site is forked from this repo — a convention set here propagates, and so does a mistake.Page metadata now merges instead of assigning, so no later bookkeeping call can erase a page's prose; the prerender reaches every visitor rather than only recognised crawlers; and the Markdown renderer emits real anchors, tables, code fences and rules. Measured on this app: link counts in crawler bodies went from 3 per page to 3–11, code fences from 0 to 5–29 per page, and horizontal rules stopped rendering as literal
---text. No page serves the crawler stub, before or after — this repo was never affected by the prose-erasure bug, having no bridge loop overdash.page_registry. -
Dash pinned to
~=4.4.1(was>=4.4.0). Verified matrix, from real apps on each backend with the failure reproduced on stock Dash:Dash Flask FastAPI Quart 4.1.0 ok n/a — no pluggable backends n/a 4.2.0 ok ok ok 4.3.0 ok broken — every non-root page 500s ok 4.4.0 ok ok ok 4.4.1 ok ok ok 4.3.0 added an early-return path guard to the ASGI middleware that returns before
set_current_request, while the page catch-all still callsget_current_request()— so it raisesRuntimeError: No active request in context. The catch-all is byte-identical between 4.2.0 and 4.3.0; only the middleware changed. 4.4.0 set the context inside the catch-all as well, so a future middleware guard cannot reintroduce it: 4.4.x is structurally safer, not merely currently-passing.~=4.4.1lets patch releases flow without twenty pull requests while blocking 4.5.0, so a minor bump goes through the matrix deliberately. Pinned for the most constrained backend network-wide, including Flask-only apps —DASH_BACKENDis an env var and this is a shared template, so a Flask deployment becomes a FastAPI deployment with one env change and no code change. -
Dependency floors are enforced at startup, not advised. A version below the floor stops the boot, names what would degrade, and prints
sys.executablealongside the expected interpreter.ALLOW_STALE_DEPS=1opts out for anyone deliberately testing an older release. The Dash floor is fatal only on FastAPI, where 4.3.0 is an outage rather than a degradation. See Fixed — environment and tooling for why this is a hard failure. -
network_directory.apply()gates thewordmarkargument on the installed signature. During a staged rollout this module reaches satellites before the new package does, and Python raisesTypeErroron an unknown keyword — so passing it unconditionally would turn an older satellite's boot into a crash rather than a missing graphic. Same techniquerun.pyuses for Dash'senable_mcp.
BASE_URLmoved tolib/constants.pyand readsAPP_BASE_URLfrom the environment, defaulting tohttps://boilerplate.2plot.dev.require_owned_base_url()refuses to boot in production whenAPP_BASE_URLis unset or points at a platform hostname (*.onrender.comand friends). This is the template's highest-consequence footgun: a fork that leaves the default in place emits the boilerplate's canonical URL on every one of its pages, which asks Google to deindex it, and nothing about the app looks broken while it happens.- YouTube links now point at @2plotai;
plotly.prois replaced by2plot.aithroughout, and the deployment host byboilerplate.2plot.dev. A test fails the build if a live link toplotly.proreappears. .claude/is untracked and gitignored. Local session workspace; noise in a template other people fork.- Dockerfile copies
vendor/before the pip layer (the build fails otherwise while the package installs from an sdist), declares aHEALTHCHECKagainst/healthz, and no longer leaves apt lists in the image.
- Every page shipped two
<link rel="canonical">tags.templates/index.htmlhard-coded one pointing at the site root while the package injected the correct per-page one. A conflicting pair is treated as no signal at all, so the per-page canonicals were doing nothing. The template no longer sets one. - Two advertised LLM endpoints were 404s.
<meta name="llms-page-json">andllms-architecturepointed at/page.jsonand/architecture.txt, both removed in dash-improve-my-llms 2.0. The<noscript>block linked to them too. - The Open Graph image never existed. Every share rendered a blank card
against
assets/og-image.png, a file not in the repo. Now points at a real asset with its actual declared dimensions. apple-touch-icon.pngandsite.webmanifest404'd on every page load — both<link>ed but neither shipped. Commented out with instructions.piratesbagain.comin the navbar (missingr) — a dead outbound link on every page.- Placeholder metadata left in the template:
"Your Organization Name","Your Name or Organization",yourdomain.com, and apriceof"29_000_000"in the SoftwareApplication schema (not a valid number, and the project is MIT-licensed).
templates/index.html hard-coded a <title> and contained no {%title%}
placeholder anywhere, so the per-page titles pages/markdown.py registers were
discarded and every page's title depended entirely on dash-improve-my-llms
rewriting that one element. LLMSConfig(prerender=False) — the documented
one-argument rollback — silently reverted every page on every satellite to one
identical string.
Now <title>{%title%}</title>, with app.title set from a new
constants.APP_TITLE. Without that second half the placeholder resolves to
Dash's default, the bare string "Dash", which is worse than what it replaced.
The trap, for anyone editing that block. The package finds the element with
re.compile(r"<title>.*?</title>", DOTALL | IGNORECASE) and rewrites the first
match:
- Delete the element and no closing tag remains to anchor on — nothing is rewritten and no page has a title at all.
- Spell the tag name in angle brackets inside a nearby comment and the match starts there instead, running to the next closing tag and replacing every line in between. The comment, and any markup after it, vanishes from the served page. With rewriting on it still looks correct, so the damage is only visible in the served bytes.
The comment above the element used to contain a literal <title> for exactly
this reason, and the first attempt at this fix reintroduced it while
explaining it. The block now describes the tags in words, and three tests pin
it: the placeholder is present, the title regex matches nothing but the element
itself, and no comment spells the tag in angle brackets. A fourth asserts every
page serves a distinct title.
Found by scripts/audit_links.py across all 10 documents and 102 links.
- The MIT
LICENSEfile did not exist.pages/home.mdand the README both linked to it, and the Schema.org block declared the licence — so the one link a reader follows to check the terms was the one that 404'd. Added. - The development-server port was wrong.
pages/home.mdsaidhttp://localhost:8553;run.pybinds 8559. The Docker instruction (8550) was right for the container but rendered as a live link that 404s for anyone not running the image — both are now code spans, so they read as instructions rather than as something to click. - The
SKILLS.mdlink pointed at the wrong path —dash-improve-my-llms/blob/main/SKILLS.md, but the file lives underdocs/. Fixed toblob/main/docs/SKILLS.md.
- A heading containing inline code crashed the site at startup.
markdown2dash's renderer does
create_heading_id(text[0]), and when the first inline token is formatted,text[0]is a component rather than a string —AttributeErrorat import, taking every page down. Fixed inlib/directives/headings.py. - TOC anchors pointed at ids that didn't exist. Even when it didn't crash,
the renderer slugged only the first inline token (
## Wiring **it** up→id="wiring") while thetocdirective slugged the raw markdown (wiring-**it**-up). Both now use oneslugify, so the link and its target agree. Plain headings slug exactly as before, so no existing anchor moved.
- The MCP server was never enabled.
run.pydidfrom dash import mcp_enabled, but the symbol lives indash.mcp— the import always raised, and the app printed "MCP not available in dash 4.4.1 (needs >=4.3)" while running 4.4.1.mcp_enabledis also the decorator for marking a function as an MCP tool, not a server switch. The server is started from Dash's constructor, soenable_mcp=/mcp_path=is now passed there, and it works on all three backends rather than only FastAPI. Passed as**kwargsso naming a 4.3+ keyword can't break the boot on an older Dash.
-
The app booted silently against another project's virtualenv. An IDE run configuration pointing elsewhere started this app against whatever versions that environment held — on
dash-improve-my-llms2.0.0 there is nollms_viewer.pyat all, so/<page>/llms.txtserved plain Markdown to every visitor and nothing in the log said why. It cost a debugging session across two repositories, chasing a stale process and a browser cache that were both innocent, and survived a server restart and an incognito window because neither was the variable.Made worse by this repo's own
enable_mcpfix, which removed theTypeErrorthat had been failing loudly on the wrong interpreter — trading a crash for a plausible wrong answer.Warnings were tried first and were not enough: they scroll past above a wall of page-loading output while the app keeps serving. The floors are now fatal (see Changed — dependencies), and
scripts/dev.shremoves the choice of interpreter entirely. A test asserts the same floor, sopytestin the wrong environment reports the cause instead of thirty downstream symptoms. -
CI installed a tarball path that no longer existed.
ci.ymlhardcoded the vendored filename for the FastAPI and Quart legs, so a version bump broke exactly two of the matrix entries. It now globsvendor/. -
Header lookups in the test client were case-sensitive. Werkzeug returns
Content-Type, httpx returnscontent-type, so the content-negotiation assertions passed on Flask and failed on FastAPI and Quart — reading like a backend bug when the served headers were identical and correct. -
A peer serving its SPA shell counted as a live document. The peer check asserted only
status == 200, but a Dash app answers its catch-all with the app shell for any unmatched path —2plot.dev/api/this-endpoint-cannot-existreturns200 text/html, as does/api/network/bulletin, which does not exist. A status-only check therefore passes against every host in the network whether or not it publishes anything.smoke_live.pynow rejects an HTML body for a document URL, and the same reasoning applies to the network-wide check inROLLOUT.md. -
smoke_live.pyextracted malformed peer URLs. Its pattern stopped only at whitespace and), and the 2.2.0 navigation block writes links as[https://host/llms.txt](https://host/llms.txt)— so it producedhttps://2plot.dev](https://2plot.dev/llms.txt, which would 404 in CD and fail a perfectly good deploy. Invisible locally, because the test shim answers 200 for off-host URLs. -
Viewer-chrome detection keyed on a bare class name.
docs/networkslegitimately documentsdv-banner, so a substring check failed on the page's own prose. Both the suite andsmoke_live.pynow match rendered markup (<div class="dv-banner"), which a Markdown document can never contain — otherwise the check quietly teaches people to stop documenting the viewer.
- AI-search crawlers were not being counted. The visitor hook was
registered after
add_llms_routes, and the package's bot middleware short-circuits ClaudeBot / ChatGPT-User / PerplexityBot with its own response — so those requests never reached the tracker. The hook is now registered first on Flask/Quart (and last on FastAPI, where Starlette runs the most recently added middleware outermost). - Every visitor looked like one visitor behind a proxy. The tracker used
remote_addr, which on Render/Cloudflare is the proxy. It now readsCF-Connecting-IP,True-Client-IP,X-Real-IPandX-Forwarded-Forfirst, and takes the country from Cloudflare'sCF-IPCountryheader when present — free, instant and accurate. - Concurrent workers overwrote each other's hits. The ledger was read,
modified and rewritten with no lock; under four workers most hits were lost.
Writes now take an
flockand land via an atomic replace. - Geolocation no longer blocks page views. The ip-api.com lookup ran inline
with a 2s timeout on the first hit from each new IP. It now runs in a bounded
background thread and is backfilled into the buffered hit before it is
written, so the country is still recorded. Disable with
ANALYTICS_GEO_LOOKUP=0. - The ledger is bounded and no longer rewritten on every request. Hits are
buffered (10 hits / 30s) and pruned to
ANALYTICS_RETENTION_DAYS(45) andANALYTICS_MAX_VISITS(20000); the hub holds the durable history. - The ledger path is now absolute (
TRAFFIC_ANALYTICS_FILE, else repo root) — a relative default wrote a different file depending on the working directory. - Tablets are no longer counted as mobile (iPads and most Android tablets send a mobile token too, and the mobile test ran first).
1.0.0 - 2026-06-14
First stable release. The boilerplate moves to Dash 4.x with pluggable backends and dash-improve-my-llms 2.0, and retires the experimental TOON format entirely. This is a significant architectural release — see the migration notes at the end of this section.
Versioning note: the
0.5.0–0.8.0entries below were the December 2025 TOON line. That work has been removed (see "Removed" below) and the project resumes a single, monotonic version line at1.0.0. A short-lived second0.5.0(the May 2026 dash-improve-my-llms 2.0 preview) has been folded into this entry.
lib/backend.py— single source of truth for backend selection. Reads theDASH_BACKENDenvironment variable (flask|fastapi|quart), falls back toflask, and exposesBackendInfo(label, color, icon, async flag) so UI components stay in sync with the running backend.run.pyconstructsDash(backend=resolve_backend(), ...)and attachesapp._backend_infofor layout components.components/backend_badge.py— a navbar/header badge that shows which backend the site is currently running on.lib/asgi_middleware.pyandlib/asgi_routes.py— ASGI middleware and showcase routes (/healthz,/api/backend,/api/pages) that light up on the FastAPI/Quart backends.- New documentation sections:
- Pluggable Backends (
docs/backends/) — run the site on any of the three backends with one env var. - Backend Deep Dive (
docs/backend-comparison/) — architecture, strengths/weaknesses, deployment, and best practices for each backend. - FastAPI Showcase (
docs/fastapi-showcase/) — OpenAPI docs, a native JSON API, ASGI middleware, async demo, endpoint explorer, and a stress test, showing what the ASGI backends unlock.
- Pluggable Backends (
LLMS_DOCpattern. Pages expose a module-level prose string (or callregister_page_metadata(path, llms_doc=...)); the package serves it verbatim at/<page>/llms.txtunder whichever backend is active.pages/markdown.pyregisters the expanded markdown body (with.. source::directives inlined) for every markdown-driven page.pages/home.pyexportsLLMS_DOC = contentfor the root prose.
- Multi-backend AI/LLM surfaces.
add_llms_routes(app)auto-detects the backend and serves/llms.txt,/<page>/llms.txt,/sitemap.xml, and/robots.txtunder Flask, FastAPI, and Quart alike — noif IS_FLASK:gate. - MCP resource bridge. Each page's prose registers as a
dash.mcpresource on Dash 4.3+ (a silent no-op on older Dash).
- Upgraded Dash 3.2.0 → 4.2.0 and Dash Mantine Components 2.4.0 → 2.7.0 (Mantine 8.3.6). React 18.2.0.
docs/ai-integration/ai-integration.mdfully rewritten for the 2.0 surface (LLMS_DOC, multi-backend, MCP bridge).requirements.txtnow pinsdash>=4.1.0,dash-mantine-components>=2.7.0, anddash-improve-my-llms[flask]>=2.0.0, with commented[fastapi],[quart], and[all]extras plusuvicornfor ASGI deployment.docs/example/example.md"Highlighting Important Elements" section rewritten around theLLMS_DOCpattern.components/header.py,components/appshell.py, andcomponents/navbar.pyupdated for the new backend badge and navigation (TOON Format and Handoff entries removed).lib/directives/llms_copy.py/assets/llms_copy.jsupdated for the 2.0/<page>/llms.txtrouting.APP_VERSIONandpackage.jsonbumped to1.0.0.
- The entire TOON format system —
lib/toon_generator.py(~1100 lines), thedocs/toon-format/page, the TOON Analytics Dashboard (docs/data-visualization/toon_dashboard.py), and all/llms.toonroutes.dash-improve-my-llms2.0 removed TOON from its public API (TOONConfig,toon_encode,generate_*_toonno longer exist). /page.jsonand/<page>/page.jsonroutes — dropped in dash-improve-my-llms 2.0; Dash 4.3 MCP exposes layouts as resources natively./architecture.txt— likewise superseded by MCP.mark_important()andmark_component_hidden()— now deprecated no-ops in 2.0. Write the emphasis directly into a page'sLLMS_DOCmarkdown.LLMS_INTEGRATION.mdand thedocs/handoff/doc (the FastAPI port plan that became 2.0) — superseded by the in-app AI Integration page.
- Backend: the site defaults to Flask, so no change is required. To run on
FastAPI or Quart, install the matching extra (
pip install "dash[fastapi]") and setDASH_BACKEND=fastapi. - AI/LLM prose: give each page module an
LLMS_DOC = """..."""string at module scope (orregister_page_metadata(path, llms_doc=...)when the prose is computed). The startupUserWarningfrom 2.0 names every page still missing prose. - dash-improve-my-llms extra: pick
[flask],[fastapi],[quart], or[all]inrequirements.txt. - Removed APIs: replace any
mark_important()/mark_component_hidden()calls (now no-ops) withLLMS_DOCcontent, and remove references to TOON,/page.json, and/architecture.txt.
0.8.0 - 2025-12-14
- TOON v3.3 Format Enhancements - Major comprehension improvements from ~75-80% to ~95%+
- New Dataclasses:
CodeTip- Short instructional code snippets with contextBestPractice- Numbered best practices with multi-line code examplesPattern- Architectural patterns with implementation codeResource- External resource links with full URLs
- New Extraction Functions:
extract_code_tips()- Finds short code snippets (2-15 lines) with headingsextract_best_practices()- Extracts numbered practices from "Best Practices" sectionsextract_patterns()- Captures pattern implementations from "Common Patterns" sectionsextract_resources()- Extracts markdown links with full URLs preserved
- New TOON Sections:
tips[N]{context,lang,code}:- Compact code tips with one-line previewsbestPractices[N]:- Full multi-line code snippets for each practicepatterns[N]:- Pattern descriptions with implementation code blocksresources[N]{name,url}:- External links without URL truncation
- New Dataclasses:
- Updated TOON format version from toon/3.2 to toon/3.3
- Enhanced summary line to include tips, best practices, patterns, and resources counts
- Improved content deduplication - Tips exclude Best Practices and Patterns sections to avoid duplicate code
- Code block detection in section boundaries - Headings inside code blocks (like
## My Visualizationin markdown examples) were incorrectly detected as section boundaries- Added code block range detection using
code_block_rangeslist - Added
is_in_code_block()helper to filter out false headings - Applied fix to
extract_code_tips(),extract_best_practices(), andextract_patterns()
- Added code block range detection using
re.escape()issue -re.escape("Best Practices")was escaping spaces incorrectly- Changed to custom escaping that only escapes regex special chars but preserves spaces
- Updated
lib/toon_generator.py(~1100 lines after updates) - Test results for Data Visualization page:
- 6 tips (properly deduplicated)
- 5 best practices (all with full multi-line code)
- 3 patterns (all with implementation code)
- 4 resources (with full URLs)
- TOON size: 11,444 chars
0.7.0 - 2025-12-13
- Custom Documentation-Aware TOON Generator (
lib/toon_generator.py)- Custom TOON route that processes raw markdown from
NAME_CONTENT_MAP - Achieves 54.7% token reduction vs llms.txt while preserving all content
- Full directive awareness (exec, source, kwargs, toc, llms_copy)
- Features:
- Section extraction with hierarchical structure (h2-h6)
- Directive parsing with option extraction
- Source file embedding with smart code compression
- Table and list preservation in compact format
- Exec component detection with callback markers
- Deduplication of code examples and directives
- Smart code compression (
compress_code()) that:- Preserves imports, function/class definitions
- Keeps callback decorators and Input/Output patterns
- Truncates long files with line count indicator
- TOON v3.2 format with optimized output:
- Compact section format:
[level] title - Grouped directives by type
- Inline table format with pipe separators
- Key lists extraction for substantial bullet points
- Compact section format:
- Custom TOON route that processes raw markdown from
- Custom
/<page>/llms.toonroute inrun.py- Overrides default dash-improve-my-llms TOON for markdown pages
- Uses raw markdown from NAME_CONTENT_MAP instead of rendered components
- Processes source directives to embed actual file content
- TOON content gap issue - Previous TOON was only capturing 15-20% of documentation content
- Root cause: dash-improve-my-llms extracts from rendered Dash components, losing directive context
- Solution: Custom route processes raw markdown with full directive awareness
- Previous TOON was 185% the size of llms.txt (27,669 chars vs 14,943 chars)
- New TOON is 45.3% the size of llms.txt (6,965 chars vs 15,369 chars)
- New module:
lib/toon_generator.py(698 lines)generate_documentation_toon()- Main entry pointbuild_documentation_toon()- TOON string builderextract_sections()- Hierarchical section parserextract_directives()- Directive extractor with optionsprocess_source_directive()- File content readerprocess_exec_directive()- Component metadata extractorcompress_code()- Smart code compressioncompress_section_content()- Content summarizationextract_tables()/extract_lists()- Structure extractors
0.6.0 - 2025-12-13
- Enhanced TOON Format v3.1 - Lossless semantic compression with 40-50% token reduction
- Application context with related pages and multi-page awareness
- Page purpose explanations with human-readable descriptions
- Component breakdown with type distribution
- Human-readable callback descriptions
- Synthesized page summaries
- Link categorization (internal vs external)
- Upgraded dash-improve-my-llms from v1.0.0 to v1.1.0
- Lossless semantic compression preserves all meaningful content
- New content extraction:
extract_markdown_content(),parse_markdown_content() - Smart compression:
compress_code_example(),compress_section_content() - New helper functions:
_generate_page_summary(),_format_callback_description()
preserve_code_examples=True- Include code snippets from markdownpreserve_headings=True- Keep section structurepreserve_markdown=True- Extract dcc.Markdown contentmax_code_lines=30- Max lines per code examplemax_sections=20- Max sections to includemax_content_items=100- Increased from 20
- Updated AI/LLM Integration Guide with v1.1.0 TOON enhancements
- Added design principle: lossless semantic compression
- Updated token efficiency comparison table
- Added 6 content gap examples (context, purpose, components, callbacks, summary, navigation)
- Updated TOONConfig with new v1.1.0 options
- Better content preservation in TOON format
- Optimal information density vs token reduction balance
- Enhanced developer experience with richer TOON output
0.5.0 - 2025-12-13
- TOON Format Support - Token-Oriented Object Notation for 50-60% fewer tokens
- New
/llms.toonendpoint for token-optimized LLM documentation - New
/architecture.toonendpoint for token-optimized architecture - New
/<page>/llms.toonper-page TOON format endpoints - TOON provides tabular arrays and explicit length markers for LLM validation
- Ideal for API calls, large apps, and cost-conscious deployments
- New
- Upgraded dash-improve-my-llms from v0.3.0 to v1.0.0
- Production-ready release with comprehensive test coverage (88 tests, 98% coverage)
- New API exports:
TOONConfig,toon_encode,generate_llms_toon,generate_architecture_toon - Zero-change migration: existing code works without modifications
- Updated AI/LLM Integration Guide with comprehensive TOON format documentation
- Added TOON format section with benefits comparison table
- Added example comparison (markdown vs TOON token usage)
- Added TOONConfig configuration examples
- Added programmatic TOON generation examples
- Updated available routes table with new TOON endpoints
- Updated key functions reference with new TOON imports
- Better AI/LLM documentation organization
- Enhanced developer experience with new format options
- Cost optimization through token-efficient TOON format
0.4.0 - 2025-11-10
- LLM Copy Button Directive (
.. llms_copy::)- New custom directive that adds a "Copy for llm 📋" button to documentation pages
- Copies the page's
/llms.txtURL to clipboard for easy AI assistant sharing - Users can paste the URL into ChatGPT, Claude, or other AI assistants for context-aware help
- Features:
- Automatic URL construction based on current page path
- Visual feedback with "✓ Copied! ✓" confirmation
- Fallback clipboard method for non-HTTPS contexts (HTTP development servers)
- Works across all modern browsers
- Tooltip: "Copy llms.txt URL for AI assistants"
- Implementation:
- Python directive:
lib/directives/llms_copy.py - JavaScript handler:
assets/llms_copy.js - Uses both modern Clipboard API and legacy
execCommandfallback - Mutation observer for Dash-rendered content detection
- Python directive:
- Documentation updated in Custom Directives guide
- Added to all 5 example documentation pages
0.3.0 - 2025-11-09
- Comprehensive Getting Started Guide (385+ lines)
- Detailed directive options documentation (
:code: false,:defaultExpanded,:withExpandedButton) - Interactive examples with best practices
- File structure examples and patterns
- Detailed directive options documentation (
- Custom Directives Guide (476 lines)
- Complete documentation for all 4 directives (toc, exec, source, kwargs)
- 3 live Python examples (button, counter, form validation)
- Data Visualization Guide (465+ lines)
- 5 chart type examples with full implementations
- Plotly template integration guide
- Real-time updates and dashboard patterns
- Interactive Components Guide (569 lines)
- 6 callback pattern examples
- State management, pattern matching, chained callbacks
- Loading states demonstration
- AI/LLM Integration Guide (577 lines)
- Complete dash-improve-my-llms documentation
- SEO optimization strategies
- Bot management and privacy controls
- DMC Figure Templates Integration
- All Plotly charts now use
dmc.add_figure_templates() - Theme-aware callbacks for 6 chart examples
- Charts dynamically update with light/dark theme toggle
- Proper background rendering in both themes
- All Plotly charts now use
- Code Block Theming
- Theme-aware CSS for markdown code blocks
- Proper syntax highlighting in light and dark modes
- Inline code and code block styling
- Comprehensive Theme Configuration
- Professional typography hierarchy (h1-h6)
- Systematic 4px-based spacing scale
- 5-level shadow system
- Consistent border radius system
- Global component defaults via theme.components
- Softer black (#1a1b1e) for better contrast
- Navigation Improvements
- Custom page ordering (Getting Started → Custom Directives → AI/LLM → Interactive → Visualization)
- Better visual hierarchy
- Organized documentation sections
- Typography System
- Inter font family across application
- Optimized line heights (md: 1.55 for body text)
- Proper font sizes (16px base)
- Font smoothing and text rendering optimization
- Layout Refinements
- Better responsive breakpoints (md for navbar)
- Improved spacing consistency
- Enhanced mobile experience
- Better heading spacing (1.5em top, 0.5em bottom)
- SEO-Ready HTML Template
- Comprehensive meta tags with developer guidance
- Open Graph and Twitter Card configuration
- Structured data (Schema.org) for Organization and SoftwareApplication
- Analytics integration (Google Analytics ready to enable)
- Favicon configuration with multiple formats
- Performance optimization (preconnect hints)
- Search engine verification placeholders
- Enhanced noscript fallback with styled content
- 297 lines of documentation and configuration
- 15 Working Python Examples
- Button interactions, counters, form validation
- 5 chart types (bar, line, scatter, realtime, dashboard)
- Callback patterns and state management
- All examples theme-aware and fully functional
- Directive System
- Fixed kwargs directive to parse component specifications (e.g.,
dmc.Button) - Better error handling and fallbacks
- Support for directive options
- Fixed kwargs directive to parse component specifications (e.g.,
- Code Quality
- Fixed JSON serialization error (removed lambda from theme styles)
- Better import statements
- Comprehensive inline comments
- Fixed DMC 2.4.0 compatibility issues
- Better Performance
- Optimized theme switching
- Smooth transitions
- Better font loading
- Documentation Organization
- Clear learning path
- Progressive complexity
- Better code examples
- Import errors in example files (missing dmc, State imports)
- DMC 2.4.0 compatibility (removed unsupported
typeprop from TextInput) - JSON serialization error in theme configuration
- Heading ID generation with code blocks in markdown
- Theme persistence and switching
- Code block rendering in dark mode
0.2.0 - 2025-11-09
- BREAKING: Migrated from Dash 2.5.0+ to Dash 3.2.0
- BREAKING: Migrated from dash-mantine-components 0.14.7 to 2.4.0
- BREAKING: Updated all Mantine packages from 7.14.1 to 8.3.6
- Updated Flask from 1.0.4+ to 3.1.2
- Updated Plotly from 5.0.0+ to 6.4.0
- Updated
app.run_server()toapp.run()(Dash 3.x standard)
- BREAKING: Removed deprecated package imports:
dash-html-components(now part of maindashpackage)dash-core-components(now part of maindashpackage)dash_table(now part of maindashpackage)
- Replaced deprecated
NotificationProviderwithNotificationContainer - Fixed Mantine version mismatch between package.json and DMC version
- Added node_modules to .gitignore
- Added package-lock.json for reproducible npm builds
- Comprehensive migration documentation (8 detailed guides)
- Project analysis and assessment documentation
- Persistent theme preference storage using localStorage
- Browser color scheme preference detection on first visit
- Smooth theme transitions without page flash
- AI/LLM & SEO Integration (dash-improve-my-llms v0.3.0)
- Automatic llms.txt, page.json, architecture.txt generation
- SEO-optimized sitemap.xml with intelligent priority
- Bot management (blocks AI training, allows AI search)
- Structured data for better search indexing
- Privacy controls for sensitive pages
- Better dependency management with cleaner requirements.txt
- Improved code organization with inline comments
- Enhanced theme management system
- Better performance with latest Dash and DMC versions
0.1.0 - 2024-11-30
- Initial release of Dash Documentation Boilerplate
- Markdown-driven documentation system
- Support for light and dark themes
- Responsive design for mobile and desktop
- Docker deployment support
- Interactive code examples with syntax highlighting
- Custom markdown directives:
toc- Table of contents generationexec- Executable Python code blockssource- Source code display with syntax highlightingkwargs- Component props documentation
- AppShell layout with header, navbar, and responsive drawer
- Search functionality for navigation
- Theme toggle with icon indicators
- Integration with dash-mantine-components (DMC)
- Integration with python-frontmatter for metadata
- Custom CSS styling system
- Docker and docker-compose configuration
- README with getting started guide
- Project structure documentation
- Example documentation pages
| Version | Date | Dash | DMC | Mantine | Python | Features |
|---|---|---|---|---|---|---|
| 1.0.0 | 2026-06-14 | 4.2.0 | 2.7.0 | 8.3.6 | 3.11+ | Pluggable backends (Flask/FastAPI/Quart), dash-improve-my-llms 2.0, TOON removed |
| 0.8.0 | 2025-12-14 | 3.2.0 | 2.4.0 | 8.3.6 | 3.11+ | TOON v3.3, tips/best practices/patterns/resources extraction |
| 0.7.0 | 2025-12-13 | 3.2.0 | 2.4.0 | 8.3.6 | 3.11+ | Custom TOON generator, documentation-aware TOON v3.2 |
| 0.6.0 | 2025-12-13 | 3.2.0 | 2.4.0 | 8.3.6 | 3.11+ | Enhanced TOON v3.1, dash-improve-my-llms v1.1.0 |
| 0.5.0 | 2025-12-13 | 3.2.0 | 2.4.0 | 8.3.6 | 3.11+ | TOON format, dash-improve-my-llms v1.0.0 |
| 0.4.0 | 2025-11-10 | 3.2.0 | 2.4.0 | 8.3.6 | 3.11+ | LLM Copy Button directive |
| 0.3.0 | 2025-11-09 | 3.2.0 | 2.4.0 | 8.3.6 | 3.11+ | Comprehensive docs, theme system, SEO |
| 0.2.0 | 2025-11-09 | 3.2.0 | 2.4.0 | 8.3.6 | 3.11+ | Migration to Dash 3.x, DMC 2.4.0, AI/LLM |
| 0.1.0 | 2024-11-30 | 2.5.0+ | 0.14.7 | 7.14.1 | 3.11+ | Initial release |
This is the major release that moves the boilerplate to Dash 4.x. See the Migration notes under 1.0.0 for the full checklist. In short:
- Backend: defaults to Flask — no change required. For FastAPI/Quart,
pip install "dash[fastapi]"(or[quart]) and setDASH_BACKEND=fastapi. - AI/LLM prose: add an
LLMS_DOCstring to each page module (or callregister_page_metadata(path, llms_doc=...)); the 2.0 startup warning lists pages still missing prose. - dash-improve-my-llms extra: pick
[flask]/[fastapi]/[quart]/[all]inrequirements.txt. - Removed APIs: drop any TOON usage (
TOONConfig,toon_encode,generate_*_toon),/page.json,/architecture.txt, and the now-no-opmark_important()/mark_component_hidden()calls — move emphasis intoLLMS_DOCinstead.
Zero changes required! The upgrade is fully backwards compatible.
Key changes:
- Update
dash-improve-my-llmsin requirements.txt to>=1.1.0 - TOON output now includes richer, lossless semantic content automatically
Optional new TOONConfig options:
from dash_improve_my_llms import TOONConfig
app._toon_config = TOONConfig(
# New in v1.1.0:
preserve_code_examples=True, # Include code snippets
preserve_headings=True, # Keep section structure
preserve_markdown=True, # Extract dcc.Markdown content
max_code_lines=30, # Max lines per code example
max_sections=20, # Max sections to include
max_content_items=100, # Increased from 20
)Zero changes required! The upgrade is fully backwards compatible.
Key changes:
- Update
dash-improve-my-llmsin requirements.txt to>=1.0.0 - New TOON endpoints are automatically available:
/llms.toon- Token-optimized LLM docs/architecture.toon- Token-optimized architecture/<page>/llms.toon- Per-page TOON format
Optional new features:
# Configure TOON output (optional)
from dash_improve_my_llms import TOONConfig
app._toon_config = TOONConfig(
indent=2,
delimiter=",",
include_metadata=True
)
# Programmatic TOON encoding (optional)
from dash_improve_my_llms import toon_encode
toon_string = toon_encode({"key": "value"})Minor updates, mostly additive. Key changes:
- Documentation content significantly expanded
- Chart examples now use DMC figure templates
- Enhanced SEO features in index.html
- Better theme integration across all components
Major breaking changes. See migration documentation:
- Quick Start:
MIGRATION_README.md - Detailed Guide:
claude.md - Step-by-Step:
MIGRATION_CHECKLIST.md - Code Changes:
CODE_CHANGES_SUMMARY.md
Key changes to be aware of:
- Update all imports from
dash_html_componentstofrom dash import html - Update all imports from
dash_core_componentstofrom dash import dcc - Replace
dmc.NotificationProvider()withdmc.NotificationContainer() - Update custom components to use DMC 2.4.0 API
- Check CSS for any Mantine 8 specific changes
- Issues: GitHub Issues
- Discussions: GitHub Discussions
- Dash Community: Plotly Community Forum