Skip to content

Fix hydration error on V2 #3272

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/cold-buckets-divide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"gitbook": patch
---

fix nested a tag causing hydration error
31 changes: 16 additions & 15 deletions packages/gitbook/src/components/DocumentView/Table/RecordCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
SiteInsightsLinkPosition,
} from '@gitbook/api';

import { Link } from '@/components/primitives';
import { LinkBox, LinkOverlay } from '@/components/primitives';
import { Image } from '@/components/utils';
import { resolveContentRef } from '@/lib/references';
import { type ClassValue, tcls } from '@/lib/tailwind';
Expand Down Expand Up @@ -44,7 +44,6 @@ export async function RecordCard(
<div
className={tcls(
'grid-area-1-1',
'z-0',
'relative',
'grid',
'bg-tint-base',
Expand Down Expand Up @@ -151,7 +150,6 @@ export async function RecordCard(
'rounded-md',
'straight-corners:rounded-none',
'dark:shadow-transparent',
'z-0',

'before:pointer-events-none',
'before:grid-area-1-1',
Expand All @@ -167,19 +165,22 @@ export async function RecordCard(

if (target && targetRef) {
return (
<Link
href={target.href}
className={tcls(style, 'hover:before:ring-tint-12/5')}
insights={{
type: 'link_click',
link: {
target: targetRef,
position: SiteInsightsLinkPosition.Content,
},
}}
>
// We don't use `Link` directly here because we could end up in a situation where
// a link is rendered inside a link, which is not allowed in HTML.
// It causes an hydration error in React.
<LinkBox href={target.href} className={tcls(style, 'hover:before:ring-tint-12/5')}>
<LinkOverlay
href={target.href}
insights={{
type: 'link_click',
link: {
target: targetRef,
position: SiteInsightsLinkPosition.Content,
},
}}
/>
{body}
</Link>
</LinkBox>
);
}

Expand Down
7 changes: 7 additions & 0 deletions packages/gitbook/src/components/RootLayout/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,13 @@
width: 100%;
}
}

.elevate-link {
& a[href]:not(.link-overlay) {
position: relative;
z-index: 20;
}
}
}

html {
Expand Down
41 changes: 41 additions & 0 deletions packages/gitbook/src/components/primitives/Link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import NextLink, { type LinkProps as NextLinkProps } from 'next/link';
import React from 'react';

import { tcls } from '@/lib/tailwind';
import { type TrackEventInput, useTrackEvent } from '../Insights';

// Props from Next, which includes NextLinkProps and all the things anchor elements support.
Expand Down Expand Up @@ -75,6 +76,46 @@ export const Link = React.forwardRef(function Link(
);
});

/**
* A box used to contain a link overlay.
* It is used to create a clickable area that can contain other elements.
*/
export const LinkBox = React.forwardRef(function LinkBox(
props: React.BaseHTMLAttributes<HTMLDivElement>,
ref: React.Ref<HTMLDivElement>
) {
const { children, className, ...domProps } = props;
return (
<div ref={ref} {...domProps} className={tcls('elevate-link relative', className)}>
{children}
</div>
);
});

/**
* A link overlay that can be used to create a clickable area on top of other elements.
* It is used to create a link that covers the entire area of the element without encapsulating it in a link tag.
* This is useful to avoid nesting links inside links.
*/
export const LinkOverlay = React.forwardRef(function LinkOverlay(
props: LinkProps,
ref: React.Ref<HTMLAnchorElement>
) {
const { children, className, ...domProps } = props;
return (
<Link
ref={ref}
{...domProps}
className={tcls(
'link-overlay static before:absolute before:top-0 before:left-0 before:z-10 before:h-full before:w-full',
className
)}
>
{children}
</Link>
);
});

/**
* Check if a link is external, compared to an origin.
*/
Expand Down