diff --git a/.gitignore b/.gitignore index c7f2b31bf..d41aec61c 100644 --- a/.gitignore +++ b/.gitignore @@ -80,3 +80,4 @@ CLAUDE.md # MkDocs GitHub metrics cache (generated at build time) docs/mkdocs/.cache/ +docs/content/javascripts/dir-repobeats-data.js diff --git a/docs/content/dir/.index b/docs/content/dir/.index index 4a0905095..7d43b9186 100644 --- a/docs/content/dir/.index +++ b/docs/content/dir/.index @@ -1,6 +1,7 @@ nav: - Get Started: - Overview: dir-overview.md + - Choosing a Setup: dir-choosing-a-setup.md - Quickstart: dir-quickstart.md - Usage Guide: dir-features-scenarios.md - Concepts: diff --git a/docs/content/dir/dir-choosing-a-setup.md b/docs/content/dir/dir-choosing-a-setup.md new file mode 100644 index 000000000..d24d84f86 --- /dev/null +++ b/docs/content/dir/dir-choosing-a-setup.md @@ -0,0 +1,139 @@ +# Choosing Your Directory Setup + +Not sure which Directory setup you need? Walk through the questions below, then find the +matching configuration — each shown as its own topology. + +
+
    +
  1. Who needs to discover your records? Only you → keep going · your team or organization → Networked · other organizations → Federated.
  2. +
  3. (Only you) Should others be able to pull them from a public registry? No → Private · Yes → Public-store.
  4. +
