|
| 1 | +import findPage from '@/frame/lib/find-page' |
| 2 | +import { allVersionKeys } from '@/versions/lib/all-versions' |
1 | 3 | import type { Context, Page } from '@/types' |
2 | 4 |
|
3 | 5 | /** |
4 | | - * Resolves an href to a Page object from the context |
| 6 | + * Resolves an href to a Page object from the context. |
5 | 7 | * |
6 | | - * This function handles various href formats: |
7 | | - * - External URLs (http/https) - returns undefined |
8 | | - * - Language-prefixed absolute paths (/en/copilot/...) - direct lookup |
9 | | - * - Absolute paths without language (/copilot/...) - adds language prefix |
10 | | - * - Relative paths (get-started) - resolved relative to pathname |
11 | | - * |
12 | | - * The function searches through context.pages using multiple strategies: |
13 | | - * 1. Direct key lookup with language prefix |
14 | | - * 2. Relative path joining with current pathname |
15 | | - * 3. endsWith matching for versioned keys (e.g., /en/enterprise-cloud@latest/...) |
16 | | - * |
17 | | - * @param href - The href to resolve |
18 | | - * @param languageCode - The language code (e.g., 'en') |
19 | | - * @param pathname - The current page's pathname (e.g., '/en/copilot') |
20 | | - * @param context - The rendering context containing all pages |
21 | | - * @returns The resolved Page object, or undefined if not found |
22 | | - * |
23 | | - * @example |
24 | | - * ```typescript |
25 | | - * // Absolute path with language |
26 | | - * resolvePath('/en/copilot/quickstart', 'en', '/en/copilot', context) |
27 | | - * |
28 | | - * // Absolute path without language (adds /en/) |
29 | | - * resolvePath('/copilot/quickstart', 'en', '/en/copilot', context) |
30 | | - * |
31 | | - * // Relative path (resolves to /en/copilot/quickstart) |
32 | | - * resolvePath('quickstart', 'en', '/en/copilot', context) |
33 | | - * |
34 | | - * // Relative path with leading slash (resolves relative to pathname) |
35 | | - * resolvePath('/quickstart', 'en', '/en/copilot', context) // -> /en/copilot/quickstart |
36 | | - * ``` |
| 8 | + * Normalizes various href formats (relative, absolute, with/without language |
| 9 | + * prefix) to canonical paths, then delegates to findPage for lookup with |
| 10 | + * redirect support and English fallback. |
37 | 11 | */ |
38 | 12 | export function resolvePath( |
39 | 13 | href: string, |
40 | 14 | languageCode: string, |
41 | 15 | pathname: string, |
42 | 16 | context: Context, |
43 | 17 | ): Page | undefined { |
44 | | - // External URLs cannot be resolved |
45 | | - if (href.startsWith('http://') || href.startsWith('https://')) { |
46 | | - return undefined |
47 | | - } |
48 | | - |
49 | | - if (!context.pages) { |
50 | | - return undefined |
51 | | - } |
| 18 | + if (href.startsWith('http://') || href.startsWith('https://')) return undefined |
| 19 | + if (!context.pages) return undefined |
52 | 20 |
|
53 | | - // Normalize href to start with / |
54 | | - const normalizedHref = href.startsWith('/') ? href : `/${href}` |
| 21 | + const { pages, redirects } = context |
55 | 22 |
|
56 | | - // Build full path with language prefix if needed |
57 | | - let fullPath: string |
58 | | - if (normalizedHref.startsWith(`/${languageCode}/`)) { |
59 | | - // Already has language prefix |
60 | | - fullPath = normalizedHref |
61 | | - } else if (href.startsWith('/') && !href.startsWith(`/${languageCode}/`)) { |
62 | | - // Path with leading slash but no language prefix - treat as relative to pathname |
63 | | - // e.g., pathname='/en/copilot', href='/get-started' -> '/en/copilot/get-started' |
64 | | - fullPath = pathname + href |
65 | | - } else { |
66 | | - // Relative path - add language prefix |
67 | | - // e.g., href='quickstart' -> '/en/quickstart' |
68 | | - fullPath = `/${languageCode}${normalizedHref}` |
| 23 | + for (const candidate of candidates(href, languageCode, pathname)) { |
| 24 | + const page = |
| 25 | + findPage(candidate, pages, redirects) || |
| 26 | + findPage(candidate.replace(/\/?$/, '/'), pages, redirects) |
| 27 | + if (page) return page |
69 | 28 | } |
70 | 29 |
|
71 | | - // Clean up trailing slashes |
72 | | - const cleanPath = fullPath.replace(/\/$/, '') |
73 | | - |
74 | | - // Strategy 1: Direct lookup |
75 | | - if (context.pages[cleanPath]) { |
76 | | - return context.pages[cleanPath] |
77 | | - } |
78 | | - |
79 | | - // Strategy 2: Try relative to current pathname |
80 | | - const currentPath = pathname.replace(/\/$/, '') |
81 | | - const relativeHref = href.startsWith('/') ? href.slice(1) : href |
82 | | - const joinedPath = `${currentPath}/${relativeHref}` |
83 | | - |
84 | | - if (context.pages[joinedPath]) { |
85 | | - return context.pages[joinedPath] |
86 | | - } |
| 30 | + return undefined |
| 31 | +} |
87 | 32 |
|
88 | | - // Strategy 3: Search for keys that end with the path (handles versioned keys) |
89 | | - // e.g., key='/en/enterprise-cloud@latest/copilot' should match path='/en/copilot' |
90 | | - for (const [key, page] of Object.entries(context.pages)) { |
91 | | - if (key.endsWith(cleanPath) || key.endsWith(`${cleanPath}/`)) { |
92 | | - return page |
93 | | - } |
| 33 | +// Lazily yields candidate paths in priority order, stopping at first match. |
| 34 | +function* candidates(href: string, lang: string, pathname: string) { |
| 35 | + const langPrefix = `/${lang}/` |
| 36 | + const cleanPathname = pathname.replace(/\/$/, '') |
| 37 | + |
| 38 | + if (href.startsWith(langPrefix)) { |
| 39 | + // Already has language prefix — use as-is |
| 40 | + yield href |
| 41 | + } else if (href.startsWith('/')) { |
| 42 | + // Leading slash without lang prefix — try relative to pathname first, |
| 43 | + // then as a direct path with lang prefix |
| 44 | + yield `${cleanPathname}${href}` |
| 45 | + yield `${langPrefix.slice(0, -1)}${href}` |
| 46 | + } else { |
| 47 | + // Relative path — try relative to pathname, then with lang prefix |
| 48 | + yield `${cleanPathname}/${href}` |
| 49 | + yield `${langPrefix}${href}` |
94 | 50 | } |
95 | 51 |
|
96 | | - // Strategy 4: If href started with /, try endsWith matching on that too |
97 | | - if (href.startsWith('/')) { |
98 | | - const hrefClean = href.replace(/\/$/, '') |
99 | | - for (const [key, page] of Object.entries(context.pages)) { |
100 | | - if (key.endsWith(hrefClean) || key.endsWith(`${hrefClean}/`)) { |
101 | | - return page |
102 | | - } |
| 52 | + // Versioned fallback: try inserting each version slug for |
| 53 | + // enterprise-only pages that don't exist on FPT. |
| 54 | + const suffix = href.startsWith(langPrefix) |
| 55 | + ? href.slice(langPrefix.length).replace(/\/$/, '') |
| 56 | + : href.replace(/^\//, '').replace(/\/$/, '') |
| 57 | + if (suffix) { |
| 58 | + for (const version of allVersionKeys) { |
| 59 | + yield `${langPrefix}${version}/${suffix}` |
103 | 60 | } |
104 | 61 | } |
105 | | - |
106 | | - return undefined |
107 | 62 | } |
0 commit comments