diff --git a/.changeset/techdocs-addons-api-migration-react.md b/.changeset/techdocs-addons-api-migration-react.md
new file mode 100644
index 0000000000..9a2c5f1da9
--- /dev/null
+++ b/.changeset/techdocs-addons-api-migration-react.md
@@ -0,0 +1,7 @@
+---
+'@backstage/plugin-techdocs-react': patch
+---
+
+TechDocs addons in the new frontend system now use a Utility API pattern instead of multiple attachment points. The `AddonBlueprint` now uses this new approach, and while addons created with older versions still work, they will produce a deprecation warning and will stop working in a future release.
+
+As part of this change, the `techDocsAddonDataRef` alpha export was removed.
diff --git a/.changeset/techdocs-addons-api-migration.md b/.changeset/techdocs-addons-api-migration.md
new file mode 100644
index 0000000000..f2e481de7e
--- /dev/null
+++ b/.changeset/techdocs-addons-api-migration.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-techdocs': patch
+---
+
+TechDocs addons in the new frontend system now use a Utility API pattern instead of multiple attachment points. The `AddonBlueprint` now uses this new approach, and while addons created with older versions still work, they will produce a deprecation warning and will stop working in a future release.
diff --git a/plugins/techdocs-react/report-alpha.api.md b/plugins/techdocs-react/report-alpha.api.md
index 1fed274f3d..cb2a86a037 100644
--- a/plugins/techdocs-react/report-alpha.api.md
+++ b/plugins/techdocs-react/report-alpha.api.md
@@ -31,13 +31,6 @@ export const attachTechDocsAddonComponentData:
(
data: TechDocsAddonOptions,
) => void;
-// @alpha (undocumented)
-export const techDocsAddonDataRef: ConfigurableExtensionDataRef<
- TechDocsAddonOptions,
- 'techdocs.addon',
- {}
->;
-
// @public
export const TechDocsAddonLocations: Readonly<{
readonly Header: 'Header';
diff --git a/plugins/techdocs-react/src/alpha.ts b/plugins/techdocs-react/src/alpha.ts
index 83c0444753..031ec6711f 100644
--- a/plugins/techdocs-react/src/alpha.ts
+++ b/plugins/techdocs-react/src/alpha.ts
@@ -29,8 +29,7 @@ import {
/** @alpha */
export type { TechDocsAddonOptions, TechDocsAddonLocations } from './types';
-/** @alpha */
-export const techDocsAddonDataRef =
+const techDocsAddonDataRef =
createExtensionDataRef().with({
id: 'techdocs.addon',
});
@@ -41,10 +40,7 @@ export const techDocsAddonDataRef =
*/
export const AddonBlueprint = createExtensionBlueprint({
kind: 'addon',
- attachTo: [
- { id: 'page:techdocs/reader', input: 'addons' },
- { id: 'entity-content:techdocs', input: 'addons' },
- ],
+ attachTo: { id: 'api:techdocs/addons', input: 'addons' },
output: [techDocsAddonDataRef],
factory: (params: TechDocsAddonOptions) => [techDocsAddonDataRef(params)],
dataRefs: {
diff --git a/plugins/techdocs/report-alpha.api.md b/plugins/techdocs/report-alpha.api.md
index cbdb912938..493d1500da 100644
--- a/plugins/techdocs/report-alpha.api.md
+++ b/plugins/techdocs/report-alpha.api.md
@@ -53,6 +53,34 @@ const _default: OverridableFrontendPlugin<
params: ApiFactory,
) => ExtensionBlueprintParams;
}>;
+ 'api:techdocs/addons': OverridableExtensionDefinition<{
+ config: {};
+ configInput: {};
+ output: ExtensionDataRef;
+ inputs: {
+ addons: ExtensionInput<
+ ConfigurableExtensionDataRef<
+ TechDocsAddonOptions,
+ 'techdocs.addon',
+ {}
+ >,
+ {
+ singleton: false;
+ optional: false;
+ internal: false;
+ }
+ >;
+ };
+ kind: 'api';
+ name: 'addons';
+ params: <
+ TApi,
+ TImpl extends TApi,
+ TDeps extends { [name in string]: unknown },
+ >(
+ params: ApiFactory,
+ ) => ExtensionBlueprintParams;
+ }>;
'api:techdocs/storage': OverridableExtensionDefinition<{
kind: 'api';
name: 'storage';
diff --git a/plugins/techdocs/src/alpha/addonsApi.ts b/plugins/techdocs/src/alpha/addonsApi.ts
new file mode 100644
index 0000000000..86d4739f46
--- /dev/null
+++ b/plugins/techdocs/src/alpha/addonsApi.ts
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2025 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ ApiBlueprint,
+ createApiRef,
+ createExtensionInput,
+} from '@backstage/frontend-plugin-api';
+import { AddonBlueprint } from '@backstage/plugin-techdocs-react/alpha';
+import { TechDocsAddonOptions } from '@backstage/plugin-techdocs-react';
+
+interface TechDocsAddonsApi {
+ getAddons(): TechDocsAddonOptions[];
+}
+
+export const techdocsAddonsApiRef = createApiRef({
+ id: 'plugin.techdocs.addons',
+});
+
+export const TechDocsAddonsApiExtension = ApiBlueprint.makeWithOverrides({
+ name: 'addons',
+ inputs: {
+ addons: createExtensionInput([AddonBlueprint.dataRefs.addon]),
+ },
+ factory(originalFactory, { inputs }) {
+ const addons = inputs.addons.map(output =>
+ output.get(AddonBlueprint.dataRefs.addon),
+ );
+ return originalFactory(defineParams =>
+ defineParams({
+ api: techdocsAddonsApiRef,
+ deps: {},
+ factory: () => ({
+ getAddons: () => addons,
+ }),
+ }),
+ );
+ },
+});
diff --git a/plugins/techdocs/src/alpha/index.tsx b/plugins/techdocs/src/alpha/index.tsx
index d3fc94c7a8..b93d5dfb0d 100644
--- a/plugins/techdocs/src/alpha/index.tsx
+++ b/plugins/techdocs/src/alpha/index.tsx
@@ -14,6 +14,7 @@
* limitations under the License.
*/
+import { Suspense } from 'react';
import LibraryBooks from '@material-ui/icons/LibraryBooks';
import {
createFrontendPlugin,
@@ -34,7 +35,11 @@ import {
EntityIconLinkBlueprint,
} from '@backstage/plugin-catalog-react/alpha';
import { SearchResultListItemBlueprint } from '@backstage/plugin-search-react/alpha';
-import { AddonBlueprint } from '@backstage/plugin-techdocs-react/alpha';
+import {
+ AddonBlueprint,
+ attachTechDocsAddonComponentData,
+} from '@backstage/plugin-techdocs-react/alpha';
+import { TechDocsAddonsApiExtension, techdocsAddonsApiRef } from './addonsApi';
import { TechDocsClient, TechDocsStorageClient } from '../client';
import {
rootCatalogDocsRouteRef,
@@ -42,7 +47,6 @@ import {
rootRouteRef,
} from '../routes';
import { TechDocsReaderLayout } from '../reader';
-import { attachTechDocsAddonComponentData } from '@backstage/plugin-techdocs-react/alpha';
import {
TechDocsAddons,
techdocsApiRef,
@@ -152,24 +156,37 @@ const techDocsReaderPage = PageBlueprint.makeWithOverrides({
inputs: {
addons: createExtensionInput([AddonBlueprint.dataRefs.addon]),
},
- factory(originalFactory, { inputs }) {
- const addons = inputs.addons.map(output => {
- const options = output.get(AddonBlueprint.dataRefs.addon);
- const Addon = options.component;
- attachTechDocsAddonComponentData(Addon, options);
- return ;
- });
+ factory(originalFactory, { apis, inputs }) {
+ const addonsApi = apis.get(techdocsAddonsApiRef);
return originalFactory({
path: '/docs/:namespace/:kind/:name',
routeRef: rootDocsRouteRef,
- loader: async () =>
- await import('../Router').then(({ TechDocsReaderRouter }) => (
+ loader: async () => {
+ // Merge addons from the API with old-style direct attachments
+ const apiAddons = addonsApi?.getAddons() ?? [];
+ const directAddons = inputs.addons.map(output =>
+ output.get(AddonBlueprint.dataRefs.addon),
+ );
+ const addonOptions = [...apiAddons, ...directAddons];
+
+ const addons = addonOptions.map(options => {
+ const Addon = options.component;
+ attachTechDocsAddonComponentData(Addon, options);
+ return (
+
+
+
+ );
+ });
+
+ return import('../Router').then(({ TechDocsReaderRouter }) => (
{addons}
- )),
+ ));
+ },
});
},
});
@@ -191,29 +208,41 @@ const techDocsEntityContent = EntityContentBlueprint.makeWithOverrides({
),
},
factory(originalFactory, context) {
+ const addonsApi = context.apis.get(techdocsAddonsApiRef);
+
return originalFactory(
{
path: 'docs',
title: 'TechDocs',
routeRef: rootCatalogDocsRouteRef,
- loader: () =>
- import('../Router').then(({ EmbeddedDocsRouter }) => {
- const addons = context.inputs.addons.map(output => {
- const options = output.get(AddonBlueprint.dataRefs.addon);
- const Addon = options.component;
- attachTechDocsAddonComponentData(Addon, options);
- return ;
- });
+ loader: () => {
+ // Merge addons from the API with old-style direct attachments
+ const apiAddons = addonsApi?.getAddons() ?? [];
+ const directAddons = context.inputs.addons.map(output =>
+ output.get(AddonBlueprint.dataRefs.addon),
+ );
+ const addonOptions = [...apiAddons, ...directAddons];
+
+ const addons = addonOptions.map(options => {
+ const Addon = options.component;
+ attachTechDocsAddonComponentData(Addon, options);
return (
-
- {addons}
-
+
+
+
);
- }),
+ });
+
+ return import('../Router').then(({ EmbeddedDocsRouter }) => (
+
+ {addons}
+
+ ));
+ },
},
context,
);
@@ -244,6 +273,7 @@ export default createFrontendPlugin({
extensions: [
techDocsClientApi,
techDocsStorageApi,
+ TechDocsAddonsApiExtension,
techDocsNavItem,
techDocsPage,
techDocsReaderPage,