Skip to content
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/nine-gorillas-turn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'gitbook': minor
---

Track clicks on links (header, footer, content) for site insights.
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ export async function BlockContentRef(props: BlockProps<DocumentBlockContentRef>
href={resolved.href}
title={resolved.text}
style={style}
insights={{
target: block.data.ref,
position: 'content',
}}
/>
);
}
Expand Down
9 changes: 7 additions & 2 deletions packages/gitbook/src/components/DocumentView/File.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { tcls } from '@/lib/tailwind';
import { BlockProps } from './Block';
import { Caption } from './Caption';
import { FileIcon } from './FileIcon';
import { Link } from '../primitives';

export async function File(props: BlockProps<DocumentBlockFile>) {
const { block, context } = props;
Expand All @@ -21,9 +22,13 @@ export async function File(props: BlockProps<DocumentBlockFile>) {

return (
<Caption {...props} wrapperStyle={[]}>
<a
<Link
href={file.downloadURL}
download={file.name}
insights={{
target: block.data.ref,
position: 'content',
}}
className={tcls(
'group/file',
'flex',
Expand Down Expand Up @@ -77,7 +82,7 @@ export async function File(props: BlockProps<DocumentBlockFile>) {
{contentType}
</div>
</div>
</a>
</Link>
</Caption>
);
}
Expand Down
6 changes: 5 additions & 1 deletion packages/gitbook/src/components/DocumentView/InlineLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ export async function InlineLink(props: InlineProps<DocumentInlineLink>) {
return (
<Link
href={resolved.href}
className="underline underline-offset-2 text-primary hover:text-primary-700 transition-colors "
className="underline underline-offset-2 text-primary hover:text-primary-700 transition-colors"
insights={{
target: inline.data.ref,
position: 'content',
}}
>
<Inlines
context={context}
Expand Down
12 changes: 11 additions & 1 deletion packages/gitbook/src/components/DocumentView/Mention.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,15 @@ export async function Mention(props: InlineProps<DocumentInlineMention>) {
return null;
}

return <StyledLink href={resolved.href}>{resolved.text}</StyledLink>;
return (
<StyledLink
href={resolved.href}
insights={{
target: inline.data.ref,
position: 'content',
}}
>
{resolved.text}
</StyledLink>
);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ContentRef, DocumentTableViewCards } from '@gitbook/api';
import React from 'react';

import { Link } from '@/components/primitives';
import { Image } from '@/components/utils';
import { ClassValue, tcls } from '@/lib/tailwind';

Expand Down Expand Up @@ -153,17 +154,21 @@ export async function RecordCard(
'before:dark:ring-light/2',
] as ClassValue;

if (target) {
if (target && targetRef) {
return (
<a
<Link
href={target.href}
className={tcls(style, [
'hover:before:ring-dark/4',
'dark:hover:before:ring-light/4',
])}
insights={{
target: targetRef,
position: 'content',
}}
>
{body}
</a>
</Link>
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ContentRef, DocumentBlockTable } from '@gitbook/api';
import { ContentRef, ContentRefUser, DocumentBlockTable } from '@gitbook/api';
import { Icon } from '@gitbook/icons';
import assertNever from 'assert-never';

Expand Down Expand Up @@ -148,6 +148,17 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
href={ref.href}
target="_blank"
style={['flex', 'flex-row', 'items-center', 'gap-2']}
insights={
ref.file
? {
target: {
kind: 'file',
file: ref.file.id,
},
position: 'content',
}
: undefined
}
>
{contentType === 'image' ? (
<Image
Expand Down Expand Up @@ -178,8 +189,9 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
</Tag>
);
case 'content-ref': {
const resolved = value
? await context.resolveContentRef(value as ContentRef, {
const contentRef = value ? (value as ContentRef) : null;
const resolved = contentRef
? await context.resolveContentRef(contentRef, {
resolveAnchorText: true,
iconStyle: ['mr-2', 'text-dark/6', 'dark:text-light/6'],
})
Expand All @@ -191,26 +203,51 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
>
{resolved?.icon ?? null}
{resolved ? (
<StyledLink href={resolved.href}>{resolved.text}</StyledLink>
<StyledLink
href={resolved.href}
insights={
contentRef
? {
target: contentRef,
position: 'content',
}
: undefined
}
>
{resolved.text}
</StyledLink>
) : null}
</Tag>
);
}
case 'users': {
const resolved = await Promise.all(
(value as string[]).map((userId) =>
context.resolveContentRef({
(value as string[]).map(async (userId) => {
const contentRef: ContentRefUser = {
kind: 'user',
user: userId,
}),
),
};
const resolved = await context.resolveContentRef(contentRef);
if (!resolved) {
return null;
}

return [contentRef, resolved] as const;
}),
);

return (
<Tag className={tcls('text-base')} aria-labelledby={ariaLabelledBy}>
{resolved.filter(filterOutNullable).map((file, index) => (
<StyledLink key={index} href={file.href}>
{file.text}
{resolved.filter(filterOutNullable).map(([contentRef, resolved], index) => (
<StyledLink
key={index}
href={resolved.href}
insights={{
target: contentRef,
position: 'content',
}}
>
{resolved.text}
</StyledLink>
))}
</Tag>
Expand Down
4 changes: 4 additions & 0 deletions packages/gitbook/src/components/Footer/FooterLinksGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ async function FooterLink(props: { link: CustomizationContentLink; context: Cont
'dark:text-light/8',
'dark:hover:text-light/9',
)}
insights={{
target: link.to,
position: 'footer',
}}
>
{link.title}
</Link>
Expand Down
19 changes: 11 additions & 8 deletions packages/gitbook/src/components/Header/Dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { DetailedHTMLProps, HTMLAttributes, useId } from 'react';

import { ClassValue, tcls } from '@/lib/tailwind';

import { Link } from '../primitives';
import { Link, LinkInsightsProps } from '../primitives';

export type DropdownButtonProps<E extends HTMLElement = HTMLElement> = Omit<
Partial<DetailedHTMLProps<HTMLAttributes<E>, E>>,
Expand Down Expand Up @@ -110,19 +110,22 @@ export function DropdownMenu(props: { children: React.ReactNode }) {
/**
* Menu item in a dropdown.
*/
export function DropdownMenuItem(props: {
href: string | null;
active?: boolean;
className?: ClassValue;
children: React.ReactNode;
}) {
const { children, active = false, href, className } = props;
export function DropdownMenuItem(
props: {
href: string | null;
active?: boolean;
className?: ClassValue;
children: React.ReactNode;
} & LinkInsightsProps,
) {
const { children, active = false, href, className, insights } = props;

if (href) {
return (
<Link
href={href}
prefetch={false}
insights={insights}
className={tcls(
'px-3 py-1 text-sm rounded straight-corners:rounded-sm',
active ? 'bg-primary/3 dark:bg-light/2 text-primary-600' : null,
Expand Down
43 changes: 32 additions & 11 deletions packages/gitbook/src/components/Header/HeaderLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
CustomizationHeaderPreset,
SiteCustomizationSettings,
CustomizationHeaderItem,
ContentRef,
} from '@gitbook/api';
import assertNever from 'assert-never';

Expand Down Expand Up @@ -35,7 +36,7 @@ export async function HeaderLink(props: {
<Dropdown
className="shrink"
button={(buttonProps) => {
if (!target) {
if (!target || !link.to) {
return (
<HeaderItemDropdown
{...buttonProps}
Expand All @@ -47,6 +48,7 @@ export async function HeaderLink(props: {
return (
<HeaderLinkNavItem
{...buttonProps}
linkTarget={link.to}
linkStyle={linkStyle}
headerPreset={headerPreset}
title={link.title}
Expand All @@ -65,12 +67,13 @@ export async function HeaderLink(props: {
);
}

if (!target) {
if (!target || !link.to) {
return null;
}

return (
<HeaderLinkNavItem
linkTarget={link.to}
linkStyle={linkStyle}
headerPreset={headerPreset}
title={link.title}
Expand All @@ -81,6 +84,7 @@ export async function HeaderLink(props: {
}

export type HeaderLinkNavItemProps = {
linkTarget: ContentRef;
linkStyle: NonNullable<CustomizationHeaderItem['style']>;
headerPreset: CustomizationHeaderPreset;
title: string;
Expand All @@ -89,14 +93,15 @@ export type HeaderLinkNavItemProps = {
} & DropdownButtonProps<HTMLElement>;

function HeaderLinkNavItem(props: HeaderLinkNavItemProps) {
switch (props.linkStyle) {
const { linkStyle, ...rest } = props;
switch (linkStyle) {
case 'button-secondary':
case 'button-primary':
return <HeaderItemButton {...props} linkStyle={props.linkStyle} />;
return <HeaderItemButton {...rest} linkStyle={linkStyle} />;
case 'link':
return <HeaderItemLink {...props} />;
return <HeaderItemLink {...rest} />;
default:
assertNever(props.linkStyle);
assertNever(linkStyle);
}
}

Expand All @@ -105,7 +110,7 @@ function HeaderItemButton(
linkStyle: 'button-secondary' | 'button-primary';
},
) {
const { linkStyle, headerPreset, title, href, isDropdown, ...rest } = props;
const { linkTarget, linkStyle, headerPreset, title, href, isDropdown, ...rest } = props;
const variant = (() => {
switch (linkStyle) {
case 'button-secondary':
Expand Down Expand Up @@ -139,6 +144,10 @@ function HeaderItemButton(
),
}[linkStyle],
)}
insights={{
target: linkTarget,
position: 'header',
}}
{...rest}
>
{title}
Expand All @@ -158,10 +167,18 @@ function getHeaderLinkClassName(props: { headerPreset: CustomizationHeaderPreset
);
}

function HeaderItemLink(props: HeaderLinkNavItemProps) {
const { headerPreset, title, isDropdown, href, ...rest } = props;
function HeaderItemLink(props: Omit<HeaderLinkNavItemProps, 'linkStyle'>) {
const { linkTarget, headerPreset, title, isDropdown, href, ...rest } = props;
return (
<Link href={href} className={getHeaderLinkClassName({ headerPreset })} {...rest}>
<Link
href={href}
className={getHeaderLinkClassName({ headerPreset })}
insights={{
target: linkTarget,
position: 'header',
}}
{...rest}
>
<span className="truncate min-w-0">{title}</span>
{isDropdown ? <DropdownChevron /> : null}
</Link>
Expand Down Expand Up @@ -198,5 +215,9 @@ async function SubHeaderLink(props: {
return null;
}

return <DropdownMenuItem href={target.href}>{link.title}</DropdownMenuItem>;
return (
<DropdownMenuItem href={target.href} insights={{ target: link.to, position: 'header' }}>
{link.title}
</DropdownMenuItem>
);
}
14 changes: 13 additions & 1 deletion packages/gitbook/src/components/Header/HeaderLinkMore.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,19 @@ async function MoreMenuLink(props: {
{'links' in link && link.links.length > 0 && (
<hr className="first:hidden border-t border-light-3 dark:border-dark-3 my-1 -mx-2" />
)}
<DropdownMenuItem href={target?.href ?? null}>{link.title}</DropdownMenuItem>
<DropdownMenuItem
href={target?.href ?? null}
insights={
link.to
? {
target: link.to,
position: 'header',
}
: undefined
}
>
{link.title}
</DropdownMenuItem>
{'links' in link
? link.links.map((subLink, index) => (
<MoreMenuLink key={index} {...props} link={subLink} />
Expand Down
Loading
Loading