diff --git a/.changeset/lazy-snakes-film.md b/.changeset/lazy-snakes-film.md
new file mode 100644
index 0000000000..ff4097f78f
--- /dev/null
+++ b/.changeset/lazy-snakes-film.md
@@ -0,0 +1,5 @@
+---
+'@backstage/core-components': minor
+---
+
+Adds code to generate ids for headers parsed through the MarkdownContent component.
diff --git a/packages/core-components/src/components/MarkdownContent/MarkdownContent.test.tsx b/packages/core-components/src/components/MarkdownContent/MarkdownContent.test.tsx
index 9c08b83c40..fa8cb35ca8 100644
--- a/packages/core-components/src/components/MarkdownContent/MarkdownContent.test.tsx
+++ b/packages/core-components/src/components/MarkdownContent/MarkdownContent.test.tsx
@@ -100,4 +100,34 @@ describe('', () => {
'https://example.com/blog/assets/6/header.png',
);
});
+
+ it('render MarkdownContent component with headings given proper ids', async () => {
+ const rendered = await renderWithEffects(
+ wrapInTestApp(
+ ,
+ ),
+ );
+
+ expect(rendered.getByText('Lorem ipsum').getAttribute('id')).toEqual(
+ 'lorem-ipsum',
+ );
+ expect(rendered.getByText('bing bong').getAttribute('id')).toEqual(
+ 'bing-bong',
+ );
+ expect(
+ rendered
+ .getByText(
+ 'The FitnessGram Pacer Test is a multistage aerobic capacity test',
+ )
+ .getAttribute('id'),
+ ).toEqual(
+ 'the-fitnessgram-pacer-test-is-a-multistage-aerobic-capacity-test',
+ );
+ });
});
diff --git a/packages/core-components/src/components/MarkdownContent/MarkdownContent.tsx b/packages/core-components/src/components/MarkdownContent/MarkdownContent.tsx
index 013e81817a..d97172bf75 100644
--- a/packages/core-components/src/components/MarkdownContent/MarkdownContent.tsx
+++ b/packages/core-components/src/components/MarkdownContent/MarkdownContent.tsx
@@ -20,6 +20,7 @@ import gfm from 'remark-gfm';
import React from 'react';
import { BackstageTheme } from '@backstage/theme';
import { CodeSnippet } from '../CodeSnippet';
+import { HeadingProps } from 'react-markdown/lib/ast-to-react';
export type MarkdownContentClassKey = 'markdown';
@@ -73,6 +74,21 @@ type Props = {
className?: string;
};
+const flatten = (text: string, child: any): string => {
+ if (!child) return text;
+
+ return typeof child === 'string'
+ ? text + child
+ : React.Children.toArray(child.props.children).reduce(flatten, text);
+};
+
+const headingRenderer = ({ level, children }: HeadingProps) => {
+ const childrenArray = React.Children.toArray(children);
+ const text = childrenArray.reduce(flatten, '');
+ const slug = text.toLocaleLowerCase('en-US').replace(/\W/g, '-');
+ return React.createElement(`h${level}`, { id: slug }, children);
+};
+
const components: Options['components'] = {
code: ({ inline, className, children, ...props }) => {
const text = String(children).replace(/\n+$/, '');
@@ -85,6 +101,12 @@ const components: Options['components'] = {
);
},
+ h1: headingRenderer,
+ h2: headingRenderer,
+ h3: headingRenderer,
+ h4: headingRenderer,
+ h5: headingRenderer,
+ h6: headingRenderer,
};
/**