Skip to content

Commit 502029a

Browse files
authored
feat: add JSON‑LD Organization, BreadcrumbList, and Article schema (#737)
Signed-off-by: amaan-bhati <[email protected]> Signed-off-by: Amaan Bhati <[email protected]>
1 parent 6cbfa49 commit 502029a

File tree

4 files changed

+344
-3
lines changed

4 files changed

+344
-3
lines changed

docusaurus.config.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,36 @@ module.exports = {
7979
logo: "https://keploy.io/docs/img/favicon.png",
8080
}),
8181
},
82+
{
83+
tagName: "script",
84+
attributes: {
85+
type: "application/ld+json",
86+
},
87+
innerHTML: JSON.stringify({
88+
"@context": "https://schema.org/",
89+
"@type": "Organization",
90+
name: "Keploy",
91+
url: "https://keploy.io/",
92+
logo: "https://keploy.io/docs/img/favicon.png",
93+
}),
94+
},
95+
{
96+
tagName: "script",
97+
attributes: {
98+
type: "application/ld+json",
99+
},
100+
innerHTML: JSON.stringify({
101+
"@context": "https://schema.org/",
102+
"@type": "WebSite",
103+
name: "Keploy Documentation",
104+
url: "https://keploy.io/docs/",
105+
potentialAction: {
106+
"@type": "SearchAction",
107+
target: "https://keploy.io/docs/search?q={search_term_string}",
108+
"query-input": "required name=search_term_string",
109+
},
110+
}),
111+
},
82112
],
83113
colorMode: {
84114
defaultMode: "light",

src/theme/DocBreadcrumbs/index.js

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import React from "react";
9+
import clsx from "clsx";
10+
import {ThemeClassNames} from "@docusaurus/theme-common";
11+
import {useSidebarBreadcrumbs} from "@docusaurus/plugin-content-docs/client";
12+
import {useHomePageRoute} from "@docusaurus/theme-common/internal";
13+
import {useLocation} from "@docusaurus/router";
14+
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
15+
import Link from "@docusaurus/Link";
16+
import {translate} from "@docusaurus/Translate";
17+
import Head from "@docusaurus/Head";
18+
import HomeBreadcrumbItem from "@theme/DocBreadcrumbs/Items/Home";
19+
20+
import styles from "./styles.module.css";
21+
22+
function BreadcrumbsItemLink({children, href, isLast}) {
23+
const className = "breadcrumbs__link";
24+
if (isLast) {
25+
return (
26+
<span className={className} itemProp="name">
27+
{children}
28+
</span>
29+
);
30+
}
31+
return href ? (
32+
<Link className={className} href={href} itemProp="item">
33+
<span itemProp="name">{children}</span>
34+
</Link>
35+
) : (
36+
<span className={className}>{children}</span>
37+
);
38+
}
39+
40+
function BreadcrumbsItem({children, active, index, addMicrodata}) {
41+
return (
42+
<li
43+
{...(addMicrodata && {
44+
itemScope: true,
45+
itemProp: "itemListElement",
46+
itemType: "https://schema.org/ListItem",
47+
})}
48+
className={clsx("breadcrumbs__item", {
49+
"breadcrumbs__item--active": active,
50+
})}
51+
>
52+
{children}
53+
<meta itemProp="position" content={String(index + 1)} />
54+
</li>
55+
);
56+
}
57+
58+
export default function DocBreadcrumbs() {
59+
const breadcrumbs = useSidebarBreadcrumbs();
60+
const homePageRoute = useHomePageRoute();
61+
const {siteConfig} = useDocusaurusContext();
62+
const {pathname} = useLocation();
63+
64+
if (!breadcrumbs) {
65+
return null;
66+
}
67+
68+
const toAbsoluteUrl = (baseUrl, url) => {
69+
if (!url) {
70+
return null;
71+
}
72+
if (url.startsWith("http://") || url.startsWith("https://")) {
73+
return url;
74+
}
75+
const trimmedBase = baseUrl?.replace(/\/$/, "") ?? "";
76+
const normalizedPath = url.startsWith("/") ? url : `/${url}`;
77+
return `${trimmedBase}${normalizedPath}`;
78+
};
79+
80+
const breadcrumbItems = [];
81+
const pushBreadcrumbItem = (item) => {
82+
if (!item?.item || !item?.name) {
83+
return;
84+
}
85+
if (breadcrumbItems.some((existing) => existing.item === item.item)) {
86+
return;
87+
}
88+
breadcrumbItems.push(item);
89+
};
90+
91+
if (siteConfig?.url) {
92+
pushBreadcrumbItem({name: "Home", item: siteConfig.url});
93+
}
94+
95+
if (siteConfig?.url && siteConfig?.baseUrl) {
96+
const docsUrl = toAbsoluteUrl(siteConfig.url, siteConfig.baseUrl);
97+
if (docsUrl && docsUrl !== siteConfig.url) {
98+
pushBreadcrumbItem({name: "Docs", item: docsUrl});
99+
}
100+
}
101+
102+
if (breadcrumbs.length > 0) {
103+
breadcrumbs.forEach((crumb, index) => {
104+
const isLast = index === breadcrumbs.length - 1;
105+
const href = crumb.href || (isLast ? pathname : null);
106+
const absoluteUrl = toAbsoluteUrl(siteConfig?.url, href);
107+
if (!absoluteUrl) {
108+
return;
109+
}
110+
pushBreadcrumbItem({
111+
name: crumb.label,
112+
item: absoluteUrl,
113+
});
114+
});
115+
}
116+
117+
const breadcrumbSchema =
118+
breadcrumbItems.length > 0
119+
? {
120+
"@context": "https://schema.org",
121+
"@type": "BreadcrumbList",
122+
itemListElement: breadcrumbItems.map((item, index) => ({
123+
"@type": "ListItem",
124+
position: index + 1,
125+
name: item.name,
126+
item: item.item,
127+
})),
128+
}
129+
: null;
130+
131+
return (
132+
<>
133+
{breadcrumbSchema && (
134+
<Head>
135+
<script type="application/ld+json">
136+
{JSON.stringify(breadcrumbSchema)}
137+
</script>
138+
</Head>
139+
)}
140+
<nav
141+
className={clsx(
142+
ThemeClassNames.docs.docBreadcrumbs,
143+
styles.breadcrumbsContainer
144+
)}
145+
aria-label={translate({
146+
id: "theme.docs.breadcrumbs.navAriaLabel",
147+
message: "Breadcrumbs",
148+
description: "The ARIA label for the breadcrumbs",
149+
})}
150+
>
151+
<ul
152+
className="breadcrumbs"
153+
itemScope
154+
itemType="https://schema.org/BreadcrumbList"
155+
>
156+
{homePageRoute && <HomeBreadcrumbItem />}
157+
{breadcrumbs.map((item, idx) => {
158+
const isLast = idx === breadcrumbs.length - 1;
159+
const href =
160+
item.type === "category" && item.linkUnlisted
161+
? undefined
162+
: item.href;
163+
return (
164+
<BreadcrumbsItem
165+
key={idx}
166+
active={isLast}
167+
index={idx}
168+
addMicrodata={!!href}
169+
>
170+
<BreadcrumbsItemLink href={href} isLast={isLast}>
171+
{item.label}
172+
</BreadcrumbsItemLink>
173+
</BreadcrumbsItem>
174+
);
175+
})}
176+
</ul>
177+
</nav>
178+
</>
179+
);
180+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
.breadcrumbsContainer {
9+
--ifm-breadcrumb-size-multiplier: 0.8;
10+
margin-bottom: 0.8rem;
11+
}

src/theme/DocItem/index.js

Lines changed: 123 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,14 @@ import DocBreadcrumbs from "@theme/DocBreadcrumbs";
1919
import Layout from "@docusaurus/core/lib/client/theme-fallback/Layout";
2020
import Head from "@docusaurus/Head";
2121
import MDXContent from "@theme/MDXContent";
22+
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
2223
import {KeployCloud} from "@site/src/components/KeployCloud";
2324

2425
export default function DocItem(props) {
2526
const {content: DocContent} = props;
2627
const {metadata, frontMatter, assets} = DocContent;
2728
const {
28-
keywords,
29+
keywords: frontMatterKeywords,
2930
hide_title: hideTitle,
3031
hide_table_of_contents: hideTableOfContents,
3132
toc_min_heading_level: tocMinHeadingLevel,
@@ -43,19 +44,138 @@ export default function DocItem(props) {
4344
!hideTableOfContents && DocContent.toc && DocContent.toc.length > 0;
4445
const renderTocDesktop =
4546
canRenderTOC && (windowSize === "desktop" || windowSize === "ssr");
46-
47+
const {siteConfig} = useDocusaurusContext();
48+
const toIsoDate = (value) => {
49+
if (!value) {
50+
return null;
51+
}
52+
const isEpochSeconds = Number.isFinite(value) && value < 1e12;
53+
const date = new Date(isEpochSeconds ? value * 1000 : value);
54+
if (Number.isNaN(date.getTime())) {
55+
return null;
56+
}
57+
return date.toISOString();
58+
};
59+
const toAbsoluteUrl = (baseUrl, url) => {
60+
if (!url) {
61+
return null;
62+
}
63+
if (url.startsWith("http://") || url.startsWith("https://")) {
64+
return url;
65+
}
66+
const trimmedBase = baseUrl?.replace(/\/$/, "") ?? "";
67+
const normalizedPath = url.startsWith("/") ? url : `/${url}`;
68+
return `${trimmedBase}${normalizedPath}`;
69+
};
70+
const toArray = (value) => {
71+
if (!value) {
72+
return [];
73+
}
74+
return Array.isArray(value) ? value : [value];
75+
};
76+
const toPersonList = (value) => {
77+
return toArray(value)
78+
.map((item) => {
79+
if (!item) {
80+
return null;
81+
}
82+
if (typeof item === "string") {
83+
return {["@type"]: "Person", name: item};
84+
}
85+
if (item.name) {
86+
return {
87+
"@type": "Person",
88+
name: item.name,
89+
...(item.url ? {url: item.url} : {}),
90+
};
91+
}
92+
return null;
93+
})
94+
.filter(Boolean);
95+
};
96+
const pageUrl = toAbsoluteUrl(siteConfig?.url, metadata?.permalink);
97+
const modifiedTime = toIsoDate(
98+
metadata?.lastUpdatedAt || frontMatter?.lastUpdatedAt
99+
);
100+
const publishedTime = toIsoDate(frontMatter?.date || frontMatter?.publishedAt);
101+
const schemaTypeFromFrontMatter =
102+
frontMatter?.schemaType || frontMatter?.schema_type;
103+
const isApi =
104+
frontMatter?.apiReference === true ||
105+
frontMatter?.type === "api" ||
106+
(frontMatter?.tags || []).includes?.("api");
107+
const isBlog =
108+
frontMatter?.type === "blog" ||
109+
frontMatter?.blog === true ||
110+
(frontMatter?.tags || []).includes?.("blog");
111+
const schemaType = schemaTypeFromFrontMatter
112+
? schemaTypeFromFrontMatter
113+
: isApi
114+
? "APIReference"
115+
: isBlog
116+
? "BlogPosting"
117+
: "Article";
118+
const authorList = toPersonList(frontMatter?.author || frontMatter?.authors);
119+
const maintainerList = toPersonList(frontMatter?.maintainer);
120+
const contributorList = toPersonList(frontMatter?.contributor);
121+
const combinedContributors = [...maintainerList, ...contributorList];
122+
const keywords = frontMatter?.keywords || metadata?.keywords;
123+
const metaKeywords = frontMatterKeywords ?? metadata?.keywords;
124+
const programmingLanguage =
125+
frontMatter?.programmingLanguage || frontMatter?.programmingLanguages;
126+
const targetPlatform = frontMatter?.targetPlatform;
127+
const proficiencyLevel = frontMatter?.proficiencyLevel;
128+
const articleSchema =
129+
pageUrl && title
130+
? {
131+
"@context": "https://schema.org",
132+
"@type": schemaType,
133+
headline: title,
134+
description,
135+
...(modifiedTime ? {dateModified: modifiedTime} : {}),
136+
...(publishedTime ? {datePublished: publishedTime} : {}),
137+
...(keywords ? {keywords} : {}),
138+
...(authorList.length ? {author: authorList} : {}),
139+
...(combinedContributors.length
140+
? {contributor: combinedContributors}
141+
: {}),
142+
...(proficiencyLevel ? {proficiencyLevel} : {}),
143+
...(programmingLanguage ? {programmingLanguage} : {}),
144+
...(targetPlatform ? {targetPlatform} : {}),
145+
mainEntityOfPage: {
146+
"@type": "WebPage",
147+
"@id": pageUrl,
148+
},
149+
publisher: {
150+
"@type": "Organization",
151+
name: "Keploy",
152+
logo: {
153+
"@type": "ImageObject",
154+
url: "https://keploy.io/docs/img/favicon.png",
155+
},
156+
},
157+
}
158+
: null;
47159
const MDXComponent = props.content;
48160
return (
49161
<>
50162
<Head>
51163
<title>{title}</title>
52164
{description && <meta name="description" content={description} />}
165+
{modifiedTime && (
166+
<meta property="article:modified_time" content={modifiedTime} />
167+
)}
168+
{articleSchema && (
169+
<script type="application/ld+json">
170+
{JSON.stringify(articleSchema)}
171+
</script>
172+
)}
53173
</Head>
54174
<Layout
55175
{...{
56176
title,
57177
description,
58-
keywords,
178+
keywords: metaKeywords,
59179
image,
60180
}}
61181
/>

0 commit comments

Comments
 (0)