Skip to content
Merged
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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,6 @@ styles/write-good/
cli-test-report.json

# Generated API documentation (rebuilt on deploy)
src/content/docs/api/
# Manual pages in _manual/ are tracked, versioned output is generated
src/content/docs/api/*
!src/content/docs/api/_manual/
1 change: 1 addition & 0 deletions astro.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ export default defineConfig({
Head: './src/components/Head.astro',
Header: './src/components/Header.astro',
Search: './src/components/Search.astro',
Sidebar: './src/components/Sidebar.astro',
ThemeSelect: './src/components/ThemeSelect.astro',
ThemeProvider: './src/components/ThemeProvider.astro',
PageTitle: './src/components/PageTitle.astro',
Expand Down
79 changes: 36 additions & 43 deletions scripts/generate-api-docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,14 @@
*/

import { constants } from 'node:fs';
import { access, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
import {
access,
mkdir,
readdir,
readFile,
rm,
writeFile,
} from 'node:fs/promises';
import { join } from 'node:path';
import {
API_VERSIONS,
Expand Down Expand Up @@ -1328,9 +1335,11 @@ ${itemsStr}
}

// ============================================================================
// Manual page preservation
// Manual page handling - shared across all versions
// ============================================================================

const MANUAL_PAGES_DIR = join(OUTPUT_BASE_DIR, '_manual');

async function fileExists(path: string): Promise<boolean> {
try {
await access(path, constants.F_OK);
Expand All @@ -1340,31 +1349,20 @@ async function fileExists(path: string): Promise<boolean> {
}
}

async function preserveManualPages(
versionId: string,
): Promise<Map<string, string>> {
const preserved = new Map<string, string>();

/**
* Copy manual pages from _manual/ directory to a specific version directory.
* Manual pages are shared across all versions.
*/
async function copyManualPagesToVersion(versionId: string): Promise<void> {
for (const page of MANUAL_PAGES) {
const filePath = join(OUTPUT_BASE_DIR, versionId, `${page.category}.mdx`);
if (await fileExists(filePath)) {
const content = await readFile(filePath, 'utf-8');
preserved.set(page.category, content);
console.log(` 📌 Preserving manual page: ${page.category}.mdx`);
}
}

return preserved;
}
const sourcePath = join(MANUAL_PAGES_DIR, `${page.category}.mdx`);
const destPath = join(OUTPUT_BASE_DIR, versionId, `${page.category}.mdx`);

async function restoreManualPages(
versionId: string,
preserved: Map<string, string>,
): Promise<void> {
for (const [category, content] of preserved) {
const filePath = join(OUTPUT_BASE_DIR, versionId, `${category}.mdx`);
await writeFile(filePath, content);
console.log(` 📌 Restored manual page: ${category}.mdx`);
if (await fileExists(sourcePath)) {
const content = await readFile(sourcePath, 'utf-8');
await writeFile(destPath, content);
console.log(` 📄 Copied manual page: ${page.category}.mdx`);
}
}
}

Expand Down Expand Up @@ -1492,34 +1490,29 @@ async function main() {
`📋 Versions to generate: ${API_VERSIONS.map((v) => v.id).join(', ')}`,
);

// Preserve manual pages before cleaning
console.log('\n📌 Preserving manual pages...');
const preservedPages = new Map<string, Map<string, string>>();
for (const version of API_VERSIONS) {
const preserved = await preserveManualPages(version.id);
if (preserved.size > 0) {
preservedPages.set(version.id, preserved);
}
}

// Clean output directory
// Clean output directory (but preserve _manual/)
console.log('\n🧹 Cleaning output directory...');
try {
await rm(OUTPUT_BASE_DIR, { recursive: true, force: true });
const entries = await readdir(OUTPUT_BASE_DIR);
for (const entry of entries) {
if (entry !== '_manual') {
await rm(join(OUTPUT_BASE_DIR, entry), {
recursive: true,
force: true,
});
}
}
} catch {
// Directory may not exist
await mkdir(OUTPUT_BASE_DIR, { recursive: true });
}
await mkdir(OUTPUT_BASE_DIR, { recursive: true });

// Generate docs for each version
for (const version of API_VERSIONS) {
await generateVersionDocs(version);

// Restore manual pages for this version
const preserved = preservedPages.get(version.id);
if (preserved) {
await restoreManualPages(version.id, preserved);
}
// Copy shared manual pages to this version
await copyManualPagesToVersion(version.id);
}

// Generate root redirect page
Expand Down
58 changes: 47 additions & 11 deletions src/components/Head.astro
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,38 @@ const breadcrumbSchema = {
item: item.url,
})),
};

