---
title: Documentation Reader
description: Heavier reading-shell composition for documentation sites — numbered spine, fillSections, backToTop pill, dwell-tuned bookmark.
visibility: guest
draft: false
---

# Documentation Reader

A heavier reading-shell composition targeting the "Stripe / Linear docs" archetype: comfortable density, sidebar TOC, time-to-read meta, and the new every-item density-overview minimap. Designed for technical docs with deep h2 / h3 / h4 outlines where readers stay on a single page for extended periods.

Differs from the `long-form-article` recipe in two ways, both about how much of the page a reader can see at once: the minimap is on, giving a paragraph-level density overview beside the heading-level spine, and the time-to-read meta is mounted by hand so it can run at a documentation pace rather than an article one.

## Full Composition

:::preview{title="Stripe / Linear archetype", frame="iframe", height="640px"}
<x-wirekit::reading-shell
    :previewMode="true"
    bookmarkKey="documentation-reader-demo"
    :minimap="true"
    :meta="false"
    density="comfortable"
>
    <x-wirekit::prose>
        <header style="margin-bottom: 2rem;">
            <h1>API Reference: Authentication</h1>
            <x-wirekit::reading-meta :showRemaining="true" :wpm="200" />
        </header>
        <article>
            <h2 id="dr-overview">Overview</h2>
            <p>API authentication uses bearer tokens. Token rotation, scope handling, and rate-limit semantics are covered in the sub-sections below. The reading-shell composition here uses the comfortable density preset with the minimap toggle on — that gives you both the heading-level spine on the right and a paragraph-level density-overview alongside it.</p>
            <p>The minimap renders one stripe per item matched by <code>itemSelector</code> (default: <code>a, h2, h3, [data-minimap-item]</code>). On a docs page with 60+ paragraphs, the minimap gives a "you are here" overview of the whole page that the heading-level spine can't — heading-only TOCs lose granularity below the heading level.</p>
            <h3 id="dr-overview-tokens">Token Format</h3>
            <p>Tokens are 64-character hex strings prefixed with <code>sk_live_</code> or <code>sk_test_</code>. The prefix denotes the environment; the suffix is the random material. Tokens never expire automatically — rotation is a deliberate operation, not a background timer.</p>
            <p>Test-mode tokens scope to test-mode resources only. Live-mode tokens never collide with test data; the prefix is the only resolution mechanism. This avoids the worst-case "test code accidentally hits production" failure mode that token-suffix-only schemes don't prevent.</p>
            <h3 id="dr-overview-headers">Required Headers</h3>
            <p>Pass <code>Authorization: Bearer {token}</code> on every request. Both HTTP/1.1 and HTTP/2 are supported. Token rotation does not invalidate active long-poll connections — the connection-level auth is performed once at handshake.</p>
            <p>The token never appears in URL query strings or request paths. It only flows through the <code>Authorization</code> header. This keeps the token out of access logs, browser history, and cached upstream proxy entries.</p>
            <h2 id="dr-rotation">Token Rotation</h2>
            <p>Tokens never expire automatically — rotate manually via the dashboard or via the <code>POST /v1/tokens/rotate</code> endpoint. The dashboard is the recommended path for production rotation; the API endpoint is for automated rotation in CI / IaC pipelines.</p>
            <p>Rotation issues a new token, marks the old one as deprecated (still valid for 24 hours), and triggers a webhook to your registered URL. The 24-hour overlap is the recommended migration window — it covers a typical CI deploy cycle plus retry budget.</p>
            <h3 id="dr-rotation-rolling">Rolling Rotation</h3>
            <p>Issue a new token, deploy with both, then revoke the old one. This is the canonical zero-downtime rotation pattern — both tokens are valid simultaneously during the rolling window so any in-flight request signed with the old token still completes.</p>
            <p>Use the <code>X-Token-Generation</code> response header to track which token signed each response. CI canaries can compare the header against the expected new generation to confirm the rolling deploy reached every replica before revoking the old token.</p>
            <h4 id="dr-rotation-rolling-deploy">Deployment Window</h4>
            <p>Allow 5 minutes for caches to flush. Edge proxies cache the token-validation result for up to 60 seconds; the dashboard's "Revoke now" button bypasses the cache and forces immediate invalidation across every region.</p>
            <p>For zero-downtime rolling, sequence the revoke 5 minutes after the last replica deployed the new token. Earlier revoke risks a small slice of in-flight requests failing during the cache-flush window.</p>
            <h2 id="dr-scopes">Scopes</h2>
            <p>Scope tokens to specific operations to limit blast radius. A leaked read-only token cannot mutate state; a leaked customer-only token cannot enumerate other resources. Scopes are an essential defense-in-depth even when the token rotation cycle is fast.</p>
            <p>Each token can carry up to 32 scopes. The dashboard's scope picker shows the per-resource matrix (read / write / delete) — pick exactly what the token needs and nothing more. Token requests rejected for missing scope return HTTP 403 with a structured error body identifying the missing scope.</p>
            <h3 id="dr-scopes-read">Read Scopes</h3>
            <p>Read-only access to specific resources. <code>customers:read</code>, <code>orders:read</code>, <code>products:read</code> are the canonical three; lower-cardinality reads (e.g. <code>tax-rates:read</code>) get their own scopes for tighter control.</p>
            <p>Read scopes never grant access to webhook endpoints, internal admin operations, or audit logs. Each of those carries its own scope namespace (<code>webhooks:read</code>, <code>admin:read</code>, <code>audit:read</code>) — separate concerns get separate scopes.</p>
            <h3 id="dr-scopes-write">Write Scopes</h3>
            <p>Mutation access — use sparingly. <code>customers:write</code> grants both create and update; the destructive verb (<code>customers:delete</code>) is its own scope so least-privilege CI tokens can't accidentally delete records during a misfire.</p>
            <p>Write scopes implicitly grant the corresponding read scope. <code>customers:write</code> grants <code>customers:read</code>; you don't need to specify both. The implication runs only one direction — read scopes never grant write access.</p>
            <h2 id="dr-rate-limits">Rate Limits</h2>
            <p>Default 100 req/min per token; burst to 200 for short windows. The rate-limiter uses a token-bucket algorithm with a per-token state — each token has its own counter, so a leaked token can't exhaust your account-wide quota.</p>
            <p>Higher limits are available via the dashboard for verified accounts. Premium tiers ship with 1000 req/min default and 5000 req/min burst. The rate-limit headers (<code>X-RateLimit-Limit</code>, <code>X-RateLimit-Remaining</code>, <code>X-RateLimit-Reset</code>) appear on every response so client SDKs can self-throttle without polling a separate quota endpoint.</p>
        </article>
    </x-wirekit::prose>
</x-wirekit::reading-shell>
:::

## Differences from long-form-article

| Aspect | long-form-article | documentation-reader |
|---|---|---|
| Minimap | off | **on** — a stripe per item beside the heading-level spine, which is what a 60-paragraph page needs and a heading TOC cannot give |
| Time-to-read meta | the shell's own, left on | the shell's turned **off** and `<x-wirekit::reading-meta>` mounted by hand — the same result reached the other way round, which is what lets this page set a documentation-paced `wpm` |

**Three things are the same on both pages, which is why they are not rows.** Density: both run
`comfortable`, one by default and one explicitly. Bookmark dwell: `reading-shell` has no dwell prop at all — 30s
is `reading-bookmark`'s own default on both pages, and `:minDwellSeconds="120"` appears only in
each page's power-user preview, `long-form-article` documenting it in prose as the tunable it
is. Power-user composition: `long-form-article` carries `## Power-user composition (skip the
shell)` too, under the same heading.

## Power-user composition (skip the shell)

When the shell's toggles + density preset don't cover the customization need (numbered spine + per-section fill + back-to-top + custom wpm + custom bookmark threshold all at once), compose the primitives directly. The shell covers the common defaults case, and power-users opt out to the primitives entirely — the shell deliberately exposes a small set of props rather than one per primitive, so the escape hatch is composing them yourself.

:::preview{title="Power-user composition — primitives mounted directly", frame="iframe", height="680px"}
<div style="overflow-x: hidden;">
    <x-wirekit::reading-progress intent="primary" :milestones="true" />
    <x-wirekit::reading-spine
        :levels="'2,3,4'"
        expand="always-md"
        :numbered="true"
        :fillSections="true"
    />
    <x-wirekit::reading-bookmark :previewMode="true" key="docs-power-demo" :minDwellSeconds="120" :threshold="0.05" />
    <x-wirekit::prose density="compact">
        <article id="docs-body" class="wk-reading-content">
            <header style="margin-bottom: 1rem;">
                <h1>Power-user documentation reader</h1>
                <x-wirekit::reading-meta :wpm="200" :showRemaining="true" />
            </header>
            <h2 id="docs-p-1">Numbered, always-expanded spine</h2>
            <p>The spine on the right is numbered (1, 2, 3...) and stays expanded at md+ breakpoints. Per-section fill marks how far you've scrolled inside the current section, not just which one is active.</p>
            <h3 id="docs-p-1-1">Levels 2-4</h3>
            <p>Pass <code>:levels="'2,3,4'"</code> to include H4 headings — useful for docs with deeply nested sections.</p>
            <h2 id="docs-p-2">Trade-off vs the shell</h2>
            <p>Primitives compose more verbosely than the shell, but every knob each primitive exposes is reachable. Reach for primitive composition when you need a prop the shell doesn't pass through.</p>
            <h2 id="docs-p-3">Reading-meta + tight bookmark</h2>
            <p>The header above shows the reading-meta with <code>:showRemaining="true"</code> — the value updates as you scroll. The bookmark dwell threshold is 120 seconds with a 5% scroll threshold — only serious readers see the resume pill on return visits.</p>
        </article>
    </x-wirekit::prose>
</div>
:::

:::source{language="blade"}
{{-- Power-user composition: skip the shell, mount primitives directly --}}
<x-wirekit::reading-progress intent="primary" :milestones="true" />
<x-wirekit::reading-spine
    :levels="'2,3,4'"
    expand="always-md"
    :numbered="true"
    :fillSections="true"
    :backToTop="true"
>
    <x-slot:filter>
        <x-wirekit::input x-model="filter" placeholder="Filter sections..." size="sm" />
    </x-slot:filter>
</x-wirekit::reading-spine>
<x-wirekit::reading-minimap target="#docs-body" item-selector="h2, h3, h4" />
<x-wirekit::reading-bookmark
    key="docs-{{ $page->slug }}"
    :minDwellSeconds="120"
    :threshold="0.05"
/>
<x-wirekit::reading-meta :wpm="200" :showRemaining="true" />

<article id="docs-body">{{ $slot }}</article>
:::

## Section-Changed Analytics

Listen for `wirekit:reading-spine:section-changed` to update browser-tab title or fire analytics on section view. The handler wraps the reading-shell so the dispatched event bubbles up; the shell otherwise stays a plain composition wrapper.

:::preview{title="Section-changed analytics — title updates on active-section change", frame="iframe", height="360px"}
<div x-data x-on:wirekit:reading-spine:section-changed.window="$el.querySelector('[data-tab-title]').textContent = $event.detail.text + ' — Docs';">
    <x-wirekit::reading-shell :previewMode="true" bookmarkKey="docs-analytics-demo">
        <x-wirekit::prose density="compact">
            <article>
                <header style="margin-bottom: 1rem; padding-bottom: 0.5rem; border-bottom: 1px solid var(--color-wk-border); font-size: 0.875rem;">
                    <x-wirekit::text intent="muted" as="span">Browser tab title (live): </x-wirekit::text>
                    <strong data-tab-title>Docs</strong>
                </header>
                <h2 id="docs-a-1">Section one</h2>
                <p>Scroll up and down to watch the title above update — every active-section change fires a custom event the handler turns into a title update. Swap the body of the handler for your analytics call (PostHog, Plausible, Fathom, etc.).</p>
                <h2 id="docs-a-2">Section two</h2>
                <p>The event detail carries <code>id</code> and <code>text</code>. Use the id when you wire deep-links; use the text when you fire a "section viewed" analytics event.</p>
                <h2 id="docs-a-3">Section three</h2>
                <p>The handler is one Alpine listener — no Livewire roundtrip, no JS file to write. Drop the listener wherever it makes architectural sense (page layout, dedicated analytics wrapper, root template).</p>
            </article>
        </x-wirekit::prose>
    </x-wirekit::reading-shell>
</div>
:::

:::source{language="blade"}
<div x-on:wirekit:reading-spine:section-changed.window="
    document.title = $event.detail.text + ' — Docs';
    /* analytics call: $event.detail.id, $event.detail.text */
">
    <x-wirekit::reading-shell>
        <article>...</article>
    </x-wirekit::reading-shell>
</div>
:::

## Print Stylesheet

All five reading-* components ship `@media print { display: none !important }` rules — the bar, spine, and bookmark vanish on print. Readers who do "Print to PDF" get the article body cleanly, no chrome overprinted on every page.

## Related

- [Long-form Article](/blueprints/recipes/long-form-article) — lighter sibling recipe for blog posts
- [Reading](/components/reading) — the consolidated family page covering every primitive + composition + family contracts
