|
1 | | -import { race, tryCatch } from '@/lib/async'; |
2 | | -import { joinPath } from '@/lib/paths'; |
3 | | -import { GitBookAPI, type GitBookAPIError, type PublishedSiteContentLookup } from '@gitbook/api'; |
4 | | -import { GITBOOK_API_TOKEN, GITBOOK_API_URL, GITBOOK_USER_AGENT } from '@v2/lib/env'; |
5 | | - |
6 | | -/** |
7 | | - * Lookup a content by its URL using the GitBook API. |
8 | | - * To optimize caching, we try multiple lookup alternatives and return the first one that matches. |
9 | | - */ |
10 | | -export async function getPublishedContentByURL(input: { |
11 | | - url: string; |
12 | | - visitorAuthToken: string | null; |
13 | | - redirectOnError: boolean; |
14 | | -}): Promise< |
15 | | - | { data: PublishedSiteContentLookup; error?: undefined } |
16 | | - | { data?: undefined; error: Error | GitBookAPIError } |
17 | | -> { |
18 | | - const lookupURL = new URL(input.url); |
19 | | - const url = stripURLSearch(lookupURL); |
20 | | - const lookup = getURLLookupAlternatives(url); |
21 | | - |
22 | | - const result = await race(lookup.urls, async (alternative, { signal }) => { |
23 | | - const api = new GitBookAPI({ |
24 | | - authToken: GITBOOK_API_TOKEN ?? undefined, |
25 | | - endpoint: GITBOOK_API_URL, |
26 | | - userAgent: GITBOOK_USER_AGENT, |
27 | | - }); |
28 | | - |
29 | | - const callResult = await tryCatch( |
30 | | - api.urls.getPublishedContentByUrl( |
31 | | - { |
32 | | - url: alternative.url, |
33 | | - visitorAuthToken: input.visitorAuthToken ?? undefined, |
34 | | - redirectOnError: input.redirectOnError, |
35 | | - cache: true, |
36 | | - }, |
37 | | - { |
38 | | - signal, |
39 | | - headers: { |
40 | | - 'x-gitbook-force-cache': 'true', |
41 | | - }, |
42 | | - } |
43 | | - ) |
44 | | - ); |
45 | | - |
46 | | - if (callResult.error) { |
47 | | - if (alternative.primary) { |
48 | | - // We only return an error for the primary alternative (full URL), |
49 | | - // as other parts could result in errors due to the URL being incomplete (share links, etc). |
50 | | - return { error: callResult.error }; |
51 | | - } |
52 | | - return null; |
53 | | - } |
54 | | - |
55 | | - const { |
56 | | - data: { data }, |
57 | | - } = callResult; |
58 | | - |
59 | | - if ('redirect' in data) { |
60 | | - if (alternative.primary) { |
61 | | - // Append the path to the redirect URL |
62 | | - // because we might have matched a shorter path and the redirect is relative to it |
63 | | - if (alternative.extraPath) { |
64 | | - if (data.target === 'content') { |
65 | | - const redirect = new URL(data.redirect); |
66 | | - redirect.pathname = joinPath(redirect.pathname, alternative.extraPath); |
67 | | - data.redirect = redirect.toString(); |
68 | | - } else { |
69 | | - const redirect = new URL(data.redirect); |
70 | | - if (redirect.searchParams.has('location')) { |
71 | | - redirect.searchParams.set( |
72 | | - 'location', |
73 | | - joinPath( |
74 | | - redirect.searchParams.get('location') ?? '', |
75 | | - alternative.extraPath |
76 | | - ) |
77 | | - ); |
78 | | - data.redirect = redirect.toString(); |
79 | | - } |
80 | | - } |
81 | | - } |
82 | | - |
83 | | - return { data }; |
84 | | - } |
85 | | - |
86 | | - return null; |
87 | | - } |
88 | | - |
89 | | - /** |
90 | | - * We use the following criteria to determine if the lookup result is the right one: |
91 | | - * - the primary alternative was resolved (because that's the longest or most inclusive path) |
92 | | - * - the resolution of the site URL is complete (because we want to resolve the deepest path possible) |
93 | | - * |
94 | | - * In both cases, the idea is to use the deepest/longest/most inclusive path to resolve the content. |
95 | | - */ |
96 | | - if (alternative.primary || ('site' in data && data.complete)) { |
97 | | - const siteResult: PublishedSiteContentLookup = { |
98 | | - ...data, |
99 | | - changeRequest: data.changeRequest ?? lookup.changeRequest, |
100 | | - revision: data.revision ?? lookup.revision, |
101 | | - basePath: joinPath(data.basePath, lookup.basePath ?? ''), |
102 | | - pathname: joinPath(data.pathname, alternative.extraPath), |
103 | | - }; |
104 | | - return { data: siteResult }; |
105 | | - } |
106 | | - |
107 | | - return null; |
108 | | - }); |
109 | | - |
110 | | - if (!result) { |
111 | | - return { |
112 | | - error: new Error('No content found'), |
113 | | - }; |
114 | | - } |
115 | | - |
116 | | - return result; |
117 | | -} |
118 | | - |
119 | 1 | /** |
120 | 2 | * For a given GitBook URL, return a list of alternative URLs that could be matched against to lookup the content. |
121 | 3 | * The approach is optimized to aim at reusing cached lookup results as much as possible. |
|
0 commit comments