From 14bb1f27d3615852270b36dfa14c14d6afe669a6 Mon Sep 17 00:00:00 2001 From: Nick Vigilante Date: Fri, 2 Oct 2026 10:30:25 -0400 Subject: [PATCH] docs: add redirects.json and rebuild the docs site when it changes (#30208) --- .claude/docs/DOCS_STYLE_GUIDE.md | 8 ++--- .claude/skills/doc-check/SKILL.md | 10 +++--- .claude/skills/write-docs/SKILL.md | 6 ++-- .github/workflows/deploy-docs.yaml | 54 +++++++++++++++++++++++------- docs/.style/content-guidelines.md | 16 ++++++--- docs/redirects.json | 1 + 6 files changed, 67 insertions(+), 28 deletions(-) create mode 100644 docs/redirects.json diff --git a/.claude/docs/DOCS_STYLE_GUIDE.md b/.claude/docs/DOCS_STYLE_GUIDE.md index c71f310bf5..290ac86f7d 100644 --- a/.claude/docs/DOCS_STYLE_GUIDE.md +++ b/.claude/docs/DOCS_STYLE_GUIDE.md @@ -359,12 +359,12 @@ Don't manually edit auto-generated sections. When renaming or moving documentation pages, redirects must be added to prevent broken links. -**Important**: Redirects are NOT configured in this repository. The coder.com website runs on Vercel with Next.js and reads redirects from a separate repository: +Redirects are configured in `docs/redirects.json` in this repository. The coder.com website runs on Vercel with Next.js, reads the file at build time, and rebuilds when it changes. -- **Redirect configuration**: https://github.com/coder/coder.com/blob/master/redirects.json +- **Redirect configuration**: `docs/redirects.json`, a JSON array of `{"source", "destination", "permanent"}` rules. Both paths are full `/docs/...` paths, `permanent` is optional and defaults to `true`, and a destination may be an external URL. A source is an exact path, a path ending in `/:path*`, or a path with a `:slug(.*)` capture. - **Do NOT create** a `docs/_redirects` file - this format (used by Netlify/Cloudflare Pages) is not processed by coder.com -When you rename or move a doc page, create a PR in coder/coder.com to add the redirect. +When you rename or move a doc page, add the rule to `docs/redirects.json` in the same PR. Each release branch has its own copy, and the unversioned docs are served from the mainline release branch, so backport the move and its redirect together. ## Key Principles @@ -375,4 +375,4 @@ When you rename or move a doc page, create a PR in coder/coder.com to add the re 3. **Screenshots** - Include one only when the topic needs it; refer to [Screenshot policy](#screenshot-policy) 4. **Link everything** - Related docs, API endpoints, CLI references 5. **Manifest inclusion** - Add to `docs/manifest/` for navigation -6. **Add redirects** - When moving/renaming pages, add redirects in coder/coder.com repo +6. **Add redirects** - When moving/renaming pages, add a rule to `docs/redirects.json` in the same PR diff --git a/.claude/skills/doc-check/SKILL.md b/.claude/skills/doc-check/SKILL.md index 2d2f84bd7d..bafa47512f 100644 --- a/.claude/skills/doc-check/SKILL.md +++ b/.claude/skills/doc-check/SKILL.md @@ -299,13 +299,13 @@ violations it generates or suggests. ### Renames and moves require redirects Redirects for [coder.com/docs](https://coder.com/docs) are configured in -a separate repo, not in this one. When a doc page is renamed or moved: +`docs/redirects.json` in this repo, and the website rebuilds when the file +changes. When a doc page is renamed or moved: 1. Update every link that relies on the old location. -2. Add an entry to - [`coder/coder.com:redirects.json`](https://github.com/coder/coder.com/blob/master/redirects.json) - that maps the old path to the new one. Open that PR alongside the - `coder/coder` rename PR. +2. Add a rule to `docs/redirects.json` that maps the old path to the new + one, in the same PR as the rename. For example: + `{"source": "/docs/old/path", "destination": "/docs/new/path"}`. Do not create a `docs/_redirects` file in this repo; that format isn't processed by coder.com. diff --git a/.claude/skills/write-docs/SKILL.md b/.claude/skills/write-docs/SKILL.md index a4ff4fb327..385252ca68 100644 --- a/.claude/skills/write-docs/SKILL.md +++ b/.claude/skills/write-docs/SKILL.md @@ -146,8 +146,8 @@ application notes the canonical rules do not spell out: - On a rename, pick the new link target by the specific page each sentence promises, not just the section hub, and confirm moved anchors still resolve. -- Keep the redirect PR in `coder/coder.com` in sync with the rename PR so the - old public path never 404s between merges. +- Add the redirect to `docs/redirects.json` in the same PR as the rename so + the old public path never 404s between merges. ## Keep PRs reviewable @@ -198,7 +198,7 @@ the whole series as a single review. not as conformance: tooling covers a small subset of the guide, and Vale never fails a build. The count lives in [What the tooling checks, and what it doesn't](../../../docs/.style/style-guide/README.md#what-the-tooling-checks-and-what-it-doesnt). -- [ ] Inbound links resolve; renames have redirects in `coder/coder.com`. +- [ ] Inbound links resolve; renames have redirects in `docs/redirects.json`. - [ ] Premium pages carry the title suffix and manifest state. - [ ] Series pages orient the reader and link the next step; no dead-ends. - [ ] The change is scoped for review: large or multi-page work is split into diff --git a/.github/workflows/deploy-docs.yaml b/.github/workflows/deploy-docs.yaml index 5adbed6e9f..07ce8df826 100644 --- a/.github/workflows/deploy-docs.yaml +++ b/.github/workflows/deploy-docs.yaml @@ -24,12 +24,14 @@ name: Update coder.com/docs # on every docs/** push. # # 2. vercel-rebuild: fires the Vercel deploy hook for a full -# build+deploy. Only runs when docs/manifest.json changed, since a -# manifest change can introduce or remove routes that Next.js's -# `getStaticPaths` only re-evaluates on a full rebuild. +# build+deploy. Only runs when docs/manifest.json or +# docs/redirects.json changed. A manifest change can introduce or +# remove routes that Next.js's `getStaticPaths` only re-evaluates on +# a full rebuild, and the website reads docs/redirects.json at build +# time, so a redirect only takes effect in a new build. # # Markdown-only edits hit only path 1 and surface in seconds. Manifest -# edits hit both paths in parallel; the ISR revalidate is harmless +# and redirects edits hit both paths in parallel; the ISR revalidate is harmless # against the previous deployment while the new build is in flight, # and Vercel only swaps to the new build atomically when ready. # @@ -92,11 +94,15 @@ concurrency: jobs: # Detect what changed so the dependent jobs know: - # - whether a Vercel full rebuild is needed (manifest changed), and + # - whether a Vercel full rebuild is needed (manifest or redirects + # changed), and # - which markdown pages to surgically reindex (the changed set). # # Outputs: - # manifest_changed: "true" | "false" + # manifest_changed: "true" | "false" + # redirects_changed: "true" | "false". Kept separate from + # manifest_changed because it must not force the + # whole-branch reindex a manifest change does. # paths_json: a JSON array of {path, status} objects, or "[]" # when no markdown changes are eligible for # surgical mode (manifest-only push, an @@ -109,6 +115,7 @@ jobs: runs-on: ubuntu-latest outputs: manifest_changed: ${{ steps.diff.outputs.manifest_changed }} + redirects_changed: ${{ steps.diff.outputs.redirects_changed }} paths_json: ${{ steps.diff.outputs.paths_json }} steps: - name: Compute changed-files signal @@ -133,6 +140,7 @@ jobs: # resulting reindex is exhaustive. if [ "$EVENT_NAME" != "push" ]; then echo "manifest_changed=false" >> "$GITHUB_OUTPUT" + echo "redirects_changed=false" >> "$GITHUB_OUTPUT" emit_whole_branch_fallback exit 0 fi @@ -142,6 +150,7 @@ jobs: # whole-branch reindex. if [ -z "${BEFORE_SHA:-}" ] || [ "$BEFORE_SHA" = "0000000000000000000000000000000000000000" ]; then echo "manifest_changed=true" >> "$GITHUB_OUTPUT" + echo "redirects_changed=true" >> "$GITHUB_OUTPUT" emit_whole_branch_fallback exit 0 fi @@ -160,9 +169,20 @@ jobs: FIRST_ERR=$(head -1 "$GIT_ERR" 2>/dev/null || true) echo "::warning::Could not fetch BEFORE_SHA=$BEFORE_SHA: ${FIRST_ERR:-unknown}; assuming manifest changed" echo "manifest_changed=true" >> "$GITHUB_OUTPUT" + echo "redirects_changed=true" >> "$GITHUB_OUTPUT" emit_whole_branch_fallback exit 0 fi + # Redirects signal. The website reads docs/redirects.json at build + # time, so a change needs a full rebuild. Unlike the manifest it + # cannot add or remove routes, so it leaves the surgical reindex + # path below alone. Computed before the manifest signal so it is + # set even when the manifest branch exits early. + if git diff --name-only "$BEFORE_SHA" "$AFTER_SHA" -- docs/redirects.json | grep -q .; then + echo "redirects_changed=true" >> "$GITHUB_OUTPUT" + else + echo "redirects_changed=false" >> "$GITHUB_OUTPUT" + fi # Manifest signal. if git diff --name-only "$BEFORE_SHA" "$AFTER_SHA" -- docs/manifest.json | grep -q .; then echo "manifest_changed=true" >> "$GITHUB_OUTPUT" @@ -491,20 +511,30 @@ jobs: exit "$RC" fi - # Path 2: full Vercel rebuild. Only fires when docs/manifest.json - # changed, because manifest changes can introduce or remove routes - # that Next.js's `getStaticPaths` only re-evaluates on a full build. - # Markdown-only edits don't need this; ISR revalidate covers them. + # Path 2: full Vercel rebuild. Only fires when docs/manifest.json or + # docs/redirects.json changed. Manifest changes can introduce or + # remove routes that Next.js's `getStaticPaths` only re-evaluates on a + # full build, and redirects are read at build time. Markdown-only + # edits don't need this; ISR revalidate covers them. vercel-rebuild: runs-on: ubuntu-latest needs: changes - if: needs.changes.outputs.manifest_changed == 'true' + if: needs.changes.outputs.manifest_changed == 'true' || needs.changes.outputs.redirects_changed == 'true' steps: - name: Trigger Vercel deploy hook env: HOOK: ${{ secrets.DEPLOY_DOCS_VERCEL_WEBHOOK }} + MANIFEST_CHANGED: ${{ needs.changes.outputs.manifest_changed }} + REDIRECTS_CHANGED: ${{ needs.changes.outputs.redirects_changed }} run: | set -euo pipefail + CHANGED_FILES="" + if [ "${MANIFEST_CHANGED:-}" = "true" ]; then + CHANGED_FILES="\`docs/manifest.json\`" + fi + if [ "${REDIRECTS_CHANGED:-}" = "true" ]; then + CHANGED_FILES="${CHANGED_FILES:+$CHANGED_FILES and }\`docs/redirects.json\`" + fi if [ -z "${HOOK:-}" ]; then echo "::error::DEPLOY_DOCS_VERCEL_WEBHOOK is not configured." exit 1 @@ -537,7 +567,7 @@ jobs: { echo "## Vercel rebuild" echo - echo "- Reason: \`docs/manifest.json\` changed" + echo "- Reason: ${CHANGED_FILES} changed" echo "- HTTP status: \`${HTTP_STATUS:-n/a}\`" echo echo "### Response (allowlisted fields)" diff --git a/docs/.style/content-guidelines.md b/docs/.style/content-guidelines.md index 9354e7780a..d30a5ae53c 100644 --- a/docs/.style/content-guidelines.md +++ b/docs/.style/content-guidelines.md @@ -243,9 +243,17 @@ These govern *how* content enters the docs, for both humans and the doc-check ag - **Moving or renaming a page requires link updates and a redirect.** If a page changes its position in the directory structure: 1. Update every link that relies on its existing location. - 2. Add a redirect in the - [`coder/coder.com`](https://github.com/coder/coder.com/blob/master/redirects.json) - repo (`redirects.json`). + 2. Add a rule to `docs/redirects.json` in the same PR that moves the page. + Each rule has a `source` and a `destination`, both full `/docs/...` paths, and an optional `permanent` flag that defaults to `true`. + A `destination` can also be an external `http` or `https` URL. + A `source` is an exact path, a path ending in `/:path*`, or a path with a `:slug(.*)` capture. + For example: `{"source": "/docs/about/architecture", "destination": "/docs/about"}`. + + The website reads `docs/redirects.json` at build time, and a change to the file starts a website rebuild. + The redirect is therefore live in the build that the merge triggers. + Each release branch has its own copy of the file, and the unversioned docs are served from the mainline release branch. + Backport the move and its redirect together so the old URL keeps working there. + Redirects that predate this file stay where they are and keep working. Do not create a `docs/_redirects` file. That format isn't processed by [coder.com](https://coder.com). @@ -358,4 +366,4 @@ Update this section as they land. If it becomes real, revisit loosening the screenshots policy. Until then, the screenshot rules in [What belongs in the docs](#what-belongs-in-the-docs) govern. - **doc-check redirect suggestions.** - When doc-check detects a moved or renamed page, it should suggest the exact `redirects.json` entry for `coder/coder.com` in a code block, so applying it is at most a copy-paste job. + When doc-check detects a moved or renamed page, it should suggest the exact `docs/redirects.json` entry in a code block, so applying it is at most a copy-paste job. diff --git a/docs/redirects.json b/docs/redirects.json new file mode 100644 index 0000000000..fe51488c70 --- /dev/null +++ b/docs/redirects.json @@ -0,0 +1 @@ +[]