mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-07-27 04:31:57 +00:00
fix(docs): one unified build — every product merged, zero broken links
The docs site fanned out into one CF Pages project per DOCS_SECTION, stitched by
an edge worker whose *.pages.dev origins had gone stale — so every
/docs/projects/* and /docs/services/{iam,kms,platform}/* page returned 530, and
those products were never actually merged into docs.hanzo.ai. Collapse it: one
unified static export → one hanzo-docs Pages project → the router passes every
path straight through.
- source.config.ts: build the whole doc set in one pass (core + every service
incl. iam/kms/platform + every synced OSS project). The next.config
ProjectDocsFallback + remarkPassthroughUnknownJsx neutralise foreign MDX from
ported docs, so a single build is safe.
- workers/docs-router: pass-through to the single hanzo-docs origin — nothing to
stitch, no stale section origin to 530.
- OpenAPI: /docs/openapi/<svc> reference pages are generated at build time from
the specs (gen-openapi-pages.ts wired into pre-build.ts; specs synced by
sync-openapi.sh). The runtime <APIPage> is export-disabled (deep operation
slugs break prerender); static per-service pages export cleanly and can't 530.
The openapi index links to the generated reference pages.
- public/_redirects: alias the console product "Docs" deep-links that live under
/docs/services or /docs/projects so console → docs always resolves.
- deploy-docs.yml: one full export (NEXT_EXPORT=1), ships content + router
together. Remove the four dead per-section deploy workflows.
Verified: full export builds green in one pass under Turbopack (1628 pages) and a
crawl of every internal link from / and /docs resolves 200 — 1320/1320, zero 404,
zero 530. iam/kms/platform/projects all 200 and reachable in nav.
This commit is contained in:
@@ -5,10 +5,6 @@ on:
|
||||
branches: [main, dev]
|
||||
paths:
|
||||
- 'apps/docs/**'
|
||||
- '!apps/docs/content/docs/services/kms/**'
|
||||
- '!apps/docs/content/docs/services/iam/**'
|
||||
- '!apps/docs/content/docs/services/platform/**'
|
||||
- '!apps/docs/content/docs/projects/**'
|
||||
- 'packages/**'
|
||||
- 'pnpm-lock.yaml'
|
||||
- '.github/workflows/deploy-docs.yml'
|
||||
@@ -43,7 +39,8 @@ jobs:
|
||||
|
||||
- run: pnpm build --filter=docs
|
||||
env:
|
||||
DOCS_SECTION: core
|
||||
# One unified static export — core + every service + every OSS project.
|
||||
NEXT_EXPORT: '1'
|
||||
NODE_OPTIONS: '--max-old-space-size=16384'
|
||||
timeout-minutes: 60
|
||||
|
||||
@@ -88,3 +85,14 @@ jobs:
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ steps.kms.outputs.cf_token }}
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ steps.kms.outputs.cf_account }}
|
||||
|
||||
# The edge router (docs.hanzo.ai/*) now passes every path through to the
|
||||
# single hanzo-docs Pages origin. Ship it with the content so the routing
|
||||
# never lags a section split — the stale-origin 530s came from exactly
|
||||
# that drift.
|
||||
- name: Deploy docs-router worker
|
||||
working-directory: workers/docs-router
|
||||
run: pnpm dlx wrangler deploy
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ steps.kms.outputs.cf_token }}
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ steps.kms.outputs.cf_account }}
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
name: Deploy Docs (IAM)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, dev]
|
||||
paths:
|
||||
- 'apps/docs/content/docs/services/iam/**'
|
||||
- '.github/workflows/deploy-iam-docs.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
permissions:
|
||||
contents: read
|
||||
deployments: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: pnpm
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Extend swap for large build
|
||||
run: |
|
||||
sudo swapoff /swapfile 2>/dev/null || true
|
||||
sudo rm -f /swapfile
|
||||
sudo fallocate -l 12G /swapfile
|
||||
sudo chmod 600 /swapfile
|
||||
sudo mkswap /swapfile
|
||||
sudo swapon /swapfile
|
||||
|
||||
- run: pnpm build --filter=docs
|
||||
env:
|
||||
DOCS_SECTION: iam
|
||||
NODE_OPTIONS: '--max-old-space-size=16384'
|
||||
timeout-minutes: 60
|
||||
|
||||
- name: Fetch CF credentials from KMS
|
||||
id: kms
|
||||
env:
|
||||
KMS_CLIENT_ID: ${{ secrets.KMS_CLIENT_ID }}
|
||||
KMS_CLIENT_SECRET: ${{ secrets.KMS_CLIENT_SECRET }}
|
||||
KMS_ENDPOINT: ${{ vars.KMS_ENDPOINT || 'https://kms.hanzo.ai' }}
|
||||
KMS_ORG: ${{ vars.KMS_ORG || 'hanzo' }}
|
||||
KMS_SECRET_ENV: ${{ vars.KMS_SECRET_ENV || 'prod' }}
|
||||
run: |
|
||||
# Canonical Hanzo KMS (/v1/kms): machine-identity login -> bearer,
|
||||
# then read CF Pages deploy creds from org path deploy/<NAME>.
|
||||
# Replaces the decommissioned Infisical /api routes (forwards-only).
|
||||
token=$(curl -fsS "${KMS_ENDPOINT}/v1/kms/auth/login" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "{\"clientId\":\"${KMS_CLIENT_ID}\",\"clientSecret\":\"${KMS_CLIENT_SECRET}\"}" \
|
||||
| jq -r '.accessToken // empty')
|
||||
if [ -z "${token}" ]; then
|
||||
echo "::error::KMS login failed at ${KMS_ENDPOINT}/v1/kms/auth/login (check KMS_CLIENT_ID/KMS_CLIENT_SECRET)."
|
||||
exit 1
|
||||
fi
|
||||
kms_get() {
|
||||
curl -fsS "${KMS_ENDPOINT}/v1/kms/orgs/${KMS_ORG}/secrets/deploy/$1?env=${KMS_SECRET_ENV}" \
|
||||
-H "Authorization: Bearer ${token}" | jq -r '.secret.value // empty'
|
||||
}
|
||||
cf_token=$(kms_get CLOUDFLARE_API_TOKEN)
|
||||
cf_account=$(kms_get CLOUDFLARE_ACCOUNT_ID)
|
||||
if [ -z "${cf_token}" ] || [ -z "${cf_account}" ]; then
|
||||
echo "::error::CF credentials missing in KMS at ${KMS_ORG}/deploy (env ${KMS_SECRET_ENV})."
|
||||
exit 1
|
||||
fi
|
||||
echo "::add-mask::${cf_token}"
|
||||
echo "::add-mask::${cf_account}"
|
||||
echo "cf_token=${cf_token}" >> "$GITHUB_OUTPUT"
|
||||
echo "cf_account=${cf_account}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Deploy to Cloudflare Pages
|
||||
working-directory: apps/docs
|
||||
run: pnpm dlx wrangler pages deploy out --project-name=iam-docs --commit-dirty=true
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ steps.kms.outputs.cf_token }}
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ steps.kms.outputs.cf_account }}
|
||||
@@ -1,84 +0,0 @@
|
||||
name: Deploy Docs (KMS)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, dev]
|
||||
paths:
|
||||
- 'apps/docs/content/docs/services/kms/**'
|
||||
- '.github/workflows/deploy-kms-docs.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
permissions:
|
||||
contents: read
|
||||
deployments: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: pnpm
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Extend swap for large build
|
||||
run: |
|
||||
sudo swapoff /swapfile 2>/dev/null || true
|
||||
sudo rm -f /swapfile
|
||||
sudo fallocate -l 12G /swapfile
|
||||
sudo chmod 600 /swapfile
|
||||
sudo mkswap /swapfile
|
||||
sudo swapon /swapfile
|
||||
|
||||
- run: pnpm build --filter=docs
|
||||
env:
|
||||
DOCS_SECTION: kms
|
||||
NODE_OPTIONS: '--max-old-space-size=16384'
|
||||
timeout-minutes: 60
|
||||
|
||||
- name: Fetch CF credentials from KMS
|
||||
id: kms
|
||||
env:
|
||||
KMS_CLIENT_ID: ${{ secrets.KMS_CLIENT_ID }}
|
||||
KMS_CLIENT_SECRET: ${{ secrets.KMS_CLIENT_SECRET }}
|
||||
KMS_ENDPOINT: ${{ vars.KMS_ENDPOINT || 'https://kms.hanzo.ai' }}
|
||||
KMS_ORG: ${{ vars.KMS_ORG || 'hanzo' }}
|
||||
KMS_SECRET_ENV: ${{ vars.KMS_SECRET_ENV || 'prod' }}
|
||||
run: |
|
||||
# Canonical Hanzo KMS (/v1/kms): machine-identity login -> bearer,
|
||||
# then read CF Pages deploy creds from org path deploy/<NAME>.
|
||||
# Replaces the decommissioned Infisical /api routes (forwards-only).
|
||||
token=$(curl -fsS "${KMS_ENDPOINT}/v1/kms/auth/login" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "{\"clientId\":\"${KMS_CLIENT_ID}\",\"clientSecret\":\"${KMS_CLIENT_SECRET}\"}" \
|
||||
| jq -r '.accessToken // empty')
|
||||
if [ -z "${token}" ]; then
|
||||
echo "::error::KMS login failed at ${KMS_ENDPOINT}/v1/kms/auth/login (check KMS_CLIENT_ID/KMS_CLIENT_SECRET)."
|
||||
exit 1
|
||||
fi
|
||||
kms_get() {
|
||||
curl -fsS "${KMS_ENDPOINT}/v1/kms/orgs/${KMS_ORG}/secrets/deploy/$1?env=${KMS_SECRET_ENV}" \
|
||||
-H "Authorization: Bearer ${token}" | jq -r '.secret.value // empty'
|
||||
}
|
||||
cf_token=$(kms_get CLOUDFLARE_API_TOKEN)
|
||||
cf_account=$(kms_get CLOUDFLARE_ACCOUNT_ID)
|
||||
if [ -z "${cf_token}" ] || [ -z "${cf_account}" ]; then
|
||||
echo "::error::CF credentials missing in KMS at ${KMS_ORG}/deploy (env ${KMS_SECRET_ENV})."
|
||||
exit 1
|
||||
fi
|
||||
echo "::add-mask::${cf_token}"
|
||||
echo "::add-mask::${cf_account}"
|
||||
echo "cf_token=${cf_token}" >> "$GITHUB_OUTPUT"
|
||||
echo "cf_account=${cf_account}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Deploy to Cloudflare Pages
|
||||
working-directory: apps/docs
|
||||
run: pnpm dlx wrangler pages deploy out --project-name=kms-docs --commit-dirty=true
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ steps.kms.outputs.cf_token }}
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ steps.kms.outputs.cf_account }}
|
||||
@@ -1,84 +0,0 @@
|
||||
name: Deploy Docs (Platform)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, dev]
|
||||
paths:
|
||||
- 'apps/docs/content/docs/services/platform/**'
|
||||
- '.github/workflows/deploy-platform-docs.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
permissions:
|
||||
contents: read
|
||||
deployments: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: pnpm
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Extend swap for large build
|
||||
run: |
|
||||
sudo swapoff /swapfile 2>/dev/null || true
|
||||
sudo rm -f /swapfile
|
||||
sudo fallocate -l 12G /swapfile
|
||||
sudo chmod 600 /swapfile
|
||||
sudo mkswap /swapfile
|
||||
sudo swapon /swapfile
|
||||
|
||||
- run: pnpm build --filter=docs
|
||||
env:
|
||||
DOCS_SECTION: platform
|
||||
NODE_OPTIONS: '--max-old-space-size=16384'
|
||||
timeout-minutes: 60
|
||||
|
||||
- name: Fetch CF credentials from KMS
|
||||
id: kms
|
||||
env:
|
||||
KMS_CLIENT_ID: ${{ secrets.KMS_CLIENT_ID }}
|
||||
KMS_CLIENT_SECRET: ${{ secrets.KMS_CLIENT_SECRET }}
|
||||
KMS_ENDPOINT: ${{ vars.KMS_ENDPOINT || 'https://kms.hanzo.ai' }}
|
||||
KMS_ORG: ${{ vars.KMS_ORG || 'hanzo' }}
|
||||
KMS_SECRET_ENV: ${{ vars.KMS_SECRET_ENV || 'prod' }}
|
||||
run: |
|
||||
# Canonical Hanzo KMS (/v1/kms): machine-identity login -> bearer,
|
||||
# then read CF Pages deploy creds from org path deploy/<NAME>.
|
||||
# Replaces the decommissioned Infisical /api routes (forwards-only).
|
||||
token=$(curl -fsS "${KMS_ENDPOINT}/v1/kms/auth/login" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "{\"clientId\":\"${KMS_CLIENT_ID}\",\"clientSecret\":\"${KMS_CLIENT_SECRET}\"}" \
|
||||
| jq -r '.accessToken // empty')
|
||||
if [ -z "${token}" ]; then
|
||||
echo "::error::KMS login failed at ${KMS_ENDPOINT}/v1/kms/auth/login (check KMS_CLIENT_ID/KMS_CLIENT_SECRET)."
|
||||
exit 1
|
||||
fi
|
||||
kms_get() {
|
||||
curl -fsS "${KMS_ENDPOINT}/v1/kms/orgs/${KMS_ORG}/secrets/deploy/$1?env=${KMS_SECRET_ENV}" \
|
||||
-H "Authorization: Bearer ${token}" | jq -r '.secret.value // empty'
|
||||
}
|
||||
cf_token=$(kms_get CLOUDFLARE_API_TOKEN)
|
||||
cf_account=$(kms_get CLOUDFLARE_ACCOUNT_ID)
|
||||
if [ -z "${cf_token}" ] || [ -z "${cf_account}" ]; then
|
||||
echo "::error::CF credentials missing in KMS at ${KMS_ORG}/deploy (env ${KMS_SECRET_ENV})."
|
||||
exit 1
|
||||
fi
|
||||
echo "::add-mask::${cf_token}"
|
||||
echo "::add-mask::${cf_account}"
|
||||
echo "cf_token=${cf_token}" >> "$GITHUB_OUTPUT"
|
||||
echo "cf_account=${cf_account}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Deploy to Cloudflare Pages
|
||||
working-directory: apps/docs
|
||||
run: pnpm dlx wrangler pages deploy out --project-name=platform-docs --commit-dirty=true
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ steps.kms.outputs.cf_token }}
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ steps.kms.outputs.cf_account }}
|
||||
@@ -1,84 +0,0 @@
|
||||
name: Deploy Docs (Projects)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, dev]
|
||||
paths:
|
||||
- 'apps/docs/content/docs/projects/**'
|
||||
- '.github/workflows/deploy-project-docs.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
permissions:
|
||||
contents: read
|
||||
deployments: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: pnpm
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Extend swap for large build
|
||||
run: |
|
||||
sudo swapoff /swapfile 2>/dev/null || true
|
||||
sudo rm -f /swapfile
|
||||
sudo fallocate -l 12G /swapfile
|
||||
sudo chmod 600 /swapfile
|
||||
sudo mkswap /swapfile
|
||||
sudo swapon /swapfile
|
||||
|
||||
- run: pnpm build --filter=docs
|
||||
env:
|
||||
DOCS_SECTION: projects
|
||||
NODE_OPTIONS: '--max-old-space-size=16384'
|
||||
timeout-minutes: 60
|
||||
|
||||
- name: Fetch CF credentials from KMS
|
||||
id: kms
|
||||
env:
|
||||
KMS_CLIENT_ID: ${{ secrets.KMS_CLIENT_ID }}
|
||||
KMS_CLIENT_SECRET: ${{ secrets.KMS_CLIENT_SECRET }}
|
||||
KMS_ENDPOINT: ${{ vars.KMS_ENDPOINT || 'https://kms.hanzo.ai' }}
|
||||
KMS_ORG: ${{ vars.KMS_ORG || 'hanzo' }}
|
||||
KMS_SECRET_ENV: ${{ vars.KMS_SECRET_ENV || 'prod' }}
|
||||
run: |
|
||||
# Canonical Hanzo KMS (/v1/kms): machine-identity login -> bearer,
|
||||
# then read CF Pages deploy creds from org path deploy/<NAME>.
|
||||
# Replaces the decommissioned Infisical /api routes (forwards-only).
|
||||
token=$(curl -fsS "${KMS_ENDPOINT}/v1/kms/auth/login" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "{\"clientId\":\"${KMS_CLIENT_ID}\",\"clientSecret\":\"${KMS_CLIENT_SECRET}\"}" \
|
||||
| jq -r '.accessToken // empty')
|
||||
if [ -z "${token}" ]; then
|
||||
echo "::error::KMS login failed at ${KMS_ENDPOINT}/v1/kms/auth/login (check KMS_CLIENT_ID/KMS_CLIENT_SECRET)."
|
||||
exit 1
|
||||
fi
|
||||
kms_get() {
|
||||
curl -fsS "${KMS_ENDPOINT}/v1/kms/orgs/${KMS_ORG}/secrets/deploy/$1?env=${KMS_SECRET_ENV}" \
|
||||
-H "Authorization: Bearer ${token}" | jq -r '.secret.value // empty'
|
||||
}
|
||||
cf_token=$(kms_get CLOUDFLARE_API_TOKEN)
|
||||
cf_account=$(kms_get CLOUDFLARE_ACCOUNT_ID)
|
||||
if [ -z "${cf_token}" ] || [ -z "${cf_account}" ]; then
|
||||
echo "::error::CF credentials missing in KMS at ${KMS_ORG}/deploy (env ${KMS_SECRET_ENV})."
|
||||
exit 1
|
||||
fi
|
||||
echo "::add-mask::${cf_token}"
|
||||
echo "::add-mask::${cf_account}"
|
||||
echo "cf_token=${cf_token}" >> "$GITHUB_OUTPUT"
|
||||
echo "cf_account=${cf_account}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Deploy to Cloudflare Pages
|
||||
working-directory: apps/docs
|
||||
run: pnpm dlx wrangler pages deploy out --project-name=project-docs --commit-dirty=true
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ steps.kms.outputs.cf_token }}
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ steps.kms.outputs.cf_account }}
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: API Reference
|
||||
description: Complete API reference for all Hanzo Cloud services. Generated from OpenAPI 3.1.0 specifications.
|
||||
index: true
|
||||
icon: BookOpen
|
||||
---
|
||||
|
||||
@@ -38,25 +37,25 @@ Use our official SDKs instead of raw HTTP:
|
||||
## AI & Intelligence
|
||||
|
||||
<Cards>
|
||||
<Card title="Cloud" href="/docs/services/cloud">
|
||||
<Card title="Cloud" href="/docs/openapi/cloud">
|
||||
LLM inference gateway — chat completions, embeddings, image generation, audio
|
||||
</Card>
|
||||
<Card title="Chat" href="/docs/services/chat">
|
||||
<Card title="Chat" href="/docs/openapi/chat">
|
||||
Multi-model AI chat with conversation management and tool use
|
||||
</Card>
|
||||
<Card title="Search" href="/docs/services/search">
|
||||
<Card title="Search" href="/docs/openapi/search">
|
||||
AI-powered search — indexing, query, and generative results
|
||||
</Card>
|
||||
<Card title="Bot" href="/docs/services/bot">
|
||||
<Card title="Bot" href="/docs/openapi/bot">
|
||||
AI assistant framework — skills, channels, webhook integrations
|
||||
</Card>
|
||||
<Card title="Nexus" href="/docs/services/nexus">
|
||||
<Card title="Nexus" href="/docs/openapi/nexus">
|
||||
RAG pipeline — document ingestion, chunking, retrieval
|
||||
</Card>
|
||||
<Card title="Vector" href="/docs/services/vector">
|
||||
<Card title="Vector" href="/docs/openapi/vector">
|
||||
Vector database — collections, embeddings, similarity search
|
||||
</Card>
|
||||
<Card title="ML" href="/docs/services/ml">
|
||||
<Card title="ML" href="/docs/openapi/ml">
|
||||
MLOps — model registry, training jobs, deployment
|
||||
</Card>
|
||||
</Cards>
|
||||
@@ -64,13 +63,13 @@ Use our official SDKs instead of raw HTTP:
|
||||
## Automation
|
||||
|
||||
<Cards>
|
||||
<Card title="Flow" href="/docs/services/flow">
|
||||
<Card title="Flow" href="/docs/openapi/flow">
|
||||
Visual workflow builder — triggers, actions, branching logic
|
||||
</Card>
|
||||
<Card title="Auto" href="/docs/services/auto">
|
||||
<Card title="Auto" href="/docs/openapi/auto">
|
||||
Event-driven automation — schedules, webhooks, task chains
|
||||
</Card>
|
||||
<Card title="Operative" href="/docs/services/operative">
|
||||
<Card title="Operative" href="/docs/openapi/operative">
|
||||
Computer use agent — browser automation, desktop control
|
||||
</Card>
|
||||
</Cards>
|
||||
@@ -78,16 +77,16 @@ Use our official SDKs instead of raw HTTP:
|
||||
## Identity & Security
|
||||
|
||||
<Cards>
|
||||
<Card title="IAM" href="/docs/services/iam">
|
||||
<Card title="IAM" href="/docs/openapi/iam">
|
||||
Identity — users, organizations, SSO, OIDC, OAuth2, SAML
|
||||
</Card>
|
||||
<Card title="DID" href="/docs/services/did">
|
||||
<Card title="DID" href="/docs/openapi/did">
|
||||
Decentralized identity — W3C DIDs, verifiable credentials
|
||||
</Card>
|
||||
<Card title="Guard" href="/docs/services/guard">
|
||||
<Card title="Guard" href="/docs/openapi/guard">
|
||||
AI safety — PII redaction, prompt injection detection, content filtering
|
||||
</Card>
|
||||
<Card title="KMS" href="/docs/services/kms">
|
||||
<Card title="KMS" href="/docs/openapi/kms">
|
||||
Secrets management — encryption keys, certificates, secret sync
|
||||
</Card>
|
||||
</Cards>
|
||||
@@ -95,16 +94,16 @@ Use our official SDKs instead of raw HTTP:
|
||||
## Commerce & Business
|
||||
|
||||
<Cards>
|
||||
<Card title="Commerce" href="/docs/services/commerce">
|
||||
<Card title="Commerce" href="/docs/openapi/commerce">
|
||||
Headless commerce — products, orders, subscriptions, payments
|
||||
</Card>
|
||||
<Card title="Console" href="/docs/services/console">
|
||||
<Card title="Console" href="/docs/openapi/console">
|
||||
LLM observability — tracing, prompt management, evaluations
|
||||
</Card>
|
||||
<Card title="Analytics" href="/docs/services/analytics">
|
||||
<Card title="Analytics" href="/docs/openapi/analytics">
|
||||
Usage metrics — API calls, cost tracking, per-model analytics
|
||||
</Card>
|
||||
<Card title="Pricing" href="/docs/services/pricing">
|
||||
<Card title="Pricing" href="/docs/openapi/pricing">
|
||||
Pricing API — model costs, subscription plans, GPU tiers
|
||||
</Card>
|
||||
</Cards>
|
||||
@@ -112,40 +111,40 @@ Use our official SDKs instead of raw HTTP:
|
||||
## Infrastructure
|
||||
|
||||
<Cards>
|
||||
<Card title="Gateway" href="/docs/services/gateway">
|
||||
<Card title="Gateway" href="/docs/openapi/gateway">
|
||||
API gateway — routing, rate limiting, circuit breakers
|
||||
</Card>
|
||||
<Card title="PaaS" href="/docs/services/paas">
|
||||
<Card title="PaaS" href="/docs/openapi/paas">
|
||||
Platform-as-a-Service — deployments, domains, databases
|
||||
</Card>
|
||||
<Card title="Platform" href="/docs/services/platform">
|
||||
<Card title="Platform" href="/docs/openapi/platform">
|
||||
Application hosting — Git push deploy, scaling, logs
|
||||
</Card>
|
||||
<Card title="S3" href="/docs/services/s3">
|
||||
<Card title="S3" href="/docs/openapi/s3">
|
||||
Object storage — S3-compatible buckets, presigned URLs
|
||||
</Card>
|
||||
<Card title="DB" href="/docs/services/db">
|
||||
<Card title="DB" href="/docs/openapi/db">
|
||||
Serverless PostgreSQL — branches, pgvector, connection pooling
|
||||
</Card>
|
||||
<Card title="KV" href="/docs/services/kv">
|
||||
<Card title="KV" href="/docs/openapi/kv">
|
||||
Key-value store — Redis/Valkey-compatible, pub/sub, streams
|
||||
</Card>
|
||||
<Card title="MQ" href="/docs/services/mq">
|
||||
<Card title="MQ" href="/docs/openapi/mq">
|
||||
Job queue — background tasks, retries, dead-letter queues
|
||||
</Card>
|
||||
<Card title="Stream" href="/docs/services/stream">
|
||||
<Card title="Stream" href="/docs/openapi/stream">
|
||||
Kafka-compatible streaming — topics, consumers, partitions
|
||||
</Card>
|
||||
<Card title="PubSub" href="/docs/services/pubsub">
|
||||
<Card title="PubSub" href="/docs/openapi/pubsub">
|
||||
Pub/sub messaging — JetStream, subjects, durable consumers
|
||||
</Card>
|
||||
<Card title="Edge" href="/docs/services/edge">
|
||||
<Card title="Edge" href="/docs/openapi/edge">
|
||||
Edge functions — serverless compute at 300+ locations
|
||||
</Card>
|
||||
<Card title="Registry" href="/docs/services/registry">
|
||||
<Card title="Registry" href="/docs/openapi/registry">
|
||||
Container registry — Docker images, OCI artifacts, scanning
|
||||
</Card>
|
||||
<Card title="Visor" href="/docs/services/visor">
|
||||
<Card title="Visor" href="/docs/openapi/visor">
|
||||
Cluster management — Kubernetes monitoring, resource visualization
|
||||
</Card>
|
||||
</Cards>
|
||||
@@ -153,16 +152,16 @@ Use our official SDKs instead of raw HTTP:
|
||||
## Operations
|
||||
|
||||
<Cards>
|
||||
<Card title="Engine" href="/docs/services/engine">
|
||||
<Card title="Engine" href="/docs/openapi/engine">
|
||||
Inference engine — GPU scheduling, quantization, model loading
|
||||
</Card>
|
||||
<Card title="O11y" href="/docs/services/o11y">
|
||||
<Card title="O11y" href="/docs/openapi/o11y">
|
||||
Observability — metrics, logs, traces, alerts, dashboards
|
||||
</Card>
|
||||
<Card title="DNS" href="/docs/services/dns">
|
||||
<Card title="DNS" href="/docs/openapi/dns">
|
||||
DNS management — zones, records, Cloudflare integration
|
||||
</Card>
|
||||
<Card title="ZT" href="/docs/services/zt">
|
||||
<Card title="ZT" href="/docs/openapi/zt">
|
||||
Zero-trust networking — tunnels, access policies, service mesh
|
||||
</Card>
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"title": "API Reference",
|
||||
"description": "REST API reference for every Hanzo service, generated from OpenAPI specs.",
|
||||
"pages": [
|
||||
"index",
|
||||
"analytics",
|
||||
"auto",
|
||||
"bot",
|
||||
"chat",
|
||||
"cloud",
|
||||
"commerce",
|
||||
"console",
|
||||
"db",
|
||||
"did",
|
||||
"dns",
|
||||
"edge",
|
||||
"engine",
|
||||
"flow",
|
||||
"gateway",
|
||||
"guard",
|
||||
"iam",
|
||||
"kms",
|
||||
"kv",
|
||||
"ml",
|
||||
"mq",
|
||||
"nexus",
|
||||
"o11y",
|
||||
"operative",
|
||||
"paas",
|
||||
"platform",
|
||||
"pricing",
|
||||
"pubsub",
|
||||
"registry",
|
||||
"s3",
|
||||
"search",
|
||||
"stream",
|
||||
"vector",
|
||||
"visor",
|
||||
"zt"
|
||||
]
|
||||
}
|
||||
@@ -77,7 +77,8 @@
|
||||
"tailwind-merge": "^3.6.0",
|
||||
"twoslash": "^0.3.8",
|
||||
"unist-util-visit": "^5.1.0",
|
||||
"zod": "^4.4.3"
|
||||
"zod": "^4.4.3",
|
||||
"yaml": "^2.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@hanzo/docs-cli": "workspace:*",
|
||||
|
||||
@@ -30,3 +30,19 @@
|
||||
/mcp /docs/mcp 301
|
||||
/sdks /docs/sdks 301
|
||||
/llm /docs/llm 301
|
||||
|
||||
# Console product "Docs" deep-links (console registry uses /docs/<product>).
|
||||
# Where the canonical page lives under /docs/services or /docs/projects, alias it
|
||||
# so the console's Docs button always resolves.
|
||||
/docs/analytics /docs/services/analytics 301
|
||||
/docs/auto /docs/services/auto 301
|
||||
/docs/cms /docs/projects/hanzoai/cms 301
|
||||
/docs/helpdesk /docs/projects/hanzoai/helpdesk 301
|
||||
/docs/apm /docs/services/o11y 301
|
||||
/docs/apps /docs/services/paas 301
|
||||
/docs/nodes /docs/services/visor 301
|
||||
|
||||
# Console module "Docs" buttons that link bare product paths (docsUrl + '/x').
|
||||
/crawl /docs/services/search 301
|
||||
/functions /docs/functions 301
|
||||
/kms /docs/services/kms 301
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { parse as parseYaml } from 'yaml';
|
||||
|
||||
// Generate one static MDX API-reference page per OpenAPI spec.
|
||||
//
|
||||
// The specs (openapi-specs/*.yaml) are synced from hanzoai/openapi by
|
||||
// scripts/sync-openapi.sh and are .gitignored, as are the generated pages
|
||||
// (content/docs/openapi/**/*.mdx — except the tracked index.mdx). We render a
|
||||
// self-contained static page (operations grouped by tag) rather than the
|
||||
// runtime <APIPage>, because the interactive loader is disabled in static
|
||||
// export (deep operation slugs break prerender). Pages are pure MDX, so they
|
||||
// export cleanly and can never 530.
|
||||
|
||||
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
||||
const APP_ROOT = path.resolve(SCRIPT_DIR, '..');
|
||||
const SPECS_DIR = path.join(APP_ROOT, 'openapi-specs');
|
||||
const OUT_DIR = path.join(APP_ROOT, 'content/docs/openapi');
|
||||
const METHODS = ['get', 'post', 'put', 'patch', 'delete', 'options', 'head'];
|
||||
|
||||
// Inline code (inside backticks): only the GFM table pipe needs escaping.
|
||||
const codeEsc = (s: unknown): string =>
|
||||
String(s ?? '').replace(/[\r\n]+/g, ' ').trim().replace(/\|/g, '\\|');
|
||||
|
||||
// Plain table/heading text: neutralise MDX ({ }) and HTML (< >) + table pipe.
|
||||
const textEsc = (s: unknown): string =>
|
||||
String(s ?? '')
|
||||
.replace(/[\r\n]+/g, ' ')
|
||||
.trim()
|
||||
.replace(/\|/g, '\\|')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/\{/g, '{')
|
||||
.replace(/\}/g, '}');
|
||||
|
||||
const firstLine = (s: unknown): string =>
|
||||
String(s ?? '').split('\n')[0].trim().slice(0, 200);
|
||||
|
||||
function syncSpecs(): void {
|
||||
try {
|
||||
execFileSync('bash', [path.join(SCRIPT_DIR, 'sync-openapi.sh')], {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn('[gen-openapi-pages] sync-openapi.sh failed; using existing specs', e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function genOpenapiPages(): Promise<void> {
|
||||
syncSpecs();
|
||||
if (!fs.existsSync(SPECS_DIR)) {
|
||||
console.warn('[gen-openapi-pages] no specs dir; skipping');
|
||||
return;
|
||||
}
|
||||
fs.mkdirSync(OUT_DIR, { recursive: true });
|
||||
|
||||
const specFiles = fs
|
||||
.readdirSync(SPECS_DIR)
|
||||
.filter((f) => f.endsWith('.yaml') && f !== 'hanzo.yaml')
|
||||
.sort();
|
||||
|
||||
const services: string[] = [];
|
||||
|
||||
for (const file of specFiles) {
|
||||
const svc = file.replace(/\.yaml$/, '');
|
||||
let spec: any;
|
||||
try {
|
||||
spec = parseYaml(fs.readFileSync(path.join(SPECS_DIR, file), 'utf8'));
|
||||
} catch (e) {
|
||||
console.warn(`[gen-openapi-pages] skip ${svc}: parse error`, e);
|
||||
continue;
|
||||
}
|
||||
if (!spec || typeof spec !== 'object') continue;
|
||||
|
||||
const info = spec.info ?? {};
|
||||
const title = info.title || `${svc} API`;
|
||||
const version = info.version || '';
|
||||
const desc = firstLine(info.description);
|
||||
const servers = Array.isArray(spec.servers) ? spec.servers : [];
|
||||
const serverUrl = servers[0]?.url || 'https://api.hanzo.ai';
|
||||
|
||||
const schemes = spec.components?.securitySchemes ?? {};
|
||||
const authLines: string[] = [];
|
||||
for (const [name, sc] of Object.entries<any>(schemes)) {
|
||||
if (!sc || typeof sc !== 'object') continue;
|
||||
if (sc.type === 'http') authLines.push(`\`${name}\` — HTTP ${sc.scheme ?? ''}`);
|
||||
else if (sc.type === 'apiKey')
|
||||
authLines.push(`\`${name}\` — API key in ${sc.in ?? ''} (\`${sc.name ?? ''}\`)`);
|
||||
else if (sc.type === 'oauth2') authLines.push(`\`${name}\` — OAuth 2.0`);
|
||||
else authLines.push(`\`${name}\` — ${sc.type ?? ''}`);
|
||||
}
|
||||
|
||||
const byTag = new Map<string, Array<[string, string, string]>>();
|
||||
let total = 0;
|
||||
for (const [p, item] of Object.entries<any>(spec.paths ?? {})) {
|
||||
if (!item || typeof item !== 'object') continue;
|
||||
for (const m of METHODS) {
|
||||
const op = item[m];
|
||||
if (!op || typeof op !== 'object') continue;
|
||||
total++;
|
||||
const tag = (Array.isArray(op.tags) && op.tags[0]) || 'General';
|
||||
const summary = op.summary || op.operationId || '';
|
||||
if (!byTag.has(tag)) byTag.set(tag, []);
|
||||
byTag.get(tag)!.push([m.toUpperCase(), p, summary]);
|
||||
}
|
||||
}
|
||||
|
||||
const L: string[] = [];
|
||||
L.push('---');
|
||||
L.push(`title: ${title}`);
|
||||
L.push(`description: ${JSON.stringify(desc || `REST API reference for ${title}.`).slice(1, -1)}`);
|
||||
L.push('---');
|
||||
L.push('');
|
||||
if (desc) L.push(desc);
|
||||
L.push('');
|
||||
L.push('| | |');
|
||||
L.push('|---|---|');
|
||||
L.push(`| **Base URL** | \`${serverUrl}\` |`);
|
||||
if (version) L.push(`| **Version** | ${textEsc(version)} |`);
|
||||
L.push(`| **Operations** | ${total} |`);
|
||||
L.push('');
|
||||
if (authLines.length) {
|
||||
L.push('## Authentication');
|
||||
L.push('');
|
||||
for (const a of authLines) L.push(`- ${a}`);
|
||||
L.push('');
|
||||
}
|
||||
L.push('```bash');
|
||||
L.push(`curl -H "Authorization: Bearer $HANZO_API_KEY" ${serverUrl}`);
|
||||
L.push('```');
|
||||
L.push('');
|
||||
L.push('## Endpoints');
|
||||
L.push('');
|
||||
if (total === 0) L.push('_No documented operations in this specification yet._');
|
||||
for (const tag of [...byTag.keys()].sort()) {
|
||||
L.push(`### ${textEsc(tag)}`);
|
||||
L.push('');
|
||||
L.push('| Method | Endpoint | Description |');
|
||||
L.push('|--------|----------|-------------|');
|
||||
for (const [method, p, summary] of byTag.get(tag)!) {
|
||||
L.push(`| \`${method}\` | \`${codeEsc(p)}\` | ${textEsc(summary)} |`);
|
||||
}
|
||||
L.push('');
|
||||
}
|
||||
fs.writeFileSync(path.join(OUT_DIR, `${svc}.mdx`), L.join('\n'));
|
||||
services.push(svc);
|
||||
}
|
||||
|
||||
const meta = {
|
||||
title: 'API Reference',
|
||||
description:
|
||||
'REST API reference for every Hanzo service, generated from OpenAPI specs.',
|
||||
pages: ['index', ...services],
|
||||
};
|
||||
fs.writeFileSync(path.join(OUT_DIR, 'meta.json'), JSON.stringify(meta, null, 2) + '\n');
|
||||
console.log(`[gen-openapi-pages] generated ${services.length} API reference pages`);
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
genOpenapiPages().catch((e) => {
|
||||
console.error('[gen-openapi-pages] failed', e);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
import { buildRegistry } from '@/scripts/build-registry';
|
||||
import { genOpenapiPages } from './gen-openapi-pages';
|
||||
import { syncProjectDocs } from './sync-project-docs';
|
||||
|
||||
async function main() {
|
||||
const tasks = [buildRegistry()];
|
||||
const tasks = [buildRegistry(), genOpenapiPages()];
|
||||
if (process.env.HANZO_DOCS_SYNC !== '0') {
|
||||
tasks.push(syncProjectDocs());
|
||||
}
|
||||
|
||||
+11
-20
@@ -15,26 +15,17 @@ const isLint = process.env.LINT === '1';
|
||||
|
||||
const isExport = process.env.NEXT_EXPORT === '1';
|
||||
|
||||
// Federated docs: DOCS_SECTION controls which MDX pages are built.
|
||||
// Each section deploys to its own CF Pages project; meta.json files are
|
||||
// always included so every build renders the complete sidebar tree.
|
||||
const section = process.env.DOCS_SECTION as 'core' | 'kms' | 'iam' | 'platform' | 'projects' | undefined;
|
||||
|
||||
const sectionFilters: Record<string, string[]> = {
|
||||
core: ['**/*.mdx', '!**/projects/**', '!**/services/kms/**', '!**/services/iam/**', '!**/services/platform/**'],
|
||||
kms: ['**/services/kms/**/*.mdx'],
|
||||
iam: ['**/services/iam/**/*.mdx'],
|
||||
platform: ['**/services/platform/**/*.mdx'],
|
||||
projects: ['**/projects/**/*.mdx'],
|
||||
};
|
||||
|
||||
function getDocsFiles(): string[] | undefined {
|
||||
if (section && sectionFilters[section]) return sectionFilters[section];
|
||||
// Default export build = core: exclude the federated sections (kms/iam/platform/
|
||||
// projects). They deploy from their own DOCS_SECTION builds and carry foreign
|
||||
// MDX refs (useTranslations, etc.) that don't resolve in the core app.
|
||||
if (isExport) return sectionFilters.core;
|
||||
return undefined;
|
||||
// One unified build. The whole doc set — core pages, every service (incl. iam,
|
||||
// kms, platform) and every synced OSS project — is compiled in a single pass and
|
||||
// deployed to a single CF Pages project (`hanzo-docs`). The previous federated
|
||||
// layout (one Pages project per DOCS_SECTION, stitched by an edge worker) is
|
||||
// retired: it left stale `*.pages.dev` origins that 530'd whole sections.
|
||||
// Foreign MDX from ported upstream docs is neutralised by next.config's
|
||||
// ProjectDocsFallback plugin + remarkPassthroughUnknownJsx (both cover
|
||||
// content/docs/projects/** and content/docs/services/**), so a single build is
|
||||
// safe. meta.json files are always included so the sidebar tree is complete.
|
||||
function getDocsFiles(): string[] {
|
||||
return ['**/*.mdx'];
|
||||
}
|
||||
|
||||
export const docs = defineDocs({
|
||||
|
||||
@@ -1,70 +1,16 @@
|
||||
interface Route {
|
||||
prefix: string;
|
||||
origin: string;
|
||||
assetPrefix?: string;
|
||||
}
|
||||
|
||||
const ROUTES: Route[] = [
|
||||
{ prefix: '/docs/services/kms', origin: 'kms-docs.pages.dev', assetPrefix: '/__kms' },
|
||||
{ prefix: '/docs/services/iam', origin: 'iam-docs-s4h.pages.dev', assetPrefix: '/__iam' },
|
||||
{ prefix: '/docs/services/platform', origin: 'platform-docs-djx.pages.dev', assetPrefix: '/__platform' },
|
||||
{ prefix: '/docs/projects', origin: 'project-docs-5r8.pages.dev', assetPrefix: '/__projects' },
|
||||
];
|
||||
|
||||
// docs.hanzo.ai edge router.
|
||||
//
|
||||
// The docs site is a single Cloudflare Pages project (`hanzo-docs`) built in one
|
||||
// unified pass — core pages, every service and every synced OSS project. The
|
||||
// former federated layout (one Pages project per section, stitched here by path
|
||||
// prefix) is retired: its `*.pages.dev` origins went stale and 530'd whole
|
||||
// sections (/docs/projects/*, /docs/services/{iam,kms,platform}/*). One origin,
|
||||
// one build — nothing to stitch, nothing to go stale.
|
||||
const MAIN_ORIGIN = 'hanzo-docs.pages.dev';
|
||||
|
||||
/**
|
||||
* Determine which section origin a request belongs to based on the Referer header.
|
||||
* When a browser loads /_next/ assets, the Referer is the page URL that triggered the load.
|
||||
*/
|
||||
function originFromReferer(request: Request): string | null {
|
||||
const referer = request.headers.get('Referer');
|
||||
if (!referer) return null;
|
||||
try {
|
||||
const refUrl = new URL(referer);
|
||||
for (const route of ROUTES) {
|
||||
if (refUrl.pathname.startsWith(route.prefix)) return route.origin;
|
||||
}
|
||||
} catch {}
|
||||
return null;
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(request: Request): Promise<Response> {
|
||||
const url = new URL(request.url);
|
||||
|
||||
// Asset prefix routing: /__kms/_next/... → kms-docs.pages.dev/_next/...
|
||||
for (const route of ROUTES) {
|
||||
if (route.assetPrefix && url.pathname.startsWith(route.assetPrefix + '/')) {
|
||||
const path = url.pathname.slice(route.assetPrefix.length);
|
||||
return fetch(new Request(`https://${route.origin}${path}${url.search}`, request));
|
||||
}
|
||||
}
|
||||
|
||||
// Page routing: /docs/services/kms/... → kms-docs.pages.dev/docs/services/kms/...
|
||||
for (const route of ROUTES) {
|
||||
if (url.pathname.startsWith(route.prefix)) {
|
||||
return fetch(new Request(`https://${route.origin}${url.pathname}${url.search}`, request));
|
||||
}
|
||||
}
|
||||
|
||||
// Static asset routing: /_next/ chunks, CSS, fonts, etc.
|
||||
// Each section build produces unique layout/page chunks that only exist on
|
||||
// that section's CF Pages origin. Use the Referer header to route assets
|
||||
// to the origin that built the page the browser is rendering.
|
||||
if (url.pathname.startsWith('/_next/')) {
|
||||
const sectionOrigin = originFromReferer(request);
|
||||
if (sectionOrigin) {
|
||||
const res = await fetch(
|
||||
new Request(`https://${sectionOrigin}${url.pathname}${url.search}`, request),
|
||||
);
|
||||
if (res.ok) return res;
|
||||
// Asset missing on section origin — fall through to main origin.
|
||||
// This handles shared framework chunks that may only exist on main.
|
||||
}
|
||||
}
|
||||
|
||||
// Default: main docs
|
||||
return fetch(new Request(`https://${MAIN_ORIGIN}${url.pathname}${url.search}`, request));
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user