Files
community-rule/app/hooks/useSchemaData.ts
T
adilalloandCursor c180e79c38 Give every public route its own description, and keep canonicals and structured data on the host that serves the page.
Decorative images on the marketing pages stay quiet, and every dialog close button is named Close.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-11-04 11:04:52 -07:00

228 lines
5.3 KiB
TypeScript

import { useMemo } from "react";
/**
* Types for Schema.org structured data
*/
export interface SchemaOrganization {
"@context": "https://schema.org";
"@type": "Organization";
name: string;
email?: string;
url: string;
sameAs?: string[];
}
export interface SchemaWebSite {
"@context": "https://schema.org";
"@type": "WebSite";
name: string;
url: string;
}
export interface SchemaHowTo {
"@context": "https://schema.org";
"@type": "HowTo";
name: string;
description: string;
step: Array<{
"@type": "HowToStep";
position: number;
name: string;
text: string;
}>;
}
export interface SchemaArticle {
"@context": "https://schema.org";
"@type": "Article";
headline: string;
description: string;
author: {
"@type": "Person";
name: string;
};
publisher: {
"@type": "Organization";
name: string;
url: string;
logo?: {
"@type": "ImageObject";
url: string;
};
};
datePublished: string;
dateModified: string;
mainEntityOfPage?: {
"@type": "WebPage";
"@id": string;
};
url: string;
articleSection?: string;
keywords?: string[];
}
export interface SchemaBreadcrumbList {
"@context": "https://schema.org";
"@type": "BreadcrumbList";
itemListElement: Array<{
"@type": "ListItem";
position: number;
name: string;
item: string;
}>;
}
/**
* Hook for generating Schema.org structured data (JSON-LD)
* Provides type-safe schema generation for SEO and search engines
*
* @example
* ```tsx
* const schemaData = useSchemaData({
* type: "HowTo",
* name: "How to build a community",
* description: "Step-by-step guide",
* steps: [
* { name: "Step 1", text: "Start here" },
* { name: "Step 2", text: "Continue here" },
* ],
* });
*
* <script
* type="application/ld+json"
* dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaData) }}
* />
* ```
*/
export function useSchemaData(
config:
| {
type: "Organization";
name: string;
email?: string;
url: string;
sameAs?: string[];
}
| {
type: "WebSite";
name: string;
url: string;
}
| {
type: "HowTo";
name: string;
description: string;
steps: Array<{ name: string; text: string }>;
}
| {
type: "Article";
headline: string;
description: string;
author: string;
publisher: {
name: string;
url: string;
logo?: string;
};
datePublished: string;
dateModified: string;
url: string;
mainEntityOfPage?: string;
articleSection?: string;
keywords?: string[];
}
| {
type: "BreadcrumbList";
items: Array<{ name: string; url: string }>;
},
):
| SchemaOrganization
| SchemaWebSite
| SchemaHowTo
| SchemaArticle
| SchemaBreadcrumbList {
return useMemo(() => {
switch (config.type) {
case "Organization":
return {
"@context": "https://schema.org",
"@type": "Organization",
name: config.name,
...(config.email && { email: config.email }),
url: config.url,
...(config.sameAs && { sameAs: config.sameAs }),
} as SchemaOrganization;
case "WebSite":
return {
"@context": "https://schema.org",
"@type": "WebSite",
name: config.name,
url: config.url,
} as SchemaWebSite;
case "HowTo":
return {
"@context": "https://schema.org",
"@type": "HowTo",
name: config.name,
description: config.description,
step: config.steps.map((step, index) => ({
"@type": "HowToStep",
position: index + 1,
name: step.name,
text: step.text,
})),
} as SchemaHowTo;
case "Article":
return {
"@context": "https://schema.org",
"@type": "Article",
headline: config.headline,
description: config.description,
author: {
"@type": "Person",
name: config.author,
},
publisher: {
"@type": "Organization",
name: config.publisher.name,
url: config.publisher.url,
...(config.publisher.logo && {
logo: {
"@type": "ImageObject",
url: config.publisher.logo,
},
}),
},
datePublished: config.datePublished,
dateModified: config.dateModified,
url: config.url,
...(config.mainEntityOfPage && {
mainEntityOfPage: {
"@type": "WebPage",
"@id": config.mainEntityOfPage,
},
}),
...(config.articleSection && {
articleSection: config.articleSection,
}),
...(config.keywords && { keywords: config.keywords }),
} as SchemaArticle;
case "BreadcrumbList":
return {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
itemListElement: config.items.map((item, index) => ({
"@type": "ListItem",
position: index + 1,
name: item.name,
item: item.url,
})),
} as SchemaBreadcrumbList;
}
}, [config]);
}