diff --git a/.changeset/fair-grapes-joke.md b/.changeset/fair-grapes-joke.md
new file mode 100644
index 0000000000..8ab6516833
--- /dev/null
+++ b/.changeset/fair-grapes-joke.md
@@ -0,0 +1,46 @@
+---
+'@backstage/create-app': patch
+---
+
+Register `TechDocs` addons on catalog entity pages, follow the steps below to add them manually:
+
+```diff
+// packages/app/src/components/catalog/EntityPage.tsx
+
++ import { TechDocsAddons } from '@backstage/plugin-techdocs-react';
++ import {
++ ReportIssue,
++ } from '@backstage/plugin-techdocs-module-addons-contrib';
+
++ const techdocsContent = (
++
++
++
++
++
++ );
+
+const defaultEntityPage = (
+ ...
+
++ {techdocsContent}
+
+ ...
+);
+
+const serviceEntityPage = (
+ ...
+
++ {techdocsContent}
+
+ ...
+);
+
+const websiteEntityPage = (
+ ...
+
++ {techdocsContent}
+
+ ...
+);
+```
diff --git a/.changeset/techdocs-buttons-film.md b/.changeset/techdocs-buttons-film.md
new file mode 100644
index 0000000000..1af1ead4c7
--- /dev/null
+++ b/.changeset/techdocs-buttons-film.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-techdocs': patch
+---
+
+Fix `EntityTechdocsContent` component to use objects instead of `` elements, otherwise "outlet" will be null on sub-pages and add-ons won't render.
diff --git a/cypress/src/integration/plugins/catalog.spec.ts b/cypress/src/integration/plugins/catalog.spec.ts
index adf9b9a384..50ef3145e5 100644
--- a/cypress/src/integration/plugins/catalog.spec.ts
+++ b/cypress/src/integration/plugins/catalog.spec.ts
@@ -82,5 +82,118 @@ describe('Catalog', () => {
.contains('Sub-page 1')
.should('be.visible');
});
+
+ it('Should render addons on docs tab homepage', () => {
+ cy.loginAsGuest();
+
+ cy.visit('/catalog');
+
+ cy.contains('techdocs-e2e-fixture').click();
+
+ cy.location().should(loc => {
+ expect(loc.pathname).to.eq(
+ '/catalog/default/component/techdocs-e2e-fixture',
+ );
+ });
+
+ cy.getCatalogDocsTab().click();
+
+ cy.wait(300);
+
+ cy.getTechDocsShadowRoot()
+ .find('h1')
+ .contains('Home page')
+ .should('be.visible');
+
+ // highlight a snippet of text
+ cy.getTechDocsShadowRoot()
+ .find('article > p')
+ .then($el => {
+ const el = $el[0];
+ const document = el.ownerDocument;
+ const range = document.createRange();
+ range.selectNodeContents(el);
+ document?.getSelection()?.removeAllRanges();
+ document?.getSelection()?.addRange(range);
+ });
+
+ cy.document().trigger('selectionchange');
+
+ // wait for new issue default debounce time
+ cy.wait(600);
+
+ // assert that the new issue button has a right url
+ cy.getTechDocsShadowRoot()
+ .contains('Open new Github issue')
+ .should(
+ 'have.attr',
+ 'href',
+ 'https://github.com/backstage/backstage/issues/new?title=Documentation%20feedback%3A%20This%20is%20a%20basic%20documentation%20used%20for%20end-to-end%20tests.&body=%23%23%20Documentation%20Feedback%20%F0%9F%93%9D%0A%0A%20%23%23%23%23%20The%20highlighted%20text%3A%20%0A%0A%20%3E%20This%20is%20a%20basic%20documentation%20used%20for%20end-to-end%20tests.%0A%0A%20%23%23%23%23%20The%20comment%20on%20the%20text%3A%20%0A%20_%3Ereplace%20this%20line%20with%20your%20comment%3C_%0A%0A%20___%0ABackstage%20URL%3A%20%3Chttp%3A%2F%2Flocalhost%3A7007%2Fcatalog%2Fdefault%2Fcomponent%2Ftechdocs-e2e-fixture%2Fdocs%3E%20%0AMarkdown%20URL%3A%20%3Chttps%3A%2F%2Fgithub.com%2Fbackstage%2Fbackstage%2Fblob%2Fmaster%2Fcypress%2Ffixtures%2Fdocs%2Findex.md%3E',
+ );
+ });
+
+ it('Should render addons on docs tab sup-page', () => {
+ cy.loginAsGuest();
+
+ cy.visit('/catalog');
+
+ cy.contains('techdocs-e2e-fixture').click();
+
+ cy.location().should(loc => {
+ expect(loc.pathname).to.eq(
+ '/catalog/default/component/techdocs-e2e-fixture',
+ );
+ });
+
+ cy.getCatalogDocsTab().click();
+
+ cy.wait(300);
+
+ cy.getTechDocsShadowRoot()
+ .find('h1')
+ .contains('Home page')
+ .should('be.visible');
+
+ cy.getTechDocsShadowRoot().within(() => {
+ cy.getTechDocsNavigation().find('a').contains('Sub-page 1').click();
+ });
+
+ cy.location().should(loc => {
+ expect(loc.pathname).to.eq(
+ '/catalog/default/component/techdocs-e2e-fixture/docs/sub-page-one/',
+ );
+ });
+
+ cy.getTechDocsShadowRoot()
+ .find('h1')
+ .contains('Sub-page 1')
+ .should('be.visible');
+
+ // highlight a snippet of text
+ cy.getTechDocsShadowRoot()
+ .find('#section-11')
+ .then($el => {
+ const el = $el[0];
+ const document = el.ownerDocument;
+ const range = document.createRange();
+ range.selectNodeContents(el);
+ document?.getSelection()?.removeAllRanges();
+ document?.getSelection()?.addRange(range);
+ });
+
+ cy.document().trigger('selectionchange');
+
+ // wait for new issue default debounce time
+ cy.wait(600);
+
+ // assert that the new issue button has a right url
+ cy.getTechDocsShadowRoot()
+ .contains('Open new Github issue')
+ .should(
+ 'have.attr',
+ 'href',
+ 'https://github.com/backstage/backstage/issues/new?title=Documentation%20feedback%3A%20Section%201.1%C2%B6&body=%23%23%20Documentation%20Feedback%20%F0%9F%93%9D%0A%0A%20%23%23%23%23%20The%20highlighted%20text%3A%20%0A%0A%20%3E%20Section%201.1%C2%B6%0A%0A%20%23%23%23%23%20The%20comment%20on%20the%20text%3A%20%0A%20_%3Ereplace%20this%20line%20with%20your%20comment%3C_%0A%0A%20___%0ABackstage%20URL%3A%20%3Chttp%3A%2F%2Flocalhost%3A7007%2Fcatalog%2Fdefault%2Fcomponent%2Ftechdocs-e2e-fixture%2Fdocs%2Fsub-page-one%2F%3E%20%0AMarkdown%20URL%3A%20%3Chttps%3A%2F%2Fgithub.com%2Fbackstage%2Fbackstage%2Fblob%2Fmaster%2Fcypress%2Ffixtures%2Fdocs%2Fsub-page-one.md%3E',
+ );
+ });
});
});
diff --git a/cypress/src/integration/plugins/techdocs.spec.ts b/cypress/src/integration/plugins/techdocs.spec.ts
index 1e69ba46ad..31094cb128 100644
--- a/cypress/src/integration/plugins/techdocs.spec.ts
+++ b/cypress/src/integration/plugins/techdocs.spec.ts
@@ -99,7 +99,7 @@ describe('TechDocs', () => {
});
describe('Rendering TechDocs Addons', () => {
- it('should render a content addon', () => {
+ it('should render a content addon in homepage', () => {
cy.visit('/docs/default/Component/techdocs-e2e-fixture');
cy.contains('e2e Fixture Documentation');
@@ -130,6 +130,43 @@ describe('TechDocs', () => {
'https://github.com/backstage/backstage/issues/new?title=Documentation%20feedback%3A%20This%20is%20a%20basic%20documentation%20used%20for%20end-to-end%20tests.&body=%23%23%20Documentation%20Feedback%20%F0%9F%93%9D%0A%0A%20%23%23%23%23%20The%20highlighted%20text%3A%20%0A%0A%20%3E%20This%20is%20a%20basic%20documentation%20used%20for%20end-to-end%20tests.%0A%0A%20%23%23%23%23%20The%20comment%20on%20the%20text%3A%20%0A%20_%3Ereplace%20this%20line%20with%20your%20comment%3C_%0A%0A%20___%0ABackstage%20URL%3A%20%3Chttp%3A%2F%2Flocalhost%3A7007%2Fdocs%2Fdefault%2FComponent%2Ftechdocs-e2e-fixture%3E%20%0AMarkdown%20URL%3A%20%3Chttps%3A%2F%2Fgithub.com%2Fbackstage%2Fbackstage%2Fblob%2Fmaster%2Fcypress%2Ffixtures%2Fdocs%2Findex.md%3E',
);
});
+
+ it('should render a content addon in sub-pages', () => {
+ cy.visit('/docs/default/Component/techdocs-e2e-fixture');
+
+ cy.contains('e2e Fixture Documentation');
+
+ // open sub-page
+ cy.getTechDocsShadowRoot().within(() => {
+ cy.getTechDocsNavigation().find('a').contains('Sub-page 1').click();
+ });
+
+ // highlight a snippet of text
+ cy.getTechDocsShadowRoot()
+ .find('#section-11')
+ .then($el => {
+ const el = $el[0];
+ const document = el.ownerDocument;
+ const range = document.createRange();
+ range.selectNodeContents(el);
+ document?.getSelection()?.removeAllRanges();
+ document?.getSelection()?.addRange(range);
+ });
+
+ cy.document().trigger('selectionchange');
+
+ // wait for new issue default debounce time
+ cy.wait(600);
+
+ // assert that the new issue button has a right url
+ cy.getTechDocsShadowRoot()
+ .contains('Open new Github issue')
+ .should(
+ 'have.attr',
+ 'href',
+ 'https://github.com/backstage/backstage/issues/new?title=Documentation%20feedback%3A%20Section%201.1%C2%B6&body=%23%23%20Documentation%20Feedback%20%F0%9F%93%9D%0A%0A%20%23%23%23%23%20The%20highlighted%20text%3A%20%0A%0A%20%3E%20Section%201.1%C2%B6%0A%0A%20%23%23%23%23%20The%20comment%20on%20the%20text%3A%20%0A%20_%3Ereplace%20this%20line%20with%20your%20comment%3C_%0A%0A%20___%0ABackstage%20URL%3A%20%3Chttp%3A%2F%2Flocalhost%3A7007%2Fdocs%2Fdefault%2FComponent%2Ftechdocs-e2e-fixture%2Fsub-page-one%2F%3E%20%0AMarkdown%20URL%3A%20%3Chttps%3A%2F%2Fgithub.com%2Fbackstage%2Fbackstage%2Fblob%2Fmaster%2Fcypress%2Ffixtures%2Fdocs%2Fsub-page-one.md%3E',
+ );
+ });
});
describe('Navigating within TechDocs', () => {
diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx
index 22d6cc3ab1..799fe3e4d9 100644
--- a/packages/app/src/components/catalog/EntityPage.tsx
+++ b/packages/app/src/components/catalog/EntityPage.tsx
@@ -140,6 +140,12 @@ import { EntityGoCdContent, isGoCdAvailable } from '@backstage/plugin-gocd';
import React, { ReactNode, useMemo, useState } from 'react';
+import { TechDocsAddons } from '@backstage/plugin-techdocs-react';
+import {
+ TextSize,
+ ReportIssue,
+} from '@backstage/plugin-techdocs-module-addons-contrib';
+
const customEntityFilterKind = ['Component', 'API', 'System'];
const EntityLayoutWrapper = (props: { children?: ReactNode }) => {
@@ -168,6 +174,15 @@ const EntityLayoutWrapper = (props: { children?: ReactNode }) => {
);
};
+const techdocsContent = (
+
+
+
+
+
+
+);
+
/**
* NOTE: This page is designed to work on small screens such as mobile devices.
* This is based on Material UI Grid. If breakpoints are used, each grid item must set the `xs` prop to a column size or to `true`,
@@ -398,7 +413,7 @@ const serviceEntityPage = (
-
+ {techdocsContent}
-
+ {techdocsContent}
+
-
+ {techdocsContent}
diff --git a/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx b/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx
index 84d094410b..6ec4da055d 100644
--- a/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx
+++ b/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx
@@ -68,6 +68,17 @@ import {
RELATION_PROVIDES_API,
} from '@backstage/catalog-model';
+import { TechDocsAddons } from '@backstage/plugin-techdocs-react';
+import { ReportIssue } from '@backstage/plugin-techdocs-module-addons-contrib';
+
+const techdocsContent = (
+
+
+
+
+
+);
+
const cicdContent = (
// This is an example of how you can implement your company's logic in entity page.
// You can for example enforce that all components of type 'service' should use GitHubActions
@@ -167,7 +178,7 @@ const serviceEntityPage = (
-
+ {techdocsContent}
);
@@ -194,7 +205,7 @@ const websiteEntityPage = (
-
+ {techdocsContent}
);
@@ -213,7 +224,7 @@ const defaultEntityPage = (
-
+ {techdocsContent}
);
diff --git a/plugins/techdocs/api-report.md b/plugins/techdocs/api-report.md
index a8204bc9af..b2090bc879 100644
--- a/plugins/techdocs/api-report.md
+++ b/plugins/techdocs/api-report.md
@@ -111,7 +111,9 @@ export type DocsTableRow = {
};
// @public
-export const EmbeddedDocsRouter: (props: PropsWithChildren<{}>) => JSX.Element;
+export const EmbeddedDocsRouter: (
+ props: PropsWithChildren<{}>,
+) => JSX.Element | null;
// @public
export const EntityListDocsGrid: () => JSX.Element;
@@ -153,7 +155,7 @@ export type EntityListDocsTableProps = {
// @public
export const EntityTechdocsContent: (props: {
children?: ReactNode;
-}) => JSX.Element;
+}) => JSX.Element | null;
// @public
export const isTechDocsAvailable: (entity: Entity) => boolean;
diff --git a/plugins/techdocs/src/Router.tsx b/plugins/techdocs/src/Router.tsx
index de63838bc8..12b3ddf611 100644
--- a/plugins/techdocs/src/Router.tsx
+++ b/plugins/techdocs/src/Router.tsx
@@ -15,7 +15,7 @@
*/
import React, { PropsWithChildren } from 'react';
-import { Route, Routes } from 'react-router-dom';
+import { Route, Routes, useRoutes } from 'react-router-dom';
import { Entity } from '@backstage/catalog-model';
import { useEntity } from '@backstage/plugin-catalog-react';
@@ -61,17 +61,25 @@ export const EmbeddedDocsRouter = (props: PropsWithChildren<{}>) => {
const { children } = props;
const { entity } = useEntity();
+ // Using objects instead of elements, otherwise "outlet" will be null on sub-pages and add-ons won't render
+ const element = useRoutes([
+ {
+ path: '/*',
+ element: ,
+ children: [
+ {
+ path: '/*',
+ element: children,
+ },
+ ],
+ },
+ ]);
+
const projectId = entity.metadata.annotations?.[TECHDOCS_ANNOTATION];
if (!projectId) {
return ;
}
- return (
-
- }>
- {children}
-
-
- );
+ return element;
};