From a0a2d0c7a6c8d8a33dea7243c68b6ba93cace8d9 Mon Sep 17 00:00:00 2001 From: jth-nw Date: Thu, 4 Jun 2026 10:45:22 -0500 Subject: [PATCH 1/8] Add evergreen links design spec Defines the approach for adding version-less redirect URLs that always point to the latest version of each product page, using Docusaurus plugin-client-redirects createRedirects callback. Generated with AI Co-Authored-By: Claude Code --- .../2026-06-04-evergreen-links-design.md | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-04-evergreen-links-design.md diff --git a/docs/superpowers/specs/2026-06-04-evergreen-links-design.md b/docs/superpowers/specs/2026-06-04-evergreen-links-design.md new file mode 100644 index 0000000000..2d8b7c0ae6 --- /dev/null +++ b/docs/superpowers/specs/2026-06-04-evergreen-links-design.md @@ -0,0 +1,103 @@ +# Evergreen Links Design + +## Problem + +External links to Netwrix documentation break whenever a product version is updated. A link like `/docs/auditor/10_7/overview/gettingstarted` stops being the "current" page once version 10.8 ships. There is no stable, version-less URL that always points to the latest version of a given page. + +The existing `plugin-client-redirects` config handles product root URLs (`/docs/auditor` -> `/docs/auditor/10_8`) but not deep page links. + +## Solution + +Add a `createRedirects` callback to the existing `@docusaurus/plugin-client-redirects` configuration. For every page in the latest version of a multi-version product, this generates a version-less redirect alias. + +**Example:** `/docs/auditor/overview/gettingstarted` redirects to `/docs/auditor/10_8/overview/gettingstarted` + +## Scope + +| Product type | Action | +|---|---| +| Multi-version, numbered latest (auditor 10.8, activitymonitor 10.0, etc.) | Generate evergreen redirects for all latest-version pages | +| Multi-version, `current` latest (identitymanager, passwordsecure) | Generate evergreen redirects (`/docs/identitymanager/X` -> `/docs/identitymanager/current/X`) | +| Single-version `current` (1secure, policypak, endpointprotector, etc.) | Skip - URLs are already version-less | +| Products with `hideFromNavbar` (recoveryforactivedirectory) | Still generate redirects - docs exist and should be linkable | + +## Behavior + +- **Redirect type:** Visible redirect via HTML `` + JS `window.location.href`. The browser URL changes to the versioned path. +- **SEO:** Each redirect page includes a `` pointing to the versioned URL. Search engines will index the versioned URL, not the redirect. +- **Search/hash forwarding:** The redirect plugin preserves query strings and hash fragments during redirect. + +## Architecture + +### Data flow + +1. Docusaurus generates all route paths during build (e.g., `/docs/auditor/10_8/overview/gettingstarted`) +2. The `createRedirects` callback is invoked for each path +3. The callback parses the path, identifies the product and version segment +4. It checks whether this version is the product's latest +5. If yes, it returns a version-less path as the redirect source (e.g., `/docs/auditor/overview/gettingstarted`) +6. The plugin writes a tiny HTML file at the version-less path that redirects to the versioned path + +### Code changes + +**`src/config/products.js`** - Add a new helper function: + +```js +export function getLatestVersionUrlMap() { + const map = {}; + for (const product of PRODUCTS) { + if (product.versions.length === 1 && product.versions[0].version === 'current') continue; + const latest = getDefaultVersion(product); + if (!latest) continue; + const urlVersion = latest.customRoutePath + ? latest.customRoutePath.split('/').pop() + : versionToUrl(latest.version); + map[product.id] = urlVersion; + } + return map; +} +``` + +**`docusaurus.config.js`** - Add `createRedirects` to the existing `plugin-client-redirects` config: + +```js +createRedirects(existingPath) { + // For each versioned page in a latest-version product, + // create a version-less alias that redirects to it. + for (const [productId, latestUrlVersion] of Object.entries(latestVersionMap)) { + const versionedPrefix = `/docs/${productId}/${latestUrlVersion}/`; + if (existingPath.startsWith(versionedPrefix)) { + const rest = existingPath.slice(versionedPrefix.length); + return [`/docs/${productId}/${rest}`]; + } + } + return undefined; +}, +``` + +The `latestVersionMap` is computed once at config load time using the new helper. + +### Conflict avoidance + +- The existing `redirects` array handles product root paths (`/docs/auditor` -> `/docs/auditor/10_8`). +- The `createRedirects` callback handles deep pages (`/docs/auditor/overview/X` -> `/docs/auditor/10_8/overview/X`). +- These do not conflict: `createRedirects` only fires for paths that contain a version segment, and root paths are already covered by the explicit `redirects` entries. + +## Build impact + +- **Extra files:** ~4,768 HTML redirect files (one per latest-version page across all multi-version products) +- **Size:** ~1.6 MB total (each file is ~350 bytes) +- **Build time:** Negligible - the redirect plugin writes files as a post-build step, taking 1-3 seconds +- **Deploy:** Negligible additional upload to Azure Blob Storage + +## Testing + +1. Run `npm run build` and verify it completes without errors +2. Check that redirect HTML files exist in the build output at version-less paths (e.g., `build/docs/auditor/overview/gettingstarted/index.html`) +3. Verify the redirect target in the generated HTML points to the correct versioned URL +4. Run `npm run serve` and test in a browser: + - `/docs/auditor/overview/gettingstarted` redirects to `/docs/auditor/10_8/overview/gettingstarted` + - `/docs/auditor/10_7/overview/gettingstarted` still works (no redirect, serves the 10.7 page directly) + - `/docs/1secure/overview` works as before (no redirect, single-version product) + - `/docs/identitymanager/overview` redirects to `/docs/identitymanager/current/overview` +5. Verify no broken links in the build output (the build throws on broken links) From 30a63f293cd91d4353f2005e25a6fa87f3247ae8 Mon Sep 17 00:00:00 2001 From: jth-nw Date: Thu, 4 Jun 2026 10:48:10 -0500 Subject: [PATCH 2/8] Add evergreen links implementation plan Three-task plan: helper function, createRedirects callback, build validation. Generated with AI Co-Authored-By: Claude Code --- .../plans/2026-06-04-evergreen-links.md | 249 ++++++++++++++++++ 1 file changed, 249 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-04-evergreen-links.md diff --git a/docs/superpowers/plans/2026-06-04-evergreen-links.md b/docs/superpowers/plans/2026-06-04-evergreen-links.md new file mode 100644 index 0000000000..979e76b9c4 --- /dev/null +++ b/docs/superpowers/plans/2026-06-04-evergreen-links.md @@ -0,0 +1,249 @@ +# Evergreen Links Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add version-less redirect URLs that always point to the latest version of each product page, so external links like `/docs/auditor/overview/gettingstarted` redirect to `/docs/auditor/10_8/overview/gettingstarted`. + +**Architecture:** A `getLatestVersionUrlMap()` helper in `products.js` builds a lookup from product ID to latest URL-version string. A `createRedirects` callback in the existing `plugin-client-redirects` config uses this map to generate version-less redirect aliases for every page in the latest version of multi-version products. Single-version `current` products are skipped (already version-less). + +**Tech Stack:** Docusaurus 3.8.1, `@docusaurus/plugin-client-redirects` 3.10.1, Node.js ESM + +**Spec:** `docs/superpowers/specs/2026-06-04-evergreen-links-design.md` + +--- + +## File Map + +| File | Action | Responsibility | +|---|---|---| +| `src/config/products.js` | Modify (add function after line 780) | New `getLatestVersionUrlMap()` helper | +| `docusaurus.config.js` | Modify (lines 10, 133-156) | Import new helper, add `createRedirects` callback to redirect plugin config | + +--- + +### Task 1: Add `getLatestVersionUrlMap` helper to products.js + +**Files:** +- Modify: `src/config/products.js:780` (insert after `getDefaultVersion` function) + +- [ ] **Step 1: Add the `getLatestVersionUrlMap` function** + +Open `src/config/products.js`. After the closing brace of `getDefaultVersion` (line 780), insert this function: + +```js +/** + * Build a map of product ID → latest URL-version string. + * Used by the evergreen-links redirect config to generate version-less aliases. + * Skips single-version 'current' products (their URLs are already version-less). + */ +export function getLatestVersionUrlMap() { + const map = {}; + for (const product of PRODUCTS) { + if (product.versions.length === 1 && product.versions[0].version === 'current') continue; + const latest = getDefaultVersion(product); + if (!latest) continue; + const urlVersion = latest.customRoutePath + ? latest.customRoutePath.split('/').pop() + : versionToUrl(latest.version); + map[product.id] = urlVersion; + } + return map; +} +``` + +This handles three cases: +- **Numbered versions** (e.g., auditor 10.8): `versionToUrl('10.8')` → `'10_8'` +- **`current` with customRoutePath** (e.g., identitymanager): extracts `'current'` from `'docs/identitymanager/current'` +- **Single-version `current` products** (e.g., 1secure): skipped by the early `continue` + +- [ ] **Step 2: Verify the file parses correctly** + +Run: +```bash +node -e "import { getLatestVersionUrlMap } from './src/config/products.js'; const m = getLatestVersionUrlMap(); console.log(JSON.stringify(m, null, 2));" +``` + +Expected output: A JSON object mapping product IDs to their latest URL-version strings. Verify these key entries: +- `"auditor": "10_8"` (numbered version, dots to underscores) +- `"identitymanager": "current"` (customRoutePath product) +- `"passwordsecure": "current"` (customRoutePath product) +- `"accessanalyzer": "2601"` (no dots, stays as-is) +- No entry for `1secure`, `policypak`, `endpointprotector`, or other single-version `current` products + +- [ ] **Step 3: Commit** + +```bash +git add src/config/products.js +git commit -m "feat: add getLatestVersionUrlMap helper for evergreen links" +``` + +--- + +### Task 2: Add `createRedirects` callback to docusaurus.config.js + +**Files:** +- Modify: `docusaurus.config.js:10` (update import) +- Modify: `docusaurus.config.js:133-156` (add `createRedirects` to redirect plugin config) + +- [ ] **Step 1: Update the import to include `getLatestVersionUrlMap`** + +In `docusaurus.config.js`, line 10, change: + +```js +import { generateDocusaurusPlugins, generateNavbarDropdowns, PRODUCTS, versionToUrl, getDefaultVersion } from './src/config/products.js'; +``` + +to: + +```js +import { generateDocusaurusPlugins, generateNavbarDropdowns, PRODUCTS, versionToUrl, getDefaultVersion, getLatestVersionUrlMap } from './src/config/products.js'; +``` + +- [ ] **Step 2: Compute the latest version map at config load time** + +After line 32 (the closing of the `apiSidebars` loop), add: + +```js +const latestVersionMap = getLatestVersionUrlMap(); +``` + +This runs once when `docusaurus.config.js` is loaded, not per-redirect. + +- [ ] **Step 3: Add `createRedirects` to the plugin-client-redirects config** + +In the `plugin-client-redirects` config block (lines 134-156), add the `createRedirects` function after the `redirects` array. The entire plugin block should become: + +```js + // Client-side redirects - redirect base product URLs to latest version + [ + '@docusaurus/plugin-client-redirects', + { + redirects: PRODUCTS.filter(product => { + // Only create redirects for products with multiple versions (not just 'current') + return !(product.versions.length === 1 && product.versions[0].version === 'current'); + }).map(product => { + const latestVersion = getDefaultVersion(product); + const latestVersionUrl = versionToUrl(latestVersion.version); + + // Use explicit customRoutePath if specified (e.g., for multi-versioned products with 'current') + // Otherwise use standard path generation + const targetPath = latestVersion.customRoutePath + ? latestVersion.customRoutePath + : `${product.path}/${latestVersionUrl}`; + + return { + from: `/${product.path}`, + to: `/${targetPath}`, + }; + }), + createRedirects(existingPath) { + for (const [productId, latestUrlVersion] of Object.entries(latestVersionMap)) { + const versionedPrefix = `/docs/${productId}/${latestUrlVersion}/`; + if (existingPath.startsWith(versionedPrefix)) { + const rest = existingPath.slice(versionedPrefix.length); + return [`/docs/${productId}/${rest}`]; + } + } + return undefined; + }, + }, + ], +``` + +- [ ] **Step 4: Verify the config loads without errors** + +Run: +```bash +node -e "import('./docusaurus.config.js').then(m => console.log('Config loaded OK. Redirect plugin found:', m.default.plugins.some(p => Array.isArray(p) && p[0] === '@docusaurus/plugin-client-redirects')))" +``` + +Expected: `Config loaded OK. Redirect plugin found: true` + +- [ ] **Step 5: Commit** + +```bash +git add docusaurus.config.js +git commit -m "feat: add createRedirects for evergreen version-less URLs" +``` + +--- + +### Task 3: Build and validate redirect output + +**Files:** +- None modified — validation only + +- [ ] **Step 1: Run a single-product build to validate quickly** + +A full build takes a long time. Use the `DOCS_PRODUCT` env var to build only auditor (the largest multi-version product): + +```bash +DOCS_PRODUCT=auditor npm run build 2>&1 | tail -20 +``` + +Expected: Build completes without errors. Look for the plugin-client-redirects output in the log — it should mention creating redirect files. + +- [ ] **Step 2: Verify redirect HTML files were generated** + +Check that a version-less redirect file exists for a known auditor page: + +```bash +cat build/docs/auditor/overview/gettingstarted/index.html +``` + +Expected: A small HTML file containing: +- `` +- `window.location.href = '/docs/auditor/10_8/overview/gettingstarted'` + +- [ ] **Step 3: Verify old versions do NOT get redirect files** + +```bash +ls build/docs/auditor/10_7/overview/gettingstarted/index.html 2>/dev/null && echo "EXISTS (expected - this is the real page)" +ls build/docs/auditor/10_6/overview/gettingstarted/index.html 2>/dev/null && echo "EXISTS (expected - this is the real page)" +``` + +These should exist as real doc pages, NOT redirect pages. Verify by checking their content is a full HTML page (not a redirect stub): + +```bash +head -5 build/docs/auditor/10_7/overview/gettingstarted/index.html +``` + +Expected: A full `` page with Docusaurus layout, not a redirect. + +- [ ] **Step 4: Count the redirect files generated for auditor** + +```bash +find build/docs/auditor -name "index.html" -exec grep -l 'http-equiv="refresh"' {} \; | wc -l +``` + +Expected: Approximately 1,160 files (matching the latest version page count). + +- [ ] **Step 5: Run a full build** + +```bash +npm run build 2>&1 | tail -30 +``` + +Expected: Build completes without errors. This validates that no redirect conflicts with an existing page and that the `onBrokenLinks: 'throw'` check passes. + +- [ ] **Step 6: Spot-check redirect files for other products** + +```bash +# identitymanager (customRoutePath product with 'current' as latest) +cat build/docs/identitymanager/overview/index.html 2>/dev/null | head -5 + +# accessanalyzer (version '2601', no dots) +find build/docs/accessanalyzer -maxdepth 2 -name "index.html" -path "*/accessanalyzer/*/index.html" -exec grep -l 'http-equiv="refresh"' {} \; | head -3 + +# Verify 1secure has NO redirect files (single-version current product) +find build/docs/1secure -name "index.html" -exec grep -l 'http-equiv="refresh"' {} \; | wc -l +``` + +Expected: +- identitymanager: redirect page pointing to `/docs/identitymanager/current/overview` +- accessanalyzer: redirect files exist under `/docs/accessanalyzer/` pointing to `/docs/accessanalyzer/2601/...` +- 1secure: 0 redirect files + +- [ ] **Step 7: Commit (no code changes, but clean build confirms correctness)** + +No commit needed for this task — it's validation only. If the build passes, the implementation is complete. From b35be33b24e1515b1447fb84a4a6d1e9863d9e80 Mon Sep 17 00:00:00 2001 From: jth-nw Date: Thu, 4 Jun 2026 10:48:52 -0500 Subject: [PATCH 3/8] feat: add getLatestVersionUrlMap helper for evergreen links Generated with AI Co-Authored-By: Claude Code --- src/config/products.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/config/products.js b/src/config/products.js index f64188a1dd..2aee811ba1 100644 --- a/src/config/products.js +++ b/src/config/products.js @@ -786,6 +786,25 @@ export function getDefaultVersion(product) { return product.versions.find((v) => v.isLatest) || product.versions[0]; } +/** + * Build a map of product ID → latest URL-version string. + * Used by the evergreen-links redirect config to generate version-less aliases. + * Skips single-version 'current' products (their URLs are already version-less). + */ +export function getLatestVersionUrlMap() { + const map = {}; + for (const product of PRODUCTS) { + if (product.versions.length === 1 && product.versions[0].version === 'current') continue; + const latest = getDefaultVersion(product); + if (!latest) continue; + const urlVersion = latest.customRoutePath + ? latest.customRoutePath.split('/').pop() + : versionToUrl(latest.version); + map[product.id] = urlVersion; + } + return map; +} + /** * Create product map for route matching (used by ProductMetaTags) */ From f0fbb815be609b4295fc9517c2b6d86fb187b01f Mon Sep 17 00:00:00 2001 From: jth-nw Date: Thu, 4 Jun 2026 10:50:12 -0500 Subject: [PATCH 4/8] feat: add createRedirects for evergreen version-less URLs Generated with AI Co-Authored-By: Claude Code --- docusaurus.config.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/docusaurus.config.js b/docusaurus.config.js index c1c1eca294..97ab0e00d4 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -7,7 +7,7 @@ import { readFileSync, existsSync } from 'fs'; import { resolve } from 'path'; import { themes as prismThemes } from 'prism-react-renderer'; -import { generateDocusaurusPlugins, generateNavbarDropdowns, PRODUCTS, versionToUrl, getDefaultVersion } from './src/config/products.js'; +import { generateDocusaurusPlugins, generateNavbarDropdowns, PRODUCTS, versionToUrl, getDefaultVersion, getLatestVersionUrlMap } from './src/config/products.js'; // Strip TypeScript syntax from a generated sidebar.ts and return its apisidebar array. // Returns [] if the file doesn't exist yet (before gen-api-docs has run). @@ -31,6 +31,8 @@ PRODUCTS.forEach(product => { }); }); +const latestVersionMap = getLatestVersionUrlMap(); + /** @type {import('@docusaurus/types').Config} */ const config = { title: 'Netwrix Product Documentation', @@ -152,6 +154,16 @@ const config = { to: `/${targetPath}`, }; }), + createRedirects(existingPath) { + for (const [productId, latestUrlVersion] of Object.entries(latestVersionMap)) { + const versionedPrefix = `/docs/${productId}/${latestUrlVersion}/`; + if (existingPath.startsWith(versionedPrefix)) { + const rest = existingPath.slice(versionedPrefix.length); + return [`/docs/${productId}/${rest}`]; + } + } + return undefined; + }, }, ], // Generate all product documentation plugins from centralized configuration From 6d5398144218e6bb5c6627e52b572babcfb69145 Mon Sep 17 00:00:00 2001 From: jth-nw Date: Thu, 4 Jun 2026 11:08:35 -0500 Subject: [PATCH 5/8] fix: skip empty rest in createRedirects to avoid root redirect conflict When createRedirects is called for a version root page (e.g., /docs/auditor/10_8/), the rest variable is empty, generating a redirect from /docs/auditor/ which conflicts with the existing explicit redirect from /docs/auditor in the redirects array. Generated with AI Co-Authored-By: Claude Code --- docusaurus.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/docusaurus.config.js b/docusaurus.config.js index 97ab0e00d4..5ab35eae68 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -159,6 +159,7 @@ const config = { const versionedPrefix = `/docs/${productId}/${latestUrlVersion}/`; if (existingPath.startsWith(versionedPrefix)) { const rest = existingPath.slice(versionedPrefix.length); + if (!rest) return undefined; return [`/docs/${productId}/${rest}`]; } } From 4446e09390e1691bf585f69492656a746f463e3f Mon Sep 17 00:00:00 2001 From: jth-nw Date: Thu, 4 Jun 2026 11:32:22 -0500 Subject: [PATCH 6/8] fix: strip trailing slash from redirect path to prevent EEXIST MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pages that are both a doc and a category parent (e.g., a .md file alongside a directory of the same name) generate two Docusaurus routes — with and without trailing slash. Both produce the same filesystem path for the redirect HTML file, causing an EEXIST error on the second write. Generated with AI Co-Authored-By: Claude Code --- docusaurus.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docusaurus.config.js b/docusaurus.config.js index 5ab35eae68..b1d3cc4f04 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -158,7 +158,7 @@ const config = { for (const [productId, latestUrlVersion] of Object.entries(latestVersionMap)) { const versionedPrefix = `/docs/${productId}/${latestUrlVersion}/`; if (existingPath.startsWith(versionedPrefix)) { - const rest = existingPath.slice(versionedPrefix.length); + const rest = existingPath.slice(versionedPrefix.length).replace(/\/$/, ''); if (!rest) return undefined; return [`/docs/${productId}/${rest}`]; } From d900d770760f9011efff5fc5c987302147b0846e Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Thu, 4 Jun 2026 17:37:17 +0000 Subject: [PATCH 7/8] fix(vale): auto-fix style issues (Vale + Dale) --- docs/superpowers/plans/2026-06-04-evergreen-links.md | 6 +++--- docs/superpowers/specs/2026-06-04-evergreen-links-design.md | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/superpowers/plans/2026-06-04-evergreen-links.md b/docs/superpowers/plans/2026-06-04-evergreen-links.md index 979e76b9c4..c051ef8cb8 100644 --- a/docs/superpowers/plans/2026-06-04-evergreen-links.md +++ b/docs/superpowers/plans/2026-06-04-evergreen-links.md @@ -4,7 +4,7 @@ **Goal:** Add version-less redirect URLs that always point to the latest version of each product page, so external links like `/docs/auditor/overview/gettingstarted` redirect to `/docs/auditor/10_8/overview/gettingstarted`. -**Architecture:** A `getLatestVersionUrlMap()` helper in `products.js` builds a lookup from product ID to latest URL-version string. A `createRedirects` callback in the existing `plugin-client-redirects` config uses this map to generate version-less redirect aliases for every page in the latest version of multi-version products. Single-version `current` products are skipped (already version-less). +**Architecture:** A `getLatestVersionUrlMap()` helper in `products.js` builds a lookup from product ID to latest URL-version string. A `createRedirects` callback in the existing `plugin-client-redirects` config uses this map to generate version-less redirect aliases for every page in the latest version of multi-version products. The helper skips single-version `current` products (already version-less). **Tech Stack:** Docusaurus 3.8.1, `@docusaurus/plugin-client-redirects` 3.10.1, Node.js ESM @@ -107,7 +107,7 @@ After line 32 (the closing of the `apiSidebars` loop), add: const latestVersionMap = getLatestVersionUrlMap(); ``` -This runs once when `docusaurus.config.js` is loaded, not per-redirect. +This runs once at config load time, not per-redirect. - [ ] **Step 3: Add `createRedirects` to the plugin-client-redirects config** @@ -195,7 +195,7 @@ Expected: A small HTML file containing: - `` - `window.location.href = '/docs/auditor/10_8/overview/gettingstarted'` -- [ ] **Step 3: Verify old versions do NOT get redirect files** +- [ ] **Step 3: Verify old versions don't get redirect files** ```bash ls build/docs/auditor/10_7/overview/gettingstarted/index.html 2>/dev/null && echo "EXISTS (expected - this is the real page)" diff --git a/docs/superpowers/specs/2026-06-04-evergreen-links-design.md b/docs/superpowers/specs/2026-06-04-evergreen-links-design.md index 2d8b7c0ae6..0b827b0655 100644 --- a/docs/superpowers/specs/2026-06-04-evergreen-links-design.md +++ b/docs/superpowers/specs/2026-06-04-evergreen-links-design.md @@ -32,7 +32,7 @@ Add a `createRedirects` callback to the existing `@docusaurus/plugin-client-redi ### Data flow 1. Docusaurus generates all route paths during build (e.g., `/docs/auditor/10_8/overview/gettingstarted`) -2. The `createRedirects` callback is invoked for each path +2. Docusaurus invokes the `createRedirects` callback for each path 3. The callback parses the path, identifies the product and version segment 4. It checks whether this version is the product's latest 5. If yes, it returns a version-less path as the redirect source (e.g., `/docs/auditor/overview/gettingstarted`) @@ -75,13 +75,13 @@ createRedirects(existingPath) { }, ``` -The `latestVersionMap` is computed once at config load time using the new helper. +The new helper computes `latestVersionMap` once at config load time. ### Conflict avoidance - The existing `redirects` array handles product root paths (`/docs/auditor` -> `/docs/auditor/10_8`). - The `createRedirects` callback handles deep pages (`/docs/auditor/overview/X` -> `/docs/auditor/10_8/overview/X`). -- These do not conflict: `createRedirects` only fires for paths that contain a version segment, and root paths are already covered by the explicit `redirects` entries. +- These don't conflict: `createRedirects` only fires for paths that contain a version segment, and the explicit `redirects` entries already cover root paths. ## Build impact From 2a7ce109958c1a82766d345939a5344064ca740a Mon Sep 17 00:00:00 2001 From: jth-nw Date: Thu, 4 Jun 2026 12:44:08 -0500 Subject: [PATCH 8/8] cleanup --- .../plans/2026-06-04-evergreen-links.md | 249 ------------------ .../2026-06-04-evergreen-links-design.md | 103 -------- 2 files changed, 352 deletions(-) delete mode 100644 docs/superpowers/plans/2026-06-04-evergreen-links.md delete mode 100644 docs/superpowers/specs/2026-06-04-evergreen-links-design.md diff --git a/docs/superpowers/plans/2026-06-04-evergreen-links.md b/docs/superpowers/plans/2026-06-04-evergreen-links.md deleted file mode 100644 index c051ef8cb8..0000000000 --- a/docs/superpowers/plans/2026-06-04-evergreen-links.md +++ /dev/null @@ -1,249 +0,0 @@ -# Evergreen Links Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Add version-less redirect URLs that always point to the latest version of each product page, so external links like `/docs/auditor/overview/gettingstarted` redirect to `/docs/auditor/10_8/overview/gettingstarted`. - -**Architecture:** A `getLatestVersionUrlMap()` helper in `products.js` builds a lookup from product ID to latest URL-version string. A `createRedirects` callback in the existing `plugin-client-redirects` config uses this map to generate version-less redirect aliases for every page in the latest version of multi-version products. The helper skips single-version `current` products (already version-less). - -**Tech Stack:** Docusaurus 3.8.1, `@docusaurus/plugin-client-redirects` 3.10.1, Node.js ESM - -**Spec:** `docs/superpowers/specs/2026-06-04-evergreen-links-design.md` - ---- - -## File Map - -| File | Action | Responsibility | -|---|---|---| -| `src/config/products.js` | Modify (add function after line 780) | New `getLatestVersionUrlMap()` helper | -| `docusaurus.config.js` | Modify (lines 10, 133-156) | Import new helper, add `createRedirects` callback to redirect plugin config | - ---- - -### Task 1: Add `getLatestVersionUrlMap` helper to products.js - -**Files:** -- Modify: `src/config/products.js:780` (insert after `getDefaultVersion` function) - -- [ ] **Step 1: Add the `getLatestVersionUrlMap` function** - -Open `src/config/products.js`. After the closing brace of `getDefaultVersion` (line 780), insert this function: - -```js -/** - * Build a map of product ID → latest URL-version string. - * Used by the evergreen-links redirect config to generate version-less aliases. - * Skips single-version 'current' products (their URLs are already version-less). - */ -export function getLatestVersionUrlMap() { - const map = {}; - for (const product of PRODUCTS) { - if (product.versions.length === 1 && product.versions[0].version === 'current') continue; - const latest = getDefaultVersion(product); - if (!latest) continue; - const urlVersion = latest.customRoutePath - ? latest.customRoutePath.split('/').pop() - : versionToUrl(latest.version); - map[product.id] = urlVersion; - } - return map; -} -``` - -This handles three cases: -- **Numbered versions** (e.g., auditor 10.8): `versionToUrl('10.8')` → `'10_8'` -- **`current` with customRoutePath** (e.g., identitymanager): extracts `'current'` from `'docs/identitymanager/current'` -- **Single-version `current` products** (e.g., 1secure): skipped by the early `continue` - -- [ ] **Step 2: Verify the file parses correctly** - -Run: -```bash -node -e "import { getLatestVersionUrlMap } from './src/config/products.js'; const m = getLatestVersionUrlMap(); console.log(JSON.stringify(m, null, 2));" -``` - -Expected output: A JSON object mapping product IDs to their latest URL-version strings. Verify these key entries: -- `"auditor": "10_8"` (numbered version, dots to underscores) -- `"identitymanager": "current"` (customRoutePath product) -- `"passwordsecure": "current"` (customRoutePath product) -- `"accessanalyzer": "2601"` (no dots, stays as-is) -- No entry for `1secure`, `policypak`, `endpointprotector`, or other single-version `current` products - -- [ ] **Step 3: Commit** - -```bash -git add src/config/products.js -git commit -m "feat: add getLatestVersionUrlMap helper for evergreen links" -``` - ---- - -### Task 2: Add `createRedirects` callback to docusaurus.config.js - -**Files:** -- Modify: `docusaurus.config.js:10` (update import) -- Modify: `docusaurus.config.js:133-156` (add `createRedirects` to redirect plugin config) - -- [ ] **Step 1: Update the import to include `getLatestVersionUrlMap`** - -In `docusaurus.config.js`, line 10, change: - -```js -import { generateDocusaurusPlugins, generateNavbarDropdowns, PRODUCTS, versionToUrl, getDefaultVersion } from './src/config/products.js'; -``` - -to: - -```js -import { generateDocusaurusPlugins, generateNavbarDropdowns, PRODUCTS, versionToUrl, getDefaultVersion, getLatestVersionUrlMap } from './src/config/products.js'; -``` - -- [ ] **Step 2: Compute the latest version map at config load time** - -After line 32 (the closing of the `apiSidebars` loop), add: - -```js -const latestVersionMap = getLatestVersionUrlMap(); -``` - -This runs once at config load time, not per-redirect. - -- [ ] **Step 3: Add `createRedirects` to the plugin-client-redirects config** - -In the `plugin-client-redirects` config block (lines 134-156), add the `createRedirects` function after the `redirects` array. The entire plugin block should become: - -```js - // Client-side redirects - redirect base product URLs to latest version - [ - '@docusaurus/plugin-client-redirects', - { - redirects: PRODUCTS.filter(product => { - // Only create redirects for products with multiple versions (not just 'current') - return !(product.versions.length === 1 && product.versions[0].version === 'current'); - }).map(product => { - const latestVersion = getDefaultVersion(product); - const latestVersionUrl = versionToUrl(latestVersion.version); - - // Use explicit customRoutePath if specified (e.g., for multi-versioned products with 'current') - // Otherwise use standard path generation - const targetPath = latestVersion.customRoutePath - ? latestVersion.customRoutePath - : `${product.path}/${latestVersionUrl}`; - - return { - from: `/${product.path}`, - to: `/${targetPath}`, - }; - }), - createRedirects(existingPath) { - for (const [productId, latestUrlVersion] of Object.entries(latestVersionMap)) { - const versionedPrefix = `/docs/${productId}/${latestUrlVersion}/`; - if (existingPath.startsWith(versionedPrefix)) { - const rest = existingPath.slice(versionedPrefix.length); - return [`/docs/${productId}/${rest}`]; - } - } - return undefined; - }, - }, - ], -``` - -- [ ] **Step 4: Verify the config loads without errors** - -Run: -```bash -node -e "import('./docusaurus.config.js').then(m => console.log('Config loaded OK. Redirect plugin found:', m.default.plugins.some(p => Array.isArray(p) && p[0] === '@docusaurus/plugin-client-redirects')))" -``` - -Expected: `Config loaded OK. Redirect plugin found: true` - -- [ ] **Step 5: Commit** - -```bash -git add docusaurus.config.js -git commit -m "feat: add createRedirects for evergreen version-less URLs" -``` - ---- - -### Task 3: Build and validate redirect output - -**Files:** -- None modified — validation only - -- [ ] **Step 1: Run a single-product build to validate quickly** - -A full build takes a long time. Use the `DOCS_PRODUCT` env var to build only auditor (the largest multi-version product): - -```bash -DOCS_PRODUCT=auditor npm run build 2>&1 | tail -20 -``` - -Expected: Build completes without errors. Look for the plugin-client-redirects output in the log — it should mention creating redirect files. - -- [ ] **Step 2: Verify redirect HTML files were generated** - -Check that a version-less redirect file exists for a known auditor page: - -```bash -cat build/docs/auditor/overview/gettingstarted/index.html -``` - -Expected: A small HTML file containing: -- `` -- `window.location.href = '/docs/auditor/10_8/overview/gettingstarted'` - -- [ ] **Step 3: Verify old versions don't get redirect files** - -```bash -ls build/docs/auditor/10_7/overview/gettingstarted/index.html 2>/dev/null && echo "EXISTS (expected - this is the real page)" -ls build/docs/auditor/10_6/overview/gettingstarted/index.html 2>/dev/null && echo "EXISTS (expected - this is the real page)" -``` - -These should exist as real doc pages, NOT redirect pages. Verify by checking their content is a full HTML page (not a redirect stub): - -```bash -head -5 build/docs/auditor/10_7/overview/gettingstarted/index.html -``` - -Expected: A full `` page with Docusaurus layout, not a redirect. - -- [ ] **Step 4: Count the redirect files generated for auditor** - -```bash -find build/docs/auditor -name "index.html" -exec grep -l 'http-equiv="refresh"' {} \; | wc -l -``` - -Expected: Approximately 1,160 files (matching the latest version page count). - -- [ ] **Step 5: Run a full build** - -```bash -npm run build 2>&1 | tail -30 -``` - -Expected: Build completes without errors. This validates that no redirect conflicts with an existing page and that the `onBrokenLinks: 'throw'` check passes. - -- [ ] **Step 6: Spot-check redirect files for other products** - -```bash -# identitymanager (customRoutePath product with 'current' as latest) -cat build/docs/identitymanager/overview/index.html 2>/dev/null | head -5 - -# accessanalyzer (version '2601', no dots) -find build/docs/accessanalyzer -maxdepth 2 -name "index.html" -path "*/accessanalyzer/*/index.html" -exec grep -l 'http-equiv="refresh"' {} \; | head -3 - -# Verify 1secure has NO redirect files (single-version current product) -find build/docs/1secure -name "index.html" -exec grep -l 'http-equiv="refresh"' {} \; | wc -l -``` - -Expected: -- identitymanager: redirect page pointing to `/docs/identitymanager/current/overview` -- accessanalyzer: redirect files exist under `/docs/accessanalyzer/` pointing to `/docs/accessanalyzer/2601/...` -- 1secure: 0 redirect files - -- [ ] **Step 7: Commit (no code changes, but clean build confirms correctness)** - -No commit needed for this task — it's validation only. If the build passes, the implementation is complete. diff --git a/docs/superpowers/specs/2026-06-04-evergreen-links-design.md b/docs/superpowers/specs/2026-06-04-evergreen-links-design.md deleted file mode 100644 index 0b827b0655..0000000000 --- a/docs/superpowers/specs/2026-06-04-evergreen-links-design.md +++ /dev/null @@ -1,103 +0,0 @@ -# Evergreen Links Design - -## Problem - -External links to Netwrix documentation break whenever a product version is updated. A link like `/docs/auditor/10_7/overview/gettingstarted` stops being the "current" page once version 10.8 ships. There is no stable, version-less URL that always points to the latest version of a given page. - -The existing `plugin-client-redirects` config handles product root URLs (`/docs/auditor` -> `/docs/auditor/10_8`) but not deep page links. - -## Solution - -Add a `createRedirects` callback to the existing `@docusaurus/plugin-client-redirects` configuration. For every page in the latest version of a multi-version product, this generates a version-less redirect alias. - -**Example:** `/docs/auditor/overview/gettingstarted` redirects to `/docs/auditor/10_8/overview/gettingstarted` - -## Scope - -| Product type | Action | -|---|---| -| Multi-version, numbered latest (auditor 10.8, activitymonitor 10.0, etc.) | Generate evergreen redirects for all latest-version pages | -| Multi-version, `current` latest (identitymanager, passwordsecure) | Generate evergreen redirects (`/docs/identitymanager/X` -> `/docs/identitymanager/current/X`) | -| Single-version `current` (1secure, policypak, endpointprotector, etc.) | Skip - URLs are already version-less | -| Products with `hideFromNavbar` (recoveryforactivedirectory) | Still generate redirects - docs exist and should be linkable | - -## Behavior - -- **Redirect type:** Visible redirect via HTML `` + JS `window.location.href`. The browser URL changes to the versioned path. -- **SEO:** Each redirect page includes a `` pointing to the versioned URL. Search engines will index the versioned URL, not the redirect. -- **Search/hash forwarding:** The redirect plugin preserves query strings and hash fragments during redirect. - -## Architecture - -### Data flow - -1. Docusaurus generates all route paths during build (e.g., `/docs/auditor/10_8/overview/gettingstarted`) -2. Docusaurus invokes the `createRedirects` callback for each path -3. The callback parses the path, identifies the product and version segment -4. It checks whether this version is the product's latest -5. If yes, it returns a version-less path as the redirect source (e.g., `/docs/auditor/overview/gettingstarted`) -6. The plugin writes a tiny HTML file at the version-less path that redirects to the versioned path - -### Code changes - -**`src/config/products.js`** - Add a new helper function: - -```js -export function getLatestVersionUrlMap() { - const map = {}; - for (const product of PRODUCTS) { - if (product.versions.length === 1 && product.versions[0].version === 'current') continue; - const latest = getDefaultVersion(product); - if (!latest) continue; - const urlVersion = latest.customRoutePath - ? latest.customRoutePath.split('/').pop() - : versionToUrl(latest.version); - map[product.id] = urlVersion; - } - return map; -} -``` - -**`docusaurus.config.js`** - Add `createRedirects` to the existing `plugin-client-redirects` config: - -```js -createRedirects(existingPath) { - // For each versioned page in a latest-version product, - // create a version-less alias that redirects to it. - for (const [productId, latestUrlVersion] of Object.entries(latestVersionMap)) { - const versionedPrefix = `/docs/${productId}/${latestUrlVersion}/`; - if (existingPath.startsWith(versionedPrefix)) { - const rest = existingPath.slice(versionedPrefix.length); - return [`/docs/${productId}/${rest}`]; - } - } - return undefined; -}, -``` - -The new helper computes `latestVersionMap` once at config load time. - -### Conflict avoidance - -- The existing `redirects` array handles product root paths (`/docs/auditor` -> `/docs/auditor/10_8`). -- The `createRedirects` callback handles deep pages (`/docs/auditor/overview/X` -> `/docs/auditor/10_8/overview/X`). -- These don't conflict: `createRedirects` only fires for paths that contain a version segment, and the explicit `redirects` entries already cover root paths. - -## Build impact - -- **Extra files:** ~4,768 HTML redirect files (one per latest-version page across all multi-version products) -- **Size:** ~1.6 MB total (each file is ~350 bytes) -- **Build time:** Negligible - the redirect plugin writes files as a post-build step, taking 1-3 seconds -- **Deploy:** Negligible additional upload to Azure Blob Storage - -## Testing - -1. Run `npm run build` and verify it completes without errors -2. Check that redirect HTML files exist in the build output at version-less paths (e.g., `build/docs/auditor/overview/gettingstarted/index.html`) -3. Verify the redirect target in the generated HTML points to the correct versioned URL -4. Run `npm run serve` and test in a browser: - - `/docs/auditor/overview/gettingstarted` redirects to `/docs/auditor/10_8/overview/gettingstarted` - - `/docs/auditor/10_7/overview/gettingstarted` still works (no redirect, serves the 10.7 page directly) - - `/docs/1secure/overview` works as before (no redirect, single-version product) - - `/docs/identitymanager/overview` redirects to `/docs/identitymanager/current/overview` -5. Verify no broken links in the build output (the build throws on broken links)