Version history of Dash Improve My LLMs, rendered from CHANGELOG.md.

Changelog

Version history of Dash Improve My LLMs. The timeline on this page is rendered from CHANGELOG.md, reproduced below.


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.

[llms-2plot-dev 1.6.0] - 2026-09-03

Sync 1.6.43, all three items.

Fixed

dropped requests carrying INTERNAL_UA_TOKEN since the network's internal-traffic contract existed; record_read — the on_document_read hook added with the 2.8.0 floor — never learned it. So the hub's health sweep, this site's own link audit and every post-deploy battery were writing reads rows, and were the busiest unidentified "vendor" on this host's board. Measured before the fix: one token-carrying probe wrote one row, vendor_key null, crawler lane. "Counted nowhere" now includes the read table. Reporting consequence: read counts DROP from this release, and the drop is the network's own probes leaving the numbers. The clean window for this host's ledger starts 2026-09-02 (ops' fleet ruling); earlier rows keep the inflation and are not being re-sent, because re-importing them would re-import exactly what this fix removes.

Changed

branch Render builds can be timed on a green push but is only PROVEN by a red one (this host has that proof, run 33337712632); a disagreeing lane IS the finding, and the browser lane spans three artifacts of which curl sees two; and assert a corpus is non-empty before trusting any negative, with four families of silent-green measured in this tree.

Notes

it — user_agent is the visits row's name and keying on it here would have been a silent no-op. Verified by reading EVENT_FIELDS out of the wheels rather than inferring: ua present at 2.8.0 (local), 2.9.0 (what production actually resolves) and 2.9.4 (what CI pulls from the >=2.8.0 floor); user_agent present at none. 2.9.4 adds vendor_class, which this fork picks up automatically because the row is built by iterating EVENT_FIELDS.

[llms-2plot-dev 1.5.0] - 2026-08-31

Sync item 18 — the 1.6.41 remainder, nineteen files as one contract.

Fixed

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.txt read "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.

that does.** network_smoke.HIDDEN_DOC_PATHS listed /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.

the crawler lane, so mark_hidden pages 404 and every-page-200 loops go red at a floor bump. tests/test_agent_key_route.py now names the browser lane and keeps the internal token.

Added

that scroll inside their own box.

— the longest name is 18 characters).

dated release heading. The sitemap honesty pin learns that second source rather than being loosened.

docs tree, that MUTATION-CHECK themselves. The expansion was disabled and every content pin confirmed red before being restored.

