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 Project per entry in projects. 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 /index is collapsed (so overview/index.md becomes the slug overview)

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.