Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/components/Head.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import React from 'react';
import { Helmet } from 'react-helmet';
import { JsonLdSchema, serializeJsonLd } from '../utilities/json-ld';

export const Head = ({
title,
canonical,
description,
metaTitle,
keywords,
jsonLd,
}: {
title: string;
canonical: string;
description: string;
metaTitle?: string;
keywords?: string;
jsonLd?: JsonLdSchema | JsonLdSchema[];
}) => (
<Helmet>
<title>{metaTitle || title}</title>
Expand All @@ -24,6 +27,18 @@ export const Head = ({
<meta name="twitter:description" content={description} />
{keywords && <meta name="keywords" content={keywords} />}

{/* JSON-LD Structured Data */}
{jsonLd &&
(Array.isArray(jsonLd) ? (
jsonLd.map((schema, index) => (
<script key={`jsonld-${index}`} type="application/ld+json">
{serializeJsonLd(schema)}
</script>
))
) : (
<script type="application/ld+json">{serializeJsonLd(jsonLd)}</script>
))}

<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="" />
<link
Expand Down
6 changes: 6 additions & 0 deletions src/components/Layout/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ export type Frontmatter = {
meta_description: string;
meta_keywords?: string;
redirect_from?: string[];
jsonld_type?: string;
jsonld_date_published?: string;
jsonld_date_modified?: string;
jsonld_author_name?: string;
jsonld_author_type?: string;
[key: string]: unknown; // Allow additional custom JSON-LD fields
};

export type PageContextType = {
Expand Down
40 changes: 39 additions & 1 deletion src/components/Layout/MDXWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { useSiteMetadata } from 'src/hooks/use-site-metadata';
import { ProductName } from 'src/templates/template-data';
import { getMetaTitle } from '../common/meta-title';
import UserContext from 'src/contexts/user-context';
import { generateArticleSchema, inferSchemaTypeFromPath } from 'src/utilities/json-ld';

type MDXWrapperProps = PageProps<unknown, PageContextType>;

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

// Generate JSON-LD schema for the page
const jsonLd = useMemo(() => {
// Extract custom JSON-LD fields from frontmatter
const customFields: Record<string, unknown> = {};

// Collect any frontmatter fields that start with 'jsonld_custom_'
Object.entries(frontmatter || {}).forEach(([key, value]) => {
if (key.startsWith('jsonld_custom_')) {
const schemaKey = key.replace('jsonld_custom_', '');
customFields[schemaKey] = value;
}
});

// Infer schema type from path if not explicitly set in frontmatter
const schemaType = frontmatter?.jsonld_type || inferSchemaTypeFromPath(location.pathname);

return generateArticleSchema({
title,
description,
url: canonical,
keywords,
schemaType,
datePublished: frontmatter?.jsonld_date_published,
dateModified: frontmatter?.jsonld_date_modified,
authorName: frontmatter?.jsonld_author_name,
authorType: frontmatter?.jsonld_author_type,
customFields,
});
}, [title, description, canonical, keywords, frontmatter, location.pathname]);

// Use the copyable headers hook
useCopyableHeaders();

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

return (
<SDKContext.Provider value={{ sdk, setSdk }}>
<Head title={title} metaTitle={metaTitle} canonical={canonical} description={description} keywords={keywords} />
<Head
title={title}
metaTitle={metaTitle}
canonical={canonical}
description={description}
keywords={keywords}
jsonLd={jsonLd}
/>
<Article>
<MarkdownProvider
components={{
Expand Down
135 changes: 135 additions & 0 deletions src/utilities/json-ld.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/**
* JSON-LD Schema Generator for Ably Documentation
*
* Generates structured data (JSON-LD) for documentation pages to improve SEO
* and provide rich snippets in search results.
*/

export type JsonLdSchema = {
'@context': string;
'@type': string;
[key: string]: unknown;
};

export interface GenerateArticleSchemaParams {
title: string;
description: string;
url: string;
dateModified?: string;
datePublished?: string;
keywords?: string;
schemaType?: string;
authorName?: string;
authorType?: string;
customFields?: Record<string, unknown>;
}

/**
* Generates a JSON-LD schema for documentation pages.
* Supports customization through frontmatter fields.
*
* @param params - The parameters for generating the schema
* @returns A JSON-LD schema object
*/
export const generateArticleSchema = ({
title,
description,
url,
dateModified,
datePublished,
keywords,
schemaType = 'TechArticle',
authorName = 'Ably',
authorType = 'Organization',
customFields = {},
}: GenerateArticleSchemaParams): JsonLdSchema => {
const schema: JsonLdSchema = {
'@context': 'https://schema.org',
'@type': schemaType,
headline: title,
description: description,
url: url,
publisher: {
'@type': 'Organization',
name: 'Ably',
url: 'https://ably.com',
},
author: {
'@type': authorType,
name: authorName,
...(authorType === 'Organization' ? { url: 'https://ably.com' } : {}),
},
};

// Add optional fields if provided
if (dateModified) {
schema.dateModified = dateModified;
}

if (datePublished) {
schema.datePublished = datePublished;
}

if (keywords) {
schema.keywords = keywords.split(',').map((k) => k.trim());
}

// Merge any custom fields from frontmatter
Object.entries(customFields).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
schema[key] = value;
}
});

return schema;
};

/**
* Generates a BreadcrumbList JSON-LD schema for navigation breadcrumbs.
*
* @param breadcrumbs - Array of breadcrumb items with name and url
* @returns A JSON-LD schema object
*/
export const generateBreadcrumbSchema = (breadcrumbs: Array<{ name: string; url: string }>): JsonLdSchema => {
return {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: breadcrumbs.map((crumb, index) => ({
'@type': 'ListItem',
position: index + 1,
name: crumb.name,
item: crumb.url,
})),
};
};

/**
* Infers the appropriate schema type based on the page URL path.
*
* @param pathname - The URL pathname of the page
* @returns The appropriate schema.org type
*/
export const inferSchemaTypeFromPath = (pathname: string): string => {
// API documentation and reference pages
if (pathname.includes('/api/')) {
return 'APIReference';
}

// Tutorial and guide pages
if (pathname.includes('/guides/') || pathname.includes('/quickstart') || pathname.includes('/getting-started')) {
return 'HowTo';
}

// Default to TechArticle for technical documentation
return 'TechArticle';
};

/**
* Serializes a JSON-LD schema object to a JSON string for use in script tags.
*
* @param schema - The JSON-LD schema object
* @returns A JSON string representation
*/
export const serializeJsonLd = (schema: JsonLdSchema): string => {
return JSON.stringify(schema);
};