Skip to content
Open
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
6 changes: 6 additions & 0 deletions .changeset/olive-bats-yell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@gitbook/icons": minor
"gitbook": patch
---

Update icon usage to render svg symbols rather than file.
2 changes: 1 addition & 1 deletion packages/gitbook/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,4 @@ screenshots/
# cloudflare
.open-next
.wrangler
worker-configuration.d.ts
worker-configuration.d.ts
27 changes: 22 additions & 5 deletions packages/gitbook/e2e/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,11 @@ export const headerLinks: CustomizationHeaderItem[] = [
},
];

type IconURLState = { state: 'pending'; uri: null } | { state: 'loaded'; uri: string };
type IconStateWindow = Window & {
__ICONS_STATES__?: Record<string, IconURLState>;
};

export async function waitForCookiesDialog(page: Page) {
const dialog = page.getByTestId('cookies-dialog');
await expect(dialog).toBeVisible({
Expand Down Expand Up @@ -396,11 +401,9 @@ export function getCustomizationURL(partial: DeepPartial<SiteCustomizationSettin
*/
export async function waitForIcons(page: Page) {
await page.waitForFunction(() => {
const urlStates: Record<
string,
{ state: 'pending'; uri: null } | { state: 'loaded'; uri: string }
> = (window as any).__ICONS_STATES__ || {};
(window as any).__ICONS_STATES__ = urlStates;
const iconWindow = window as IconStateWindow;
const urlStates: Record<string, IconURLState> = iconWindow.__ICONS_STATES__ || {};
iconWindow.__ICONS_STATES__ = urlStates;

const fetchSvgAsDataUri = async (url: string): Promise<string> => {
const response = await fetch(url);
Expand Down Expand Up @@ -433,6 +436,20 @@ export async function waitForIcons(page: Page) {
return true;
}

const svgSymbol = icon.querySelector('[data-testid="symbol-use"]');
if (svgSymbol) {
if (icon.dataset.gbIconSymbolState === 'loaded') {
return true;
}

const href = svgSymbol.getAttribute('href') ?? svgSymbol.getAttribute('xlink:href');
if (!href?.startsWith('#')) {
return false;
}

return document.getElementById(href.slice(1)) instanceof SVGElement;
}

const state = icon.getAttribute('data-argos-state');

if (state === 'pending') {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { type NextRequest, NextResponse } from 'next/server';

import { getIconSymbol } from '@/lib/icons/symbols';
import { getIconSymbolId } from '@gitbook/icons';

export async function GET(
_request: NextRequest,
{ params }: { params: Promise<{ style: string; icon: string }> }
) {
const { style, icon } = await params;
const symbol = await getIconSymbol(style, icon, getIconSymbolId(style, icon));

if (!symbol) {
return NextResponse.json(
{
error: 'Symbol not found',
},
{ status: 404 }
);
}

return new NextResponse(symbol.document, {
headers: {
'content-type': 'image/svg+xml; charset=utf-8',
'cache-control': 'public, max-age=31536000, immutable',
},
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { getContentLocale, getSpaceLanguage } from '@/intl/server';
import { getAssetURL } from '@/lib/assets';
import { tcls } from '@/lib/tailwind';

import { IconSpriteDefinitions } from './IconSpriteDefinitions';
import { RootLayoutClientContexts } from './RootLayoutClientContexts';

import './globals.css';
Expand Down Expand Up @@ -76,6 +77,9 @@ export async function CustomizationRootLayout(props: {
const sidebarStyles = getSidebarStyles(customization);
const { infoColor, successColor, warningColor, dangerColor } = getSemanticColors(customization);
const fontData = getFontData(customization.styling.font, 'content');
const iconStyle =
('icons' in customization.styling ? apiToIconsStyles[customization.styling.icons] : null) ||
IconStyle.Regular;
// Temporarily add a if here while the cache is being warmed up.
// We can remove the condition after 14-07-2025.
const monospaceFontData = customization.styling.monospaceFont
Expand Down Expand Up @@ -194,12 +198,11 @@ export async function CustomizationRootLayout(props: {
assetsURL: getAssetURL('icons'),
},
}}
iconStyle={
('icons' in customization.styling
? apiToIconsStyles[customization.styling.icons]
: null) || IconStyle.Regular
}
renderMode="symbol"
symbolLoaderURL="/~gitbook/icons/symbol"
iconStyle={iconStyle}
>
<IconSpriteDefinitions />
<RootLayoutClientContexts language={language}>
{children}
</RootLayoutClientContexts>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { getIconSymbol } from '@/lib/icons/symbols';
import {
type RegisteredIconSymbol,
clearRegisteredServerIconSymbols,
getRegisteredServerIconSymbols,
} from '@gitbook/icons';

/**
* Emits the subset of icon symbols that were rendered during the current request.
*/
export async function IconSpriteDefinitions() {
const registered: Map<string, RegisteredIconSymbol> = new Map(
getRegisteredServerIconSymbols().map((symbol: RegisteredIconSymbol) => [
`${symbol.style}/${symbol.icon}`,
symbol,
])
);

if (registered.size === 0) {
return null;
}

const definitions = (
await Promise.all(
[...registered.values()].map((symbol) => {
return getIconSymbol(symbol.style, symbol.icon, symbol.symbolId);
})
)
).filter((symbol): symbol is NonNullable<typeof symbol> => !!symbol);

clearRegisteredServerIconSymbols();

if (!definitions.length) {
return null;
}

return (
<svg
id="gb-icon-sprite-root"
data-testid="icon-sprite-root"
aria-hidden="true"
focusable="false"
xmlns="http://www.w3.org/2000/svg"
style={{
position: 'absolute',
width: 0,
height: 0,
overflow: 'hidden',
pointerEvents: 'none',
}}
dangerouslySetInnerHTML={{
__html: definitions.map((symbol) => symbol.symbol).join(''),
}}
/>
);
}
3 changes: 1 addition & 2 deletions packages/gitbook/src/components/SiteLayout/SiteLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { LoadIntegrations } from '@/components/Integrations';
import { SpaceLayout } from '@/components/SpaceLayout';
import type { VisitorAuthClaims } from '@/lib/adaptive';
import { buildVersion } from '@/lib/build';
import { GITBOOK_API_PUBLIC_URL, GITBOOK_ASSETS_URL, GITBOOK_ICONS_URL } from '@/lib/env';
import { GITBOOK_API_PUBLIC_URL, GITBOOK_ASSETS_URL } from '@/lib/env';
import { getResizedImageURL } from '@/lib/images';
import { isSiteIndexable } from '@/lib/seo';
import { AIContextProvider } from '../AI';
Expand All @@ -34,7 +34,6 @@ export async function SiteLayout(props: {
const scripts = withTracking ? context.scripts : [];

ReactDOM.preconnect(GITBOOK_API_PUBLIC_URL);
ReactDOM.preconnect(GITBOOK_ICONS_URL);
if (GITBOOK_ASSETS_URL) {
ReactDOM.preconnect(GITBOOK_ASSETS_URL);
}
Expand Down
Loading
Loading