How it works
The pieces that power a SharpDocs site.
AddSharpDocs() registers the singletons, and an internal hosted service warms them at startup so disk scanning happens once, before Kestrel begins listening.
ProjectRegistry
The entry point. At construction it reads the root sharpdocs.json, decides single- vs multi-project mode (presence of a projects array), and constructs a Project per entry. Each project owns a Dictionary<string, LocalePack> — one resolved pack per declared locale, each carrying its own DocsIndex + SearchIndex + merged content config.
- Single-project mode — the registry contains exactly one synthetic project, derived from the root config. Today's default.
- Multi-project mode — one
Projectper entry inprojects. Each carries its own per-locale docs/search indexes and a resolved artifacts path.
Per-project failures degrade rather than crash. A bad sharpdocs.json or missing slug in one project marks that project with an error and lets the rest of the site boot. Per-locale failures degrade only that locale — the project keeps its other locales unless the default locale fails.
DocsIndex (per project × locale)
Receives a pre-resolved page list (overlay applied — *.{locale}.md wins over *.md) plus the merged sidebar config, and exposes a slug lookup plus the sidebar nav. Disk scanning + locale-suffix detection live in ProjectRegistry, so the project root is walked exactly once regardless of how many locales it ships.
Slug rules:
- File path relative to the project's root, with the extension stripped
- Trailing locale suffix (
.{locale}for any declared locale) stripped \is normalized to/- A trailing
/indexis collapsed (sooverview/index.mdbecomes the slugoverview)
The sidebar must reference slugs that exist for the target locale (base or override). Missing slugs degrade that locale.
See Localization for the overlay model.
SearchIndex (per project × locale)
Tokenizes its locale's resolved pages and answers scoped search. Search is per-locale per-project, never unified — each (project, locale) has its own index and results never cross project or locale boundaries. The scorer is hand-rolled:
- Title match: 10 points
- Heading match: 3 points
- Body token match: 1 point
LocalFolderSource
A single instance that scans *.nupkg across every project's artifacts root (one root in single-project mode, N in multi-project). Skips *.symbols.nupkg, parses each nuspec, and dedups (id, version) first-wins by source order. Pure read-through — restart the host to pick up new packages.
Upstream NuGet feeds
Optional. Configure one or more upstream NuGet v3 feeds via AddUpstream(...) or appsettings.json, and the /v3/* endpoints layer them behind your local artifacts. Service-index resolution runs once at startup with a per-upstream timeout — a slow or broken upstream logs and is excluded, never blocks the site. See NuGet feed.
Routing
The host calls app.MapSharpDocs(), which registers routes based on mode:
| Mode | URL | Handler |
|---|---|---|
| Single-project | / |
302 → /{defaultLocale} |
| Single-project | /{locale}/{**slug} |
docs page |
| Single-project | /{locale}/search |
search |
| Multi-project | / |
landing picker (or single-project shortcut) |
| Multi-project | /docs/{projectId} |
302 → /docs/{projectId}/{defaultLocale} |
| Multi-project | /docs/{projectId}/{locale}/{**slug} |
docs page |
| Multi-project | /docs/{projectId}/{locale}/search |
per-locale per-project search |
| Both | /v3/* |
NuGet feed (locale-free) |
| Both | /packages, /packages/{id} |
package browser + detail (locale-free) |
| Both | /api/manifest |
site/project manifest (JSON) |
Requests to an unsupported locale (e.g. /zz/foo when the project doesn't ship zz) 302 to the project's default locale, preserving the slug — URLs stay honest about which locale they're in.
Docs and search controllers return their _Article / _Results partials when the request has an HX-Request header (htmx swap), otherwise the full view.