Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 26 additions & 9 deletions .github/workflows/continuous-deployment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,27 @@ jobs:
sign=false
fi

# Upstream is the organization Pages site served at the root of the
# custom domain (documentdb.io), so it must NOT set a base path:
# prefixing every internal link and asset with the repository name
# makes GitHub Pages 301 back to the root on every request and leaves
# the prefixed URL in the address bar after client-side navigation.
# A fork is the opposite case - it is served as a project page under
# /<repo>/, where the prefix is required for assets to resolve at all.
# Deriving it here is what lets a fork deploy a working preview of a
# docs change without editing the workflow.
if [ "$IS_UPSTREAM" = 'true' ]; then
base_path=''
jekyll_base_path='/blogs'
else
base_path="${GITHUB_REPOSITORY##*/}"
jekyll_base_path="/${base_path}/blogs"
fi

echo "packages=$packages" >> "$GITHUB_OUTPUT"
echo "sign=$sign" >> "$GITHUB_OUTPUT"
echo "base_path=$base_path" >> "$GITHUB_OUTPUT"
echo "jekyll_base_path=$jekyll_base_path" >> "$GITHUB_OUTPUT"

{
echo "### Build configuration"
Expand All @@ -75,6 +94,7 @@ jobs:
echo "| Static site | true | always built |"
echo "| Package repositories | $packages | \`BUILD_PACKAGES\` variable |"
echo "| Package signing | $sign | \`GPG_PRIVATE_KEY\` secret |"
echo "| Base path | ${base_path:-(none)} | upstream vs. fork |"
} >> "$GITHUB_STEP_SUMMARY"

if [ "$packages" = 'true' ] && [ "$sign" != 'true' ]; then
Expand Down Expand Up @@ -146,23 +166,20 @@ jobs:
- name: Install dependencies
run: npm ci
- name: Build with Next.js
# This repository is the organization Pages site served at the root of
# the custom domain (documentdb.io), so the build must NOT set
# NEXT_BASE_PATH. Setting it to the repository name (the usual trick
# for project pages) prefixes every internal link and asset URL with
# /documentdb.github.io/, which GitHub Pages then 301-redirects back
# to the root on every request and leaves the prefixed URL visible in
# the address bar after client-side navigation.
# Both base paths are resolved in the "Resolve optional build features"
# step; upstream builds resolve NEXT_BASE_PATH to the empty string,
# which next.config.ts treats as "no base path".
env:
JEKYLL_BASE_PATH: /blogs
NEXT_BASE_PATH: ${{ steps.features.outputs.base_path }}
JEKYLL_BASE_PATH: ${{ steps.features.outputs.jekyll_base_path }}
run: npm run build
- name: Verify exported documentation pages
# A partially failed content compile must never reach production as a
# docs-less site. compile-content fails the build on clone/copy errors;
# this is the independent belt-and-braces check on the final artifact.
run: |
set -euo pipefail
for page in out/index.html out/docs/index.html out/docs/getting-started/index.html out/docs/reference/index.html; do
for page in out/index.html out/docs/index.html out/docs/getting-started/index.html out/docs/reference/index.html out/docs/versions/index.html; do
if [ ! -f "$page" ]; then
echo "Missing expected page: $page"
exit 1
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
# Temporary content cloning directory
_tmp/

# Compiled versioned documentation snapshots
/versioned/

