Skip to content

Commit db43f9d

Browse files
authored
Merge pull request #1004 from nteract/render-docs
Docs build: emit route JSON and prerender notes
2 parents 0c17dab + 4509ccc commit db43f9d

10 files changed

Lines changed: 626 additions & 37 deletions

File tree

docs/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Build the production site:
2222
npm run website:build
2323
```
2424

25-
The Parcel website target uses `docs/public/index.html` as its source and writes the built site to `docs/build`.
25+
The Parcel website target uses `docs/public/index.html` as its source and writes the built site to `docs/build`. After Parcel emits the interactive SPA bundle, `scripts/prerender.mjs` creates one HTML file per route and embeds sanitized, route-specific machine-readable content in each page's `<noscript>` fallback. It also writes `docs/build/llms-routes.json`, a structured manifest with route text, headings, code blocks, and links for agents that prefer JSON over HTML.
2626

2727
## Current Structure
2828

@@ -91,6 +91,7 @@ Some documentation artifacts are generated or synchronized from source files:
9191

9292
- API JSON and component descriptions come from `npm run docs:api:json`.
9393
- AI behavior contract sections are checked with `npm run check:ai-contracts` and regenerated with `npm run docs:ai-contracts`.
94+
- Route-level machine-readable HTML and `llms-routes.json` are generated by `npm run website:build` after the Parcel docs build.
9495
- `CLAUDE.md` is the primary assistant instruction source and is synced into other AI instruction files during the build pipeline.
9596

9697
Avoid hand-editing generated output unless the corresponding source cannot reasonably be updated.
4.76 KB
Loading

docs/public/blog/feed.xml

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,19 @@
55
<link rel="self" type="application/atom+xml" href="https://semiotic3.nteract.io/blog/feed.xml"/>
66
<link rel="alternate" type="text/html" href="https://semiotic3.nteract.io/blog"/>
77
<id>https://semiotic3.nteract.io/blog/</id>
8-
<updated>2026-06-08T00:00:00Z</updated>
8+
<updated>2026-06-07T00:00:00Z</updated>
99
<author><name>Semiotic</name></author>
1010
<entry>
11-
<title>Annotations That Adapt and Travel</title>
12-
<id>https://semiotic3.nteract.io/blog/annotations-that-adapt-and-travel</id>
13-
<link rel="alternate" type="text/html" href="https://semiotic3.nteract.io/blog/annotations-that-adapt-and-travel"/>
14-
<link rel="enclosure" type="image/png" href="https://semiotic3.nteract.io/blog/og/annotations-that-adapt-and-travel.png"/>
15-
<published>2026-06-08T00:00:00Z</published>
16-
<updated>2026-06-08T00:00:00Z</updated>
11+
<title>Annotations That Lead and Land</title>
12+
<id>https://semiotic3.nteract.io/blog/annotations-that-lead-and-land</id>
13+
<link rel="alternate" type="text/html" href="https://semiotic3.nteract.io/blog/annotations-that-lead-and-land"/>
14+
<link rel="enclosure" type="image/png" href="https://semiotic3.nteract.io/blog/og/annotations-that-lead-and-land.png"/>
15+
<published>2026-06-07T00:00:00Z</published>
16+
<updated>2026-06-07T00:00:00Z</updated>
1717
<author><name>Elijah Meeks</name></author>
1818
<category term="case-study"/>
1919
<category term="roadmap"/>
20-
<summary type="text">Placement (M2) and density (M3) decided where notes land and how many fit. M5 makes that adapt to space and house style. It sheds secondary notes as the plot narrows, and chooses whether notes blend into the chart or read as a distinct editorial layer. M6 adapts to the reader and to reuse: scale annotation amount by audience familiarity, and mark a note defensive so it survives every export with its source and confidence baked in.</summary>
20+
<summary type="text">Semiotic already treated annotations as data-bound objects. Now it helps authors make design decisions with those objects: primary and secondary notes, inferred reading order from confidence, an accessibility audit hook, and an opt-in annotationLayout recipe that places notes near their targets while avoiding obvious collisions.</summary>
2121
</entry>
2222
<entry>
2323
<title>Semiotic 3.7.0</title>

docs/public/ssr-gallery.html

Lines changed: 14 additions & 14 deletions
Large diffs are not rendered by default.

scripts/check-docs-routes.d.mts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,16 @@ export interface RequiredApiAsset {
1010
description: string
1111
}
1212

13+
export interface RequiredMachineReadableRoute {
14+
routePath: string
15+
keyword: string
16+
}
17+
1318
export interface DocsBuildValidationOptions {
1419
buildDir?: string
1520
routes?: RequiredDocsRoute[]
1621
apiAssets?: RequiredApiAsset[]
22+
machineReadableRoutes?: RequiredMachineReadableRoute[]
1723
}
1824

1925
export interface DocsBuildValidationResult {
@@ -25,6 +31,8 @@ export const REQUIRED_DOCS_ROUTES: RequiredDocsRoute[]
2531

2632
export const REQUIRED_API_ASSETS: RequiredApiAsset[]
2733

34+
export const REQUIRED_MACHINE_READABLE_ROUTES: RequiredMachineReadableRoute[]
35+
2836
export function routeHtmlPath(buildDir: string, routePath: string): string
2937

3038
export function validateDocsBuild(options?: DocsBuildValidationOptions): DocsBuildValidationResult

scripts/check-docs-routes.mjs

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,25 @@ export const REQUIRED_API_ASSETS = [
5454
},
5555
]
5656

57+
export const REQUIRED_MACHINE_READABLE_ROUTES = [
58+
{
59+
routePath: "",
60+
keyword: "Streaming-First Visualization for React",
61+
},
62+
{
63+
routePath: "getting-started",
64+
keyword: "streaming-first visualization library",
65+
},
66+
{
67+
routePath: "charts/line-chart",
68+
keyword: "LineChart",
69+
},
70+
{
71+
routePath: "blog/release-3-7-0",
72+
keyword: "receivability release",
73+
},
74+
]
75+
5776
export function routeHtmlPath(buildDir, routePath) {
5877
return routePath ? resolve(buildDir, routePath, "index.html") : resolve(buildDir, "index.html")
5978
}
@@ -62,6 +81,7 @@ export function validateDocsBuild({
6281
buildDir = DEFAULT_BUILD_DIR,
6382
routes = REQUIRED_DOCS_ROUTES,
6483
apiAssets = REQUIRED_API_ASSETS,
84+
machineReadableRoutes = REQUIRED_MACHINE_READABLE_ROUTES,
6585
} = {}) {
6686
const failures = []
6787

@@ -81,6 +101,52 @@ export function validateDocsBuild({
81101
expectIncludes(failures, html, "AI / Machine-readable docs", route.routePath, "noscript AI docs fallback")
82102
}
83103

104+
const manifestPath = resolve(buildDir, "llms-routes.json")
105+
let routeDocs = []
106+
if (!existsSync(manifestPath)) {
107+
failures.push(`Missing machine-readable route manifest at ${manifestPath}`)
108+
} else {
109+
try {
110+
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"))
111+
routeDocs = Array.isArray(manifest.routes) ? manifest.routes : []
112+
if (routeDocs.length === 0) {
113+
failures.push(`Machine-readable route manifest has no routes at ${manifestPath}`)
114+
}
115+
} catch (err) {
116+
const message = err instanceof Error ? err.message : String(err)
117+
failures.push(`Could not parse machine-readable route manifest at ${manifestPath}: ${message}`)
118+
}
119+
}
120+
121+
for (const route of machineReadableRoutes) {
122+
const filePath = routeHtmlPath(buildDir, route.routePath)
123+
if (!existsSync(filePath)) {
124+
failures.push(`Missing machine-readable route HTML ${route.routePath || "/"} at ${filePath}`)
125+
continue
126+
}
127+
128+
const html = readFileSync(filePath, "utf8")
129+
expectIncludes(failures, html, 'id="semiotic-route-doc"', route.routePath, "route JSON doc")
130+
expectIncludes(failures, html, 'id="machine-readable-page"', route.routePath, "machine-readable noscript content")
131+
expectIncludes(failures, html, route.keyword, route.routePath, "machine-readable keyword")
132+
133+
const routeKey = route.routePath || "/"
134+
const routeDoc = routeDocs.find((doc) => doc?.route === routeKey)
135+
if (!routeDoc) {
136+
failures.push(`Missing ${routeKey} in llms-routes.json`)
137+
continue
138+
}
139+
if (typeof routeDoc.text !== "string" || routeDoc.text.length < 200) {
140+
failures.push(`Machine-readable text for ${routeKey} is too short in llms-routes.json`)
141+
}
142+
if (!routeDoc.text?.includes(route.keyword)) {
143+
failures.push(`Machine-readable text for ${routeKey} is missing keyword: ${route.keyword}`)
144+
}
145+
if (!Array.isArray(routeDoc.headings) || routeDoc.headings.length === 0) {
146+
failures.push(`Machine-readable headings for ${routeKey} are missing in llms-routes.json`)
147+
}
148+
}
149+
84150
for (const asset of apiAssets) {
85151
const filePath = resolve(buildDir, asset.path)
86152
if (!existsSync(filePath)) {
@@ -131,5 +197,7 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
131197
process.exit(1)
132198
}
133199

134-
console.log(`✅ docs route smoke check passed (${REQUIRED_DOCS_ROUTES.length} routes, ${REQUIRED_API_ASSETS.length} API assets)`)
200+
console.log(
201+
`✅ docs route smoke check passed (${REQUIRED_DOCS_ROUTES.length} routes, ${REQUIRED_API_ASSETS.length} API assets, ${REQUIRED_MACHINE_READABLE_ROUTES.length} machine-readable routes)`
202+
)
135203
}

scripts/prerender.d.mts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,23 @@ export interface BlogEntryMeta {
1414
tags?: string[]
1515
}
1616

17+
export interface MachineReadableRouteDoc {
18+
route: string
19+
url: string
20+
html: string
21+
text: string
22+
headings?: Array<{ level: number; text: string }>
23+
codeBlocks?: string[]
24+
links?: Array<{ text: string; href: string }>
25+
}
26+
1727
export function generatePage(
1828
shellHtml: string,
1929
routePath: string,
20-
blogMeta?: BlogEntryMeta | null
30+
blogMeta?: BlogEntryMeta | null,
31+
machineDoc?: MachineReadableRouteDoc | null
2132
): string
2233

34+
export function sanitizeRouteHtml(renderedHtml: string, routePath: string): MachineReadableRouteDoc | null
35+
2336
export function prerender(): Promise<void>

0 commit comments

Comments
 (0)