// API version redirect - check if we need to redirect to stored preference
const currentPath = Astro.url.pathname;
const apiMatch = currentPath.match(/^\/api\/([^/]+)/);
const isApiPage = apiMatch !== null;

// Build version map for the inline script
import { API_VERSIONS, versionToSlug, getVersionFromPath } from '@/lib/api-versions';
const versionMap = Object.fromEntries(
API_VERSIONS.map(v => [v.id, versionToSlug(v.id)])
);
const currentVersionId = isApiPage ? getVersionFromPath(currentPath) : null;
---

{/* Version redirect script - must run immediately before any content */}
{isApiPage && (
<script is:inline define:vars={{ versionMap, currentVersionId }}>
(function() {
var STORAGE_KEY = 'sprites-api-version';
var storedVersion = localStorage.getItem(STORAGE_KEY);
if (!storedVersion || !currentVersionId || storedVersion === currentVersionId) return;
var storedSlug = versionMap[storedVersion];
if (!storedSlug) return;
var path = window.location.pathname;
var match = path.match(/^\/api\/[^/]+\/?(.*)$/);
var page = match ? match[1] : '';
var newPath = '/api/' + storedSlug + (page ? '/' + page : '/');
window.location.replace(newPath);
})();
</script>
)}

<Default {...Astro.props}><slot /></Default>
<Background />

