diff --git a/.github/workflows/docs-preview-main.yml b/.github/workflows/docs-preview-main.yml
new file mode 100644
index 0000000000..7a2bdfa793
--- /dev/null
+++ b/.github/workflows/docs-preview-main.yml
@@ -0,0 +1,55 @@
+# Refreshes the persistent Mintlify preview for `main` so the team can see the
+# current "next" state of the docs at superdoc-main.mintlify.app without
+# opening a draft PR. Mintlify reuses the URL per branch, so each push
+# refreshes content in place. Production docs stay on docs-stable; this is
+# unrelated to the production gate.
+name: 📖 Docs preview (main)
+
+on:
+ push:
+ branches:
+ - main
+ paths:
+ - 'apps/docs/**'
+ - 'packages/document-api/src/contract/**'
+ - 'scripts/generate-all.mjs'
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: refresh-main-docs-preview
+ cancel-in-progress: true
+
+jobs:
+ trigger-preview:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Trigger Mintlify preview for main
+ env:
+ MINTLIFY_API_KEY: ${{ secrets.MINTLIFY_API_KEY }}
+ MINTLIFY_PROJECT_ID: ${{ secrets.MINTLIFY_PROJECT_ID }}
+ run: |
+ set -euo pipefail
+ if [ -z "${MINTLIFY_API_KEY}" ] || [ -z "${MINTLIFY_PROJECT_ID}" ]; then
+ echo "MINTLIFY_API_KEY or MINTLIFY_PROJECT_ID not set; skipping."
+ exit 0
+ fi
+
+ response=$(curl -sS -X POST \
+ -H "Authorization: Bearer ${MINTLIFY_API_KEY}" \
+ -H "Content-Type: application/json" \
+ -d '{"branch": "main"}' \
+ "https://api.mintlify.com/v1/project/preview/${MINTLIFY_PROJECT_ID}")
+
+ echo "Mintlify response: ${response}"
+ preview_url=$(echo "${response}" | jq -r '.previewUrl // empty')
+
+ if [ -n "$preview_url" ]; then
+ {
+ echo "## Next docs preview refreshed"
+ echo
+ echo "URL: $preview_url"
+ } >> "$GITHUB_STEP_SUMMARY"
+ fi
diff --git a/.github/workflows/docs-preview-pr.yml b/.github/workflows/docs-preview-pr.yml
new file mode 100644
index 0000000000..572b8bc77d
--- /dev/null
+++ b/.github/workflows/docs-preview-pr.yml
@@ -0,0 +1,76 @@
+# Triggers a Mintlify preview deployment for docs-touching PRs targeting
+# `main` or `stable`. The `stable` case covers promote-stable PRs so reviewers
+# see the docs that will go live the moment the release succeeds. The `main`
+# case gives doc authors a hosted preview during normal development, since
+# Mintlify's automatic previews only fire on PRs targeting the deployment
+# branch (`docs-stable`). Path filter keeps the 5 req/min Mintlify API quota
+# from being spent on unrelated code PRs.
+name: 📖 Docs preview (PR)
+
+on:
+ pull_request:
+ types: [opened, reopened, synchronize]
+ branches:
+ - main
+ - stable
+ paths:
+ - 'apps/docs/**'
+ - 'packages/document-api/src/contract/**'
+ - 'scripts/generate-all.mjs'
+
+permissions:
+ pull-requests: write
+
+concurrency:
+ group: preview-stable-docs-${{ github.event.pull_request.number }}
+ cancel-in-progress: true
+
+jobs:
+ trigger-preview:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Trigger Mintlify preview
+ id: preview
+ env:
+ MINTLIFY_API_KEY: ${{ secrets.MINTLIFY_API_KEY }}
+ MINTLIFY_PROJECT_ID: ${{ secrets.MINTLIFY_PROJECT_ID }}
+ BRANCH: ${{ github.head_ref }}
+ run: |
+ set -euo pipefail
+ if [ -z "${MINTLIFY_API_KEY}" ] || [ -z "${MINTLIFY_PROJECT_ID}" ]; then
+ echo "MINTLIFY_API_KEY or MINTLIFY_PROJECT_ID not set; skipping."
+ echo "preview_url=" >> "$GITHUB_OUTPUT"
+ exit 0
+ fi
+
+ response=$(curl -sS -X POST \
+ -H "Authorization: Bearer ${MINTLIFY_API_KEY}" \
+ -H "Content-Type: application/json" \
+ -d "{\"branch\": \"${BRANCH}\"}" \
+ "https://api.mintlify.com/v1/project/preview/${MINTLIFY_PROJECT_ID}")
+
+ echo "Mintlify response: ${response}"
+ preview_url=$(echo "${response}" | jq -r '.previewUrl // empty')
+ echo "preview_url=${preview_url}" >> "$GITHUB_OUTPUT"
+
+ # Comment once per PR. Mintlify reuses the same URL for the same branch
+ # on redeploys, so subsequent pushes refresh content in place at the same
+ # URL. We can't gate on `action == 'opened'` because the workflow may
+ # first run on a `synchronize` event (e.g., when this workflow file
+ # didn't exist on the base branch when the PR was opened), so we check
+ # for an existing preview comment instead.
+ - name: Comment preview URL on PR
+ if: steps.preview.outputs.preview_url != ''
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ PR_NUMBER: ${{ github.event.pull_request.number }}
+ PREVIEW_URL: ${{ steps.preview.outputs.preview_url }}
+ run: |
+ set -euo pipefail
+ existing=$(gh pr view "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --json comments \
+ --jq '[.comments[] | select(.body | startswith("📖 Docs preview:"))] | length')
+ if [ "$existing" -gt 0 ]; then
+ echo "Preview comment already exists; skipping."
+ exit 0
+ fi
+ gh pr comment "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --body "📖 Docs preview: ${PREVIEW_URL}"
diff --git a/.github/workflows/release-stable.yml b/.github/workflows/release-stable.yml
index 08611c6a1a..15a9c56422 100644
--- a/.github/workflows/release-stable.yml
+++ b/.github/workflows/release-stable.yml
@@ -171,3 +171,16 @@ jobs:
with:
packages-dir: packages/sdk/langs/python/dist/
skip-existing: true
+
+ # Stable docs deploy from `docs-stable`. The orchestrator emits
+ # `promote_sha` only when the run released (or no-op'd) on the SHA it
+ # checked out; it stays empty on failure or deferral, so production docs
+ # never advance past a commit whose packages were not actually published.
+ - name: Promote stable docs
+ if: steps.stable_release.outputs.promote_sha != ''
+ env:
+ GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
+ PROMOTE_SHA: ${{ steps.stable_release.outputs.promote_sha }}
+ run: |
+ set -euo pipefail
+ git push origin "${PROMOTE_SHA}:refs/heads/docs-stable"
diff --git a/apps/docs/getting-started/introduction.mdx b/apps/docs/getting-started/introduction.mdx
index a7bd32e415..ce05d258fb 100644
--- a/apps/docs/getting-started/introduction.mdx
+++ b/apps/docs/getting-started/introduction.mdx
@@ -28,14 +28,14 @@ SuperDoc renders and edits DOCX files directly in the browser. Built on OOXML
icon="download"
href="/getting-started/installation"
>
- Get started in minutes
+ Get started in seconds
- Headless mode and LLM workflows
+ Headless mode and agent workflows
r.status === 'released');
if (anyReleased && !isDryRun) {