# Reference files (compiled into the repo root from documentdb/docs; anchored
# so the patterns cannot swallow tracked paths like app/docs/reference/)
/api-reference/
Expand Down
4 changes: 4 additions & 0 deletions app/components/Breadcrumb.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ export default function Breadcrumb({ type, category, name }: {
}) {
return (
<nav className="mb-6 text-sm text-gray-400">
<Link href="/docs" className="hover:text-blue-400 transition-colors">
Docs
</Link>
<span className="mx-2">/</span>
<Link href="/docs/reference" className="hover:text-blue-400 transition-colors">
Reference
</Link>
Expand Down
30 changes: 30 additions & 0 deletions app/components/DocsBreadcrumb.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import Link from "next/link";

export interface Crumb {
title: string;
href?: string;
}

/**
* Breadcrumb trail for documentation pages:
* Docs / [version] / Section / Page
* The last crumb (no href) is the page being viewed.
*/
export default function DocsBreadcrumb({ crumbs }: { crumbs: Crumb[] }) {
return (
<nav className="mb-6 text-sm text-gray-400">
{crumbs.map((crumb, index) => (
<span key={`${crumb.title}-${index}`}>
{index > 0 && <span className="mx-2">/</span>}
{crumb.href ? (
<Link href={crumb.href} className="hover:text-blue-400 transition-colors">
{crumb.title}
</Link>
) : (
<span className="text-white">{crumb.title}</span>
)}
</span>
))}
</nav>
);
}
145 changes: 145 additions & 0 deletions app/components/DocsSidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import Link from "next/link";
import VersionSwitcher from "./VersionSwitcher";
import { VersionSwitcherEntry } from "../services/versionService";

export interface SidebarNavItem {
title: string;
href: string;
active?: boolean;
}

export interface DocsSidebarProps {
/** null when viewing current docs; the version label on archived pages. */
version: string | null;
/** Where the top back link goes: /docs for current, /docs/versions/<v> for a version. */
backHref: string;
backLabel: string;
sectionTitle: string;
/** Pages of the section being viewed. */
nav: SidebarNavItem[];
/** All sections in the SAME version context (never leaves the version). */
sections: SidebarNavItem[];
switcherEntries: VersionSwitcherEntry[];
}

function navLinkClass(active?: boolean): string {
return `block w-full text-left px-4 py-2.5 rounded-lg text-sm transition-all duration-200 ${active
? "bg-blue-500/20 text-blue-300 border border-blue-500/30"
: "text-gray-300 hover:text-white hover:bg-neutral-700/50"
}`;
}

/**
* Shared sidebar for current and versioned documentation pages. Every link in
* it stays inside the version being viewed, so readers never fall out of an
* archived version by navigating.
*/
export function DocsSidebarContent({
version,
backHref,
backLabel,
sectionTitle,
nav,
sections,
switcherEntries,
}: DocsSidebarProps) {
return (
<>
{/* Header: back link + section title + version state */}
<div className="p-6 border-b border-neutral-700/50">
<Link
href={backHref}
className="text-blue-400 hover:text-blue-300 text-sm mb-3 flex items-center transition-colors"
>
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
{backLabel}
</Link>
<p className="text-2xl font-bold text-white">{sectionTitle}</p>
{version && (
<p className="mt-1 inline-flex items-center rounded-full bg-amber-500/15 border border-amber-500/40 px-2.5 py-0.5 text-xs font-semibold text-amber-300">
{version} · archived
</p>
)}
<div className="mt-4">
<VersionSwitcher entries={switcherEntries} />
</div>
</div>

{/* Section pages */}
<div className="flex-1 p-4 overflow-y-auto">
<nav className="space-y-1">
{nav.map((item) => (
<Link key={item.href} href={item.href} className={navLinkClass(item.active)}>
{item.title}
</Link>
))}
</nav>

{/* Other sections, same version context */}
{sections.length > 0 && (
<div className="mt-6 border-t border-neutral-700/50 pt-4">
<p className="px-4 pb-2 text-xs font-semibold uppercase tracking-wider text-gray-500">
All sections
</p>
<nav className="space-y-1">
{sections.map((item) => (
<Link
key={item.href}
href={item.href}
className={`block w-full px-4 py-2 rounded-lg text-sm transition-colors ${item.active
? "text-white font-medium"
: "text-gray-400 hover:text-white hover:bg-neutral-700/40"
}`}
>
{item.title}
</Link>
))}
</nav>
</div>
)}
</div>

{/* Footer meta links */}
<div className="p-4 border-t border-neutral-700/50 space-y-1">
<Link
href="/docs/release-notes"
className="block px-4 py-1.5 text-xs text-gray-400 hover:text-white transition-colors"
>
Release notes
</Link>
<Link
href="/docs/versions"
className="block px-4 py-1.5 text-xs text-gray-400 hover:text-white transition-colors"
>
All documentation versions
</Link>
</div>
</>
);
}

/** Desktop sidebar wrapper. */
export default function DocsSidebar(props: DocsSidebarProps) {
return (
<div className="hidden w-80 bg-neutral-800/50 backdrop-blur-sm border-r border-neutral-700/50 md:flex flex-col">
<DocsSidebarContent {...props} />
</div>
);
}

/** Mobile disclosure variant of the same navigation. */
export function DocsSidebarMobile(props: DocsSidebarProps) {
return (
<details className="mb-6 rounded-lg border border-neutral-700/50 bg-neutral-800/50 md:hidden">
<summary className="cursor-pointer px-4 py-3 text-sm font-semibold text-gray-200">
{props.sectionTitle} navigation
{props.version ? ` (${props.version})` : ""}
</summary>
<div className="border-t border-neutral-700/50 flex flex-col">
<DocsSidebarContent {...props} />
</div>
</details>
);
}
55 changes: 55 additions & 0 deletions app/components/VersionSwitcher.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import Link from "next/link";
import { VersionSwitcherEntry } from "../services/versionService";

/**
* Static version dropdown shown on every documentation page. Server-rendered
* (a <details> disclosure, no client JS) so it works in the static export.
* The active entry is marked; every other entry links to the best equivalent
* of the page being viewed in that version.
*/
export default function VersionSwitcher({ entries }: { entries: VersionSwitcherEntry[] }) {
const active = entries.find((entry) => entry.active);

return (
<details className="relative block text-left">
<summary className="cursor-pointer list-none flex w-full items-center justify-between gap-2 rounded-lg border border-neutral-700/60 bg-neutral-900/60 px-3 py-2 text-xs font-medium text-gray-300 hover:text-white hover:border-neutral-500 transition-colors">
<span className="inline-flex items-center gap-2 truncate">
<svg className="h-3.5 w-3.5 shrink-0 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M7 7h10M7 12h10M7 17h6" />
</svg>
{active?.label ?? "Select version"}
</span>
<svg className="h-3 w-3 shrink-0 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</summary>
<div className="absolute z-20 mt-2 w-full min-w-56 rounded-lg border border-neutral-700/60 bg-neutral-800 p-1 shadow-xl">
{entries.map((entry) =>
entry.active ? (
<span
key={entry.label}
className="block rounded-md px-3 py-2 text-xs font-semibold text-white bg-neutral-700/40"
>
{entry.label} ✓
</span>
) : (
<Link
key={entry.label}
href={entry.href}
className="block rounded-md px-3 py-2 text-xs text-gray-300 hover:bg-neutral-700/60 hover:text-white transition-colors"
>
{entry.label}
</Link>
)
)}
<Link
href="/docs/versions"
className="block rounded-md px-3 py-2 text-xs text-blue-400 hover:bg-neutral-700/60 hover:text-blue-300 transition-colors border-t border-neutral-700/60 mt-1 pt-2"
>
All versions →
</Link>
</div>
</details>
);
}
Loading