Skip to content

Commit f25cd82

Browse files
committed
Add automatic JSON-LD schema generation for docs
1 parent cefc98f commit f25cd82

File tree

4 files changed

+195
-1
lines changed

4 files changed

+195
-1
lines changed

src/components/Head.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
11
import React from 'react';
22
import { Helmet } from 'react-helmet';
3+
import { JsonLdSchema, serializeJsonLd } from '../utilities/json-ld';
34

45
export const Head = ({
56
title,
67
canonical,
78
description,
89
metaTitle,
910
keywords,
11+
jsonLd,
1012
}: {
1113
title: string;
1214
canonical: string;
1315
description: string;
1416
metaTitle?: string;
1517
keywords?: string;
18+
jsonLd?: JsonLdSchema | JsonLdSchema[];
1619
}) => (
1720
<Helmet>
1821
<title>{metaTitle || title}</title>
@@ -24,6 +27,18 @@ export const Head = ({
2427
<meta name="twitter:description" content={description} />
2528
{keywords && <meta name="keywords" content={keywords} />}
2629

30+
{/* JSON-LD Structured Data */}
31+
{jsonLd &&
32+
(Array.isArray(jsonLd) ? (
33+
jsonLd.map((schema, index) => (
34+
<script key={`jsonld-${index}`} type="application/ld+json">
35+
{serializeJsonLd(schema)}
36+
</script>
37+
))
38+
) : (
39+
<script type="application/ld+json">{serializeJsonLd(jsonLd)}</script>
40+
))}
41+
2742
<link rel="preconnect" href="https://fonts.googleapis.com" />
2843
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="" />
2944
<link

src/components/Layout/Layout.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ export type Frontmatter = {
1919
meta_description: string;
2020
meta_keywords?: string;
2121
redirect_from?: string[];
22+
jsonld_type?: string;
23+
jsonld_date_published?: string;
24+
jsonld_date_modified?: string;
25+
jsonld_author_name?: string;
26+
jsonld_author_type?: string;
27+
[key: string]: unknown; // Allow additional custom JSON-LD fields
2228
};
2329

2430
export type PageContextType = {

src/components/Layout/MDXWrapper.tsx

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { useSiteMetadata } from 'src/hooks/use-site-metadata';
3333
import { ProductName } from 'src/templates/template-data';
3434
import { getMetaTitle } from '../common/meta-title';
3535
import UserContext from 'src/contexts/user-context';
36+
import { generateArticleSchema, inferSchemaTypeFromPath } from 'src/utilities/json-ld';
3637

3738
type MDXWrapperProps = PageProps<unknown, PageContextType>;
3839

@@ -184,6 +185,36 @@ const MDXWrapper: React.FC<MDXWrapperProps> = ({ children, pageContext, location
184185
const { canonicalUrl } = useSiteMetadata();
185186
const canonical = canonicalUrl(location.pathname);
186187

188+
// Generate JSON-LD schema for the page
189+
const jsonLd = useMemo(() => {
190+
// Extract custom JSON-LD fields from frontmatter
191+
const customFields: Record<string, unknown> = {};
192+
193+
// Collect any frontmatter fields that start with 'jsonld_custom_'
194+
Object.entries(frontmatter || {}).forEach(([key, value]) => {
195+
if (key.startsWith('jsonld_custom_')) {
196+
const schemaKey = key.replace('jsonld_custom_', '');
197+
customFields[schemaKey] = value;
198+
}
199+
});
200+
201+
// Infer schema type from path if not explicitly set in frontmatter
202+
const schemaType = frontmatter?.jsonld_type || inferSchemaTypeFromPath(location.pathname);
203+
204+
return generateArticleSchema({
205+
title,
206+
description,
207+
url: canonical,
208+
keywords,
209+
schemaType,
210+
datePublished: frontmatter?.jsonld_date_published,
211+
dateModified: frontmatter?.jsonld_date_modified,
212+
authorName: frontmatter?.jsonld_author_name,
213+
authorType: frontmatter?.jsonld_author_type,
214+
customFields,
215+
});
216+
}, [title, description, canonical, keywords, frontmatter, location.pathname]);
217+
187218
// Use the copyable headers hook
188219
useCopyableHeaders();
189220

@@ -206,7 +237,14 @@ const MDXWrapper: React.FC<MDXWrapperProps> = ({ children, pageContext, location
206237

207238
return (
208239
<SDKContext.Provider value={{ sdk, setSdk }}>
209-
<Head title={title} metaTitle={metaTitle} canonical={canonical} description={description} keywords={keywords} />
240+
<Head
241+
title={title}
242+
metaTitle={metaTitle}
243+
canonical={canonical}
244+
description={description}
245+
keywords={keywords}
246+
jsonLd={jsonLd}
247+
/>
210248
<Article>
211249
<MarkdownProvider
212250
components={{

src/utilities/json-ld.ts

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/**
2+
* JSON-LD Schema Generator for Ably Documentation
3+
*
4+
* Generates structured data (JSON-LD) for documentation pages to improve SEO
5+
* and provide rich snippets in search results.
6+
*/
7+
8+
export type JsonLdSchema = {
9+
'@context': string;
10+
'@type': string;
11+
[key: string]: unknown;
12+
};
13+
14+
export interface GenerateArticleSchemaParams {
15+
title: string;
16+
description: string;
17+
url: string;
18+
dateModified?: string;
19+
datePublished?: string;
20+
keywords?: string;
21+
schemaType?: string;
22+
authorName?: string;
23+
authorType?: string;
24+
customFields?: Record<string, unknown>;
25+
}
26+
27+
/**
28+
* Generates a JSON-LD schema for documentation pages.
29+
* Supports customization through frontmatter fields.
30+
*
31+
* @param params - The parameters for generating the schema
32+
* @returns A JSON-LD schema object
33+
*/
34+
export const generateArticleSchema = ({
35+
title,
36+
description,
37+
url,
38+
dateModified,
39+
datePublished,
40+
keywords,
41+
schemaType = 'TechArticle',
42+
authorName = 'Ably',
43+
authorType = 'Organization',
44+
customFields = {},
45+
}: GenerateArticleSchemaParams): JsonLdSchema => {
46+
const schema: JsonLdSchema = {
47+
'@context': 'https://schema.org',
48+
'@type': schemaType,
49+
headline: title,
50+
description: description,
51+
url: url,
52+
publisher: {
53+
'@type': 'Organization',
54+
name: 'Ably',
55+
url: 'https://ably.com',
56+
},
57+
author: {
58+
'@type': authorType,
59+
name: authorName,
60+
...(authorType === 'Organization' ? { url: 'https://ably.com' } : {}),
61+
},
62+
};
63+
64+
// Add optional fields if provided
65+
if (dateModified) {
66+
schema.dateModified = dateModified;
67+
}
68+
69+
if (datePublished) {
70+
schema.datePublished = datePublished;
71+
}
72+
73+
if (keywords) {
74+
schema.keywords = keywords.split(',').map((k) => k.trim());
75+
}
76+
77+
// Merge any custom fields from frontmatter
78+
Object.entries(customFields).forEach(([key, value]) => {
79+
if (value !== undefined && value !== null) {
80+
schema[key] = value;
81+
}
82+
});
83+
84+
return schema;
85+
};
86+
87+
/**
88+
* Generates a BreadcrumbList JSON-LD schema for navigation breadcrumbs.
89+
*
90+
* @param breadcrumbs - Array of breadcrumb items with name and url
91+
* @returns A JSON-LD schema object
92+
*/
93+
export const generateBreadcrumbSchema = (breadcrumbs: Array<{ name: string; url: string }>): JsonLdSchema => {
94+
return {
95+
'@context': 'https://schema.org',
96+
'@type': 'BreadcrumbList',
97+
itemListElement: breadcrumbs.map((crumb, index) => ({
98+
'@type': 'ListItem',
99+
position: index + 1,
100+
name: crumb.name,
101+
item: crumb.url,
102+
})),
103+
};
104+
};
105+
106+
/**
107+
* Infers the appropriate schema type based on the page URL path.
108+
*
109+
* @param pathname - The URL pathname of the page
110+
* @returns The appropriate schema.org type
111+
*/
112+
export const inferSchemaTypeFromPath = (pathname: string): string => {
113+
// API documentation and reference pages
114+
if (pathname.includes('/api/')) {
115+
return 'APIReference';
116+
}
117+
118+
// Tutorial and guide pages
119+
if (pathname.includes('/guides/') || pathname.includes('/quickstart') || pathname.includes('/getting-started')) {
120+
return 'HowTo';
121+
}
122+
123+
// Default to TechArticle for technical documentation
124+
return 'TechArticle';
125+
};
126+
127+
/**
128+
* Serializes a JSON-LD schema object to a JSON string for use in script tags.
129+
*
130+
* @param schema - The JSON-LD schema object
131+
* @returns A JSON string representation
132+
*/
133+
export const serializeJsonLd = (schema: JsonLdSchema): string => {
134+
return JSON.stringify(schema);
135+
};

0 commit comments

Comments
 (0)