|
| 1 | +"""Cross-host directory for the 2plot network — one definition, every satellite. |
| 2 | +
|
| 3 | +Why this file exists |
| 4 | +-------------------- |
| 5 | +Search engines follow links between hosts weakly; agents don't follow them at |
| 6 | +all. A model answering "what does this ecosystem provide?" fetches one or two |
| 7 | +URLs and reasons from what came back. Landing on ``leaflet.2plot.dev`` it sees |
| 8 | +one library, with nothing in the markup saying the other eleven hosts exist. |
| 9 | +``sitemap.xml`` cannot fix that — a sitemap is scoped to its own origin by |
| 10 | +design — so ``dash-improve-my-llms`` 2.1 emits an explicit machine-readable |
| 11 | +directory instead: ``<link rel="related">`` tags in ``<head>``, a ``## Network`` |
| 12 | +section in ``/llms.txt``, and followed links in the prerendered body. |
| 13 | +
|
| 14 | +Keep the definition **here**, in the template, and import it. Twelve |
| 15 | +hand-maintained copies of the same peer list will drift, and a directory that |
| 16 | +disagrees with itself across hosts is worse than no directory at all. |
| 17 | +
|
| 18 | +Three tiers, and the distinction is load-bearing: |
| 19 | +
|
| 20 | +``PEERS`` |
| 21 | + Same network, same operator. These build the cross-host graph you own. |
| 22 | +``AFFILIATED`` |
| 23 | + Yours, on unrelated domains. Findable when asked "what else did you |
| 24 | + build?" without being swept into "what is the 2plot network?". |
| 25 | +``EXTERNAL`` |
| 26 | + Third-party docs you reference but don't own. Emitted ``rel="nofollow"`` |
| 27 | + — references, not endorsements. |
| 28 | +
|
| 29 | +Usage in a satellite's ``run.py``, before ``add_llms_routes(app)``:: |
| 30 | +
|
| 31 | + from lib.constants import BASE_URL |
| 32 | + from lib import network_directory |
| 33 | +
|
| 34 | + app._base_url = BASE_URL |
| 35 | + network_directory.apply(BASE_URL) |
| 36 | +""" |
| 37 | + |
| 38 | +from __future__ import annotations |
| 39 | + |
| 40 | +from typing import Any, Dict, List |
| 41 | + |
| 42 | +# Only list hosts that are actually live. A directory entry pointing at a |
| 43 | +# subdomain with no site is a dead link an agent will follow once and then |
| 44 | +# distrust the rest of the list for. muicharts.2plot.dev and |
| 45 | +# flexlayout.2plot.dev have no docs site yet — add them in the same change |
| 46 | +# that ships them, not before. |
| 47 | +# UPSTREAM DIVERGENCE — the only edit this file carries, and it is temporary. |
| 48 | +# |
| 49 | +# pannellum.2plot.dev and emojimart.2plot.dev are commented out below because |
| 50 | +# they are NXDOMAIN as of 2026-07-31 (verified via DNS, not just a failed |
| 51 | +# request — email/flows returned 000 on a first curl too, but that was a Render |
| 52 | +# free-tier cold start and they serve fine on retry). |
| 53 | +# |
| 54 | +# The rule this enforces is the module's own, three paragraphs up: "a directory |
| 55 | +# entry pointing at a subdomain with no site is a dead link an agent will follow |
| 56 | +# once and then distrust the rest of the list for." Shipping them would publish |
| 57 | +# two dead links from a live docs site. |
| 58 | +# |
| 59 | +# The real fix belongs in dash-documentation-boilerplate, which is the single |
| 60 | +# definition every satellite copies — otherwise each repo rediscovers this |
| 61 | +# independently. Once those hosts resolve, delete this note and re-copy the file |
| 62 | +# from the boilerplate rather than un-commenting by hand. |
| 63 | +PEERS: List[Dict[str, str]] = [ |
| 64 | + { |
| 65 | + "name": "2plot.ai", |
| 66 | + "url": "https://2plot.ai", |
| 67 | + "description": "Network hub and account origin.", |
| 68 | + }, |
| 69 | + { |
| 70 | + "name": "2plot.dev", |
| 71 | + "url": "https://2plot.dev", |
| 72 | + "description": "Package index for every open-source component in the network.", |
| 73 | + }, |
| 74 | + { |
| 75 | + "name": "Documentation boilerplate", |
| 76 | + "url": "https://boilerplate.2plot.dev", |
| 77 | + "description": "The markdown-driven documentation template every satellite site is built from.", |
| 78 | + }, |
| 79 | + { |
| 80 | + "name": "dash-leaflet2", |
| 81 | + "url": "https://leaflet.2plot.dev", |
| 82 | + "description": "Leaflet 2 maps as Dash components.", |
| 83 | + }, |
| 84 | + { |
| 85 | + "name": "dash-mui-scheduler", |
| 86 | + "url": "https://muischeduler.2plot.dev", |
| 87 | + "description": "MUI X Scheduler — calendars and event scheduling for Dash.", |
| 88 | + }, |
| 89 | + { |
| 90 | + "name": "dash-flows", |
| 91 | + "url": "https://flows.2plot.dev", |
| 92 | + "description": "Node-graph editors built on React Flow.", |
| 93 | + }, |
| 94 | +# { |
| 95 | +# "name": "dash-pannellum", |
| 96 | +# "url": "https://pannellum.2plot.dev", |
| 97 | +# "description": "360° panorama and virtual-tour viewer.", |
| 98 | +# }, |
| 99 | +# { |
| 100 | +# "name": "dash-emoji-mart", |
| 101 | +# "url": "https://emojimart.2plot.dev", |
| 102 | +# "description": "Emoji picker component.", |
| 103 | +# }, |
| 104 | + { |
| 105 | + "name": "dash-email", |
| 106 | + "url": "https://email.2plot.dev", |
| 107 | + "description": "Email composition and delivery components.", |
| 108 | + }, |
| 109 | +] |
| 110 | + |
| 111 | +AFFILIATED: List[Dict[str, str]] = [ |
| 112 | + { |
| 113 | + "name": "Pip Install Python", |
| 114 | + "url": "https://pip-install-python.com", |
| 115 | + "description": "The original component documentation site.", |
| 116 | + }, |
| 117 | + { |
| 118 | + "name": "Pirate's Bargain", |
| 119 | + "url": "https://piratesbargain.com", |
| 120 | + "description": "Deal aggregator built on the same Dash stack.", |
| 121 | + }, |
| 122 | + { |
| 123 | + "name": "ai-agent.buzz", |
| 124 | + "url": "https://ai-agent.buzz", |
| 125 | + "description": "Agent tooling directory.", |
| 126 | + }, |
| 127 | +] |
| 128 | + |
| 129 | +EXTERNAL: List[Dict[str, Any]] = [ |
| 130 | + { |
| 131 | + "name": "Dash Mantine Components", |
| 132 | + "url": "https://www.dash-mantine-components.com", |
| 133 | + "description": "The UI component layer these docs are built with.", |
| 134 | + "llms_txt": "https://www.dash-mantine-components.com/llms.txt", |
| 135 | + }, |
| 136 | + { |
| 137 | + "name": "Plotly Dash documentation", |
| 138 | + "url": "https://dash.plotly.com", |
| 139 | + "description": "Upstream framework documentation.", |
| 140 | + }, |
| 141 | +] |
| 142 | + |
| 143 | +NETWORK_NAME = "The 2plot network" |
| 144 | +NETWORK_DESCRIPTION = ( |
| 145 | + "Open-source Dash component libraries by Pip Install Python. Each component " |
| 146 | + "has its own documentation site and its own llms.txt; 2plot.dev indexes all " |
| 147 | + "of them, and 2plot.ai is the hub." |
| 148 | +) |
| 149 | +HUB_URL = "https://2plot.dev" |
| 150 | + |
| 151 | +# The mark drawn in the header of the rendered llms.txt view: "2" + morse |
| 152 | +# encoding of "plot" + "ai", as columns of dots and dashes. |
| 153 | +# |
| 154 | +# No period glyph between the halves — the morse block already separates them, |
| 155 | +# and a literal "." next to it reads as punctuation dropped into a graphic. |
| 156 | +# The renderer turns a suffix ending in "i" into an upward flourish, so "ai" |
| 157 | +# draws as "a" plus that mark; `label` carries the real domain for screen |
| 158 | +# readers and the SVG <title>, which is the only place the dot belongs. |
| 159 | +# |
| 160 | +# Defined here rather than per-app because this module is copied verbatim into |
| 161 | +# every satellite — that is what keeps one mark across the network instead of |
| 162 | +# twelve slightly different ones. |
| 163 | +WORDMARK = { |
| 164 | + "morse": "plot", |
| 165 | + "prefix": "2", |
| 166 | + "suffix": "ai", |
| 167 | + "label": "2plot.ai", |
| 168 | +} |
| 169 | + |
| 170 | + |
| 171 | +def peers_for(app_url: str) -> List[Dict[str, str]]: |
| 172 | + """`PEERS` with this app removed. |
| 173 | +
|
| 174 | + A site listing itself as its own peer reads as generated rather than |
| 175 | + curated, and it wastes a slot in a list an agent may only skim. |
| 176 | + """ |
| 177 | + own = app_url.rstrip("/") |
| 178 | + return [p for p in PEERS if p["url"].rstrip("/") != own] |
| 179 | + |
| 180 | + |
| 181 | +def apply(app_url: str) -> None: |
| 182 | + """Publish the directory for the app served at ``app_url``. |
| 183 | +
|
| 184 | + Degrades rather than fails on older releases of the package. A satellite |
| 185 | + pinned behind this file should still boot: losing the directory, or losing |
| 186 | + the wordmark, is a degradation — refusing to start is not. |
| 187 | +
|
| 188 | + That matters during a staged rollout, when this module reaches satellites |
| 189 | + before the new package does. ``register_network`` arrived in 2.1 and its |
| 190 | + ``wordmark`` argument in 2.2, and Python raises ``TypeError`` on an unknown |
| 191 | + keyword, so the argument is only passed when the installed signature |
| 192 | + actually accepts it. |
| 193 | + """ |
| 194 | + try: |
| 195 | + from dash_improve_my_llms import register_network |
| 196 | + except ImportError: # pragma: no cover - only on <2.1 |
| 197 | + import warnings |
| 198 | + |
| 199 | + warnings.warn( |
| 200 | + "dash-improve-my-llms is older than 2.1, so the cross-host network " |
| 201 | + "directory will not be published. Upgrade to publish it.", |
| 202 | + RuntimeWarning, |
| 203 | + stacklevel=2, |
| 204 | + ) |
| 205 | + return |
| 206 | + |
| 207 | + import inspect |
| 208 | + |
| 209 | + extra: Dict[str, Any] = {} |
| 210 | + if "wordmark" in inspect.signature(register_network).parameters: |
| 211 | + extra["wordmark"] = WORDMARK |
| 212 | + |
| 213 | + register_network( |
| 214 | + name=NETWORK_NAME, |
| 215 | + description=NETWORK_DESCRIPTION, |
| 216 | + hub_url=HUB_URL, |
| 217 | + peers=peers_for(app_url), |
| 218 | + affiliated=AFFILIATED, |
| 219 | + external=EXTERNAL, |
| 220 | + **extra, |
| 221 | + ) |
0 commit comments