-
Notifications
You must be signed in to change notification settings - Fork 36
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
DOP-4916: Implement SoftwareSourceCode structured data #1258
Changes from 5 commits
240bb5b
1b7f246
73991da
b3767e8
0338a97
997cf50
6ba18e4
937e586
291b09a
4d087d4
5dee6d9
e47ebf6
114d8c6
d2f7261
e5e89b5
24c1284
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
css={css` | ||
${baseCodeStyle} | ||
|
||
// Remove whitespace when copyable false | ||
> 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() && ( | ||
<script | ||
type="application/ld+json" | ||
dangerouslySetInnerHTML={{ | ||
__html: softwareSourceCodeSd.toString(), | ||
}} | ||
/> | ||
)} | ||
<div | ||
css={css` | ||
${baseCodeStyle} | ||
|
||
// Remove whitespace when copyable false | ||
> 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}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No CSS changes actually applied here |
||
} | ||
} | ||
} | ||
|
||
[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 && ( | ||
<div> | ||
<CaptionContainer style={{ '--border-color': darkMode ? palette.gray.dark2 : palette.gray.light2 }}> | ||
<Caption style={{ '--color': darkMode ? palette.gray.light2 : palette.gray.dark1 }}>{caption}</Caption> | ||
</CaptionContainer> | ||
</div> | ||
)} | ||
<CodeBlock | ||
copyable={copyable} | ||
highlightLines={emphasizeLines} | ||
language={language} | ||
languageOptions={languageOptions} | ||
onChange={(selectedOption) => { | ||
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} | ||
</CodeBlock> | ||
</div> | ||
{captionSpecified && ( | ||
<div> | ||
<CaptionContainer style={{ '--border-color': darkMode ? palette.gray.dark2 : palette.gray.light2 }}> | ||
<Caption style={{ '--color': darkMode ? palette.gray.light2 : palette.gray.dark1 }}>{caption}</Caption> | ||
</CaptionContainer> | ||
</div> | ||
)} | ||
<CodeBlock | ||
copyable={copyable} | ||
highlightLines={emphasizeLines} | ||
language={language} | ||
languageOptions={languageOptions} | ||
onChange={(selectedOption) => { | ||
setActiveTab({ drivers: selectedOption.id }); | ||
}} | ||
onCopy={reportCodeCopied} | ||
showLineNumbers={linenos} | ||
showCustomActionButtons={sourceSpecified} | ||
customActionButtons={customActionButtonList} | ||
lineNumberStart={lineno_start} | ||
> | ||
{code} | ||
</CodeBlock> | ||
</div> | ||
</> | ||
); | ||
}; | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 && ( | ||
<script id={`video-object-sd-${url}`} type="application/ld+json"> | ||
{JSON.stringify(structuredData)} | ||
</script> | ||
{videoObjectSd.isValid() && ( | ||
<script | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Matt had suggested doing this in the memo and returning ! 👍 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Makes sense. Done. Thanks for flagging (and to Matt for suggesting) |
||
type="application/ld+json" | ||
dangerouslySetInnerHTML={{ | ||
__html: videoObjectSd.toString(), | ||
}} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ditto |
||
/> | ||
)} | ||
<ReactPlayerWrapper> | ||
<ReactPlayer | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
/** | ||
* 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.text = code; | ||
|
||
const programmingLanguage = getFullLanguageName(lang); | ||
if (programmingLanguage) { | ||
this.programmingLanguage = programmingLanguage; | ||
} | ||
} | ||
} | ||
|
||
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(); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same minor point here for checking validity and returning !