website: Fix version origin detection, build-time URLs (#15774)

* website: Update route base path.

* website: Add copy step for migration.

* website: Use build redirects.

* website: Ensure that netlify config is picked up.

* website: Add shared Netlify plugin cache.

* website: Use relative path.

* website: Fix routing when moving across versioned URLs.

* website: Fix issues surrounding origin detection.

* website: Allow integrations to omit plugin data, fix types.
This commit is contained in:
Teffen Ellis
2025-08-21 20:31:54 +02:00
committed by GitHub
parent 7861f5a40e
commit 536688f23b
18 changed files with 1197 additions and 1255 deletions
+1 -1
View File
@@ -23,4 +23,4 @@ RUN npm run build
FROM docker.io/library/nginx:1.29.0
COPY --from=docs-builder /work/website/build /usr/share/nginx/html
COPY --from=docs-builder /work/website/docs/build /usr/share/nginx/html
+11
View File
@@ -2,6 +2,7 @@
* @file Docusaurus config.
*
* @import { UserThemeConfig, UserThemeConfigExtra } from "@goauthentik/docusaurus-config";
* @import { AKReleasesPluginOptions } from "@goauthentik/docusaurus-theme/releases/plugin"
* @import * as OpenApiPlugin from "docusaurus-plugin-openapi-docs";
* @import {Options as PresetOptions} from '@docusaurus/preset-classic';
*/
@@ -12,10 +13,12 @@ import { basename, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { createDocusaurusConfig } from "@goauthentik/docusaurus-config";
import { prepareReleaseEnvironment } from "@goauthentik/docusaurus-theme/releases/utils";
import { remarkLinkRewrite } from "@goauthentik/docusaurus-theme/remark";
const __dirname = fileURLToPath(new URL(".", import.meta.url));
const require = createRequire(import.meta.url);
const releaseEnvironment = prepareReleaseEnvironment();
const rootStaticDirectory = resolve(__dirname, "..", "static");
const authentikModulePath = resolve(__dirname, "..", "..");
@@ -67,6 +70,7 @@ export default createDocusaurusConfig({
theme: {
customCss: [require.resolve("@goauthentik/docusaurus-config/css/index.css")],
},
pages: false,
docs: {
routeBasePath: "/",
path: ".",
@@ -103,6 +107,13 @@ export default createDocusaurusConfig({
//#region Plugins
plugins: [
[
"@goauthentik/docusaurus-theme/releases/plugin",
/** @type {AKReleasesPluginOptions} */ ({
docsDirectory: __dirname,
environment: releaseEnvironment,
}),
],
[
"docusaurus-plugin-openapi-docs",
{
+4 -2
View File
@@ -11,7 +11,9 @@ import "./ensure-reference-sidebar.mjs";
// @ts-ignore - Allows for project-wide type checking when partially building docs.
import apiReference from "./reference/sidebar";
const DOCS_URL = process.env.DOCS_URL || "https://docs.goauthentik.io";
import { prepareReleaseEnvironment } from "@goauthentik/docusaurus-theme/releases/utils";
const releaseEnvironment = prepareReleaseEnvironment();
/**
* @type {SidebarItemConfig}
@@ -21,7 +23,7 @@ const sidebar = {
{
type: "link",
label: "← Back to Developer Docs",
href: new URL("/developer-docs", DOCS_URL).href,
href: new URL("/developer-docs", releaseEnvironment.preReleaseOrigin).href,
className: "navbar-sidebar__upwards",
},
{
@@ -1,5 +1,7 @@
import "./styles.css";
import { useCachedVersionPluginData } from "@goauthentik/docusaurus-theme/components/VersionPicker/utils.ts";
import isInternalUrl from "@docusaurus/isInternalUrl";
import Link from "@docusaurus/Link";
import { isActiveSidebarItem } from "@docusaurus/plugin-content-docs/client";
@@ -7,16 +9,7 @@ import { ThemeClassNames } from "@docusaurus/theme-common";
import type { Props } from "@theme/DocSidebarItem/Link";
import IconExternalLink from "@theme/Icon/ExternalLink";
import clsx from "clsx";
import React from "react";
const docsURL = new URL(process.env.DOCS_URL || "https://docs.goauthentik.io");
function isInternalUrlOrDocsUrl(url: string) {
if (isInternalUrl(url)) return true;
const inputURL = new URL(url);
return inputURL.origin === docsURL.origin;
}
import React, { useMemo } from "react";
const DocSidebarItemLink: React.FC<Props> = ({
item,
@@ -29,7 +22,18 @@ const DocSidebarItemLink: React.FC<Props> = ({
}) => {
const { href, label, className, autoAddBaseUrl } = item;
const isActive = isActiveSidebarItem(item, activePath);
const internalLink = isInternalUrlOrDocsUrl(href);
const versionPluginData = useCachedVersionPluginData();
const apiReferenceOrigin = versionPluginData?.env.apiReferenceOrigin;
const internalLink = useMemo(() => {
if (isInternalUrl(href)) return true;
if (!apiReferenceOrigin) return false;
const inputURL = new URL(href);
return inputURL.origin === apiReferenceOrigin;
}, [href, apiReferenceOrigin]);
return (
<li
+30 -10
View File
@@ -2,7 +2,8 @@
* @file Docusaurus Documentation config.
*
* @import { UserThemeConfig, UserThemeConfigExtra } from "@goauthentik/docusaurus-config";
* @import { ReleasesPluginOptions } from "@goauthentik/docusaurus-theme/releases/plugin"
* @import { AKReleasesPluginOptions } from "@goauthentik/docusaurus-theme/releases/plugin"
* @import { Options as RedirectsPluginOptions } from "@docusaurus/plugin-client-redirects";
*/
import { cp } from "node:fs/promises";
@@ -15,6 +16,7 @@ import {
createClassicPreset,
extendConfig,
} from "@goauthentik/docusaurus-theme/config";
import { prepareReleaseEnvironment } from "@goauthentik/docusaurus-theme/releases/utils";
import { remarkLinkRewrite } from "@goauthentik/docusaurus-theme/remark";
const __dirname = fileURLToPath(new URL(".", import.meta.url));
@@ -22,6 +24,8 @@ const __dirname = fileURLToPath(new URL(".", import.meta.url));
const rootStaticDirectory = resolve(__dirname, "..", "static");
const authentikModulePath = resolve(__dirname, "..", "..");
const releaseEnvironment = prepareReleaseEnvironment();
//#region Copy static files
const files = [
@@ -52,9 +56,7 @@ export default createDocusaurusConfig(
presets: [
createClassicPreset({
pages: {
path: "pages",
},
pages: false,
docs: {
exclude: [
/**
@@ -64,7 +66,7 @@ export default createDocusaurusConfig(
*/
"**/developer-docs/api/reference/**",
],
routeBasePath: "/docs",
routeBasePath: "/",
path: ".",
sidebarPath: "./sidebar.mjs",
@@ -75,9 +77,6 @@ export default createDocusaurusConfig(
beforeDefaultRemarkPlugins: [
remarkLinkRewrite([
// ---
// TODO: Enable after base path is set to '/'
// ["/docs", ""],
["/api", "https://api.goauthentik.io"],
["/integrations", "https://integrations.goauthentik.io"],
]),
@@ -93,8 +92,29 @@ export default createDocusaurusConfig(
plugins: [
[
"@goauthentik/docusaurus-theme/releases/plugin",
/** @type {ReleasesPluginOptions} */ ({
/** @type {AKReleasesPluginOptions} */ ({
docsDirectory: __dirname,
environment: releaseEnvironment,
}),
],
[
"@docusaurus/plugin-client-redirects",
/** @type {RedirectsPluginOptions} */ ({
redirects: [
{
from: [
"/api",
"/docs/api",
"/docs/developer-docs/api/",
"/developer-docs/api/",
],
to: releaseEnvironment.apiReferenceOrigin,
},
],
createRedirects(existingPath) {
// Redirect to their respective path without the `docs/` prefix
return `/docs${existingPath}`;
},
}),
],
],
@@ -112,7 +132,7 @@ export default createDocusaurusConfig(
image: "img/social.png",
navbarReplacements: {
DOCS_URL: "/docs",
DOCS_URL: "/",
},
navbar: {
logo: {
File diff suppressed because it is too large Load Diff
+4 -3
View File
@@ -4,12 +4,13 @@
"license": "MIT",
"private": true,
"scripts": {
"build": "run-s build:docusaurus",
"build:docusaurus": "docusaurus build --out-dir ../build",
"build": "run-s build:docusaurus build:copy",
"build:copy": "cp -r ./build ../build",
"build:docusaurus": "docusaurus build",
"check-types": "tsc -b .",
"clear": "docusaurus clear",
"docusaurus": "docusaurus",
"serve": "docusaurus serve --dir ../build",
"serve": "docusaurus serve",
"start": "docusaurus start"
},
"dependencies": {
-7
View File
@@ -1,7 +0,0 @@
import { Redirect } from "@docusaurus/router";
function Home() {
return <Redirect to="docs" />;
}
export default Home;
+3 -1
View File
@@ -10,11 +10,13 @@ import { fileURLToPath } from "node:url";
import {
collectReleaseFiles,
createReleaseSidebarEntries,
prepareReleaseEnvironment,
} from "@goauthentik/docusaurus-theme/releases/utils";
const __dirname = fileURLToPath(new URL(".", import.meta.url));
const releases = collectReleaseFiles(path.join(__dirname));
const releaseEnvironment = prepareReleaseEnvironment();
/**
* @type {SidebarItemConfig[]}
@@ -640,7 +642,7 @@ const items = [
items: [
{
type: "link",
href: "https://api.goauthentik.io",
href: releaseEnvironment.apiReferenceOrigin,
label: "API Overview",
className: "api-overview",
},
@@ -4,16 +4,16 @@ import { createVersionURL, parseBranchSemVer } from "#components/VersionPicker/u
import clsx from "clsx";
import React, { memo } from "react";
import { AKReleasesPluginEnvironment } from "releases/utils.mjs";
export interface VersionDropdownProps {
/**
* The hostname of the client.
*/
hostname: string | null;
/**
* The branch of the documentation.
*/
branch?: string;
environment: AKReleasesPluginEnvironment;
/**
* The available versions of the documentation.
*
@@ -25,7 +25,8 @@ export interface VersionDropdownProps {
/**
* A dropdown that shows the available versions of the documentation.
*/
export const VersionDropdown = memo<VersionDropdownProps>(({ branch, releases }) => {
export const VersionDropdown = memo<VersionDropdownProps>(({ environment, releases }) => {
const { branch, preReleaseOrigin } = environment;
const parsedSemVer = parseBranchSemVer(branch);
const currentLabel = parsedSemVer || "Pre-release";
@@ -50,7 +51,7 @@ export const VersionDropdown = memo<VersionDropdownProps>(({ branch, releases })
<>
<li>
<a
href="https://docs.goauthentik.io"
href={preReleaseOrigin}
target="_blank"
rel="noopener noreferrer"
className="dropdown__link menu__link"
@@ -63,12 +64,11 @@ export const VersionDropdown = memo<VersionDropdownProps>(({ branch, releases })
) : null}
{visibleReleases.map((semVer, idx) => {
const label = semVer;
let label = semVer;
// TODO: Flesh this out after we settle on versioning strategy.
// if (idx === 0) {
// label += " (Current Release)";
// }
if (idx === 0) {
label += " (Current Release)";
}
return (
<li key={idx}>
@@ -1,10 +1,10 @@
import { LocalhostAliases, ProductionURL, useHostname } from "#components/VersionPicker/utils.ts";
import { useHostname } from "#components/VersionPicker/utils.ts";
import { VersionDropdown } from "#components/VersionPicker/VersionDropdown.tsx";
import { AKReleasesPluginData } from "@goauthentik/docusaurus-theme/releases/plugin";
import useIsBrowser from "@docusaurus/useIsBrowser";
import React, { useEffect, useMemo, useState } from "react";
import React, { useEffect, useState } from "react";
export interface VersionPickerLoaderProps {
pluginData: AKReleasesPluginData;
@@ -18,24 +18,18 @@ export interface VersionPickerLoaderProps {
* @client
*/
export const VersionPickerLoader: React.FC<VersionPickerLoaderProps> = ({ pluginData }) => {
const { preReleaseOrigin } = pluginData.env;
const [releases, setReleases] = useState(pluginData.releases);
const browser = useIsBrowser();
const hostname = useHostname();
const prereleaseOrigin = useMemo(() => {
if (browser && LocalhostAliases.has(window.location.hostname)) {
return window.location.origin;
}
return ProductionURL.href;
}, [browser]);
useEffect(() => {
if (!browser || !prereleaseOrigin) return;
if (!browser || !preReleaseOrigin) return;
const controller = new AbortController();
const updateURL = new URL(pluginData.publicPath, prereleaseOrigin);
const updateURL = new URL(pluginData.publicPath, preReleaseOrigin);
fetch(updateURL, {
signal: controller.signal,
@@ -64,7 +58,7 @@ export const VersionPickerLoader: React.FC<VersionPickerLoaderProps> = ({ plugin
// eslint-disable-next-line consistent-return
return () => controller.abort("unmount");
}, [browser, pluginData.publicPath, prereleaseOrigin]);
}, [browser, pluginData.publicPath, preReleaseOrigin]);
return <VersionDropdown hostname={hostname} branch={pluginData.branch} releases={releases} />;
return <VersionDropdown hostname={hostname} releases={releases} environment={pluginData.env} />;
};
@@ -1,9 +1,5 @@
import { useHostname } from "#components/VersionPicker/utils.ts";
import { VersionDropdown } from "#components/VersionPicker/VersionDropdown.tsx";
import { AKReleasesPluginData } from "@goauthentik/docusaurus-theme/releases/plugin";
import { usePluginData } from "@docusaurus/useGlobalData";
import { useVersionPluginData } from "#components/VersionPicker/utils.ts";
import { VersionPickerLoader } from "#components/VersionPicker/VersionPickerLoader.tsx";
/**
* A component that shows the available versions of the documentation.
@@ -11,21 +7,9 @@ import { usePluginData } from "@docusaurus/useGlobalData";
* @see {@linkcode VersionPickerLoader} for the data-fetching component.
*/
export const VersionPicker: React.FC = () => {
const hostname = useHostname();
const pluginData = useVersionPluginData();
const pluginData = usePluginData("ak-releases-plugin", undefined) as
| AKReleasesPluginData
| undefined;
if (!pluginData) return null;
if (!pluginData?.releases.length) return null;
// return <VersionPickerLoader pluginData={pluginData} />;
return (
<VersionDropdown
hostname={hostname}
releases={pluginData.releases}
branch={pluginData.branch}
/>
);
return <VersionPickerLoader pluginData={pluginData} />;
};
@@ -1,9 +1,10 @@
import type { AKReleasesPluginData } from "@goauthentik/docusaurus-theme/releases/plugin";
import { usePluginData } from "@docusaurus/useGlobalData";
import useIsBrowser from "@docusaurus/useIsBrowser";
import { useMemo } from "react";
import { coerce } from "semver";
export const ProductionURL = new URL("https://docs.goauthentik.io");
export const LocalhostAliases: ReadonlySet<string> = new Set(["localhost", "127.0.0.1"]);
/**
@@ -15,20 +16,6 @@ export function createVersionURL(semver: string): string {
return `https://${subdomain}.goauthentik.io`;
}
/**
* Predicate to determine if a hostname appears to be a prerelease origin.
*/
export function isPrerelease(hostname: string | null): boolean {
if (!hostname) return false;
if (hostname === ProductionURL.hostname) return true;
if (hostname.endsWith(".netlify.app")) return true;
if (LocalhostAliases.has(hostname)) return true;
return false;
}
/**
* Given a hostname, parse the semver from the subdomain.
*/
@@ -66,16 +53,40 @@ export function useHostname() {
return hostname;
}
export function usePrereleaseOrigin() {
const browser = useIsBrowser();
export function useCachedVersionPluginData(): AKReleasesPluginData | null {
const pluginData = usePluginData("ak-releases-plugin", undefined) as
| AKReleasesPluginData
| undefined;
const prereleaseOrigin = useMemo(() => {
if (browser && LocalhostAliases.has(window.location.hostname)) {
return window.location.origin;
}
return ProductionURL.href;
}, [browser]);
return prereleaseOrigin;
return pluginData ?? null;
}
function preferredPreReleaseOrigin(browser: boolean, fallback: string): string {
if (browser && LocalhostAliases.has(window.location.hostname)) {
return window.location.origin;
}
return fallback;
}
export function useVersionPluginData(): AKReleasesPluginData | null {
const browser = useIsBrowser();
const cachedPluginData = useCachedVersionPluginData();
return useMemo(() => {
if (!cachedPluginData) return null;
const preReleaseOrigin = preferredPreReleaseOrigin(
browser,
cachedPluginData.env.preReleaseOrigin,
);
return {
...cachedPluginData,
env: {
...cachedPluginData.env,
preReleaseOrigin,
},
};
}, [browser, cachedPluginData]);
}
+20 -9
View File
@@ -3,41 +3,50 @@
* @file Docusaurus releases plugin.
*
* @import { LoadContext, Plugin } from "@docusaurus/types"
* @import { AKReleasesPluginEnvironment } from "./utils.mjs"
*/
import * as fs from "node:fs/promises";
import * as path from "node:path";
import { collectReleaseFiles } from "./utils.mjs";
import { collectReleaseFiles, prepareReleaseEnvironment } from "./utils.mjs";
const PLUGIN_NAME = "ak-releases-plugin";
const RELEASES_FILENAME = "releases.gen.json";
/**
* @typedef {object} ReleasesPluginOptions
* @typedef {object} AKReleasesPluginOptions
* @property {string} docsDirectory The path to the documentation directory.
* @property {AKReleasesPluginEnvironment} [environment] Optional environment variables overrides.
*/
/**
* @typedef {object} AKReleasesPluginData
* @property {string} [branch]
* @property {string} publicPath The URL to the plugin's public directory.
* @property {string[]} releases The available versions of the documentation.
* @property {string} publicPath URL to the plugin's public directory.
* @property {string[]} releases Available versions of the documentation.
* @property {AKReleasesPluginEnvironment} env Environment variables
*/
/**
* @param {LoadContext} loadContext
* @param {ReleasesPluginOptions} options
* @param {AKReleasesPluginOptions} options
* @returns {Promise<Plugin<AKReleasesPluginData>>}
*/
async function akReleasesPlugin(loadContext, { docsDirectory }) {
async function akReleasesPlugin(loadContext, options) {
return {
name: PLUGIN_NAME,
async loadContent() {
console.log(`🚀 ${PLUGIN_NAME} loaded`);
const releases = collectReleaseFiles(docsDirectory).map((release) => release.name);
const environment = {
...prepareReleaseEnvironment(),
...options.environment,
};
const releases = collectReleaseFiles(options.docsDirectory).map(
(release) => release.name,
);
const outputPath = path.join(loadContext.siteDir, "static", RELEASES_FILENAME);
@@ -49,11 +58,13 @@ async function akReleasesPlugin(loadContext, { docsDirectory }) {
* @type {AKReleasesPluginData}
*/
const content = {
branch: process.env.BRANCH,
releases,
publicPath: path.join("/", RELEASES_FILENAME),
env: environment,
};
content.publicPath;
return content;
},
@@ -69,3 +69,26 @@ export function createReleaseSidebarEntries(releaseFiles) {
return sidebarEntries;
}
/**
* @typedef {object} AKReleasesPluginEnvironment
* @property {string} [branch] The current branch name, if available.
* e.g. "main" `version-${year}.${month}`, "feature-branch"
* @property {string} currentReleaseOrigin The URL to the current release documentation.
* @property {string} preReleaseOrigin The URL to the pre-release documentation.
* @property {string} apiReferenceOrigin The URL to the API reference documentation.
*/
/**
* Prepare the environment variables for the releases plugin.
*
* @returns {AKReleasesPluginEnvironment}
*/
export function prepareReleaseEnvironment() {
return {
branch: process.env.BRANCH,
currentReleaseOrigin: process.env.CURRENT_RELEASE_ORIGIN || "https://docs.goauthentik.io",
preReleaseOrigin: process.env.PRE_RELEASE_ORIGIN || "https://next.goauthentik.io",
apiReferenceOrigin: process.env.API_REFERENCE_ORIGIN || "https://api.goauthentik.io",
};
}
+1016
View File
File diff suppressed because it is too large Load Diff
+7
View File
@@ -23,6 +23,7 @@
"@typescript-eslint/eslint-plugin": "^8.40.0",
"@typescript-eslint/parser": "^8.40.0",
"eslint": "^9.33.0",
"netlify-plugin-cache": "^1.0.3",
"npm-run-all": "^4.1.5",
"prettier": "^3.6.2",
"prettier-plugin-packagejson": "^2.5.19",
@@ -19633,6 +19634,12 @@
"node": ">= 10"
}
},
"node_modules/netlify-plugin-cache": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/netlify-plugin-cache/-/netlify-plugin-cache-1.0.3.tgz",
"integrity": "sha512-CTOwNWrTOP59T6y6unxQNnp1WX702v2R/faR5peSH94ebrYfyY4zT5IsRcIiHKq57jXeyCrhy0GLuTN8ktzuQg==",
"license": "MIT"
},
"node_modules/nice-try": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
+1
View File
@@ -26,6 +26,7 @@
"@typescript-eslint/eslint-plugin": "^8.40.0",
"@typescript-eslint/parser": "^8.40.0",
"eslint": "^9.33.0",
"netlify-plugin-cache": "^1.0.3",
"npm-run-all": "^4.1.5",
"prettier": "^3.6.2",
"prettier-plugin-packagejson": "^2.5.19",