Skip to content
53 changes: 43 additions & 10 deletions docusaurus.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { resolve } from 'path';
import { themes as prismThemes } from 'prism-react-renderer';
import { generateDocusaurusPlugins, generateNavbarDropdowns, PRODUCTS, versionToUrl, getDefaultVersion, getLatestVersionUrlMap, getActiveProducts, getActiveVersions, generateRouteBasePath } from './src/config/products.js';
import { accessAnalyzer261Redirects } from './src/config/redirects/accessanalyzer-26.1.js';
import { passwordPolicyEnforcerDeversionRedirects } from './src/config/redirects/passwordpolicyenforcer-deversion.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).
Expand Down Expand Up @@ -63,10 +64,30 @@ const activeVersionsByProduct = Object.fromEntries(
// DOCS_PRODUCT_LATEST_ONLY narrows the active list to one version, which would
// make a genuinely multi-version product look collapsed and start rewriting its
// valid versioned URLs in local single-product builds.
// A version can opt into "root-only" stale-link handling (redirectStaleVersionsToRoot)
// instead of the default "preserve the sub-path" handling below — see
// rootOnlyUnversionedDocsBasePaths.
const unversionedDocsBasePaths = redirectProducts
.map(product => {
if (product.versions.length !== 1) return null;
const [version] = product.versions;
if (version.redirectStaleVersionsToRoot) return null;
const routeBasePath = version.customRoutePath || generateRouteBasePath(product.path, version.version);
return routeBasePath === product.path ? `/${routeBasePath}` : null;
})
.filter(Boolean);

// Same idea as unversionedDocsBasePaths, but for a product whose old versions'
// page structure isn't guaranteed to line up with the new unversioned latest
// (e.g. passwordpolicyenforcer, whose 10.2 layout was reorganized in 11.x/12.0).
// Rather than guess at an equivalent sub-path, any stale versioned URL under
// these base paths sends the reader to the docs root. Opt in per-version via
// `redirectStaleVersionsToRoot: true` on the version object.
const rootOnlyUnversionedDocsBasePaths = redirectProducts
.map(product => {
if (product.versions.length !== 1) return null;
const [version] = product.versions;
if (!version.redirectStaleVersionsToRoot) return null;
const routeBasePath = version.customRoutePath || generateRouteBasePath(product.path, version.version);
return routeBasePath === product.path ? `/${routeBasePath}` : null;
})
Expand Down Expand Up @@ -145,6 +166,7 @@ const config = {
activeProductIds,
activeVersionsByProduct,
unversionedDocsBasePaths,
rootOnlyUnversionedDocsBasePaths,
},
clientModules: ['./src/clientModules/scrollBehavior.js'],
presets: [
Expand Down Expand Up @@ -175,23 +197,34 @@ const config = {
};
},

// Google Analytics
[
'@docusaurus/plugin-google-gtag',
{
trackingID: 'G-FZPWSDMTEX',
anonymizeIP: true,
},
],
// Google Analytics — only loaded for production builds. In dev
// (npm run start), the gtag script often can't load (network, ad
// blockers), leaving window.gtag undefined and throwing a runtime error
// overlay on every route change.
...(process.env.NODE_ENV === 'production' ? [
[
'@docusaurus/plugin-google-gtag',
{
trackingID: 'G-FZPWSDMTEX',
anonymizeIP: true,
},
],
] : []),
// Client-side redirects - redirect base product URLs to latest version
[
'@docusaurus/plugin-client-redirects',
{
redirects: [
...(activeProductIds.includes('accessanalyzer') ? accessAnalyzer261Redirects : []),
...(activeProductIds.includes('passwordpolicyenforcer') ? passwordPolicyEnforcerDeversionRedirects : []),
...redirectProducts.filter(product => {
// Only create redirects for products with multiple versions (not just 'current')
return !(product.versions.length === 1 && product.versions[0].version === 'current');
// Only create redirects for products with multiple versions (not just 'current'),
// and skip products whose latest version already serves at the bare product
// path (e.g. passwordpolicyenforcer 12.0) — redirecting the base path to
// itself would be a no-op redirect loop.
if (product.versions.length === 1 && product.versions[0].version === 'current') return false;
const latestVersion = getDefaultVersion(product);
return latestVersion.customRoutePath !== product.path;
}).map(product => {
const latestVersion = getDefaultVersion(product);
const latestVersionUrl = versionToUrl(latestVersion.version);
Expand Down
47 changes: 18 additions & 29 deletions src/config/products.js
Original file line number Diff line number Diff line change
Expand Up @@ -336,40 +336,26 @@ export const PRODUCTS = [
path: 'docs/passwordpolicyenforcer',
categories: ['Directory Management'],
icon: '',
// De-versioned 2026-09: only 12.0 is built/served now, at the bare product
// path (no version segment). The 11.2/11.1/11.0/10.2 entries that used to
// live here were removed, not deleted — their docs, images, and sidebar
// files are untouched on disk (docs/passwordpolicyenforcer/<version>/,
// sidebars/passwordpolicyenforcer/<version>.js). To revive a version,
// re-add its entry here (see git history of this file for the exact
// shape) and it picks back up where it left off. Every old versioned URL
// (including the old /12_0 one) now redirects to the bare product root —
// see passwordPolicyEnforcerDeversionRedirects in docusaurus.config.js for
// known static paths, and redirectStaleVersionsToRoot below for the
// catch-all client-side fallback for unlisted old deep links.
versions: [
{
version: '12.0',
label: '12.0',
isLatest: true,
sidebarFile: './sidebars/passwordpolicyenforcer/12.0.js',
},
{
version: '11.2',
label: '11.2',
isLatest: false,
hidden: true,
sidebarFile: './sidebars/passwordpolicyenforcer/11.2.js',
},
{
version: '11.1',
label: '11.1',
isLatest: false,
hidden: true,
sidebarFile: './sidebars/passwordpolicyenforcer/11.1.js',
},
{
version: '11.0',
label: '11.0',
isLatest: false,
hidden: true,
sidebarFile: './sidebars/passwordpolicyenforcer/11.0.js',
},
{
version: '10.2',
label: '10.2',
isLatest: false,
hidden: true,
sidebarFile: './sidebars/passwordpolicyenforcer/10.2.js',
customRoutePath: 'docs/passwordpolicyenforcer',
customDocPath: 'docs/passwordpolicyenforcer/12.0',
redirectStaleVersionsToRoot: true,
},
],
defaultVersion: '12.0',
Expand Down Expand Up @@ -829,14 +815,17 @@ export function getActiveVersions(product) {
/**
* 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).
* Skips single-version 'current' products (their URLs are already version-less),
* and any product whose latest version already serves at the bare product path
* (e.g. passwordpolicyenforcer 12.0) — those have no version segment to alias.
*/
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;
if (latest.customRoutePath === product.path) continue;
const urlVersion = latest.customRoutePath
? latest.customRoutePath.split('/').pop()
: versionToUrl(latest.version);
Expand Down
Loading