diff --git a/plugins/catalog/src/alpha/components/EntityLayout/EntityLayout.tsx b/plugins/catalog/src/alpha/components/EntityLayout/EntityLayout.tsx
index 17c4693bde..895f36150f 100644
--- a/plugins/catalog/src/alpha/components/EntityLayout/EntityLayout.tsx
+++ b/plugins/catalog/src/alpha/components/EntityLayout/EntityLayout.tsx
@@ -27,6 +27,7 @@ import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
import {
Content,
Link,
+ Page,
Progress,
WarningPanel,
} from '@backstage/core-components';
@@ -149,7 +150,7 @@ export const EntityLayout = (props: EntityLayoutProps) => {
const { t } = useTranslationRef(catalogTranslationRef);
return (
- <>
+
{header ?? (
{
)}
)}
- >
+
);
};
diff --git a/plugins/devtools/src/alpha/plugin.tsx b/plugins/devtools/src/alpha/plugin.tsx
index 4d81d43e17..0144308069 100644
--- a/plugins/devtools/src/alpha/plugin.tsx
+++ b/plugins/devtools/src/alpha/plugin.tsx
@@ -16,6 +16,7 @@
import {
createFrontendPlugin,
+ coreExtensionData,
discoveryApiRef,
fetchApiRef,
ApiBlueprint,
@@ -50,11 +51,34 @@ export const devToolsApi = ApiBlueprint.make({
});
/** @alpha */
-export const devToolsPage = PageBlueprint.make({
- params: {
- path: '/devtools',
- routeRef: rootRouteRef,
- title: 'DevTools',
+export const devToolsPage = PageBlueprint.makeWithOverrides({
+ factory(originalFactory, { inputs }) {
+ const pages = [...inputs.pages].sort((left, right) => {
+ const leftPath = left.get(coreExtensionData.routePath);
+ const rightPath = right.get(coreExtensionData.routePath);
+
+ if (leftPath === 'info' && rightPath !== 'info') {
+ return -1;
+ }
+ if (leftPath !== 'info' && rightPath === 'info') {
+ return 1;
+ }
+
+ return 0;
+ });
+
+ return originalFactory(
+ {
+ path: '/devtools',
+ routeRef: rootRouteRef,
+ title: 'DevTools',
+ },
+ {
+ inputs: {
+ pages,
+ },
+ },
+ );
},
});
diff --git a/plugins/scaffolder/src/components/ScaffolderPageLayout.test.tsx b/plugins/scaffolder/src/components/ScaffolderPageLayout.test.tsx
new file mode 100644
index 0000000000..510d69d748
--- /dev/null
+++ b/plugins/scaffolder/src/components/ScaffolderPageLayout.test.tsx
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2026 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 { renderInTestApp } from '@backstage/test-utils';
+import { screen } from '@testing-library/react';
+import { ScaffolderPageLayout } from './ScaffolderPageLayout';
+
+describe('ScaffolderPageLayout', () => {
+ it('preserves BUI titles, subtitles, and breadcrumb links', async () => {
+ await renderInTestApp(
+
+ Task My template
+
+ }
+ subtitle="Task ID: 123"
+ type="Scaffolder"
+ typeLink="/create"
+ >
+ Body content
+ ,
+ );
+
+ expect(
+ screen.getByRole('heading', { name: 'Task page' }),
+ ).toBeInTheDocument();
+ expect(screen.getByText('My template')).toBeInTheDocument();
+ expect(screen.getByText('Task ID: 123')).toBeInTheDocument();
+ expect(screen.getByRole('link', { name: 'Scaffolder' })).toHaveAttribute(
+ 'href',
+ '/create',
+ );
+ });
+});
diff --git a/plugins/scaffolder/src/components/ScaffolderPageLayout.tsx b/plugins/scaffolder/src/components/ScaffolderPageLayout.tsx
index fafddb2590..31d7233435 100644
--- a/plugins/scaffolder/src/components/ScaffolderPageLayout.tsx
+++ b/plugins/scaffolder/src/components/ScaffolderPageLayout.tsx
@@ -14,9 +14,11 @@
* limitations under the License.
*/
+import Box from '@material-ui/core/Box';
import { Content, Header, Page } from '@backstage/core-components';
import { HeaderPage } from '@backstage/ui';
import type { ReactNode } from 'react';
+import { useEffect } from 'react';
type HeaderVariant = 'legacy' | 'bui';
@@ -55,19 +57,44 @@ export const ScaffolderPageLayout = (props: ScaffolderPageLayoutProps) => {
children
);
+ useEffect(() => {
+ if (!pageTitleOverride) {
+ return;
+ }
+
+ document.title = pageTitleOverride;
+ }, [pageTitleOverride]);
+
if (headerVariant === 'bui') {
let buiTitle: string | undefined;
if (typeof title === 'string') {
buiTitle = title;
+ } else if (pageTitleOverride) {
+ buiTitle = pageTitleOverride;
} else if (typeof subtitle === 'string') {
buiTitle = subtitle;
}
+ const breadcrumbs =
+ type && typeLink ? [{ label: type, href: typeLink }] : undefined;
+ const customTitle = typeof title === 'string' ? undefined : title;
+ const hasHeaderDetails = Boolean(customTitle) || Boolean(subtitle);
+
return (
- <>
-
+
+
+ {hasHeaderDetails && (
+
+ {customTitle}
+ {subtitle && {subtitle}}
+
+ )}
{pageContent}
- >
+
);
}
diff --git a/plugins/techdocs/src/alpha/NfsTechDocsIndexPage.test.tsx b/plugins/techdocs/src/alpha/NfsTechDocsIndexPage.test.tsx
new file mode 100644
index 0000000000..bd39a7c267
--- /dev/null
+++ b/plugins/techdocs/src/alpha/NfsTechDocsIndexPage.test.tsx
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2026 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 { ApiProvider } from '@backstage/core-app-api';
+import { configApiRef, storageApiRef } from '@backstage/core-plugin-api';
+import {
+ MockStarredEntitiesApi,
+ catalogApiRef,
+ starredEntitiesApiRef,
+} from '@backstage/plugin-catalog-react';
+import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils';
+import {
+ TestApiRegistry,
+ mockApis,
+ renderInTestApp,
+} from '@backstage/test-utils';
+import { screen } from '@testing-library/react';
+import { rootDocsRouteRef } from '../routes';
+import { NfsTechDocsIndexPage } from './NfsTechDocsIndexPage';
+
+const mockCatalogApi = catalogApiMock({
+ entities: [
+ {
+ apiVersion: 'version',
+ kind: 'User',
+ metadata: {
+ name: 'owned',
+ namespace: 'default',
+ },
+ },
+ ],
+});
+
+describe('NfsTechDocsIndexPage', () => {
+ it('renders the documentation heading and subtitle', async () => {
+ const apiRegistry = TestApiRegistry.from(
+ [catalogApiRef, mockCatalogApi],
+ [
+ configApiRef,
+ mockApis.config({
+ data: { organization: { name: 'My Company' } },
+ }),
+ ],
+ [storageApiRef, mockApis.storage()],
+ [starredEntitiesApiRef, new MockStarredEntitiesApi()],
+ );
+
+ await renderInTestApp(
+
+
+ ,
+ {
+ mountedRoutes: {
+ '/docs/:namespace/:kind/:name/*': rootDocsRouteRef,
+ },
+ },
+ );
+
+ expect(
+ await screen.findByRole('heading', { name: 'Documentation' }),
+ ).toBeInTheDocument();
+ expect(
+ await screen.findByText(/Documentation available in My Company/i),
+ ).toBeInTheDocument();
+ });
+});
diff --git a/plugins/techdocs/src/alpha/NfsTechDocsIndexPage.tsx b/plugins/techdocs/src/alpha/NfsTechDocsIndexPage.tsx
index 31ffe55ca3..3b2450d404 100644
--- a/plugins/techdocs/src/alpha/NfsTechDocsIndexPage.tsx
+++ b/plugins/techdocs/src/alpha/NfsTechDocsIndexPage.tsx
@@ -15,7 +15,9 @@
*/
import { FC, ReactNode } from 'react';
+import Box from '@material-ui/core/Box';
import { useOutlet } from 'react-router-dom';
+import { Page } from '@backstage/core-components';
import { HeaderPage } from '@backstage/ui';
import {
TechDocsIndexPageProps,
@@ -30,10 +32,11 @@ const NfsTechDocsPageWrapper: FC<{ children?: ReactNode }> = ({ children }) => {
}`;
return (
- <>
-
+
+
+ {generatedSubtitle}
{children}
- >
+
);
};
diff --git a/plugins/techdocs/src/alpha/NfsTechDocsReaderLayout.tsx b/plugins/techdocs/src/alpha/NfsTechDocsReaderLayout.tsx
index 1bbf5f8673..806d286ceb 100644
--- a/plugins/techdocs/src/alpha/NfsTechDocsReaderLayout.tsx
+++ b/plugins/techdocs/src/alpha/NfsTechDocsReaderLayout.tsx
@@ -22,7 +22,7 @@ import Skeleton from '@material-ui/lab/Skeleton';
import CodeIcon from '@material-ui/icons/Code';
import capitalize from 'lodash/capitalize';
import { useParams } from 'react-router-dom';
-import { HeaderLabel } from '@backstage/core-components';
+import { HeaderLabel, Page } from '@backstage/core-components';
import { HeaderPage } from '@backstage/ui';
import {
useTechDocsAddons,
@@ -39,9 +39,10 @@ import {
RELATION_OWNED_BY,
stringifyEntityRef,
} from '@backstage/catalog-model';
-import { configApiRef, useApi } from '@backstage/core-plugin-api';
+import { configApiRef, useApi, useRouteRef } from '@backstage/core-plugin-api';
import { TechDocsReaderPageContent } from '../reader/components/TechDocsReaderPageContent';
import { TechDocsReaderPageSubheader } from '../reader/components/TechDocsReaderPageSubheader';
+import { rootDocsRouteRef } from '../routes';
const skeleton = ;
@@ -50,6 +51,7 @@ const NfsTechDocsReaderPageHeader = (props: PropsWithChildren<{}>) => {
const addons = useTechDocsAddons();
const configApi = useApi(configApiRef);
const entityPresentationApi = useApi(entityPresentationApiRef);
+ const docsRootLink = useRouteRef(rootDocsRouteRef)();
const { '*': path = '' } = useParams();
const {
@@ -161,7 +163,8 @@ const NfsTechDocsReaderPageHeader = (props: PropsWithChildren<{}>) => {
{tabTitle}
{children}
@@ -193,10 +196,10 @@ export const NfsTechDocsReaderLayout = (
const { withSearch, withHeader = true } = props;
return (
- <>
+
{withHeader && }
- >
+
);
};