+
+ +## Recommended configurations + +The configurations below cover the useful combinations. In every diagram +dashed lines mean discovery (only CIDs/labels move), solid lines mean retrieval (record bytes move). + +### Private node { #private-node } + +Keep records to yourself. Store locally, no network. + +You store, search, sign, and pull locally. You can still pull records +inbound, both from a public registry and from a remote Directory node you authenticate to. + +What stays true either way: nobody else can discover or retrieve **your** records. + +```mermaid +flowchart LR + user(["You · dirctl"]) -->|"push · search · pull"| api + user -->|"🔑 logged in"| peer + subgraph node["Your node"] + api["API server"] --> store[("Local store
not network-reachable")] + end + reg[("Public OCI registry")] -->|"sync create --registry"| api + peer["Remote Directory node"] -->|"sync create"| api +``` + +Choose when: + +- Discovery = only you +- Store = bundled/local + +See [Local Deployment](dir-deployment-local.md) for more details. + +### Public-store node { #public-store-node } + +Let others retrieve, without a network. + +The store is a public registry (GHCR, Docker Hub). Anyone who knows the registry can pull +records straight from it — a specific CID or all of them — with `dirctl sync create +--registry`; your node need not even be online. This still isn't network discovery: there's no +DHT, so consumers point sync at your registry rather than finding you across a network. + +```mermaid +flowchart LR + subgraph node["Your node"] + api["API server"] + end + api -->|"records stored here"| reg[("Public OCI registry
GHCR · Docker Hub")] + other(["Another node"]) -->|"sync create --registry"| reg +``` + +Choose when: + +- Discovery = only you +- Store = public registry + +See [Local Deployment](dir-deployment-local.md) and [Store](dir-component-store.md) for more details. + +### Networked node { #networked-node } + +Be discoverable. + +A bootstrap connection puts the node on the DHT: it announces its records and can search +for records held by other nodes. + +Being networked also enables autosync — direct node-to-node transfer over libp2p. Autosync +is receiver-controlled: each node automatically pulls only from the peers in its own +trusted allow-list. So you autosync records from peers you list, and a peer that lists +you autosyncs yours. It is opt-in and off by default. + +Because it is receiver-controlled, your own autosync setting changes what you can pull in — +not what others can take from you: + +| Your store | Your autosync | Can others retrieve your records? | Can you retrieve others' records? | +|---|:--:|---|---| +| Local | off | Only a peer that lists you in its own allow-list, over libp2p | On-demand `sync create` only | +| Local | on | Same — your autosync does not change this | Yes, from peers in your own allow-list (+ on-demand) | +| Public | off | Yes — anyone pulls from the registry | On-demand `sync create` only | +| Public | on | Yes — anyone pulls from the registry | Yes, from peers in your own allow-list (+ on-demand) | + +```mermaid +flowchart TB + subgraph node["Your node · bootstrapped"] + api["API server"] --> store[("store · local or public")] + end + dht{{"P2P network · DHT"}} + peer(["Peer that trusts you"]) + api -. "1 · announce CIDs" .-> dht + dht -. "2 · discovers your CIDs" .-> peer + peer -->|"3 · autosync pulls record (direct, libp2p)"| api +``` + +*Choose when:* + +- Discovery = within your team or organization +- Store = local or public + +See [Local Deployment](dir-deployment-local.md), [Connecting to a Remote Directory](dir-deployment-local.md#connecting-to-a-remote-directory), and [Routing](dir-component-routing.md) for more details. + +### Federated { #federated } + +Exchange across organizations. + +Multiple production nodes peer under a shared trust root. Organizations discover each +other's records over a shared DHT and retrieve them with authenticated, authorized access +(SPIFFE mTLS + authorization policies). Establishing that shared trust root takes a one-time +SPIRE federation step, where each organization's SPIRE server exchanges trust bundles with +the others (see [Federation Bundle Profiles](dir-federation-profiles.md)). + +```mermaid +flowchart LR + dht{{"Shared DHT · GossipSub"}} + subgraph orgA["Organization A"] + a(["Node A"]) + end + subgraph orgB["Organization B"] + b(["Node B"]) + end + dht <-. "announce + discover" .-> a + dht <-. "announce + discover" .-> b + a <-->|"retrieve · SPIFFE mTLS + authz"| b +``` + +Choose when: + +- Discovery = other organizations + +See [Federation](dir-federation-overview.md) and [Trust Model](dir-component-trust-model.md) for more details. diff --git a/docs/content/javascripts/dir-repobeats-data.js b/docs/content/javascripts/dir-repobeats-data.js deleted file mode 100644 index 2186f2cfa..000000000 --- a/docs/content/javascripts/dir-repobeats-data.js +++ /dev/null @@ -1,2 +0,0 @@ -/* Generated by MkDocs hooks on build — do not edit by hand. */ -window.__DIR_REPOBEATS_DATA__ = {"repo":"agntcy/dir","recentCommits":150,"commitBars":[31,31,16,16,17,17,38,38,41,41,38,38],"openPrs":16,"prsOpened":138,"prsMerged":127,"issuesOpened":51,"issuesClosed":67,"issueWeeks":[17,38,41,38],"prWeeks":[17,38,41,38],"contributors":[{"login":"paralta","avatar_url":"https://avatars.githubusercontent.com/u/46568597?v=4","contributions":215},{"login":"agntcy-automation[bot]","avatar_url":"https://avatars.githubusercontent.com/in/3133123?v=4","contributions":153},{"login":"tkircsi","avatar_url":"https://avatars.githubusercontent.com/u/28192787?v=4","contributions":124},{"login":"ramizpolic","avatar_url":"https://avatars.githubusercontent.com/u/32913827?v=4","contributions":118},{"login":"arpad-csepi","avatar_url":"https://avatars.githubusercontent.com/u/21104922?v=4","contributions":99},{"login":"adamtagscherer","avatar_url":"https://avatars.githubusercontent.com/u/11526157?v=4","contributions":82},{"login":"csirmazbendeguz","avatar_url":"https://avatars.githubusercontent.com/u/19559023?v=4","contributions":67},{"login":"akijakya","avatar_url":"https://avatars.githubusercontent.com/u/39521244?v=4","contributions":46},{"login":"muscariello","avatar_url":"https://avatars.githubusercontent.com/u/168545?v=4","contributions":40},{"login":"build-agntcy","avatar_url":"https://avatars.githubusercontent.com/u/198542035?v=4","contributions":18},{"login":"dependabot[bot]","avatar_url":"https://avatars.githubusercontent.com/in/29110?v=4","contributions":16},{"login":"keraron","avatar_url":"https://avatars.githubusercontent.com/u/53857116?v=4","contributions":14}]}; diff --git a/docs/content/javascripts/dir-setup-chooser.js b/docs/content/javascripts/dir-setup-chooser.js new file mode 100644 index 000000000..dcc1f9c81 --- /dev/null +++ b/docs/content/javascripts/dir-setup-chooser.js @@ -0,0 +1,237 @@ +/* Copyright AGNTCY Contributors (https://github.com/agntcy) */ +/* SPDX-License-Identifier: Apache-2.0 */ + +/* Interactive "choose your setup" wizard for the Configurations page. + Only two things decide the configuration: how far your records need to be + discoverable, and (when they don't) whether others may pull them. So this is + a 1-2 step conditional flow that maps 1:1 to the four configurations and + deep-links to each section on the same page. Progressive enhancement: + replaces the static fallback list inside #setup-chooser. */ +(function () { + var SECTION_IDS = ["private-node", "public-store-node", "networked-node", "federated"]; + var activeWrap = null; + var activeTimer = null; + + function getSectionNodes(heading) { + var nodes = [heading]; + var el = heading.nextElementSibling; + while (el && el.tagName !== "H3") { + nodes.push(el); + el = el.nextElementSibling; + } + return nodes; + } + + function unwrapSection(wrap) { + if (!wrap || !wrap.parentNode) { + return; + } + var parent = wrap.parentNode; + while (wrap.firstChild) { + parent.insertBefore(wrap.firstChild, wrap); + } + wrap.remove(); + } + + function clearSectionHighlight() { + if (activeTimer) { + clearTimeout(activeTimer); + activeTimer = null; + } + if (activeWrap) { + unwrapSection(activeWrap); + activeWrap = null; + } + } + + function highlightSection(id) { + clearSectionHighlight(); + var heading = document.getElementById(id); + if (!heading) { + return; + } + + var nodes = getSectionNodes(heading); + var wrap = document.createElement("div"); + wrap.className = "sc-section-target sc-section-flash"; + heading.parentNode.insertBefore(wrap, heading); + nodes.forEach(function (node) { + wrap.appendChild(node); + }); + activeWrap = wrap; + + wrap.scrollIntoView({ behavior: "smooth", block: "start" }); + + activeTimer = setTimeout(function () { + wrap.classList.remove("sc-section-flash"); + activeTimer = null; + }, 2600); + } + + function flashFromHash() { + var id = location.hash.slice(1); + if (SECTION_IDS.indexOf(id) === -1) { + return; + } + requestAnimationFrame(function () { + highlightSection(id); + }); + } + + if (!window.__dirSetupSectionFlash) { + window.__dirSetupSectionFlash = true; + window.addEventListener("hashchange", flashFromHash); + } + + document$.subscribe(function () { + flashFromHash(); + + var mount = document.getElementById("setup-chooser"); + if (!mount) { + return; + } + + var DISCOVER = { + key: "discover", + title: "Who needs to discover your records?", + hint: "Discovery = others finding out what records you have, and you discovering what others have, over the network.", + options: [ + { v: "onlyme", label: "Only me", sub: "No one else needs to find them." }, + { v: "team", label: "My team or organization", sub: "Nodes and teammates connected to a shared DHT." }, + { v: "orgs", label: "Other organizations", sub: "Cross-organization discovery and exchange." }, + ], + }; + var STORE = { + key: "store", + title: "Should others be able to pull your records?", + hint: "Directly from a public registry (GHCR / Docker Hub), by CID — no network needed.", + options: [ + { v: "local", label: "No — keep them local", sub: "Bundled local store; not network-reachable." }, + { v: "public", label: "Yes — via a public registry", sub: "Anyone with the registry can pull them." }, + ], + }; + + var CONFIGS = { + privatenode: { n: 1, name: "Private node", href: "#private-node", why: "Records stay on your machine — no one else can discover or retrieve them." }, + publicstore: { n: 2, name: "Public-store node", href: "#public-store-node", why: "A public registry lets anyone pull your records by CID — without a network." }, + networked: { n: 3, name: "Networked node", href: "#networked-node", why: "On the DHT you announce your records and can discover others across the network." }, + federated: { n: 4, name: "Federated", href: "#federated", why: "Peered nodes exchange records across organizations under a shared trust root." }, + }; + + var state = {}; + var screen = "discover"; // discover | store | result + + function pick(s) { + if (s.discover === "orgs") return "federated"; + if (s.discover === "team") return "networked"; + return s.store === "public" ? "publicstore" : "privatenode"; + } + + function esc(s) { + return String(s).replace(/[&<>"]/g, function (c) { + return { "&": "&", "<": "<", ">": ">", '"': """ }[c]; + }); + } + + function optionsHTML(q) { + var chosen = state[q.key]; + return q.options + .map(function (o) { + var on = chosen === o.v; + return ( + '" + ); + }) + .join(""); + } + + function questionHTML(q, count, nextLabel, canBack) { + return ( + '
' + + '
' + esc(count) + "
" + + '
' + esc(q.title) + "
" + + '

