From 4df1fb95e0097edaa5d3207480c8bbe60056dd29 Mon Sep 17 00:00:00 2001
From: rayangler <27821750+rayangler@users.noreply.github.com>
Date: Wed, 11 Sep 2024 16:11:10 -0400
Subject: [PATCH 1/6] DOP-4920: Add SearchAction structured data (#1230)
---
src/components/DocumentBody.js | 2 +-
src/components/StructuredData/DocsLandingSD.js | 8 ++++++++
2 files changed, 9 insertions(+), 1 deletion(-)
diff --git a/src/components/DocumentBody.js b/src/components/DocumentBody.js
index 7d7c78f8c..20b82c43a 100644
--- a/src/components/DocumentBody.js
+++ b/src/components/DocumentBody.js
@@ -223,7 +223,7 @@ export const Head = ({ pageContext, data }) => {
const pageTitle = getPlaintext(getNestedValue(['slugToTitle', lookup], metadata));
const siteTitle = getSiteTitle(metadata);
- const isDocsLandingHomepage = metadata.project === 'landing' && template === 'landing';
+ const isDocsLandingHomepage = metadata.project === 'landing' && template === 'landing' && slug === '/';
const needsBreadcrumbs = template === 'document' || template === undefined;
// Retrieves the canonical URL based on certain situations
diff --git a/src/components/StructuredData/DocsLandingSD.js b/src/components/StructuredData/DocsLandingSD.js
index f599ffadd..4f83fe332 100644
--- a/src/components/StructuredData/DocsLandingSD.js
+++ b/src/components/StructuredData/DocsLandingSD.js
@@ -18,6 +18,14 @@ const DocsLandingSD = () => (
},
author: 'MongoDB Documentation Team',
inLanguage: 'English',
+ potentialAction: {
+ '@type': 'SearchAction',
+ target: {
+ '@type': 'EntryPoint',
+ urlTemplate: 'https://mongodb.com/docs/search/?q={search_term_string}&page=1',
+ },
+ 'query-input': 'required name=search_term_string',
+ },
})}
);
From e85adf0bbec64e5669eb2aa8af2955e7a279e1c8 Mon Sep 17 00:00:00 2001
From: rayangler <27821750+rayangler@users.noreply.github.com>
Date: Thu, 19 Sep 2024 11:55:11 -0400
Subject: [PATCH 2/6] DOP-4918: Add VideoObject structured data to Video
component (#1236)
---
src/components/Video/index.js | 57 ++++++++++++++++-----
tests/unit/Video.test.js | 57 ++++++++++++++++++++-
tests/unit/__snapshots__/Video.test.js.snap | 4 +-
3 files changed, 100 insertions(+), 18 deletions(-)
diff --git a/src/components/Video/index.js b/src/components/Video/index.js
index 4b0029e89..922252fb6 100644
--- a/src/components/Video/index.js
+++ b/src/components/Video/index.js
@@ -68,10 +68,26 @@ const getTheSupportedMedia = (url) => {
return REACT_PLAYERS[supportedType];
};
-const Video = ({ nodeData: { argument }, ...rest }) => {
+const Video = ({ nodeData: { argument, options = {} } }) => {
const url = `${argument[0]['refuri']}`;
// use placeholder image for video thumbnail if invalid URL provided
const [previewImage, setPreviewImage] = useState(withPrefix('assets/meta_generic.png'));
+ const { title, description, 'upload-date': uploadDate, 'thumbnail-url': thumbnailUrl } = options;
+ // Required fields based on https://developers.google.com/search/docs/appearance/structured-data/video#video-object
+ const hasAllReqFields = [url, title, uploadDate, thumbnailUrl].every((val) => !!val);
+
+ const structuredData = {
+ '@context': 'https://schema.org',
+ '@type': 'VideoObject',
+ embedUrl: url,
+ name: title,
+ uploadDate,
+ thumbnailUrl,
+ };
+
+ if (description) {
+ structuredData['description'] = description;
+ }
useEffect(() => {
// handles URL validity checking for well-formed YT links
@@ -104,25 +120,38 @@ const Video = ({ nodeData: { argument }, ...rest }) => {
}
return (
-
- }
- light={previewImage}
- />
-
+ <>
+ {hasAllReqFields && (
+
+ )}
+
+ }
+ light={previewImage}
+ />
+
+ >
);
};
Video.propTypes = {
nodeData: PropTypes.shape({
argument: PropTypes.array.isRequired,
+ options: PropTypes.shape({
+ title: PropTypes.string,
+ description: PropTypes.string,
+ 'upload-date': PropTypes.string,
+ 'thumbnail-url': PropTypes.string,
+ }),
}).isRequired,
};
diff --git a/tests/unit/Video.test.js b/tests/unit/Video.test.js
index 09adb0924..0c16f2366 100644
--- a/tests/unit/Video.test.js
+++ b/tests/unit/Video.test.js
@@ -5,12 +5,15 @@ import Video from '../../src/components/Video';
// data for this component
import mockData from './data/Video.test.json';
+const REACT_PLAYER_QUERY = 'div.react-player__preview';
+const SD_SCRIPT_QUERY = 'script[type="application/ld+json"]';
+
it('YouTube video renders correctly', () => {
const tree = render( );
expect(tree.asFragment()).toMatchSnapshot();
});
-it('Vimeo video renders correctly', () => {
+it('Vimeo video renders null', () => {
const tree = render( );
expect(tree.asFragment()).toMatchSnapshot();
});
@@ -20,7 +23,7 @@ it('Wistia video renders correctly', () => {
expect(tree.asFragment()).toMatchSnapshot();
});
-it('DailyMotion video renders correctly', () => {
+it('DailyMotion video renders null', () => {
const tree = render( );
expect(tree.asFragment()).toMatchSnapshot();
});
@@ -40,3 +43,53 @@ describe('Console warning messages', () => {
]);
});
});
+
+describe('Structured data', () => {
+ it('is defined when all fields required by Google are present', () => {
+ const mockNodeData = { ...mockData.validYouTubeURL };
+ mockNodeData['options'] = {
+ title: 'Test Video',
+ 'upload-date': '2023-11-08T05:00:28-08:00',
+ 'thumbnail-url': 'https://i.ytimg.com/vi/o2ss2LJNZVE/maxresdefault.jpg',
+ };
+
+ const { container } = render( );
+ const videoPlayer = container.querySelector(REACT_PLAYER_QUERY);
+ const sdScript = container.querySelector(SD_SCRIPT_QUERY);
+
+ expect(videoPlayer).toBeInTheDocument();
+ expect(sdScript).toBeInTheDocument();
+ expect(sdScript.textContent).toEqual(
+ '{"@context":"https://schema.org","@type":"VideoObject","embedUrl":"https://www.youtube.com/watch?v=YBOiX8DwinE&ab_channel=MongoDB","name":"Test Video","uploadDate":"2023-11-08T05:00:28-08:00","thumbnailUrl":"https://i.ytimg.com/vi/o2ss2LJNZVE/maxresdefault.jpg"}'
+ );
+ });
+
+ it('is defined with optional fields', () => {
+ const mockNodeData = { ...mockData.validYouTubeURL };
+ mockNodeData['options'] = {
+ title: 'Test Video',
+ 'upload-date': '2023-11-08T05:00:28-08:00',
+ 'thumbnail-url': 'https://i.ytimg.com/vi/o2ss2LJNZVE/maxresdefault.jpg',
+ description: 'Optional description field',
+ };
+
+ const { container } = render( );
+ const videoPlayer = container.querySelector(REACT_PLAYER_QUERY);
+ const sdScript = container.querySelector(SD_SCRIPT_QUERY);
+
+ expect(videoPlayer).toBeInTheDocument();
+ expect(sdScript).toBeInTheDocument();
+ expect(sdScript.textContent).toEqual(
+ '{"@context":"https://schema.org","@type":"VideoObject","embedUrl":"https://www.youtube.com/watch?v=YBOiX8DwinE&ab_channel=MongoDB","name":"Test Video","uploadDate":"2023-11-08T05:00:28-08:00","thumbnailUrl":"https://i.ytimg.com/vi/o2ss2LJNZVE/maxresdefault.jpg","description":"Optional description field"}'
+ );
+ });
+
+ it('is not defined when missing one or more field(s) required by Google', () => {
+ const mockNodeData = { ...mockData.validYouTubeURL };
+ const { container } = render( );
+ const videoPlayer = container.querySelector(REACT_PLAYER_QUERY);
+ const sdScript = container.querySelector(SD_SCRIPT_QUERY);
+ expect(videoPlayer).toBeInTheDocument();
+ expect(sdScript).not.toBeInTheDocument();
+ });
+});
diff --git a/tests/unit/__snapshots__/Video.test.js.snap b/tests/unit/__snapshots__/Video.test.js.snap
index 71675fe8d..1339d9499 100644
--- a/tests/unit/__snapshots__/Video.test.js.snap
+++ b/tests/unit/__snapshots__/Video.test.js.snap
@@ -1,8 +1,8 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`DailyMotion video renders correctly 1`] = ` `;
+exports[`DailyMotion video renders null 1`] = ` `;
-exports[`Vimeo video renders correctly 1`] = ` `;
+exports[`Vimeo video renders null 1`] = ` `;
exports[`Wistia video renders correctly 1`] = `
From d7c87020cfedfcc79762a4eb02674f475ee5e412 Mon Sep 17 00:00:00 2001
From: Seung Park
Date: Fri, 27 Sep 2024 11:36:17 -0400
Subject: [PATCH 3/6] DOP-4915: Add Tech Article Structured Data to page
(#1249)
---
.../gatsby-source-snooty-prod/gatsby-node.js | 2 +
src/components/DocumentBody.js | 18 ++-
src/utils/structured-data.js | 126 ++++++++++++++++++
3 files changed, 145 insertions(+), 1 deletion(-)
create mode 100644 src/utils/structured-data.js
diff --git a/plugins/gatsby-source-snooty-prod/gatsby-node.js b/plugins/gatsby-source-snooty-prod/gatsby-node.js
index 34344ec97..cba7efee3 100644
--- a/plugins/gatsby-source-snooty-prod/gatsby-node.js
+++ b/plugins/gatsby-source-snooty-prod/gatsby-node.js
@@ -160,6 +160,7 @@ exports.sourceNodes = async ({ actions, createContentDigest, createNodeId, getNo
id: key,
page_id: key,
ast: doc.ast,
+ facets: doc.facets,
internal: {
type: 'Page',
contentDigest: createContentDigest(doc),
@@ -379,6 +380,7 @@ exports.createSchemaCustomization = ({ actions }) => {
branch: String
pagePath: String
ast: JSON!
+ facets: [JSON]
metadata: SnootyMetadata @link
componentNames: [String!]
}
diff --git a/src/components/DocumentBody.js b/src/components/DocumentBody.js
index 20b82c43a..579ac3833 100644
--- a/src/components/DocumentBody.js
+++ b/src/components/DocumentBody.js
@@ -1,4 +1,4 @@
-import React, { useState, lazy } from 'react';
+import React, { useState, useMemo, lazy } from 'react';
import PropTypes from 'prop-types';
import { graphql } from 'gatsby';
import { Global, css } from '@emotion/react';
@@ -13,6 +13,7 @@ import { getTemplate } from '../utils/get-template';
import useSnootyMetadata from '../utils/use-snooty-metadata';
import { getCurrentLocaleFontFamilyValue } from '../utils/locale';
import { getSiteTitle } from '../utils/get-site-title';
+import { constructTechArticle } from '../utils/structured-data';
import { PageContext } from '../context/page-context';
import { useBreadcrumbs } from '../hooks/use-breadcrumbs';
import { isBrowser } from '../utils/is-browser';
@@ -230,6 +231,15 @@ export const Head = ({ pageContext, data }) => {
// i.e. eol'd, non-eol'd, snooty.toml or ..metadata:: directive (highest priority)
const canonical = useCanonicalUrl(meta, metadata, slug, repoBranches);
+ // construct Structured Data
+ const techArticleSd = useMemo(() => {
+ if (['product-landing', 'landing', 'search', 'errorpage', 'drivers-index'].includes(template)) {
+ return;
+ }
+ const techArticle = constructTechArticle({ facets: data.page.facets || [], pageTitle });
+ return techArticle.isValid() ? techArticle : undefined;
+ }, [data.page.facets, pageTitle, template]);
+
return (
<>
{
{twitter.length > 0 && twitter.map((c) => )}
{isDocsLandingHomepage && }
{needsBreadcrumbs && }
+ {techArticleSd && (
+
+ )}
>
);
};
@@ -251,6 +266,7 @@ export const query = graphql`
query ($page_id: String, $slug: String) {
page(id: { eq: $page_id }) {
ast
+ facets
}
pageImage(slug: { eq: $slug }) {
slug
diff --git a/src/utils/structured-data.js b/src/utils/structured-data.js
new file mode 100644
index 000000000..7649eb819
--- /dev/null
+++ b/src/utils/structured-data.js
@@ -0,0 +1,126 @@
+/**
+ * Classes to construct Structured Data JSON.
+ * Required props should be read in constructor function (to fail validity).
+ * Optional props can be set conditionally.
+ * Constant values should be set in the constructor function.
+ * Optional overwrites can be set in params as default values
+ */
+
+export class StructuredData {
+ constructor(type) {
+ this['@context'] = 'https://schema.org';
+ this['@type'] = type;
+ }
+
+ isValid() {
+ function recursiveValidity(param) {
+ // array
+ if (Array.isArray(param)) {
+ return param.every((e) => recursiveValidity(e));
+ }
+ // object
+ else if (param && typeof param === 'object') {
+ return Object.keys(param).every((e) => {
+ if (param.hasOwnProperty(e)) return recursiveValidity(param[e]);
+ return true;
+ });
+ }
+
+ // string or number
+ return String(param).length > 0;
+ }
+
+ return recursiveValidity(this);
+ }
+
+ toString() {
+ return JSON.stringify(this);
+ }
+
+ static addCompanyToName(name) {
+ if (!name) {
+ return name;
+ }
+ if (Array.isArray(name)) {
+ return name.map(this.addCompanyToName);
+ }
+ if (name.toLowerCase().includes('mongodb')) {
+ return name;
+ }
+ return `MongoDB ` + name;
+ }
+}
+
+export class TechArticleSd extends StructuredData {
+ constructor({ headline, mainEntity, genre }) {
+ super('TechArticle');
+
+ this.author = {
+ '@type': 'Organization',
+ name: 'MongoDB Documentation Team',
+ };
+ this.headline = headline;
+ this.mainEntity = mainEntity;
+ if (genre) {
+ this.genre = genre;
+ }
+ }
+}
+
+/**
+ * get TechArticle Structured Data from page facets and pageTitle.
+ * @param {{category: string, sub_facets: object[]}[]} facets
+ * @param {string} pageTitle
+ * @returns {StructuredData}
+ */
+export const constructTechArticle = ({ facets, pageTitle }) => {
+ // get display name from facets
+ function getDisplayName(facet) {
+ return facet.display_name;
+ }
+
+ // extract genre facets
+ function getGenreNames(facets) {
+ return facets?.filter((facet) => facet.category === 'genre').map(getDisplayName) || [];
+ }
+
+ // extract target product facets
+ function getTargetProductsNames(facets) {
+ // TODO: these products and sub products need version data from facets
+ // https://jira.mongodb.org/browse/DOP-5037
+ let res = [];
+ const productFacets = facets?.filter((facet) => facet.category === 'target_product') || [];
+ for (let index = 0; index < productFacets.length; index++) {
+ const productFacet = productFacets[index];
+ const subProducts =
+ productFacet.sub_facets?.filter((facet) => facet.category === 'sub_product').map(getDisplayName) || [];
+ if (subProducts.length) {
+ res = res.concat(subProducts);
+ } else {
+ res.push(getDisplayName(productFacet));
+ }
+ }
+
+ return res;
+ }
+
+ const techArticleProps = {
+ mainEntity: getTargetProductsNames(facets).map((name) => ({
+ '@type': 'SoftwareApplication',
+ name: StructuredData.addCompanyToName(name),
+ applicationCategory: 'DeveloperApplication',
+ offers: {
+ price: 0,
+ priceCurrency: 'USD',
+ },
+ })),
+ headline: pageTitle,
+ };
+
+ const genres = getGenreNames(facets);
+ if (genres.length) {
+ techArticleProps['genre'] = genres;
+ }
+
+ return new TechArticleSd(techArticleProps);
+};
From d9e1421cd3affdce90a1ec187326cd3533417bd2 Mon Sep 17 00:00:00 2001
From: Seung Park
Date: Fri, 27 Sep 2024 16:48:37 -0400
Subject: [PATCH 4/6] DOP-4919: add how-to structured data to procedures
(#1255)
---
src/components/Procedure/index.js | 34 +-
src/context/ancestor-components-context.js | 1 +
src/utils/structured-data.js | 72 ++
.../unit/__snapshots__/Procedure.test.js.snap | 15 +
.../structured-data.test.js.snap | 58 +
tests/unit/utils/structured-data.test.js | 11 +
tests/utils/data/how-to-structured-data.json | 1020 +++++++++++++++++
7 files changed, 1205 insertions(+), 6 deletions(-)
create mode 100644 tests/unit/utils/__snapshots__/structured-data.test.js.snap
create mode 100644 tests/unit/utils/structured-data.test.js
create mode 100644 tests/utils/data/how-to-structured-data.json
diff --git a/src/components/Procedure/index.js b/src/components/Procedure/index.js
index 478bbc9ba..05eff7a71 100644
--- a/src/components/Procedure/index.js
+++ b/src/components/Procedure/index.js
@@ -2,7 +2,12 @@ import React, { useMemo } from 'react';
import PropTypes from 'prop-types';
import styled from '@emotion/styled';
import { palette } from '@leafygreen-ui/palette';
+import {
+ AncestorComponentContextProvider,
+ useAncestorComponentContext,
+} from '../../context/ancestor-components-context';
import { theme } from '../../theme/docsTheme';
+import { constructHowToSd } from '../../utils/structured-data';
import Step from './Step';
const StyledProcedure = styled('div')`
@@ -47,18 +52,35 @@ const getSteps = (children) => {
return steps;
};
-const Procedure = ({ nodeData: { children, options }, ...rest }) => {
+const Procedure = ({ nodeData, ...rest }) => {
// Make the style 'connected' by default for now to give time for PLPs that use this directive to
// add the "style" option
+ const children = nodeData['children'];
+ const options = nodeData['options'];
const style = options?.style || 'connected';
const steps = useMemo(() => getSteps(children), [children]);
+ const ancestors = useAncestorComponentContext();
+
+ // construct Structured Data
+ const howToSd = useMemo(() => {
+ if (ancestors['procedure']) return undefined;
+ const howToSd = constructHowToSd({ steps });
+ return howToSd.isValid() ? howToSd.toString() : undefined;
+ }, [steps, ancestors]);
return (
-
- {steps.map((child, i) => (
-
- ))}
-
+
+ {howToSd && (
+ // using dangerouslySetInnerHTML as JSON is rendered with
+ // encoded quotes at build time
+
+ )}
+
+ {steps.map((child, i) => (
+
+ ))}
+
+
);
};
diff --git a/src/context/ancestor-components-context.js b/src/context/ancestor-components-context.js
index 9ecbad74c..f09e1ad44 100644
--- a/src/context/ancestor-components-context.js
+++ b/src/context/ancestor-components-context.js
@@ -2,6 +2,7 @@ import React, { createContext, useContext } from 'react';
const defaultVal = {
table: false,
+ procedure: false,
};
const AncestorComponentContext = createContext(defaultVal);
diff --git a/src/utils/structured-data.js b/src/utils/structured-data.js
index 7649eb819..3065c146c 100644
--- a/src/utils/structured-data.js
+++ b/src/utils/structured-data.js
@@ -6,6 +6,9 @@
* Optional overwrites can be set in params as default values
*/
+import { findKeyValuePair } from './find-key-value-pair';
+import { getPlaintext } from './get-plaintext';
+
export class StructuredData {
constructor(type) {
this['@context'] = 'https://schema.org';
@@ -124,3 +127,72 @@ export const constructTechArticle = ({ facets, pageTitle }) => {
return new TechArticleSd(techArticleProps);
};
+
+class HowToSd extends StructuredData {
+ constructor({ steps }) {
+ super('HowTo');
+
+ this.steps = steps;
+ // TODO: DOP-5040
+ // include name and default image
+ }
+}
+
+export const constructHowToSd = ({ steps }) => {
+ function getHowToSection(procedureDirective, name) {
+ const howToSection = {
+ '@type': 'HowToSection',
+ name,
+ itemListElement: [],
+ };
+
+ for (const step of procedureDirective.children) {
+ handleStep(step, howToSection['itemListElement']);
+ }
+
+ return howToSection;
+ }
+
+ /**
+ *
+ * @param {node} step
+ * @param {step[]} targetList can be either steps[] of HowTo or itemListElement[] of HowToSection
+ */
+ function handleStep(step, targetList) {
+ if (step['name'] !== 'step') {
+ return;
+ }
+ // text of step is derived from children, or fallback to step's argument
+ const childText = getPlaintext(step.children);
+ const argText = getPlaintext(step.argument);
+ // NOTE: step.argument is repeated in step.children as a Heading component
+ // so strip the heading from children
+ const bodyText = childText.replace(argText, '');
+
+ // deep search for nested procedure to make sibling sections
+ const nestedProcedure = findKeyValuePair(step.children, 'name', 'procedure');
+ if (nestedProcedure) {
+ targetList.push(getHowToSection(nestedProcedure, argText || bodyText));
+ } else {
+ // build step
+ const stepSD = {
+ '@type': 'HowToStep',
+ text: bodyText.length ? bodyText : argText,
+ };
+ if (bodyText.length && argText) {
+ stepSD['name'] = argText;
+ }
+ targetList.push(stepSD);
+ }
+ }
+
+ const howToProps = {
+ steps: [],
+ };
+
+ for (const step of steps) {
+ handleStep(step, howToProps['steps']);
+ }
+
+ return new HowToSd(howToProps);
+};
diff --git a/tests/unit/__snapshots__/Procedure.test.js.snap b/tests/unit/__snapshots__/Procedure.test.js.snap
index 2ea8d736b..fd8dee19c 100644
--- a/tests/unit/__snapshots__/Procedure.test.js.snap
+++ b/tests/unit/__snapshots__/Procedure.test.js.snap
@@ -2,6 +2,11 @@
exports[`renders correctly 1`] = `
+
.emotion-0 {
margin-top: 16px;
background-color: initial;
@@ -224,6 +229,11 @@ exports[`renders correctly 1`] = `
exports[`renders steps nested in include nodes 1`] = `
+
.emotion-0 {
margin-top: 16px;
background-color: initial;
@@ -793,6 +803,11 @@ exports[`renders steps nested in include nodes 1`] = `
exports[`renders with "normal" or YAML steps styling 1`] = `
+
.emotion-0 {
margin-top: 16px;
background-color: initial;
diff --git a/tests/unit/utils/__snapshots__/structured-data.test.js.snap b/tests/unit/utils/__snapshots__/structured-data.test.js.snap
new file mode 100644
index 000000000..d99956c58
--- /dev/null
+++ b/tests/unit/utils/__snapshots__/structured-data.test.js.snap
@@ -0,0 +1,58 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Structured Data HowTo Structured Data converts steps into expected structured data format 1`] = `
+HowToSd {
+ "@context": "https://schema.org",
+ "@type": "HowTo",
+ "steps": [
+ {
+ "@type": "HowToStep",
+ "name": "In Atlas, go to your federated database instance for your project.",
+ "text": "If it's not already displayed, select the
+organization that contains your project from the
+ Organizations menu in the navigation bar.If it's not already displayed, select your project
+from the Projects menu in the navigation bar.In the sidebar, click Data Federation under
+the Services heading.The Data Federation page displays.",
+ },
+ {
+ "@type": "HowToStep",
+ "name": "Click Create Federated Database Instance.",
+ "text": "If you have an existing federated database instance, instead click
+Create Federated Database in the
+top right corner of the dashboard.",
+ },
+ {
+ "@type": "HowToSection",
+ "itemListElement": [
+ {
+ "@type": "HowToStep",
+ "name": "Rename the default collection.",
+ "text": "Click next to the default collection
+VirtualCollection0 to edit its name. For this tutorial,
+rename your collection Sessions.",
+ },
+ {
+ "@type": "HowToStep",
+ "name": "Create a second collection.",
+ "text": "Click next to the default name
+VirtualDatabase0 to add a collection to the database.
+For this tutorial, name your new collection Users.",
+ },
+ {
+ "@type": "HowToStep",
+ "name": "Add data to your virtual database.",
+ "text": "Drag and drop the following data sources into the
+respective federated database instance virtual collections:/mflix/sessions.json, into the Sessions
+collection, and/mflix/users.json into the Users collection.",
+ },
+ ],
+ "name": "Connect to a Data Source and add sample data to your federated database instance.",
+ },
+ {
+ "@type": "HowToStep",
+ "name": "Click Save.",
+ "text": "Your federated database instance appears on the Data Federation page.",
+ },
+ ],
+}
+`;
diff --git a/tests/unit/utils/structured-data.test.js b/tests/unit/utils/structured-data.test.js
new file mode 100644
index 000000000..476479037
--- /dev/null
+++ b/tests/unit/utils/structured-data.test.js
@@ -0,0 +1,11 @@
+import { constructHowToSd } from '../../../src/utils/structured-data';
+import stepsData from '../../utils/data/how-to-structured-data.json';
+
+describe('Structured Data', () => {
+ describe('HowTo Structured Data', () => {
+ it('converts steps into expected structured data format', () => {
+ const howToSd = constructHowToSd({ steps: stepsData });
+ expect(howToSd).toMatchSnapshot();
+ });
+ });
+});
diff --git a/tests/utils/data/how-to-structured-data.json b/tests/utils/data/how-to-structured-data.json
new file mode 100644
index 000000000..0448552dd
--- /dev/null
+++ b/tests/utils/data/how-to-structured-data.json
@@ -0,0 +1,1020 @@
+[
+ {
+ "type": "directive",
+ "children": [
+ {
+ "type": "section",
+ "children": [
+ {
+ "type": "heading",
+ "children": [
+ {
+ "type": "text",
+ "value": "In "
+ },
+ {
+ "type": "substitution_reference",
+ "children": [
+ {
+ "type": "text",
+ "value": "Atlas"
+ }
+ ],
+ "name": "service"
+ },
+ {
+ "type": "text",
+ "value": ", go to your federated database instance for your project."
+ }
+ ],
+ "id": "in---go-to-your-federated-database-instance-for-your-project."
+ },
+ {
+ "type": "list",
+ "children": [
+ {
+ "type": "listItem",
+ "children": [
+ {
+ "type": "paragraph",
+ "children": [
+ {
+ "type": "text",
+ "value": "If it's not already displayed, select the\norganization that contains your project from the\n"
+ },
+ {
+ "type": "substitution_reference",
+ "children": [
+ {
+ "type": "role",
+ "children": [],
+ "domain": "",
+ "name": "icon-mms",
+ "target": "office",
+ "flag": ""
+ },
+ {
+ "type": "text",
+ "value": " "
+ },
+ {
+ "type": "role",
+ "children": [
+ {
+ "type": "text",
+ "value": "Organizations"
+ }
+ ],
+ "domain": "",
+ "name": "guilabel",
+ "target": "",
+ "flag": ""
+ },
+ {
+ "type": "text",
+ "value": " menu"
+ }
+ ],
+ "name": "ui-org-menu"
+ },
+ {
+ "type": "text",
+ "value": " in the navigation bar."
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "listItem",
+ "children": [
+ {
+ "type": "paragraph",
+ "children": [
+ {
+ "type": "text",
+ "value": "If it's not already displayed, select your project\nfrom the "
+ },
+ {
+ "type": "role",
+ "children": [
+ {
+ "type": "text",
+ "value": "Projects"
+ }
+ ],
+ "domain": "",
+ "name": "guilabel",
+ "target": "",
+ "flag": ""
+ },
+ {
+ "type": "text",
+ "value": " menu in the navigation bar."
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "listItem",
+ "children": [
+ {
+ "type": "paragraph",
+ "children": [
+ {
+ "type": "text",
+ "value": "In the sidebar, click "
+ },
+ {
+ "type": "role",
+ "children": [
+ {
+ "type": "text",
+ "value": "Data Federation"
+ }
+ ],
+ "domain": "",
+ "name": "guilabel",
+ "target": "",
+ "flag": ""
+ },
+ {
+ "type": "text",
+ "value": " under\nthe "
+ },
+ {
+ "type": "role",
+ "children": [
+ {
+ "type": "text",
+ "value": "Services"
+ }
+ ],
+ "domain": "",
+ "name": "guilabel",
+ "target": "",
+ "flag": ""
+ },
+ {
+ "type": "text",
+ "value": " heading."
+ }
+ ]
+ },
+ {
+ "type": "paragraph",
+ "children": [
+ {
+ "type": "text",
+ "value": "The "
+ },
+ {
+ "type": "reference",
+ "children": [
+ {
+ "type": "text",
+ "value": "Data Federation"
+ }
+ ],
+ "refuri": "https://cloud.mongodb.com/go?l=https%3A%2F%2Fcloud.mongodb.com%2Fv2%2F%3Cproject%3E%23%2FdataFederation"
+ },
+ {
+ "type": "text",
+ "value": " page displays."
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "enumtype": "loweralpha"
+ }
+ ]
+ }
+ ],
+ "domain": "mongodb",
+ "name": "step",
+ "argument": [
+ {
+ "type": "text",
+ "position": {
+ "start": {
+ "line": 0
+ }
+ },
+ "value": "In "
+ },
+ {
+ "type": "substitution_reference",
+ "position": {
+ "start": {
+ "line": 0
+ }
+ },
+ "children": [
+ {
+ "type": "text",
+ "position": {
+ "start": {
+ "line": 0
+ }
+ },
+ "value": "Atlas"
+ }
+ ],
+ "name": "service"
+ },
+ {
+ "type": "text",
+ "position": {
+ "start": {
+ "line": 0
+ }
+ },
+ "value": ", go to your federated database instance for your project."
+ }
+ ]
+ },
+ {
+ "type": "directive",
+ "children": [
+ {
+ "type": "section",
+ "children": [
+ {
+ "type": "heading",
+ "children": [
+ {
+ "type": "text",
+ "value": "Click "
+ },
+ {
+ "type": "role",
+ "children": [
+ {
+ "type": "text",
+ "value": "Create Federated Database Instance"
+ }
+ ],
+ "domain": "",
+ "name": "guilabel",
+ "target": "",
+ "flag": ""
+ },
+ {
+ "type": "text",
+ "value": "."
+ }
+ ],
+ "id": "click-create-federated-database-instance."
+ },
+ {
+ "type": "paragraph",
+ "children": [
+ {
+ "type": "text",
+ "value": "If you have an existing federated database instance, instead click\n"
+ },
+ {
+ "type": "role",
+ "children": [
+ {
+ "type": "text",
+ "value": "Create Federated Database"
+ }
+ ],
+ "domain": "",
+ "name": "guilabel",
+ "target": "",
+ "flag": ""
+ },
+ {
+ "type": "text",
+ "value": " in the\ntop right corner of the dashboard."
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "domain": "mongodb",
+ "name": "step",
+ "argument": [
+ {
+ "type": "text",
+ "position": {
+ "start": {
+ "line": 5
+ }
+ },
+ "value": "Click "
+ },
+ {
+ "type": "role",
+ "position": {
+ "start": {
+ "line": 5
+ }
+ },
+ "children": [
+ {
+ "type": "text",
+ "position": {
+ "start": {
+ "line": 5
+ }
+ },
+ "value": "Create Federated Database Instance"
+ }
+ ],
+ "domain": "",
+ "name": "guilabel",
+ "target": "",
+ "flag": ""
+ },
+ {
+ "type": "text",
+ "position": {
+ "start": {
+ "line": 5
+ }
+ },
+ "value": "."
+ }
+ ]
+ },
+ {
+ "type": "directive",
+ "children": [
+ {
+ "type": "section",
+ "children": [
+ {
+ "type": "heading",
+ "children": [
+ {
+ "type": "text",
+ "value": "Connect to a Data Source and add sample data to your federated database instance."
+ }
+ ],
+ "id": "connect-to-a-data-source-and-add-sample-data-to-your-federated-database-instance."
+ },
+ {
+ "type": "paragraph",
+ "children": [
+ {
+ "type": "text",
+ "value": "You can use a sample dataset to start exploring\nAtlas SQL through Atlas Data Federation without configuring a data source\nyourself. This tutorial references a specific sample dataset."
+ }
+ ]
+ },
+ {
+ "type": "directive",
+ "children": [
+ {
+ "type": "paragraph",
+ "children": [
+ {
+ "type": "text",
+ "value": "To connect to your own data instead, click\n"
+ },
+ {
+ "type": "role",
+ "children": [
+ {
+ "type": "text",
+ "value": "Add Data Sources"
+ }
+ ],
+ "domain": "",
+ "name": "guilabel",
+ "target": "",
+ "flag": ""
+ },
+ {
+ "type": "text",
+ "value": ". To learn more about\nconfiguring different types of data sources, see\n"
+ },
+ {
+ "type": "ref_role",
+ "children": [
+ {
+ "type": "text",
+ "value": "Define Data Stores for a Federated Database Instance"
+ }
+ ],
+ "domain": "std",
+ "name": "label",
+ "target": "config-adf",
+ "flag": "",
+ "fileid": ["data-federation/config/config-data-stores", "std-label-config-adf"]
+ },
+ {
+ "type": "text",
+ "value": "."
+ }
+ ]
+ },
+ {
+ "type": "paragraph",
+ "children": [
+ {
+ "type": "text",
+ "value": "If you want to configure data from a "
+ },
+ {
+ "type": "substitution_reference",
+ "children": [
+ {
+ "type": "text",
+ "value": "Atlas"
+ }
+ ],
+ "name": "service"
+ },
+ {
+ "type": "text",
+ "value": " cluster, you\nmust use MongoDB version 5.0 or greater for that cluster to\ntake advantage of Atlas SQL."
+ }
+ ]
+ }
+ ],
+ "domain": "",
+ "name": "note",
+ "argument": []
+ },
+ {
+ "type": "list",
+ "children": [
+ {
+ "type": "listItem",
+ "children": [
+ {
+ "type": "paragraph",
+ "children": [
+ {
+ "type": "text",
+ "value": "Click "
+ },
+ {
+ "type": "role",
+ "children": [
+ {
+ "type": "text",
+ "value": "Add Sample Data"
+ }
+ ],
+ "domain": "",
+ "name": "guilabel",
+ "target": "",
+ "flag": ""
+ },
+ {
+ "type": "text",
+ "value": "."
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "listItem",
+ "children": [
+ {
+ "type": "paragraph",
+ "children": [
+ {
+ "type": "text",
+ "value": "Select "
+ },
+ {
+ "type": "literal",
+ "children": [
+ {
+ "type": "text",
+ "value": "AWS S3"
+ }
+ ]
+ },
+ {
+ "type": "text",
+ "value": " from the "
+ },
+ {
+ "type": "role",
+ "children": [
+ {
+ "type": "text",
+ "value": "Filter"
+ }
+ ],
+ "domain": "",
+ "name": "guilabel",
+ "target": "",
+ "flag": ""
+ },
+ {
+ "type": "text",
+ "value": " dropdown if it\nisn't selected already."
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "listItem",
+ "children": [
+ {
+ "type": "paragraph",
+ "children": [
+ {
+ "type": "text",
+ "value": "Expand the "
+ },
+ {
+ "type": "substitution_reference",
+ "children": [
+ {
+ "type": "role",
+ "children": [
+ {
+ "type": "text",
+ "value": "S3 (Simple Storage Service)"
+ }
+ ],
+ "domain": "",
+ "name": "abbr",
+ "target": "",
+ "flag": ""
+ }
+ ],
+ "name": "s3"
+ },
+ {
+ "type": "text",
+ "value": " store "
+ },
+ {
+ "type": "literal",
+ "children": [
+ {
+ "type": "text",
+ "value": "sample-data-atlas-data-lake"
+ }
+ ]
+ },
+ {
+ "type": "text",
+ "value": "\nif it isn't expanded already."
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "enumtype": "loweralpha"
+ },
+ {
+ "type": "paragraph",
+ "children": [
+ {
+ "type": "text",
+ "value": "For this tutorial, configure your federated database instance as follows using the\n"
+ },
+ {
+ "type": "role",
+ "children": [
+ {
+ "type": "text",
+ "value": "Federated Database Instance"
+ }
+ ],
+ "domain": "",
+ "name": "guilabel",
+ "target": "",
+ "flag": ""
+ },
+ {
+ "type": "text",
+ "value": " panel:"
+ }
+ ]
+ },
+ {
+ "type": "directive",
+ "children": [
+ {
+ "type": "directive",
+ "children": [
+ {
+ "type": "section",
+ "children": [
+ {
+ "type": "heading",
+ "children": [
+ {
+ "type": "text",
+ "value": "Rename the default collection."
+ }
+ ],
+ "id": "rename-the-default-collection."
+ },
+ {
+ "type": "paragraph",
+ "children": [
+ {
+ "type": "text",
+ "value": "Click "
+ },
+ {
+ "type": "role",
+ "children": [],
+ "domain": "",
+ "name": "icon-fa4",
+ "target": "pencil",
+ "flag": ""
+ },
+ {
+ "type": "text",
+ "value": " next to the default collection\n"
+ },
+ {
+ "type": "literal",
+ "children": [
+ {
+ "type": "text",
+ "value": "VirtualCollection0"
+ }
+ ]
+ },
+ {
+ "type": "text",
+ "value": " to edit its name. For this tutorial,\nrename your collection "
+ },
+ {
+ "type": "literal",
+ "children": [
+ {
+ "type": "text",
+ "value": "Sessions"
+ }
+ ]
+ },
+ {
+ "type": "text",
+ "value": "."
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "domain": "mongodb",
+ "name": "step",
+ "argument": [
+ {
+ "type": "text",
+ "position": {
+ "start": {
+ "line": 42
+ }
+ },
+ "value": "Rename the default collection."
+ }
+ ]
+ },
+ {
+ "type": "directive",
+ "children": [
+ {
+ "type": "section",
+ "children": [
+ {
+ "type": "heading",
+ "children": [
+ {
+ "type": "text",
+ "value": "Create a second collection."
+ }
+ ],
+ "id": "create-a-second-collection."
+ },
+ {
+ "type": "paragraph",
+ "children": [
+ {
+ "type": "text",
+ "value": "Click "
+ },
+ {
+ "type": "role",
+ "children": [],
+ "domain": "",
+ "name": "icon-fa4",
+ "target": "plus-square",
+ "flag": ""
+ },
+ {
+ "type": "text",
+ "value": " next to the default name\n"
+ },
+ {
+ "type": "literal",
+ "children": [
+ {
+ "type": "text",
+ "value": "VirtualDatabase0"
+ }
+ ]
+ },
+ {
+ "type": "text",
+ "value": " to add a collection to the database.\nFor this tutorial, name your new collection "
+ },
+ {
+ "type": "literal",
+ "children": [
+ {
+ "type": "text",
+ "value": "Users"
+ }
+ ]
+ },
+ {
+ "type": "text",
+ "value": "."
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "domain": "mongodb",
+ "name": "step",
+ "argument": [
+ {
+ "type": "text",
+ "position": {
+ "start": {
+ "line": 48
+ }
+ },
+ "value": "Create a second collection."
+ }
+ ]
+ },
+ {
+ "type": "directive",
+ "children": [
+ {
+ "type": "section",
+ "children": [
+ {
+ "type": "heading",
+ "children": [
+ {
+ "type": "text",
+ "value": "Add data to your virtual database."
+ }
+ ],
+ "id": "add-data-to-your-virtual-database."
+ },
+ {
+ "type": "paragraph",
+ "children": [
+ {
+ "type": "text",
+ "value": "Drag and drop the following data sources into the\nrespective federated database instance virtual collections:"
+ }
+ ]
+ },
+ {
+ "type": "list",
+ "children": [
+ {
+ "type": "listItem",
+ "children": [
+ {
+ "type": "paragraph",
+ "children": [
+ {
+ "type": "literal",
+ "children": [
+ {
+ "type": "text",
+ "value": "/mflix/sessions.json"
+ }
+ ]
+ },
+ {
+ "type": "text",
+ "value": ", into the "
+ },
+ {
+ "type": "literal",
+ "children": [
+ {
+ "type": "text",
+ "value": "Sessions"
+ }
+ ]
+ },
+ {
+ "type": "text",
+ "value": "\ncollection, and"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "listItem",
+ "children": [
+ {
+ "type": "paragraph",
+ "children": [
+ {
+ "type": "literal",
+ "children": [
+ {
+ "type": "text",
+ "value": "/mflix/users.json"
+ }
+ ]
+ },
+ {
+ "type": "text",
+ "value": " into the "
+ },
+ {
+ "type": "literal",
+ "children": [
+ {
+ "type": "text",
+ "value": "Users"
+ }
+ ]
+ },
+ {
+ "type": "text",
+ "value": " collection."
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "enumtype": "unordered"
+ }
+ ]
+ }
+ ],
+ "domain": "mongodb",
+ "name": "step",
+ "argument": [
+ {
+ "type": "text",
+ "position": {
+ "start": {
+ "line": 54
+ }
+ },
+ "value": "Add data to your virtual database."
+ }
+ ]
+ }
+ ],
+ "domain": "mongodb",
+ "name": "procedure",
+ "argument": [],
+ "options": {
+ "style": "normal"
+ }
+ }
+ ]
+ }
+ ],
+ "domain": "mongodb",
+ "name": "step",
+ "argument": [
+ {
+ "type": "text",
+ "position": {
+ "start": {
+ "line": 11
+ }
+ },
+ "value": "Connect to a Data Source and add sample data to your federated database instance."
+ }
+ ]
+ },
+ {
+ "type": "directive",
+ "children": [
+ {
+ "type": "section",
+ "children": [
+ {
+ "type": "heading",
+ "children": [
+ {
+ "type": "text",
+ "value": "Click "
+ },
+ {
+ "type": "role",
+ "children": [
+ {
+ "type": "text",
+ "value": "Save"
+ }
+ ],
+ "domain": "",
+ "name": "guilabel",
+ "target": "",
+ "flag": ""
+ },
+ {
+ "type": "text",
+ "value": "."
+ }
+ ],
+ "id": "click-save."
+ },
+ {
+ "type": "paragraph",
+ "children": [
+ {
+ "type": "text",
+ "value": "Your federated database instance appears on the "
+ },
+ {
+ "type": "role",
+ "children": [
+ {
+ "type": "text",
+ "value": "Data Federation"
+ }
+ ],
+ "domain": "",
+ "name": "guilabel",
+ "target": "",
+ "flag": ""
+ },
+ {
+ "type": "text",
+ "value": " page."
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "domain": "mongodb",
+ "name": "step",
+ "argument": [
+ {
+ "type": "text",
+ "position": {
+ "start": {
+ "line": 63
+ }
+ },
+ "value": "Click "
+ },
+ {
+ "type": "role",
+ "position": {
+ "start": {
+ "line": 63
+ }
+ },
+ "children": [
+ {
+ "type": "text",
+ "position": {
+ "start": {
+ "line": 63
+ }
+ },
+ "value": "Save"
+ }
+ ],
+ "domain": "",
+ "name": "guilabel",
+ "target": "",
+ "flag": ""
+ },
+ {
+ "type": "text",
+ "position": {
+ "start": {
+ "line": 63
+ }
+ },
+ "value": "."
+ }
+ ]
+ }
+]
From 07747ff829290acf33ebd2a91e7d58cbb620c946 Mon Sep 17 00:00:00 2001
From: rayangler <27821750+rayangler@users.noreply.github.com>
Date: Mon, 30 Sep 2024 14:52:53 -0400
Subject: [PATCH 5/6] DOP-4916: Implement SoftwareSourceCode structured data
(#1258)
---
.../Breadcrumbs/BreadcrumbContainer.js | 2 +-
src/components/Code/Code.js | 144 ++++++++++--------
src/components/Code/CodeIO.js | 16 +-
src/components/Code/Output.js | 67 +++++---
.../StructuredData/DocsLandingSD.js | 2 +-
src/components/Video/index.js | 33 ++--
src/utils/get-language.js | 60 ++++++++
src/utils/structured-data.js | 56 +++++--
tests/unit/CodeIO.test.js | 8 +-
tests/unit/__snapshots__/Code.test.js.snap | 10 ++
tests/unit/__snapshots__/CodeIO.test.js.snap | 108 +++++++------
.../__snapshots__/Collapsible.test.js.snap | 5 +
.../__snapshots__/LiteralInclude.test.js.snap | 5 +
.../ReleaseSpecification.test.js.snap | 5 +
.../structured-data.test.js.snap | 41 +++++
tests/unit/utils/structured-data.test.js | 44 +++++-
16 files changed, 426 insertions(+), 180 deletions(-)
diff --git a/src/components/Breadcrumbs/BreadcrumbContainer.js b/src/components/Breadcrumbs/BreadcrumbContainer.js
index 68e67936e..8ded22a2e 100644
--- a/src/components/Breadcrumbs/BreadcrumbContainer.js
+++ b/src/components/Breadcrumbs/BreadcrumbContainer.js
@@ -86,7 +86,7 @@ const crumbObjectShape = {
};
BreadcrumbContainer.propTypes = {
- breadcrumbs: PropTypes.shape(crumbObjectShape).isRequired,
+ breadcrumbs: PropTypes.arrayOf(PropTypes.shape(crumbObjectShape)).isRequired,
};
export default BreadcrumbContainer;
diff --git a/src/components/Code/Code.js b/src/components/Code/Code.js
index e5d5a080f..081955720 100644
--- a/src/components/Code/Code.js
+++ b/src/components/Code/Code.js
@@ -1,5 +1,5 @@
import { css } from '@emotion/react';
-import React, { useCallback, useContext } from 'react';
+import React, { useCallback, useContext, useMemo } from 'react';
import PropTypes from 'prop-types';
import styled from '@emotion/styled';
import { default as CodeBlock } from '@leafygreen-ui/code';
@@ -12,6 +12,8 @@ import { TabContext } from '../Tabs/tab-context';
import { reportAnalytics } from '../../utils/report-analytics';
import { getLanguage } from '../../utils/get-language';
import { DRIVER_ICON_MAP } from '../icons/DriverIconMap';
+import { SoftwareSourceCodeSd } from '../../utils/structured-data';
+import { usePageContext } from '../../context/page-context';
import { baseCodeStyle, borderCodeStyle, lgStyles } from './styles/codeStyle';
import { CodeContext } from './code-context';
@@ -41,6 +43,7 @@ const Code = ({
const { setActiveTab } = useContext(TabContext);
const { languageOptions, codeBlockLanguage } = useContext(CodeContext);
const { darkMode } = useDarkMode();
+ const { slug } = usePageContext();
const code = value;
let language = (languageOptions?.length > 0 && codeBlockLanguage) || getLanguage(lang);
@@ -85,77 +88,92 @@ const Code = ({
reportAnalytics('CodeblockCopied', { code });
}, [code]);
+ const softwareSourceCodeSd = useMemo(() => {
+ const sd = new SoftwareSourceCodeSd({ code, lang, slug });
+ return sd.isValid() ? sd.toString() : undefined;
+ }, [code, lang, slug]);
+
return (
- div > div {
- display: grid;
- grid-template-columns: ${!copyable && (languageOptions?.length === 0 || language === 'none')
- ? 'auto 0px !important'
- : 'code panel'};
- }
-
- > div {
- border-top-left-radius: ${captionBorderRadius};
- border-top-right-radius: ${captionBorderRadius};
- display: grid;
- border-color: ${palette.gray.light2};
-
- .dark-theme & {
- border-color: ${palette.gray.dark2};
+ <>
+ {softwareSourceCodeSd && (
+
+ )}
+
div > div {
+ display: grid;
+ grid-template-columns: ${!copyable && (languageOptions?.length === 0 || language === 'none')
+ ? 'auto 0px !important'
+ : 'code panel'};
}
- }
- pre {
- background-color: ${palette.gray.light3};
- color: ${palette.black};
+ > div {
+ border-top-left-radius: ${captionBorderRadius};
+ border-top-right-radius: ${captionBorderRadius};
+ display: grid;
+ border-color: ${palette.gray.light2};
- .dark-theme & {
- background-color: ${palette.black};
- color: ${palette.gray.light3};
+ .dark-theme & {
+ border-color: ${palette.gray.dark2};
+ }
}
- }
- [data-testid='leafygreen-code-panel'] {
- background-color: ${palette.white};
- border-color: ${palette.gray.light2};
+ pre {
+ background-color: ${palette.gray.light3};
+ color: ${palette.black};
- .dark-theme & {
- background-color: ${palette.gray.dark2};
- border-color: ${palette.gray.dark2};
+ .dark-theme & {
+ background-color: ${palette.black};
+ color: ${palette.gray.light3};
+ }
}
- }
-
- ${lgStyles}
- `}
- >
- {captionSpecified && (
-
-
- {caption}
-
-
- )}
-
{
- setActiveTab({ drivers: selectedOption.id });
- }}
- onCopy={reportCodeCopied}
- showLineNumbers={linenos}
- showCustomActionButtons={sourceSpecified}
- customActionButtons={customActionButtonList}
- lineNumberStart={lineno_start}
+
+ [data-testid='leafygreen-code-panel'] {
+ background-color: ${palette.white};
+ border-color: ${palette.gray.light2};
+
+ .dark-theme & {
+ background-color: ${palette.gray.dark2};
+ border-color: ${palette.gray.dark2};
+ }
+ }
+
+ ${lgStyles}
+ `}
>
- {code}
-
-
+ {captionSpecified && (
+
+
+ {caption}
+
+
+ )}
+
{
+ setActiveTab({ drivers: selectedOption.id });
+ }}
+ onCopy={reportCodeCopied}
+ showLineNumbers={linenos}
+ showCustomActionButtons={sourceSpecified}
+ customActionButtons={customActionButtonList}
+ lineNumberStart={lineno_start}
+ >
+ {code}
+
+
+ >
);
};
diff --git a/src/components/Code/CodeIO.js b/src/components/Code/CodeIO.js
index bada950aa..ae627b3e5 100644
--- a/src/components/Code/CodeIO.js
+++ b/src/components/Code/CodeIO.js
@@ -17,6 +17,10 @@ const outputButtonStyling = LeafyCss`
margin: 8px;
`;
+const outputContainerStyle = (showOutput) => LeafyCss`
+ ${!showOutput && 'display: none;'}
+`;
+
const CodeIO = ({ nodeData: { children }, ...rest }) => {
const { darkMode } = useDarkMode();
const needsIOToggle = children.length === 2;
@@ -32,12 +36,8 @@ const CodeIO = ({ nodeData: { children }, ...rest }) => {
const outputBorderRadius = !showOutput ? '12px' : '0px';
const singleInputBorderRadius = onlyInputSpecified ? '12px' : '0px';
- const handleClick = (e) => {
- if (showOutput) {
- setShowOutput(false);
- } else {
- setShowOutput(true);
- }
+ const handleClick = () => {
+ setShowOutput((val) => !val);
};
if (children.length === 0) {
@@ -77,7 +77,9 @@ const CodeIO = ({ nodeData: { children }, ...rest }) => {
{buttonText}
- {showOutput && }
+
+
+
>
)}
{onlyInputSpecified && }
diff --git a/src/components/Code/Output.js b/src/components/Code/Output.js
index dbdb2f9b2..3b93509c9 100644
--- a/src/components/Code/Output.js
+++ b/src/components/Code/Output.js
@@ -1,10 +1,12 @@
-import React from 'react';
+import React, { useMemo } from 'react';
import styled from '@emotion/styled';
import PropTypes from 'prop-types';
import { default as CodeBlock } from '@leafygreen-ui/code';
import { useDarkMode } from '@leafygreen-ui/leafygreen-provider';
import { palette } from '@leafygreen-ui/palette';
import { getLanguage } from '../../utils/get-language';
+import { SoftwareSourceCodeSd } from '../../utils/structured-data';
+import { usePageContext } from '../../context/page-context';
const OutputContainer = styled.div`
> div > * {
@@ -27,36 +29,51 @@ const OutputContainer = styled.div`
}
`;
-const Output = ({ nodeData: { children }, ...rest }) => {
+const Output = ({ nodeData: { children } }) => {
const { darkMode } = useDarkMode();
const { emphasize_lines, value, linenos, lang, lineno_start } = children[0];
const language = getLanguage(lang);
+ const { slug } = usePageContext();
+ const softwareSourceCodeSd = useMemo(() => {
+ const sd = new SoftwareSourceCodeSd({ code: value, lang, slug });
+ return sd.isValid() ? sd.toString() : undefined;
+ }, [value, lang, slug]);
return (
-
-
+ {softwareSourceCodeSd && (
+
+ )}
+
- {value}
-
-
+
+ {value}
+
+
+ >
);
};
diff --git a/src/components/StructuredData/DocsLandingSD.js b/src/components/StructuredData/DocsLandingSD.js
index 4f83fe332..26e47e55f 100644
--- a/src/components/StructuredData/DocsLandingSD.js
+++ b/src/components/StructuredData/DocsLandingSD.js
@@ -2,7 +2,7 @@ import React from 'react';
import { baseUrl } from '../../utils/base-url';
const DocsLandingSD = () => (
-
+ {videoObjectSd && (
+
)}
{
if (Object.values(Language).includes(lang)) {
return lang;
@@ -15,3 +55,23 @@ export const getLanguage = (lang) => {
}
return 'none';
};
+
+/**
+ * @param {string | undefined} lang The language passed to the code block directive
+ * @returns {string | undefined} The formal name of the language, if it exists
+ */
+export const getFullLanguageName = (lang, slug) => {
+ const normalizedLang = lang?.toLowerCase();
+ if (!normalizedLang || ['none', 'text'].includes(normalizedLang)) {
+ return undefined;
+ }
+
+ const fullLangName = LANGUAGE_NAMES.hasOwnProperty(normalizedLang) ? LANGUAGE_NAMES[normalizedLang] : undefined;
+ if (!fullLangName) {
+ console.warn(
+ `${normalizedLang} in ${slug} does not have a valid language name for structured data SEO. Please contact DOP to add.`
+ );
+ }
+
+ return fullLangName;
+};
diff --git a/src/utils/structured-data.js b/src/utils/structured-data.js
index 3065c146c..77f888e06 100644
--- a/src/utils/structured-data.js
+++ b/src/utils/structured-data.js
@@ -6,6 +6,7 @@
* Optional overwrites can be set in params as default values
*/
+import { getFullLanguageName } from './get-language';
import { findKeyValuePair } from './find-key-value-pair';
import { getPlaintext } from './get-plaintext';
@@ -54,7 +55,30 @@ export class StructuredData {
}
}
-export class TechArticleSd extends StructuredData {
+class HowToSd extends StructuredData {
+ constructor({ steps }) {
+ super('HowTo');
+
+ this.steps = steps;
+ // TODO: DOP-5040
+ // include name and default image
+ }
+}
+
+export class SoftwareSourceCodeSd extends StructuredData {
+ constructor({ code, lang, slug }) {
+ super('SoftwareSourceCode');
+ this.codeSampleType = 'code snippet';
+ this.text = code;
+
+ const programmingLanguage = getFullLanguageName(lang, slug);
+ if (programmingLanguage) {
+ this.programmingLanguage = programmingLanguage;
+ }
+ }
+}
+
+class TechArticleSd extends StructuredData {
constructor({ headline, mainEntity, genre }) {
super('TechArticle');
@@ -70,6 +94,26 @@ export class TechArticleSd extends StructuredData {
}
}
+export class VideoObjectSd extends StructuredData {
+ constructor({ embedUrl, name, uploadDate, thumbnailUrl, description }) {
+ super('VideoObject');
+
+ this.embedUrl = embedUrl;
+ this.name = name;
+ this.uploadDate = uploadDate;
+ this.thumbnailUrl = thumbnailUrl;
+
+ if (description) {
+ this.description = description;
+ }
+ }
+
+ isValid() {
+ const hasAllReqFields = [this.embedUrl, this.name, this.uploadDate, this.thumbnailUrl].every((val) => !!val);
+ return hasAllReqFields && super.isValid();
+ }
+}
+
/**
* get TechArticle Structured Data from page facets and pageTitle.
* @param {{category: string, sub_facets: object[]}[]} facets
@@ -128,16 +172,6 @@ export const constructTechArticle = ({ facets, pageTitle }) => {
return new TechArticleSd(techArticleProps);
};
-class HowToSd extends StructuredData {
- constructor({ steps }) {
- super('HowTo');
-
- this.steps = steps;
- // TODO: DOP-5040
- // include name and default image
- }
-}
-
export const constructHowToSd = ({ steps }) => {
function getHowToSection(procedureDirective, name) {
const howToSection = {
diff --git a/tests/unit/CodeIO.test.js b/tests/unit/CodeIO.test.js
index 4debb6a3b..e24a5d124 100644
--- a/tests/unit/CodeIO.test.js
+++ b/tests/unit/CodeIO.test.js
@@ -15,16 +15,16 @@ describe('CodeIO', () => {
it('closes and opens output code snippet on io button click when output is visible by default', () => {
const wrapper = render( );
userEvent.click(wrapper.getByRole('button'));
- expect(wrapper.queryAllByText('hello world')).toHaveLength(0);
+ expect(wrapper.getByText('hello world')).not.toBeVisible();
userEvent.click(wrapper.getByRole('button'));
- expect(wrapper.getByText('hello world')).toBeTruthy();
+ expect(wrapper.getByText('hello world')).toBeVisible();
});
it('opens and closes output code snippet on io button click when output is hidden by default', () => {
const wrapper = render( );
userEvent.click(wrapper.getByRole('button'));
- expect(wrapper.getByText('hello world')).toBeTruthy();
+ expect(wrapper.getByText('hello world')).toBeVisible();
userEvent.click(wrapper.getByRole('button'));
- expect(wrapper.queryAllByText('hello world')).toHaveLength(0);
+ expect(wrapper.getByText('hello world')).not.toBeVisible();
});
});
diff --git a/tests/unit/__snapshots__/Code.test.js.snap b/tests/unit/__snapshots__/Code.test.js.snap
index 6267988e8..2ac3be798 100644
--- a/tests/unit/__snapshots__/Code.test.js.snap
+++ b/tests/unit/__snapshots__/Code.test.js.snap
@@ -2,6 +2,11 @@
exports[`renders correctly 1`] = `
+
.emotion-0 {
display: table;
margin: 24px 0;
@@ -402,6 +407,11 @@ exports[`renders correctly 1`] = `
exports[`renders correctly when none is passed in as a language 1`] = `
+
.emotion-0 {
display: table;
margin: 24px 0;
diff --git a/tests/unit/__snapshots__/CodeIO.test.js.snap b/tests/unit/__snapshots__/CodeIO.test.js.snap
index 983f5b7e4..08e775de1 100644
--- a/tests/unit/__snapshots__/CodeIO.test.js.snap
+++ b/tests/unit/__snapshots__/CodeIO.test.js.snap
@@ -505,33 +505,33 @@ exports[`CodeIO renders correctly 1`] = `
justify-self: right;
}
-.emotion-21>div>* {
+.emotion-22>div>* {
display: inline!important;
}
-.emotion-21 * {
+.emotion-22 * {
border-top-right-radius: 0px;
border-top-left-radius: 0px;
border-bottom-right-radius: 12px;
border-bottom-left-radius: 12px;
}
-.emotion-21>div {
+.emotion-22>div {
border: var(--code-container-border);
}
-.emotion-21>div>div>pre {
+.emotion-22>div>div>pre {
border: var(--code-pre-border);
border-top: none;
}
-.emotion-23 {
+.emotion-24 {
border: 1px solid #3D4F58;
border-radius: 12px;
overflow: hidden;
}
-.emotion-24 {
+.emotion-25 {
position: relative;
display: grid;
grid-template-areas: 'code panel';
@@ -541,8 +541,8 @@ exports[`CodeIO renders correctly 1`] = `
grid-template-areas: 'code code';
}
-.emotion-24:before,
-.emotion-24:after {
+.emotion-25:before,
+.emotion-25:after {
content: '';
display: block;
position: absolute;
@@ -556,20 +556,20 @@ exports[`CodeIO renders correctly 1`] = `
transition: box-shadow 100ms ease-in-out;
}
-.emotion-24:before {
+.emotion-25:before {
grid-column: 1;
left: -40px;
}
-.emotion-24:after {
+.emotion-25:after {
grid-column: 2;
}
-.emotion-24:after {
+.emotion-25:after {
grid-column: -1;
}
-.emotion-25 {
+.emotion-26 {
grid-area: code;
overflow-x: auto;
border-radius: inherit;
@@ -602,45 +602,45 @@ exports[`CodeIO renders correctly 1`] = `
}
@media only screen and (max-device-width: 812px) and (-webkit-min-device-pixel-ratio: 2) {
- .emotion-25 {
+ .emotion-26 {
white-space: pre-wrap;
}
}
@media only screen and (min-device-width: 813px) and (-webkit-min-device-pixel-ratio: 2) {
- .emotion-25 {
+ .emotion-26 {
white-space: pre;
}
}
-.emotion-25:focus-visible {
+.emotion-26:focus-visible {
outline: none;
box-shadow: 0 0 0 2px #0498EC inset;
}
-.emotion-28 {
+.emotion-29 {
background-color: transparent;
background-image: linear-gradient(90deg, #1C2D38, #001E2B);
background-attachment: fixed;
}
-.emotion-28>td {
+.emotion-29>td {
border-top: 1px solid #1C2D38;
}
-.emotion-28+tr>td {
+.emotion-29+tr>td {
border-top: 1px solid #1C2D38;
}
-.emotion-28+.emotion-28>td {
+.emotion-29+.emotion-29>td {
border-top: 0;
}
-.emotion-28:last-child>td {
+.emotion-29:last-child>td {
border-bottom: 1px solid #1C2D38;
}
-.emotion-29 {
+.emotion-30 {
border-spacing: 0;
vertical-align: top;
padding: 0 16px;
@@ -657,6 +657,11 @@ exports[`CodeIO renders correctly 1`] = `
+
@@ -766,44 +771,53 @@ exports[`CodeIO renders correctly 1`] = `
+
-
-
-
-
-
-
+
+
- 1
-
-
- hello world
-
-
-
-
-
-
+
+ 1
+
+
+ hello world
+
+
+
+
+
+
+
diff --git a/tests/unit/__snapshots__/Collapsible.test.js.snap b/tests/unit/__snapshots__/Collapsible.test.js.snap
index 6a068d4b6..8cfe60d3b 100644
--- a/tests/unit/__snapshots__/Collapsible.test.js.snap
+++ b/tests/unit/__snapshots__/Collapsible.test.js.snap
@@ -586,6 +586,11 @@ exports[`collapsible component renders all the content in the options and childr
>
This is collapsible content
+
diff --git a/tests/unit/__snapshots__/LiteralInclude.test.js.snap b/tests/unit/__snapshots__/LiteralInclude.test.js.snap
index 3fb0fb180..f55e71ab2 100644
--- a/tests/unit/__snapshots__/LiteralInclude.test.js.snap
+++ b/tests/unit/__snapshots__/LiteralInclude.test.js.snap
@@ -2,6 +2,11 @@
exports[`renders correctly 1`] = `
+
.emotion-0 {
display: table;
margin: 24px 0;
diff --git a/tests/unit/__snapshots__/ReleaseSpecification.test.js.snap b/tests/unit/__snapshots__/ReleaseSpecification.test.js.snap
index 2731a55ee..f812ed02a 100644
--- a/tests/unit/__snapshots__/ReleaseSpecification.test.js.snap
+++ b/tests/unit/__snapshots__/ReleaseSpecification.test.js.snap
@@ -2,6 +2,11 @@
exports[`renders correctly 1`] = `
+
.emotion-0 {
display: table;
margin: 24px 0;
diff --git a/tests/unit/utils/__snapshots__/structured-data.test.js.snap b/tests/unit/utils/__snapshots__/structured-data.test.js.snap
index d99956c58..f3a3930b0 100644
--- a/tests/unit/utils/__snapshots__/structured-data.test.js.snap
+++ b/tests/unit/utils/__snapshots__/structured-data.test.js.snap
@@ -56,3 +56,44 @@ collection, and/mflix/users.json into the Users collection.",
],
}
`;
+
+exports[`Structured Data SoftwareSourceCode returns valid structured data with programmingLanguage 1`] = `
+SoftwareSourceCodeSd {
+ "@context": "https://schema.org",
+ "@type": "SoftwareSourceCode",
+ "codeSampleType": "code snippet",
+ "text": "print("hello world")",
+}
+`;
+
+exports[`Structured Data SoftwareSourceCode returns valid structured data without programmingLangauge 1`] = `
+SoftwareSourceCodeSd {
+ "@context": "https://schema.org",
+ "@type": "SoftwareSourceCode",
+ "codeSampleType": "code snippet",
+ "text": "print("hello world")",
+}
+`;
+
+exports[`Structured Data VideoObject returns valid structured data with description 1`] = `
+VideoObjectSd {
+ "@context": "https://schema.org",
+ "@type": "VideoObject",
+ "description": "Learn more about indexes in Atlas Search",
+ "embedUrl": "https://www.youtube.com/embed/XrJG994YxD8",
+ "name": "Mastering Indexing for Perfect Query Matching",
+ "thumbnailUrl": "https://i.ytimg.com/vi/XrJG994YxD8/maxresdefault.jpg",
+ "uploadDate": "2023-11-08T05:00:28-08:00",
+}
+`;
+
+exports[`Structured Data VideoObject returns valid structured data without description 1`] = `
+VideoObjectSd {
+ "@context": "https://schema.org",
+ "@type": "VideoObject",
+ "embedUrl": "https://www.youtube.com/embed/XrJG994YxD8",
+ "name": "Mastering Indexing for Perfect Query Matching",
+ "thumbnailUrl": "https://i.ytimg.com/vi/XrJG994YxD8/maxresdefault.jpg",
+ "uploadDate": "2023-11-08T05:00:28-08:00",
+}
+`;
diff --git a/tests/unit/utils/structured-data.test.js b/tests/unit/utils/structured-data.test.js
index 476479037..17184dfe4 100644
--- a/tests/unit/utils/structured-data.test.js
+++ b/tests/unit/utils/structured-data.test.js
@@ -1,4 +1,4 @@
-import { constructHowToSd } from '../../../src/utils/structured-data';
+import { SoftwareSourceCodeSd, VideoObjectSd, constructHowToSd } from '../../../src/utils/structured-data';
import stepsData from '../../utils/data/how-to-structured-data.json';
describe('Structured Data', () => {
@@ -8,4 +8,46 @@ describe('Structured Data', () => {
expect(howToSd).toMatchSnapshot();
});
});
+
+ describe('SoftwareSourceCode', () => {
+ it('returns valid structured data with programmingLanguage', () => {
+ const code = 'print("hello world")';
+ const lang = 'py';
+ const softwareSourceCodeSd = new SoftwareSourceCodeSd({ code, lang });
+ expect(softwareSourceCodeSd.isValid()).toBeTruthy();
+ expect(softwareSourceCodeSd).toMatchSnapshot();
+ });
+
+ it('returns valid structured data without programmingLangauge', () => {
+ const code = 'print("hello world")';
+ const softwareSourceCodeSd = new SoftwareSourceCodeSd({ code });
+ expect(softwareSourceCodeSd.isValid()).toBeTruthy();
+ expect(softwareSourceCodeSd).toMatchSnapshot();
+ });
+ });
+
+ describe('VideoObject', () => {
+ const embedUrl = 'https://www.youtube.com/embed/XrJG994YxD8';
+ const name = 'Mastering Indexing for Perfect Query Matching';
+ const uploadDate = '2023-11-08T05:00:28-08:00';
+ const thumbnailUrl = 'https://i.ytimg.com/vi/XrJG994YxD8/maxresdefault.jpg';
+ const description = 'Learn more about indexes in Atlas Search';
+
+ it('returns valid structured data with description', () => {
+ const videoObjectSd = new VideoObjectSd({ embedUrl, name, uploadDate, thumbnailUrl, description });
+ expect(videoObjectSd.isValid()).toBeTruthy();
+ expect(videoObjectSd).toMatchSnapshot();
+ });
+
+ it('returns valid structured data without description', () => {
+ const videoObjectSd = new VideoObjectSd({ embedUrl, name, uploadDate, thumbnailUrl });
+ expect(videoObjectSd.isValid()).toBeTruthy();
+ expect(videoObjectSd).toMatchSnapshot();
+ });
+
+ it('returns invalid structured data with missing name field', () => {
+ const videoObjectSd = new VideoObjectSd({ embedUrl, uploadDate, thumbnailUrl, description });
+ expect(videoObjectSd.isValid()).toBeFalsy();
+ });
+ });
});
From bf3ecb4ff45fa8bc74c4df06f2de9adca4009f24 Mon Sep 17 00:00:00 2001
From: Seung Park
Date: Fri, 4 Oct 2024 16:43:55 -0400
Subject: [PATCH 6/6] DOP-5040 - add image and name to How To structured data
(#1267)
---
src/components/Collapsible/index.js | 37 +++++++-------
src/components/Procedure/index.js | 7 ++-
src/components/Section.js | 17 +++++--
src/components/Tabs/index.js | 17 +++++--
src/context/heading-context.js | 37 ++++++++++++++
src/utils/get-plaintext.js | 1 +
src/utils/structured-data.js | 12 +++--
tests/context/heading-context.test.js | 49 +++++++++++++++++++
.../unit/__snapshots__/Procedure.test.js.snap | 15 ------
.../structured-data.test.js.snap | 2 +
10 files changed, 146 insertions(+), 48 deletions(-)
create mode 100644 src/context/heading-context.js
create mode 100644 tests/context/heading-context.test.js
diff --git a/src/components/Collapsible/index.js b/src/components/Collapsible/index.js
index d77e83e4c..ea9b7cf71 100644
--- a/src/components/Collapsible/index.js
+++ b/src/components/Collapsible/index.js
@@ -6,6 +6,7 @@ import Icon from '@leafygreen-ui/icon';
import IconButton from '@leafygreen-ui/icon-button';
import { cx } from '@leafygreen-ui/emotion';
import { Body } from '@leafygreen-ui/typography';
+import { HeadingContextProvider } from '../../context/heading-context';
import { findAllNestedAttribute } from '../../utils/find-all-nested-attribute';
import { isBrowser } from '../../utils/is-browser';
import { reportAnalytics } from '../../utils/report-analytics';
@@ -65,25 +66,27 @@ const Collapsible = ({ nodeData: { children, options }, sectionDepth, ...rest })
}, [childrenHashIds, hash, open]);
return (
-
-
-
- {/* Adding 1 to reflect logic in parser, but want to show up as H2 for SEO reasons */}
-
- {heading}
-
- {subHeading}
+
+
+
+
+ {/* Adding 1 to reflect logic in parser, but want to show up as H2 for SEO reasons */}
+
+ {heading}
+
+ {subHeading}
+
+
+
+
+
+
+ {children.map((c, i) => (
+
+ ))}
-
-
-
-
-
- {children.map((c, i) => (
-
- ))}
-
+
);
};
diff --git a/src/components/Procedure/index.js b/src/components/Procedure/index.js
index 05eff7a71..f58ad5444 100644
--- a/src/components/Procedure/index.js
+++ b/src/components/Procedure/index.js
@@ -8,6 +8,7 @@ import {
} from '../../context/ancestor-components-context';
import { theme } from '../../theme/docsTheme';
import { constructHowToSd } from '../../utils/structured-data';
+import { useHeadingContext } from '../../context/heading-context';
import Step from './Step';
const StyledProcedure = styled('div')`
@@ -60,13 +61,15 @@ const Procedure = ({ nodeData, ...rest }) => {
const style = options?.style || 'connected';
const steps = useMemo(() => getSteps(children), [children]);
const ancestors = useAncestorComponentContext();
+ const { lastHeading } = useHeadingContext();
// construct Structured Data
const howToSd = useMemo(() => {
if (ancestors['procedure']) return undefined;
- const howToSd = constructHowToSd({ steps });
+
+ const howToSd = constructHowToSd({ steps, parentHeading: lastHeading });
return howToSd.isValid() ? howToSd.toString() : undefined;
- }, [steps, ancestors]);
+ }, [ancestors, lastHeading, steps]);
return (
diff --git a/src/components/Section.js b/src/components/Section.js
index 59e27fecc..7d7972ace 100644
--- a/src/components/Section.js
+++ b/src/components/Section.js
@@ -1,14 +1,21 @@
import React from 'react';
import PropTypes from 'prop-types';
+import { HeadingContextProvider } from '../context/heading-context';
+import { getPlaintext } from '../utils/get-plaintext';
+import { findKeyValuePair } from '../utils/find-key-value-pair';
import ComponentFactory from './ComponentFactory';
const Section = ({ sectionDepth, nodeData: { children }, ...rest }) => {
+ const headingNode = findKeyValuePair(children, 'type', 'heading');
+
return (
-
- {children.map((child, index) => {
- return ;
- })}
-
+
+
+ {children.map((child, index) => {
+ return ;
+ })}
+
+
);
};
diff --git a/src/components/Tabs/index.js b/src/components/Tabs/index.js
index b242891f2..7c0e54699 100644
--- a/src/components/Tabs/index.js
+++ b/src/components/Tabs/index.js
@@ -9,6 +9,8 @@ import { theme } from '../../theme/docsTheme';
import { reportAnalytics } from '../../utils/report-analytics';
import { getNestedValue } from '../../utils/get-nested-value';
import { isBrowser } from '../../utils/is-browser';
+import { HeadingContextProvider, useHeadingContext } from '../../context/heading-context';
+import { getPlaintext } from '../../utils/get-plaintext';
import { TabContext } from './tab-context';
const TAB_BUTTON_SELECTOR = 'button[role="tab"]';
@@ -115,6 +117,7 @@ const Tabs = ({ nodeData: { children, options = {} }, page, ...rest }) => {
// Hide tabset if it includes the :hidden: option, or if it is controlled by a dropdown selector
const isHidden = options.hidden || Object.keys(selectors).includes(tabsetName);
const isProductLanding = page?.options?.template === 'product-landing';
+ const { lastHeading } = useHeadingContext();
useEffect(() => {
const index = tabIds.indexOf(activeTabs[tabsetName]);
@@ -166,11 +169,15 @@ const Tabs = ({ nodeData: { children, options = {} }, page, ...rest }) => {
return (
-
- {tab.children.map((child, i) => (
-
- ))}
-
+
+
+ {tab.children.map((child, i) => (
+
+ ))}
+
+
);
})}
diff --git a/src/context/heading-context.js b/src/context/heading-context.js
new file mode 100644
index 000000000..61b159f18
--- /dev/null
+++ b/src/context/heading-context.js
@@ -0,0 +1,37 @@
+import React, { createContext, useContext } from 'react';
+
+const defaultVal = {
+ lastHeading: '',
+ ignoreNextheading: false,
+};
+
+const HeadingContext = createContext(defaultVal);
+
+/**
+ * Context provider to help track of page headings until the consuming component.
+ * Headings are pushed into a list, with the last being the nearest heading, upwards in the AST tree.
+ * Designed to be called in the init, so each child node of sections
+ * as consumers can access the section header.
+ *
+ * @param {node[]} children
+ * @param {string} heading
+ * @param {boolean} ignoreNextHeading
+ * @returns
+ */
+const HeadingContextProvider = ({ children, heading, ignoreNextHeading }) => {
+ const { lastHeading: prevHeading, ignoreNextHeading: skipHeading } = useHeadingContext();
+
+ const newHeading = skipHeading || !heading ? prevHeading : heading;
+
+ return (
+
+ {children}
+
+ );
+};
+
+const useHeadingContext = () => {
+ return useContext(HeadingContext);
+};
+
+export { HeadingContextProvider, useHeadingContext };
diff --git a/src/utils/get-plaintext.js b/src/utils/get-plaintext.js
index fd828fccc..1bdb1ca4d 100644
--- a/src/utils/get-plaintext.js
+++ b/src/utils/get-plaintext.js
@@ -10,6 +10,7 @@ const getPlaintext = (nodeArray) => {
} else if (node.children) {
return title + node.children.reduce(extractText, '');
}
+ return title;
};
return nodeArray && nodeArray.length > 0 ? nodeArray.reduce(extractText, '') : '';
diff --git a/src/utils/structured-data.js b/src/utils/structured-data.js
index 77f888e06..3c1cf9cfd 100644
--- a/src/utils/structured-data.js
+++ b/src/utils/structured-data.js
@@ -56,12 +56,15 @@ export class StructuredData {
}
class HowToSd extends StructuredData {
- constructor({ steps }) {
+ constructor({ steps, name }) {
super('HowTo');
this.steps = steps;
- // TODO: DOP-5040
- // include name and default image
+ this.name = name;
+ // image comes from Flora constants
+ // https://github.com/10gen/flora/blob/v1.14.2/src/MDBLogo/constants.ts
+ this.image =
+ 'https://webimages.mongodb.com/_com_assets/cms/kuyj2focmkbxv7gh3-stacked_default_slate_blue.svg?auto=format%252Ccompress';
}
}
@@ -172,7 +175,7 @@ export const constructTechArticle = ({ facets, pageTitle }) => {
return new TechArticleSd(techArticleProps);
};
-export const constructHowToSd = ({ steps }) => {
+export const constructHowToSd = ({ steps, parentHeading }) => {
function getHowToSection(procedureDirective, name) {
const howToSection = {
'@type': 'HowToSection',
@@ -222,6 +225,7 @@ export const constructHowToSd = ({ steps }) => {
const howToProps = {
steps: [],
+ name: parentHeading,
};
for (const step of steps) {
diff --git a/tests/context/heading-context.test.js b/tests/context/heading-context.test.js
new file mode 100644
index 000000000..6c12c56e0
--- /dev/null
+++ b/tests/context/heading-context.test.js
@@ -0,0 +1,49 @@
+import { render } from '@testing-library/react';
+import { HeadingContextProvider, useHeadingContext } from '../../src/context/heading-context';
+
+const Consumer = ({ test_id }) => {
+ const { lastHeading } = useHeadingContext();
+ return {lastHeading} ;
+};
+
+describe('Heading Context', () => {
+ it('initializes with an empty string', () => {
+ const testId = 'check-headings';
+ const wrapper = render( );
+ expect(wrapper.queryByTestId(testId).innerHTML).toBeFalsy();
+ });
+
+ it('is used by consumer to get all the preceding headings on the page', () => {
+ const wrapper = render(
+
+
+
+
+
+
+
+
+
+
+
+ );
+
+ expect(wrapper.queryByTestId('consumer-1').innerHTML).toEqual('Heading 1Aa');
+ expect(wrapper.queryByTestId('consumer-2').innerHTML).toEqual('Heading 1Aa');
+ expect(wrapper.queryByTestId('consumer-3').innerHTML).toEqual('Heading 1B');
+ });
+
+ it('can ignore the next heading if specified', () => {
+ const wrapper = render(
+
+
+
+
+
+
+
+ );
+
+ expect(wrapper.queryByTestId('ignore-prev-heading-1').innerHTML).toEqual('Heading 1');
+ });
+});
diff --git a/tests/unit/__snapshots__/Procedure.test.js.snap b/tests/unit/__snapshots__/Procedure.test.js.snap
index 53b0cd86d..df48add57 100644
--- a/tests/unit/__snapshots__/Procedure.test.js.snap
+++ b/tests/unit/__snapshots__/Procedure.test.js.snap
@@ -2,11 +2,6 @@
exports[`renders correctly 1`] = `
-
.emotion-0 {
margin-top: 16px;
background-color: initial;
@@ -229,11 +224,6 @@ exports[`renders correctly 1`] = `
exports[`renders steps nested in include nodes 1`] = `
-
.emotion-0 {
margin-top: 16px;
background-color: initial;
@@ -803,11 +793,6 @@ exports[`renders steps nested in include nodes 1`] = `
exports[`renders with "normal" or YAML steps styling 1`] = `
-
.emotion-0 {
margin-top: 16px;
background-color: initial;
diff --git a/tests/unit/utils/__snapshots__/structured-data.test.js.snap b/tests/unit/utils/__snapshots__/structured-data.test.js.snap
index f3a3930b0..e48583bfd 100644
--- a/tests/unit/utils/__snapshots__/structured-data.test.js.snap
+++ b/tests/unit/utils/__snapshots__/structured-data.test.js.snap
@@ -4,6 +4,8 @@ exports[`Structured Data HowTo Structured Data converts steps into expected stru
HowToSd {
"@context": "https://schema.org",
"@type": "HowTo",
+ "image": "https://webimages.mongodb.com/_com_assets/cms/kuyj2focmkbxv7gh3-stacked_default_slate_blue.svg?auto=format%252Ccompress",
+ "name": undefined,
"steps": [
{
"@type": "HowToStep",