-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrun.py
More file actions
861 lines (765 loc) · 40.2 KB
/
Copy pathrun.py
File metadata and controls
861 lines (765 loc) · 40.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
import inspect
import os
import sys
import dash
from dash import Dash
from components.appshell import create_appshell
def _version(text: str) -> tuple:
"""("4.4.1rc0") -> (4, 4, 1). Trailing rc/dev segments are dropped."""
parts = []
for chunk in text.split(".")[:3]:
digits = ""
for char in chunk:
if not char.isdigit():
break
digits += char
if not digits:
break
parts.append(int(digits))
return tuple(parts)
# Feature gates below are keyed off this rather than off a try/except, so the
# reason a feature is unavailable is stated once, in one place.
DASH_VERSION = _version(dash.__version__)
# AI/LLM Integration & SEO — dash-improve-my-llms 2.2
# 2.0 replaced the custom Flask-only routes that used to live in this file
# (`/<page>/llms.txt`, `/<page>/page.json`, `/<page>/llms.toon`) with a single
# backend-detecting dispatcher covering Flask, FastAPI and Quart; `/page.json`
# and `/llms.toon` were dropped outright (Dash 4.3 MCP covers the structured
# introspection they were doing).
#
# 2.2 adds, on top of that: merge (not assign) semantics in
# register_page_metadata so no later call can erase a page's prose, a
# prerender that serves the content to every visitor rather than only to
# recognised crawlers, a Markdown renderer that emits real anchors/tables/code
# fences, and `register_network` — the cross-host directory fed by
# lib/network_directory.py below.
#
# Also new in 2.2: a page's llms.txt opens with a nav block back to the site
# and network indexes rather than being a dead end, and the same URL
# content-negotiates — Markdown for agents, a rendered view for browsers.
# (2.1 was assigned during development and never published; 2.0 upgrades
# straight to 2.2.)
from dash_improve_my_llms import (
__version__ as LLMS_PKG_VERSION,
add_llms_routes,
LLMSConfig,
RobotsConfig,
on_document_read,
register_page_metadata,
)
# The version requirements.txt pins. Checked at startup — see the floors block
# below for why this is worth a line of output on every boot.
#
# 2.5.1 was the Tier-B SEO standard: `configure_seo` (icons, social card,
# publisher/sameAs), the crawler <title> carrying the site name, per-page
# `title`/`image_url`/`schema_type` actually reaching the crawler document,
# /favicon.ico answered with a redirect instead of the app shell, and a
# prerender that no longer clobbers the browser's per-page <title>.
# 2.6.0 raises it to the honesty standard: sitemap <lastmod> is emitted
# verbatim from `register_page_metadata(lastmod=)` and OMITTED when unset.
# The floor is load-bearing for HONESTY, not crash avoidance: older
# packages take `lastmod=` into **kwargs and silently ignore it (measured
# on 2.5.1 by the pip-docs+ stage-4 session, 2026-08-21 — the signature is
# `(path, name=None, description=None, llms_doc=None, **kwargs)`), so
# below the floor every date this repo stamped is swallowed and the
# sitemap goes back to swearing everything changed at build time. Also in
# 2.6.0: icon autodiscovery (this app still declares explicitly; the two
# must agree — tests/test_seo_icons.py), JSON-LD publisher.logo, and the
# viewer banner de-dup.
# 2.6.1 moved the floor again: below it the universal prerender ships with
# a literal `hidden` attribute, so every visibility-respecting consumer
# (html-to-text extractors, plausibly crawler content-weighting) reads
# "Loading..." instead of the page's prose — the outside-audit finding of
# 2026-08-22. tests/test_pages.py pins the visible shape.
# 2.8.0 is the ledger floor (2026-08-29): ONE classifier — `classify()` is
# the same vendor registry robots.txt is rendered from, and
# lib/analytics_tracker delegates to it instead of carrying a fourth UA list
# that filed ClaudeBot (Anthropic's TRAINING crawler) under "search", six
# lines from where this file already said otherwise, and counted every
# UA-less client as a person; the READ EVENT — `on_document_read` hands the
# app one row per corpus document served (tier, verdict, bytes, verified
# vendor), which the tracker keeps as the ledger's `reads` table and
# /admin/traffic reads back; `Vary: User-Agent` on the lane-split
# responses. Load-bearing for CRASH avoidance, not just honesty:
# lib/analytics_tracker imports `classify` and `_ledger.EVENT_FIELDS` at
# module scope and the registration below is unconditional. `policy` is
# None on every event until 2.8.1 ships and the rollup groups None as
# "default" — which is why the floor does not wait for it.
# `configure_seo` is deliberately imported AFTER this floor fires (see the
# floors block) so a stale environment gets the floor's diagnosis instead of
# a bare ImportError.
LLMS_PKG_FLOOR = (2, 8, 0)
# THE FORK POINT — claim this app's network identity before any
# hub-facing module imports. Every module that names this app
# (satellite_reporter, ad_client, hub_client, bulletin) carries its own
# fallback default, and after a template sync those defaults can
# DISAGREE: the byte-copied reporter says "boilerplate" while the
# fork's other modules say its own key, so an unset SATELLITE_APP_KEY
# files the fork's traffic under the TEMPLATE's hub row (found live on
# pannellum, 2026-08-21 — the same class as the flows-reported-as-
# boilerplate contamination in the hub's history). setdefault: a real
# env value (Render dashboard, .env — loaded by lib/backend before
# this runs on import chains that need it) always wins; this line only
# closes the unset gap. FORKS CHANGE THIS ONE STRING.
os.environ.setdefault("SATELLITE_APP_KEY", "llms")
# Analytics tracking
from lib.analytics_tracker import tracker
# Site identity, public origin, and the cross-host network directory
from lib.constants import (
APP_TITLE,
BASE_URL,
OG_IMAGE_ALT,
OG_IMAGE_HEIGHT,
OG_IMAGE_URL,
OG_IMAGE_WIDTH,
PUBLISHER,
SAME_AS,
SITE_BRAND,
SITE_DESCRIPTION,
require_owned_base_url,
)
from lib import network_directory
# Backend selection (flask | fastapi | quart) — see lib/backend.py
from lib.backend import resolve_backend, get_backend_info
scripts = [
"https://unpkg.com/hotkeys-js/dist/hotkeys.min.js",
]
# ----------------------------------------------------------------------------
# Pluggable backend (Dash 4.1+)
# ----------------------------------------------------------------------------
BACKEND = resolve_backend()
BACKEND_INFO = get_backend_info(BACKEND)
IS_FLASK = BACKEND == "flask"
print(
f"[boilerplate] Starting Dash {dash.__version__} "
f"(dash-improve-my-llms {LLMS_PKG_VERSION}) on backend='{BACKEND}'"
)
# ----------------------------------------------------------------------------
# Dependency floors — enforced, not advised.
#
# These were warnings first. That was not enough: an IDE run configuration
# pointing at another project's virtualenv starts this app quite happily
# against whatever versions that environment holds, serves visibly older
# behaviour, and the warning scrolls past above a wall of page-loading logs.
# Diagnosing it from the outside costs hours — the browser, the cache and the
# process all look innocent, because they are.
#
# So a version below the floor stops the boot and says what to do. The app is
# never wrong-but-running. Set ALLOW_STALE_DEPS=1 to downgrade these to
# warnings if you are deliberately testing an older release.
# ----------------------------------------------------------------------------
ALLOW_STALE_DEPS = os.environ.get("ALLOW_STALE_DEPS", "0") == "1"
def _dependency_floor(message: str, fatal: bool) -> None:
"""Print, or refuse to start. Either way, name the interpreter.
`sys.executable` is the fact that settles which environment is actually
serving, and it is the one nobody thinks to check first.
"""
detail = (
f"{message}\n"
f" running from: {sys.executable}\n"
f" expected: {os.path.join(os.path.dirname(os.path.abspath(__file__)), '.venv/bin/python')}\n"
" fix: point your run configuration at this project's own .venv, "
"or reinstall with `pip install -r requirements.txt`.\n"
" (set ALLOW_STALE_DEPS=1 to start anyway)"
)
if fatal and not ALLOW_STALE_DEPS:
raise RuntimeError("\n[boilerplate] " + detail)
print("[boilerplate] WARNING: " + detail)
if LLMS_PKG_FLOOR > _version(LLMS_PKG_VERSION):
_dependency_floor(
f"dash-improve-my-llms {LLMS_PKG_VERSION} is below the "
f"{'.'.join(str(n) for n in LLMS_PKG_FLOOR)} floor in requirements.txt. "
"Below 2.7.0 this app does not start at all: it calls configure_geo, "
"LLMSConfig(panel=), RobotsConfig(vendor_policy=) and the rate "
"ceiling unconditionally, and the control board writes through the "
"callable seams all four expose. "
"Below 2.6.1 the universal prerender ships `hidden`, so every "
"visibility-respecting consumer (text extractors, arguably crawler "
"content-weighting) reads 'Loading...' instead of the page's prose. "
"Below 2.6.0 the sitemap goes back to lying: `lastmod=` is accepted "
"into **kwargs and SILENTLY IGNORED, so every date this repo stamped "
"is swallowed and <lastmod> reverts to invented build dates. Below "
"2.5.1 the Tier-B SEO standard additionally unwinds: `configure_seo` "
"does not exist, the crawler <title> drops back to the bare page name, "
"per-page title/image_url/schema_type never reach the crawler "
"document, and /favicon.ico serves the app shell instead of an icon — "
"the crawler/browser identity drift this site exists to demonstrate "
"the fix for. (2.3.x additionally loses resolve_site_title and the "
"tiered corpus documents.)",
fatal=True,
)
# Imported after the floor on purpose: on a pre-2.5.0 package this name does
# not exist, and the floor's diagnosis above beats a bare ImportError. The
# fallback exists only for ALLOW_STALE_DEPS=1 — the floor is fatal otherwise.
try:
from dash_improve_my_llms import configure_seo # noqa: E402
except ImportError: # pragma: no cover — ALLOW_STALE_DEPS with a pre-2.5.0 package
def configure_seo(**_kwargs) -> None:
print(
"[boilerplate] WARNING: configure_seo unavailable (pre-2.5.0 "
"package) — crawler identity tags and root icons not emitted."
)
# The 2.7.0 surface: the geo guardrail and the operator panel. Imported after
# the floor check for the same reason configure_seo is — on a package below
# the floor these names do not exist, and the floor's diagnosis above beats a
# bare ImportError.
#
# This was a LLMS_HAS_27 capability block until 2026-08-23, carrying a degrade
# path for every call site because requirements.txt still floored at >=2.6.1
# while 2.7.0 was unpublished. 2.7.0 is on PyPI and the floor moved, so the
# block collapsed to this import — which was its stated design promise, not a
# convenience. A guard kept past the condition it guards is a guard nobody
# can reason about.
from dash_improve_my_llms import configure_geo, geo as _geo # noqa: E402
def _rate_ceiling():
"""`LLMS_RATE_LIMIT_PER_MINUTE`, or None for the unlimited default.
W4 is per-process: N gunicorn workers mean N x this ceiling in aggregate.
Documented in the package, restated here because the number an operator
types on the board is not the number the origin enforces.
"""
raw = (os.environ.get("LLMS_RATE_LIMIT_PER_MINUTE") or "").strip()
if not raw:
return None
try:
value = int(raw)
except ValueError:
print(f"[llms] WARNING: LLMS_RATE_LIMIT_PER_MINUTE={raw!r} is not an "
"integer — no rate ceiling applied.")
return None
return value if value > 0 else None
if DASH_VERSION < (4, 4):
# Fatal only on FastAPI, where it is not a degradation but an outage:
# 4.3.0's ASGI middleware returns before setting the request context, so
# the page catch-all raises "No active request in context" and every
# non-root URL 500s. Fixed upstream in 4.4.0. On Flask and Quart the same
# Dash release works, so a warning is proportionate there.
_dependency_floor(
f"dash {dash.__version__} is below the 4.4.0 floor in requirements.txt."
+ (
" On the FastAPI backend every non-root URL returns 500 "
"('No active request in context') — an upstream defect in 4.3.0, "
"fixed in 4.4.0."
if BACKEND == "fastapi"
else ""
),
fatal=BACKEND == "fastapi",
)
# Dash 4.3+ MCP server: exposes layout, components, pages and (whitelisted)
# callbacks to MCP clients over Streamable HTTP. Off unless DASH_MCP_ENABLED=1,
# because it is a live introspection surface on a public host.
#
# This has to be a constructor argument — Dash starts the server during
# __init__, so there is no supported way to switch it on afterwards.
# dash-improve-my-llms separately registers each page's prose as a `dash.mcp`
# resource, which is what gives an MCP client the docs alongside the
# introspection.
MCP_ENABLED = os.environ.get("DASH_MCP_ENABLED", "0") == "1"
MCP_PATH = os.environ.get("DASH_MCP_PATH", "_mcp")
# Passed as **kwargs rather than named arguments, because Dash validates
# unknown constructor keywords by raising TypeError. `enable_mcp` landed in
# 4.3, so naming it unconditionally makes the app refuse to boot on 4.2 with
# an error that says nothing about MCP — and the only thing being lost is a
# feature that is off by default anyway.
# ----------------------------------------------------------------------------
# Clerk satellite auth. MUST run BEFORE Dash(...) — register_clerk_auth
# installs @dash.hooks callbacks that fire during app construction, so calling
# it afterwards silently does nothing. Fully optional: a no-op with no CLERK_*
# keys, which is the default. See lib/auth.py.
# ----------------------------------------------------------------------------
from lib import auth as _auth # noqa: E402
CLERK_ENABLED = _auth.register()
MCP_KWARGS = {}
if MCP_ENABLED:
if "enable_mcp" in inspect.signature(Dash.__init__).parameters:
MCP_KWARGS = {"enable_mcp": True, "mcp_path": MCP_PATH}
else:
print(
f"[boilerplate] DASH_MCP_ENABLED=1 ignored: dash "
f"{dash.__version__} has no MCP server (needs >= 4.3)."
)
app = Dash(
__name__,
backend=BACKEND,
title=APP_TITLE,
suppress_callback_exceptions=True,
use_pages=True,
external_scripts=scripts,
update_title=None,
prevent_initial_callbacks=True,
index_string=open('templates/index.html').read(),
**MCP_KWARGS,
)
if MCP_KWARGS:
print(
f"[boilerplate] Dash MCP server enabled at /{MCP_PATH.lstrip('/')} "
f"(dash {dash.__version__})."
)
# dash-clerk-auth splits its setup either side of Dash(...): sessions, the
# /api/auth/* routes and per-request identity are wired here. No-op when off.
_auth.configure_app(app)
# ----------------------------------------------------------------------------
# Trust the proxy's forwarded scheme. Immediately after the server object
# exists and before anything can serve a request.
#
# Dash builds `twitter:url` from `request.url` for every page, and behind
# Cloudflare -> Render the last hop is plain HTTP, so production advertised
# `http://boilerplate.2plot.dev/` to every social scraper while `og:url` (which
# templates/index.html hard-codes) looked correct. Scrapers do not run
# JavaScript, so the client-side canonical sync in the template cannot reach
# this. See lib/proxy.py for why gunicorn's own forwarded-header handling does
# not cover it, and for the trust boundary.
# ----------------------------------------------------------------------------
from lib import proxy as _proxy # noqa: E402
PROXY_FIX_APPLIED = _proxy.apply(app, BACKEND)
print(
"[boilerplate] forwarded-scheme trust: "
+ ("on" if PROXY_FIX_APPLIED else "OFF — request.url will report the "
"scheme of the last proxy hop, and social cards will advertise it")
)
# Expose backend info so layout components can render a badge without
# re-reading the env var (which could drift between processes/workers).
app._backend_info = BACKEND_INFO
# ============================================================================
# AI/LLM & SEO Configuration
# ============================================================================
# Public origin. Drives <link rel="canonical">, sitemap.xml and the absolute
# URLs in llms.txt — see lib/constants.py for why a fork that leaves this at
# the default deindexes itself, and set APP_BASE_URL per deployment.
require_owned_base_url()
app._base_url = BASE_URL
# Cross-host directory: <link rel="related"> tags, a "## Network" section in
# /llms.txt, and followed links in the prerendered body, so an agent that
# lands on one satellite can enumerate the rest. The peer list lives in
# lib/network_directory.py — one definition, imported by every satellite.
network_directory.apply(BASE_URL)
# The writable policy store — this site's half of the 2.7.0 callable seam.
# Imported here rather than at the top because it must come after .env is
# loaded (lib/backend.py does that on import) or POLICY_STORE_FILE from a
# developer's .env would be read too late to matter.
from lib import policy_store as _policy_store # noqa: E402
_policy_store.persistence_warning()
# Configure bot management policies. THE TRAINING WALL IS RETIRED (owner
# decision, 2026-08-30, round 3.4 — this host is a canary with
# boilerplate.2plot.dev). It stood while a blocked 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. The per-vendor block/meter through `vendor_policy` below is the
# instrument now: it is aimed, live, and revocable per request, which a
# blanket bucket never was.
#
# As of dash-improve-my-llms 2.3.3 the buckets are correct per vendor:
# ClaudeBot is Anthropic's *training* crawler, distinct from the
# user-triggered and search fetchers Claude-User / Claude-SearchBot, which
# were allowed throughout alongside ChatGPT-User / OAI-SearchBot /
# PerplexityBot. Note the SHAPE of the change: with block_ai_training=False
# the package emits NO training stanza at all, so robots.txt gains no
# `Allow:` line for these vendors — it simply stops carrying
# `Disallow: /` for them. Assert the absence, not an allow (the boilerplate
# canary's correction to item 3, same day).
#
# 2.7.0 adds `vendor_policy=`, and this fork passes the CALLABLE form: the
# package re-reads it on every robots.txt render AND on every middleware
# decision, so the control board's per-vendor overrides apply without a
# restart. An empty store is a strict no-op — the class defaults above still
# describe what is served — which is what lets the seam sit live from boot
# instead of needing a redeploy the day an override is first wanted.
app._robots_config = RobotsConfig(
block_ai_training=False, # Retired 2026-08-30: no training stanza at all
allow_ai_search=True, # Allow Claude-User/-SearchBot, ChatGPT-User, ...
allow_traditional=True, # Allow Googlebot, Bingbot, etc.
crawl_delay=10,
disallowed_paths=[],
vendor_policy=_policy_store.vendor_policy,
)
# ============================================================================
# Register supplemental metadata for the home page.
# Markdown-driven pages register their own LLMS_DOC inside pages/markdown.py
# (the expanded markdown body becomes the literal /llms.txt response).
# ============================================================================
# `name` here is not a nav label — dash-improve-my-llms 2.3.4 resolves it into
# the /llms.txt H1 and the llms viewer's brand chip (`resolve_site_title`,
# home-page name first, `app.title` second, generic values skipped). It is the
# site's published identity, so it is SITE_BRAND and nothing else; the package
# name lives in the description. See lib/constants.py.
register_page_metadata(
path="/",
name=SITE_BRAND,
description=SITE_DESCRIPTION,
# The home page of a software template is a SoftwareApplication, not a
# generic WebPage — the one structured-data type that exactly describes
# it. Docs pages default to TechArticle in pages/markdown.py.
schema_type="SoftwareApplication",
)
# ============================================================================
# Site identity for the CRAWLER document (dash-improve-my-llms 2.5.0).
# Until 2.5.0 the generated crawler HTML carried the page's content signals
# and none of its identity: browsers got 4-7 icon links, og:image and a
# twitter card from templates/index.html while Googlebot got zero of any of
# them, on every host in the network — so search showed the generic globe.
# One declaration covers every crawler surface, and it also claims
# /favicon.ico (Google's fallback), which Dash's page catch-all was
# answering with the app shell. Content may differ between the crawler
# document and the browser document; identity may not.
# ============================================================================
configure_seo(
icons=[
# Same paths templates/index.html links, so the two heads agree.
# The .ico href is the assets/favicon/ copy (byte-identical to the
# root one index.html links) so this list is SET-equal to what
# 2.6.0's autodiscovery finds — tests/test_seo_icons.py pins that
# agreement, which is the proof the fleet can rely on discovery
# alone once its pixels are right.
"/assets/favicon/favicon.ico",
{"href": "/assets/favicon/favicon-32x32.png", "sizes": "32x32"},
{"href": "/assets/favicon/favicon-16x16.png", "sizes": "16x16"},
{"href": "/assets/favicon/favicon-96x96.png", "sizes": "96x96"},
{"href": "/assets/favicon/android-chrome-192x192.png", "sizes": "192x192"},
{"href": "/assets/favicon/android-chrome-512x512.png", "sizes": "512x512"},
{"href": "/assets/favicon/apple-touch-icon.png",
"rel": "apple-touch-icon", "sizes": "180x180"},
],
social_image=OG_IMAGE_URL,
social_image_alt=OG_IMAGE_ALT,
social_image_width=OG_IMAGE_WIDTH,
social_image_height=OG_IMAGE_HEIGHT,
publisher=PUBLISHER,
same_as=SAME_AS,
)
# Internal pages — excluded from /sitemap.xml, blocked in /robots.txt,
# skipped by the MCP bridge, and return 404 to crawler requests on the
# page URL and on /<page>/llms.txt. This boilerplate ships no internal pages,
# so there is nothing to hide; see /reference/configuration for usage:
#
# from dash_improve_my_llms import mark_hidden
# mark_hidden("/admin")
# ============================================================================
# Retired URLs from the old dash-hook-my-ai service — 301, not 404.
#
# This site replaces a service whose pages are in Google's index, in other
# sites' links, and in the training data of every model that has read the
# network. A 404 throws that away; a 301 hands the accumulated authority to
# the page that replaced it. The /audiences/* URLs are preserved BYTE FOR
# BYTE elsewhere in this file's page tree precisely so that most of the old
# surface needs no redirect at all — this list is only what genuinely moved.
#
# Mounted BEFORE add_llms_routes so the package's `/<page>/llms.txt`
# catch-all cannot shadow them, and registered per backend because Flask and
# Starlette disagree about everything except the status code.
# ============================================================================
RETIRED_URLS = {
# networks.py folded into Showcase C's live network section. This is a
# REAL old-service URL, not a placeholder: the B1 page table retires
# networks.py, and the boilerplate this site forked from also served
# /networks — so the path has inbound links from two directions.
"/networks": "/showcase/policy-panel",
# The template's own pages, deleted rather than hidden when this site
# stopped publishing dash-documentation-boilerplate's documentation as
# its own. They were never on llms.2plot.dev publicly, but the fork's
# history contains them and a stray link costs nothing to honour.
"/network-standard": "/showcase/policy-panel",
"/backends": "/reference/configuration",
"/backend-comparison": "/reference/configuration",
"/fastapi-showcase": "/reference/configuration",
"/authentication": "/reference/access",
"/examples/ai-integration": "/getting-started",
"/examples/directives": "/getting-started",
"/examples/interactive": "/getting-started",
"/examples/visualization": "/getting-started",
# analytics.py + admin.py retire with the old service. The hub's /traffic
# board replaced the first; the control board replaced the second, and
# ADMIN_DASH_TOKEN dies with them (the archived KICKOFF-llms.md addendum
# is superseded at cutover — do not resurrect the visitor dashboard).
"/analytics": "/showcase/policy-panel",
"/admin": "/admin/control-board",
# v200_features.py retired everywhere
"/v200-features": "/",
# the old service's audience pages lived one level up
"/mcp-clients": "/audiences/mcp-clients",
"/web-crawlers": "/audiences/web-crawlers",
"/llm-context": "/audiences/llm-context",
}
def _register_redirects(dash_app, backend, routes):
"""301 every retired URL at its replacement, on whichever backend."""
if backend == "fastapi":
from starlette.responses import RedirectResponse
def _make(target):
async def _redirect(_request):
return RedirectResponse(target, status_code=301)
return _redirect
for source, target in routes.items():
dash_app.server.add_route(source, _make(target), methods=["GET", "HEAD"])
return
from flask import redirect as _flask_redirect
def _make(target):
def _redirect():
return _flask_redirect(target, code=301)
return _redirect
for source, target in routes.items():
# endpoint= is required: Flask keys view functions by name and every
# closure here is called `_redirect`, so without it the second route
# raises "View function mapping is overwriting an existing endpoint".
dash_app.server.add_url_rule(
source, endpoint=f"retired{source.replace('/', '_')}",
view_func=_make(target), methods=["GET", "HEAD"],
)
_register_redirects(app, BACKEND, RETIRED_URLS)
print(f"[llms] retired URLs: {len(RETIRED_URLS)} permanent redirect(s) mounted")
# ============================================================================
# FastAPI showcase routes (only when running on FastAPI).
# These are NOT the AI/LLM endpoints — those are handled by add_llms_routes
# below. They are a small native API surface (`/healthz`, `/api/backend`,
# `/api/pages`) that demonstrates first-class OpenAPI/Swagger UI integration
# under Dash 4.1+'s FastAPI backend.
#
# Mounted BEFORE add_llms_routes so the package's catch-all
# `/<page>/llms.txt` matcher doesn't shadow these.
# ============================================================================
if BACKEND == "fastapi":
from lib.asgi_routes import register_asgi_routes
register_asgi_routes(app, BACKEND_INFO)
print(
"[boilerplate] FastAPI showcase routers mounted: /healthz, "
"/api/backend, /api/pages. Swagger UI at /docs, ReDoc at /redoc."
)
else:
# Flask/Quart get the same /healthz the FastAPI build declares — the
# 2plot.ai hub's hourly sweep probes it for the network health panel.
from lib.health import register_health_route
register_health_route(app, BACKEND)
# ============================================================================
# Analytics tracking (Flask / Quart) — MUST be registered BEFORE
# add_llms_routes.
#
# `before_request` hooks run in registration order, and the package's
# `_bot_middleware` short-circuits AI-search crawlers (ClaudeBot, ChatGPT-User,
# PerplexityBot, ...) with its own response. Registered after it, this hook
# never runs for exactly the bot traffic a docs site most wants counted, and
# the `bot_hits` we report to 2plot.ai would be quietly too low.
#
# FastAPI is the mirror image and is wired further down: Starlette runs the
# LAST-added middleware outermost, so ours goes on after add_llms_routes.
# ============================================================================
if IS_FLASK:
from flask import request as _flask_request
@app.server.before_request
def track_visitor():
"""Track visitor analytics before each request."""
try:
# Headers are passed so the tracker can read the REAL client IP
# and country from the proxy/CDN (behind Render or Cloudflare,
# remote_addr is the proxy — every visitor would look like one).
tracker.track_visit(
_flask_request.path,
_flask_request.headers.get('User-Agent', ''),
_flask_request.remote_addr,
headers=dict(_flask_request.headers),
)
except Exception:
pass
elif BACKEND == "quart":
from quart import request as _quart_request
@app.server.before_request
async def track_visitor():
"""Track visitor analytics before each request (Quart)."""
try:
tracker.track_visit(
_quart_request.path,
_quart_request.headers.get('User-Agent', ''),
_quart_request.remote_addr,
headers=dict(_quart_request.headers),
)
except Exception:
pass
# Network bulletin — hub-published tips and announcements rendered in the
# header of the llms.txt view, so a twenty-site network says "here is what
# changed" once instead of in twenty repositories.
#
# This was commented out, with a note saying 2plot.dev did not serve
# /api/network/bulletin yet. The hub started serving it; the comment did not
# change; and NETWORK_BULLETIN_URL sat set in production against code that
# never read it. Nothing failed — the feature is opt-in, so an unwired app
# makes no request and the viewer header renders fine on the package's
# defaults. The only symptom was an announcement that never appeared.
#
# Hence lib/bulletin.py, and the boot line below: no commented-out wiring, and
# the log says which of the two states this process is in.
from lib import bulletin as _bulletin # noqa: E402
BULLETIN_ENABLED = _bulletin.configure()
print(
f"[boilerplate] network bulletin: {_bulletin.url()} "
f"(app='{_bulletin.app_id()}')"
if BULLETIN_ENABLED else
"[boilerplate] network bulletin: off — set NETWORK_BULLETIN_URL="
f"{_bulletin.HUB_BULLETIN_URL} to render the hub's announcements"
)
# ============================================================================
# Access control (dash-improve-my-llms 2.3). Reads the tiers the pages just
# declared, so it must run after they are registered and before the routes are
# attached. Stays OFF unless some page declares a non-public tier — the policy
# and the reasoning live in lib/access.py.
# ============================================================================
from lib import access as _access # noqa: E402
from lib import page_tiers as _page_tiers # noqa: E402
from lib import page_visibility as _page_visibility # noqa: E402
# Tiered corpus documents (dash-improve-my-llms >= 2.4.0). Pseudo-paths:
# they never enter dash.page_registry, so they cannot leak into listings —
# registering them here lets this satellite tier its compact briefing and
# full corpus via env (LLMS_SMALL_TIER / LLMS_FULL_TIER), and the hub can
# tighten either network-wide through its page-tier ceilings with no
# redeploy here. The explicit `or "public"` matters: these registered under
# the PAGE_DEFAULT_TIER fallback before, which meant flipping that env to
# gate the *interactive* site would silently gate the corpus documents too.
# Their tier is now always a deliberate setting, never an ambient default.
_page_tiers.register("/llms-small.txt",
os.environ.get("LLMS_SMALL_TIER") or "public")
_page_tiers.register("/llms-full.txt",
os.environ.get("LLMS_FULL_TIER") or "public")
# The home page registers via pages/home.py, not pages/markdown.py, so no
# frontmatter ever declares its tier — under PAGE_DEFAULT_TIER=auth it would
# silently inherit the gate. The funnel's front door stays public, always.
_page_tiers.register("/", "public")
# force= when either gate env is present: with every tier still public the
# auto-detect would skip the wiring, but a host that flips by env needs the
# verdict plumbing (and the prerender's use of it) live during the dark
# launch, not on the flip.
ACCESS_ENABLED = _access.configure(
force=bool(os.environ.get("PAGE_DEFAULT_TIER")
or os.environ.get("LLMS_PUBLIC_DEFAULT"))
)
# Wire up the package: /llms.txt, /<page>/llms.txt, /robots.txt, /sitemap.xml,
# bot-detection middleware, and (on Dash 4.3+) MCP resource registration.
# Works under Flask, FastAPI, and Quart — no gating needed.
#
# /<page>/llms.txt content-negotiates from 2.2.0 on: agents and crawlers get
# the Markdown byte for byte, browsers get it rendered behind the network
# header. `?raw=1` and `?format=html` force either side, and both variants
# send `Vary: Accept` so a CDN cannot hand cached HTML to the next agent.
# ============================================================================
# The country guardrail (dash-improve-my-llms 2.7.0 — docs/GEO.md).
#
# Wired UNCONDITIONALLY, with the callable form. Three reasons it is not
# behind an `if`:
#
# 1. An empty denylist is a strict no-op — every response is byte-identical
# to a build that never calls this. The inherited 322-test suite runs
# with the seam live and proves it.
# 2. The seam's whole promise is "no restart". Gating the CALL on a env var
# would mean the first country the owner ever blocks costs a redeploy,
# which is the problem the callable exists to solve.
# 3. `deny_countries=` validates a STATIC list at config time and raises;
# the callable form cannot, so the store validates on write instead
# (lib/policy_store.normalize_country) and this line can never raise at
# boot on a bad stored value.
#
# `unknown=` is NOT a seam — the package reads it once, here — so the board
# labels it "applies at next deploy" rather than pretending otherwise.
#
# THE TRUST MODEL, restated because it is easy to lose: the country comes
# from an edge header. Behind Cloudflare CF-IPCountry is trustworthy and
# client copies are stripped; a client that reaches this origin directly can
# say anything. This is a compliance guardrail, not a security boundary — if
# the block matters adversarially, add the Cloudflare country WAF rule too.
# The per-host check is the panel's "resolved to X via <header>" line, and
# on a DNS-only host every request resolves "unknown" and this ships inert.
configure_geo(
deny_countries=_policy_store.geo_deny,
unknown=_policy_store.geo_unknown(),
# EXACT-match paths only. /healthz is what the hub's hourly sweep probes;
# without the exemption a geo-blocked country's health check would 451
# and the network health panel would report this host down.
exempt_paths=("/healthz", "/health", "/livez", "/readyz"),
policy_url=os.environ.get("GEO_POLICY_URL", ""),
)
_geo_policy = _geo.effective_policy()
print(
f"[llms] geo guardrail: {len(_geo_policy['deny_countries'])} country(ies) "
f"denied via {_geo_policy['denylist_source']}, unknown="
f"{_geo_policy['unknown']} — store {_policy_store.path()}"
)
# Wire up the package: /llms.txt, /<page>/llms.txt, /robots.txt, /sitemap.xml,
# bot-detection middleware, and (on Dash 4.3+) MCP resource registration.
#
# panel=True unconditionally, and it is not a leak: with no token the panel
# 404s for everyone (docs/PANEL.md), and the token is read PER REQUEST from
# DIMLL_PANEL_TOKEN — so an operator can rotate or revoke it live. Register
# it only when the token happens to be set at boot and that promise breaks:
# turning the panel on would cost a redeploy, which is when nobody does it.
add_llms_routes(app, LLMSConfig(
warn_missing_llms_doc=True,
panel=True,
rate_limit_per_minute=_rate_ceiling(),
))
# The ledger row (dimll 2.8.0): the package emits one event per corpus
# document it serves and does no I/O with it; the tracker keeps it as the
# `reads` table next to `visits` in the same analytics file. Registered
# ONCE — the test suite imports run.py more than once per process and
# `on_document_read` APPENDS, so a marker on the callback's owner guards
# the second import (the package also dedups an identical callable; belt
# and braces). Read back by /admin/traffic and folded into the v4 rollup.
if not getattr(tracker, "_read_hook_registered", False):
on_document_read(tracker.record_read)
tracker._read_hook_registered = True
# ============================================================================
app.layout = create_appshell(dash.page_registry.values())
server = app.server
# ============================================================================
# The person→agent handoff: /api/agent-key turns the browser's Clerk session
# into a portable ?key= for copied llms.txt URLs (lib/agent_key.py). 204 for
# everyone until Clerk and the hub are configured — safe to mount always.
# ============================================================================
from lib.agent_key import register_agent_key_route # noqa: E402
register_agent_key_route(app, BACKEND)
_non_public = sum(1 for t in _page_tiers.registered().values() if t != "public")
print(
f"[boilerplate] interactive gate: default tier "
f"'{os.environ.get('PAGE_DEFAULT_TIER') or 'public'}', "
f"{_non_public} non-public page(s), machine surfaces "
f"{'GATED' if not _page_tiers.get_llms_public('/__probe__') else 'open'} "
f"by default (LLMS_PUBLIC_DEFAULT), access wiring "
f"{'ON' if ACCESS_ENABLED else 'off'}, control board at "
f"/admin/control-board ({_page_visibility.override_count()} live "
f"override(s))."
)
# ============================================================================
# Analytics Tracking (FastAPI) — added LAST on purpose.
# Starlette runs the most recently added middleware outermost, so registering
# here (after add_llms_routes) puts the tracker in front of the package's bot
# middleware and every request gets counted. The Flask/Quart hooks are the
# mirror image and live above.
# ============================================================================
if BACKEND == "fastapi":
from lib.asgi_middleware import register_asgi_middleware
register_asgi_middleware(app)
# ============================================================================
# Network analytics — hourly signed rollup POSTed to 2plot.ai so the hub's
# owner-only /traffic dashboard can chart this app alongside the network.
# Contract: 2plotai/docs/network/satellite-analytics.md.
# No-op unless CROSS_APP_WEBHOOK_SECRET is set.
# ============================================================================
from lib.satellite_reporter import start_reporter
start_reporter()
# MCP wiring used to live down here, calling `from dash import mcp_enabled`
# and `mcp_enabled(app)`. Both were wrong: the symbol lives in `dash.mcp`, not
# `dash`, so the import always raised ImportError and the app printed
# "MCP not available in dash 4.4.1 (needs >=4.3)" — while running 4.4.1. And
# `mcp_enabled` is the decorator that marks a *function* as an MCP tool, not a
# server switch. The server is started from Dash's constructor, so the real
# wiring is `enable_mcp=` at the top of this file.
# ============================================================================
if __name__ == "__main__":
# Env-overridable, same defaults as before. Two reasons beyond taste:
# a platform that injects $PORT (Render does) can be honoured without a
# code change, and a developer can bind to loopback only — 0.0.0.0
# publishes the dev server to every device on the network, which is a
# surprise on a shared wifi and is refused outright inside a sandbox.
#
# port= was a STRING here. Werkzeug coerces it, so it worked; int() makes
# a bad value fail at boot with a clear error instead of deep in the
# server.
app.run(
debug=os.environ.get("DASH_DEBUG") == "1",
host=os.environ.get("HOST", "0.0.0.0"),
port=int(os.environ.get("PORT", "8959")),
)