Skip to content

docs: rebuild convt.app/docs on Blume with a full API reference - #61

Merged
leoisadev1 merged 4 commits into
mainfrom
cnv-38-blume-docs
Oct 7, 2026
Merged

leoisadev1 merged 4 commits into
mainfrom
cnv-38-blume-docs

Conversation

@leoisadev1

@leoisadev1 leoisadev1 commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

Summary

The only API docs were one generated page at /docs/api, rendered from a thin spec with no guides, examples or error reference, and they pointed at api.convt.app, which does not resolve. This replaces it with a Blume docs site in apps/docs, laid out like the TanStack docs, and deploys it to convt.app/docs.

 apps/
+├── docs/                      # new: Blume site, static build
+│   ├── content/               # overview, quick start, guides, reference, examples
+│   ├── openapi/public.yaml    # overlay: prose, tags, examples on convt-server's spec
+│   ├── scripts/               # formats page generator, deploy.sh
+│   └── wrangler.jsonc         # convt-docs Worker on convt.app/docs*
 └── web/
-    └── routes/_site/docs/api.tsx   # old single-page reference, removed
  • Quick start with Node, cURL, Browser and CLI tabs. Each tab is a complete program.
  • API reference generated from crates/convt-server/openapi.json through an overlay, so the server spec stays the source of truth. Each operation gets its own page, samples in cURL, Node, JS and Python, and a working Try it panel.
  • Guides for authentication, the job lifecycle, uploads, polling and downloads, cancelling, browser apps, errors and retries, and the unpublished SDK.
  • Reference pages for the base URL, every error code, limits, and formats. The formats table is generated from cloud-formats.json, the same list the API checks.
  • Code highlighting is Shiki with the Vitesse light and dark themes.
  • The base URL is one variable in blume.config.ts. It is the Railway host for now, and every page notes that api.convt.app is not live (CNV-36).

Evidence

Before After
Old /docs/api page New API reference overview
Quick start, desktop Quick start, mobile
Quick start with tabs Quick start cURL tab on mobile
Operation page Try it against the live API
Create a job reference page Try it returning the API's real 403
  • Deployed: convt-docs is live on convt.app/docs and convt.app/docs/*. /docs, /docs/quick-start, /docs/api, every guide and example return 200, unknown paths return the docs 404, and www.convt.app/docs redirects to the apex. The landing page and dashboard still come from convt-web.
  • Samples: every program on the site was extracted from the built Markdown and run against the live Railway API with a dummy key. Each one reached the API and surfaced its 403 unauthorized correctly. The output renaming was checked separately.
  • Build: blume build (strict), blume validate (no broken links), bun run check, and check-types in apps/web all pass.
  • Review: an independent review found six factual errors, all fixed in the second commit. The worst: outputs come back named input.<ext>, so the batch example overwrote its own results, and the polling advice would have hit the rate limit.

Not tested: a full conversion with a real cvt_live_ key, since I had none. The flow matches @convt/sdk, which the server's replica acceptance test exercises.

Follow-ups

  • CNV-40: start before upload returns 502 storage_unavailable and leaves the job open. The docs describe the current behavior.
  • When CNV-36 lands, change apiBase in apps/docs/blume.config.ts and run bun run --cwd apps/docs deploy.

Merge Danger

Door: two-way. The docs Worker is already deployed; removing its two routes in Cloudflare hands /docs back to convt-web, and reverting restores the old page.

Blast radius: convt.app/docs only. Merging also drops /docs/api from convt-web, so staging (which has no docs Worker) will 404 there. CI's web job now installs Node 24 because Blume needs Node 22.19 or newer.

Fixes CNV-38

Created with Claude Opus 5.5 in Claude Code.


Devin Review

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Devin Review found 8 potential issues.

Devin Review

Comment on lines +25 to +29
while (next < files.length) {
const file = files[next++];
try {
const saved = await convertFile(file, to, { pollMs });
console.log(`${file} -> ${saved.join(", ")}`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Fast batches exhaust the API limit

When conversions finish quickly, pollMs does not pace convertFile calls; workers immediately create, start, and download more jobs. A batch exceeding 40 files per minute hits the 120-request limit, and short retries fail before reset.

Learn more

The API counts create, start, status, and download requests against a fixed 120-per-minute key limit; signed uploads and output downloads do not count. The loop starts the next conversion as soon as the prior one finishes, while pollMs only controls status checks inside convert. Forty-one short conversions need at least 123 create/start/download requests in the same minute, before any polls; the helper's five short 429 attempts cannot reliably wait for a minute-long window to reset.

Example: Four workers convert 45 small PNGs to WebP in 30 seconds. The 135 required API calls alone exceed the key's 120-request window; some conversions report failures although every input is valid.

Recommended fix: Add a shared per-key request scheduler covering create, start, status, cancel, and download calls, or wait for the window to reset after 429. Account for bursts as well as average polling rate; a concurrency limit alone does not cap requests per minute.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread apps/docs/content/quick-start.mdx Outdated
Comment on lines +64 to +67
const res = await fetch(output.url);
const name = output.name.replace(/^input/, "photo");
await writeFile(name, Buffer.from(await res.arrayBuffer()));
console.log(`saved ${name}`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Failed downloads overwrite saved conversions

When a signed URL expires or storage rejects it, the quick-start fetch writes the error body without checking res.ok. writeFile can replace an existing valid output, then report the corrupt file as saved.

Suggested change
const res = await fetch(output.url);
const name = output.name.replace(/^input/, "photo");
await writeFile(name, Buffer.from(await res.arrayBuffer()));
console.log(`saved ${name}`);
const res = await fetch(output.url);
if (!res.ok) throw new Error(`download failed with ${res.status}`);
const name = output.name.replace(/^input/, "photo");
await writeFile(name, Buffer.from(await res.arrayBuffer()));
console.log(`saved ${name}`);

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +90 to +93
} catch (error) {
await request(`/v1/jobs/${job.id}/cancel`, { method: "POST" }).catch(() => {});
throw error;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Failed cancellation strands a job reservation

When cancellation fails after an upload or polling error, convert hides that failure and throws the original error. Callers receive no job ID to retry cancellation, leaving the reservation open until expiry.

Learn more

Creating a job reserves one cent, released by success, failure, cancellation, or expiry. After a failed upload, start, or poll, this catch tries to cancel it, but swallows any cancellation failure. The exported convert method then throws an error without the created job's ID, preventing callers from releasing a reservation that may remain open for up to 24 hours. The existing ConvtCancellationError shows how the SDK preserves the ID when cancellation cannot be confirmed.

Example: A successful reservation is followed by a network failure during upload; cancellation also receives a network error. The caller gets only the upload error, not the job ID needed to retry releasing its reservation.

Recommended fix: Surface a cancellation-unconfirmed error containing job.id whenever the cancel call fails, and preserve the original failure as its cause. Optionally inspect a successful cancel response for a terminal status.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +82 to +83
await sleep(pollMs);
current = await request(`/v1/jobs/${job.id}`, { signal });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Conversion timeout ignores waiting periods

If timeoutMs expires during sleep, convert continues waiting until the next poll or retry. Large pollMs values can delay cancellation well past the configured deadline.

Learn more

The AbortSignal.timeout(timeoutMs) signal only reaches fetch requests. Neither the polling sleep here nor the retry sleep in request observes it. Expiry while waiting thus has no effect until the delay completes and the next fetch checks the aborted signal; cancellation is also postponed.

Example: With timeoutMs: 1000 and pollMs: 60000, a job that is still running after the start request holds convert for roughly 60 seconds instead of timing out after one.

Recommended fix: Make both sleeps abortable with the conversion signal and cancel the job immediately when the timeout fires; ensure the retry loop checks the signal before each delay.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

fi

# Outputs are named input.<ext>; save them under the input's own name.
STEM=$(basename "${FILE%.*}")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Dotted folders corrupt shell output names

For an extensionless input inside a dotted folder, STEM strips the folder suffix instead of the file extension. The script saves the conversion under the wrong name.

Suggested change
STEM=$(basename "${FILE%.*}")
STEM=$(basename "$FILE")
[[ $STEM == *.* ]] && STEM=${STEM%.*}

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread apps/docs/package.json Outdated
"build": "bun scripts/generate-formats.ts && blume build",
"preview": "blume preview",
"doctor": "blume doctor",
"validate": "bun scripts/generate-formats.ts && blume validate",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔍 Docs links lack a CI gate

The new validate script checks links, but CI runs only build and workspace checks. Broken docs links can reach deployment without a validation failure.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

4. **Poll** the job until its status is `succeeded`, `failed` or `cancelled`.
5. **Download** the outputs from the signed URLs the API returns.

The [Quick start](/quick-start) runs all five steps in Node, cURL, a browser app and the CLI.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔍 Existing links bypass the docs overview

The web site's API-docs links still target /docs/api. Visitors entering from the main site skip the new overview and quick-start path.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +14 to +17
export async function handleConvert(request) {
// Authenticate your own user here before spending your convt budget.

const form = await request.formData();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟥 Unauthenticated uploads spend the server's API budget

When deployed as written, handleConvert accepts unauthenticated uploads and converts them with the server's key. Anyone can consume the owner's paid conversion budget.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

@greptile-apps

greptile-apps Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

[Medium risk] Rebuilds the documentation site on a new platform.

The PR appears safe to merge; the latest changes fix all four numbered findings without breaking the related samples.

What we checked:

  • Helper callers still work: Both direct callers read the object’s fields. convertFile() still returns the array used by the batch script and command-line sample.

Summary

Replaces the old API page with a separate Blume docs site at /docs, including guides, examples, and a reference built from the server’s spec.

  • The latest changes fix all four numbered findings.
  • The helper, server route, and browser sample agree on the new { job, outputs } result.
  • No new actionable issues or repository-rule violations were found.
  • This re-check inspected code only. No samples, builds, or browser checks were run.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  Spec["Server API spec"] --> Blume["Blume docs build"]
  Overlay["Descriptions and shared API host"] --> Blume
  Formats["Cloud formats list"] --> Generator["Generate formats page"]
  Generator --> Blume
  Guides["Guides and examples"] --> Blume
  Blume --> Worker["convt-docs Worker"]
  Worker --> Site["convt.app/docs"]
Loading

Reviews (3) · Last reviewed commit: "docs: return job ids and harden copied e..." · Reviewed by Greptile

Comment thread apps/docs/content/examples/batch-folder.mdx Outdated
Comment thread apps/docs/content/examples/node-script.mdx Outdated
Comment thread apps/docs/package.json Outdated
Comment thread apps/docs/openapi/public.yaml Outdated
Comment thread apps/docs/content/quick-start.mdx Outdated
Comment thread apps/docs/content/quick-start.mdx Outdated
Comment thread apps/docs/content/examples/batch-folder.mdx
leoisadev1 and others added 3 commits October 7, 2026 12:51
Replace the single generated /docs/api page in apps/web with a Blume site in
apps/docs: overview, quick start with Node, cURL, Browser and CLI tabs, guides,
reference pages, examples, and an API reference rendered from convt-server's
OpenAPI spec through an overlay. It deploys as the static convt-docs Worker on
convt.app/docs*.

Fixes CNV-38

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Outputs come back named input.<ext>, so the samples now rename them after the
local file instead of overwriting each other. Polling guidance now stays under
120 requests a minute, start before upload is documented as the 502 it returns,
expired jobs are documented as 404s, and the API reference links to the formats
page that exists.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Keep batch conversions under the 120/min job-route limit with one shared
request queue, and make the Railway host a single source of truth for
samples, the OpenAPI overlay and Try it.

Co-authored-by: Leo <leoisadev1@users.noreply.github.com>
@cursor
cursor Bot force-pushed the cnv-38-blume-docs branch from d1e956c to f698dc1 Compare October 7, 2026 12:53
Comment thread apps/docs/content/examples/server-upload-route.mdx Outdated
Comment thread apps/docs/content/examples/node-script.mdx Outdated
Comment thread apps/docs/content/guides/javascript-sdk.mdx Outdated
Comment thread apps/docs/content/examples/server-upload-route.mdx Outdated
Guard the Bun demo server with import.meta.main, keep replace() from
rewriting stems that contain $&, require a string API key in the SDK
sample, and return job.id with outputs so later downloads have an id.

Co-authored-by: Leo <leoisadev1@users.noreply.github.com>
@leoisadev1
leoisadev1 merged commit d64ee1b into main Oct 7, 2026
9 checks passed
@leoisadev1
leoisadev1 deleted the cnv-38-blume-docs branch October 7, 2026 13:17
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.

2 participants