Thanks to visit codestin.com
Credit goes to github.com

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
Prev Previous commit
Next Next commit
fixup! Add automatic JSON-LD schema generation for docs
  • Loading branch information
GregHolmes committed Nov 19, 2025
commit 3e2f77338b8f22b60aa574ae2c5947db276b1e77
10 changes: 4 additions & 6 deletions src/components/Layout/MDXWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +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';
import { generateCompleteSchema } from 'src/utilities/json-ld';

type MDXWrapperProps = PageProps<unknown, PageContextType>;

Expand Down Expand Up @@ -198,15 +198,13 @@ const MDXWrapper: React.FC<MDXWrapperProps> = ({ children, pageContext, location
}
});

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

return generateArticleSchema({
return generateCompleteSchema({
title,
description,
url: canonical,
pathname: location.pathname,
keywords,
schemaType,
schemaType: frontmatter?.jsonld_type,
datePublished: frontmatter?.jsonld_date_published,
dateModified: frontmatter?.jsonld_date_modified,
authorName: frontmatter?.jsonld_author_name,
Expand Down
227 changes: 168 additions & 59 deletions src/utilities/json-ld.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,27 @@
/**
* 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.
* Generates comprehensive structured data (JSON-LD) using @graph for documentation pages
* to improve discoverability by search engines and AI (LLMs).
*
* Based on Ably's JSON-LD Schema Prompt requirements.
*/

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

export type JsonLdNode = {
'@type': string | string[];
'@id'?: string;
[key: string]: unknown;
};

export interface GenerateArticleSchemaParams {
export interface GenerateSchemaParams {
title: string;
description: string;
url: string;
Expand All @@ -21,17 +31,117 @@ export interface GenerateArticleSchemaParams {
schemaType?: string;
authorName?: string;
authorType?: string;
pathname?: 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
* Generates the Ably Organization entity (always included once in @graph).
*/
export const generateAblyOrganization = (): JsonLdNode => {
return {
'@type': 'Organization',
'@id': 'https://ably.com#organization',
name: 'Ably',
url: 'https://ably.com',
logo: {
'@type': 'ImageObject',
url: 'https://ably.com/favicon-512x512.png',
},
sameAs: [
'https://www.linkedin.com/company/ably-realtime/',
'https://twitter.com/ablyrealtime',
'https://github.com/ably',
'https://www.g2.com/products/ably',
],
};
};

/**
* Generates a WebSite node for the Ably documentation site.
*/
export const generateWebSiteNode = (): JsonLdNode => {
return {
'@type': 'WebSite',
'@id': 'https://ably.com#website',
name: 'Ably Documentation',
url: 'https://ably.com',
publisher: {
'@id': 'https://ably.com#organization',
},
};
};

/**
* Generates a BreadcrumbList node for navigation.
*/
export const generateBreadcrumbNode = (pathname: string, url: string): JsonLdNode | null => {
// Parse pathname to create breadcrumbs
const segments = pathname.split('/').filter(Boolean);

if (segments.length === 0) {
return null;
}

const breadcrumbs: Array<{ name: string; url: string }> = [];
let currentPath = '';

// Add home
breadcrumbs.push({ name: 'Home', url: 'https://ably.com' });

// Add each segment
segments.forEach((segment) => {
currentPath += `/${segment}`;
const name = segment
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');

breadcrumbs.push({
name,
url: `https://ably.com${currentPath}`,
});
});

return {
'@type': 'BreadcrumbList',
'@id': `${url}#breadcrumb`,
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.
*/
export const generateArticleSchema = ({
export const inferSchemaTypeFromPath = (pathname: string): string => {
// API documentation and reference pages
if (pathname.includes('/api/') || pathname.includes('/reference/')) {
return 'APIReference';
}

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

// Conceptual/learning pages
if (pathname.includes('/concepts/') || pathname.includes('/learn/')) {
return 'Article';
}

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

/**
* Generates the main content node (TechArticle, APIReference, HowTo, etc.)
*/
export const generateMainContentNode = ({
title,
description,
url,
Expand All @@ -42,93 +152,92 @@ export const generateArticleSchema = ({
authorName = 'Ably',
authorType = 'Organization',
customFields = {},
}: GenerateArticleSchemaParams): JsonLdSchema => {
const schema: JsonLdSchema = {
'@context': 'https://schema.org',
}: GenerateSchemaParams): JsonLdNode => {
const entityId = `${url}#${schemaType.toLowerCase()}`;

const node: JsonLdNode = {
'@type': schemaType,
'@id': entityId,
headline: title,
name: title,
description: description,
url: url,
inLanguage: 'en',
mainEntityOfPage: url,
publisher: {
'@type': 'Organization',
name: 'Ably',
url: 'https://ably.com',
'@id': 'https://ably.com#organization',
},
author: {
'@type': authorType,
name: authorName,
...(authorType === 'Organization' ? { url: 'https://ably.com' } : {}),
...(authorType === 'Organization' ? { '@id': 'https://ably.com#organization' } : {}),
},
};

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

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

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

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

return schema;
return node;
};

/**
* Generates a BreadcrumbList JSON-LD schema for navigation breadcrumbs.
* Generates a complete JSON-LD schema with @graph structure.
*
* @param breadcrumbs - Array of breadcrumb items with name and url
* @returns A JSON-LD schema object
* This follows the Ably JSON-LD Schema Prompt requirements for comprehensive,
* truthful structured data that improves discoverability.
*/
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,
})),
};
};
export const generateCompleteSchema = (params: GenerateSchemaParams): JsonLdSchema => {
const { url, pathname = '', schemaType: explicitSchemaType } = params;

/**
* 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';
}
// Infer schema type if not explicitly provided
const schemaType = explicitSchemaType || inferSchemaTypeFromPath(pathname);

// Tutorial and guide pages
if (pathname.includes('/guides/') || pathname.includes('/quickstart') || pathname.includes('/getting-started')) {
return 'HowTo';
// Build the @graph array
const graph: JsonLdNode[] = [];

// 1. Always include Ably Organization
graph.push(generateAblyOrganization());

// 2. Include WebSite node
graph.push(generateWebSiteNode());

// 3. Include BreadcrumbList if we have a pathname
if (pathname) {
const breadcrumb = generateBreadcrumbNode(pathname, url);
if (breadcrumb) {
graph.push(breadcrumb);
}
}

// Default to TechArticle for technical documentation
return 'TechArticle';
// 4. Include main content node
graph.push(generateMainContentNode({ ...params, schemaType }));

// Return complete schema with @graph
return {
'@context': 'https://schema.org',
'@graph': graph,
};
};

/**
* 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);
Expand Down