Skip to content

fix(docs): missing vitepress route for api-docs#1278

Merged
webern merged 3 commits into
mainfrom
matt.briggs/fix-api-docs
Mar 30, 2026
Merged

fix(docs): missing vitepress route for api-docs#1278
webern merged 3 commits into
mainfrom
matt.briggs/fix-api-docs

Conversation

@webern
Copy link
Copy Markdown
Contributor

@webern webern commented Mar 29, 2026

Summary

TL;DR

This link button is broken and I think this PR will fix it 🤞

image

Too Short; Need More

Vitepress is using some sort of router that doesn't have an entry for api-docs/index.html because there isn't something there when we do the vitepress build step. If we put a placeholder Markdown file there then the router will have an entry. In our CI publish step we write the output of our Rust docs build to that location, so we will serve the Rust docs as intended. Claude is highly convinced this is going to work.

Long-Winded AI Explaination

Add a placeholder file at docs/api-docs/index.md. This gives VitePress a page at that route,
so the SPA router resolves it instead of showing 404.

In production, docs.yml runs two jobs: generate-docs-site (VitePress build) and
generate-api-docs (cargo +nightly doc). The publish job extracts VitePress to /tmp/docs/,
then extracts rustdoc to /tmp/docs/api-docs/, overwriting the placeholder index.html with
the real rustdoc output. Locally, make run-docs serves only VitePress, so developers see the
placeholder explaining what belongs there and how to generate API docs with cargo doc.

Detailed Reasoning

Step 1: VitePress builds the docs site (including our placeholder)

.github/workflows/docs.yml:17-22 — the generate-docs-site job:

- name: Build documentation
  run: |
    yarn install
    yarn run docs:build
- name: Compress generated documentation assets
  run: tar -C ./docs/.vitepress/dist -c -z -f docs.tar.gz .

yarn run docs:build invokes VitePress, which compiles every .md file under docs/ into
static HTML. Our new file docs/api-docs/index.md becomes
docs/.vitepress/dist/api-docs/index.html — the placeholder. VitePress also adds
api-docs_index.md to its hashmap.json, which is the SPA router's page lookup table. This is
the key part of the fix: without an entry in the hashmap, VitePress's client-side JavaScript
intercepts clicks to /api-docs/, finds no match, and renders "404 page not found" without ever
making a server request. With the entry, VitePress routes to it normally.

The job tars the entire docs/.vitepress/dist/ directory (including api-docs/index.html) into
docs.tar.gz and uploads it as an artifact.

Step 2: CI generates rustdoc

.github/workflows/docs.yml:42-48 — the generate-api-docs job:

- name: Generate API documentation
  run: |
    RUSTDOCFLAGS="--enable-index-page -Zunstable-options" cargo +nightly doc --no-deps -Zrustdoc-map --lib
- name: Compress generated API documentation assets
  run: tar -C ./target/doc -c -z -f api-docs.tar.gz .

This runs cargo doc on nightly, generating rustdoc HTML into target/doc/. The
--enable-index-page flag produces an index.html listing all crates. The job tars target/doc/
into api-docs.tar.gz and uploads it.

Step 3: Publish composites them — rustdoc overwrites the placeholder

.github/workflows/docs.yml:78-83 — the publish job extracts both artifacts sequentially:

- name: Create extract directory
  run: mkdir -p /tmp/docs/api-docs
- name: Uncompress generated documentation assets
  run: tar -C /tmp/docs -x -z -f ./docs.tar.gz && rm ./docs.tar.gz
- name: Uncompress generated API documentation assets
  run: tar -C /tmp/docs/api-docs -x -z -f ./api-docs.tar.gz && rm ./api-docs.tar.gz

Line 81 extracts VitePress into /tmp/docs/. This puts our placeholder at
/tmp/docs/api-docs/index.html. Line 83 then extracts rustdoc into /tmp/docs/api-docs/,
overwriting /tmp/docs/api-docs/index.html with rustdoc's crate index page. The order matters:
VitePress first, rustdoc second. The entire /tmp/docs/ directory is then published to gh-pages
(line 84-91).

Why the placeholder never appears in production

