Static documentation hosting platform for Cocoar products. Serves VitePress-generated documentation with multi-product and multi-version support.
dotnet build ./src -c Release
dotnet test ./src -c Release
dotnet publish ./src/Cocoar.Shelf -c Release -o ./artifactsdocker compose up --build- ASP.NET Core app serving static files from a mounted volume
- Products and versions organized as
{docsRoot}/{product}/{version}/ - Versions are auto-detected by scanning the filesystem (SemVer:
v5,v5.2,v5.2.0,v5.2.0-beta.1) - Highest version number is automatically the "latest"
- Requests without explicit version are redirected (302) to the latest version
FileSystemWatcherinvalidates the cache when directories are added/removed- Base path rewriting: Shelf detects the original VitePress base path and rewrites it in HTML, CSS, and JS responses to match the product/version URL
All settings are in the Shelf section of appsettings.json or via environment variables (Shelf__PropertyName).
| Property | Default | Description |
|---|---|---|
DocsRoot |
/data/docs |
Root directory for documentation files |
ConfigRoot |
/data/config |
Root directory for product config files |
PathBase |
"" |
Global URL prefix (e.g. /docs to serve under example.com/docs/) |
VersionPattern |
^v?\d+(\.\d+(\.\d+(-[\w.]+)?)?)?$ |
Regex for valid version directory names (SemVer) |
BasePlaceholder |
/__shelf__/ |
Placeholder for non-root VitePress base paths |
ApiKey |
"" |
API key for upload endpoint. Empty = upload disabled (503) |
MaxUploadSizeBytes |
104857600 |
Max upload size (100 MB) |
When Shelf runs behind a reverse proxy under a sub-path, set PathBase to match. All routes, redirects, and base path rewriting automatically include the prefix:
PathBase="" → /configuration/v5/ /api/products
PathBase="/docs" → /docs/configuration/v5/ /docs/api/products
Products must be registered via JSON files in {ConfigRoot}/products/. One file per product:
/data/config/products/configuration.json
{
"name": "configuration",
"displayName": "Cocoar.Configuration",
"description": "Reactive configuration for .NET",
"source": "upload"
}Config files are loaded at startup and live-reloaded via FileSystemWatcher. The name field is the product identifier used in URLs and API calls.
Shelf provides an HTTP API for deploying documentation versions, designed for CI/CD pipelines.
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/products |
No | All registered products with version info |
GET |
/api/products/{product}/versions |
No | Versions of a specific product |
POST |
/api/products/{product}/versions/{version} |
Bearer | Upload a ZIP as a new version |
curl -X POST \
-H "Authorization: Bearer $SHELF_API_KEY" \
-H "Content-Type: application/zip" \
--data-binary @docs.zip \
https://docs.cocoar.dev/api/products/configuration/versions/v6- API key is validated (401 if wrong, 503 if not configured)
- Product must be registered in config (404 if not)
- Version format must match
VersionPattern(400 if not) - ZIP is extracted to a temp directory, validated (index.html required), and atomically moved into place
- ManifestService detects the new version automatically via FileSystemWatcher
- Response: 201 Created
| Status | When |
|---|---|
| 201 | Successfully deployed |
| 400 | Invalid version format, corrupt ZIP, or missing index.html |
| 401 | Missing or invalid API key |
| 404 | Product not registered |
| 409 | Concurrent upload for same product/version |
| 413 | ZIP exceeds MaxUploadSizeBytes |
| 503 | No API key configured (upload disabled) |
- API key auth:
Authorization: Bearer {key}checked againstShelfOptions.ApiKey - ZIP-Slip protection: All extracted paths are validated to stay within the target directory
- Atomic deployment: Extract to temp dir, validate, then move — Shelf never serves a half-extracted state
- Per-version locking: Concurrent uploads for the same product/version return 409
Shelf rewrites the VitePress base path so docs built with base: '/' work under /{product}/{version}/ (or {PathBase}/{product}/{version}/ when PathBase is set).
Rewriting happens at four levels:
- HTML:
href="/..."andsrc="/..."attribute values - HTML site data:
"base":"/"in the inlined__VP_SITE_DATA__JSON - CSS:
url(/...)font/asset references - JS: Two targeted VitePress patterns (router base constant + modulepreload URL builder) and search index document IDs
For non-root bases (e.g., /__shelf__/), a simple global string replace is used instead.
src/Cocoar.Shelf/Program.cs— App startup, service registration, middleware pipelinesrc/Cocoar.Shelf/ShelfOptions.cs— All configuration propertiessrc/Cocoar.Shelf/Middleware/DocsRoutingMiddleware.cs— Core routing logic + response rewritingsrc/Cocoar.Shelf/Services/ManifestService.cs— Scans filesystem, caches product versionssrc/Cocoar.Shelf/Services/ProductConfigService.cs— Reads product JSON configs, live-reloadsrc/Cocoar.Shelf/Services/UploadService.cs— ZIP extraction, validation, atomic deploymentsrc/Cocoar.Shelf/Endpoints/ApiEndpoints.cs— Minimal API endpoints (products, versions, upload)src/Cocoar.Shelf/Endpoints/ApiKeyFilter.cs— Bearer token auth for upload endpointsrc/Cocoar.Shelf/Services/BasePathDetector.cs— Detects original VitePress base from index.htmlsrc/Cocoar.Shelf/Services/BasePathRewriter.cs— Rewrites base path in HTML/CSS/JS responses
/data/docs/{product}/{version}/ (e.g. /data/docs/configuration/v6/)
/data/config/products/{product}.json
Docs: New versions are detected automatically via FileSystemWatcher — either deployed via Upload API or placed manually. Config: Product registration files, also live-reloaded via FileSystemWatcher.
- ProductConfigService and ManifestService are independent — no mutual dependency. API endpoints compose both.
- Upload writes directly into the docs volume → ManifestService detects it automatically. No manual cache invalidation needed.
- Product must be registered first (JSON config file) → no auto-creation. Enforces explicit product registration.
- Source type as string in ProductConfig (
"upload", future"github-release") → extensible without breaking changes.