the compare API) instead of timing out, and refuses to push when a release/* branch would make release an unusable ref.

404 that no amount of "profile vs repo" framing catches.

Changed

seams this fork reported at 1.6.38 as template-side were closed at 1.6.41 (LOGO_ASSET et al., and a registry-derived aside pin), so DIVERGENCES.md 8 is retired. The byte-identity evidence recorded against 519d496 was stale and is superseded by a re-measurement against 4ac02e0.

component metadata, so all four empty-/api mechanisms are moot here.

[llms-2plot-dev 1.4.0] - 2026-08-30

Sync items 16 and 17 — the navigation contract and the battery's lane.

Added

sidebar and from the footer, and served as its own llms.txt.

Discord and YouTube, every icon labelled.

pages (/admin/control-board, /admin/traffic) were previously hidden from everyone including the owner by a blanket /admin/* path filter; they are now listed for is_admin_user() and for nobody else — the anonymous tree carries no /admin/ href at all.

network_directory.PRIMARY rather than a hand-typed list.

.. toc::, so /changelog and / render full width.

Changed

hand-typed link clusters in components/navbar.py. Every page already declared category:; the navbar simply ignored it. Each page now also declares order:. /audiences/web-crawlers is listed once — it appeared twice, as "Web Crawlers" under This package and as "A · What the crawler sees" under Showcase.

are gone** — the network is listed once, in the top bar's menu. Resources is third-party only: dmc. No community.plotly.com, no 2plot.dev.

so home's headings, tables and code fences match every docs page.

dcc.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.

scroll in their own box instead of widening the document; the rules are in main.css against public Mantine class names, never per page.

dash_improve_my_llms ships no component metadata — no metadata.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_PACKAGES is [].

(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), and CRAWLER_UA is untouched.

Fixed

hyperlinked /admin/control-board in their prose, which put an admin path in /llms.txt. Found by item 16's rewritten tests/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.

[llms-2plot-dev 1.3.0] - 2026-08-30

Round 3.4, the posture flip — consumed as sync item 15, with boilerplate.2plot.dev as the other canary.

Changed

run.py sets RobotsConfig(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 callable vendor_policy seam 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.

/ 403 → 200 (13,778 B crawler document) · /llms.txt 200 → 200 · /healthz 403 → 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.

(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 all Allow: /), and ClaudeBot/GPTBot/CCBot are now asserted NOT to carry Disallow: /.

where it asserted 403, and tests/test_showcase.py asserts 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.

/ 200 (14,133 B crawler document) · /llms.txt 200 (13,801 B) · /healthz 200, and /robots.txt carrying Allow: / 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. The DIVERGENCES.md posture fence is re-dated to 200/200/200.

[llms-2plot-dev 1.2.0] - 2026-08-29

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.

Added

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).

corpus document it serves (on_document_read) and does no I/O with it; run.py registers AnalyticsTracker.record_read once, which keeps the row as a reads table in the SAME analytics file — same buffer, lock, flush cadence and retention as visits, with client_ip dropped unless ANALYTICS_KEEP_CLIENT_IP=1. reads is JOINED by the rollup, never summed into human_hits / bot_hits / pages.

vendors[] (one row per vendor key × verified × policy, with per-tier counts and bytes, null key kept as the unverifiable bulk) and reads. Every v3 key is byte-identical; the reporter POSTs what daily_rollup returns and is unchanged.

job now fast-forwards release to the run's own sha after the matrix is green, and Render watches release (render.yaml). needs: [test] is the whole gate, so release cannot receive an uncertified commit by construction. tests/test_cd_promotes_release.py pins the structure.

ai_bots, healthz: full, runtime: python, deploy: release-branch.

Notes on the first promoted run

the owner step outstanding, not a defect.** 5c73a53 reached main at 17:43:00Z and release at 17:45:24Z; at 18:22:44Z /healthz still served ae1dce6. This service is not Blueprint-managed, so render.yaml's branch: 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.sha step. main == release == wire.

skipped rather than run. Under the previous always() && != 'cancelled' && != 'skipped' gate it would have run after the failed deploy and reported GREEN against ae1dce6 — the previous build. That is the defect the gate change exists to prevent, observed here on the first run that could exhibit it.

Changed

and library clients (httpx, Go-http-client, node-fetch, an empty User-Agent) move from the human lane to the crawler lane, because classify() 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.

is_bot and detect_bot_type to dash_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 retired anthropic-ai / claude-web tokens, and knew nothing of bytespider or Claude-User. The names and signatures are kept for callers. Crawler rows gain vendor_key, vendor_class, verified, lane; human rows are byte-identical to before, and the INTERNAL_UA_TOKEN drop still happens FIRST.

requirements.txt (four lines), run.py's LLMS_PKG_FLOOR, and CI's install line plus both version asserts. Not a degradable feature — lib/analytics_tracker imports classify and _ledger.EVENT_FIELDS at module scope.

always() && != 'cancelled' && != 'skipped' admitted failure, so a failed promote could still be followed by a green verify of the PREVIOUS build. Verify's first step now also asserts /healthz build == this run's sha itself. This fork's SITE_URL guard (DIVERGENCES.md 4) is kept as a second conjunct.

Removed

cd.yml. A push to main is no longer a deploy; it is a candidate.

Fixed

an absent User-Agent is the crawler lane, so the UA-less probe received the crawler document — which carries no twitter: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.

[llms-2plot-dev 1.1.0] - 2026-08-27

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.

Added

above the network's behavioral contract and verification traps, both ported verbatim), settings.json pointed at THIS host, and the three shipped skills (wire-verify, sync-template, report) byte-verbatim. .gitignore moves 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.

machine-readable byte-owned fence (empty: this fork makes no byte-level claim on any sync-verbatim path, and drift is never fenced).

kit cargo. tests/test_python_version.py — the one-fleet-Python agreement pins.

python_matches_declared check: the serving interpreter is now on the wire, so a stale image can be contradicted from outside.

lib.auth.register() with a FAKE non-empty Clerk config — the branch a zero-secret suite had never executed, including the pk_live satellite-mode auto-enable that can only exist in production.

.State.Health.Status), failing on none`: the external curl proves the app answers, never the HEALTHCHECK instruction itself.

Changed

(python:3.14-slim, a MINOR tag: the old 3.11.8 patch 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, and render.yaml's PYTHON_VERSION (full X.Y.Z, as Render's native runtime requires). The window legs are 3.13 and 3.12.

point of use (${PORT:-8550}), and the HEALTHCHECK probes the same variable. Exec-form CMD never expands env, so the old form hardcoded the port whatever the platform asked for.

boilerplate — service name, domain, APP_BASE_URL, SATELLITE_APP_KEY, AD_APP_ID — inherited at fork time and never corrected, while production served llms.2plot.dev with app: "llms". POLICY_STORE_FILE is now declared on the mounted disk beside the other two stores.

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 ::warning rather than a quiet notice, and the verify job now stands down on a skipped deploy as well as a cancelled one.

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.

now carries the same SSL context as fetch (without it every POST died in the macOS handshake and read as missing auth wiring), and wake() tolerates a legacy fetch stub. A source pin in tests/test_auth_wiring.py holds the SSL half — no wired test can, they all monkeypatch post.

gone from the demo-card copy; nothing here wires one.

inherited /examples/visualization entry 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.

Removed

upstream in 1.6.9 and carried here with this Dockerfile sync). package.json / package-lock.json were dash-mantine-components' component-build toolchain, inherited through the fork lineage and used by nothing in this repo — no webpack config, no src/ts, no CI job, no served asset — while the image apt-installed nodejs+npm and npm installed a known-vulnerable jsonpath@1.1.1 into every production build.

[llms-2plot-dev 1.0.0] - 2026-08-22

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.

Added

/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.

validated on write, atomic on replace, fail-open on read, re-stat'd on every call. Reaches dash-improve-my-llms 2.7.0 through its callable seams, so a control-board toggle lands on the next request in every worker with no restart.

over the inherited page-visibility board, with the admin gate re-checked server-side in the write callback.

Changed

the hook mark, header logo and wordmark, the GitHub link, the social-card object, and the JSON-LD blocks.

excluded_links hid the eleven tutorial pages from the sidebar but left them in sitemap.xml, /llms.txt, /llms-full.txt and 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 touching docs/ now need resolving by hand, and that cost was accepted for a site that stands on its own.

Notes

unpublished, so every 2.7.0 call site sits behind the LLMS_HAS_27 capability probe in run.py and the app boots on either release.


[1.6.7] - 2026-08-22

Added

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/session answered 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 and configure_app no-ops without keys. Two guards now, one per environment: tests/test_auth_wiring.py pins structurally (AST) that run.py calls BOTH halves; scripts/smoke_live.py gains 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.

[1.6.6] - 2026-08-22

Changed

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 hidden attribute, 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 without hidden, 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.

[1.6.5] - 2026-08-22

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.

Added

modelviewer finding): a fork died in production on a function-local import PIL that 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-time scripts/.

finding, landed at the source): fails on any hashed .m_* Mantine selector in assets/*.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.

(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.

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.

Fixed

(dmc-docs fork era, present since the initial commit): .m_46b77525 put an !important margin on every Input wrapper in every docs example; .m_5caae85b was dead in DMC 2.7 and 2.8; .m_9cdde9a restated Mantine's own aside declarations around one intentful pixel — the TOC's 15px breathing gap, which moved to the static aside.mantine-AppShell-aside rule.

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 own apple-touch-icon.png is 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.

preserved** — the pattern both modelviewer and excalidraw needed and implemented divergently. visibleFrom keeps the node in the DOM (the typing animation still finds it) but display:none DOES remove it from the accessibility tree, so the home link now carries a permanent aria-label and 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.

[1.6.4] - 2026-08-21

Two fleet-class fixes surfaced by the wave's first pair, landed at the source so the other eighteen forks inherit them.

Fixed

(the muicharts finding): with RENDER_DEPLOY_HOOK_URL unset, the old workflow skipped the wait and ran the live battery seconds after the push — against the previous release, every run, invisibly. /healthz now 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.

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.py now claims the identity via os.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.

[1.6.3] - 2026-08-21

Changed

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_identity minting. 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.

[1.6.2] - 2026-08-21

The pre-wave hygiene pass, from the four-repo review.

Fixed

provenance stamps read 2026-08-22 for events whose verified date is 2026-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.

control-board override layer (override → frontmatter → PAGE_DEFAULT_TIER, hub ceiling on top) instead of contradicting shipped behavior; lastmod bumped accordingly.

loses first — silently swallowed lastmod, the lying sitemap — matching the comment that raised the floor.

(upstream fix shipped in 1.0.3/1.0.4; the shim is a deliberate duplicate until the fleet-wide retirement pass).

Changed

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.

ADMIN_USER_IDS, ALLOW_UNGATED_ADMIN) — the gate for the 1.6.0 headline feature was previously undiscoverable from the env template.

the override store, and the auth-demo teasers.

[1.6.1] - 2026-08-21

Fixed

(hamburger, theme toggle, GitHub link — create_link now requires a label), and the network-ad image reserves a square box via aspect-ratio so 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.

[1.6.0] - 2026-08-21

Every fork gets its own live control board — the leaflet pilot's proven UX, ported with its scar tissue included.

Added

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=1 for 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 via mark_hidden() (sitemap, llms.txt, MCP, prerender, crawler HTML all treat it as absent) so access.gating_configured() stays False on all-public forks and the hot path stays check-free.

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 when PAGE_VISIBILITY_FILE is unset OR points under /var/ without a real mount — the twice-observed silent-reset-per-deploy class).

frontmatter → env default, with the hub ceiling still applied on top (an override can loosen a local declaration, never a network restriction). pages/markdown.py registers every docs page on both ledgers from the one declared value.

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."

/var/data disk, with the blueprint-vs-dashboard drift warning inline. 14 new tests (tests/test_control_board.py).

[1.5.4] - 2026-08-21

Changed

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.

[1.5.3] - 2026-08-21

Changed

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. revokeServerSession also 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.

[1.5.2] - 2026-08-21

Changed

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-absolute satellite_sign_in_redirect values loudly. 1.5.1's app-side signout shim is idempotent alongside it and retires next release.

repo's dist/ artifact ONLY — its main currently 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.

[1.5.1] - 2026-08-21

Pilot-week hotfix: Sign Out that actually signs out, and an honest floor comment.

Fixed

logout runs window.Clerk.signOut() client-side and reloads — but the server keeps trusting the signed __dca_identity cookie (max-age session_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. New lib/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 sequences Clerk.signOut() FIRST (so the slow path can't re-verify __session and 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.

Changed

claim that passing lastmod= "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.

[1.5.0] - 2026-08-20

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.

Changed

LLMS_PKG_FLOOR = (2, 6, 0). What arrives: icon autodiscovery, truthful sitemap <lastmod>, JSON-LD publisher.logo, and the llms.txt viewer banner de-dup (package-side, free).

last-commit date (2025-11-09 through 2026-08-19 — eleven pages, zero invented dates). The Meta model gains the field with a YAML-date-to-ISO validator; register_page_metadata passes 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.

assets/favicon/favicon.ico copy (byte-identical to the root one index.html links) so the declared list is SET-equal to what 2.6.0's discovery finds.

Added

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).

[1.4.1] - 2026-08-19

Changed

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 on sys.path. Runtime posture is unchanged: with no CLERK_* 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,<8 and cryptography>=50.0.0 (the four-advisory baseline dash-clerk-auth 1.0.1 widened its cap for).

[1.4.0] - 2026-08-19

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.

Added

renders through a per-request verdict — sign-in card at HTTP 200 (with an optional live teaser demo via lib/auth_demos.py, table empty in the template), forbidden and 404 cards, the content on allow. The verdict is the new access.resolve_page_access(): docs fall open without Clerk, admin fails closed, and ?key= never unlocks a browser layout. The gate switch is PAGE_DEFAULT_TIER=auth per deployment; /, /getting-started and the corpus pseudo-paths are pinned public so no env flip can gate the funnel. Card buttons ride assets/auth_gate.js/.css (satellite mode navigates to the primary with ?returnTo=; local dev opens the Clerk modal).

LLMS_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 is LLMS_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.

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-store always; the token is read from the __session cookie, never the query.

fail-silent daemon thread POSTs {app, active} to the hub's /api/satellite/active every 60s (SATELLITE_PRESENCE_INTERVAL_S, floor 30, 0 disables) — 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.

rendered only when Clerk is configured.

Changed

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.

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, preferring buildSatelliteRedirect() with ?returnTo=, falling back to redirectToSignIn on origin+pathname so stale __clerk_* params never ride into the next sign-in.

public explicitly instead of falling through the tier default, so PAGE_DEFAULT_TIER can never gate them; / likewise (it registers via pages/home.py, which no frontmatter ever tiers).

cap for the cryptography>=50 floor, plus the avatar session fix).

Fixed

bare-host matching flags a site's own links whenever a peer host is a substring of its own (2plot.devleaflet.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.

from __future__ import annotations: PEP 563 turns the FastAPI Request annotation 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.

[1.3.0] - 2026-08-15

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).

Added

v3 analytics fields): unique bot visitors per day (bot_visitors, a daily distinct count), and llms.txt / robots / sitemap / page.json rows in pages with 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.

— 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 distinct bot_visitors count. 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.

from this template now declares access tiers for /llms-small.txt and /llms-full.txt (served by dash-improve-my-llms ≥ 2.4.0; inert on older versions): LLMS_SMALL_TIER / LLMS_FULL_TIER env vars set them locally (unset = public; documented in .env.example and visible in render.yaml so 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.

lib/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.txt and every /<page>/llms.txt on 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.

Fixed

/llms.txt, /robots and /sitemap from page visits by substring — but /llms.txt does not substring-match /llms-small.txt, so the tier documents and page.json twins landed in BOTH load_visits and load_agent_hits, inflating human_hits/bot_hits/pages for exactly the surfaces the 402 board prices. _SKIP now names all three. The hub's traffic_insights._SKIP has the same gap (its comment claims the exclusion; its tuple doesn't deliver it) — port this fix there before the data window opens.

numpy-docstring branch in lib/directives/kwargs.py (for dash-mantine-components' hand-written docs) shadowed the base markdown2dash parser for the Keyword 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.

[1.2.5] - 2026-08-01

Fixed — scripts/smoke_live.py failed CD on healthy sites

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):

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 means ok: 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.

already 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.

[1.2.4] - 2026-08-01

Fixed — the network bulletin was never wired up

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:

lib.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 as boilerplate would receive this template's news and be miscounted. One notion of "which satellite am I", not two that can disagree.

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.

Changed — one identifier for this app on every hub surface

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.

Added — .env.example

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.

[1.2.3] - 2026-08-01

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.

Added — scripts/make_social_card.py

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.

Changed — og:image moved to the CDN

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.

Added — the live card check that no offline test can make

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.

Changed — the two peer tests narrowed to peers

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.

Changed — build/ and kickoff/ are gitignored

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.

[1.2.2] - 2026-08-01

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.

Fixed — the 1.2.1 files were never committed

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.

Fixed — the fork source's brand on every share card

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.

Fixed — twitter:url advertised http:// (lib/proxy.py)

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:

X-Forwarded-Host; BASE_URL is 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.

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.

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.

Flask/FastAPI/Quart instance that gunicorn imports as run:server and that run.py hangs before_request off. 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.

Added — client-side URL sync on SPA navigation

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.

Changed — test_exactly_one_canonical_tag_for_browsers counts elements

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.

Still open — the card image is not on the CDN

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.

[1.2.1] - 2026-07-31

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.

Fixed

declared og:image / twitter:image statically while Dash also emits both per page. With no image_url= passed, Dash inferred an image from the assets folder, found assets/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_URL is now passed to register_page, and the template keeps only the auxiliaries Dash omits.

og:title, og:description, twitter:card, twitter:url, twitter:title, twitter:description, twitter:image were 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_statically pins the rule.

passed one, so Dash emitted description, og:description and twitter:description as content="" on the most-linked page on the site.

the apple-touch-icon were commented out behind a note saying the files were missing — a note that outlived their arrival in assets/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 its theme_color reconciled with the theme-color meta tag.

Added

exactly once, is absolute, is not an SVG and resolves; that the manifest is linked, served, correctly named and has resolving icons; and that templates/index.html is 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.

values a fork changes.

[1.2.0] - 2026-07-31

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.

Added — explicit site identity (lib/constants.SITE_BRAND)

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.

Added — the internal-traffic contract, both halves

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:

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.

Added — scripts/network_smoke.py, run in three seats

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.

Changed — CI on the network baseline

.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.

Changed — dependency floors

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 in Dockerfile, render.yaml and README.md that still described one are gone. vendor/ holds dash_clerk_auth alone.

request-smuggling CVEs (CVE-2024-6827, CVE-2024-1135), both fixed in 23.0. markdown2dash 0.1.2 declares gunicorn>=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-deps and its real dependencies (docutils, jsonpath, mistune) are listed in requirements.txt instead. Every install path does the same two commands: requirements.txt, scripts/dev.sh, the Dockerfile, render.yaml's buildCommand, CI, and the README. CI's in-image assert is what keeps the dodge honest.

Added — .dockerignore

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.

Note on versioning

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.

Changed — dash-improve-my-llms from PyPI (2.3.3); vendored copy removed

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.

Changed — production rollout: re-vendor 2.3.2 / 0.9.1, live hub contract

Deployment prep for boilerplate.2plot.dev (rollout step 4; the hub's auth endpoints are now live in production).

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-SearchBotAllow: / in a live host's /robots.txt is the fingerprint that it runs the fixed artifact (pip metadata is invisible from outside); test_robots_artifact_fingerprint now asserts it locally so a vendored regression fails CI, not production. 2.3.1 was assigned during development and never published.

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's data-* 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 emits data-clerk-domain="<satellite_domain>" on the tag when is_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.

predated the hub going live. current_key() now sends {"token": <Clerk session token>, "app": ...} — the hub 401s any caller-asserted identity (user_id in the payload is the forgery path) and verifies the token against Clerk's JWKS, minting at scope=auth, never admin. Call it on copy-button click, never on page render; None degrades 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.

Added — AI/LLM surfaces (dash-improve-my-llms 2.2.0)

defined once here and copied verbatim into every satellite. Publishes <link rel="related"> tags, a ## Network section 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 of peers.

dashes in the header of the rendered llms.txt view. 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 in i into an upward flourish, so "ai" draws as a plus that mark, with the real domain in label for 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.

the site index, the network index one level up the hub chain (2plot.dev, correct for a *.2plot.dev subdomain), 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.

Markdown byte for byte; browsers get it rendered behind a header carrying the network identity. ?raw=1 and ?format=html override, both variants send Vary: Accept, and the rendered view is noindex so it never competes with the page it documents. Verified identical on Flask, FastAPI and Quart.

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.

commented next to add_llms_routes with a pointer to the contract. 2plot.dev does not serve /api/network/bulletin yet, 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.

Added — Clerk authentication and llms.txt access control

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.

implementation already sharing authenticated state across 2plot.ai2plot.devleaflet.2plot.dev in production. Keeps both satellite fixups for dash-clerk-auth 0.9.0 (clerk-js reads domain as a constructor option from data-clerk-domain, and a satellite must redirectToSignIn() rather than open a modal that 403s), the pk_live auto-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 from APP_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.

markdown frontmatter (tier: admin) because this template is already frontmatter-driven and marking one page should not require a control board. Two rules: everything except hidden falls open when Clerk is unavailable (documentation must not brick over a missing credential), and effective_tier = more_restrictive(local, hub) so a satellite may restrict further but never loosen.

and /api/agent-key/verify. Authenticates the caller with the network's existing CROSS_APP_WEBHOOK_SECRET HMAC scheme, the one lib/satellite_reporter already 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. allow cached 900s, deny 60s — a brief hub outage must not gate readers who were fine a minute ago, while a revoked key should stop working promptly.

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 of run.py so satellites inherit one file.

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's iat is 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.

pairing with the pip-docs+ hub brief, carrying the request shapes and cache TTLs the client already sends, and a per-subdomain port guide.

justify the design: signed-in browser with the hub unreachable still resolves to allow, and a valid key with the hub down degrades to gated rather 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.

Changed — dash-improve-my-llms 2.2.0 → 2.3.0

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.

Added — CI/CD and tests

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.1 range 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 and dash-improve-my-llms versions 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.

secret, waits for the new instance to be sustainably healthy (Render swaps instances rather than restarting in place, so a single 200 from /healthz proves 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.

app. conftest.py normalises 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 the BASE_URL guard.

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 missing Vary: Accept, a missing network directory, and dead peer llms.txt links. 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.

interpreter, resolved from the script's own location rather than from an IDE setting or PATH.

link, resolves internal paths in-process and checks the rest over the network. A dead link in an llms.txt is 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: internal is a real defect, self-host is correct once deployed, network is a peer awaiting the rollout, unpushed is a file that exists locally and 404s only until the branch is pushed, and external is 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.

Schema.org block have all claimed since 0.1.0 without the file ever existing.

/healthz health 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).

Added — Network analytics reporting to 2plot.ai

https://2plot.ai/api/satellite/traffic, so a deployed docs site shows up on the hub's owner-only /traffic dashboard. HMAC-SHA256 over "{timestamp}." + body with CROSS_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-run prints the payload without sending it.

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.

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.

Changed — dependencies

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 over dash.page_registry.

on each backend with the failure reproduced on stock Dash:

DashFlaskFastAPIQuart
4.1.0okn/a — no pluggable backendsn/a
4.2.0okokok
4.3.0okbroken — every non-root page 500sok
4.4.0okokok
4.4.1okokok

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 calls get_current_request() — so it raises RuntimeError: 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.1 lets 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 appsDASH_BACKEND is 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.

the floor stops the boot, names what would degrade, and prints sys.executable alongside the expected interpreter. ALLOW_STALE_DEPS=1 opts 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.

installed signature. During a staged rollout this module reaches satellites before the new package does, and Python raises TypeError on an unknown keyword — so passing it unconditionally would turn an older satellite's boot into a crash rather than a missing graphic. Same technique run.py uses for Dash's enable_mcp.

Changed — hosts, branding and repo hygiene

environment, defaulting to https://boilerplate.2plot.dev. require_owned_base_url() refuses to boot in production when APP_BASE_URL is unset or points at a platform hostname (*.onrender.com and 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.

plotly.pro is replaced by 2plot.ai throughout, and the deployment host by boilerplate.2plot.dev. A test fails the build if a live link to plotly.pro reappears.

a template other people fork.

otherwise while the package installs from an sdist), declares a HEALTHCHECK against /healthz, and no longer leaves apt lists in the image.

Fixed — SEO and template

hard-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.

and llms-architecture pointed at /page.json and /architecture.txt, both removed in dash-improve-my-llms 2.0. The <noscript> block linked to them too.

against assets/og-image.png, a file not in the repo. Now points at a real asset with its actual declared dimensions.

both <link>ed but neither shipped. Commented out with instructions.

on every page.

"Your Name or Organization", yourdomain.com, and a price of "29_000_000" in the SoftwareApplication schema (not a valid number, and the project is MIT-licensed).

Fixed — every page shipped the same hard-coded title

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:

rewritten and no page has a title at all.

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.

Fixed — dead links in the llms.txt documents

Found by scripts/audit_links.py across all 10 documents and 102 links.

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.

http://localhost:8553; run.py binds 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.

dash-improve-my-llms/blob/main/SKILLS.md, but the file lives under docs/. Fixed to blob/main/docs/SKILLS.md.

Fixed — Markdown rendering

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 — AttributeError at import, taking every page down. Fixed in lib/directives/headings.py.

the renderer slugged only the first inline token (## Wiring **it** upid="wiring") while the toc directive slugged the raw markdown (wiring-**it**-up). Both now use one slugify, so the link and its target agree. Plain headings slug exactly as before, so no existing anchor moved.

Fixed — MCP wiring

from dash import mcp_enabled, but the symbol lives in dash.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_enabled is also the decorator for marking a function as an MCP tool, not a server switch. The server is started from Dash's constructor, so enable_mcp= / mcp_path= is now passed there, and it works on all three backends rather than only FastAPI. Passed as **kwargs so naming a 4.3+ keyword can't break the boot on an older Dash.

Fixed — environment and tooling

configuration pointing elsewhere started this app against whatever versions that environment held — on dash-improve-my-llms 2.0.0 there is no llms_viewer.py at all, so /<page>/llms.txt served 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_mcp fix, which removed the TypeError that 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.sh removes the choice of interpreter entirely. A test asserts the same floor, so pytest in the wrong environment reports the cause instead of thirty downstream symptoms.

the vendored filename for the FastAPI and Quart legs, so a version bump broke exactly two of the matrix entries. It now globs vendor/.

Content-Type, httpx returns content-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.

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-exist returns 200 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.py now rejects an HTML body for a document URL, and the same reasoning applies to the network-wide check in ROLLOUT.md.

at whitespace and ), and the 2.2.0 navigation block writes links as [https://host/llms.txt](https://host/llms.txt) — so it produced https://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.

legitimately documents dv-banner, so a substring check failed on the page's own prose. Both the suite and smoke_live.py now 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.

Fixed — Analytics accuracy

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).

remote_addr, which on Render/Cloudflare is the proxy. It now reads CF-Connecting-IP, True-Client-IP, X-Real-IP and X-Forwarded-For first, and takes the country from Cloudflare's CF-IPCountry header when present — free, instant and accurate.

modified and rewritten with no lock; under four workers most hits were lost. Writes now take an flock and land via an atomic replace.

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.

buffered (10 hits / 30s) and pruned to ANALYTICS_RETENTION_DAYS (45) and ANALYTICS_MAX_VISITS (20000); the hub holds the durable history.

a relative default wrote a different file depending on the working directory.

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.00.8.0 entries below were the December 2025

TOON line. That work has been removed (see "Removed" below) and the project

resumes a single, monotonic version line at 1.0.0. A short-lived second

0.5.0 (the May 2026 dash-improve-my-llms 2.0 preview) has been folded into

this entry.

Added — Pluggable backends (Flask / FastAPI / Quart)

the DASH_BACKEND environment variable (flask | fastapi | quart), falls back to flask, and exposes BackendInfo (label, color, icon, async flag) so UI components stay in sync with the running backend.

app._backend_info for layout components.

backend the site is currently running on.

and showcase routes (/healthz, /api/backend, /api/pages) that light up on the FastAPI/Quart backends.

three backends with one env var.

strengths/weaknesses, deployment, and best practices for each backend.

JSON API, ASGI middleware, async demo, endpoint explorer, and a stress test, showing what the ASGI backends unlock.

Added — AI/LLM integration via dash-improve-my-llms 2.0

register_page_metadata(path, llms_doc=...)); the package serves it verbatim at /<page>/llms.txt under whichever backend is active.

.. source:: directives inlined) for every markdown-driven page.

backend and serves /llms.txt, /<page>/llms.txt, /sitemap.xml, and /robots.txt under Flask, FastAPI, and Quart alike — no if IS_FLASK: gate.

on Dash 4.3+ (a silent no-op on older Dash).

Changed

(Mantine 8.3.6). React 18.2.0.

surface (LLMS_DOC, multi-backend, MCP bridge).

and dash-improve-my-llms[flask]>=2.0.0, with commented [fastapi], [quart], and [all] extras plus uvicorn for ASGI deployment.

rewritten around the LLMS_DOC pattern.

components/navbar.py updated for the new backend badge and navigation (TOON Format and Handoff entries removed).

2.0 /<page>/llms.txt routing.

Removed

the docs/toon-format/ page, the TOON Analytics Dashboard (docs/data-visualization/toon_dashboard.py), and all /llms.toon routes. dash-improve-my-llms 2.0 removed TOON from its public API (TOONConfig, toon_encode, generate_*_toon no longer exist).

dash-improve-my-llms 2.0; Dash 4.3 MCP exposes layouts as resources natively.

no-ops in 2.0. Write the emphasis directly into a page's LLMS_DOC markdown.

that became 2.0) — superseded by the in-app AI Integration page.

Migration notes (from any 0.x)

  1. 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 set DASH_BACKEND=fastapi.

  1. AI/LLM prose: give each page module an LLMS_DOC = """...""" string at

module scope (or register_page_metadata(path, llms_doc=...) when the prose is computed). The startup UserWarning from 2.0 names every page still missing prose.

  1. dash-improve-my-llms extra: pick [flask], [fastapi], [quart], or

[all] in requirements.txt.

  1. Removed APIs: replace any mark_important() / mark_component_hidden()

calls (now no-ops) with LLMS_DOC content, and remove references to TOON, /page.json, and /architecture.txt.


[0.8.0] - 2025-12-14

Added

Changed

Fixed

Technical Details


[0.7.0] - 2025-12-13

Added

Changed

Fixed

Technical Details


[0.6.0] - 2025-12-13

Added

Changed

New TOONConfig Options

Documentation

Improved


[0.5.0] - 2025-12-13

Added

Changed

Documentation

Improved


[0.4.0] - 2025-11-10

Added

[0.3.0] - 2025-11-09

Added - Documentation System

Added - Theme System

Added - UI/UX Enhancements

Added - Production Features

Improved

Changed

Fixed

[0.2.0] - 2025-11-09

Changed

Removed

Fixed

Added

Improved

[0.1.0] - 2024-11-30

Added

Documentation


Version History Summary

VersionDateDashDMCMantinePythonFeatures
1.0.02026-06-144.2.02.7.08.3.63.11+Pluggable backends (Flask/FastAPI/Quart), dash-improve-my-llms 2.0, TOON removed
0.8.02025-12-143.2.02.4.08.3.63.11+TOON v3.3, tips/best practices/patterns/resources extraction
0.7.02025-12-133.2.02.4.08.3.63.11+Custom TOON generator, documentation-aware TOON v3.2
0.6.02025-12-133.2.02.4.08.3.63.11+Enhanced TOON v3.1, dash-improve-my-llms v1.1.0
0.5.02025-12-133.2.02.4.08.3.63.11+TOON format, dash-improve-my-llms v1.0.0
0.4.02025-11-103.2.02.4.08.3.63.11+LLM Copy Button directive
0.3.02025-11-093.2.02.4.08.3.63.11+Comprehensive docs, theme system, SEO
0.2.02025-11-093.2.02.4.08.3.63.11+Migration to Dash 3.x, DMC 2.4.0, AI/LLM
0.1.02024-11-302.5.0+0.14.77.14.13.11+Initial release

Migration Guides

Migrating to 1.0.0 from any 0.x

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:

  1. Backend: defaults to Flask — no change required. For FastAPI/Quart,

pip install "dash[fastapi]" (or [quart]) and set DASH_BACKEND=fastapi.

  1. AI/LLM prose: add an LLMS_DOC string to each page module (or call

register_page_metadata(path, llms_doc=...)); the 2.0 startup warning lists pages still missing prose.

  1. dash-improve-my-llms extra: pick [flask] / [fastapi] / [quart] /

[all] in requirements.txt.

  1. Removed APIs: drop any TOON usage (TOONConfig, toon_encode,

generate_*_toon), /page.json, /architecture.txt, and the now-no-op mark_important() / mark_component_hidden() calls — move emphasis into LLMS_DOC instead.

Migrating to 0.6.0 from 0.5.0

Zero changes required! The upgrade is fully backwards compatible.

Key changes:

  1. Update dash-improve-my-llms in requirements.txt to >=1.1.0
  2. 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
)

Migrating to 0.5.0 from 0.4.0

Zero changes required! The upgrade is fully backwards compatible.

Key changes:

  1. Update dash-improve-my-llms in requirements.txt to >=1.0.0
  2. New TOON endpoints are automatically available:

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"})

Migrating to 0.3.0 from 0.2.0

Minor updates, mostly additive. Key changes:

  1. Documentation content significantly expanded
  2. Chart examples now use DMC figure templates
  3. Enhanced SEO features in index.html
  4. Better theme integration across all components

Migrating to 0.2.0 from 0.1.0

Major breaking changes. See migration documentation:

Key changes to be aware of:

  1. Update all imports from dash_html_components to from dash import html
  2. Update all imports from dash_core_components to from dash import dcc
  3. Replace dmc.NotificationProvider() with dmc.NotificationContainer()
  4. Update custom components to use DMC 2.4.0 API
  5. Check CSS for any Mantine 8 specific changes

Support


[unreleased]: https://github.com/pip-install-python/Dash-Documentation-Boilerplate/compare/v1.0.0...HEAD [1.0.0]: https://github.com/pip-install-python/Dash-Documentation-Boilerplate/compare/v0.8.0...v1.0.0 [0.8.0]: https://github.com/pip-install-python/Dash-Documentation-Boilerplate/compare/v0.7.0...v0.8.0 [0.7.0]: https://github.com/pip-install-python/Dash-Documentation-Boilerplate/compare/v0.6.0...v0.7.0 [0.6.0]: https://github.com/pip-install-python/Dash-Documentation-Boilerplate/compare/v0.5.0...v0.6.0 [0.5.0]: https://github.com/pip-install-python/Dash-Documentation-Boilerplate/compare/v0.4.0...v0.5.0 [0.4.0]: https://github.com/pip-install-python/Dash-Documentation-Boilerplate/compare/v0.3.0...v0.4.0 [0.3.0]: https://github.com/pip-install-python/Dash-Documentation-Boilerplate/compare/v0.2.0...v0.3.0 [0.2.0]: https://github.com/pip-install-python/Dash-Documentation-Boilerplate/compare/v0.1.0...v0.2.0 [0.1.0]: https://github.com/pip-install-python/Dash-Documentation-Boilerplate/releases/tag/v0.1.0

Note for AI agents: This is the static, prerendered view of an interactive Dash application served because we detected a non-JS user agent. Full prose docs: