From 240bb5bd68dc60162211630769cc247461a9e5d7 Mon Sep 17 00:00:00 2001
From: rayangler <27821750+rayangler@users.noreply.github.com>
Date: Fri, 27 Sep 2024 08:24:29 -0400
Subject: [PATCH 01/14] DOP-4916: Implement SoftwareSourceCode structured data
---
src/components/Code/Code.js | 139 +++++++++++++++++++----------------
src/utils/get-language.js | 49 ++++++++++++
src/utils/structured-data.js | 63 ++++++++++++++++
3 files changed, 188 insertions(+), 63 deletions(-)
create mode 100644 src/utils/structured-data.js
diff --git a/src/components/Code/Code.js b/src/components/Code/Code.js
index e5d5a080f..9a969b044 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,7 @@ 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 { baseCodeStyle, borderCodeStyle, lgStyles } from './styles/codeStyle';
import { CodeContext } from './code-context';
@@ -85,77 +86,89 @@ const Code = ({
reportAnalytics('CodeblockCopied', { code });
}, [code]);
+ const softwareSourceCodeSd = useMemo(() => new SoftwareSourceCodeSd({ code, lang }), [code, lang]);
+
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.isValid() && (
+
+ )}
+
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/utils/get-language.js b/src/utils/get-language.js
index 594c12d79..1fca66f5c 100644
--- a/src/utils/get-language.js
+++ b/src/utils/get-language.js
@@ -3,6 +3,44 @@
*/
import { Language } from '@leafygreen-ui/code';
+// Mapping of formal language names based on loose inputs
+const LANGUAGE_NAMES = {
+ bash: 'Bash',
+ c: 'C',
+ cpp: 'C++',
+ 'c#': 'C#',
+ cs: 'C#',
+ csharp: 'C#',
+ dart: 'Dart',
+ diff: 'Diff',
+ go: 'Golang',
+ graphql: 'GraphQL',
+ html: 'HTML',
+ http: 'HTTP',
+ ini: 'Ini',
+ java: 'Java',
+ javascript: 'JavaScript',
+ js: 'JavaScript',
+ json: 'JSON',
+ kotlin: 'Kotlin',
+ objectivec: 'Objective-C',
+ perl: 'Perl',
+ php: 'PHP',
+ properties: 'Properties',
+ python: 'Python',
+ ruby: 'Ruby',
+ rust: 'Rust',
+ scala: 'Scala',
+ sh: 'Shell',
+ shell: 'Shell',
+ sql: 'SQL',
+ swift: 'Swift',
+ ts: 'TypeScript',
+ typescript: 'TypeScript',
+ xml: 'XML',
+ yaml: 'YAML',
+};
+
export const getLanguage = (lang) => {
if (Object.values(Language).includes(lang)) {
return lang;
@@ -15,3 +53,14 @@ export const getLanguage = (lang) => {
}
return 'none';
};
+
+/**
+ * @param {string} 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) => {
+ if (!lang) {
+ return undefined;
+ }
+ return LANGUAGE_NAMES[lang];
+};
diff --git a/src/utils/structured-data.js b/src/utils/structured-data.js
new file mode 100644
index 000000000..23d3399df
--- /dev/null
+++ b/src/utils/structured-data.js
@@ -0,0 +1,63 @@
+/**
+ * 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
+ */
+
+import { getFullLanguageName } from './get-language';
+
+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 SoftwareSourceCodeSd extends StructuredData {
+ constructor({ code, lang }) {
+ super('SoftwareSourceCode');
+ this.codeSampleType = 'code snippet';
+ this.programmingLanguage = getFullLanguageName(lang);
+ this.text = code;
+ }
+}
From 1b7f24670299e4590b1ffbe426bc8181317ea20d Mon Sep 17 00:00:00 2001
From: rayangler <27821750+rayangler@users.noreply.github.com>
Date: Fri, 27 Sep 2024 09:06:05 -0400
Subject: [PATCH 02/14] Update tests
---
src/utils/structured-data.js | 6 +++++-
tests/unit/__snapshots__/Code.test.js.snap | 10 ++++++++++
tests/unit/__snapshots__/CodeIO.test.js.snap | 5 +++++
.../__snapshots__/Collapsible.test.js.snap | 5 +++++
.../__snapshots__/LiteralInclude.test.js.snap | 5 +++++
.../ReleaseSpecification.test.js.snap | 5 +++++
.../structured-data.test.js.snap | 19 +++++++++++++++++++
tests/unit/utils/structured-data.test.js | 16 ++++++++++++++++
8 files changed, 70 insertions(+), 1 deletion(-)
create mode 100644 tests/unit/utils/__snapshots__/structured-data.test.js.snap
create mode 100644 tests/unit/utils/structured-data.test.js
diff --git a/src/utils/structured-data.js b/src/utils/structured-data.js
index 23d3399df..89e98163f 100644
--- a/src/utils/structured-data.js
+++ b/src/utils/structured-data.js
@@ -57,7 +57,11 @@ export class SoftwareSourceCodeSd extends StructuredData {
constructor({ code, lang }) {
super('SoftwareSourceCode');
this.codeSampleType = 'code snippet';
- this.programmingLanguage = getFullLanguageName(lang);
this.text = code;
+
+ const programmingLanguage = getFullLanguageName(lang);
+ if (programmingLanguage) {
+ this.programmingLanguage = programmingLanguage;
+ }
}
}
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..fb00c0bed 100644
--- a/tests/unit/__snapshots__/CodeIO.test.js.snap
+++ b/tests/unit/__snapshots__/CodeIO.test.js.snap
@@ -657,6 +657,11 @@ exports[`CodeIO renders correctly 1`] = `
+
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
new file mode 100644
index 000000000..9c2791b69
--- /dev/null
+++ b/tests/unit/utils/__snapshots__/structured-data.test.js.snap
@@ -0,0 +1,19 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`SoftwareSourceCode returns valid structured data with programmingLanguage 1`] = `
+SoftwareSourceCodeSd {
+ "@context": "https://schema.org",
+ "@type": "SoftwareSourceCode",
+ "codeSampleType": "code snippet",
+ "text": "print("hello world")",
+}
+`;
+
+exports[`SoftwareSourceCode returns valid structured data without programmingLangauge 1`] = `
+SoftwareSourceCodeSd {
+ "@context": "https://schema.org",
+ "@type": "SoftwareSourceCode",
+ "codeSampleType": "code snippet",
+ "text": "print("hello world")",
+}
+`;
diff --git a/tests/unit/utils/structured-data.test.js b/tests/unit/utils/structured-data.test.js
new file mode 100644
index 000000000..8f7e30ed9
--- /dev/null
+++ b/tests/unit/utils/structured-data.test.js
@@ -0,0 +1,16 @@
+import { SoftwareSourceCodeSd } from '../../../src/utils/structured-data';
+
+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).toMatchSnapshot();
+ });
+
+ it('returns valid structured data without programmingLangauge', () => {
+ const code = 'print("hello world")';
+ const softwareSourceCodeSd = new SoftwareSourceCodeSd({ code });
+ expect(softwareSourceCodeSd).toMatchSnapshot();
+ });
+});
From 73991da9c00826551f29eb86754e7c39d7163f58 Mon Sep 17 00:00:00 2001
From: rayangler <27821750+rayangler@users.noreply.github.com>
Date: Fri, 27 Sep 2024 09:24:31 -0400
Subject: [PATCH 03/14] Update VideObject structured data
---
src/components/Video/index.js | 33 ++++++++-----------
src/utils/structured-data.js | 20 +++++++++++
.../structured-data.test.js.snap | 23 +++++++++++++
tests/unit/utils/structured-data.test.js | 29 +++++++++++++++-
4 files changed, 84 insertions(+), 21 deletions(-)
diff --git a/src/components/Video/index.js b/src/components/Video/index.js
index 922252fb6..430ee4cc1 100644
--- a/src/components/Video/index.js
+++ b/src/components/Video/index.js
@@ -1,4 +1,4 @@
-import React, { useState, useEffect } from 'react';
+import React, { useState, useEffect, useMemo } from 'react';
import ReactPlayerYT from 'react-player/youtube';
import ReactPlayerWistia from 'react-player/wistia';
import PropTypes from 'prop-types';
@@ -7,6 +7,7 @@ import { css } from '@emotion/react';
import { palette } from '@leafygreen-ui/palette';
import { withPrefix } from 'gatsby';
import { theme } from '../../theme/docsTheme';
+import { VideoObjectSd } from '../../utils/structured-data';
import VideoPlayButton from './VideoPlayButton';
// Imported both players to keep bundle size low and rendering the one associated to the URL being passed in
@@ -73,21 +74,10 @@ const Video = ({ nodeData: { argument, options = {} } }) => {
// 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;
- }
+ const videoObjectSd = useMemo(
+ () => new VideoObjectSd({ embedUrl: url, name: title, uploadDate, thumbnailUrl, description }),
+ [url, title, uploadDate, thumbnailUrl, description]
+ );
useEffect(() => {
// handles URL validity checking for well-formed YT links
@@ -121,10 +111,13 @@ const Video = ({ nodeData: { argument, options = {} } }) => {
return (
<>
- {hasAllReqFields && (
-
+ {videoObjectSd.isValid() && (
+
)}
!!val);
+ return hasAllReqFields && super.isValid();
+ }
+}
diff --git a/tests/unit/utils/__snapshots__/structured-data.test.js.snap b/tests/unit/utils/__snapshots__/structured-data.test.js.snap
index 9c2791b69..743ae787a 100644
--- a/tests/unit/utils/__snapshots__/structured-data.test.js.snap
+++ b/tests/unit/utils/__snapshots__/structured-data.test.js.snap
@@ -17,3 +17,26 @@ SoftwareSourceCodeSd {
"text": "print("hello world")",
}
`;
+
+exports[`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[`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 8f7e30ed9..35584e7f0 100644
--- a/tests/unit/utils/structured-data.test.js
+++ b/tests/unit/utils/structured-data.test.js
@@ -1,16 +1,43 @@
-import { SoftwareSourceCodeSd } from '../../../src/utils/structured-data';
+import { SoftwareSourceCodeSd, VideoObjectSd } from '../../../src/utils/structured-data';
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 b3767e8db2932b76904be7af1e6cf69f92a8593f Mon Sep 17 00:00:00 2001
From: rayangler <27821750+rayangler@users.noreply.github.com>
Date: Fri, 27 Sep 2024 09:25:41 -0400
Subject: [PATCH 04/14] Remove unnecessary id
---
src/components/StructuredData/DocsLandingSD.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
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 = () => (
-
-
-
-
-
-
-
+ |
+
- 1
-
-
- hello world
- |
-
-
-
-
-
+
+ 1
+ |
+
+ hello world
+ |
+
+
+
+
+
+
From 4d087d43e373c1b799a3b7c3953eab64d989f636 Mon Sep 17 00:00:00 2001
From: rayangler <27821750+rayangler@users.noreply.github.com>
Date: Fri, 27 Sep 2024 14:22:32 -0400
Subject: [PATCH 09/14] Validate as part of memo
---
src/components/Code/Code.js | 7 +++++--
src/components/Code/Output.js | 7 +++++--
src/components/Video/index.js | 10 +++++-----
3 files changed, 15 insertions(+), 9 deletions(-)
diff --git a/src/components/Code/Code.js b/src/components/Code/Code.js
index 9a969b044..b151e5b9f 100644
--- a/src/components/Code/Code.js
+++ b/src/components/Code/Code.js
@@ -86,11 +86,14 @@ const Code = ({
reportAnalytics('CodeblockCopied', { code });
}, [code]);
- const softwareSourceCodeSd = useMemo(() => new SoftwareSourceCodeSd({ code, lang }), [code, lang]);
+ const softwareSourceCodeSd = useMemo(() => {
+ const sd = new SoftwareSourceCodeSd({ code, lang });
+ return sd.isValid() ? sd.toString() : undefined;
+ }, [code, lang]);
return (
<>
- {softwareSourceCodeSd.isValid() && (
+ {softwareSourceCodeSd && (