website: Unlisted & Draft Release Notes (#18210)

* website: Unlisted Release Notes

* Swizzle unlisted component. Revise copy for pre-release.
This commit is contained in:
Teffen Ellis
2025-11-25 17:36:53 +01:00
committed by GitHub
parent a37162aebd
commit 12c8bca8bf
7 changed files with 180 additions and 39 deletions

View File

@@ -2,9 +2,13 @@ import "./styles.css";
import { createVersionURL, parseBranchSemVer } from "#components/VersionPicker/utils.ts";
import type {
AKReleaseFrontMatter,
AKReleasesPluginEnvironment,
} from "@goauthentik/docusaurus-theme/releases/common";
import clsx from "clsx";
import React, { memo } from "react";
import { AKReleasesPluginEnvironment } from "releases/node.mjs";
export interface VersionDropdownProps {
/**
@@ -20,12 +24,19 @@ export interface VersionDropdownProps {
* @format semver
*/
releases: string[];
/**
* A possible record of parsed front-matter for each release.
*/
frontMatterRecord: Record<string, AKReleaseFrontMatter>;
}
/**
* A dropdown that shows the available versions of the documentation.
*/
export const VersionDropdown = memo<VersionDropdownProps>(({ environment, releases }) => {
export const VersionDropdown = memo<VersionDropdownProps>((props) => {
const { environment, releases, frontMatterRecord } = props;
const { branch, preReleaseOrigin } = environment;
const parsedSemVer = parseBranchSemVer(branch);
@@ -65,6 +76,11 @@ export const VersionDropdown = memo<VersionDropdownProps>(({ environment, releas
{visibleReleases.map((semVer, idx) => {
let label = semVer;
const frontmatter = frontMatterRecord[semVer];
if (frontmatter?.unlisted || frontmatter?.draft) {
return null;
}
if (idx === 0) {
label += " (Current Release)";

View File

@@ -1,10 +1,13 @@
import { useHostname } from "#components/VersionPicker/utils.ts";
import { VersionDropdown } from "#components/VersionPicker/VersionDropdown.tsx";
import { AKReleasesPluginData } from "@goauthentik/docusaurus-theme/releases/plugin";
import type {
AKReleaseFrontMatter,
AKReleasesPluginData,
} from "@goauthentik/docusaurus-theme/releases/common";
import useIsBrowser from "@docusaurus/useIsBrowser";
import React, { useEffect, useState } from "react";
import React, { useEffect, useMemo, useState } from "react";
export interface VersionPickerLoaderProps {
pluginData: AKReleasesPluginData;
@@ -20,7 +23,23 @@ export interface VersionPickerLoaderProps {
export const VersionPickerLoader: React.FC<VersionPickerLoaderProps> = ({ pluginData }) => {
const { preReleaseOrigin } = pluginData.env;
const [releases, setReleases] = useState(pluginData.releases);
const [releases, setReleases] = useState(() =>
pluginData.releases.map((release) => release.name),
);
const frontMatterRecord = useMemo(() => {
const record: Record<string, AKReleaseFrontMatter> = {};
for (const release of pluginData.releases) {
if (!release.frontMatter) {
continue;
}
record[release.name] = release.frontMatter;
}
return record;
}, [pluginData.releases]);
const browser = useIsBrowser();
const hostname = useHostname();
@@ -60,5 +79,12 @@ export const VersionPickerLoader: React.FC<VersionPickerLoaderProps> = ({ plugin
return () => controller.abort("unmount");
}, [browser, pluginData.publicPath, preReleaseOrigin]);
return <VersionDropdown hostname={hostname} releases={releases} environment={pluginData.env} />;
return (
<VersionDropdown
hostname={hostname}
releases={releases}
frontMatterRecord={frontMatterRecord}
environment={pluginData.env}
/>
);
};

View File

@@ -14,6 +14,7 @@
"./redirects/plugin": "./redirects/plugin.mjs",
"./redirects/node": "./redirects/node.mjs",
"./redirects": "./redirects/index.mjs",
"./releases/common": "./releases/common.mjs",
"./releases/plugin": "./releases/plugin.mjs",
"./releases/node": "./releases/node.mjs"
},

View File

@@ -0,0 +1,41 @@
/**
* @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.
*/
/**
* @typedef {object} AKReleaseFrontMatter
* @property {boolean} [draft] Whether the release is a draft.
* @property {boolean} [unlisted] Whether the release is unlisted.
*/
/**
* @typedef {object} AKReleaseFileMetadata
* @property {string} name The name of the release file.
* @property {string} path The relative path to the release file.
*/
/**
* @typedef {AKReleaseFileMetadata & { frontMatter?: AKReleaseFrontMatter }} AKReleaseFile
*
* Represents a release file with additional frontmatter properties.
*/
/**
* @typedef {object} AKReleasesPluginOptions
* @property {string} docsDirectory The path to the documentation directory.
* @property {AKReleasesPluginEnvironment} [environment] Optional environment variables overrides.
*/
/**
* @typedef {object} AKReleasesPluginData
* @property {string} publicPath URL to the plugin's public directory.
* @property {AKReleaseFile[]} releases Available versions of the documentation.
* @property {AKReleasesPluginEnvironment} env Environment variables
*/
export {};

View File

@@ -2,19 +2,26 @@
* @file Docusaurus release utils.
*
* @import { SidebarItemConfig } from "@docusaurus/plugin-content-docs/src/sidebars/types.js"
* @import { AKReleaseFile, AKReleasesPluginEnvironment } from "./common.mjs"
*/
import * as path from "node:path";
import { readFileSync } from "node:fs";
import { extname, join } from "node:path";
import { parseFileContentFrontMatter } from "@docusaurus/utils/lib/markdownUtils.js";
import FastGlob from "fast-glob";
import { coerce } from "semver";
/**
* Collect all Markdown files from the releases directory.
*
* @param {string} releasesParentDirectory
* @returns {FastGlob.Entry[]}
* @returns {AKReleaseFile[]}
*/
export function collectReleaseFiles(releasesParentDirectory) {
/**
* @type {Array<FastGlob.Entry & AKReleaseFile>}
*/
const releaseFiles = FastGlob.sync("releases/**/v*.{md,mdx}", {
cwd: releasesParentDirectory,
onlyFiles: true,
@@ -38,6 +45,21 @@ export function collectReleaseFiles(releasesParentDirectory) {
return b.name.localeCompare(a.name);
});
const [latestRelease] = releaseFiles;
if (latestRelease) {
const extension = extname(latestRelease.dirent.name);
const fileContent = readFileSync(
join(releasesParentDirectory, `${latestRelease.path}${extension}`),
"utf-8",
);
const { frontMatter } = parseFileContentFrontMatter(fileContent);
latestRelease.frontMatter = frontMatter;
}
return releaseFiles;
}
@@ -45,15 +67,13 @@ export const SUPPORTED_RELEASE_COUNT = 3;
/**
*
* @param {FastGlob.Entry[]} releaseFiles
* @param {AKReleaseFile[]} releaseFiles
*/
export function createReleaseSidebarEntries(releaseFiles) {
/**
* @type {SidebarItemConfig[]}
*/
let sidebarEntries = releaseFiles.map((fileEntry) => {
return path.join(fileEntry.path);
});
let sidebarEntries = releaseFiles.map((fileEntry) => fileEntry.path);
if (releaseFiles.length > SUPPORTED_RELEASE_COUNT) {
// Then we add the rest of the releases as a category.
@@ -70,15 +90,6 @@ 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.
*

View File

@@ -3,7 +3,7 @@
* @file Docusaurus releases plugin.
*
* @import { LoadContext, Plugin } from "@docusaurus/types"
* @import { AKReleasesPluginEnvironment } from "./node.mjs"
* @import { AKReleasesPluginOptions, AKReleasesPluginData } from "./common.mjs"
*/
import * as fs from "node:fs/promises";
@@ -14,19 +14,6 @@ import { collectReleaseFiles, prepareReleaseEnvironment } from "./node.mjs";
const PLUGIN_NAME = "ak-releases-plugin";
const RELEASES_FILENAME = "releases.gen.json";
/**
* @typedef {object} AKReleasesPluginOptions
* @property {string} docsDirectory The path to the documentation directory.
* @property {AKReleasesPluginEnvironment} [environment] Optional environment variables overrides.
*/
/**
* @typedef {object} AKReleasesPluginData
* @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 {AKReleasesPluginOptions} options
@@ -44,14 +31,13 @@ async function akReleasesPlugin(loadContext, options) {
...options.environment,
};
const releases = collectReleaseFiles(options.docsDirectory).map(
(release) => release.name,
);
const releases = collectReleaseFiles(options.docsDirectory);
const releaseNames = releases.map((release) => release.name);
const outputPath = path.join(loadContext.siteDir, "static", RELEASES_FILENAME);
await fs.mkdir(path.dirname(outputPath), { recursive: true });
await fs.writeFile(outputPath, JSON.stringify(releases, null, 2), "utf-8");
await fs.writeFile(outputPath, JSON.stringify(releaseNames, null, 2), "utf-8");
console.log(`${RELEASES_FILENAME} generated`);
/**

View File

@@ -0,0 +1,60 @@
import { useDoc } from "@docusaurus/plugin-content-docs/client";
import {
ThemeClassNames,
UnlistedBannerMessage,
UnlistedBannerTitle,
UnlistedMetadata,
} from "@docusaurus/theme-common";
import Translate from "@docusaurus/Translate";
import Admonition from "@theme/Admonition";
import type { Props } from "@theme/ContentVisibility/Unlisted";
import clsx from "clsx";
import React, { type ReactNode } from "react";
function UnlistedBanner({ className }: Props) {
const context = useDoc();
if (context.metadata.id?.startsWith("releases")) {
return (
<Admonition
type="note"
title={
<Translate
id="theme.contentVisibility.unlistedBanner.preRelease.title"
description="The unlisted content banner title"
>
Pre-Release Documentation
</Translate>
}
className={clsx(className, ThemeClassNames.common.unlistedBanner)}
>
<Translate
id="theme.contentVisibility.unlistedBanner.preRelease.message"
description="The unlisted content banner message"
>
This documentation is for an upcoming version of authentik. It may be incomplete
or subject to changes before the final release.
</Translate>
</Admonition>
);
}
return (
<Admonition
type="caution"
title={<UnlistedBannerTitle />}
className={clsx(className, ThemeClassNames.common.unlistedBanner)}
>
<UnlistedBannerMessage />
</Admonition>
);
}
export default function Unlisted(props: Props): ReactNode {
return (
<>
<UnlistedMetadata />
<UnlistedBanner {...props} />
</>
);
}