The rustdoc index.html (a full standalone HTML page listing all Saluki crates) replaces the
VitePress-generated index.html (our placeholder) byte-for-byte on disk before publish. GitHub
Pages serves the rustdoc version. The placeholder only exists transiently during CI and locally
when running make run-docs.

Change Type

  • Bug fix
  • New feature
  • Non-functional (chore, refactoring, docs)
  • Performance

How did you test this PR?

Hard to know for sure if this will fix the issue in production, but I tried to replicate what happens in production locally like this (and it worked):

#!/usr/bin/env bash
set -euo pipefail
cd ~/repos/saluki

# ─── generate-docs-site job (.github/workflows/docs.yml:17-22) ───
# Corresponds to: yarn run docs:build
# VitePress compiles all .md files under docs/ into static HTML.
# Our placeholder at docs/api-docs/index.md becomes dist/api-docs/index.html.
bun run docs:build

# Corresponds to: tar -C ./docs/.vitepress/dist -c -z -f docs.tar.gz .
tar -C ./docs/.vitepress/dist -c -z -f /tmp/docs.tar.gz .

# ─── generate-api-docs job (.github/workflows/docs.yml:42-48) ───
# Corresponds to: cargo +nightly doc (generates rustdoc into target/doc/)
RUSTDOCFLAGS="--enable-index-page -Zunstable-options" \
  cargo +nightly doc --no-deps -Zrustdoc-map --lib

# Corresponds to: tar -C ./target/doc -c -z -f api-docs.tar.gz .
tar -C ./target/doc -c -z -f /tmp/api-docs.tar.gz .

# ─── publish job (.github/workflows/docs.yml:78-83) ───
# Corresponds to: mkdir -p /tmp/docs/api-docs
rm -rf /tmp/docs
mkdir -p /tmp/docs/api-docs

# Corresponds to: tar -C /tmp/docs -x -z -f ./docs.tar.gz
# Extracts VitePress output. Placeholder lands at /tmp/docs/api-docs/index.html.
tar -C /tmp/docs -x -z -f /tmp/docs.tar.gz

# Corresponds to: tar -C /tmp/docs/api-docs -x -z -f ./api-docs.tar.gz
# Extracts rustdoc. Overwrites /tmp/docs/api-docs/index.html with rustdoc's crate index.
tar -C /tmp/docs/api-docs -x -z -f /tmp/api-docs.tar.gz

# ─── Verify the overwrite ───
echo "--- Checking /tmp/docs/api-docs/index.html ---"
if grep -q 'rustdoc' /tmp/docs/api-docs/index.html; then
  echo "PASS: rustdoc replaced the placeholder"
else
  echo "FAIL: placeholder was NOT overwritten"
fi

# ─── Serve locally ───
# Note: the site expects to be served under /saluki/ (see docs/.vitepress/config.mts base option).
# Create the expected path structure for the local server.
rm -rf /tmp/docs-serve
mkdir -p /tmp/docs-serve/saluki
cp -r /tmp/docs/* /tmp/docs-serve/saluki/

echo ""
echo "Serving at http://localhost:8080/saluki/"
echo "  Homepage:          http://localhost:8080/saluki/"
echo "  API Documentation: http://localhost:8080/saluki/api-docs/"
echo ""
cd /tmp/docs-serve && python3 -m http.server 8080

References

Closes #1276

webern added 2 commits March 29, 2026 20:30
Apparently a newer version of bun adds a configVersion key to the lock
file.
The button on the homepage links to /api-docs/ is intented to be
populated by rustdoc output during CI. However, VitePress has no page at
that route, so its router intercepts the click and renders a 404 without
even though an index.html exists.

Add a placeholder at `docs/api-docs/index.md` so VitePress registers the
route. When publishing docs we overwrite this and instead serve the
real API documentation.
@webern webern requested a review from a team as a code owner March 29, 2026 19:02
@dd-octo-sts dd-octo-sts Bot added the area/docs Reference documentation. label Mar 29, 2026
@pr-commenter
Copy link
Copy Markdown

pr-commenter Bot commented Mar 29, 2026

Binary Size Analysis (Agent Data Plane)

Target: f40ce45 (baseline) vs 00f180c (comparison) diff
Analysis Type: Stripped binaries (debug symbols excluded)
Baseline Size: 26.20 MiB
Comparison Size: 26.20 MiB
Size Change: +0 B (+0.00%)
Pass/Fail Threshold: +5%
Result: PASSED ✅

Changes by Module

Module File Size Symbols

Detailed Symbol Changes

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  [ = ]       0  [ = ]       0    TOTAL

@webern webern changed the title Matt.briggs/fix api docs fix(docs): missing vitepress route for api-docs Mar 29, 2026
@pr-commenter
Copy link
Copy Markdown

pr-commenter Bot commented Mar 29, 2026

Regression Detector (Agent Data Plane)

Regression Detector Results

Run ID: 8fad7f25-76ea-439e-acdb-9f8764307789

Baseline: f40ce45
Comparison: 00f180c
Diff

Optimization Goals: ✅ No significant changes detected

Experiments ignored for regressions

Regressions in experiments with settings containing erratic: true are ignored.

perf experiment goal Δ mean % Δ mean % CI trials links
otlp_ingest_logs_5mb_memory memory utilization +7.56 [+6.89, +8.23] 1 (metrics) (profiles) (logs)
otlp_ingest_logs_5mb_cpu % cpu utilization +0.30 [-4.64, +5.23] 1 (metrics) (profiles) (logs)
otlp_ingest_logs_5mb_throughput ingress throughput -0.03 [-0.16, +0.10] 1 (metrics) (profiles) (logs)

Fine details of change detection per experiment

perf experiment goal Δ mean % Δ mean % CI trials links
dsd_uds_1mb_3k_contexts_cpu % cpu utilization +10.26 [-47.06, +67.59] 1 (metrics) (profiles) (logs)
otlp_ingest_logs_5mb_memory memory utilization +7.56 [+6.89, +8.23] 1 (metrics) (profiles) (logs)
otlp_ingest_metrics_5mb_memory memory utilization +3.31 [+3.09, +3.54] 1 (metrics) (profiles) (logs)
dsd_uds_500mb_3k_contexts_throughput ingress throughput +3.23 [+3.03, +3.43] 1 (metrics) (profiles) (logs)
dsd_uds_500mb_3k_contexts_memory memory utilization +0.55 [+0.38, +0.72] 1 (metrics) (profiles) (logs)
otlp_ingest_traces_5mb_cpu % cpu utilization +0.44 [-1.57, +2.44] 1 (metrics) (profiles) (logs)
quality_gates_rss_dsd_low memory utilization +0.40 [+0.20, +0.60] 1 (metrics) (profiles) (logs)
otlp_ingest_logs_5mb_cpu % cpu utilization +0.30 [-4.64, +5.23] 1 (metrics) (profiles) (logs)
dsd_uds_100mb_3k_contexts_cpu % cpu utilization +0.27 [-5.79, +6.33] 1 (metrics) (profiles) (logs)
otlp_ingest_traces_ottl_filtering_5mb_memory memory utilization +0.24 [-0.10, +0.58] 1 (metrics) (profiles) (logs)
dsd_uds_10mb_3k_contexts_memory memory utilization +0.09 [-0.09, +0.28] 1 (metrics) (profiles) (logs)
dsd_uds_500mb_3k_contexts_cpu % cpu utilization +0.03 [-1.26, +1.31] 1 (metrics) (profiles) (logs)
dsd_uds_10mb_3k_contexts_throughput ingress throughput +0.02 [-0.11, +0.15] 1 (metrics) (profiles) (logs)
otlp_ingest_traces_5mb_memory memory utilization +0.01 [-0.25, +0.27] 1 (metrics) (profiles) (logs)
dsd_uds_100mb_3k_contexts_throughput ingress throughput +0.01 [-0.03, +0.04] 1 (metrics) (profiles) (logs)
dsd_uds_512kb_3k_contexts_throughput ingress throughput +0.01 [-0.05, +0.06] 1 (metrics) (profiles) (logs)
dsd_uds_1mb_3k_contexts_throughput ingress throughput +0.00 [-0.05, +0.06] 1 (metrics) (profiles) (logs)
otlp_ingest_traces_ottl_filtering_5mb_throughput ingress throughput -0.00 [-0.02, +0.02] 1 (metrics) (profiles) (logs)
otlp_ingest_traces_5mb_throughput ingress throughput -0.00 [-0.02, +0.02] 1 (metrics) (profiles) (logs)
otlp_ingest_traces_ottl_transform_5mb_throughput ingress throughput -0.00 [-0.02, +0.02] 1 (metrics) (profiles) (logs)
otlp_ingest_metrics_5mb_throughput ingress throughput -0.01 [-0.13, +0.11] 1 (metrics) (profiles) (logs)
otlp_ingest_logs_5mb_throughput ingress throughput -0.03 [-0.16, +0.10] 1 (metrics) (profiles) (logs)
dsd_uds_100mb_3k_contexts_memory memory utilization -0.10 [-0.28, +0.08] 1 (metrics) (profiles) (logs)
quality_gates_rss_dsd_medium memory utilization -0.10 [-0.30, +0.09] 1 (metrics) (profiles) (logs)
otlp_ingest_traces_ottl_transform_5mb_memory memory utilization -0.11 [-0.36, +0.14] 1 (metrics) (profiles) (logs)
quality_gates_rss_idle memory utilization -0.19 [-0.23, -0.15] 1 (metrics) (profiles) (logs)
quality_gates_rss_dsd_ultraheavy memory utilization -0.29 [-0.41, -0.17] 1 (metrics) (profiles) (logs)
otlp_ingest_traces_ottl_transform_5mb_cpu % cpu utilization -0.39 [-2.22, +1.43] 1 (metrics) (profiles) (logs)
dsd_uds_512kb_3k_contexts_memory memory utilization -0.40 [-0.57, -0.23] 1 (metrics) (profiles) (logs)
quality_gates_rss_dsd_heavy memory utilization -0.50 [-0.64, -0.37] 1 (metrics) (profiles) (logs)
dsd_uds_1mb_3k_contexts_memory memory utilization -0.61 [-0.79, -0.44] 1 (metrics) (profiles) (logs)
otlp_ingest_metrics_5mb_cpu % cpu utilization -0.90 [-7.69, +5.89] 1 (metrics) (profiles) (logs)
otlp_ingest_traces_ottl_filtering_5mb_cpu % cpu utilization -1.12 [-3.42, +1.17] 1 (metrics) (profiles) (logs)
dsd_uds_512kb_3k_contexts_cpu % cpu utilization -4.38 [-60.65, +51.90] 1 (metrics) (profiles) (logs)
dsd_uds_10mb_3k_contexts_cpu % cpu utilization -5.70 [-36.52, +25.12] 1 (metrics) (profiles) (logs)

Bounds Checks: ✅ Passed

perf experiment bounds_check_name replicates_passed observed_value links
quality_gates_rss_dsd_heavy memory_usage 10/10 113.38MiB ≤ 140MiB (metrics) (profiles) (logs)
quality_gates_rss_dsd_low memory_usage 10/10 33.71MiB ≤ 50MiB (metrics) (profiles) (logs)
quality_gates_rss_dsd_medium memory_usage 10/10 53.30MiB ≤ 75MiB (metrics) (profiles) (logs)
quality_gates_rss_dsd_ultraheavy memory_usage 10/10 164.48MiB ≤ 200MiB (metrics) (profiles) (logs)
quality_gates_rss_idle memory_usage 10/10 20.88MiB ≤ 40MiB (metrics) (profiles) (logs)

Explanation

Confidence level: 90.00%
Effect size tolerance: |Δ mean %| ≥ 5.00%

Performance changes are noted in the perf column of each table:

  • ✅ = significantly better comparison variant performance
  • ❌ = significantly worse comparison variant performance
  • ➖ = no significant change in performance

A regression test is an A/B test of target performance in a repeatable rig, where "performance" is measured as "comparison variant minus baseline variant" for an optimization goal (e.g., ingress throughput). Due to intrinsic variability in measuring that goal, we can only estimate its mean value for each experiment; we report uncertainty in that value as a 90.00% confidence interval denoted "Δ mean % CI".

For each experiment, we decide whether a change in performance is a "regression" -- a change worth investigating further -- if all of the following criteria are true:

  1. Its estimated |Δ mean %| ≥ 5.00%, indicating the change is big enough to merit a closer look.

  2. Its 90.00% confidence interval "Δ mean % CI" does not contain zero, indicating that if our statistical model is accurate, there is at least a 90.00% chance there is a difference in performance between baseline and comparison variants.

  3. Its configuration does not mark it "erratic".

Copy link
Copy Markdown
Member

@tobz tobz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, doesn't hurt to give an explainer for local dev on why there's no actual docs. 👍🏻

@webern
Copy link
Copy Markdown
Contributor Author

webern commented Mar 30, 2026

Yeah, doesn't hurt to give an explainer for local dev on why there's no actual docs. 👍🏻

That button doesn't work on the live site either and I think this may fix it.

@webern webern merged commit 7d03456 into main Mar 30, 2026
59 checks passed
@webern webern deleted the matt.briggs/fix-api-docs branch March 30, 2026 15:12
dd-octo-sts Bot pushed a commit that referenced this pull request Mar 30, 2026
## Summary

### TL;DR

This link button is broken and I think this PR will fix it 🤞

<img width="787" height="574" alt="image"
src="https://github.com/user-attachments/assets/33cf11ea-8eed-4ed5-96cb-15b7e7d52cb8"
/>

### Too Short; Need More

Vitepress is using some sort of router that doesn't have an entry for
`api-docs/index.html` because there isn't something there when we do the
vitepress build step. If we put a placeholder Markdown file there then
the router will have an entry. In our CI publish step we write the
output of our Rust docs build to that location, so we will serve the
Rust docs as intended. Claude is highly convinced this is going to work.

### Long-Winded AI Explaination

Add a placeholder file at `docs/api-docs/index.md`. This gives VitePress
a page at that route,
so the SPA router resolves it instead of showing 404.

In production, `docs.yml` runs two jobs: `generate-docs-site` (VitePress
build) and
`generate-api-docs` (`cargo +nightly doc`). The `publish` job extracts
VitePress to `/tmp/docs/`,
then extracts rustdoc to `/tmp/docs/api-docs/`, overwriting the
placeholder `index.html` with
the real rustdoc output. Locally, `make run-docs` serves only VitePress,
so developers see the
placeholder explaining what belongs there and how to generate API docs
with `cargo doc`.

#### Detailed Reasoning

**Step 1: VitePress builds the docs site (including our placeholder)**

`.github/workflows/docs.yml:17-22` — the `generate-docs-site` job:

```yaml
- name: Build documentation
  run: |
    yarn install
    yarn run docs:build
- name: Compress generated documentation assets
  run: tar -C ./docs/.vitepress/dist -c -z -f docs.tar.gz .
```

`yarn run docs:build` invokes VitePress, which compiles every `.md` file
under `docs/` into
static HTML. Our new file `docs/api-docs/index.md` becomes
`docs/.vitepress/dist/api-docs/index.html` — the placeholder. VitePress
also adds
`api-docs_index.md` to its `hashmap.json`, which is the SPA router's
page lookup table. This is
the key part of the fix: without an entry in the hashmap, VitePress's
client-side JavaScript
intercepts clicks to `/api-docs/`, finds no match, and renders "404 page
not found" without ever
making a server request. With the entry, VitePress routes to it
normally.

The job tars the entire `docs/.vitepress/dist/` directory (including
`api-docs/index.html`) into
`docs.tar.gz` and uploads it as an artifact.

**Step 2: CI generates rustdoc**

`.github/workflows/docs.yml:42-48` — the `generate-api-docs` job:

```yaml
- name: Generate API documentation
  run: |
    RUSTDOCFLAGS="--enable-index-page -Zunstable-options" cargo +nightly doc --no-deps -Zrustdoc-map --lib
- name: Compress generated API documentation assets
  run: tar -C ./target/doc -c -z -f api-docs.tar.gz .
```

This runs `cargo doc` on nightly, generating rustdoc HTML into
`target/doc/`. The
`--enable-index-page` flag produces an `index.html` listing all crates.
The job tars `target/doc/`
into `api-docs.tar.gz` and uploads it.

**Step 3: Publish composites them — rustdoc overwrites the placeholder**

`.github/workflows/docs.yml:78-83` — the `publish` job extracts both
artifacts sequentially:

```yaml
- name: Create extract directory
  run: mkdir -p /tmp/docs/api-docs
- name: Uncompress generated documentation assets
  run: tar -C /tmp/docs -x -z -f ./docs.tar.gz && rm ./docs.tar.gz
- name: Uncompress generated API documentation assets
  run: tar -C /tmp/docs/api-docs -x -z -f ./api-docs.tar.gz && rm ./api-docs.tar.gz
```

Line 81 extracts VitePress into `/tmp/docs/`. This puts our placeholder
at
`/tmp/docs/api-docs/index.html`. Line 83 then extracts rustdoc into
`/tmp/docs/api-docs/`,
overwriting `/tmp/docs/api-docs/index.html` with rustdoc's crate index
page. The order matters:
VitePress first, rustdoc second. The entire `/tmp/docs/` directory is
then published to `gh-pages`
(line 84-91).

**Why the placeholder never appears in production**

The rustdoc `index.html` (a full standalone HTML page listing all Saluki
crates) replaces the
VitePress-generated `index.html` (our placeholder) byte-for-byte on disk
before publish. GitHub
Pages serves the rustdoc version. The placeholder only exists
transiently during CI and locally
when running `make run-docs`.

## Change Type
- [X] Bug fix
- [ ] New feature
- [X] Non-functional (chore, refactoring, docs)
- [ ] Performance

## How did you test this PR?

Hard to know for sure if this will fix the issue in production, but I
tried to replicate what happens in production locally like this (and it
worked):

```bash
#!/usr/bin/env bash
set -euo pipefail
cd ~/repos/saluki

# ─── generate-docs-site job (.github/workflows/docs.yml:17-22) ───
# Corresponds to: yarn run docs:build
# VitePress compiles all .md files under docs/ into static HTML.
# Our placeholder at docs/api-docs/index.md becomes dist/api-docs/index.html.
bun run docs:build

# Corresponds to: tar -C ./docs/.vitepress/dist -c -z -f docs.tar.gz .
tar -C ./docs/.vitepress/dist -c -z -f /tmp/docs.tar.gz .

# ─── generate-api-docs job (.github/workflows/docs.yml:42-48) ───
# Corresponds to: cargo +nightly doc (generates rustdoc into target/doc/)
RUSTDOCFLAGS="--enable-index-page -Zunstable-options" \
  cargo +nightly doc --no-deps -Zrustdoc-map --lib

# Corresponds to: tar -C ./target/doc -c -z -f api-docs.tar.gz .
tar -C ./target/doc -c -z -f /tmp/api-docs.tar.gz .

# ─── publish job (.github/workflows/docs.yml:78-83) ───
# Corresponds to: mkdir -p /tmp/docs/api-docs
rm -rf /tmp/docs
mkdir -p /tmp/docs/api-docs

# Corresponds to: tar -C /tmp/docs -x -z -f ./docs.tar.gz
# Extracts VitePress output. Placeholder lands at /tmp/docs/api-docs/index.html.
tar -C /tmp/docs -x -z -f /tmp/docs.tar.gz

# Corresponds to: tar -C /tmp/docs/api-docs -x -z -f ./api-docs.tar.gz
# Extracts rustdoc. Overwrites /tmp/docs/api-docs/index.html with rustdoc's crate index.
tar -C /tmp/docs/api-docs -x -z -f /tmp/api-docs.tar.gz

# ─── Verify the overwrite ───
echo "--- Checking /tmp/docs/api-docs/index.html ---"
if grep -q 'rustdoc' /tmp/docs/api-docs/index.html; then
  echo "PASS: rustdoc replaced the placeholder"
else
  echo "FAIL: placeholder was NOT overwritten"
fi

# ─── Serve locally ───
# Note: the site expects to be served under /saluki/ (see docs/.vitepress/config.mts base option).
# Create the expected path structure for the local server.
rm -rf /tmp/docs-serve
mkdir -p /tmp/docs-serve/saluki
cp -r /tmp/docs/* /tmp/docs-serve/saluki/

echo ""
echo "Serving at http://localhost:8080/saluki/"
echo "  Homepage:          http://localhost:8080/saluki/"
echo "  API Documentation: http://localhost:8080/saluki/api-docs/"
echo ""
cd /tmp/docs-serve && python3 -m http.server 8080
```

## References

Closes #1276 7d03456
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/docs Reference documentation.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug(docs)]: 404 from the developer API docs button

2 participants