# ADR: Every diagram opens on its own page

Status: accepted, 2026-09-02, shipped as 1.0.0.25. Steve's ask, from his
phone, after reading the day's records: "All diagrams should be separate
and open in a new page, so you can zoom in and follow."

## In plain words

Every diagram opens on a page of its own, where you can zoom in on a phone and search its text, and every link in a document opens in a new tab so the site stays open behind it.

What that is worth: a reader can follow a drawing on any screen, and the organization's architecture pictures work on a phone as well as on a big monitor.

## Context

The infrastructure diagram (ADR: Docs and testing, addendum) sat inside the
Hosting page and the README as a PNG scaled to the width of the document
dialog. On a laptop that reads; on a phone the dialog is about 340 pixels
wide and the diagram's text becomes texture. The Data Flow page had a text
diagram in a code block, which on a phone scrolls sideways inside the
dialog and cannot be zoomed at all. And every link inside a document had no
target, so a tap on a GitHub link in a Files section navigated the app's
own tab away from the app.

## Decision

1. **A diagram is a page.** `/api/docs/diagrams/{name}` serves a small HTML
   document with the SVG inlined: the title in the tab, the palette, a
   viewport line so a phone can pinch to zoom, and the text left selectable
   so a reader can find a name with the browser's own search. The names
   come from a catalog beside the documents catalog; anything else is a
   404 and never a file read.
2. **The record keeps a preview.** The PNG stays inline at the document's
   width, as the link to the page, with a caption under it that says where
   it opens. The picture still gives the shape at a glance; the page is
   where you read it.
3. **The data flow is drawn.** `docs/images/dataflow.svg` replaces the text
   diagram in the Data Flow page, in the same style as the infrastructure
   drawing: two lanes, the read path top to bottom and a bid's path beside
   it, every box a file with its path under the title, and the refetch loop
   that ties the two together. Like the first drawing it is redrawn when
   the code moves; the SVG is generated from a short layout script kept
   with the session notes, not drawn by hand.
4. **Links in a document open in a new tab.** A `marked` hook adds
   `target="_blank"` and `rel="noopener"` to every link in a served document
   and turns the site's own absolute links into relative ones. The
   documents name the live domain in full because they must also read right
   on GitHub; in the app the same link stays on whichever host is serving,
   so a checkout on localhost opens its own diagram page and not the live
   one.

## In the code

The endpoint, in `api/TheYard.Api/Program.cs`, and the catalog beside the
documents in `api/TheYard.Api/DocumentationCatalog.cs`:

