From 904ee27f26e09fffd309bf2aac4fd7f99b9dd265 Mon Sep 17 00:00:00 2001 From: Eva Sarafianou Date: Tue, 8 Sep 2026 09:41:56 +0300 Subject: [PATCH] docs: emit redirect stubs at both /index.html and .html (#38389) active-redirects.json entries whose `from` lives outside the docs CloudFront function's frozen legacy-Sphinx prefix set currently 404 in production. The stub file exists in S3 at /index.html but the CFF (mattermost-iac-docs-prod -> 05-cdn -> clean-url-rewrite.js) only serves that key for the frozen legacy prefixes; for any other first path segment it falls back to .html, which doesn't exist for plugin-client-redirects stubs. Add a postbuild step that duplicates every stub at the sibling flat key. The legacy branch keeps hitting /index.html unchanged; the fallback branch now finds .html. Real content pages are never overwritten (the script skips any path where .html already exists), .html-suffixed from-values are skipped (CFF Phase A canonicalizes them before Phase B routes them), and .. traversal in a malformed from is rejected before any filesystem call. Smoke-tested against a local build: 741 stubs duplicated, 0 real content pages touched, byte-identical meta-refresh content. --- docs/site/package.json | 1 + .../site/scripts/duplicate-redirect-stubs.mjs | 67 +++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 docs/site/scripts/duplicate-redirect-stubs.mjs diff --git a/docs/site/package.json b/docs/site/package.json index ea1585c3de3..e0d230a385e 100644 --- a/docs/site/package.json +++ b/docs/site/package.json @@ -6,6 +6,7 @@ "docusaurus": "docusaurus", "start": "docusaurus start", "build": "docusaurus build", + "postbuild": "node scripts/duplicate-redirect-stubs.mjs", "stage:agents-docs": "node scripts/stage-agents-docs.mjs", "build:sidebars": "node scripts/gen-documentation-sidebar.mjs && node scripts/gen-developer-sidebar.mjs", "build:openapi:spec": "node scripts/build-openapi.mjs", diff --git a/docs/site/scripts/duplicate-redirect-stubs.mjs b/docs/site/scripts/duplicate-redirect-stubs.mjs new file mode 100644 index 00000000000..15c4fdb5251 --- /dev/null +++ b/docs/site/scripts/duplicate-redirect-stubs.mjs @@ -0,0 +1,67 @@ +#!/usr/bin/env node +// Copy every plugin-client-redirects stub from `build//index.html` to +// `build/.html` so the docs CloudFront function's `.html` fallback +// branch finds it (see mattermost-iac-docs-prod → 05-cdn → +// clean-url-rewrite.js). + +import {readFileSync, writeFileSync, existsSync, statSync} from 'node:fs'; +import {join, resolve, dirname, sep} from 'node:path'; +import {fileURLToPath} from 'node:url'; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const SITE_ROOT = resolve(HERE, '..'); +const BUILD_DIR = join(SITE_ROOT, 'build'); +const REDIRECTS_JSON = join(SITE_ROOT, 'sidebars', 'active-redirects.json'); + +if (!existsSync(BUILD_DIR)) { + console.error(`[duplicate-redirect-stubs] ${BUILD_DIR} not found; run \`docusaurus build\` first.`); + process.exit(1); +} + +const {redirects} = JSON.parse(readFileSync(REDIRECTS_JSON, 'utf8')); +const BUILD_DIR_PREFIX = BUILD_DIR + sep; + +let duplicated = 0; +let skippedExisting = 0; +let skippedMissingStub = 0; +let skippedHtmlSuffix = 0; +let skippedOutsideBuild = 0; + +for (const {from} of redirects) { + // CFF Phase A already 301s `.html` to the extensionless form. + if (from.endsWith('.html')) { + skippedHtmlSuffix++; + continue; + } + + const rel = from.replace(/^\/+/, ''); + const stubPath = resolve(BUILD_DIR, rel, 'index.html'); + const flatPath = resolve(BUILD_DIR, `${rel}.html`); + if (!stubPath.startsWith(BUILD_DIR_PREFIX) || !flatPath.startsWith(BUILD_DIR_PREFIX)) { + skippedOutsideBuild++; + continue; + } + + if (!existsSync(stubPath) || !statSync(stubPath).isFile()) { + skippedMissingStub++; + continue; + } + + // Never shadow a real content page at the flat key. + if (existsSync(flatPath)) { + skippedExisting++; + continue; + } + + writeFileSync(flatPath, readFileSync(stubPath)); + duplicated++; +} + +console.log( + `[duplicate-redirect-stubs] duplicated=${duplicated} ` + + `skipped-existing=${skippedExisting} ` + + `skipped-missing-stub=${skippedMissingStub} ` + + `skipped-html-suffix=${skippedHtmlSuffix} ` + + `skipped-outside-build=${skippedOutsideBuild} ` + + `total=${redirects.length}`, +);