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
21 changes: 17 additions & 4 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,30 @@
<link rel="icon" type="image/png" href="/assets/askr-logo-64.png" />
<link rel="apple-touch-icon" href="/assets/askr-logo-180.png" />
<!-- Google tag (gtag.js) -->
<script
async
src="https://www.googletagmanager.com/gtag/js?id=G-TXZTWQFV59"
></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-TXZTWQFV59', { send_page_view: false });

// gtag.js is ~170KB and, requested from the head, contends for bandwidth
// across the entire critical window on a throttled mobile connection.
// The stub above is what route-analytics actually calls, and dataLayer
// holds queued events until the library arrives to drain them, so
// loading it after onload costs no analytics and no contention.
(function () {
function loadGtag() {
var script = document.createElement('script');
script.async = true;
script.src =
'https://www.googletagmanager.com/gtag/js?id=G-TXZTWQFV59';
document.head.appendChild(script);
}
if (document.readyState === 'complete') loadGtag();
else window.addEventListener('load', loadGtag, { once: true });
})();
</script>
</head>

Expand Down
9 changes: 5 additions & 4 deletions src/pages/docs/catalog.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { apiManifest } from './api-manifest';
import {
descriptionOverrides,
headingOverrides,
lateHeadingOverrides,
} from './content-overrides';
import { docsPrimarySections } from './primary-sections';
Expand Down Expand Up @@ -116,9 +115,11 @@ function heading(
) {
if (typeof value !== 'string') return value;
const id = slug(value);
const override = route
? (headingOverrides[route]?.[id] ?? lateHeadingOverrides[route]?.[id])
: undefined;
// The main body corpus lives in heading-bodies.ts and is applied by
// resolveHeadingBodies() when a docs page renders, so that ~520KB of prose
// stays out of the eager route-registry chunk. Only the late overrides --
// a handful of pages, ~5KB -- are cheap enough to resolve here.
const override = route ? lateHeadingOverrides[route]?.[id] : undefined;
return {
id,
title: value,
Expand Down
2,349 changes: 0 additions & 2,349 deletions src/pages/docs/content-overrides.ts

Large diffs are not rendered by default.

2,376 changes: 2,376 additions & 0 deletions src/pages/docs/heading-bodies.ts

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion src/pages/docs/markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { apiSymbolSets } from './api-snapshot';
import { docsCatalog, docsSections } from './catalog';
import { cliSnapshot } from './cli-snapshot';
import { componentPropReferences } from './component-props';
import { resolveHeadingBodies } from './heading-bodies';
import { upgradeGuidance } from './release-notes';
import type { DocsPageDefinition } from './types';
import { buildUsageGuide, routeExampleFor } from './usage-guide';
Expand Down Expand Up @@ -238,7 +239,7 @@ export function renderDocsPageMarkdown(page: DocsPageDefinition): string {

lines.push(...componentPropsMarkdown(page));

for (const heading of page.headings) {
for (const heading of resolveHeadingBodies(page)) {
lines.push(
'',
`## ${markdownProse(heading.title)}`,
Expand Down
3 changes: 2 additions & 1 deletion src/pages/docs/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { docsByRoute, docsSections, resolveDocsRoute } from './catalog';
import { cliSnapshot } from './cli-snapshot';
import { componentDemoFor } from './component-demos';
import { componentPropReferences } from './component-props';
import { resolveHeadingBodies } from './heading-bodies';
import { upgradeGuidance } from './release-notes';
import type { DocsHeadingDefinition, DocsPageDefinition } from './types';
import { buildUsageGuide, routeExampleFor } from './usage-guide';
Expand Down Expand Up @@ -303,7 +304,7 @@ export default function DocsPage() {
<UsageGuide page={page} />
<ComponentDemo page={page} />
<ComponentPropsReference page={page} />
<HeadingContentList headings={page.headings} page={page} />
<HeadingContentList headings={resolveHeadingBodies(page)} page={page} />
{page.route === '/docs' && <DocsLandingDetails />}
{page.route === '/docs/tooling/cli-overview' && <CliReferenceSection />}
<PreviousNext page={page} />
Expand Down
36 changes: 35 additions & 1 deletion src/styles.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,39 @@
@import '@askrjs/themes/default';
@import '@fontsource-variable/domine';

/* Declared here rather than via @fontsource's own stylesheet so that
font-display can be `optional`. The hero <h1> is the LCP element on every
marketing route and it renders in Domine. With `swap`, a font arriving after
first paint re-lays out that heading into a taller box, and a larger paint
box registers a *second* LCP candidate -- which on throttled mobile landed
at ~4.9s against a 1.8s first paint. `optional` gives the font no swap
period: it is used if it is ready for the first layout and otherwise sits
out that page load entirely, so LCP can never be reset by typography.
The preload injected by ssg.config.ts is what makes it ready in time. */
@font-face {
font-family: 'Domine Variable';
font-style: normal;
font-display: optional;
font-weight: 400 700;
src: url('@fontsource-variable/domine/files/domine-latin-wght-normal.woff2')
format('woff2-variations');
unicode-range:
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212,
U+2215, U+FEFF, U+FFFD;
}

@font-face {
font-family: 'Domine Variable';
font-style: normal;
font-display: optional;
font-weight: 400 700;
src: url('@fontsource-variable/domine/files/domine-latin-ext-wght-normal.woff2')
format('woff2-variations');
unicode-range:
U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304,
U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB,
U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

* {
box-sizing: border-box;
Expand Down
34 changes: 29 additions & 5 deletions ssg.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { readFileSync } from 'node:fs';
import { readdirSync, readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { serializeRouteMeta } from '@askrjs/askr/router';
import type { DocumentRenderArgs } from '@askrjs/askr/ssg';
Expand All @@ -9,6 +9,29 @@ export const registry = routeRegistry;
export const outputDir = 'dist';

let clientTemplate: string | undefined;
let headExtras: string | undefined;

/**
* The hero <h1> is the LCP element on every marketing route and it renders in
* Domine. The font is only discoverable after the render-blocking stylesheet
* parses, so without a preload it is requested roughly a second late, swaps in
* after first paint, and resizes the heading -- which registers a second,
* larger LCP candidate. Preloading the latin subset (the only one English
* content pulls) starts the request alongside the stylesheet instead.
*/
function buildHeadExtras(): string {
const assetDir = resolve(process.cwd(), '.askr/client/assets');
const font = readdirSync(assetDir).find(
(name) =>
name.startsWith('domine-latin-wght-normal-') && name.endsWith('.woff2')
);
if (!font) {
throw new Error(
`Could not find the Domine latin woff2 in ${assetDir} to preload.`
);
}
return `<link rel="preload" as="font" type="font/woff2" crossorigin href="/assets/${font}" />`;
}

function renderDocument({ appHtml, context }: DocumentRenderArgs) {
clientTemplate ??= readFileSync(
Expand All @@ -20,10 +43,11 @@ function renderDocument({ appHtml, context }: DocumentRenderArgs) {
if (!metadata) {
throw new Error(`Missing metadata for route: ${context.route.path}`);
}
const document = clientTemplate.replace(
/<title(?:\s[^>]*)?>.*?<\/title>/,
serializeRouteMeta(metadata)
);
headExtras ??= buildHeadExtras();

const document = clientTemplate
.replace(/<title(?:\s[^>]*)?>.*?<\/title>/, serializeRouteMeta(metadata))
.replace('</head>', `${headExtras}</head>`);

return document.replace(
'<div id="app"></div>',
Expand Down
2 changes: 1 addition & 1 deletion tests/docs-catalog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { lucideIcons } from '../src/pages/docs/lucide-icons';
import { searchDocs } from '../src/pages/docs/search-index';
import { buildUsageGuide } from '../src/pages/docs/usage-guide';
import { upgradeGuidance } from '../src/pages/docs/release-notes';
import { headingOverrides } from '../src/pages/docs/content-overrides';
import { headingOverrides } from '../src/pages/docs/heading-bodies';
import { routeRegistry } from '../src/pages/_routes';
import { registry as staticRegistry } from '../ssg.config';
import {
Expand Down
5 changes: 4 additions & 1 deletion tests/llm-docs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest';
import { apiManifest } from '../src/pages/docs/api-manifest';
import { apiSymbolSets } from '../src/pages/docs/api-snapshot';
import { docsCatalog } from '../src/pages/docs/catalog';
import { resolveHeadingBodies } from '../src/pages/docs/heading-bodies';
import {
docsMarkdownPath,
docsMarkdownUrl,
Expand Down Expand Up @@ -38,7 +39,9 @@ describe('LLM documentation artifacts', () => {
expect(markdown?.endsWith('\n'), page.route).toBe(true);

if (page.navSection !== 'Generated API') {
for (const heading of page.headings) {
// Bodies are resolved the same way the renderer resolves them, so
// this also covers the lazy heading-bodies lookup.
for (const heading of resolveHeadingBodies(page)) {
expect(markdown, `${page.route}#${heading.id}`).toContain(
`## ${markdownProse(heading.title)}`
);
Expand Down