---
title: Version a DocSite
description: "Ship /v1/ and /v2/ URL trees from one DocSite host, each with its own content area and its own reflection-based API reference."
canonical_url: https://usepennington.net/how-to/versioning/docsite/
sidecar_url: https://usepennington.net/how-to/versioning/docsite.md
content_hash: sha256:737e813684af0b07d1bb06af8d66370e5807a68a21eba703f1405bc4cb128db6
tokens: 2256
uid: how-to.versioning.docsite
reading_time_minutes: 4
---

Guides
# Version a DocSite

Ship /v1/ and /v2/ URL trees from one DocSite host, each with its own content area and its own reflection-based API reference.

 
To serve `/v1/` and `/v2/` URL trees from one DocSite host, give each version its own `ContentArea`. One area per version is the whole mechanism for prose-only docs — the Lay out content by version section below is all you need.

 
The rest of this page layers a per-version API reference on top, which is where the only real friction lives: NuGet allows one version of an assembly per project, so the off-version DLL needs a `<PackageDownload>` workaround. If you don't need a reflected API tree, stop after the areas section.

 
The recipe references `examples/VersionedDocSiteExample/`, which documents `Humanizer.Core` 2.8.26 alongside 2.14.1. For how `AddApiMetadataFromCompiledAssembly` and `AddApiReference` work on a single version, see [Auto-generate an API reference tree for a class library](https://usepennington.net/how-to/content-services/auto-api-reference.md).

 
## Before you begin

 
 - A DocSite host already wired with `AddDocSite` (see [Scaffold a documentation site with DocSite](https://usepennington.net/tutorials/docsite/scaffold.md)).
 - A decision about which version is the *active* `PackageReference`. That version resolves via `FromPackageReference("AssemblyName")`. Every other version is staged via `<PackageDownload>` and an explicit `AssemblyFiles` path.
 
 
## Lay out content by version

 
Use one `ContentArea` per version. The `Slug` is both the URL prefix and the folder name under `Content/`, so files at `Content/v1/foo.md` route to `/v1/foo` and the sidebar renders an area selector that doubles as a version switcher.

 
```csharp:symbol
public static void AddVersionedAreas(WebApplicationBuilder builder)
{
    builder.Services.AddDocSite(() => new DocSiteOptions
    {
        SiteTitle = "Humanizer Docs",
        SiteDescription = "Side-by-side documentation for two versions of Humanizer.Core, with version-scoped content and a sidebar version selector.",
        GitHubUrl = "https://github.com/Humanizr/Humanizer",
        HeaderContent = """<a href="/" class="font-bold text-lg">Humanizer Docs</a>""",
        FooterContent = """<footer class="mt-16 py-8 text-center text-sm text-base-500">Humanizer © Mehdi Khalili & contributors. Rendered by Pennington.</footer>""",
        Areas =
        [
            new ContentArea("v1", "v1"),
            new ContentArea("v2", "v2"),
        ],
    });
}
```

 
The `Areas` declaration is the only place the version names appear in the host wiring. Adding a `v3` later is two lines plus a `Content/v3/` folder.

 
A bare `/` request — anyone landing on the site root with no version prefix — falls through to the DocSite not-found page unless you give the root a page; add a `Content/index.md` (or a routed landing component) that redirects to or links the version you treat as current. Marking one version "latest" and showing a deprecation banner on older trees are content-level conventions, not host wiring: drop a shared `[!INCLUDE]` partial into each old version's pages for the banner, and point the root and header link at the current slug. See [Forward visitors from a renamed page](https://usepennington.net/how-to/pages/redirects.md) for the root-redirect mechanics.

 
## Reference two versions of the same NuGet package

 
NuGet allows only one `<PackageReference>` per assembly per project. To document a second version, add a `<PackageDownload>` element pinned with square-bracket exact-version syntax. `<PackageDownload>` fetches the package into the NuGet cache without adding it to the compile graph, leaving the `<PackageReference>` version as the one resolved through the default load context.

 
```xml:symbol
<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
  <ItemGroup>
    <ProjectReference Include="..\..\src\Pennington.DocSite\Pennington.DocSite.csproj" />
    <ProjectReference Include="..\..\src\Pennington.DocSite.Api\Pennington.DocSite.Api.csproj" />
    <ProjectReference Include="..\..\src\Pennington.ApiMetadata.Reflection\Pennington.ApiMetadata.Reflection.csproj" />
  </ItemGroup>
  <ItemGroup>
    <!-- v2 is the active PackageReference. FromPackageReference("Humanizer")
         resolves this through the default load context. -->
    <PackageReference Include="Humanizer.Core" />
    <!-- v1 is staged into the NuGet cache without being compiled against.
         PackageDownload is how NuGet expresses "fetch this version, but do not
         add it to the compile graph" — necessary because a single project
         cannot PackageReference two versions of the same assembly. The exact
         version pin (square brackets) is required by PackageDownload. -->
    <PackageDownload Include="Humanizer.Core" Version="[2.8.26]" />
  </ItemGroup>
  <ItemGroup>
    <Watch Include="Content\**\*.*" />
  </ItemGroup>
</Project>
```

 
In `Program.cs`, register one named provider per version, then pair each with an `AddApiReference` registration whose `RoutePrefix` nests under the matching area slug:

 
```csharp:symbol
public static void AddVersionedApiReferences(WebApplicationBuilder builder)
{
    var nuGetPackages = Environment.GetEnvironmentVariable("NUGET_PACKAGES")
        ?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nuget", "packages");
    var humanizerV1Dll = Path.Combine(nuGetPackages, "humanizer.core", "2.8.26", "lib", "netstandard2.0", "Humanizer.dll");
  
    builder.Services.AddApiMetadataFromCompiledAssembly("humanizer-v1", opts =>
        opts.AssemblyFiles.Add(humanizerV1Dll));
    builder.Services.AddApiMetadataFromCompiledAssembly("humanizer-v2", opts =>
        opts.FromPackageReference("Humanizer"));
  
    builder.Services.AddApiReference("humanizer-v1", opts =>
    {
        opts.RoutePrefix = "/v1/api/";
        opts.TocTitle = "API reference";
    });
    builder.Services.AddApiReference("humanizer-v2", opts =>
    {
        opts.RoutePrefix = "/v2/api/";
        opts.TocTitle = "API reference";
    });
}
```

 
 - The active reference uses `FromPackageReference("Humanizer")` — `Assembly.Load` finds the v2 DLL via the project's `deps.json`.
 - The off-version uses `AssemblyFiles.Add(path)` with an explicit path under the NuGet global-packages folder. Read that folder from the `NUGET_PACKAGES` environment variable and fall back to the per-user default (`~/.nuget/packages` on Linux and macOS, `%USERPROFILE%\.nuget\packages` on Windows) — `Environment.GetFolderPath(SpecialFolder.UserProfile)` resolves the home directory on every platform. Inside it, the simple-name folder is lowercased; the version is the literal `<PackageDownload>` value; the TFM is whichever `lib/<tfm>/` the package ships.
 
 
The two registrations resolve as follows:

 
    Provider name `RoutePrefix` Resolves     `humanizer-v1` `/v1/api/` `Humanizer.dll` 2.8.26 from the NuGet cache   `humanizer-v2` `/v2/api/` `Humanizer.dll` 2.14.1 via the active `PackageReference`    
 
The Mdazor components (`<ApiMemberTable>`, `<ApiSummary>`, …) are registered once and resolve metadata per page via the keyed provider, so two trees coexist with no further wiring.

 
## Cross-link between versions

 
Each named registration emits xref uids under `reference.api.{name}.{slug}` — for example, `<xref:reference.api.humanizer-v1.string-humanize-extensions>` and `<xref:reference.api.humanizer-v2.string-humanize-extensions>`. Use the qualified form when a v2 content page links to a v1 type to show what changed, or vice versa.

 
## Verify

 
 - Run `dotnet run --project examples/VersionedDocSiteExample`.
 - Visit `/v1/`, `/v2/`, `/v1/api/`, and `/v2/api/` — each renders independently.
 - The startup log prints one `ApiReferenceIndex({name}): published N auto-discovered type pages` line per registration. `N` differs between versions when the two assemblies differ.
 - Confirm the sidebar area selector switches between `v1` and `v2` while staying on the same page kind.
 
 
## Related

 
 - How-to: [Auto-generate an API reference tree for a class library](https://usepennington.net/how-to/content-services/auto-api-reference.md) — the single-source backend setup this recipe builds on.
 - Tutorial: [Organize content with sections and areas](https://usepennington.net/tutorials/docsite/sections-and-areas.md) — the area-driven URL prefix mechanism reused for version slugs.
 - Reference: [DI and middleware extension methods](https://usepennington.net/reference/host/extensions.md) — `AddDocSite` and `DocSiteOptions` surface.
 
 
[Previous
                
                Populate the blog homepage](https://usepennington.net/how-to/theming/blogsite-homepage.md)[Next
                    
                Serve docs and a blog from separate content roots](https://usepennington.net/how-to/discovery/multiple-sources.md)