Expand Down Expand Up @@ -219,9 +249,13 @@ const breadcrumbSchema = {
function initSidebarNavigation() {
// Normalize path by removing trailing slash for comparison
const normalizePath = (path) => path.replace(/\/$/, '') || '/';

// Strip API version from path for comparison (e.g., /api/v001-rc30/sprites -> /api/sprites)
const stripApiVersion = (path) => path.replace(/^\/api\/[^/]+/, '/api');

const currentPath = normalizePath(window.location.pathname);
const currentPathNoVersion = stripApiVersion(currentPath);
const currentHash = window.location.hash;
const currentUrl = currentPath + currentHash;

// Find all nested sidebar groups within API Reference
const sidebarGroups = document.querySelectorAll('nav[aria-label="Main"] details details');
Expand All @@ -237,10 +271,11 @@ const breadcrumbSchema = {
// Extract the page URL without the anchor and normalize
const href = firstLink.getAttribute('href') || '';
const pageUrl = normalizePath(href.split('#')[0]);
const pageUrlNoVersion = stripApiVersion(pageUrl);
if (!pageUrl) return;

// Auto-expand if current page matches this group's page
if (currentPath === pageUrl) {
// Auto-expand if current page matches this group's page (comparing without version)
if (currentPathNoVersion === pageUrlNoVersion) {
details.setAttribute('open', '');
}

Expand All @@ -249,15 +284,15 @@ const breadcrumbSchema = {
allLinks.forEach((link) => {
const linkHref = link.getAttribute('href') || '';
const linkPath = normalizePath(linkHref.split('#')[0]);
const linkPathNoVersion = stripApiVersion(linkPath);
const linkHash = linkHref.includes('#') ? '#' + linkHref.split('#')[1] : '';
const fullLinkUrl = linkPath + linkHash;

// Only mark as current if:
// 1. Link has a hash and it matches the current URL exactly (path + hash)
// 2. Link has no hash and we're on that exact page with no hash
// 1. Link has a hash and path matches (ignoring version) + hash matches exactly
// 2. Link has no hash and we're on that exact page (ignoring version) with no hash
const isCurrentLink = linkHash
? fullLinkUrl === currentUrl
: (linkPath === currentPath && !currentHash);
? (linkPathNoVersion === currentPathNoVersion && linkHash === currentHash)
: (linkPathNoVersion === currentPathNoVersion && !currentHash);

if (isCurrentLink) {
link.setAttribute('data-current', 'true');
Expand All @@ -275,14 +310,15 @@ const breadcrumbSchema = {
// Allow normal toggle behavior with modifier keys
if (e.shiftKey || e.ctrlKey || e.metaKey) return;

// If currently closed, expand and navigate to page
if (!details.open) {
// If currently closed and we're NOT already on this page, expand and navigate
// (Compare without version to handle being on different version of same page)
if (!details.open && currentPathNoVersion !== pageUrlNoVersion) {
e.preventDefault();
e.stopPropagation();
// Navigate to the page (without anchor hash)
window.location.href = pageUrl;
}
// If already open, just toggle closed (default behavior)
// If already on this page or already open, let default toggle behavior happen
});

// Make summary look clickable
Expand Down
9 changes: 0 additions & 9 deletions src/components/PageTitle.astro
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,14 @@
import StarlightPageTitle from '@astrojs/starlight/components/PageTitle.astro';
import CopyPageButton from './CopyPageButton.astro';
import { ContentBreadcrumbs } from './react/Breadcrumbs';
import { VersionSelector } from './react/api/VersionSelector';
import { getVersionFromPath, API_VERSIONS } from '@/lib/api-versions';

const currentPath = Astro.url.pathname;
const sidebar = Astro.locals.starlightRoute?.sidebar ?? [];

// Check if this is a versioned API page
const versionId = getVersionFromPath(currentPath);
const isVersionedApiPage = versionId !== null && API_VERSIONS.length > 1;
---

<div class="page-nav-container">
<ContentBreadcrumbs client:load currentPath={currentPath} sidebar={sidebar} />
<div class="page-nav-actions">
{isVersionedApiPage && (
<VersionSelector client:load currentPath={currentPath} />
)}
<CopyPageButton />
</div>
</div>
Expand Down
33 changes: 33 additions & 0 deletions src/components/Sidebar.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
import StarlightSidebar from '@astrojs/starlight/components/Sidebar.astro';
import { VersionSelector } from './react/api/VersionSelector';
import { getVersionFromPath, API_VERSIONS } from '@/lib/api-versions';

const currentPath = Astro.url.pathname;

// Check if this is a versioned API page (or any page where we want to show the selector)
const versionId = getVersionFromPath(currentPath);
const showVersionSelector = versionId !== null && API_VERSIONS.length > 1;
---

<StarlightSidebar {...Astro.props}>
<slot />
</StarlightSidebar>

{showVersionSelector && (
<div class="sidebar-version-selector">
<VersionSelector client:load currentPath={currentPath} />
</div>
)}

{/* Version redirect is handled in Head.astro for earliest execution */}

<style>
.sidebar-version-selector {
position: sticky;
bottom: 0;
padding: 1rem;
background: var(--sl-color-bg-sidebar);
margin-top: auto;
}
</style>
16 changes: 10 additions & 6 deletions src/components/react/api/VersionSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {
getVersionFromPath,
} from '@/lib/api-versions';

const STORAGE_KEY = 'sprites-api-version';

interface VersionSelectorProps {
currentPath: string;
}
Expand All @@ -36,7 +38,7 @@ function BadgeIcon({ badge }: { badge?: APIVersion['badge'] }) {

return (
<span
className={`ml-2 px-1.5 py-0.5 text-[10px] font-medium rounded ${colors[badge]}`}
className={`px-1.5 py-0.5 text-[10px] font-medium rounded ${colors[badge]}`}
>
{labels[badge]}
</span>
Expand All @@ -54,24 +56,26 @@ export function VersionSelector({ currentPath }: VersionSelectorProps) {
}

const handleVersionChange = (newVersionId: string) => {
// Save preference and navigate
localStorage.setItem(STORAGE_KEY, newVersionId);
const newPath = buildVersionedPath(newVersionId, currentPage);
window.location.href = newPath;
};

return (
<Select value={currentVersionId || ''} onValueChange={handleVersionChange}>
<SelectTrigger className="w-[160px] h-8 text-sm bg-[var(--sl-color-bg)] border-[var(--sl-color-gray-5)]">
<SelectTrigger className="w-full h-9 text-sm bg-[var(--sl-color-bg)] border-[var(--sl-color-gray-5)]">
<SelectValue>
<div className="flex items-center">
<div className="flex items-center gap-2">
<span>{currentVersion.label}</span>
<BadgeIcon badge={currentVersion.badge} />
</div>
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectContent className="min-w-[200px]">
{API_VERSIONS.map((version) => (
<SelectItem key={version.id} value={version.id}>
<div className="flex items-center">
<SelectItem key={version.id} value={version.id} className="py-2">
<div className="flex items-center gap-2">
<span>{version.label}</span>
<BadgeIcon badge={version.badge} />
</div>
Expand Down
Loading
Loading