@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,383 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Next.js 15 SEO Features Comprehensive Example
|
|
3
|
+
* @module @repo/seo/examples/nextjs-15-features
|
|
4
|
+
*
|
|
5
|
+
* This example demonstrates all the enhanced Next.js 15 SEO features
|
|
6
|
+
* provided by the @repo/seo package, including:
|
|
7
|
+
*
|
|
8
|
+
* - Optimized JsonLd components with Script strategy
|
|
9
|
+
* - Streaming-compatible structured data
|
|
10
|
+
* - Metadata templates for common patterns
|
|
11
|
+
* - Dynamic viewport configuration
|
|
12
|
+
* - Multi-language sitemap generation
|
|
13
|
+
* - Preview mode metadata handling
|
|
14
|
+
* - Edge-compatible metadata generation
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* // Import the features you need
|
|
18
|
+
* import { OptimizedJsonLd, metadataTemplates } from '@repo/seo/client/next';
|
|
19
|
+
* import { generateMetadataAsync, generateI18nSitemap } from '@repo/seo/server/next';
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
// ============================================
|
|
23
|
+
// CLIENT-SIDE EXAMPLES (client-next.tsx)
|
|
24
|
+
// ============================================
|
|
25
|
+
|
|
26
|
+
// ============================================
|
|
27
|
+
// SERVER-SIDE EXAMPLES (server-next.ts)
|
|
28
|
+
// ============================================
|
|
29
|
+
import { type Metadata } from 'next';
|
|
30
|
+
|
|
31
|
+
import { type Product, type WithContext } from 'schema-dts';
|
|
32
|
+
|
|
33
|
+
import { OptimizedJsonLd, StreamingJsonLd, useOpenGraphPreview } from '../client-next';
|
|
34
|
+
import {
|
|
35
|
+
generateI18nSitemap,
|
|
36
|
+
generateMetadataAsync,
|
|
37
|
+
generateMetadataEdge,
|
|
38
|
+
generatePreviewMetadata,
|
|
39
|
+
generateViewport,
|
|
40
|
+
metadataTemplates,
|
|
41
|
+
SEOManager,
|
|
42
|
+
viewportPresets,
|
|
43
|
+
} from '../server-next';
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Example: Optimized JsonLd with Next.js Script component
|
|
47
|
+
* This provides better performance by controlling when the script loads
|
|
48
|
+
*/
|
|
49
|
+
export function ProductPageWithOptimizedSEO({ product }: { product: any }) {
|
|
50
|
+
const structuredData: WithContext<Product> = {
|
|
51
|
+
'@context': 'https://schema.org',
|
|
52
|
+
'@type': 'Product',
|
|
53
|
+
name: product.name,
|
|
54
|
+
description: product.description,
|
|
55
|
+
image: product.images,
|
|
56
|
+
offers: {
|
|
57
|
+
'@type': 'Offer',
|
|
58
|
+
price: product.price,
|
|
59
|
+
priceCurrency: product.currency,
|
|
60
|
+
availability: product.inStock
|
|
61
|
+
? 'https://schema.org/InStock'
|
|
62
|
+
: 'https://schema.org/OutOfStock',
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<>
|
|
68
|
+
<OptimizedJsonLd data={structuredData} id="product-data" strategy="afterInteractive" />
|
|
69
|
+
|
|
70
|
+
<OptimizedJsonLd
|
|
71
|
+
data={structuredData}
|
|
72
|
+
id="critical-product-data"
|
|
73
|
+
strategy="beforeInteractive"
|
|
74
|
+
/>
|
|
75
|
+
</>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Example: Streaming JsonLd for React Server Components
|
|
81
|
+
* Perfect for async data that needs SEO optimization
|
|
82
|
+
*/
|
|
83
|
+
export function StreamingProductPage({ productId }: { productId: string }) {
|
|
84
|
+
// This could be a fetch from your database
|
|
85
|
+
const productPromise = (async () => {
|
|
86
|
+
const res = await fetch(`/api/products/${productId}`);
|
|
87
|
+
const product = await res.json();
|
|
88
|
+
return {
|
|
89
|
+
'@context': 'https://schema.org',
|
|
90
|
+
'@type': 'Product',
|
|
91
|
+
name: product.name,
|
|
92
|
+
description: product.description,
|
|
93
|
+
} as WithContext<Product>;
|
|
94
|
+
})();
|
|
95
|
+
|
|
96
|
+
const fallbackData: WithContext<Product> = {
|
|
97
|
+
'@context': 'https://schema.org',
|
|
98
|
+
'@type': 'Product',
|
|
99
|
+
name: 'Loading...',
|
|
100
|
+
description: 'Product information is loading',
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<StreamingJsonLd dataPromise={productPromise} fallback={fallbackData} id="streaming-product" />
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Example: Dynamic Open Graph Preview Hook
|
|
110
|
+
* Useful for content editors or preview modes
|
|
111
|
+
*/
|
|
112
|
+
export function ContentEditor() {
|
|
113
|
+
const { preview, updatePreview, generatePreviewHtml } = useOpenGraphPreview({
|
|
114
|
+
title: 'My Article',
|
|
115
|
+
description: 'Article description',
|
|
116
|
+
image: '/images/article-hero.jpg',
|
|
117
|
+
url: 'https://example.com/article',
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<div>
|
|
122
|
+
<h2>SEO Preview</h2>
|
|
123
|
+
<input
|
|
124
|
+
placeholder="Title"
|
|
125
|
+
value={preview.title}
|
|
126
|
+
onChange={(e: any) => updatePreview({ title: e.target.value })}
|
|
127
|
+
/>
|
|
128
|
+
<textarea
|
|
129
|
+
placeholder="Description"
|
|
130
|
+
value={preview.description}
|
|
131
|
+
onChange={(e: any) => updatePreview({ description: e.target.value })}
|
|
132
|
+
/>
|
|
133
|
+
<div>
|
|
134
|
+
<h3>Generated Meta Tags:</h3>
|
|
135
|
+
<pre>{generatePreviewHtml}</pre>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Example: Using metadata templates for a product page
|
|
143
|
+
*/
|
|
144
|
+
export async function generateMetadata({ params }: { params: { id: string } }) {
|
|
145
|
+
const product = await getProduct(params.id);
|
|
146
|
+
|
|
147
|
+
return metadataTemplates.product({
|
|
148
|
+
name: product.name,
|
|
149
|
+
description: product.description,
|
|
150
|
+
price: product.price,
|
|
151
|
+
currency: 'USD',
|
|
152
|
+
image: product.image,
|
|
153
|
+
availability: product.inStock ? 'InStock' : 'OutOfStock',
|
|
154
|
+
brand: product.brand,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Example: Article metadata with author and publishing info
|
|
160
|
+
*/
|
|
161
|
+
export function ArticleMetadata({ article }: { article: any }): Metadata {
|
|
162
|
+
return metadataTemplates.article({
|
|
163
|
+
title: article.title,
|
|
164
|
+
description: article.excerpt,
|
|
165
|
+
author: article.author.name,
|
|
166
|
+
publishedTime: new Date(article.publishedAt),
|
|
167
|
+
modifiedTime: article.updatedAt ? new Date(article.updatedAt) : undefined,
|
|
168
|
+
image: article.featuredImage,
|
|
169
|
+
tags: article.tags,
|
|
170
|
+
section: article.category,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Example: User profile metadata
|
|
176
|
+
*/
|
|
177
|
+
export function ProfileMetadata({ user }: { user: any }): Metadata {
|
|
178
|
+
return metadataTemplates.profile({
|
|
179
|
+
name: user.displayName,
|
|
180
|
+
bio: user.bio,
|
|
181
|
+
image: user.avatar,
|
|
182
|
+
username: user.username,
|
|
183
|
+
firstName: user.firstName,
|
|
184
|
+
lastName: user.lastName,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Example: Dynamic viewport based on user agent
|
|
190
|
+
*/
|
|
191
|
+
export function generateDynamicViewport(request: any) {
|
|
192
|
+
const userAgent = request.headers.get('user-agent') ?? '';
|
|
193
|
+
return generateViewport(userAgent);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Example: Using viewport presets
|
|
198
|
+
*/
|
|
199
|
+
export const viewportExamples = {
|
|
200
|
+
// Default responsive viewport
|
|
201
|
+
default: viewportPresets.default,
|
|
202
|
+
|
|
203
|
+
// Mobile app that shouldn't zoom
|
|
204
|
+
mobileApp: viewportPresets.mobileOptimized,
|
|
205
|
+
|
|
206
|
+
// Content-heavy tablet site
|
|
207
|
+
tablet: viewportPresets.tablet,
|
|
208
|
+
|
|
209
|
+
// Desktop-first application
|
|
210
|
+
desktop: viewportPresets.desktop,
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Example: Multi-language sitemap generation
|
|
215
|
+
*/
|
|
216
|
+
export async function generateMultiLangSitemap() {
|
|
217
|
+
const products = await getProducts();
|
|
218
|
+
const locales = ['en', 'es', 'fr', 'de'];
|
|
219
|
+
const baseUrl = 'https://example.com';
|
|
220
|
+
|
|
221
|
+
const routes = products.map((product: any) => ({
|
|
222
|
+
url: `${baseUrl}/products/${product.slug}`,
|
|
223
|
+
lastModified: product.updatedAt,
|
|
224
|
+
changeFrequency: 'daily' as const,
|
|
225
|
+
priority: 0.8,
|
|
226
|
+
}));
|
|
227
|
+
|
|
228
|
+
return generateI18nSitemap(routes, locales, 'en');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Example: Preview mode metadata
|
|
233
|
+
*/
|
|
234
|
+
export function DraftPageMetadata({ isDraft, page }: { isDraft: boolean; page: any }) {
|
|
235
|
+
const baseMetadata: Metadata = {
|
|
236
|
+
title: page.title,
|
|
237
|
+
description: page.description,
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
return generatePreviewMetadata(isDraft, baseMetadata, {
|
|
241
|
+
draftIndicator: '🚧 DRAFT',
|
|
242
|
+
noIndexDrafts: true,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Example: Async metadata generation with Next.js 15 patterns
|
|
248
|
+
*/
|
|
249
|
+
export async function ProductPageMetadata(props: {
|
|
250
|
+
params: Promise<{ id: string }>;
|
|
251
|
+
searchParams: Promise<{ variant?: string }>;
|
|
252
|
+
}) {
|
|
253
|
+
return generateMetadataAsync({
|
|
254
|
+
params: props.params as Promise<Record<string, string>>,
|
|
255
|
+
searchParams: props.searchParams as Promise<Record<string, string | string[] | undefined>>,
|
|
256
|
+
generator: async (params, searchParams: any) => {
|
|
257
|
+
const product = await getProduct(params.id ?? '');
|
|
258
|
+
const { variant } = searchParams;
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
title: variant ? `${product.name} - ${variant as string}` : product.name,
|
|
262
|
+
description: product.description,
|
|
263
|
+
};
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Example: Edge-compatible metadata generation
|
|
270
|
+
*/
|
|
271
|
+
export async function EdgeMetadata(request: any) {
|
|
272
|
+
// This runs on the edge runtime
|
|
273
|
+
const url = new URL(request.url);
|
|
274
|
+
const slug = url.pathname.split('/').pop();
|
|
275
|
+
|
|
276
|
+
return generateMetadataEdge(request, {
|
|
277
|
+
title: `Edge Page - ${slug}`,
|
|
278
|
+
description: 'This metadata was generated on the edge',
|
|
279
|
+
image: '/images/edge-og.jpg',
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Example: Using SEOManager for consistent configuration
|
|
285
|
+
*/
|
|
286
|
+
const seoManager = new SEOManager({
|
|
287
|
+
applicationName: 'My E-commerce Store',
|
|
288
|
+
author: {
|
|
289
|
+
name: 'John Doe',
|
|
290
|
+
url: 'https://johndoe.com',
|
|
291
|
+
},
|
|
292
|
+
keywords: ['ecommerce', 'shopping', 'online store'],
|
|
293
|
+
locale: 'en_US',
|
|
294
|
+
publisher: 'My Company',
|
|
295
|
+
themeColor: '#0070f3',
|
|
296
|
+
twitterHandle: '@mystore',
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Example: Generate error page metadata
|
|
301
|
+
*/
|
|
302
|
+
export function ErrorPageMetadata(statusCode: number) {
|
|
303
|
+
return seoManager.createErrorMetadata(statusCode);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Example: Generate metadata with SEOManager
|
|
308
|
+
*/
|
|
309
|
+
export function PageWithSEOManager({ page }: { page: any }) {
|
|
310
|
+
return seoManager.createMetadata({
|
|
311
|
+
title: page.title,
|
|
312
|
+
description: page.description,
|
|
313
|
+
image: page.image,
|
|
314
|
+
keywords: page.tags,
|
|
315
|
+
article:
|
|
316
|
+
page.type === 'article'
|
|
317
|
+
? {
|
|
318
|
+
publishedTime: page.publishedAt,
|
|
319
|
+
modifiedTime: page.updatedAt,
|
|
320
|
+
authors: [page.author],
|
|
321
|
+
tags: page.tags,
|
|
322
|
+
}
|
|
323
|
+
: undefined,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ============================================
|
|
328
|
+
// HELPER FUNCTIONS (for examples)
|
|
329
|
+
// ============================================
|
|
330
|
+
|
|
331
|
+
async function getProduct(id: string) {
|
|
332
|
+
// Mock implementation
|
|
333
|
+
return {
|
|
334
|
+
id,
|
|
335
|
+
name: 'Example Product',
|
|
336
|
+
description: 'A great product',
|
|
337
|
+
price: 99.99,
|
|
338
|
+
currency: 'USD',
|
|
339
|
+
image: '/images/product.jpg',
|
|
340
|
+
inStock: true,
|
|
341
|
+
brand: 'Example Brand',
|
|
342
|
+
|
|
343
|
+
slug: 'example-product',
|
|
344
|
+
updatedAt: new Date(),
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async function getProducts() {
|
|
349
|
+
// Mock implementation
|
|
350
|
+
return [
|
|
351
|
+
{ slug: 'product-1', updatedAt: new Date() },
|
|
352
|
+
{ slug: 'product-2', updatedAt: new Date() },
|
|
353
|
+
];
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Example: Complete page with all SEO features
|
|
358
|
+
*/
|
|
359
|
+
export default async function ComprehensiveSEOExample() {
|
|
360
|
+
const product = await getProduct('123');
|
|
361
|
+
|
|
362
|
+
// Generate metadata
|
|
363
|
+
const metadata = metadataTemplates.product({
|
|
364
|
+
name: product.name,
|
|
365
|
+
description: product.description,
|
|
366
|
+
price: product.price,
|
|
367
|
+
currency: product.currency,
|
|
368
|
+
image: product.image,
|
|
369
|
+
availability: 'InStock',
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
// Return complete page setup
|
|
373
|
+
return {
|
|
374
|
+
metadata,
|
|
375
|
+
viewport: viewportPresets.default,
|
|
376
|
+
structuredData: {
|
|
377
|
+
'@context': 'https://schema.org',
|
|
378
|
+
'@type': 'Product',
|
|
379
|
+
name: product.name,
|
|
380
|
+
description: product.description,
|
|
381
|
+
},
|
|
382
|
+
};
|
|
383
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Next.js 15 + next-sitemap Integration Example
|
|
3
|
+
* @module @repo/seo/examples/nextjs-15-integration
|
|
4
|
+
*
|
|
5
|
+
* This example demonstrates how to integrate Next.js 15's native sitemap
|
|
6
|
+
* functionality with next-sitemap for the best of both worlds.
|
|
7
|
+
*
|
|
8
|
+
* Benefits of this approach:
|
|
9
|
+
* - Use Next.js 15's app directory for dynamic sitemaps
|
|
10
|
+
* - Use next-sitemap for static generation and advanced features
|
|
11
|
+
* - Automatic sitemap index generation for large sites
|
|
12
|
+
* - Unified robots.txt generation
|
|
13
|
+
*
|
|
14
|
+
* Prerequisites:
|
|
15
|
+
* - Next.js 15.0.0 or later
|
|
16
|
+
* - next-sitemap 4.0.0 or later (optional)
|
|
17
|
+
* - @repo/seo package
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* // 1. Create app directory sitemaps for dynamic content
|
|
21
|
+
* // 2. Use next-sitemap for static pages and sitemap index
|
|
22
|
+
* // 3. Combine both in robots.txt
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
// ============================================
|
|
26
|
+
// STEP 1: App Directory Dynamic Sitemaps
|
|
27
|
+
// ============================================
|
|
28
|
+
|
|
29
|
+
// ============================================
|
|
30
|
+
// STEP 5: Helper for Hybrid Approach
|
|
31
|
+
// ============================================
|
|
32
|
+
import { type MetadataRoute } from 'next';
|
|
33
|
+
|
|
34
|
+
// app/sitemaps/products/sitemap.ts
|
|
35
|
+
import { logError } from '@repo/shared/logs';
|
|
36
|
+
|
|
37
|
+
import {
|
|
38
|
+
convertToNextSitemap,
|
|
39
|
+
createIntegratedSitemapConfig,
|
|
40
|
+
generateSitemapObject,
|
|
41
|
+
type DynamicSitemapRoute,
|
|
42
|
+
} from '../server-next';
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Dynamic product sitemap using Next.js 15 native support
|
|
46
|
+
* This will be available at /sitemaps/products/sitemap.xml
|
|
47
|
+
*/
|
|
48
|
+
export async function generateProductSitemap(): Promise<MetadataRoute.Sitemap> {
|
|
49
|
+
const baseUrl = process.env.NEXT_PUBLIC_URL ?? 'https://example.com';
|
|
50
|
+
|
|
51
|
+
// Fetch your products
|
|
52
|
+
const products = (await fetch(`${baseUrl}/api/products`).then(res => res.json())) as Array<{
|
|
53
|
+
slug: string;
|
|
54
|
+
updatedAt: string;
|
|
55
|
+
name: string;
|
|
56
|
+
images?: Array<{ url: string; alt: string }>;
|
|
57
|
+
}>;
|
|
58
|
+
|
|
59
|
+
const routes: DynamicSitemapRoute[] = products.map(product => ({
|
|
60
|
+
url: `${baseUrl}/products/${product.slug}`,
|
|
61
|
+
lastModified: new Date(product.updatedAt),
|
|
62
|
+
changeFrequency: 'daily',
|
|
63
|
+
priority: 0.8,
|
|
64
|
+
// Next.js 15 supports images in sitemaps!
|
|
65
|
+
images: product.images?.map(img => ({
|
|
66
|
+
url: img.url,
|
|
67
|
+
title: img.alt,
|
|
68
|
+
caption: product.name,
|
|
69
|
+
})),
|
|
70
|
+
}));
|
|
71
|
+
|
|
72
|
+
return generateSitemapObject(routes);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// app/sitemaps/blog/sitemap.ts
|
|
76
|
+
/**
|
|
77
|
+
* Dynamic blog sitemap
|
|
78
|
+
* This will be available at /sitemaps/blog/sitemap.xml
|
|
79
|
+
*/
|
|
80
|
+
export async function generateBlogSitemap(): Promise<MetadataRoute.Sitemap> {
|
|
81
|
+
const baseUrl = process.env.NEXT_PUBLIC_URL ?? 'https://example.com';
|
|
82
|
+
|
|
83
|
+
const posts = (await fetch(`${baseUrl}/api/posts`).then(res => res.json())) as Array<{
|
|
84
|
+
slug: string;
|
|
85
|
+
publishedAt: string;
|
|
86
|
+
}>;
|
|
87
|
+
|
|
88
|
+
const routes: DynamicSitemapRoute[] = posts.map(post => ({
|
|
89
|
+
url: `${baseUrl}/blog/${post.slug}`,
|
|
90
|
+
lastModified: new Date(post.publishedAt),
|
|
91
|
+
changeFrequency: 'weekly',
|
|
92
|
+
priority: 0.6,
|
|
93
|
+
}));
|
|
94
|
+
|
|
95
|
+
return generateSitemapObject(routes);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const config = createIntegratedSitemapConfig({
|
|
99
|
+
siteUrl: process.env.SITE_URL ?? 'https://example.com',
|
|
100
|
+
generateRobotsTxt: true,
|
|
101
|
+
|
|
102
|
+
// Tell next-sitemap about your app directory sitemaps
|
|
103
|
+
appDirSitemaps: ['/sitemaps/products/sitemap.xml', '/sitemaps/blog/sitemap.xml'],
|
|
104
|
+
|
|
105
|
+
// Handle static pages with next-sitemap
|
|
106
|
+
exclude: [
|
|
107
|
+
'/api/*',
|
|
108
|
+
'/admin/*',
|
|
109
|
+
// Exclude dynamic routes handled by app directory
|
|
110
|
+
'/products/*',
|
|
111
|
+
'/blog/*',
|
|
112
|
+
],
|
|
113
|
+
|
|
114
|
+
// Merge additional static routes
|
|
115
|
+
|
|
116
|
+
additionalPaths: async (_config: unknown) => {
|
|
117
|
+
return [
|
|
118
|
+
{ loc: '/', changefreq: 'daily', priority: 1 },
|
|
119
|
+
{ loc: '/about', changefreq: 'monthly', priority: 0.8 },
|
|
120
|
+
{ loc: '/contact', changefreq: 'monthly', priority: 0.7 },
|
|
121
|
+
];
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
// Advanced: Transform function for static pages (must be synchronous)
|
|
125
|
+
transform: (_config: unknown, path: string) => {
|
|
126
|
+
// Custom logic for specific paths
|
|
127
|
+
if (path === '/') {
|
|
128
|
+
return {
|
|
129
|
+
loc: path,
|
|
130
|
+
changefreq: 'daily',
|
|
131
|
+
priority: 1,
|
|
132
|
+
lastmod: new Date().toISOString(),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
loc: path,
|
|
138
|
+
changefreq: 'monthly',
|
|
139
|
+
priority: 0.5,
|
|
140
|
+
lastmod: new Date().toISOString(),
|
|
141
|
+
};
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
export default config;
|
|
146
|
+
|
|
147
|
+
// ============================================
|
|
148
|
+
// STEP 3: Unified Robots.txt
|
|
149
|
+
// ============================================
|
|
150
|
+
|
|
151
|
+
// app/robots.ts
|
|
152
|
+
/**
|
|
153
|
+
* Unified robots.txt that references all sitemaps
|
|
154
|
+
* Both from next-sitemap and app directory
|
|
155
|
+
*/
|
|
156
|
+
export function robots(): MetadataRoute.Robots {
|
|
157
|
+
const baseUrl = process.env.NEXT_PUBLIC_URL ?? 'https://example.com';
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
rules: [
|
|
161
|
+
{
|
|
162
|
+
userAgent: '*',
|
|
163
|
+
allow: '/',
|
|
164
|
+
disallow: ['/api/', '/admin/', '/private/'],
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
userAgent: 'Googlebot',
|
|
168
|
+
allow: '/',
|
|
169
|
+
},
|
|
170
|
+
],
|
|
171
|
+
// Reference all sitemaps
|
|
172
|
+
sitemap: [
|
|
173
|
+
`${baseUrl}/sitemap.xml`, // Main sitemap from next-sitemap
|
|
174
|
+
`${baseUrl}/sitemap-0.xml`, // Additional sitemaps from next-sitemap
|
|
175
|
+
`${baseUrl}/sitemaps/products/sitemap.xml`, // App directory sitemap
|
|
176
|
+
`${baseUrl}/sitemaps/blog/sitemap.xml`, // App directory sitemap
|
|
177
|
+
],
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ============================================
|
|
182
|
+
// STEP 4: Sitemap Index (Optional)
|
|
183
|
+
// ============================================
|
|
184
|
+
|
|
185
|
+
// app/sitemap.ts
|
|
186
|
+
/**
|
|
187
|
+
* Main sitemap index that combines all sitemaps
|
|
188
|
+
* This creates a sitemap index for very large sites
|
|
189
|
+
*/
|
|
190
|
+
export function sitemap(): MetadataRoute.Sitemap {
|
|
191
|
+
const baseUrl = process.env.NEXT_PUBLIC_URL ?? 'https://example.com';
|
|
192
|
+
|
|
193
|
+
// Return sitemap index entries
|
|
194
|
+
return [
|
|
195
|
+
{
|
|
196
|
+
url: `${baseUrl}/sitemap-static.xml`,
|
|
197
|
+
lastModified: new Date(),
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
url: `${baseUrl}/sitemaps/products/sitemap.xml`,
|
|
201
|
+
lastModified: new Date(),
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
url: `${baseUrl}/sitemaps/blog/sitemap.xml`,
|
|
205
|
+
lastModified: new Date(),
|
|
206
|
+
},
|
|
207
|
+
];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Utility to fetch and merge sitemaps from different sources
|
|
212
|
+
*/
|
|
213
|
+
export async function mergeSitemaps(sources: string[]): Promise<MetadataRoute.Sitemap> {
|
|
214
|
+
const allRoutes: MetadataRoute.Sitemap = [];
|
|
215
|
+
|
|
216
|
+
for (const source of sources) {
|
|
217
|
+
try {
|
|
218
|
+
const response = await fetch(source);
|
|
219
|
+
const data = (await response.json()) as MetadataRoute.Sitemap;
|
|
220
|
+
allRoutes.push(...data);
|
|
221
|
+
} catch (error: unknown) {
|
|
222
|
+
logError(`Failed to fetch sitemap from ${source}`, { error, source });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return allRoutes;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Convert between Next.js and next-sitemap formats
|
|
231
|
+
*/
|
|
232
|
+
export function convertSitemapFormats(
|
|
233
|
+
nextjsSitemap: Array<{
|
|
234
|
+
url: string;
|
|
235
|
+
lastModified?: Date;
|
|
236
|
+
changeFrequency?: string;
|
|
237
|
+
priority?: number;
|
|
238
|
+
}>,
|
|
239
|
+
) {
|
|
240
|
+
return convertToNextSitemap(nextjsSitemap);
|
|
241
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Main SEO Package Index
|
|
3
|
+
*
|
|
4
|
+
* Re-exports commonly used SEO functionality from various modules.
|
|
5
|
+
* This provides a convenient single import point for basic SEO needs.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Structured data (JSON-LD) generation
|
|
9
|
+
* - Next.js metadata utilities
|
|
10
|
+
* - SEO validation utilities
|
|
11
|
+
* - i18n-enhanced metadata
|
|
12
|
+
*
|
|
13
|
+
* @module @repo/seo
|
|
14
|
+
*
|
|
15
|
+
* @example Basic usage with structured data
|
|
16
|
+
* ```tsx
|
|
17
|
+
* import { JsonLd, structuredData } from '@repo/seo';
|
|
18
|
+
*
|
|
19
|
+
* const article = structuredData.article({
|
|
20
|
+
* headline: 'Getting Started',
|
|
21
|
+
* author: 'John Doe',
|
|
22
|
+
* datePublished: '2024-01-01'
|
|
23
|
+
* });
|
|
24
|
+
*
|
|
25
|
+
* export default function Page() {
|
|
26
|
+
* return <JsonLd data={article} />;
|
|
27
|
+
* }
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* @example With validation
|
|
31
|
+
* ```ts
|
|
32
|
+
* import { validateSchemaOrg, structuredData } from '@repo/seo';
|
|
33
|
+
*
|
|
34
|
+
* const org = structuredData.organization({
|
|
35
|
+
* name: 'Acme Corp',
|
|
36
|
+
* url: 'https://acme.com'
|
|
37
|
+
* });
|
|
38
|
+
*
|
|
39
|
+
* if (validateSchemaOrg(org)) {
|
|
40
|
+
* // Data is valid
|
|
41
|
+
* }
|
|
42
|
+
* ```
|
|
43
|
+
*
|
|
44
|
+
* @example Next.js metadata
|
|
45
|
+
* ```ts
|
|
46
|
+
* import { createMetadata } from '@repo/seo';
|
|
47
|
+
*
|
|
48
|
+
* export const metadata = createMetadata({
|
|
49
|
+
* title: 'Page Title',
|
|
50
|
+
* description: 'Page description',
|
|
51
|
+
* image: '/og-image.png'
|
|
52
|
+
* });
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
// Re-export client-side components
|
|
57
|
+
export { JsonLd } from './client';
|
|
58
|
+
|
|
59
|
+
// Re-export server-side utilities
|
|
60
|
+
export { createStructuredData, structuredData } from './server';
|
|
61
|
+
|
|
62
|
+
// Re-export structured data utilities
|
|
63
|
+
export {
|
|
64
|
+
JsonLd as StructuredDataJsonLd,
|
|
65
|
+
createStructuredData as createStructuredDataUtil,
|
|
66
|
+
structuredData as structuredDataHelpers,
|
|
67
|
+
} from './components/structured-data';
|
|
68
|
+
|
|
69
|
+
// Re-export metadata utilities
|
|
70
|
+
export { createMetadata } from './utils/metadata';
|
|
71
|
+
|
|
72
|
+
// Re-export validation utilities (for convenience)
|
|
73
|
+
export {
|
|
74
|
+
validateImages,
|
|
75
|
+
validateImagesDetailed,
|
|
76
|
+
validateMetadata,
|
|
77
|
+
validateOpenGraph,
|
|
78
|
+
validateSchemaOrg,
|
|
79
|
+
validateSchemaOrgDetailed,
|
|
80
|
+
validateUrl,
|
|
81
|
+
validateUrls,
|
|
82
|
+
validateUrlsDetailed,
|
|
83
|
+
} from './utils/validation';
|
|
84
|
+
|
|
85
|
+
// Re-export types
|
|
86
|
+
export type { StructuredDataType, Thing, WithContext } from './server';
|
|
87
|
+
export type { ValidationResult } from './utils/validation';
|