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';