```csharp
// A diagram on its own page (ADR-020): the SVG inlined in a small HTML document,
// so it opens in a new tab, zooms with the browser, and keeps its text
// selectable. The name is looked up in the catalog; nothing else is read.
app.MapGet("/api/docs/diagrams/{name}", Diagram)
    .WithName("GetDiagram")
    .WithTags("Documents")
    .WithSummary("One diagram on its own page")
    .WithDescription("The SVG inlined in a small HTML document, so it zooms with the browser and keeps its text selectable.")
    .Produces<string>(StatusCodes.Status200OK, "text/html")
    .ProducesProblem(StatusCodes.Status404NotFound);
```
*Live from [`api/TheYard.Api/Endpoints/DocumentationEndpoints.cs`](https://github.com/SteveStout/TheYard/blob/1d6b50b/api/TheYard.Api/Endpoints/DocumentationEndpoints.cs#L49-L58), region `diagram-page`, read from this build at 1d6b50b.*

```csharp
/// <summary>Diagram name to its SVG file and page title (ADR-020): each opens on its own page at /api/docs/diagrams/{name}.</summary>
public static readonly IReadOnlyDictionary<string, (string File, string Title)> Diagrams = new Dictionary<string, (string File, string Title)>(StringComparer.Ordinal)
{
    ["infrastructure"] = ("docs/images/infrastructure.svg", "TheYard infrastructure"),
    ["dataflow"] = ("docs/images/dataflow.svg", "TheYard data flow"),
    ["erd"] = ("docs/images/erd.svg", "TheYard's database"),
    ["two-sites"] = ("docs/images/two-sites.svg", "TheYard's two sites"),
    ["server-page"] = ("docs/images/server-page.svg", "A page drawn on the server"),
    ["compiled-ahead"] = ("docs/images/compiled-ahead.svg", "Compiled before it ships"),
    ["sql-vs-cosmos"] = ("docs/images/sql-vs-cosmos.svg", "SQL Server and Cosmos DB, side by side"),
    ["ui-architecture"] = ("docs/images/ui-architecture.svg", "TheYard's UI, layer by layer"),
    ["rings"] = ("docs/images/rings.svg", "TheYard's rings"),
    ["onion-side-by-side"] = ("docs/images/onion-side-by-side.svg", "The textbook onion, and TheYard's"),
    ["bid-walk"] = ("docs/images/bid-walk.svg", "One bid through the rings"),
    ["port-adapter"] = ("docs/images/port-adapter.svg", "The inner ring owns the interface"),
};
```
*Live from [`api/TheYard.Api/DocumentationCatalog.cs`](https://github.com/SteveStout/TheYard/blob/1d6b50b/api/TheYard.Api/DocumentationCatalog.cs#L156-L171), region `diagrams`, read from this build at 1d6b50b.*

The page itself, `api/TheYard.Api/DiagramPage.cs`. The palette is repeated
here and in each SVG on purpose: the page carries no bundle, so the tokens
file is not loaded, and a palette change touches the drawings anyway (ADR:
The palette):

```csharp
/// <summary>
/// The page around one SVG. The palette is repeated here on purpose: no bundle loads on this page. So is the
/// operator's look (ADR: The glass look, the addendum on the operator's look): the drawing sits on the one panel
/// every page wears, the glass with the dark green rule and two corner brackets, at the token sheet's values, over
/// the site's own ground, the green grey to white gradient, with the site's teal for a link. The tab carries the site's own icon, the one index.html and the reference page
/// carry, so a browser does not go looking for /favicon.ico and log a 404 (read in Chrome at 390, 1400 and 1406).
/// </summary>
public static string Render(string title, string svg, string sourcePath, string face = "")
{
    // A standalone SVG file may open with an XML prolog, which HTML must not carry.
    int start = svg.IndexOf("<svg", StringComparison.OrdinalIgnoreCase);
    string inline = start > 0 ? svg[start..] : svg;
    string safeTitle = WebUtility.HtmlEncode(title);
    return $$"""
        <!doctype html>
        <html lang="en">
        <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>{{safeTitle}}</title>
        <link rel="icon" href="{{ApiDocument.Favicon}}">
        {{face}}
        <style>
          html { background: #f3f7f6; }
          html, body { margin: 0; color: #5e5653; font-family: 'IBM Plex Sans', 'Segoe UI', system-ui, Arial, sans-serif; font-variant-numeric: tabular-nums; }
          body { min-height: 100vh; background: linear-gradient(90deg, #deeaed 0%, #f3f7f6 45%, #ffffff 100%) fixed; }
          header { display: flex; flex-wrap: wrap; gap: 4px 16px; align-items: baseline; padding: 12px 16px 8px; }
          h1 { margin: 0; font-size: 16px; color: #3f3a37; }
          header p { margin: 0; font-size: 13px; color: #5f636c; }
          a { color: #12677f; }
          main { padding: 0 16px 16px; }
          .op-glass { position: relative; max-width: 1600px; margin: 0 auto; padding: 8px; background: rgba(255, 255, 255, 0.42); -webkit-backdrop-filter: blur(20px) saturate(1.5); backdrop-filter: blur(20px) saturate(1.5); border: 1px solid rgba(16, 73, 90, 0.18); border-top: 3px solid #10495a; border-radius: 10px; box-shadow: 0 8px 24px rgba(16, 73, 90, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.8), inset 0 0 0 1px rgba(255, 255, 255, 0.35); }
          .op-glass::before, .op-glass::after { content: ''; position: absolute; width: 18px; height: 18px; border-color: #10495a; border-style: solid; pointer-events: none; }
          .op-glass::before { left: -1px; top: -3px; border-width: 3px 0 0 2px; border-top-left-radius: 10px; }
          .op-glass::after { right: -1px; bottom: -1px; border-width: 0 2px 2px 0; border-bottom-right-radius: 10px; }
          main svg { display: block; width: 100%; height: auto; }
        </style>
        </head>
        <body>
        <header>
          <h1>{{safeTitle}}</h1>
          <p>Pinch or Ctrl+scroll to zoom; the text is selectable. <a href="https://github.com/SteveStout/TheYard/blob/main/{{sourcePath}}">Source</a> &middot; <a href="/">TheYard</a></p>
        </header>
        <main>
        <figure class="op-glass">
        {{inline}}
        </figure>
        </main>
        </body>
        </html>
        """;
}
```
*Live from [`api/TheYard.Api/DiagramPage.cs`](https://github.com/SteveStout/TheYard/blob/1d6b50b/api/TheYard.Api/DiagramPage.cs#L14-L65), region `page`, read from this build at 1d6b50b.*

The hook that makes links leave the dialog without leaving the app, in
`src/lib/markdown.ts` since 1.0.0.141, when the renderer moved into a chunk
of its own (ADR: Code that reads like code, addendum):

```ts
// Links in a served document lead out of the app (GitHub, a diagram page), so
// they open in a new tab and the dialog stays where the reader was. The docs
// name the live domain in full, which keeps them right on GitHub; here the same
// links are made relative, so a checkout on localhost opens its own diagram
// page and not the live one (ADR-020).
const SITE = 'https://theyard.stevenstout.biz/';
marked.use({
  hooks: {
    postprocess(html: string) {
      return html
        .replaceAll(`href="${SITE}`, 'href="/')
        .replace(/<a href="(?!#)/g, '<a target="_blank" rel="noopener" href="');
    },
  },
});
```
*Live from [`src/lib/markdown.ts`](https://github.com/SteveStout/TheYard/blob/1d6b50b/src/lib/markdown.ts#L20-L34), region `doc-links`, read from this build at 1d6b50b.*

The tests, in `api/TheYard.Tests/DiagramPageTests.cs`: every name in the
catalog opens as HTML with its SVG and its title, an unknown name is a 404,
and the XML prolog a standalone SVG may carry never reaches the page:

```csharp
[Fact]
public async Task Every_diagram_in_the_catalog_opens_as_a_page_with_its_svg_inlined()
{
    Assert.NotEmpty(DocumentationCatalog.Diagrams);
    foreach (var (name, diagram) in DocumentationCatalog.Diagrams)
    {
        var response = await _client.GetAsync($"/api/docs/diagrams/{name}");
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        Assert.Equal("text/html", response.Content.Headers.ContentType?.MediaType);
        string body = await response.Content.ReadAsStringAsync();
        // The title goes through HtmlEncode on the way into the tab, so this
        // asks for the encoded form. Asserting the raw string only ever passed
        // because the first two diagram titles had nothing in them to encode:
        // the ERD arrived with an apostrophe and the assertion fell over.
        Assert.Contains($"<title>{WebUtility.HtmlEncode(diagram.Title)}</title>", body);
        Assert.Contains("name=\"viewport\"", body);
        Assert.Contains("<svg", body);
        Assert.Contains("</svg>", body);
        Assert.DoesNotContain("<?xml", body);
    }
}

[Theory]
[InlineData("/api/docs/diagrams/nope")]
[InlineData("/api/docs/diagrams/infrastructure.svg")]
[InlineData("/api/docs/diagrams")]
public async Task An_unknown_diagram_is_a_404_not_a_file_read(string path)
{
    var response = await _client.GetAsync(path);
    Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}

[Fact]
public void The_page_drops_an_xml_prolog_and_keeps_the_svg()
{
    string page = DiagramPage.Render("A & B", "<?xml version=\"1.0\"?>\n<svg xmlns=\"http://www.w3.org/2000/svg\"><title>t</title></svg>", "docs/images/x.svg");

    Assert.Contains("<title>A &amp; B</title>", page);
    Assert.DoesNotContain("<?xml", page);
    Assert.Contains("<svg xmlns=\"http://www.w3.org/2000/svg\"><title>t</title></svg>", page);
    Assert.Contains("blob/main/docs/images/x.svg", page);
    // The site's own icon, so no browser asks for /favicon.ico and logs a 404.
    Assert.Contains($"<link rel=\"icon\" href=\"{ApiDocument.Favicon}\">", page);
}

[Fact]
public void An_apostrophe_in_a_title_reaches_the_tab_encoded()
{
    string page = DiagramPage.Render("TheYard's database", "<svg></svg>", "docs/images/erd.svg");

    Assert.Contains("<title>TheYard&#39;s database</title>", page);
    Assert.DoesNotContain("<title>TheYard's database</title>", page);
}

// #region about-page
/// <summary>
/// /about, the page a search for his name should find (AboutPage.cs), on this
/// class's pattern: one small document, served by the API, its own head for a
/// search engine and an unfurler, and a Person in its structured data that says
/// his name, his title, his two profiles and his city, and nothing more.
/// </summary>
[Fact]
public async Task The_about_page_is_served_with_its_own_head_and_a_person_that_says_no_more_than_it_should()
{
    var response = await _client.GetAsync("/about");
    Assert.Equal(HttpStatusCode.OK, response.StatusCode);
    Assert.Equal("text/html", response.Content.Headers.ContentType?.MediaType);
    string body = await response.Content.ReadAsStringAsync();

    Assert.Contains($"<title>{WebUtility.HtmlEncode(AboutPage.Title)}</title>", body);
    Assert.Contains("<h1>Steven Stout</h1>", body);
    Assert.Contains("name=\"viewport\"", body);
    // The test host has no Site:Url, so the page names itself from the request.
    Assert.Matches("<link rel=\"canonical\" href=\"https?://[^\"]+/about\">", body);
    foreach (string property in new[] { "og:url", "og:title", "og:description", "og:image" })
    {
        Assert.Contains($"property=\"{property}\"", body);
    }
    Assert.InRange(AboutPage.Description.Length, 50, 160);
    Assert.Contains("href=\"/api/docs/resume\"", body);
    foreach (string profile in AboutPage.Profiles)
    {
        Assert.Contains($"href=\"{profile}\"", body);
    }

    // Nothing fetched from another host: no stylesheet, script or font from anywhere else.
    Assert.DoesNotMatch("<link[^>]+rel=\"stylesheet\"", body);
    Assert.DoesNotMatch("<script[^>]+src=", body);
    Assert.DoesNotContain("fonts.g", body, StringComparison.Ordinal);
    Assert.DoesNotContain("\u2014", body, StringComparison.Ordinal);

    // The person: name, title, the two profiles, the city. No street, no postcode, no
    // email and no phone number, in the data or on the page.
    Match data = Regex.Match(body, "<script type=\"application/ld\\+json\">(.*?)</script>", RegexOptions.Singleline);
    Assert.True(data.Success, "/about should carry its structured data");
    using var person = JsonDocument.Parse(data.Groups[1].Value);
    JsonElement root = person.RootElement;
    Assert.Equal("Person", root.GetProperty("@type").GetString());
    Assert.Equal("Steven Stout", root.GetProperty("name").GetString());
    Assert.Equal(AboutPage.JobTitle, root.GetProperty("jobTitle").GetString());
    Assert.Equal(AboutPage.Profiles, root.GetProperty("sameAs").EnumerateArray().Select(item => item.GetString()!).ToArray());
    JsonElement address = root.GetProperty("address");
    Assert.Equal("Saint Charles", address.GetProperty("addressLocality").GetString());
    Assert.Equal(new[] { "@type", "addressLocality", "addressRegion", "addressCountry" }, address.EnumerateObject().Select(field => field.Name).ToArray());
    Assert.False(root.TryGetProperty("email", out _) || root.TryGetProperty("telephone", out _) || root.TryGetProperty("birthDate", out _));
    string text = Regex.Replace(body, "\"@(context|type|id)\"", "");
    Assert.DoesNotMatch(@"[\w.+-]+@[\w-]+\.[\w.]+", text);
    Assert.DoesNotMatch(@"\(?\b\d{3}\)?[ .-]\d{3}[ .-]\d{4}\b", body);
}

/// <summary>
/// The About page and the diagram pages wear the site's face from the site's own file
/// (1.0.3.148): SiteFace finds the hashed woff2 under wwwroot/assets and the pages carry
/// its declaration; with no built site beside the process they carry none and the
/// fallback face in their own rule stands.
/// </summary>
[Fact]
public void The_pages_the_api_draws_wear_the_site_face_from_its_own_file()
{
    string assets = Path.Combine(Path.GetTempPath(), "theyard-siteface-" + Guid.NewGuid().ToString("N"));
    Directory.CreateDirectory(assets);
    try
    {
        Assert.Equal(string.Empty, SiteFace.Find(assets));
        File.WriteAllBytes(Path.Combine(assets, "ibm-plex-sans-latin-BgnScaL7.woff2"), [0]);
        string face = SiteFace.Find(assets);
        Assert.Contains("url('/assets/ibm-plex-sans-latin-BgnScaL7.woff2') format('woff2')", face);
        Assert.Contains("font-family: 'IBM Plex Sans'", face);
        Assert.Contains(face, AboutPage.Render("https://theyard.stevenstout.biz/", AboutPage.LeadImage, face));
        Assert.Contains(face, DiagramPage.Render("A drawing", "<svg xmlns=\"http://www.w3.org/2000/svg\"></svg>", "docs/images/a.svg", face));
    }
    finally
    {
        Directory.Delete(assets, true);
    }
    Assert.Equal(string.Empty, SiteFace.Find(Path.Combine(assets, "gone")));
    Assert.DoesNotContain("fonts.googleapis.com", DiagramPage.Render("A drawing", "<svg></svg>", "docs/images/a.svg"));
    Assert.DoesNotContain("@font-face", AboutPage.Render("https://theyard.stevenstout.biz/"));
}

[Fact]
public void Each_site_names_itself_on_the_about_page()
{
    string page = AboutPage.Render("https://theyard-cosmos.stevenstout.biz/");

    Assert.Contains("<link rel=\"canonical\" href=\"https://theyard-cosmos.stevenstout.biz/about\">", page);
    Assert.Contains("property=\"og:url\" content=\"https://theyard-cosmos.stevenstout.biz/about\"", page);
    Assert.Contains("\"url\":\"https://theyard-cosmos.stevenstout.biz/\"", page);
}

/// <summary>
/// How I lead, the section in his own words: four habits, each with a link to
/// the place on the site or in the repository that shows it, and the picture
/// beside it drawn with its size, or not at all when none is named.
/// </summary>
[Fact]
public void The_about_page_says_how_he_leads_and_links_each_habit_to_its_proof()
{
    string page = AboutPage.Render("https://theyard.stevenstout.biz/");

    Assert.Contains("<h2 id=\"how-i-lead\">How I lead</h2>", page);
    foreach (string habit in new[] { "Decisions written down.", "Tests that guard the business.", "Tradeoffs in the open.", "Coaching inside the system." })
    {
        Assert.Contains($"<strong>{habit}</strong>", page);
    }
    Assert.Contains("href=\"https://github.com/SteveStout/TheYard/tree/main/docs\"", page);
    Assert.Contains("href=\"https://github.com/SteveStout/TheYard/blob/main/data/test-results.json\"", page);
    Assert.Contains("href=\"https://github.com/SteveStout/TheYard/blob/main/docs/decisions/ADR-059-a-second-store-priced.md\"", page);

    // The picture is drawn with its words and its size, so it holds its room before it arrives;
    // with none named there is no empty frame.
    Assert.Contains($"<img src=\"{AboutPage.LeadImage}\"", page);
    Assert.Contains("width=\"960\" height=\"540\"", page);
    Assert.DoesNotContain("<figure", AboutPage.Render("https://theyard.stevenstout.biz/", null));
    string pictured = AboutPage.Render("https://theyard.stevenstout.biz/", "/about-lead.webp");
    Assert.Contains($"<figure class=\"lead-image\"><img src=\"/about-lead.webp\" alt=\"{AboutPage.LeadImageAlt}\"", pictured);
}
// #endregion about-page
```
*Live from [`api/TheYard.Tests/DiagramPageTests.cs`](https://github.com/SteveStout/TheYard/blob/1d6b50b/api/TheYard.Tests/DiagramPageTests.cs#L19-L196), region `page-tests`, read from this build at 1d6b50b.*

## Consequences

- The page is on the domain under the API's no-cache rule (ADR: Cache
  headers), so a redrawn diagram shows on the next open.
- An SVG's own `<style>` rules are unscoped once inlined; the page uses
  element selectors only, so nothing collides. A future drawing keeps to
  the same class names.
- Screenshots are not diagrams and stay inline; a tap on one opens nothing.
  If that turns out to be wanted, the hook is the place.
- The Data Flow page lost its text diagram. The drawing carries every box
  and path the text had; the text version stays in the history at
  1.0.0.24 if anyone misses it.

## Where it sits

Diagram pages are served by the host Api, where DiagramPage.cs builds the HTML around an SVG and DocumentationCatalog.Diagrams names which drawings exist; the link hook sits in the front end in src/lib/markdown.ts. DiagramPage has one job, wrapping a drawing in a page, which is single responsibility, and the catalog is the one authority the sidebar's list is tested against. The cost is the palette repeated in the page and in each SVG, since the page loads no bundle. Drawings that had to be interactive would belong inside the React app instead of on a page of their own.

## Files

- [`api/TheYard.Api/Endpoints/DocumentationEndpoints.cs`](https://github.com/SteveStout/TheYard/blob/main/api/TheYard.Api/Endpoints/DocumentationEndpoints.cs): the `/api/docs/diagrams/{name}` endpoint.
- [`api/TheYard.Api/DocumentationCatalog.cs`](https://github.com/SteveStout/TheYard/blob/main/api/TheYard.Api/DocumentationCatalog.cs): the diagram catalog beside the documents.
- [`api/TheYard.Api/DiagramPage.cs`](https://github.com/SteveStout/TheYard/blob/main/api/TheYard.Api/DiagramPage.cs): the HTML page around an SVG.
- [`api/TheYard.Tests/DiagramPageTests.cs`](https://github.com/SteveStout/TheYard/blob/main/api/TheYard.Tests/DiagramPageTests.cs): the page tests.
- [`src/lib/markdown.ts`](https://github.com/SteveStout/TheYard/blob/main/src/lib/markdown.ts): the link hook, beside the renderer it belongs to.
- [`docs/images/dataflow.svg`](https://github.com/SteveStout/TheYard/blob/main/docs/images/dataflow.svg) and [`docs/images/dataflow.png`](https://github.com/SteveStout/TheYard/blob/main/docs/images/dataflow.png): the new drawing and its preview; [`docs/images/infrastructure.svg`](https://github.com/SteveStout/TheYard/blob/main/docs/images/infrastructure.svg) the first one.
- [`docs/app-architecture/DATAFLOW.md`](https://github.com/SteveStout/TheYard/blob/main/docs/app-architecture/DATAFLOW.md), [`docs/hosting/HOSTING.md`](https://github.com/SteveStout/TheYard/blob/main/docs/hosting/HOSTING.md), [`README.md`](https://github.com/SteveStout/TheYard/blob/main/README.md): the previews and their captions.
- [`tests/e2e/hosting.spec.ts`](https://github.com/SteveStout/TheYard/blob/main/tests/e2e/hosting.spec.ts) and [`tests/e2e/smoke.spec.ts`](https://github.com/SteveStout/TheYard/blob/main/tests/e2e/smoke.spec.ts): the browser checks that the captions link to the pages, in a new tab, and that the pages answer.

## The look, from the live site

![The Hosting page on a 375 pixel phone: the infrastructure preview, unreadable at that width, with the caption under it that opens the diagram in a new page](/api/docs/images/app-diagram-caption.jpg#750x1624)

![The data flow page as it opens on the phone: the title, the zoom hint, the Source and TheYard links, and the whole drawing fitted to the width](/api/docs/images/app-diagram-page.jpg#750x1624)

![The same page zoomed in, the way a pinch does it: the read path boxes readable, InventoryService through Cards, detail, bid panel](/api/docs/images/app-diagram-zoomed.jpg#750x1624)

![The infrastructure page on a laptop: the drawing at its full width with the header above it](/api/docs/images/app-diagram-laptop.jpg#1600x1125)

Taken from the domain at 1.0.0.25 by the repository's headless Chrome
(`mentor` tooling, no sign-in), the phone captures at 375 pixels and twice
the density; the third one has the SVG widened to 270 percent and scrolled,
which is what a pinch does on a phone.

## Addendum, 2026-09-09: the section

Five drawings now open on pages of their own, and until this addendum a
reader reached each one only through the document that carried its preview:
the infrastructure from four documents, the data flow from three, the two
sites from three, the database from App Architecture alone, and the SQL
Server and Cosmos DB comparison from its own page alone. Steve, asked whether
every diagram was linked on the UI, wanted one place, and the sidebar now
has a Diagrams section near the top: one row per page, each a link
that opens in a new tab the way the preview links already do, in the order a
reader meets the system (the whole, the data, the schema, the two sites, the
two stores). The list lives beside the menus in `src/library/sections.ts`, the server's
`DocumentationCatalog.Diagrams` stays the authority for which drawings exist, and a
test holds the two equal, so a drawing cannot gain a page without a row or a
row without a page; the browser suite reads the section and every row's
address. The rows are links rather than documents on purpose: a drawing is
read zoomed, on a page of its own, and the dialog the documents open in is
the wrong shape for it, which is the decision this record made in the first
place.

![The Diagrams section on the live site: five rows, Infrastructure, Data flow, The database, The two sites, SQL Server vs Cosmos DB, each with the new-tab icon, under the SQL vs Cosmos DB section](/api/docs/images/sidebar-sections.png#272x900)

## Addendum, 2026-10-09 (1.0.3.108): a tenth drawing, the page drawn on the server

The rendering service (ADR: A rendering service beside the API) gives the site a path a reader has to picture: a page's request leaving the edge for the service, which reads the API and sends the HTML drawn for React to take over. `docs/images/server-page.mjs` draws it with the first load down one lane and the takeover down the other, and it opens as A page drawn on the server, the row after The two sites in the Diagrams section. The sitemap lists it with the others.

## Addendum, 2026-09-29 (1.0.3.47): a page about him, on the same pattern

A search for his name with ".NET" or "Azure" found his LinkedIn and not this site. The site had a head that described the auction and a structured-data block naming him only as the author of the code, and no page whose subject was him. `/about` is that page, served by the API the way a diagram is: one small HTML document, `api/TheYard.Api/AboutPage.cs`, with the palette inline, a viewport line, selectable text, and nothing fetched from another host, the font included (the diagram pages still ask Google for theirs; this one uses the reader's own copy of the face or the system's). Outside `/api` because it is a page a search engine lists; a literal route wins over the app's fallback, and the dev server proxies it to the API like `/api`.

What it says comes from the served resume and the README and nothing else: his name as the heading, the resume's title line under it, the resume's summary tightened to one paragraph, one paragraph on what TheYard is with links to the Performance page, the decision records, the API reference and the gate's test record, and three links out: LinkedIn, the repository and the resume. Its head is its own: a title and description written for a search result, a canonical, Open Graph and Twitter tags, and a schema.org Person. Each container names itself from `Site:Url`, so the Cosmos DB site's page says its own address; with none set (a developer's machine, the test host) the request's address is used.

The Person says his name, the resume's title, the site, the two profiles that are him (`sameAs`) and his city, Saint Charles, Missouri, and nothing else: no street, no postcode, no email, no phone. `index.html` carries the same Person in a graph beside the `SoftwareSourceCode` node it already had, whose `author` now points at it by id. The sitemap lists `/about` at priority 0.9 with a lastmod the Dockerfile writes on the day the image is built, and the About section of the sidebar opens with it, in a new tab like every served page. Search Console's HTML-tag check has a slot: a comment in `index.html` that the image build replaces with the meta tag when the repository variable `GOOGLE_SITE_VERIFICATION` is set, and leaves alone when it is not. It ships empty.

Held by `DiagramPageTests` (the page, its head, what the Person may and may not say, each site naming itself), `PublicFaceTests` (the head's Person and slot, the sitemap row) and `DockerBuildInputsTests` (the stamp and the tag, and the variable passed to both builds of the image).

## Addendum, 2026-10-16 (1.0.3.148): the site's face, from the site's own file

The About page opened in Segoe UI on Windows and in whatever the system had elsewhere, a different face from every other page of the site, because it asked no host for the font and the reader's own copy of IBM Plex Sans was the exception, not the rule. The diagram pages wore the face, but asked Google for it, the one third host left on the site after 1.0.0.140. Both now declare the face from the file the build already ships: `SiteFace` finds the hashed `ibm-plex-sans-latin-*.woff2` under `wwwroot/assets` once and hands the pages a `@font-face` line that points at it, the same declaration src/styles/fonts.css carries, so the page and the app share one cached file. Where no built site sits beside the process (the test host, `dotnet run` against the dev server) the line is empty and the fallback face in the page's own rule stands, as it did. The drawing beside How I lead lost the gradient ground it carried, which met the panel's glass at a visible seam; it sits on the panel now.

```csharp
/// <summary>
/// The one woff2 the build wrote for the face, by the name src/styles/fonts.css gives it with
/// the build's hash after it; the first by name when a folder somehow holds two. The rule is
/// the one fonts.css declares, weights 400 to 700 in one file, swap while it lands.
/// </summary>
public static string Find(string assetsRoot)
{
    if (!Directory.Exists(assetsRoot))
    {
        return string.Empty;
    }
    string? file = Directory.EnumerateFiles(assetsRoot, "ibm-plex-sans-latin*.woff2")
        .Select(Path.GetFileName)
        .OfType<string>()
        .Order(StringComparer.Ordinal)
        .FirstOrDefault();
    return file is null
        ? string.Empty
        : "<style>@font-face { font-family: 'IBM Plex Sans'; font-style: normal; font-weight: 400 700; font-display: swap; src: url('/assets/"
            + file + "') format('woff2'); }</style>";
}
```
*Live from [`api/TheYard.Api/SiteFace.cs`](https://github.com/SteveStout/TheYard/blob/1d6b50b/api/TheYard.Api/SiteFace.cs#L34-L54), region `find`, read from this build at 1d6b50b.*
