mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-26 12:18:01 +00:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b8ba267c4c | |||
| dd28fb0114 | |||
| 8dc137e994 | |||
| 9078855d44 | |||
| f2fddad24c |
@@ -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: '该页面可能存在,但您可能需要登录后才能访问。',
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user