A page ships the page tree without its fallback, and an operation page breadcrumbs as its product

The fallback listed the 2,400 operation pages no meta lists, and every page carried it five times over: 16 GB of the 19 GB export. The browser used it only for an operation page's breadcrumb, which now comes from the product page above it.


Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
2026-09-25 23:58:52 -07:00
co-authored by Hanzo Dev
parent fb991d3403
commit 7134deb49b
8 changed files with 77 additions and 9 deletions
+2 -2
View File
@@ -9,7 +9,7 @@ import { Provider } from './provider';
import type { ReactNode } from 'react';
import { Zen, ZenMono } from '@hanzo/font';
import { TreeContextProvider } from '@hanzo/docs-base-ui/contexts/tree';
import { source } from '@/lib/source';
import { clientTree } from '@/lib/source';
import { NextProvider } from '@hanzo/docs/core/framework/next';
import { Analytics } from '@hanzo/docs-analytics';
import { AppearanceDock } from '@/components/appearance-dock';
@@ -36,7 +36,7 @@ export default function RootLayout({ children }: { children: ReactNode }) {
<html lang="en" className={`${Zen.variable} ${ZenMono.variable}`} suppressHydrationWarning>
<Body>
<NextProvider>
<TreeContextProvider tree={source.getPageTree()}>
<TreeContextProvider tree={clientTree()}>
<Provider>{children}</Provider>
</TreeContextProvider>
</NextProvider>
+2 -2
View File
@@ -4,7 +4,7 @@ import { DocsNavbar } from '@/components/docs-navbar';
import { Brand } from '@/components/brand';
import { HanzoDocsIcon } from '@/app/layout.client';
import { baseOptions } from '@/components/layouts/shared';
import { source } from '@/lib/source';
import { clientTree, source } from '@/lib/source';
import { getSection } from '@/lib/source/navigation';
import { SidebarSearch } from '@/components/sidebar-search';
import { SidebarAccount } from '@/components/sidebar-account';
@@ -43,7 +43,7 @@ export function Docs({
// The theme control lives in the bottom-right dock (components/appearance-dock),
// the same corner hanzo.ai keeps it in — so the rail carries none.
slots={{ header: DocsNavbar, themeSwitch: false }}
tree={source.getPageTree()}
tree={clientTree()}
nav={{
...base.nav,
// The mark and the wordmark are alternatives, not a pair — see Brand.
+2 -2
View File
@@ -1,5 +1,5 @@
import { baseOptions, linkItems, logo } from '@/components/layouts/shared';
import { source } from '@/lib/source';
import { clientTree, source } from '@/lib/source';
import { getSection } from '@/lib/source/navigation';
import { getSidebarTabs } from '@hanzo/docs-base-ui/components/sidebar/tabs/index';
import type { ReactNode } from 'react';
@@ -12,7 +12,7 @@ export function FluxLayout({ children }: { children: ReactNode }) {
return (
<LayoutClient
{...base}
tree={source.getPageTree()}
tree={clientTree()}
// just icon items
links={linkItems.filter((item) => item.type === 'icon')}
nav={{
+11
View File
@@ -141,6 +141,17 @@ for (const theme of ['dark', 'light'] as const) {
});
}
// Every page carried the page tree's fallback — the 2,400 operation pages no
// meta lists — so an operation page weighed 1.7 MB of HTML, most of it a list of
// other pages. It ships without it and still says where it is.
test('an operation page carries no fallback tree and names its product', async ({ page }) => {
const res = await page.goto('/docs/openapi/bot/get-bot-runs/', { waitUntil: 'load' });
const html = await res!.text();
expect(html).not.toContain('fallback:openapi');
expect(html.length, 'HTML bytes').toBeLessThan(1_000_000);
await expect(page.locator('article a[href="/docs/openapi/bot/"]', { hasText: /^Bot$/ })).toBeVisible();
});
test('prose is spaced: a paragraph has a margin', async ({ page }) => {
await open(page, '/docs/quickstart/', 'dark');
const margin = await page
+20
View File
@@ -63,6 +63,26 @@ function pageTreeCodeTitles(): LoaderPlugin {
};
}
/**
* The page tree the chrome sends to the browser: the tree without its fallback.
*
* The fallback holds every page no meta.json lists — the 2,400 operation pages
* under /docs/openapi — and the layout serialises the tree into every page it
* renders, so each of 6,000 pages carried 0.8 MB of it, five times over (the
* HTML and four RSC files): 16 GB of the 19 GB export. The browser used it for
* one thing, the breadcrumb of an operation page, and that is found from the
* operation's product page instead (base-ui contexts/tree `nearest`). The server
* keeps the whole tree for neighbours, the search index and the sitemap.
*/
let shipped: ReturnType<typeof source.getPageTree> | undefined;
export function clientTree(): ReturnType<typeof source.getPageTree> {
if (!shipped) {
const { fallback: _served, ...tree } = source.getPageTree();
shipped = tree;
}
return shipped;
}
export const blog = loader(createSource(blogPosts, []), {
baseUrl: '/blog',
});
+3 -1
View File
@@ -2,7 +2,7 @@
import type * as PageTree from '@hanzo/docs-core/page-tree';
import { usePathname } from '@hanzo/docs-core/framework';
import { type ReactNode, useMemo, useRef, createContext, use } from 'react';
import { searchPath } from '@hanzo/docs-core/breadcrumb';
import { searchNearestPath, searchPath } from '@hanzo/docs-core/breadcrumb';
type MakeRequired<O, K extends keyof O> = Omit<O, K> & Pick<Required<O>, K>;
@@ -32,6 +32,8 @@ export function TreeContextProvider({
return (
searchPath(tree.children, pathname) ??
(tree.fallback ? searchPath(tree.fallback.children, pathname) : null) ??
// An operation page no meta lists breadcrumbs as its product's page.
searchNearestPath(tree.children, pathname) ??
[]
);
}, [tree, pathname]);
+20
View File
@@ -117,3 +117,23 @@ export function searchPath(nodes: PageTree.Node[], url: string): PageTree.Node[]
return findPath(nodes, (node) => node.type === 'page' && node.url === normalizedUrl);
}
/**
* The path to the closest page ABOVE `url`, for a page the tree does not list.
*
* A generated reference lists its product pages and not every operation under
* them: `/docs/openapi/bot/get-bot-runs` has no node, and `/docs/openapi/bot`
* does. The page that answered is an ancestor, not the page being read, so it
* leaves the path and its folder ends it — a breadcrumb then names the product,
* and a sidebar opens where the product page opens it.
*
* - When no page above `url` exists either, return null
*/
export function searchNearestPath(nodes: PageTree.Node[], url: string): PageTree.Node[] | null {
for (let at = normalizeUrl(url); at.lastIndexOf('/') > 0; ) {
at = at.slice(0, at.lastIndexOf('/'));
const found = searchPath(nodes, at);
if (found) return found.at(-1)?.type === 'page' ? found.slice(0, -1) : found;
}
return null;
}
+17 -2
View File
@@ -1,8 +1,8 @@
import { joinPath, splitPath } from '@/source/path';
import { describe, expect, test } from 'vitest';
import type { Root } from '@/page-tree/definitions';
import type { Folder, Item, Root } from '@/page-tree/definitions';
import { findNeighbour, findSiblings } from '@/page-tree/utils';
import { getBreadcrumbItems } from '@/breadcrumb';
import { getBreadcrumbItems, searchNearestPath } from '@/breadcrumb';
import { DefaultFormatter } from '@/i18n/middleware';
import { NextURL } from 'next/dist/server/web/next-url';
import { updateHref } from '@/dynamic-link';
@@ -266,3 +266,18 @@ test('Dynamic Link: update href', () => {
// relative -> relative
expect(updateHref('[lang]/test', {})).toBe('test');
});
// A generated reference lists a product's page and not each operation under it,
// so an operation page is placed under its product rather than nowhere.
test('Search Nearest Path', () => {
const index: Item = { type: 'page', name: 'Bot', url: '/docs/openapi/bot' };
const bot: Folder = { type: 'folder', name: 'Bot', index, children: [] };
const api: Folder = { type: 'folder', name: 'By domain', children: [bot] };
const top: Item = { type: 'page', name: 'Home', url: '/docs' };
const nodes = [top, api];
expect(searchNearestPath(nodes, '/docs/openapi/bot/get-bot-runs')).toEqual([api, bot]);
expect(searchNearestPath(nodes, '/docs/openapi/bot/get-bot-runs/')).toEqual([api, bot]);
expect(searchNearestPath(nodes, '/docs/games')).toEqual([]);
expect(searchNearestPath(nodes, '/elsewhere')).toBeNull();
});