' + esc(q.hint) + "

" + + '
' + optionsHTML(q) + "
" + + '
' + + '" + + '" + + "
" + ); + } + + function render() { + if (screen === "discover") { + var toResult = state.discover && state.discover !== "onlyme"; + mount.innerHTML = questionHTML(DISCOVER, "Question 1", toResult ? "See recommendation" : "Next", false); + return; + } + if (screen === "store") { + mount.innerHTML = questionHTML(STORE, "Question 2", "See recommendation", true); + return; + } + // result — just the recommendation and two actions + var c = CONFIGS[pick(state)]; + mount.innerHTML = + '
' + + '
Recommended
' + + '
' + c.n + ". " + esc(c.name) + "
" + + '

' + esc(c.why) + "

" + + '
' + + 'Jump to ' + esc(c.name) + " ↓" + + '' + + "
"; + } + + mount.addEventListener("click", function (e) { + var jump = e.target.closest ? e.target.closest(".sc-jump") : null; + if (jump && mount.contains(jump)) { + e.preventDefault(); + var href = jump.getAttribute("href") || ""; + if (href.charAt(0) === "#") { + history.pushState(null, "", href); + highlightSection(href.slice(1)); + } + jump.blur(); + return; + } + + var opt = e.target.closest ? e.target.closest(".sc-opt") : null; + if (opt && mount.contains(opt)) { + var key = screen === "store" ? "store" : "discover"; + state[key] = opt.getAttribute("data-v"); + render(); + return; + } + var btn = e.target.closest ? e.target.closest("[data-act]") : null; + if (!btn || !mount.contains(btn)) return; + var act = btn.getAttribute("data-act"); + if (act === "next") { + if (screen === "discover" && state.discover) { + screen = state.discover === "onlyme" ? "store" : "result"; + render(); + } else if (screen === "store" && state.store) { + screen = "result"; + render(); + } + } else if (act === "back") { + if (screen === "store") { screen = "discover"; render(); } + } else if (act === "restart") { + clearSectionHighlight(); + if (location.hash) { + history.replaceState(null, "", location.pathname + location.search); + } + state = {}; + screen = "discover"; + render(); + } + }); + + state = {}; + screen = "discover"; + render(); + }); +})(); diff --git a/docs/content/stylesheets/dir-setup-chooser.css b/docs/content/stylesheets/dir-setup-chooser.css new file mode 100644 index 000000000..ef4234ab9 --- /dev/null +++ b/docs/content/stylesheets/dir-setup-chooser.css @@ -0,0 +1,283 @@ +/* Copyright AGNTCY Contributors (https://github.com/agntcy) */ +/* SPDX-License-Identifier: Apache-2.0 */ + +/* Interactive setup chooser. Uses Material theme variables so it adapts to + light/dark automatically. */ + +.setup-chooser { + margin: 1.2em 0 1.6em; +} + +/* card */ +.setup-chooser .sc-card { + border: 1px solid var(--md-default-fg-color--lightest); + border-radius: 12px; + padding: 20px 22px; + background: var(--md-default-bg-color); + box-shadow: var(--md-shadow-z1); +} +.setup-chooser .sc-count { + font-size: 0.72rem; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--md-default-fg-color--light); + font-family: var(--md-code-font-family, monospace); +} +.setup-chooser .sc-title { + font-size: 1.15rem; + font-weight: 700; + margin: 4px 0 2px; +} +.setup-chooser .sc-hint { + color: var(--md-default-fg-color--light); + font-size: 0.8rem; + margin: 0 0 16px; +} + +/* options */ +.setup-chooser .sc-options { + display: grid; + gap: 8px; +} +.setup-chooser .sc-opt { + display: flex; + align-items: flex-start; + gap: 12px; + text-align: left; + width: 100%; + cursor: pointer; + border: 1.5px solid var(--md-default-fg-color--lightest); + border-radius: 9px; + padding: 12px 14px; + background: var(--md-default-bg-color); + color: var(--md-default-fg-color); + font: inherit; + transition: border-color 0.15s, background 0.15s; +} +.setup-chooser .sc-opt:hover { + border-color: var(--md-accent-fg-color); + background: var(--md-code-bg-color); +} +.setup-chooser .sc-opt:focus-visible { + outline: 2px solid var(--md-accent-fg-color); + outline-offset: 2px; +} +.setup-chooser .sc-opt[aria-checked="true"] { + border-color: var(--md-accent-fg-color); + background: color-mix(in srgb, var(--md-accent-fg-color) 10%, transparent); +} +.setup-chooser .sc-radio { + flex: 0 0 auto; + width: 20px; + height: 20px; + border-radius: 50%; + border: 1.5px solid var(--md-default-fg-color--lighter); + display: grid; + place-items: center; + margin-top: 1px; +} +.setup-chooser .sc-radio::after { + content: ""; + width: 10px; + height: 10px; + border-radius: 50%; + background: var(--md-accent-fg-color); + opacity: 0; + transform: scale(0.5); + transition: opacity 0.15s, transform 0.15s; +} +.setup-chooser .sc-opt[aria-checked="true"] .sc-radio { + border-color: var(--md-accent-fg-color); +} +.setup-chooser .sc-opt[aria-checked="true"] .sc-radio::after { + opacity: 1; + transform: scale(1); +} +.setup-chooser .sc-olabel { + display: block; + font-weight: 600; +} +.setup-chooser .sc-osub { + display: block; + color: var(--md-default-fg-color--light); + font-size: 0.8rem; + margin-top: 1px; +} + +/* nav + buttons */ +.setup-chooser .sc-nav { + display: flex; + justify-content: space-between; + gap: 10px; + margin-top: 18px; +} +.setup-chooser .sc-btn { + font: inherit; + font-weight: 600; + font-size: 0.8rem; + border-radius: 8px; + padding: 9px 15px; + cursor: pointer; + border: 1px solid var(--md-default-fg-color--lighter); + background: var(--md-default-bg-color); + color: var(--md-default-fg-color); + text-decoration: none; + display: inline-flex; + align-items: center; + gap: 6px; +} +.setup-chooser .sc-btn:hover { + background: var(--md-code-bg-color); +} +.setup-chooser .sc-btn:focus-visible { + outline: 2px solid var(--md-accent-fg-color); + outline-offset: 2px; +} +.setup-chooser .sc-btn[disabled] { + opacity: 0.4; + cursor: not-allowed; +} +.setup-chooser .sc-ghost { + border-color: transparent; + color: var(--md-default-fg-color--light); +} +.setup-chooser .sc-primary { + background: var(--md-accent-fg-color); + border-color: var(--md-accent-fg-color); + color: #fff; +} +/* Anchor primary buttons need explicit state colors — Material's + .md-typeset a:hover/:focus sets accent text color and hides white-on-accent labels. */ +.setup-chooser .sc-primary:hover, +.setup-chooser .sc-primary:focus-visible { + background: var(--md-accent-fg-color); + filter: brightness(1.08); + color: #fff; +} +.md-typeset .setup-chooser a.sc-primary, +.md-typeset .setup-chooser a.sc-primary:hover, +.md-typeset .setup-chooser a.sc-primary:focus, +.md-typeset .setup-chooser a.sc-primary:focus-visible, +.md-typeset .setup-chooser a.sc-primary:active, +.md-typeset .setup-chooser a.sc-primary:visited { + background: var(--md-accent-fg-color); + border-color: var(--md-accent-fg-color); + color: #fff; + text-decoration: none; +} +.md-typeset .setup-chooser a.sc-primary:hover, +.md-typeset .setup-chooser a.sc-primary:focus, +.md-typeset .setup-chooser a.sc-primary:focus-visible, +.md-typeset .setup-chooser a.sc-primary:active { + filter: brightness(1.08); +} + +/* result */ +.setup-chooser .sc-badge { + display: inline-block; + font-size: 0.68rem; + letter-spacing: 0.08em; + text-transform: uppercase; + font-weight: 700; + color: var(--md-accent-fg-color); + background: color-mix(in srgb, var(--md-accent-fg-color) 12%, transparent); + padding: 3px 9px; + border-radius: 99px; + margin-bottom: 8px; +} +.setup-chooser .sc-rec-name { + font-size: 1.3rem; + font-weight: 700; + margin-bottom: 4px; +} +.setup-chooser .sc-why { + color: var(--md-default-fg-color--light); + margin: 0 0 14px; +} +.setup-chooser .sc-cta { + display: flex; + flex-wrap: wrap; + gap: 10px; + align-items: center; +} + +/* Dark mode: --md-default-fg-color--lightest matches the page background in slate, + so card/option borders disappear unless overridden. */ +[data-md-color-scheme="slate"] .setup-chooser .sc-card { + border-color: rgba(227, 227, 227, 0.18); + background: var(--md-code-bg-color); + box-shadow: 0 4px 24px rgba(0, 0, 0, 0.35); +} + +[data-md-color-scheme="slate"] .setup-chooser .sc-opt { + border-color: rgba(227, 227, 227, 0.16); + background: var(--md-default-bg-color); +} + +[data-md-color-scheme="slate"] .setup-chooser .sc-opt:hover { + background: rgb(12, 31, 54); +} + +[data-md-color-scheme="slate"] .setup-chooser .sc-btn:not(.sc-primary):not(.sc-ghost) { + border-color: rgba(227, 227, 227, 0.22); +} + +[data-md-color-scheme="slate"] .setup-chooser .sc-radio { + border-color: rgba(227, 227, 227, 0.35); +} + +[data-md-color-scheme="slate"] .setup-chooser .sc-primary, +[data-md-color-scheme="slate"] .setup-chooser .sc-primary:hover, +[data-md-color-scheme="slate"] .setup-chooser .sc-primary:focus-visible { + color: rgb(3, 20, 43); +} +[data-md-color-scheme="slate"] .md-typeset .setup-chooser a.sc-primary, +[data-md-color-scheme="slate"] .md-typeset .setup-chooser a.sc-primary:hover, +[data-md-color-scheme="slate"] .md-typeset .setup-chooser a.sc-primary:focus, +[data-md-color-scheme="slate"] .md-typeset .setup-chooser a.sc-primary:focus-visible, +[data-md-color-scheme="slate"] .md-typeset .setup-chooser a.sc-primary:active, +[data-md-color-scheme="slate"] .md-typeset .setup-chooser a.sc-primary:visited { + color: rgb(3, 20, 43); +} + +/* Target configuration section: persistent frame after "Jump to …". */ +.md-typeset .sc-section-target { + scroll-margin-top: calc(var(--md-header-height, 4rem) + 0.75rem); + margin: 0 -16px 1.2em; + padding: 8px 16px 4px; + border-radius: 12px; + border: 1.5px solid var(--md-accent-fg-color); + box-shadow: inset 3px 0 0 var(--md-accent-fg-color); +} + +.md-typeset .sc-section-target.sc-section-flash { + animation: sc-section-flash 2.4s ease-out forwards; +} + +@keyframes sc-section-flash { + 0%, + 18% { + background: color-mix(in srgb, var(--md-accent-fg-color) 16%, transparent); + } + 100% { + background: transparent; + } +} + +[data-md-color-scheme="slate"] .md-typeset .sc-section-target { + border-color: color-mix(in srgb, var(--md-accent-fg-color) 75%, transparent); +} + +[data-md-color-scheme="slate"] .md-typeset .sc-section-target.sc-section-flash { + animation-name: sc-section-flash-slate; +} + +@keyframes sc-section-flash-slate { + 0%, + 18% { + background: color-mix(in srgb, var(--md-accent-fg-color) 22%, transparent); + } + 100% { + background: transparent; + } +} diff --git a/docs/mkdocs/hooks.py b/docs/mkdocs/hooks.py index 5ed2607e6..bf0ab67e7 100644 --- a/docs/mkdocs/hooks.py +++ b/docs/mkdocs/hooks.py @@ -72,13 +72,16 @@ def on_pre_build(config, **kwargs): payload: dict[str, Any] = {"repo": _DEFAULT_REPO, "embedId": embed_id} else: payload = _load_repobeats_payload(_DEFAULT_REPO) - output.write_text( + content = ( "/* Generated by MkDocs hooks on build — do not edit by hand. */\n" "window.__DIR_REPOBEATS_DATA__ = " + json.dumps(payload, separators=(",", ":")) - + ";\n", - encoding="utf-8", + + ";\n" ) + # Only write when content changes — writing on every build updates mtime + # inside docs_dir and triggers mkdocs serve's file watcher in a loop. + if not output.exists() or output.read_text(encoding="utf-8") != content: + output.write_text(content, encoding="utf-8") def on_page_markdown(markdown: str, **kwargs) -> str: diff --git a/docs/mkdocs/mkdocs.yml b/docs/mkdocs/mkdocs.yml index 95b61ddf2..9afd15007 100644 --- a/docs/mkdocs/mkdocs.yml +++ b/docs/mkdocs/mkdocs.yml @@ -44,6 +44,7 @@ extra_css: - stylesheets/agntcy-docs.css - stylesheets/dir-landing.css - stylesheets/dirctl-terminal.css + - stylesheets/dir-setup-chooser.css extra_javascript: - javascripts/dirctl-demo-data.js @@ -55,6 +56,7 @@ extra_javascript: - javascripts/card-links.js - javascripts/external-links.js - javascripts/palette-tooltip-fix.js + - javascripts/dir-setup-chooser.js markdown_extensions: - admonition @@ -65,6 +67,9 @@ markdown_extensions: - pymdownx.superfences - pymdownx.tabbed: alternate_style: true + slugify: !!python/object/apply:pymdownx.slugs.slugify + kwds: + case: lower - pymdownx.emoji: emoji_index: !!python/name:material.extensions.emoji.twemoji emoji_generator: !!python/name:material.extensions.emoji.to_svg @@ -114,6 +119,7 @@ nav: - Documentation: - Get Started: - Overview: dir/dir-overview.md + - Choosing a Setup: dir/dir-choosing-a-setup.md - Quickstart: dir/dir-quickstart.md - Usage Guide: dir/dir-features-scenarios.md - Concepts: