Writing These Docs
Where docs actually live
You are reading a file that lives in this repo, under docs/. It is not the Documentation site itself — the site is a separate Docusaurus repo (Alert.CA.Documentation) that renders these files. An Azure Pipeline keeps the two in sync automatically:
- It triggers on any push to
mainthat touchesdocs/**. - It clones
Alert.CA.Documentation, copies every file from this repo'sdocs/folder intodocs/Alert.CA.Frontend/there, and commits/pushes the result — a raw, one-way file copy with no filtering or validation step in between.
Practical consequence: always edit docs here, never in Alert.CA.Documentation directly. A direct edit there survives only until the next sync from this repo overwrites it. See documentation-sync.md at this repo's root for the full pipeline reference.
The full lesson-style rewrite of these docs was authored directly in
Alert.CA.Documentation and has not yet been synced back into this repo's docs/ folder.
Until it is (or the pipeline is retargeted), a push to this repo's main touching docs/**
would overwrite the rewritten site with older content. Coordinate before touching docs/
here.
Because the copy is raw and unvalidated, anything that breaks the Docusaurus build only surfaces after it lands in the other repo — there is no local build step here to catch it first. That makes the next section load-bearing, not a style nitpick.
The MDX trap: literal {...} breaks the build
Docusaurus parses every .md file as MDX (v3), not plain Markdown. MDX treats an unescaped {...} outside a code span or fenced code block as a JavaScript expression to evaluate — not literal text. This has actually broken a production build in this project before: a sentence containing literal text like {center, extent, edge} compiled fine as Markdown but threw ReferenceError: center is not defined the moment it hit MDX, because MDX tried to evaluate center as a variable.
The fix is always the same: wrap the literal braces in backticks.
BAD: choose one of {center, extent, edge}
GOOD: choose one of `{center, extent, edge}`
This applies to anything with curly braces used as literal text — object-shorthand examples, set notation, template-looking strings — not just this specific phrase. When in doubt, backtick it.
Live code sandboxes: CodePlayground
For math or logic worth letting a reader edit and re-run, use the shared CodePlayground component instead of hand-rolling a BrowserOnly-wrapped inline component (the old pattern this overhaul replaced — it duplicated the same 50–150 lines of boilerplate in every playground doc).
interface CodePlaygroundProps {
code: string; // initial editable snippet
scope?: Record<string, unknown>;
title?: string;
renderResult?: boolean; // default true: live JSX preview. false: pure computed-output panel (use for math/logic)
noInline?: boolean; // default true: multi-statement snippet ending in a render(...) call
}
Import and use it from an .md file with two separate mdx-code-block fences — an import fence, then a usage fence:
import CodePlayground from '@site/src/components/docs/CodePlayground';
<CodePlayground
title="calculateZoom"
renderResult={false}
code={`
// Mirrored from util/cameras/calculateZoom.ts — verify against source if this looks stale
function calculateZoom(fovDegrees) { /* ... */ }
const result = calculateZoom(12.5);
render(<pre>{JSON.stringify(result, null, 2)}</pre>);
`}
/>
One hard rule first: render() must be given JSX, never a bare string. The react-live
version in the platform silently renders nothing for render('some text') — no error, just
an empty output panel. End logic demos with render(<pre style={{margin: 0}}>{text}</pre>).
Two more things worth knowing before you add one:
- The Docusaurus build cannot resolve a cross-repo TypeScript import. You can't
import { calculateZoom } from '@/util/cameras/calculateZoom'from a doc in this repo, because by the time this file is rendered, it's sitting in a completely different repo's build. Instead, copy the real function's logic directly into thecodestring, and leave a one-line comment noting which real source file it mirrors — see any file underdeep-dives/for the pattern. This means a playground can drift from the real implementation over time; when you touch the source function, check whether a playground mirrors it and update both together. - Only build a playground for genuinely standalone logic. Pure functions (math, parsing, diffing) are great candidates. Anything wired into Zustand, TanStack Query, ArcGIS, or SignalR is not — faking that kind of interactivity is worse than not having a demo. Write a clear prose + code-reference walkthrough instead; several docs in
features/anddeep-dives/do exactly this.
Diagrams: Mermaid by default
Mermaid is enabled site-wide — a ```mermaid fence renders directly, no import needed:
```mermaid
sequenceDiagram
participant A as Operator A
participant API as /api/lease
A->>API: POST { cameraId }
```
Mermaid should be your default for any flow, sequence, or state diagram. There is exactly one hand-built, animated, click-through diagram component in this whole documentation system — SignalRFanoutDiagram, used in deep-dives/signalr-realtime-architecture.md, built on a shared StepDiagram engine that lives in the Alert.CA.Documentation repo's src/components/docs/StepDiagram/. It exists because that specific flow (one hub connection fanning out to three different stores, three different ways) genuinely benefits from being walked through step-by-step with visual state. Reach for Mermaid first; only consider a custom StepDiagram instance for something with real branching, multiple independent actors, and enough steps that a single static diagram would be unreadable.
The information architecture
Docs are organized under docs/, one top-level folder per concern, each with a _category_.json carrying a unique position (don't renumber these without checking every sibling first — duplicate positions produce a nondeterministic sidebar order, which is exactly the bug this overhaul fixed):
| Folder | What goes here |
|---|---|
get-started/ | Onboarding: what the app is, how to run it locally |
core-concepts/ | The mental model: architecture, design tokens, analytics |
features/ | One subfolder per user-facing capability — how it works, how it's built |
state-management/ | Zustand store reference |
deep-dives/ | The hardest subsystems, explained in full — math, concurrency, security, real-time |
security/ | Auth, RBAC, and anything security-relevant |
accessibility/ | WCAG/a11y guidance and tooling |
testing/ | Test conventions and CI integration |
contributing/ | This page, and anything else about maintaining the docs themselves |
When you're not sure where something goes: a doc explaining how a feature works belongs in features/; a doc explaining why something is hard, or how several pieces interact under load/concurrency/security constraints belongs in deep-dives/. If you're only extending an existing doc, match its neighbors rather than inventing a new pattern — every doc under deep-dives/ follows the same shape (Overview → real source-grounded detail, with real file paths and function names → a diagram → a "Related" cross-link section at the end); that consistency is deliberate and worth preserving.
The standard a new doc should meet
Every fact in a doc should be something you verified by reading the actual current source — not something copied from an older doc, not something inferred from a variable name. This project has twice shipped a doc that confidently described behavior that no longer existed (an authentication flow that had been replaced, and a lease/concurrency flow with invented functions and events) — both were caught only when someone read the real source side-by-side with the doc. Read the code before you write about it, and if you find an existing doc that's wrong, fix it rather than building on top of it.