Files
v10/site/src/utils/docs/routing.ts
T
2026-02-24 15:34:34 -06:00

237 lines
6.7 KiB
TypeScript

import { sidebar as defaultSidebar } from '@/docs.config';
import type { Sidebar, SupportedFramework } from '@/types/docs';
import { DEFAULT_FRAMEWORK, isValidFramework } from '@/types/docs';
import { findFirstGuide, findGuideBySlug, getValidFrameworksForGuide } from './sidebar';
/**
* Build a docs URL from framework and guide slug components.
*/
export function buildDocsUrl(framework: SupportedFramework, guideSlug: string): string {
return `/docs/framework/${framework}/${guideSlug}`;
}
/**
* Input for resolveIndexRedirect
*/
export interface IndexRedirectInput {
preferences: {
framework: string | null;
};
params: {
framework?: string;
};
}
/**
* Output from resolveIndexRedirect
*/
export interface IndexRedirectResult {
url: string;
selectedFramework: SupportedFramework;
selectedSlug: string;
reason: string;
}
/**
* Resolve redirect for index pages (/docs, /docs/framework/X).
* Nothing is pinned - we must select framework AND slug.
*
* Logic:
* 1. If params.framework → validate → find first guide
* 2. If no param → get from preferences or defaults → find first guide
*
* @param input - The input containing preferences and params
* @param sidebar - Optional sidebar to search (defaults to main sidebar config)
*/
export function resolveIndexRedirect(
input: IndexRedirectInput,
sidebar: Sidebar = defaultSidebar
): IndexRedirectResult {
const { preferences, params } = input;
let selectedFramework: SupportedFramework;
let reason: string;
if (params.framework) {
// Framework in params - validate it
if (!isValidFramework(params.framework)) {
throw new Error(`Invalid framework param: ${params.framework}`);
}
selectedFramework = params.framework;
reason = 'Using validated params.framework';
} else {
// No params - use preferences or defaults
if (preferences.framework && isValidFramework(preferences.framework)) {
selectedFramework = preferences.framework;
reason = 'Using preferences.framework';
} else {
// Use all defaults
selectedFramework = DEFAULT_FRAMEWORK;
reason = 'Using default framework (no valid preferences)';
}
}
// Find the first guide for the selected framework
const selectedSlug = findFirstGuide(selectedFramework, sidebar);
const url = buildDocsUrl(selectedFramework, selectedSlug);
return {
url,
selectedFramework,
selectedSlug,
reason,
};
}
/**
* Input for resolveFrameworkChange
*/
export interface FrameworkChangeInput {
currentFramework: SupportedFramework;
currentSlug: string;
newFramework: SupportedFramework;
}
/**
* Output from resolveFrameworkChange
*/
export interface FrameworkChangeResult {
url: string;
shouldReplace: boolean;
selectedFramework: SupportedFramework;
selectedSlug: string;
slugChanged: boolean;
reason: string;
}
/**
* Resolve URL when user changes framework selector.
* newFramework is PINNED (must keep), slug MAY change if not visible.
*
* Logic:
* 1. framework = newFramework (PINNED)
* 2. If currentSlug visible in newFramework → slug = currentSlug, shouldReplace = true
* Else → slug = first guide in newFramework, shouldReplace = false
*
* @param input - The input containing current state and new framework
* @param sidebar - Optional sidebar to search (defaults to main sidebar config)
*/
export function resolveFrameworkChange(
input: FrameworkChangeInput,
sidebar: Sidebar = defaultSidebar
): FrameworkChangeResult {
const { currentSlug, newFramework } = input;
if (!isValidFramework(newFramework)) {
throw new Error(`Invalid framework: ${newFramework}`);
}
const selectedFramework = newFramework; // PINNED
// Determine the slug to use
let selectedSlug: string;
let shouldReplace: boolean;
let slugChanged: boolean;
let reason: string;
const guide = findGuideBySlug(currentSlug, sidebar);
const validFrameworks = guide ? getValidFrameworksForGuide(guide, sidebar) : [];
if (guide && validFrameworks.includes(selectedFramework)) {
// Current slug is visible in the new framework
selectedSlug = currentSlug;
shouldReplace = true;
slugChanged = false;
reason = 'Changed framework, kept slug (visible in new framework)';
} else {
// Current slug is not visible, find first guide
selectedSlug = findFirstGuide(selectedFramework, sidebar);
shouldReplace = false;
slugChanged = true;
reason = 'Changed framework, changed slug (slug not visible in new framework)';
}
const url = buildDocsUrl(selectedFramework, selectedSlug);
return {
url,
shouldReplace,
selectedFramework,
selectedSlug,
slugChanged,
reason,
};
}
/**
* Input for resolveDocsLinkUrl
*/
export interface DocsLinkInput {
targetSlug: string;
contextFramework: SupportedFramework;
}
/**
* Output from resolveDocsLinkUrl
*/
export interface DocsLinkResult {
url: string;
selectedFramework: SupportedFramework;
selectedSlug: string;
priorityLevel: 1 | 2;
reason: string;
}
/**
* Resolve the best URL for a guide slug link given current context.
* targetSlug is PINNED (must keep), framework MAY change.
*
* Logic (2-level priority cascade):
* 1. slug = targetSlug (PINNED)
* 2. Try to find best framework that supports targetSlug:
* - Priority 1: If targetSlug visible in contextFramework → use it (best UX)
* - Priority 2: Use guide's first valid framework
*
* @param input - The input containing target slug and context
* @param sidebar - Optional sidebar to search (defaults to main sidebar config)
*/
export function resolveDocsLinkUrl(input: DocsLinkInput, sidebar: Sidebar = defaultSidebar): DocsLinkResult {
const { targetSlug, contextFramework } = input;
const guide = findGuideBySlug(targetSlug, sidebar);
if (!guide) {
throw new Error(`No guide found with slug "${targetSlug}"`);
}
if (!isValidFramework(contextFramework)) {
throw new Error(`Invalid context framework: ${contextFramework}`);
}
const selectedSlug = targetSlug; // PINNED
let selectedFramework: SupportedFramework;
let priorityLevel: 1 | 2;
let reason: string;
// Priority 1: Try current framework
const validFrameworks = getValidFrameworksForGuide(guide, sidebar);
if (validFrameworks.includes(contextFramework)) {
selectedFramework = contextFramework;
priorityLevel = 1;
reason = 'Priority 1: Kept framework (slug visible in current context)';
} else {
// Priority 2: Fallback to guide's first valid framework
selectedFramework = validFrameworks[0];
priorityLevel = 2;
reason = 'Priority 2: Changed framework (slug not visible in current context)';
}
const url = buildDocsUrl(selectedFramework, selectedSlug);
return {
url,
selectedFramework,
selectedSlug,
priorityLevel,
reason,
};
}