@od-oneapp/seo 2026.1.1301
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +586 -0
- package/dist/client-next.d.mts +46 -0
- package/dist/client-next.d.mts.map +1 -0
- package/dist/client-next.mjs +92 -0
- package/dist/client-next.mjs.map +1 -0
- package/dist/client.d.mts +16 -0
- package/dist/client.d.mts.map +1 -0
- package/dist/client.mjs +47 -0
- package/dist/client.mjs.map +1 -0
- package/dist/env.d.mts +31 -0
- package/dist/env.d.mts.map +1 -0
- package/dist/env.mjs +125 -0
- package/dist/env.mjs.map +1 -0
- package/dist/index.d.mts +30 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +129 -0
- package/dist/index.mjs.map +1 -0
- package/dist/server-next.d.mts +230 -0
- package/dist/server-next.d.mts.map +1 -0
- package/dist/server-next.mjs +541 -0
- package/dist/server-next.mjs.map +1 -0
- package/dist/server.d.mts +3 -0
- package/dist/server.mjs +3 -0
- package/dist/structured-data-builders-ByJ4KCEf.mjs +176 -0
- package/dist/structured-data-builders-ByJ4KCEf.mjs.map +1 -0
- package/dist/structured-data-builders-CAgdYvmz.d.mts +74 -0
- package/dist/structured-data-builders-CAgdYvmz.d.mts.map +1 -0
- package/dist/structured-data.d.mts +16 -0
- package/dist/structured-data.d.mts.map +1 -0
- package/dist/structured-data.mjs +62 -0
- package/dist/structured-data.mjs.map +1 -0
- package/dist/validation.d.mts +20 -0
- package/dist/validation.d.mts.map +1 -0
- package/dist/validation.mjs +233 -0
- package/dist/validation.mjs.map +1 -0
- package/package.json +110 -0
- package/src/client-next.tsx +134 -0
- package/src/client.tsx +96 -0
- package/src/components/json-ld.tsx +74 -0
- package/src/components/structured-data.tsx +91 -0
- package/src/examples/app-router-sitemap.ts +109 -0
- package/src/examples/metadata-patterns.ts +528 -0
- package/src/examples/next-sitemap-config.ts +92 -0
- package/src/examples/nextjs-15-features.tsx +383 -0
- package/src/examples/nextjs-15-integration.ts +241 -0
- package/src/index.ts +87 -0
- package/src/server-next.ts +958 -0
- package/src/server.ts +27 -0
- package/src/types/metadata.ts +85 -0
- package/src/types/seo.ts +60 -0
- package/src/types/structured-data.ts +94 -0
- package/src/utils/i18n-enhanced.ts +148 -0
- package/src/utils/metadata-enhanced.ts +238 -0
- package/src/utils/metadata.ts +169 -0
- package/src/utils/structured-data-builders.ts +322 -0
- package/src/utils/validation.ts +284 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Metadata Utilities
|
|
3
|
+
*
|
|
4
|
+
* Core utilities for generating Next.js metadata with validation and error handling.
|
|
5
|
+
* Provides type-safe metadata creation with OpenGraph and Twitter Card support.
|
|
6
|
+
*
|
|
7
|
+
* @module @repo/seo/utils/metadata
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { type Metadata } from 'next';
|
|
11
|
+
|
|
12
|
+
import { logWarn } from '@repo/shared/logger';
|
|
13
|
+
import merge from 'lodash.merge';
|
|
14
|
+
|
|
15
|
+
import { getProtocol, safeEnv } from '../../env';
|
|
16
|
+
|
|
17
|
+
import { validateUrls } from './validation';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Open Graph image dimensions recommended by OG specification
|
|
21
|
+
* Aspect ratio: ~1.91:1 (1200x630)
|
|
22
|
+
* @see https://ogp.me/#structured
|
|
23
|
+
*/
|
|
24
|
+
const OG_IMAGE_WIDTH = 1200;
|
|
25
|
+
const OG_IMAGE_HEIGHT = 630;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Safely creates a URL object with comprehensive error handling
|
|
29
|
+
*
|
|
30
|
+
* This function handles URL creation with validation and graceful degradation:
|
|
31
|
+
* - Returns undefined for empty/null input
|
|
32
|
+
* - Validates URL format before creating URL object
|
|
33
|
+
* - Adds protocol if missing
|
|
34
|
+
* - Logs warnings for invalid URLs but doesn't throw
|
|
35
|
+
*
|
|
36
|
+
* @param url - URL string to parse (with or without protocol)
|
|
37
|
+
* @param protocol - Protocol to use if URL doesn't have one ('http' or 'https')
|
|
38
|
+
* @returns Parsed URL object or undefined if invalid
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```typescript
|
|
42
|
+
* const url = safeCreateUrl('example.com', 'https');
|
|
43
|
+
* // Returns: URL { href: 'https://example.com', ... }
|
|
44
|
+
*
|
|
45
|
+
* const invalid = safeCreateUrl('not a url', 'https');
|
|
46
|
+
* // Logs warning, returns: undefined
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
function safeCreateUrl(url: string | undefined, protocol: string): URL | undefined {
|
|
50
|
+
if (!url) return undefined;
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const fullUrl = url.startsWith('http') ? url : `${protocol}://${url}`;
|
|
54
|
+
if (!validateUrls([fullUrl])) {
|
|
55
|
+
logWarn(`[SEO] Invalid URL provided: ${url}, skipping metadataBase`, { url });
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
return new URL(fullUrl);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
logWarn(`[SEO] Failed to create URL from ${url}`, { error, url });
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Metadata generator options
|
|
67
|
+
*/
|
|
68
|
+
type MetadataGenerator = Omit<Metadata, 'description' | 'title'> & {
|
|
69
|
+
/** Page description (required) */
|
|
70
|
+
description: string;
|
|
71
|
+
/** Page title (required, will be combined with applicationName) */
|
|
72
|
+
title: string;
|
|
73
|
+
/** Optional Open Graph image URL */
|
|
74
|
+
image?: string;
|
|
75
|
+
/** Application name (defaults to 'Application') */
|
|
76
|
+
applicationName?: string;
|
|
77
|
+
/** Author information */
|
|
78
|
+
author?: Metadata['authors'];
|
|
79
|
+
/** Publisher name */
|
|
80
|
+
publisher?: string;
|
|
81
|
+
/** Twitter handle (e.g., '@username') */
|
|
82
|
+
twitterHandle?: string;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Create comprehensive Next.js metadata with sensible defaults
|
|
87
|
+
*
|
|
88
|
+
* Generates a complete metadata object with:
|
|
89
|
+
* - Title and description
|
|
90
|
+
* - Open Graph tags for social media
|
|
91
|
+
* - Twitter Card metadata
|
|
92
|
+
* - Apple Web App configuration
|
|
93
|
+
* - Format detection settings
|
|
94
|
+
*
|
|
95
|
+
* @param options - Metadata configuration
|
|
96
|
+
* @returns Complete Next.js Metadata object
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* ```typescript
|
|
100
|
+
* export const metadata = createMetadata({
|
|
101
|
+
* title: 'Home Page',
|
|
102
|
+
* description: 'Welcome to our site',
|
|
103
|
+
* applicationName: 'My App',
|
|
104
|
+
* image: '/og-image.png',
|
|
105
|
+
* author: { name: 'John Doe', url: 'https://johndoe.com' },
|
|
106
|
+
* publisher: 'My Company',
|
|
107
|
+
* twitterHandle: '@myapp'
|
|
108
|
+
* });
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
export const createMetadata = ({
|
|
112
|
+
description,
|
|
113
|
+
image,
|
|
114
|
+
title,
|
|
115
|
+
applicationName = 'Application',
|
|
116
|
+
author = { name: 'Author', url: 'https://example.com' },
|
|
117
|
+
publisher = 'Publisher',
|
|
118
|
+
twitterHandle = '@site',
|
|
119
|
+
...properties
|
|
120
|
+
}: MetadataGenerator): Metadata => {
|
|
121
|
+
const protocol = getProtocol();
|
|
122
|
+
const envVars = safeEnv();
|
|
123
|
+
const productionUrl = envVars.VERCEL_PROJECT_PRODUCTION_URL ?? envVars.NEXT_PUBLIC_URL;
|
|
124
|
+
const parsedTitle = `${title} | ${applicationName}`;
|
|
125
|
+
const defaultMetadata: Metadata = {
|
|
126
|
+
appleWebApp: {
|
|
127
|
+
capable: true,
|
|
128
|
+
statusBarStyle: 'default',
|
|
129
|
+
title: parsedTitle,
|
|
130
|
+
},
|
|
131
|
+
applicationName,
|
|
132
|
+
authors: author ? (Array.isArray(author) ? author : [author]) : undefined,
|
|
133
|
+
creator: author ? (Array.isArray(author) ? author[0]?.name : author.name) : undefined,
|
|
134
|
+
description,
|
|
135
|
+
formatDetection: {
|
|
136
|
+
telephone: false,
|
|
137
|
+
},
|
|
138
|
+
metadataBase: safeCreateUrl(productionUrl, protocol),
|
|
139
|
+
openGraph: {
|
|
140
|
+
description,
|
|
141
|
+
locale: 'en_US',
|
|
142
|
+
siteName: applicationName,
|
|
143
|
+
title: parsedTitle,
|
|
144
|
+
type: 'website',
|
|
145
|
+
},
|
|
146
|
+
publisher,
|
|
147
|
+
title: parsedTitle,
|
|
148
|
+
twitter: {
|
|
149
|
+
card: 'summary_large_image',
|
|
150
|
+
creator: twitterHandle,
|
|
151
|
+
site: twitterHandle,
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const metadata: Metadata = merge(defaultMetadata, properties);
|
|
156
|
+
|
|
157
|
+
if (image && metadata.openGraph) {
|
|
158
|
+
metadata.openGraph.images = [
|
|
159
|
+
{
|
|
160
|
+
alt: title,
|
|
161
|
+
height: OG_IMAGE_HEIGHT,
|
|
162
|
+
url: image,
|
|
163
|
+
width: OG_IMAGE_WIDTH,
|
|
164
|
+
},
|
|
165
|
+
];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return metadata;
|
|
169
|
+
};
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Structured Data Builders
|
|
3
|
+
*
|
|
4
|
+
* Shared utilities for creating Schema.org structured data (JSON-LD).
|
|
5
|
+
* This file contains type-safe builders for common schema types.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Type-safe structured data creation
|
|
9
|
+
* - Schema.org validation
|
|
10
|
+
* - Common schema builders (Article, Product, Organization, etc.)
|
|
11
|
+
*
|
|
12
|
+
* @module @repo/seo/utils/structured-data-builders
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { logWarn } from '@repo/shared/logger';
|
|
16
|
+
import {
|
|
17
|
+
type Article,
|
|
18
|
+
type BreadcrumbList,
|
|
19
|
+
type FAQPage,
|
|
20
|
+
type Organization,
|
|
21
|
+
type Product,
|
|
22
|
+
type Thing,
|
|
23
|
+
type WebSite,
|
|
24
|
+
type WithContext,
|
|
25
|
+
} from 'schema-dts';
|
|
26
|
+
|
|
27
|
+
import { isProduction } from '../../env';
|
|
28
|
+
|
|
29
|
+
import { validateSchemaOrgDetailed } from './validation';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Common structured data types with better type safety
|
|
33
|
+
*/
|
|
34
|
+
export type StructuredDataType =
|
|
35
|
+
| 'Article'
|
|
36
|
+
| 'BlogPosting'
|
|
37
|
+
| 'BreadcrumbList'
|
|
38
|
+
| 'Course'
|
|
39
|
+
| 'Event'
|
|
40
|
+
| 'FAQPage'
|
|
41
|
+
| 'HowTo'
|
|
42
|
+
| 'LocalBusiness'
|
|
43
|
+
| 'Organization'
|
|
44
|
+
| 'Person'
|
|
45
|
+
| 'Product'
|
|
46
|
+
| 'Recipe'
|
|
47
|
+
| 'SoftwareApplication'
|
|
48
|
+
| 'Thing'
|
|
49
|
+
| 'VideoObject'
|
|
50
|
+
| 'WebSite';
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Helper function to create structured data with proper typing and validation
|
|
54
|
+
*
|
|
55
|
+
* Creates Schema.org compliant structured data with automatic validation in
|
|
56
|
+
* development mode. Validation errors are logged to console but don't throw
|
|
57
|
+
* to avoid breaking builds.
|
|
58
|
+
*
|
|
59
|
+
* @param type - Schema.org type (e.g., 'Article', 'Product', 'Organization')
|
|
60
|
+
* @param data - The data object (without @context and @type)
|
|
61
|
+
* @param options - Optional configuration
|
|
62
|
+
* @param options.validate - Force validation even in production (default: false)
|
|
63
|
+
* @returns WithContext<T> structured data ready for JSON-LD
|
|
64
|
+
* @throws Never throws - validation errors are logged only
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* ```typescript
|
|
68
|
+
* const article = createStructuredData('Article', {
|
|
69
|
+
* headline: 'My Article',
|
|
70
|
+
* author: { '@type': 'Person', name: 'John Doe' },
|
|
71
|
+
* datePublished: '2024-01-01'
|
|
72
|
+
* });
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
export function createStructuredData<TType extends StructuredDataType, T extends Thing>(
|
|
76
|
+
type: TType,
|
|
77
|
+
data: Omit<T, '@context' | '@type'>,
|
|
78
|
+
options?: { validate?: boolean },
|
|
79
|
+
): WithContext<T> {
|
|
80
|
+
const structured = {
|
|
81
|
+
'@context': 'https://schema.org',
|
|
82
|
+
'@type': type,
|
|
83
|
+
...data,
|
|
84
|
+
} as unknown as WithContext<T>;
|
|
85
|
+
|
|
86
|
+
// Validation in development or when explicitly requested
|
|
87
|
+
const shouldValidate = options?.validate ?? !isProduction();
|
|
88
|
+
|
|
89
|
+
if (shouldValidate) {
|
|
90
|
+
const result = validateSchemaOrgDetailed(structured, type);
|
|
91
|
+
if (!result.valid) {
|
|
92
|
+
logWarn(`[SEO] Structured data validation failed for type '${type}'`, {
|
|
93
|
+
errors: result.errors.join(', '),
|
|
94
|
+
type,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return structured;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Pre-built structured data builders for common schema types.
|
|
104
|
+
* Each builder provides type-safe creation of Schema.org compliant JSON-LD.
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* ```typescript
|
|
108
|
+
* // Article schema
|
|
109
|
+
* const article = structuredData.article({
|
|
110
|
+
* headline: 'My Article',
|
|
111
|
+
* author: 'John Doe',
|
|
112
|
+
* datePublished: '2024-01-01',
|
|
113
|
+
* publisher: { name: 'My Site', logo: '/logo.png' }
|
|
114
|
+
* });
|
|
115
|
+
*
|
|
116
|
+
* // Product schema
|
|
117
|
+
* const product = structuredData.product({
|
|
118
|
+
* name: 'Product Name',
|
|
119
|
+
* image: '/product.jpg',
|
|
120
|
+
* offers: {
|
|
121
|
+
* price: '99.99',
|
|
122
|
+
* priceCurrency: 'USD'
|
|
123
|
+
* }
|
|
124
|
+
* });
|
|
125
|
+
* ```
|
|
126
|
+
*/
|
|
127
|
+
export const structuredData = {
|
|
128
|
+
/**
|
|
129
|
+
* Create an Article schema
|
|
130
|
+
* @param data - Article data
|
|
131
|
+
* @returns Article structured data
|
|
132
|
+
*/
|
|
133
|
+
article: (data: {
|
|
134
|
+
author: string | { name: string; url?: string };
|
|
135
|
+
dateModified?: string;
|
|
136
|
+
datePublished: string;
|
|
137
|
+
description?: string;
|
|
138
|
+
headline: string;
|
|
139
|
+
image?: string | string[];
|
|
140
|
+
mainEntityOfPage?: string;
|
|
141
|
+
publisher: {
|
|
142
|
+
logo?: string;
|
|
143
|
+
name: string;
|
|
144
|
+
};
|
|
145
|
+
}): WithContext<Article> =>
|
|
146
|
+
createStructuredData<'Article', Article>('Article', {
|
|
147
|
+
author:
|
|
148
|
+
typeof data.author === 'string'
|
|
149
|
+
? { '@type': 'Person', name: data.author }
|
|
150
|
+
: { '@type': 'Person', ...data.author },
|
|
151
|
+
dateModified: data.dateModified ?? data.datePublished,
|
|
152
|
+
datePublished: data.datePublished,
|
|
153
|
+
description: data.description,
|
|
154
|
+
headline: data.headline,
|
|
155
|
+
image: data.image,
|
|
156
|
+
mainEntityOfPage: data.mainEntityOfPage && {
|
|
157
|
+
'@id': data.mainEntityOfPage,
|
|
158
|
+
'@type': 'WebPage',
|
|
159
|
+
},
|
|
160
|
+
publisher: {
|
|
161
|
+
'@type': 'Organization',
|
|
162
|
+
name: data.publisher.name,
|
|
163
|
+
...(data.publisher.logo && {
|
|
164
|
+
logo: {
|
|
165
|
+
'@type': 'ImageObject',
|
|
166
|
+
url: data.publisher.logo,
|
|
167
|
+
},
|
|
168
|
+
}),
|
|
169
|
+
},
|
|
170
|
+
}),
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Create a BreadcrumbList schema
|
|
174
|
+
* @param items - Array of breadcrumb items
|
|
175
|
+
* @returns BreadcrumbList structured data
|
|
176
|
+
*/
|
|
177
|
+
breadcrumbs: (items: { name: string; url: string }[]): WithContext<BreadcrumbList> =>
|
|
178
|
+
createStructuredData<'BreadcrumbList', BreadcrumbList>('BreadcrumbList', {
|
|
179
|
+
itemListElement: items.map((item, index) => ({
|
|
180
|
+
'@type': 'ListItem',
|
|
181
|
+
item: item.url,
|
|
182
|
+
name: item.name,
|
|
183
|
+
position: index + 1,
|
|
184
|
+
})),
|
|
185
|
+
}),
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Create an FAQPage schema
|
|
189
|
+
* @param items - Array of question/answer pairs
|
|
190
|
+
* @returns FAQPage structured data
|
|
191
|
+
*/
|
|
192
|
+
faq: (items: { answer: string; question: string }[]): WithContext<FAQPage> =>
|
|
193
|
+
createStructuredData<'FAQPage', FAQPage>('FAQPage', {
|
|
194
|
+
mainEntity: items.map(item => ({
|
|
195
|
+
'@type': 'Question',
|
|
196
|
+
acceptedAnswer: {
|
|
197
|
+
'@type': 'Answer',
|
|
198
|
+
text: item.answer,
|
|
199
|
+
},
|
|
200
|
+
name: item.question,
|
|
201
|
+
})),
|
|
202
|
+
}),
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Create an Organization schema
|
|
206
|
+
* @param data - Organization data
|
|
207
|
+
* @returns Organization structured data
|
|
208
|
+
*/
|
|
209
|
+
organization: (data: {
|
|
210
|
+
contactPoint?: {
|
|
211
|
+
areaServed?: string | string[];
|
|
212
|
+
availableLanguage?: string | string[];
|
|
213
|
+
contactType: string;
|
|
214
|
+
telephone: string;
|
|
215
|
+
};
|
|
216
|
+
description?: string;
|
|
217
|
+
logo?: string;
|
|
218
|
+
name: string;
|
|
219
|
+
sameAs?: string[];
|
|
220
|
+
url: string;
|
|
221
|
+
}): WithContext<Organization> =>
|
|
222
|
+
createStructuredData<'Organization', Organization>('Organization', {
|
|
223
|
+
description: data.description,
|
|
224
|
+
logo: data.logo,
|
|
225
|
+
name: data.name,
|
|
226
|
+
sameAs: data.sameAs,
|
|
227
|
+
url: data.url,
|
|
228
|
+
...(data.contactPoint && {
|
|
229
|
+
contactPoint: {
|
|
230
|
+
'@type': 'ContactPoint',
|
|
231
|
+
...data.contactPoint,
|
|
232
|
+
},
|
|
233
|
+
}),
|
|
234
|
+
}),
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Create a Product schema
|
|
238
|
+
* @param data - Product data
|
|
239
|
+
* @returns Product structured data
|
|
240
|
+
*/
|
|
241
|
+
product: (data: {
|
|
242
|
+
aggregateRating?: {
|
|
243
|
+
ratingValue: number;
|
|
244
|
+
reviewCount: number;
|
|
245
|
+
};
|
|
246
|
+
brand?: string;
|
|
247
|
+
description?: string;
|
|
248
|
+
image?: string | string[];
|
|
249
|
+
name: string;
|
|
250
|
+
offers?: {
|
|
251
|
+
availability?: string;
|
|
252
|
+
price: string;
|
|
253
|
+
priceCurrency: string;
|
|
254
|
+
seller?: string;
|
|
255
|
+
};
|
|
256
|
+
}): WithContext<Product> =>
|
|
257
|
+
createStructuredData<'Product', Product>('Product', {
|
|
258
|
+
description: data.description,
|
|
259
|
+
image: data.image,
|
|
260
|
+
name: data.name,
|
|
261
|
+
...(data.brand && {
|
|
262
|
+
brand: {
|
|
263
|
+
'@type': 'Brand',
|
|
264
|
+
name: data.brand,
|
|
265
|
+
},
|
|
266
|
+
}),
|
|
267
|
+
...(data.offers && {
|
|
268
|
+
offers: {
|
|
269
|
+
'@type': 'Offer',
|
|
270
|
+
availability: (data.offers.availability ?? 'https://schema.org/InStock') as any,
|
|
271
|
+
price: data.offers.price,
|
|
272
|
+
priceCurrency: data.offers.priceCurrency,
|
|
273
|
+
...(data.offers.seller && {
|
|
274
|
+
seller: {
|
|
275
|
+
'@type': 'Organization',
|
|
276
|
+
name: data.offers.seller,
|
|
277
|
+
},
|
|
278
|
+
}),
|
|
279
|
+
},
|
|
280
|
+
}),
|
|
281
|
+
...(data.aggregateRating && {
|
|
282
|
+
aggregateRating: {
|
|
283
|
+
'@type': 'AggregateRating',
|
|
284
|
+
ratingValue: data.aggregateRating.ratingValue,
|
|
285
|
+
reviewCount: data.aggregateRating.reviewCount,
|
|
286
|
+
},
|
|
287
|
+
}),
|
|
288
|
+
}),
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Create a WebSite schema
|
|
292
|
+
* @param data - Website data
|
|
293
|
+
* @returns WebSite structured data
|
|
294
|
+
*/
|
|
295
|
+
website: (data: {
|
|
296
|
+
description?: string;
|
|
297
|
+
name: string;
|
|
298
|
+
potentialAction?: {
|
|
299
|
+
queryInput: string;
|
|
300
|
+
target: string;
|
|
301
|
+
};
|
|
302
|
+
url: string;
|
|
303
|
+
}): WithContext<WebSite> =>
|
|
304
|
+
createStructuredData<'WebSite', WebSite>('WebSite', {
|
|
305
|
+
description: data.description,
|
|
306
|
+
name: data.name,
|
|
307
|
+
url: data.url,
|
|
308
|
+
...(data.potentialAction && {
|
|
309
|
+
potentialAction: {
|
|
310
|
+
'@type': 'SearchAction',
|
|
311
|
+
'query-input': `required name=${data.potentialAction.queryInput}` as any,
|
|
312
|
+
target: {
|
|
313
|
+
'@type': 'EntryPoint',
|
|
314
|
+
urlTemplate: data.potentialAction.target,
|
|
315
|
+
},
|
|
316
|
+
} as any,
|
|
317
|
+
}),
|
|
318
|
+
}),
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
// Re-export types for convenience
|
|
322
|
+
export type { Thing, WithContext } from 'schema-dts';
|