Publish multi-version documentation site (#1727)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: bb4b44f0-f505-49c5-9878-76e2b40c7a15
Copilot-Session: 8eac1204-10ae-4319-9333-aee5dd43dcc8
This commit is contained in:
Jeff Handley 2026-07-24 15:55:35 -07:00 • committed by GitHub
commit ae36026695
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 819 additions and 134 deletions

View file

@ -0,0 +1,102 @@
/*
* Version picker widget for the multi-version MCP C# SDK docs site.
* Injected into every generated page by scripts/inject-version-picker.mjs.
* Two placements: inline in the DocFX "modern" navbar, or a fixed floating
* fallback when the navbar cannot be found.
*/
.dv-picker {
--dv-fg: #24292f;
--dv-bg: #ffffff;
--dv-border: #d0d7de;
--dv-accent: #0969da;
display: inline-flex;
align-items: center;
gap: 0.4rem;
font-size: 0.85rem;
line-height: 1;
color: var(--dv-fg);
}
.dv-picker__label {
opacity: 0.75;
white-space: nowrap;
}
.dv-picker__select {
appearance: none;
-webkit-appearance: none;
background-color: var(--dv-bg);
color: var(--dv-fg);
border: 1px solid var(--dv-border);
border-radius: 6px;
padding: 0.25rem 1.55rem 0.25rem 0.55rem;
font: inherit;
cursor: pointer;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23636c76'%3E%3Cpath d='M4.22 6.22a.75.75 0 0 1 1.06 0L8 8.94l2.72-2.72a.75.75 0 1 1 1.06 1.06l-3.25 3.25a.75.75 0 0 1-1.06 0L4.22 7.28a.75.75 0 0 1 0-1.06Z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 0.45rem center;
background-size: 0.72rem;
}
.dv-picker__select:hover {
border-color: var(--dv-accent);
}
.dv-picker__badge {
display: inline-block;
padding: 0.1rem 0.4rem;
border-radius: 999px;
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.02em;
color: #9a6700;
background: #fff8c5;
border: 1px solid #eac54f;
}
.dv-prerelease-banner {
display: flex;
justify-content: center;
align-items: center;
gap: 0.35rem;
min-height: 2.5rem;
padding: 0.4rem 1rem;
color: #24292f;
background: #fff8c5;
border-bottom: 1px solid #eac54f;
font-size: 0.9rem;
text-align: center;
}
/* Inline in the navbar (preferred placement). */
.dv-picker--navbar {
margin-left: 0.75rem;
}
/* Fixed fallback when no navbar brand was found. */
.dv-picker--floating {
position: fixed;
right: 1rem;
bottom: 1rem;
z-index: 2147483000;
padding: 0.45rem 0.6rem;
background: var(--dv-bg);
border: 1px solid var(--dv-border);
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.15);
}
/* DocFX's explicit Bootstrap theme controls the widget appearance. */
[data-bs-theme="dark"] .dv-picker {
--dv-fg: #e6edf3;
--dv-bg: #161b22;
--dv-border: #30363d;
--dv-accent: #4493f8;
}
[data-bs-theme="dark"] .dv-prerelease-banner {
color: #e6edf3;
background: #3d2e00;
border-color: #9a6700;
}

View file

@ -0,0 +1,159 @@
/*
* Version picker widget for the multi-version MCP C# SDK docs site.
*
* Configuration is provided per-page via a `window.__DOCS__` object that
* scripts/inject-version-picker.mjs writes into each page's <head>:
*
* window.__DOCS__ = {
* version: "2.0", // slug of the version this page belongs to
* base: "/", // site base path ("/" for a custom domain)
* default: "1.x", // slug the site root redirects to
* versions: [ // newest first; drives the dropdown
* { slug: "2.0", label: "2.0 (preview)", prerelease: true },
* { slug: "1.x", label: "1.x (latest)", prerelease: false }
* ]
* };
*
* The pure helpers (joinPath / stripVersionPrefix / targetPath) are exported so
* they can be unit-tested in Node without a DOM.
*/
export function joinPath(a, b) {
const left = String(a || "").replace(/\/+$/, "");
const right = String(b == null ? "" : b).replace(/^\/+/, "");
return (left + "/" + right).replace(/\/{2,}/g, "/");
}
// Given the current pathname, return the part after "<base>/<currentSlug>/".
export function stripVersionPrefix(pathname, base, currentSlug) {
const prefix = joinPath(joinPath(base, currentSlug), "");
if (pathname.startsWith(prefix)) return pathname.slice(prefix.length);
// Tolerate a missing trailing slash (e.g. "/2.0").
const bare = joinPath(base, currentSlug);
if (pathname === bare) return "";
return "";
}
// Derive the deployment base from the page URL. This keeps picker navigation
// within project Pages sites, whose content is hosted below "/<repository>/".
export function getBasePath(cfg, pathname) {
const marker = `/${cfg.version}/`;
const markerIndex = pathname.indexOf(marker);
if (markerIndex !== -1) return pathname.slice(0, markerIndex + 1) || "/";
const bareMarker = `/${cfg.version}`;
if (pathname.endsWith(bareMarker)) {
return pathname.slice(0, -bareMarker.length + 1) || "/";
}
return cfg.base || "/";
}
// Compute the equivalent path in another version, preserving the sub-page.
export function targetPath(cfg, targetSlug, pathname) {
const base = getBasePath(cfg, pathname);
const sub = stripVersionPrefix(pathname, base, cfg.version);
return joinPath(joinPath(base, targetSlug), sub);
}
function h(tag, attrs, children) {
const el = document.createElement(tag);
for (const k in attrs || {}) {
if (k === "class") el.className = attrs[k];
else el.setAttribute(k, attrs[k]);
}
for (const c of children || []) {
el.appendChild(typeof c === "string" ? document.createTextNode(c) : c);
}
return el;
}
function versionRoot(cfg, slug, pathname) {
return joinPath(joinPath(getBasePath(cfg, pathname), slug), "");
}
async function navigate(cfg, slug) {
const candidate = targetPath(cfg, slug, location.pathname);
const home = versionRoot(cfg, slug, location.pathname);
// Best effort: keep the reader on the same page if it exists in the target
// version; otherwise fall back to that version's landing page.
try {
const res = await fetch(candidate, { method: "HEAD" });
if (res.ok) {
location.assign(candidate + location.hash);
return;
}
} catch (_) {
/* HEAD may be blocked (e.g. file://) -- fall through to the version home. */
}
location.assign(home);
}
function previewBadge() {
return h("span", { class: "dv-picker__badge" }, ["preview"]);
}
function buildPicker(cfg) {
const current = cfg.versions.find((v) => v.slug === cfg.version);
const select = h("select", {
class: "dv-picker__select",
"aria-label": "Select documentation version",
});
for (const v of cfg.versions) {
const opt = h("option", { value: v.slug }, [v.label || v.slug]);
if (v.slug === cfg.version) opt.selected = true;
select.appendChild(opt);
}
select.addEventListener("change", () => navigate(cfg, select.value));
const children = [h("span", { class: "dv-picker__label" }, ["Version"]), select];
if (current && current.prerelease) {
children.push(previewBadge());
}
return h("div", { class: "dv-picker" }, children);
}
function buildPrereleaseBanner() {
return h("div", {
class: "dv-prerelease-banner",
role: "status",
}, [
"You are currently viewing",
previewBadge(),
"documentation.",
]);
}
function mount(picker) {
const brand = document.querySelector(".navbar-brand");
if (brand && brand.parentNode) {
picker.classList.add("dv-picker--navbar");
brand.parentNode.insertBefore(picker, brand.nextSibling);
return;
}
picker.classList.add("dv-picker--floating");
document.body.appendChild(picker);
}
function init() {
const cfg = window.__DOCS__;
if (!cfg || !Array.isArray(cfg.versions) || cfg.versions.length < 2) return;
if (document.querySelector(".dv-picker, .dv-prerelease-banner")) return; // idempotent
const current = cfg.versions.find((v) => v.slug === cfg.version);
if (!current) return;
if (current.prerelease) {
document.body.prepend(buildPrereleaseBanner());
}
mount(buildPicker(cfg));
}
if (typeof document !== "undefined" && typeof document.createElement === "function") {
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
}