Compare commits

...

5 Commits

Author SHA1 Message Date
Brett Jephson b8ba267c4c Show a dead link's destination on its own line in the card
The destination read as an afterthought at the end of the not-found
paragraph. It now has its own "→ Opens <space>" line with the space title in
bold, and the string is shortened to "Opens ${1}" in every language.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rt9YwdSUu7gHehYbEkvEJ8
2026-09-25 12:05:40 +01:00
Brett Jephson dd28fb0114 Merge branch 'main' into brett/rnd-13042-render-app-urls 2026-09-25 12:00:28 +01:00
Brett Jephson 8dc137e994 Say where a dead link goes in the "Page not found" card
A link to a page that no longer exists opens its space on the site, but the
card only said the page was gone. It now adds which space the link opens,
and the fallback's text is that site space's title instead of "space", which
also fixes the title shown on a dead content-ref block card. A dead link into
a space outside the site keeps the existing wording and app fallback.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rt9YwdSUu7gHehYbEkvEJ8
2026-09-25 11:27:55 +01:00
Brett Jephson 9078855d44 Merge branch 'main' into brett/rnd-13042-render-app-urls 2026-09-25 09:03:55 +01:00
Brett Jephson f2fddad24c Keep app URLs into the same site's spaces on the site
A link stored as a URL to a GitBook app page in a space of the current site
now renders as the same page path on the site, so readers of a published
site aren't sent to the app. When a page ref can't be resolved, its fallback
link goes to the site space instead of the app.

