diff --git a/src/components/Breadcrumbs/BreadcrumbContainer.js b/src/components/Breadcrumbs/BreadcrumbContainer.js
index 8ded22a2e..589d2bbf7 100644
--- a/src/components/Breadcrumbs/BreadcrumbContainer.js
+++ b/src/components/Breadcrumbs/BreadcrumbContainer.js
@@ -4,6 +4,7 @@ import styled from '@emotion/styled';
import { reportAnalytics } from '../../utils/report-analytics';
import { theme } from '../../theme/docsTheme';
import { getFullBreadcrumbPath } from '../../utils/get-complete-breadcrumb-data';
+import { useSiteMetadata } from '../../hooks/use-site-metadata';
import IndividualBreadcrumb from './IndividualBreadcrumb';
import CollapsedBreadcrumbs from './CollapsedBreadcrumbs';
@@ -23,6 +24,7 @@ const initialMaxCrumbs = (breadcrumbs) => breadcrumbs.length + 1;
const BreadcrumbContainer = ({ breadcrumbs }) => {
const [maxCrumbs, setMaxCrumbs] = React.useState(initialMaxCrumbs(breadcrumbs));
+ const { siteUrl } = useSiteMetadata();
React.useEffect(() => {
const handleResize = () => {
@@ -68,7 +70,7 @@ const BreadcrumbContainer = ({ breadcrumbs }) => {
setIsExcessivelyTruncated={collapseBreadcrumbs}
onClick={() =>
reportAnalytics('BreadcrumbClick', {
- breadcrumbClicked: getFullBreadcrumbPath(crumb.path, true),
+ breadcrumbClicked: getFullBreadcrumbPath(siteUrl, crumb.path, true),
})
}
>
diff --git a/src/components/Breadcrumbs/index.js b/src/components/Breadcrumbs/index.js
index d8e064647..90ca1679a 100644
--- a/src/components/Breadcrumbs/index.js
+++ b/src/components/Breadcrumbs/index.js
@@ -6,6 +6,7 @@ import { theme } from '../../theme/docsTheme';
import { getCompleteBreadcrumbData } from '../../utils/get-complete-breadcrumb-data.js';
import { useBreadcrumbs } from '../../hooks/use-breadcrumbs';
import useSnootyMetadata from '../../utils/use-snooty-metadata';
+import { useSiteMetadata } from '../../hooks/use-site-metadata.js';
import BreadcrumbContainer from './BreadcrumbContainer';
const breadcrumbBodyStyle = css`
@@ -35,9 +36,11 @@ const Breadcrumbs = ({
const { parentPaths } = useSnootyMetadata();
const parentPathsData = parentPathsProp ?? parentPaths[slug];
+ const { siteUrl } = useSiteMetadata();
const breadcrumbs = React.useMemo(
() =>
getCompleteBreadcrumbData({
+ siteUrl,
siteTitle,
slug,
queriedCrumbs,
@@ -45,7 +48,7 @@ const Breadcrumbs = ({
selfCrumbContent: selfCrumb,
pageInfo: pageInfo,
}),
- [parentPathsData, queriedCrumbs, siteTitle, slug, selfCrumb, pageInfo]
+ [siteUrl, parentPathsData, queriedCrumbs, siteTitle, slug, selfCrumb, pageInfo]
);
return (
diff --git a/src/components/Footnote/FootnoteReference.js b/src/components/Footnote/FootnoteReference.js
index 48419e831..46458a271 100644
--- a/src/components/Footnote/FootnoteReference.js
+++ b/src/components/Footnote/FootnoteReference.js
@@ -22,10 +22,6 @@ const FootnoteReference = ({ nodeData: { id, refname } }) => {
const { footnotes } = useContext(FootnoteContext);
const { darkMode } = useDarkMode();
- // the nodeData originates from docutils, and may be incorrect for
- // anonymous footnoteReferences originating from included files -- docutils
- // appears to assign IDs within the included files before they are collated
-
const ref = refname || id.replace('id', '');
const uid = refname ? `${refname}-${id}` : id;
diff --git a/src/components/StructuredData/BreadcrumbSchema.js b/src/components/StructuredData/BreadcrumbSchema.js
index c8f3f6e39..a68cc58fc 100644
--- a/src/components/StructuredData/BreadcrumbSchema.js
+++ b/src/components/StructuredData/BreadcrumbSchema.js
@@ -1,46 +1,28 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { getCompleteBreadcrumbData, getFullBreadcrumbPath } from '../../utils/get-complete-breadcrumb-data.js';
import { useBreadcrumbs } from '../../hooks/use-breadcrumbs';
import useSnootyMetadata from '../../utils/use-snooty-metadata';
-import { STRUCTURED_DATA_CLASSNAME } from '../../utils/structured-data.js';
-
-const getBreadcrumbList = (breadcrumbs) =>
- breadcrumbs.map(({ path, title }, index) => {
- path = getFullBreadcrumbPath(path, true);
-
- return {
- '@type': 'ListItem',
- position: index + 1,
- name: title,
- item: path,
- };
- });
+import { BreadcrumbListSd, STRUCTURED_DATA_CLASSNAME } from '../../utils/structured-data.js';
+import { useSiteMetadata } from '../../hooks/use-site-metadata.js';
const BreadcrumbSchema = ({ slug }) => {
const { parentPaths, title: siteTitle } = useSnootyMetadata();
+ const { siteUrl } = useSiteMetadata();
const parentPathsSlug = parentPaths[slug];
const queriedCrumbs = useBreadcrumbs();
- const breadcrumbList = React.useMemo(
- () => [
- ...getBreadcrumbList([
- ...getCompleteBreadcrumbData({ siteTitle, slug, queriedCrumbs, parentPaths: parentPathsSlug }),
- ]),
- ],
- [siteTitle, slug, queriedCrumbs, parentPathsSlug]
- );
+
+ const breadcrumbSd = React.useMemo(() => {
+ const sd = new BreadcrumbListSd({ siteUrl, siteTitle, slug, queriedCrumbs, parentPaths: parentPathsSlug });
+ return sd.isValid() ? sd.toString() : undefined;
+ }, [siteUrl, siteTitle, slug, queriedCrumbs, parentPathsSlug]);
return (
<>
- {Array.isArray(queriedCrumbs.breadcrumbs) && (
+ {Array.isArray(queriedCrumbs.breadcrumbs) && breadcrumbSd && (
)}
>
diff --git a/src/hooks/use-site-metadata.js b/src/hooks/use-site-metadata.js
index 698b58a06..11c37299c 100644
--- a/src/hooks/use-site-metadata.js
+++ b/src/hooks/use-site-metadata.js
@@ -12,6 +12,7 @@ export const useSiteMetadata = () => {
parserUser
patchId
pathPrefix
+ project
reposDatabase
siteUrl
snootyBranch
diff --git a/src/utils/get-complete-breadcrumb-data.js b/src/utils/get-complete-breadcrumb-data.js
index 6b25aacca..427d149ae 100644
--- a/src/utils/get-complete-breadcrumb-data.js
+++ b/src/utils/get-complete-breadcrumb-data.js
@@ -1,7 +1,6 @@
import { withPrefix } from 'gatsby';
-import { baseUrl } from './base-url';
+import { baseUrl, joinUrlAndPath } from './base-url';
import { assertTrailingSlash } from './assert-trailing-slash';
-import { removeLeadingSlash } from './remove-leading-slash';
import { assertLeadingSlash } from './assert-leading-slash';
import { isRelativeUrl } from './is-relative-url';
import { getUrl, getCompleteUrl } from './url-utils';
@@ -26,17 +25,18 @@ const nodesToString = (titleNodes) => {
.join('');
};
-export const getFullBreadcrumbPath = (path, needsPrefix) => {
- if (needsPrefix) {
- path = withPrefix(path);
- }
+export const getFullBreadcrumbPath = (siteUrl, path, needsPrefix) => {
if (isRelativeUrl(path)) {
- path = baseUrl() + removeLeadingSlash(path);
+ if (needsPrefix) {
+ path = withPrefix(path);
+ }
+ path = joinUrlAndPath(siteUrl, path);
}
return assertTrailingSlash(path);
};
export const getCompleteBreadcrumbData = ({
+ siteUrl,
siteTitle,
slug,
queriedCrumbs,
@@ -50,7 +50,7 @@ export const getCompleteBreadcrumbData = ({
//get intermediate breadcrumbs
const intermediateCrumbs = (queriedCrumbs?.breadcrumbs ?? []).map((crumb) => {
- return { ...crumb, path: getFullBreadcrumbPath(crumb.path, false) };
+ return { ...crumb, path: getFullBreadcrumbPath(siteUrl, crumb.path, false) };
});
const homeCrumb = {
diff --git a/src/utils/structured-data.js b/src/utils/structured-data.js
index becd2082d..91a2d2928 100644
--- a/src/utils/structured-data.js
+++ b/src/utils/structured-data.js
@@ -9,6 +9,7 @@
import { getFullLanguageName } from './get-language';
import { findKeyValuePair } from './find-key-value-pair';
import { getPlaintext } from './get-plaintext';
+import { getCompleteBreadcrumbData, getFullBreadcrumbPath } from './get-complete-breadcrumb-data';
// Class name to help Smartling identify all structured data, if needed
export const STRUCTURED_DATA_CLASSNAME = 'structured_data';
@@ -58,6 +59,27 @@ export class StructuredData {
}
}
+export class BreadcrumbListSd extends StructuredData {
+ constructor({ siteUrl, siteTitle, slug, queriedCrumbs, parentPaths }) {
+ super('BreadcrumbList');
+ const breadcrumbs = getCompleteBreadcrumbData({ siteUrl, siteTitle, slug, queriedCrumbs, parentPaths });
+ this.itemListElement = this.getBreadcrumbList(breadcrumbs, siteUrl);
+ }
+
+ /**
+ * @param {object[]} breadcrumbs
+ * @param {string} siteUrl
+ */
+ getBreadcrumbList(breadcrumbs, siteUrl) {
+ return breadcrumbs.map(({ path, title }, index) => ({
+ '@type': 'ListItem',
+ position: index + 1,
+ name: title,
+ item: getFullBreadcrumbPath(siteUrl, path, true),
+ }));
+ }
+}
+
class HowToSd extends StructuredData {
constructor({ steps, name }) {
super('HowTo');
diff --git a/tests/unit/BreadcrumbContainer.test.js b/tests/unit/BreadcrumbContainer.test.js
index f9a746656..c59f98ab6 100644
--- a/tests/unit/BreadcrumbContainer.test.js
+++ b/tests/unit/BreadcrumbContainer.test.js
@@ -1,16 +1,24 @@
import React from 'react';
+import * as Gatsby from 'gatsby';
import { render } from '@testing-library/react';
import BreadcrumbContainer from '../../src/components/Breadcrumbs/BreadcrumbContainer';
import mockData from './data/Breadcrumbs.test.json';
jest.mock(`../../src/utils/use-snooty-metadata`, () => jest.fn());
+const useStaticQuery = jest.spyOn(Gatsby, 'useStaticQuery');
+useStaticQuery.mockImplementation(() => ({
+ site: {
+ siteMetadata: {
+ siteUrl: 'https://www.mongodb.com/',
+ },
+ },
+}));
+
const mountBreadcrumbContainer = (breadcrumbs) => {
return render();
};
-jest.mock(`../../src/utils/use-snooty-metadata`, () => jest.fn());
-
const mockIntermediateCrumbs = {
title: 'MongoDB Atlas',
path: 'https://www.mongodb.com/docs/atlas/',
diff --git a/tests/unit/BreadcrumbSchema.test.js b/tests/unit/BreadcrumbSchema.test.js
new file mode 100644
index 000000000..816ebe92a
--- /dev/null
+++ b/tests/unit/BreadcrumbSchema.test.js
@@ -0,0 +1,48 @@
+import React from 'react';
+import * as Gatsby from 'gatsby';
+import { render } from '@testing-library/react';
+import useSnootyMetadata from '../../src/utils/use-snooty-metadata';
+import BreadcrumbSchema from '../../src/components/StructuredData/BreadcrumbSchema';
+import { mockWithPrefix } from '../utils/mock-with-prefix';
+import mockParents from './data/Breadcrumbs.test.json';
+
+jest.mock(`../../src/utils/use-snooty-metadata`, () => jest.fn());
+
+const mockIntermediateCrumbs = [
+ {
+ title: 'MongoDB Atlas',
+ path: 'https://www.mongodb.com/docs/atlas',
+ },
+];
+
+const useStaticQuery = jest.spyOn(Gatsby, 'useStaticQuery');
+useStaticQuery.mockImplementation(() => ({
+ site: {
+ siteMetadata: {
+ siteUrl: 'https://www.mongodb.com/',
+ },
+ },
+ allBreadcrumb: {
+ nodes: [
+ {
+ project: 'realm',
+ breadcrumbs: mockIntermediateCrumbs,
+ propertyUrl: 'https://www.mongodb.com/docs/atlas/device-sdks/',
+ },
+ ],
+ },
+}));
+
+describe('BreadcrumbSchema', () => {
+ beforeAll(() => {
+ useSnootyMetadata.mockImplementation(() => ({
+ parentPaths: mockParents,
+ }));
+ mockWithPrefix('/docs/atlas/device-sdks');
+ });
+
+ it('returns correct structured data with parents and intermediate breadcrumbs', () => {
+ const { asFragment } = render();
+ expect(asFragment()).toMatchSnapshot();
+ });
+});
diff --git a/tests/unit/Breadcrumbs.test.js b/tests/unit/Breadcrumbs.test.js
index dc8357909..4edd97781 100644
--- a/tests/unit/Breadcrumbs.test.js
+++ b/tests/unit/Breadcrumbs.test.js
@@ -4,6 +4,7 @@ import { render } from '@testing-library/react';
import Breadcrumbs from '../../src/components/Breadcrumbs/index';
import useSnootyMetadata from '../../src/utils/use-snooty-metadata';
+import { mockWithPrefix } from '../utils/mock-with-prefix';
import mockData from './data/Breadcrumbs.test.json';
jest.mock(`../../src/utils/use-snooty-metadata`, () => jest.fn());
@@ -18,7 +19,7 @@ beforeAll(() => {
const mockIntermediateCrumbs = [
{
title: 'MongoDB Atlas',
- path: '/atlas',
+ path: 'https://www.mongodb.com/docs/atlas/',
},
];
const useStaticQuery = jest.spyOn(Gatsby, 'useStaticQuery');
@@ -34,8 +35,15 @@ useStaticQuery.mockImplementation(() => ({
},
],
},
+ site: {
+ siteMetadata: {
+ siteUrl: 'https://www.mongodb.com/',
+ },
+ },
}));
+mockWithPrefix('/docs/atlas/device-sdks');
+
it('renders correctly with siteTitle', () => {
const tree = render();
expect(tree.asFragment()).toMatchSnapshot();
diff --git a/tests/unit/__snapshots__/BreadcrumbSchema.test.js.snap b/tests/unit/__snapshots__/BreadcrumbSchema.test.js.snap
new file mode 100644
index 000000000..92d980368
--- /dev/null
+++ b/tests/unit/__snapshots__/BreadcrumbSchema.test.js.snap
@@ -0,0 +1,12 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`BreadcrumbSchema returns correct structured data with parents and intermediate breadcrumbs 1`] = `
+
+
+
+`;
diff --git a/tests/utils/mock-with-prefix.js b/tests/utils/mock-with-prefix.js
new file mode 100644
index 000000000..56fd0160f
--- /dev/null
+++ b/tests/utils/mock-with-prefix.js
@@ -0,0 +1,24 @@
+import * as Gatsby from 'gatsby';
+
+const withPrefix = jest.spyOn(Gatsby, 'withPrefix');
+
+export const mockWithPrefix = (prefix) => {
+ withPrefix.mockImplementation((path) => {
+ let normalizedPrefix = prefix;
+ let normalizedPath = path;
+
+ if (!normalizedPrefix.startsWith('/')) {
+ normalizedPrefix = `/${normalizedPrefix}`;
+ }
+
+ if (normalizedPrefix.endsWith('/')) {
+ normalizedPrefix = normalizedPath.slice(0, -1);
+ }
+
+ if (normalizedPath.startsWith('/')) {
+ normalizedPath = normalizedPath.slice(1);
+ }
+
+ return `${normalizedPrefix}/${normalizedPath}`;
+ });
+};