Skip to content

docs: Astro Starlight documentation site - #233

Draft
dimitropoulos wants to merge 11 commits into
mainfrom
docs/astro-starlight-site
Draft

docs: Astro Starlight documentation site#233
dimitropoulos wants to merge 11 commits into
mainfrom
docs/astro-starlight-site

Conversation

@dimitropoulos

Copy link
Copy Markdown
Collaborator

A Starlight docs site under packages/docs, and the two examples/ turned into playgrounds embedded in their own pages.

The playgrounds have no backend. Each example's real Worker is bundled into the page next to its real client, and the client's fetch of the RPC path is routed into the Worker's fetch handler, so the batching and round-trip counts the demos report are genuine while the site stays static and deployable anywhere.

Draft: the preview deploy currently sits behind an account-wide Cloudflare Access policy, so there is no public URL to link yet.

Adds packages/docs, a Starlight site covering the library, and turns the two
examples into in-page playgrounds.

The playgrounds have no backend. scripts/build-playgrounds.mjs bundles each
example's own Worker into the page alongside its own client and routes the
client's fetch of the RPC path into the Worker's fetch handler, so the batching
and round-trip counts the demos report are real while the site stays static.
Source panes are read from the repo at build time, so a renamed file or a
missing #region fails the build rather than rendering an empty tab.
Syncs the site with the two changes merged from main.

URL joins the by-value type lists on What Can Be Passed, the API reference and
the validation guide's coverage paragraph, and the protocol reference gains the
["url", href] expression.

The validation guide gains a Schema evolution section for #227: which
cross-version changes a validator accepts, and the distinction that matters
once extra arguments are allowed -- they are dropped before the method runs, so
an implementation still cannot read an argument no validator checked, while
extra object properties are forwarded unvalidated.

Also collapses the empty title bar Expressive Code leaves above terminal code
blocks, and adds a wrangler config that serves dist/ from a Worker's asset
store.
@changeset-bot

changeset-bot Bot commented Aug 7, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 5ce3fcb

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@pkg-pr-new

pkg-pr-new Bot commented Aug 7, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/cloudflare/capnweb@233

commit: 5ce3fcb

@ask-bonk

ask-bonk Bot commented Aug 7, 2026

Copy link
Copy Markdown

This demo code is correct and well-designed — the pipelining example genuinely demonstrates the feature. The fetch wrapping restores on finally. The playground shim installs before this runs, and this layers on top correctly.

There's one subtle interaction worth noting: in the playground shim (build-playgrounds.mjs:159), globalThis.fetch is replaced permanently (no restore), and the demo's withSimulatedNetwork captures globalThis.fetch as realFetch then calls it for the RPC path — so it correctly reaches the shim. This composes correctly.

I've reviewed the substantive code paths: the build-time bundler, the example Worker/API/server, the demo client, the Astro component, and the doc accuracy against the actual library. This PR is a documentation site plus example scaffolding. No changes touch src/ library code (the URL serialization commit is an inherited merge from main, PR #224). There are:

  • No untrusted-input/deserialization changes
  • No wire-protocol changes (protocol.md doc is a rendered copy, not a protocol change)
  • No public API changes
  • No capability-safety concerns
  • No cross-runtime leaks into shared library paths (the Unix-shell wrangler build command and example-local runtime code are fine)
  • No changeset needed (docs/examples only, not published-package behavior)

The documentation I spot-checked (security guide, api cheat sheet) matches the actual library behavior and signatures. The code is defensive and well-commented.

LGTM

github run

Replaces the nautical theme and the logo with a dark-first palette: a
near-black with a blue undertone, a deep saturated blue as the structural
colour, and an electric azure for anything interactive. Cloudflare orange
appears in three places and nowhere else -- the hero pulses, the primary call
to action, and the current sidebar page -- because a fourth use is the point at
which it stops meaning anything. Light mode is a second scheme rather than an
inversion, and the chrome stays near-black in both.

The landing page now runs a 3D network in WebGL. It is raw WebGL2 with no
dependency: the page claims the library is under 10 kB with nothing behind it,
and shipping a 3D framework to draw points and lines would undercut that in the
first paint. The animation is the argument rather than decoration -- each pulse
leaves a node, crosses several hops and returns along the same path, which is
what pipelining actually buys you.

Everything degrades to a designed state: the CSS gradient with no JavaScript or
no WebGL2, a single still frame under prefers-reduced-motion, and a parked loop
off-screen or on a hidden tab. GPU objects live in one struct that is rebuilt on
webglcontextrestored, so a driver reset or a wake from sleep does not leave a
dead rectangle. Additive blending cannot darken a pale page, so light mode
composites the same geometry normally, as ink.

Also fixes a horizontal overflow the new landing page exposed: cards holding a
code block would not shrink below the widest unwrapped line, so the whole
document scrolled sideways below about 1140px. And drops the explicit `sharp`
dependency, which existed only to optimise the raster logo.
Derived from an analysis of the Hacker News discussion of the Cap'n Web
launch announcement -- the twenty most frequently asked questions and the
ten most common points of confusion in that thread. Each one was checked
against the docs; this closes the gaps.

Two new guides:

- guides/comparisons.md -- positioning against tRPC/oRPC, JSON-RPC and
  GraphQL, an honest account of why .map() relocates the N+1 problem
  rather than solving it, why the library is scoped to JS/TS and what to
  use instead, a direct answer to the "isn't this CORBA again" objection,
  and the protocol-versus-library distinction.
- guides/sessions.md -- session lifetime and why nothing is persisted,
  designing so a dropped session is survivable, reconnection patterns
  including the React one, resumable subscriptions, API versioning, and
  the load-balancing and scale-in consequences of long-lived sessions.

Expanded in place:

- concepts/map.md -- which side of the wire actually runs your callback,
  why there is no index argument, why filter()/reduce() are deliberately
  absent, the failure modes TypeScript does not catch, and the cost of
  nesting.
- guides/security.md -- the Object.prototype and toJSON hardening the
  protocol performs, unbounded memory from un-awaited calls, and the
  server-side shape of the authentication example.
- concepts/promises.md -- one round trip is a claim about awaits, not
  about message count.
- guides/validation.md -- why generating validators from types is not
  defining the interface twice.
- Smaller additions on the name, the protocol-vs-library framing, why
  JSON rather than a binary format, streaming versus callbacks, async
  generators, documenting an API without OpenAPI, runtime requirements
  beyond "any JavaScript", and Workers RPC convergence.

Also corrects start/pipelining-tour.md, which said .map() runs the
callback on the server. The recording is made on the calling side; only
the RPCs it captured replay on the peer.

Every behavioural claim added here was verified against src/ or the test
suite rather than taken from the thread. Several thread answers are now
out of date -- streams have since shipped with automatic flow control --
and are documented as the code actually behaves.
The root README had grown into a 821-line manual that duplicated the
documentation site, and protocol.md duplicated reference/protocol. Two
copies of the same material means one of them is always the stale one --
the README still claimed Workers RPC had streams that Cap'n Web lacked,
which stopped being true some releases ago.

Delete protocol.md. Its content lives in
packages/docs/src/content/docs/reference/protocol.md, which is a strict
superset: same eight top-level messages and eighteen expression types,
plus a UInt8Array typo fixed and a section on why JSON rather than a
binary format.

Trim README.md from 821 lines to 187: what it is, why it is interesting,
how to install it, a hello-world client and server, the pipelining
example that makes the point, and then links into the docs. Everything
removed was verified present in the docs first, page by page.

The one detail the docs did not already state -- that disposing a
non-root stub releases the object on the peer without closing the
connection -- has been added to transports/index.

Also repoints the wire-protocol references in the bonk agent definition,
including the process rule that protocol changes must update the spec in
the same PR, and rewrites the provenance section of packages/docs/README
which described the root files as canonical.

Documentation links are relative paths into packages/docs, so they
resolve on GitHub and npm today. They should become URLs once the site
has a published home.
Each documentation page now builds its own Open Graph image at
/og/<slug>.png, in the site's own idiom rather than a generic template.

The card reuses the hero's motif: nodes on a jittered sphere wired to
their nearest neighbours, with three edges lit in Cloudflare orange to
stand for calls in flight -- the same restraint the rest of the theme
applies to that colour. The network is seeded from the page slug, so
every page gets a different one and none of them change between builds.
src/lib/og-network.ts is a deliberately separate, much smaller
implementation of the hero's idea, because the hero needs GPU buffers and
a frame loop and this needs a string of SVG.

Satori lays out the text and resvg rasterizes. Both run only at build
time. That means committing font data -- Satori has to measure glyphs to
break lines -- so Inter is subset to the characters titles actually use,
about 80 kB per weight instead of 400. Nothing is served to a browser and
the site still ships no web fonts.

The sidebar moves from astro.config.mjs to src/sidebar.mjs so that the
card and the navigation cannot disagree about which group a page is in.
Starlight already emits og:title, og:description, og:url and
twitter:card, so the Head override adds only the image tags.

Open Graph wants an absolute URL and `site` is still driven by
DOCS_SITE_URL, so without that variable the tag falls back to a
root-relative path. Correct in a browser, and it becomes absolute the
moment the site has a canonical home.
The two existing examples both make the pipelining argument. Neither shows
what a session *is*, which turns out to be the thing people most often get
wrong: they expect stubs to survive a reconnect, or expect the library to
re-establish them.

The new example is a WebSocket session with a button that severs it. It
demonstrates, in order: authentication as a capability rather than a header,
one round trip to connect and subscribe, server push through a client-side
RpcTarget, every stub rejecting once the socket dies, and a reconnect that
resumes without a gap because the client kept a cursor of its own. Untick
"resume from cursor" and the gap appears, which is the point -- the event log
lives outside the session, and recovery is the application's job.

The docs playground previously only handled HTTP examples, by shimming fetch.
It now also handles WebSocket ones: `new WebSocket()` on the configured path
returns one end of an in-page pair, with the other end handed to a real
newWebSocketRpcSession running the Worker's main interface. The transport
reads WebSocket.CONNECTING and friends off the constructor, so the shim
carries those too. Everything but the network hop is genuine, including the
disconnect and the broken-stub rejections.

Also in the bundler: examples can now declare static assets to copy next to
the page, which a zero-build example needs for a plain <link> stylesheet, and
client.css is only linked when esbuild actually emitted one -- two 404s on the
existing pages.
Two example files were shown as `#region` excerpts, because both had their RPC
code tangled up with a few hundred lines of DOM wiring, chart drawing and CSS.
The region markers made the docs tabs look tidy without fixing the actual
problem, and an excerpt is a claim the reader cannot check: it asserts that the
rest of the file does not matter, and offers no way to confirm it.

So the files are split instead, the way the new example already was:

  batch-pipelining  public/index.html inline script -> demo.js + main.js
  worker-react      App.tsx #region runs            -> runs.ts

demo.js and runs.ts contain the two strategies and nothing else -- no DOM, no
React, no chart -- which is what the tab was trying to point at. main.js and
App.tsx keep the wiring. The latency slider now reaches the RPC code as an
argument rather than through a getElementById inside it, and runs.ts also picks
up the deliberate validation failure, so it holds every RPC call the app makes.

With no excerpts left, `region` comes out of PlaygroundFile and readRepoRegion
out of source.ts. The docs now have exactly one way to show source: the whole
file, read from the repo at build time.

Also: batch-pipelining's favicon was still the pre-redesign palette, and the
React shell had none at all, so the playground was requesting a /favicon.ico
that does not exist.

Verified the demos still behave, in the playground and against a real
`wrangler dev`: 1 round trip vs 3 either way, and no console errors.
Popped out of the docs, the three example pages looked like three different
projects. batch-pipelining had a title bar, a centred column and a footer;
worker-react was full-bleed with a stray orange stripe pinned to the top of the
viewport and no footer; session-recovery was centred but had neither bar nor
footer, and set its own 15px body font.

They now share the chrome, to the pixel: a title bar linking home, a 60rem
column with matching padding, the same h1 scale and lede treatment, and a footer
crediting the source and pointing at the relevant concept page. All of it still
comes off when `data-embedded` is set, because the docs page around the iframe
supplies a title and a frame of its own.

The orange stripe is gone rather than restyled. Links no longer use the accent
either -- orange is spent on the one primary action per page, which is what
makes it read as the primary action -- so a `--link` colour joins the palettes
and the buttons converge on the same pill.

worker-react also loses its wrapper div's inline styles and its bullet list,
which just restated the two section headings underneath it. Its h1 was the
product name, which the title bar now carries; the h1 is the claim being
demonstrated, as on the other two.

Checked standalone and embedded, light and dark, at eight widths from 360 to
1920: no overflow anywhere, chrome hidden in every embed, and all three demos
still produce 1 round trip against 3.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant