diff --git a/src/components/DocumentView/Inline.tsx b/src/components/DocumentView/Inline.tsx index 76d7dc38a..abb1812d2 100644 --- a/src/components/DocumentView/Inline.tsx +++ b/src/components/DocumentView/Inline.tsx @@ -14,7 +14,7 @@ import { Annotation } from './Annotation/Annotation'; import { DocumentContextProps } from './DocumentView'; import { Emoji } from './Emoji'; import { InlineImage } from './InlineImage'; -import { Link } from './Link'; +import { InlineLink } from './InlineLink'; import { InlineMath } from './Math'; import { Mention } from './Mention'; @@ -45,7 +45,7 @@ export function Inline< switch (inline.type) { case 'link': - return ; + return ; case 'inline-math': return ; case 'annotation': diff --git a/src/components/DocumentView/Link.tsx b/src/components/DocumentView/InlineLink.tsx similarity index 83% rename from src/components/DocumentView/Link.tsx rename to src/components/DocumentView/InlineLink.tsx index 0e8e165bc..68e4fe69e 100644 --- a/src/components/DocumentView/Link.tsx +++ b/src/components/DocumentView/InlineLink.tsx @@ -1,10 +1,10 @@ import { DocumentInlineLink } from '@gitbook/api'; -import NextLink from 'next/link'; import { InlineProps } from './Inline'; import { Inlines } from './Inlines'; +import { Link } from '../primitives'; -export async function Link(props: InlineProps) { +export async function InlineLink(props: InlineProps) { const { inline, document, context } = props; const resolved = await context.resolveContentRef(inline.data.ref); @@ -18,11 +18,11 @@ export async function Link(props: InlineProps) { } return ( - - + ); } diff --git a/src/components/DocumentView/Mention.tsx b/src/components/DocumentView/Mention.tsx index 1b89530d4..90f3277a3 100644 --- a/src/components/DocumentView/Mention.tsx +++ b/src/components/DocumentView/Mention.tsx @@ -1,6 +1,6 @@ import { DocumentInlineMention } from '@gitbook/api'; -import { Link } from '@/components/primitives'; +import { StyledLink } from '@/components/primitives'; import { InlineProps } from './Inline'; @@ -13,5 +13,5 @@ export async function Mention(props: InlineProps) { return null; } - return {resolved.text}; + return {resolved.text}; } diff --git a/src/components/DocumentView/Table/RecordColumnValue.tsx b/src/components/DocumentView/Table/RecordColumnValue.tsx index 9d18f8048..dc21e410c 100644 --- a/src/components/DocumentView/Table/RecordColumnValue.tsx +++ b/src/components/DocumentView/Table/RecordColumnValue.tsx @@ -2,7 +2,8 @@ import IconStar from '@geist-ui/icons/star'; import { ContentRef, DocumentBlockTable } from '@gitbook/api'; import assertNever from 'assert-never'; -import { Checkbox, Emoji, Link } from '@/components/primitives'; +import { Checkbox, Emoji } from '@/components/primitives'; +import { StyledLink } from '@/components/primitives'; import { getNodeFragmentByName } from '@/lib/document'; import { tcls } from '@/lib/tailwind'; import { filterOutNullable } from '@/lib/typescript'; @@ -125,9 +126,9 @@ export async function RecordColumnValue( return ( {files.filter(filterOutNullable).map((file, index) => ( - + {file.text} - + ))} ); @@ -138,7 +139,9 @@ export async function RecordColumnValue( {resolved && resolved.emoji ? ( ) : null} - {resolved ? {resolved.text} : null} + {resolved ? ( + {resolved.text} + ) : null} ); } @@ -155,9 +158,9 @@ export async function RecordColumnValue( return ( {resolved.filter(filterOutNullable).map((file, index) => ( - + {file.text} - + ))} ); diff --git a/src/components/Footer/FooterLinksGroup.tsx b/src/components/Footer/FooterLinksGroup.tsx index eb7e3a0c0..4eb64e0e8 100644 --- a/src/components/Footer/FooterLinksGroup.tsx +++ b/src/components/Footer/FooterLinksGroup.tsx @@ -1,9 +1,10 @@ import { CustomizationContentLink, CustomizationFooterGroup } from '@gitbook/api'; -import Link from 'next/link'; import { ContentRefContext, resolveContentRef } from '@/lib/references'; import { tcls } from '@/lib/tailwind'; +import { Link } from '../primitives'; + export function FooterLinksGroup(props: { group: CustomizationFooterGroup; context: ContentRefContext; diff --git a/src/components/Header/Dropdown.tsx b/src/components/Header/Dropdown.tsx index 454a660a4..40a8a59b3 100644 --- a/src/components/Header/Dropdown.tsx +++ b/src/components/Header/Dropdown.tsx @@ -1,9 +1,10 @@ import IconChevronDown from '@geist-ui/icons/chevronDown'; -import Link from 'next/link'; import { DetailedHTMLProps, HTMLAttributes, useId } from 'react'; import { tcls } from '@/lib/tailwind'; +import { Link } from '../primitives'; + export type DropdownButtonProps = Omit< Partial, E>>, 'ref' @@ -111,15 +112,10 @@ export function DropdownMenuItem(props: { }) { const { children, active = false, href } = props; - // Use a real anchor tag for external links, and a Next.js Link for internal links. - // The main reason is to prevent a bug where next.js Link will result in the page not changing when navigating between spaces in a collection - const isExternal = href.startsWith('http'); - const A = isExternal ? 'a' : Link; - return ( - {children} - + ); } diff --git a/src/components/Header/HeaderLink.tsx b/src/components/Header/HeaderLink.tsx index 0c6705991..3c05b6ea9 100644 --- a/src/components/Header/HeaderLink.tsx +++ b/src/components/Header/HeaderLink.tsx @@ -4,7 +4,6 @@ import { CustomizationSettings, CustomizationHeaderPreset, } from '@gitbook/api'; -import Link from 'next/link'; import { ContentRefContext, resolveContentRef } from '@/lib/references'; import { tcls } from '@/lib/tailwind'; @@ -16,6 +15,7 @@ import { DropdownMenu, DropdownMenuItem, } from './Dropdown'; +import { Link } from '../primitives'; export async function HeaderLink(props: { context: ContentRefContext; diff --git a/src/components/Header/HeaderLogo.tsx b/src/components/Header/HeaderLogo.tsx index 2f6edc0f5..afae0199e 100644 --- a/src/components/Header/HeaderLogo.tsx +++ b/src/components/Header/HeaderLogo.tsx @@ -1,10 +1,11 @@ import { Collection, CustomizationHeaderPreset, CustomizationSettings, Space } from '@gitbook/api'; -import Link from 'next/link'; import { HeaderMobileMenu } from '@/components/Header/HeaderMobileMenu'; import { Image } from '@/components/utils'; import { absoluteHref } from '@/lib/links'; -import { ClassValue, tcls } from '@/lib/tailwind'; +import { tcls } from '@/lib/tailwind'; + +import { Link } from '../primitives'; interface HeaderLogoProps { collection: Collection | null; diff --git a/src/components/PageBody/PageFooterNavigation.tsx b/src/components/PageBody/PageFooterNavigation.tsx index c9f8236c5..06baeaca0 100644 --- a/src/components/PageBody/PageFooterNavigation.tsx +++ b/src/components/PageBody/PageFooterNavigation.tsx @@ -1,7 +1,6 @@ import ChevronLeft from '@geist-ui/icons/chevronLeft'; import ChevronRight from '@geist-ui/icons/chevronRight'; import { CustomizationSettings, Revision, RevisionPageDocument, Space } from '@gitbook/api'; -import Link from 'next/link'; import React from 'react'; import { t, getSpaceLanguage } from '@/intl/server'; @@ -9,6 +8,8 @@ import { pageHref } from '@/lib/links'; import { resolvePrevNextPages } from '@/lib/pages'; import { tcls } from '@/lib/tailwind'; +import { Link } from '../primitives'; + /** * Show cards to go to previous/next pages at the bottom. */ diff --git a/src/components/Search/SearchAskAnswer.tsx b/src/components/Search/SearchAskAnswer.tsx index 4253692d2..c4f21ebe0 100644 --- a/src/components/Search/SearchAskAnswer.tsx +++ b/src/components/Search/SearchAskAnswer.tsx @@ -1,6 +1,5 @@ import IconBox from '@geist-ui/icons/box'; import IconSearch from '@geist-ui/icons/search'; -import Link from 'next/link'; import React from 'react'; import { atom, useRecoilState } from 'recoil'; @@ -12,6 +11,7 @@ import { tcls } from '@/lib/tailwind'; import { AskAnswerResult, streamAskQuestion } from './server-actions'; import { useSearch, useSearchLink } from './useSearch'; +import { Link } from '../primitives'; /** * Store the state of the answer in a global state so that it can be diff --git a/src/components/Search/SearchPageResultItem.tsx b/src/components/Search/SearchPageResultItem.tsx index f26bd1d8c..df110b575 100644 --- a/src/components/Search/SearchPageResultItem.tsx +++ b/src/components/Search/SearchPageResultItem.tsx @@ -1,10 +1,10 @@ -import Link from 'next/link'; import React from 'react'; import { tcls } from '@/lib/tailwind'; import { HighlightQuery } from './HighlightQuery'; import type { ComputedPageResult } from './server-actions'; +import { Link } from '../primitives'; export const SearchPageResultItem = React.forwardRef(function SearchPageResultItem( props: { diff --git a/src/components/Search/SearchQuestionResultItem.tsx b/src/components/Search/SearchQuestionResultItem.tsx index 920ef2938..5a9c64116 100644 --- a/src/components/Search/SearchQuestionResultItem.tsx +++ b/src/components/Search/SearchQuestionResultItem.tsx @@ -1,11 +1,11 @@ import IconSearch from '@geist-ui/icons/search'; -import Link from 'next/link'; import React from 'react'; import { t, useLanguage } from '@/intl/client'; import { tcls } from '@/lib/tailwind'; import { useSearchLink } from './useSearch'; +import { Link } from '../primitives'; export const SearchQuestionResultItem = React.forwardRef(function SearchQuestionResultItem( props: { diff --git a/src/components/Search/SearchSectionResultItem.tsx b/src/components/Search/SearchSectionResultItem.tsx index 2e4fbbfc8..dfc3efbe0 100644 --- a/src/components/Search/SearchSectionResultItem.tsx +++ b/src/components/Search/SearchSectionResultItem.tsx @@ -1,10 +1,10 @@ -import Link from 'next/link'; import React from 'react'; import { tcls } from '@/lib/tailwind'; import { HighlightQuery } from './HighlightQuery'; import type { ComputedSectionResult } from './server-actions'; +import { Link } from '../primitives'; export const SearchSectionResultItem = React.forwardRef(function SearchSectionResultItem( props: { diff --git a/src/components/Search/useSearch.ts b/src/components/Search/useSearch.ts index ad225253f..6301ec4f3 100644 --- a/src/components/Search/useSearch.ts +++ b/src/components/Search/useSearch.ts @@ -1,7 +1,8 @@ -import { LinkProps } from 'next/link'; import { useQueryStates, parseAsBoolean, parseAsString, UseQueryStatesOptions } from 'nuqs'; import React from 'react'; +import { LinkProps } from '../primitives'; + export interface SearchState { query: string; ask: boolean; diff --git a/src/components/TableOfContents/PageLinkItem.tsx b/src/components/TableOfContents/PageLinkItem.tsx index 0b639f046..db3577979 100644 --- a/src/components/TableOfContents/PageLinkItem.tsx +++ b/src/components/TableOfContents/PageLinkItem.tsx @@ -1,8 +1,7 @@ import IconExternal from '@geist-ui/icons/externalLink'; import { RevisionPageLink } from '@gitbook/api'; -import Link from 'next/link'; -import { Emoji } from '@/components/primitives'; +import { Emoji, Link } from '@/components/primitives'; import { ContentRefContext, resolveContentRef } from '@/lib/references'; import { tcls } from '@/lib/tailwind'; diff --git a/src/components/TableOfContents/ToggleableLinkItem.tsx b/src/components/TableOfContents/ToggleableLinkItem.tsx index 26f743217..d8483aedb 100644 --- a/src/components/TableOfContents/ToggleableLinkItem.tsx +++ b/src/components/TableOfContents/ToggleableLinkItem.tsx @@ -2,12 +2,13 @@ import IconChevronRight from '@geist-ui/icons/chevronRight'; import { motion, stagger, useAnimate } from 'framer-motion'; -import Link from 'next/link'; import { useSelectedLayoutSegment } from 'next/navigation'; import React from 'react'; import { tcls } from '@/lib/tailwind'; +import { Link } from '../primitives'; + const show = { opacity: 1, height: 'auto', diff --git a/src/components/primitives/Button.tsx b/src/components/primitives/Button.tsx index bb529283b..1406068ac 100644 --- a/src/components/primitives/Button.tsx +++ b/src/components/primitives/Button.tsx @@ -1,9 +1,9 @@ 'use client'; -import Link from 'next/link'; - import { tcls, ClassValue } from '@/lib/tailwind'; +import { Link } from './Link'; + type ButtonProps = { href?: string; onClick?: () => void; diff --git a/src/components/primitives/Card.tsx b/src/components/primitives/Card.tsx index 5f5c6fac9..cc1700907 100644 --- a/src/components/primitives/Card.tsx +++ b/src/components/primitives/Card.tsx @@ -1,8 +1,9 @@ import ChevronRight from '@geist-ui/icons/chevronRight'; -import Link from 'next/link'; import { ClassValue, tcls } from '@/lib/tailwind'; +import { Link } from './Link'; + export async function Card(props: { href: string; leadingIcon?: React.ReactNode; diff --git a/src/components/primitives/Link.tsx b/src/components/primitives/Link.tsx index 55e12225d..8a556ed49 100644 --- a/src/components/primitives/Link.tsx +++ b/src/components/primitives/Link.tsx @@ -1,24 +1,31 @@ -import NextLink, { LinkProps } from 'next/link'; +import NextLink, { LinkProps as NextLinkProps } from 'next/link'; -import { tcls } from '@/lib/tailwind'; +// Props from Next, which includes NextLinkProps and all the things anchor elements support. +type BaseLinkProps = Omit, keyof NextLinkProps> & + NextLinkProps & { + children?: React.ReactNode; + } & React.RefAttributes; + +// Enforce href is passed as a string (not a URL). +export type LinkProps = Omit & { href: string }; /** - * Styled version of Next.js Link component. + * Low-level Link component that handles navigation to external urls. + * It does not contain any styling. */ -export function Link(props: LinkProps & { children: React.ReactNode }) { - return ( - - {props.children} - - ); +export function Link(props: LinkProps) { + const { href, prefetch, children, ...domProps } = props; + + // Use a real anchor tag for external links,s and a Next.js Link for internal links. + // If we use a NextLink for external links, Nextjs won't rerender the top-level layouts. + const isExternal = URL.canParse(props.href); + if (isExternal) { + return ( + + {children} + + ); + } + + return {children}; } diff --git a/src/components/primitives/StyledLink.tsx b/src/components/primitives/StyledLink.tsx new file mode 100644 index 000000000..7d5d1fed8 --- /dev/null +++ b/src/components/primitives/StyledLink.tsx @@ -0,0 +1,24 @@ +import { tcls } from '@/lib/tailwind'; + +import { Link, LinkProps } from '../primitives/Link'; + +/** + * Styled version of Link component. + */ +export function StyledLink(props: LinkProps) { + return ( + + {props.children} + + ); +} diff --git a/src/components/primitives/index.ts b/src/components/primitives/index.ts index 4c0e3b013..58927084a 100644 --- a/src/components/primitives/index.ts +++ b/src/components/primitives/index.ts @@ -4,6 +4,7 @@ export * from './Loading'; export * from './Card'; export * from './Skeleton'; export * from './Link'; +export * from './StyledLink'; export * from './DateRelative'; export * from './Emoji'; export * from './LoadingPane';