Neither case checks that the page exists: a missing page lands on the site's
not-found page rather than the app's login.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rt9YwdSUu7gHehYbEkvEJ8
2026-09-24 14:42:16 +01:00
46 changed files with 234 additions and 18 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Keep links to GitBook app URLs of spaces in the same site on the site, instead of sending readers to app.gitbook.com. A link to a page that no longer exists now opens its space on the site, and its "Page not found" card says which space it opens.
@@ -24,12 +24,12 @@ export async function BlockContentRef(props: BlockProps<DocumentBlockContentRef>
: null;
if (!resolved) {
const fallback = resolveContentRefFallback(block.data.ref);
const fallback = resolveContentRefFallback(block.data.ref, context.contentContext);
if (!fallback) {
return null;
}
return (
<NotFoundRefHoverCard context={context}>
<NotFoundRefHoverCard context={context} fallback={fallback}>
<BlockContentRefCard
contentRef={block.data.ref}
resolved={fallback}
@@ -79,9 +79,11 @@ export async function InlineLinkButton(
? await resolveContentRefInDocument(document, inline.data.ref, context.contentContext)
: null;
const href =
resolved?.href ??
(inline.data.ref ? resolveContentRefFallback(inline.data.ref)?.href : undefined);
const fallback =
!resolved && inline.data.ref
? resolveContentRefFallback(inline.data.ref, context.contentContext)
: null;
const href = resolved?.href ?? fallback?.href;
const sharedProps: React.ComponentProps<typeof Button> = {
...buttonProps,
insights: {
@@ -105,7 +107,11 @@ export async function InlineLinkButton(
);
if (inline.data.ref && !resolved) {
return <NotFoundRefHoverCard context={context}>{button}</NotFoundRefHoverCard>;
return (
<NotFoundRefHoverCard context={context} fallback={fallback}>
{button}
</NotFoundRefHoverCard>
);
}
return button;
@@ -13,6 +13,7 @@ import {
resolveContentRefFallback,
resolveContentRefInDocument,
} from '@/lib/references';
import { checkIsExternalURL } from '@/lib/urls';
export async function InlineLink(props: InlineProps<DocumentInlineLink>) {
const { document, inline, context, ancestorInlines } = props;
@@ -36,11 +37,15 @@ export async function InlineLink(props: InlineProps<DocumentInlineLink>) {
);
if (!resolved) {
const fallback = resolveContentRefFallback(inline.data.ref);
const fallback = resolveContentRefFallback(inline.data.ref, contentContext);
return (
<NotFoundRefHoverCard context={context}>
<NotFoundRefHoverCard context={context} fallback={fallback}>
{fallback ? (
<InlineLinkAnchor href={fallback.href} contentRef={inline.data.ref} isExternal>
<InlineLinkAnchor
href={fallback.href}
contentRef={inline.data.ref}
isExternal={checkIsExternalURL(fallback.href)}
>
{inlinesElement}
</InlineLinkAnchor>
) : (
@@ -53,7 +58,7 @@ export async function InlineLink(props: InlineProps<DocumentInlineLink>) {
<InlineLinkAnchor
href={resolved.href}
contentRef={inline.data.ref}
isExternal={inline.data.ref.kind === 'url'}
isExternal={isExternalLink(inline, resolved)}
>
{inlinesElement}
</InlineLinkAnchor>
@@ -121,7 +126,7 @@ function InlineLinkTooltipWrapper(props: {
let breadcrumbs = resolved.ancestors ?? [];
const isMailto = resolved.href.startsWith('mailto:');
const isExternal = inline.data.ref.kind === 'url';
const isExternal = isExternalLink(inline, resolved);
const isSamePage = inline.data.ref.kind === 'anchor' && inline.data.ref.page === undefined;
if (isMailto) {
@@ -161,3 +166,10 @@ function InlineLinkTooltipWrapper(props: {
</InlineLinkTooltip>
);
}
/**
* A URL link resolved to a path in the site is internal, even though its ref is a URL.
*/
function isExternalLink(inline: DocumentInlineLink, resolved: ResolvedContentRef): boolean {
return inline.data.ref.kind === 'url' && checkIsExternalURL(resolved.href);
}
@@ -2,8 +2,10 @@ import { Icon } from '@gitbook/icons';
import type { DocumentContextProps } from '../DocumentView';
import { HoverCard, HoverCardRoot, HoverCardTrigger } from '../primitives';
import { getSpaceLanguage, tString } from '@/intl/server';
import { getSpaceLanguage, t, tString } from '@/intl/server';
import { defaultLanguage } from '@/intl/translations';
import type { ResolvedContentRef } from '@/lib/references';
import { checkIsExternalURL } from '@/lib/urls';
/**
* Hover card displayed for a link not found.
@@ -11,13 +13,17 @@ import { defaultLanguage } from '@/intl/translations';
export async function NotFoundRefHoverCard(
props: DocumentContextProps & {
children: React.ReactNode;
/** Where the link goes instead, named in the card when it stays on the site. */
fallback?: ResolvedContentRef | null;
}
) {
const {
context: { contentContext },
children,
fallback,
} = props;
const language = contentContext ? await getSpaceLanguage(contentContext) : defaultLanguage;
const destination = fallback && !checkIsExternalURL(fallback.href) ? fallback.text : null;
return (
<HoverCardRoot>
<HoverCardTrigger>{children}</HoverCardTrigger>
@@ -27,6 +33,20 @@ export async function NotFoundRefHoverCard(
<h5 className="font-semibold">{tString(language, 'notfound_title')}</h5>
</div>
<p className="text-sm text-tint">{tString(language, 'notfound_link')}</p>
{destination ? (
<p className="mt-1 flex items-center gap-1.5 text-sm text-tint">
<Icon icon="arrow-right" className="size-3 shrink-0 text-tint-subtle" />
<span>
{t(
language,
'notfound_link_opens',
<span className="font-semibold text-tint-strong">
{destination}
</span>
)}
</span>
</p>
) : null}
</HoverCard>
</HoverCardRoot>
);
@@ -76,6 +76,7 @@ export const ar: TranslationLanguage = {
edit: 'تحرير',
notfound_title: 'الصفحة غير موجودة',
notfound_link: 'يشير هذا الرابط إلى صفحة تمت إزالتها أو لم تعد موجودة.',
notfound_link_opens: 'يفتح: ${1}',
notfound: 'الصفحة التي تبحث عنها غير موجودة.',
notfound_adaptive_title: 'الصفحة غير متاحة',
notfound_adaptive: 'قد تكون هذه الصفحة موجودة، لكن قد تحتاج إلى تسجيل الدخول للوصول إليها.',
@@ -77,6 +77,7 @@ export const bg: TranslationLanguage = {
edit: 'Редактиране',
notfound_title: 'Страницата не е намерена',
notfound_link: 'Тази връзка сочи към страница, която е премахната или вече не съществува.',
notfound_link_opens: 'Отваря: ${1}',
notfound: 'Страницата, която търсите, не съществува.',
notfound_adaptive_title: 'Страницата не е налична',
notfound_adaptive:
@@ -76,6 +76,7 @@ export const cs: TranslationLanguage = {
edit: 'Upravit',
notfound_title: 'Stránka nenalezena',
notfound_link: 'Tento odkaz vede na stránku, která byla odstraněna nebo již neexistuje.',
notfound_link_opens: 'Otevře: ${1}',
notfound: 'Stránka, kterou hledáte, neexistuje.',
notfound_adaptive_title: 'Stránka není dostupná',
notfound_adaptive:
@@ -76,6 +76,7 @@ export const da: TranslationLanguage = {
edit: 'Rediger',
notfound_title: 'Siden blev ikke fundet',
notfound_link: 'Dette link peger på en side, der er blevet fjernet eller ikke længere findes.',
notfound_link_opens: 'Åbner ${1}',
notfound: 'Siden, du leder efter, findes ikke.',
notfound_adaptive_title: 'Siden er ikke tilgængelig',
notfound_adaptive:
@@ -79,6 +79,7 @@ export const de: TranslationLanguage = {
notfound_title: 'Seite nicht gefunden',
notfound_link:
'Dieser Link verweist auf eine Seite, die entfernt wurde oder nicht mehr existiert.',
notfound_link_opens: 'Öffnet ${1}',
notfound: 'Die gesuchte Seite existiert nicht.',
notfound_adaptive_title: 'Seite nicht verfügbar',
notfound_adaptive:
@@ -77,6 +77,7 @@ export const el: TranslationLanguage = {
edit: 'Επεξεργασία',
notfound_title: 'Η σελίδα δεν βρέθηκε',
notfound_link: 'Αυτός ο σύνδεσμος οδηγεί σε μια σελίδα που έχει αφαιρεθεί ή δεν υπάρχει πλέον.',
notfound_link_opens: 'Ανοίγει: ${1}',
notfound: 'Η σελίδα που αναζητάτε δεν υπάρχει.',
notfound_adaptive_title: 'Η σελίδα δεν είναι διαθέσιμη',
notfound_adaptive:
@@ -74,6 +74,7 @@ export const en = {
edit: 'Edit',
notfound_title: 'Page not found',
notfound_link: 'This link points to a page that has been removed or no longer exists.',
notfound_link_opens: 'Opens ${1}',
notfound: "The page you're looking for doesn't exist.",
notfound_adaptive_title: 'Page unavailable',
notfound_adaptive: 'This page may exist, but you may need to log in to access it.',
@@ -78,6 +78,7 @@ export const es: TranslationLanguage = {
edit: 'Editar',
notfound_title: 'Página no encontrada',
notfound_link: 'Este enlace apunta a una página que ha sido eliminada o ya no existe.',
notfound_link_opens: 'Abre ${1}',
notfound: 'La página que buscas no existe.',
notfound_adaptive_title: 'Página no disponible',
notfound_adaptive:
@@ -76,6 +76,7 @@ export const et: TranslationLanguage = {
edit: 'Muuda',
notfound_title: 'Lehte ei leitud',
notfound_link: 'See link viitab lehele, mis on eemaldatud või mida enam ei eksisteeri.',
notfound_link_opens: 'Avab: ${1}',
notfound: 'Lehte, mida otsite, ei eksisteeri.',
notfound_adaptive_title: 'Leht pole saadaval',
notfound_adaptive: 'See leht võib olemas olla, kuid juurdepääsuks võib olla vaja sisse logida.',
@@ -77,6 +77,7 @@ export const fi: TranslationLanguage = {
edit: 'Muokkaa',
notfound_title: 'Sivua ei löytynyt',
notfound_link: 'Tämä linkki osoittaa sivulle, joka on poistettu tai jota ei enää ole.',
notfound_link_opens: 'Avaa: ${1}',
notfound: 'Etsimääsi sivua ei ole olemassa.',
notfound_adaptive_title: 'Sivu ei ole saatavilla',
notfound_adaptive:
@@ -77,6 +77,7 @@ export const fr: TranslationLanguage = {
edit: 'Modifier',
notfound_title: 'Page introuvable',
notfound_link: "Ce lien pointe vers une page qui a été supprimée ou n'existe plus.",
notfound_link_opens: 'Ouvre ${1}',
notfound: 'La page que vous cherchez n’existe pas.',
notfound_adaptive_title: 'Page inaccessible',
notfound_adaptive:
@@ -76,6 +76,7 @@ export const he: TranslationLanguage = {
edit: 'עריכה',
notfound_title: 'הדף לא נמצא',
notfound_link: 'קישור זה מפנה לדף שהוסר או אינו קיים עוד.',
notfound_link_opens: 'פותח: ${1}',
notfound: 'הדף שחיפשת אינו קיים.',
notfound_adaptive_title: 'הדף אינו זמין',
notfound_adaptive: 'ייתכן שהדף קיים, אך ייתכן שעליך להתחבר כדי לגשת אליו.',
@@ -76,6 +76,7 @@ export const hi: TranslationLanguage = {
edit: 'संपादित करें',
notfound_title: 'पृष्ठ नहीं मिला',
notfound_link: 'यह लिंक ऐसे पृष्ठ पर जाता है जिसे हटा दिया गया है या जो अब मौजूद नहीं है।',
notfound_link_opens: '${1} खोलता है',
notfound: 'आप जिस पृष्ठ को खोज रहे हैं वह मौजूद नहीं है।',
notfound_adaptive_title: 'पृष्ठ उपलब्ध नहीं',
notfound_adaptive: 'यह पृष्ठ मौजूद हो सकता है, लेकिन इसे देखने के लिए आपको लॉग इन करना पड़ सकता है।',
@@ -76,6 +76,7 @@ export const hr: TranslationLanguage = {
edit: 'Uredi',
notfound_title: 'Stranica nije pronađena',
notfound_link: 'Ova poveznica vodi na stranicu koja je uklonjena ili više ne postoji.',
notfound_link_opens: 'Otvara: ${1}',
notfound: 'Stranica koju tražite ne postoji.',
notfound_adaptive_title: 'Stranica nije dostupna',
notfound_adaptive:
@@ -77,6 +77,7 @@ export const hu: TranslationLanguage = {
edit: 'Szerkesztés',
notfound_title: 'Az oldal nem található',
notfound_link: 'Ez a hivatkozás egy eltávolított vagy már nem létező oldalra mutat.',
notfound_link_opens: 'Megnyitja: ${1}',
notfound: 'A keresett oldal nem létezik.',
notfound_adaptive_title: 'Az oldal nem érhető el',
notfound_adaptive: 'Ez az oldal létezhet, de a hozzáféréshez lehet, hogy be kell jelentkeznie.',
@@ -76,6 +76,7 @@ export const id: TranslationLanguage = {
edit: 'Edit',
notfound_title: 'Halaman tidak ditemukan',
notfound_link: 'Tautan ini mengarah ke halaman yang telah dihapus atau sudah tidak ada.',
notfound_link_opens: 'Membuka ${1}',
notfound: 'Halaman yang Anda cari tidak ada.',
notfound_adaptive_title: 'Halaman tidak tersedia',
notfound_adaptive:
@@ -78,6 +78,7 @@ export const it: TranslationLanguage = {
edit: 'Modifica',
notfound_title: 'Pagina non trovata',
notfound_link: 'Questo link punta a una pagina che è stata rimossa o non esiste più.',
notfound_link_opens: 'Apre ${1}',
notfound: 'La pagina che cerchi non esiste.',
notfound_adaptive_title: 'Pagina non disponibile',
notfound_adaptive:
@@ -77,6 +77,7 @@ export const ja: TranslationLanguage = {
edit: '編集',
notfound_title: 'ページが見つかりません',
notfound_link: 'このリンクは、削除されたか、もはや存在しないページを指しています。',
notfound_link_opens: '${1} を開きます',
notfound: 'お探しのページは存在しません。',
notfound_adaptive_title: 'ページにアクセスできません',
notfound_adaptive:
@@ -77,6 +77,7 @@ export const ko: TranslationLanguage = {
edit: '수정',
notfound_title: '페이지를 찾을 수 없음',
notfound_link: '이 링크는 삭제되었거나 더 이상 존재하지 않는 페이지를 가리킵니다.',
notfound_link_opens: '${1}(으)로 이동',
notfound: '찾으시는 페이지가 존재하지 않습니다.',
notfound_adaptive_title: '페이지에 접근할 수 없음',
notfound_adaptive: '이 페이지는 존재할 수 있지만, 접근하려면 로그인해야 할 수 있습니다.',
@@ -76,6 +76,7 @@ export const lt: TranslationLanguage = {
edit: 'Redaguoti',
notfound_title: 'Puslapis nerastas',
notfound_link: 'Ši nuoroda veda į puslapį, kuris buvo pašalintas arba nebeegzistuoja.',
notfound_link_opens: 'Atidaro: ${1}',
notfound: 'Puslapis, kurio ieškote, neegzistuoja.',
notfound_adaptive_title: 'Puslapis nepasiekiamas',
notfound_adaptive:
@@ -76,6 +76,7 @@ export const lv: TranslationLanguage = {
edit: 'Rediģēt',
notfound_title: 'Lapa nav atrasta',
notfound_link: 'Šī saite norāda uz lapu, kas ir noņemta vai vairs nepastāv.',
notfound_link_opens: 'Atver: ${1}',
notfound: 'Meklētā lapa nepastāv.',
notfound_adaptive_title: 'Lapa nav pieejama',
notfound_adaptive: 'Šī lapa var pastāvēt, bet, iespējams, jums jāpierakstās, lai tai piekļūtu.',
@@ -76,6 +76,7 @@ export const ms: TranslationLanguage = {
edit: 'Edit',
notfound_title: 'Halaman tidak ditemui',
notfound_link: 'Pautan ini menghala ke halaman yang telah dialih keluar atau tidak lagi wujud.',
notfound_link_opens: 'Membuka ${1}',
notfound: 'Halaman yang anda cari tidak wujud.',
notfound_adaptive_title: 'Halaman tidak tersedia',
notfound_adaptive:
@@ -78,6 +78,7 @@ export const nl: TranslationLanguage = {
edit: 'Bewerken',
notfound_title: 'Pagina niet gevonden',
notfound_link: 'Deze link verwijst naar een pagina die is verwijderd of niet meer bestaat.',
notfound_link_opens: 'Opent ${1}',
notfound: 'De pagina die je zoekt, bestaat niet.',
notfound_adaptive_title: 'Pagina niet beschikbaar',
notfound_adaptive:
@@ -78,6 +78,7 @@ export const no: TranslationLanguage = {
notfound_title: 'Siden ble ikke funnet',
notfound_link:
'Denne lenken peker til en side som har blitt fjernet eller ikke lenger eksisterer.',
notfound_link_opens: 'Åpner ${1}',
notfound: 'Siden du leter etter eksisterer ikke.',
notfound_adaptive_title: 'Siden er ikke tilgjengelig',
notfound_adaptive: 'Denne siden kan finnes, men du må kanskje logge inn for å få tilgang.',
@@ -76,6 +76,7 @@ export const pl: TranslationLanguage = {
edit: 'Edytuj',
notfound_title: 'Nie znaleziono strony',
notfound_link: 'Ten link prowadzi do strony, która została usunięta lub już nie istnieje.',
notfound_link_opens: 'Otwiera: ${1}',
notfound: 'Strona, której szukasz, nie istnieje.',
notfound_adaptive_title: 'Strona niedostępna',
notfound_adaptive: 'Ta strona może istnieć, ale dostęp do niej może wymagać zalogowania.',
@@ -78,6 +78,7 @@ export const pt_br: TranslationLanguage = {
edit: 'Editar',
notfound_title: 'Página não encontrada',
notfound_link: 'Este link aponta para uma página que foi removida ou não existe mais.',
notfound_link_opens: 'Abre ${1}',
notfound: 'A página que você está procurando não existe.',
notfound_adaptive_title: 'Página indisponível',
notfound_adaptive:
@@ -77,6 +77,7 @@ export const pt: TranslationLanguage = {
edit: 'Editar',
notfound_title: 'Página não encontrada',
notfound_link: 'Esta ligação aponta para uma página que foi removida ou já não existe.',
notfound_link_opens: 'Abre ${1}',
notfound: 'A página que procura não existe.',
notfound_adaptive_title: 'Página indisponível',
notfound_adaptive:
@@ -77,6 +77,7 @@ export const ro: TranslationLanguage = {
edit: 'Editează',
notfound_title: 'Pagina nu a fost găsită',
notfound_link: 'Acest link indică o pagină care a fost eliminată sau nu mai există.',
notfound_link_opens: 'Deschide ${1}',
notfound: 'Pagina pe care o cauți nu există.',
notfound_adaptive_title: 'Pagina nu este disponibilă',
notfound_adaptive:
@@ -78,6 +78,7 @@ export const ru: TranslationLanguage = {
edit: 'Редактировать',
notfound_title: 'Страница не найдена',
notfound_link: 'Эта ссылка ведёт на страницу, которая была удалена или больше не существует.',
notfound_link_opens: 'Откроется: ${1}',
notfound: 'Страница, которую вы ищете, не существует.',
notfound_adaptive_title: 'Страница недоступна',
notfound_adaptive:
@@ -77,6 +77,7 @@ export const sk: TranslationLanguage = {
edit: 'Upraviť',
notfound_title: 'Stránka sa nenašla',
notfound_link: 'Tento odkaz smeruje na stránku, ktorá bola odstránená alebo už neexistuje.',
notfound_link_opens: 'Otvorí: ${1}',
notfound: 'Stránka, ktorú hľadáte, neexistuje.',
notfound_adaptive_title: 'Stránka nie je dostupná',
notfound_adaptive:
@@ -77,6 +77,7 @@ export const sl: TranslationLanguage = {
edit: 'Uredi',
notfound_title: 'Strani ni bilo mogoče najti',
notfound_link: 'Ta povezava kaže na stran, ki je bila odstranjena ali ne obstaja več.',
notfound_link_opens: 'Odpre: ${1}',
notfound: 'Stran, ki jo iščete, ne obstaja.',
notfound_adaptive_title: 'Stran ni na voljo',
notfound_adaptive: 'Ta stran morda obstaja, vendar se boste za dostop morda morali prijaviti.',
@@ -76,6 +76,7 @@ export const sv: TranslationLanguage = {
edit: 'Redigera',
notfound_title: 'Sidan hittades inte',
notfound_link: 'Den här länken pekar på en sida som har tagits bort eller inte längre finns.',
notfound_link_opens: 'Öppnar ${1}',
notfound: 'Sidan du letar efter finns inte.',
notfound_adaptive_title: 'Sidan är inte tillgänglig',
notfound_adaptive: 'Sidan kan finnas, men du kan behöva logga in för att få åtkomst.',
@@ -75,6 +75,7 @@ export const th: TranslationLanguage = {
edit: 'แก้ไข',
notfound_title: 'ไม่พบหน้า',
notfound_link: 'ลิงก์นี้ชี้ไปยังหน้าที่ถูกลบหรือไม่มีอยู่อีกต่อไป',
notfound_link_opens: 'เปิด ${1}',
notfound: 'หน้าที่คุณกำลังค้นหาไม่มีอยู่',
notfound_adaptive_title: 'หน้าไม่พร้อมใช้งาน',
notfound_adaptive: 'หน้านี้อาจมีอยู่ แต่คุณอาจต้องเข้าสู่ระบบเพื่อเข้าถึง',
@@ -76,6 +76,7 @@ export const tr: TranslationLanguage = {
edit: 'Düzenle',
notfound_title: 'Sayfa bulunamadı',
notfound_link: 'Bu bağlantı kaldırılmış veya artık mevcut olmayan bir sayfaya işaret ediyor.',
notfound_link_opens: '${1} açılır',
notfound: 'Aradığınız sayfa mevcut değil.',
notfound_adaptive_title: 'Sayfa kullanılamıyor',
notfound_adaptive: 'Bu sayfa mevcut olabilir, ancak erişmek için oturum açmanız gerekebilir.',
@@ -76,6 +76,7 @@ export const uk: TranslationLanguage = {
edit: 'Редагувати',
notfound_title: 'Сторінку не знайдено',
notfound_link: 'Це посилання веде на сторінку, яку видалено або якої більше не існує.',
notfound_link_opens: 'Відкриється: ${1}',
notfound: 'Сторінка, яку ви шукаєте, не існує.',
notfound_adaptive_title: 'Сторінка недоступна',
notfound_adaptive: 'Ця сторінка може існувати, але для доступу до неї може знадобитися вхід.',
@@ -76,6 +76,7 @@ export const vi: TranslationLanguage = {
edit: 'Chỉnh sửa',
notfound_title: 'Không tìm thấy trang',
notfound_link: 'Liên kết này trỏ đến một trang đã bị xóa hoặc không còn tồn tại.',
notfound_link_opens: 'Mở ${1}',
notfound: 'Trang bạn đang tìm kiếm không tồn tại.',
notfound_adaptive_title: 'Trang không khả dụng',
notfound_adaptive: 'Trang này có thể tồn tại, nhưng bạn có thể cần đăng nhập để truy cập.',
@@ -74,6 +74,7 @@ export const yue: TranslationLanguage = {
edit: '編輯',
notfound_title: '搵唔到頁面',
notfound_link: '呢條連結指向嘅頁面已被移除或已經唔存在。',
notfound_link_opens: '開啟 ${1}',
notfound: '你搵緊嘅頁面唔存在。',
notfound_adaptive_title: '頁面無法使用',
notfound_adaptive: '呢個頁面可能存在,但你可能需要登入先可以存取。',
@@ -74,6 +74,7 @@ export const zh_tw: TranslationLanguage = {
edit: '編輯',
notfound_title: '找不到頁面',
notfound_link: '此連結指向的頁面已被移除或不再存在。',
notfound_link_opens: '開啟 ${1}',
notfound: '您要尋找的頁面不存在。',
notfound_adaptive_title: '頁面無法使用',
notfound_adaptive: '此頁面可能存在,但您可能需要登入才能存取。',
@@ -75,6 +75,7 @@ export const zh: TranslationLanguage = {
edit: '编辑',
notfound_title: '页面未找到',
notfound_link: '此链接指向已被删除或不再存在的页面。',
notfound_link_opens: '打开 ${1}',
notfound: '您要找的页面不存在。',
notfound_adaptive_title: '页面无法访问',
notfound_adaptive: '该页面可能存在,但您可能需要登录后才能访问。',
+81 -1
View File
@@ -2,7 +2,11 @@ import { describe, expect, it } from 'bun:test';
import type { Revision, RevisionPageDocument, SiteSpace, Space } from '@gitbook/api';
import { resolveContentRef, resolveStringContentRef } from './references';
import {
resolveContentRef,
resolveContentRefFallback,
resolveStringContentRef,
} from './references';
import type { GitBookAnyContext } from '@/lib/context';
import type { GitBookDataFetcher } from '@/lib/data';
import { createLinker } from '@/lib/links';
@@ -738,3 +742,79 @@ describe('resolveContentRef for direct space links', () => {
]);
});
});
describe('resolveContentRef for application URLs into the site', () => {
const alerts = {
object: 'space',
id: 'space-alerts',
title: 'Alerts',
urls: {
app: 'https://app.gitbook.com/o/org/s/space-alerts/',
published: 'https://docs.example.com/analytics-alerts/',
},
} as unknown as Space;
const alertsSiteSpace = {
object: 'site-space',
id: 'site-space-alerts',
path: 'analytics-alerts',
space: alerts,
title: 'Alerts',
urls: { published: 'https://docs.example.com/analytics-alerts/' },
} as unknown as SiteSpace;
const context = {
linker: createLinker({ host: 'docs.example.com', spaceBasePath: '/', siteBasePath: '/' }),
space: { id: 'space-notes' },
site: { object: 'site', id: 'site-1' },
sections: null,
structure: { type: 'siteSpaces', structure: [alertsSiteSpace] },
} as unknown as GitBookAnyContext;
it('keeps the page path and anchor of an application URL into a site space', async () => {
const result = await resolveContentRef(
{
kind: 'url',
url: 'https://app.gitbook.com/o/org/s/space-alerts/alerts-by-name/amsi-bypass#rules',
},
context
);
expect(result?.href).toBe('/analytics-alerts/alerts-by-name/amsi-bypass#rules');
});
it('leaves an application URL into a space outside the site untouched', async () => {
const url = 'https://app.gitbook.com/o/org/s/space-elsewhere/alerts-by-name/amsi-bypass';
const result = await resolveContentRef({ kind: 'url', url }, context);
expect(result?.href).toBe(url);
});
it('leaves an application URL into a change request untouched', async () => {
const url = 'https://app.gitbook.com/o/org/s/space-alerts/~/changes/1/alerts-by-name';
const result = await resolveContentRef({ kind: 'url', url }, context);
expect(result?.href).toBe(url);
});
it('falls back to the site space, not the application, for a page ref that no longer resolves', () => {
const fallback = resolveContentRefFallback(
{ kind: 'page', space: 'space-alerts', page: 'page-deleted' },
context
);
expect(fallback?.href).toBe('/analytics-alerts');
expect(fallback?.text).toBe('Alerts');
});
it('keeps the application fallback for a page ref into a space outside the site', () => {
const fallback = resolveContentRefFallback(
{ kind: 'page', space: 'space-elsewhere', page: 'page-deleted' },
context
);
expect(fallback?.href).toBe('https://app.gitbook.com/s/space-elsewhere');
expect(fallback?.text).toBe('space');
});
});
+59 -5
View File
@@ -40,8 +40,16 @@ import {
getRevisionReusableContent,
ignoreDataThrownError,
} from '@/lib/data';
import { GITBOOK_APP_URL } from '@/lib/env';
import { type GitBookLinker, createLinker, linkerWithAbsoluteURLs } from '@/lib/links';
/**
* Path of an application URL to a space's content: `/o/:org/s/:space/:pagePath`, optionally under
* `/sites/:site`. A path under `~/` (a change request or revision) is not published content.
*/
const APP_SPACE_CONTENT_PATH =
/^(?:\/o\/[^/]+)?(?:\/sites\/[^/]+)?\/s\/([^/]+)(?:\/(?!~)(.*?))?\/?$/;
export interface ResolvedContentRef {
/** Text to render in the content ref */
text: string;
@@ -143,9 +151,10 @@ export async function resolveContentRef(
switch (contentRef.kind) {
case 'url': {
const href = resolveAppURLInSite(contentRef.url, context) ?? contentRef.url;
return {
href: contentRef.url,
text: contentRef.url,
href,
text: href,
active: false,
};
}
@@ -419,11 +428,16 @@ export function isContentRefInDifferentSpace<Ref extends ContentRef>(
* Called if we can't resolve the content ref to have a potential fallback to display to the
* user instead of not found.
*/
export function resolveContentRefFallback(contentRef: ContentRef): ResolvedContentRef | null {
export function resolveContentRefFallback(
contentRef: ContentRef,
context: GitBookAnyContext | undefined
): ResolvedContentRef | null {
if ('space' in contentRef && contentRef.space) {
const linker = context ? getLinkerForSpaceInSite(context, contentRef.space) : null;
const inSite = context ? getBestTargetSpaceFromSite(context, contentRef.space) : undefined;
return {
href: getGitBookAppHref(`/s/${contentRef.space}`),
text: 'space',
href: linker?.toPathInSpace('') ?? getGitBookAppHref(`/s/${contentRef.space}`),
text: linker && inSite ? getSpaceRefText(inSite, context?.locale) : 'space',
active: false,
};
}
@@ -460,6 +474,46 @@ async function getBestTargetSpace(
return fetchedSpace ? { space: fetchedSpace, siteSpace: null, siteSection: null } : undefined;
}
/**
* Map an application URL into a space of the current site to the same page path on the site, so
* a link that was never resolved to a page doesn't send visitors to the app. The page isn't looked
* up: one that doesn't exist lands on the site's not-found page.
*/
function resolveAppURLInSite(url: string, context: GitBookAnyContext): string | null {
if (!URL.canParse(url)) {
return null;
}
const parsed = new URL(url);
if (parsed.origin !== new URL(GITBOOK_APP_URL).origin) {
return null;
}
const match = parsed.pathname.match(APP_SPACE_CONTENT_PATH);
const linker = match?.[1] ? getLinkerForSpaceInSite(context, match[1]) : null;
if (!linker) {
return null;
}
return linker.toPathForPagePath({
path: match?.[2] ?? '',
anchor: parsed.hash.slice(1) || undefined,
});
}
/**
* Linker for a space that is part of the current site, or null when it isn't.
*/
function getLinkerForSpaceInSite(
context: GitBookAnyContext,
spaceId: string
): GitBookLinker | null {
const target = getBestTargetSpaceFromSite(context, spaceId);
if (!target?.siteSpace || !('site' in context)) {
return null;
}
return context.linker.withOtherSiteSpace({
spaceBasePath: getFallbackSiteSpacePath(context, target.siteSpace),
});
}
/**
* Find the best target space for a content ref, from the current site.
*/