@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.
Files changed (56) hide show
  1. package/README.md +586 -0
  2. package/dist/client-next.d.mts +46 -0
  3. package/dist/client-next.d.mts.map +1 -0
  4. package/dist/client-next.mjs +92 -0
  5. package/dist/client-next.mjs.map +1 -0
  6. package/dist/client.d.mts +16 -0
  7. package/dist/client.d.mts.map +1 -0
  8. package/dist/client.mjs +47 -0
  9. package/dist/client.mjs.map +1 -0
  10. package/dist/env.d.mts +31 -0
  11. package/dist/env.d.mts.map +1 -0
  12. package/dist/env.mjs +125 -0
  13. package/dist/env.mjs.map +1 -0
  14. package/dist/index.d.mts +30 -0
  15. package/dist/index.d.mts.map +1 -0
  16. package/dist/index.mjs +129 -0
  17. package/dist/index.mjs.map +1 -0
  18. package/dist/server-next.d.mts +230 -0
  19. package/dist/server-next.d.mts.map +1 -0
  20. package/dist/server-next.mjs +541 -0
  21. package/dist/server-next.mjs.map +1 -0
  22. package/dist/server.d.mts +3 -0
  23. package/dist/server.mjs +3 -0
  24. package/dist/structured-data-builders-ByJ4KCEf.mjs +176 -0
  25. package/dist/structured-data-builders-ByJ4KCEf.mjs.map +1 -0
  26. package/dist/structured-data-builders-CAgdYvmz.d.mts +74 -0
  27. package/dist/structured-data-builders-CAgdYvmz.d.mts.map +1 -0
  28. package/dist/structured-data.d.mts +16 -0
  29. package/dist/structured-data.d.mts.map +1 -0
  30. package/dist/structured-data.mjs +62 -0
  31. package/dist/structured-data.mjs.map +1 -0
  32. package/dist/validation.d.mts +20 -0
  33. package/dist/validation.d.mts.map +1 -0
  34. package/dist/validation.mjs +233 -0
  35. package/dist/validation.mjs.map +1 -0
  36. package/package.json +110 -0
  37. package/src/client-next.tsx +134 -0
  38. package/src/client.tsx +96 -0
  39. package/src/components/json-ld.tsx +74 -0
  40. package/src/components/structured-data.tsx +91 -0
  41. package/src/examples/app-router-sitemap.ts +109 -0
  42. package/src/examples/metadata-patterns.ts +528 -0
  43. package/src/examples/next-sitemap-config.ts +92 -0
  44. package/src/examples/nextjs-15-features.tsx +383 -0
  45. package/src/examples/nextjs-15-integration.ts +241 -0
  46. package/src/index.ts +87 -0
  47. package/src/server-next.ts +958 -0
  48. package/src/server.ts +27 -0
  49. package/src/types/metadata.ts +85 -0
  50. package/src/types/seo.ts +60 -0
  51. package/src/types/structured-data.ts +94 -0
  52. package/src/utils/i18n-enhanced.ts +148 -0
  53. package/src/utils/metadata-enhanced.ts +238 -0
  54. package/src/utils/metadata.ts +169 -0
  55. package/src/utils/structured-data-builders.ts +322 -0
  56. package/src/utils/validation.ts +284 -0
@@ -0,0 +1,541 @@
1
+ import { env, getProtocol, safeEnv } from "./env.mjs";
2
+ import { validateUrls } from "./validation.mjs";
3
+ import { n as structuredData, t as createStructuredData } from "./structured-data-builders-ByJ4KCEf.mjs";
4
+ import { logWarn } from "@od-oneapp/shared/logger";
5
+ import "next";
6
+ import merge from "lodash.merge";
7
+ import "server-only";
8
+
9
+ //#region src/server-next.ts
10
+ const viewport = {
11
+ initialScale: 1,
12
+ maximumScale: 5,
13
+ userScalable: true,
14
+ viewportFit: "cover",
15
+ width: "device-width"
16
+ };
17
+ const OG_IMAGE_WIDTH = 1200;
18
+ const OG_IMAGE_HEIGHT = 630;
19
+ /**
20
+ * Safely creates a URL object with error handling
21
+ */
22
+ function safeCreateUrl(url, protocol) {
23
+ if (!url) return void 0;
24
+ try {
25
+ const fullUrl = url.startsWith("http") ? url : `${protocol}://${url}`;
26
+ if (!validateUrls([fullUrl])) {
27
+ logWarn(`[SEO] Invalid URL provided: ${url}, skipping metadataBase`, { url });
28
+ return;
29
+ }
30
+ return new URL(fullUrl);
31
+ } catch (error) {
32
+ logWarn(`[SEO] Failed to create URL from ${url}`, {
33
+ error,
34
+ url
35
+ });
36
+ return;
37
+ }
38
+ }
39
+ /**
40
+ * Simple metadata generator for Next.js applications
41
+ *
42
+ * Creates a complete metadata object with sensible defaults for Open Graph,
43
+ * Twitter Cards, and other meta tags.
44
+ *
45
+ * @param options - Metadata configuration options
46
+ * @param options.title - Page title (will be combined with applicationName)
47
+ * @param options.description - Page description
48
+ * @param options.image - Optional Open Graph image URL
49
+ * @param options.applicationName - Application name (defaults to 'Application')
50
+ * @param options.author - Author information
51
+ * @param options.publisher - Publisher name
52
+ * @param options.twitterHandle - Twitter handle (e.g., '@username')
53
+ * @returns Complete Next.js Metadata object
54
+ *
55
+ * @example
56
+ * ```typescript
57
+ * export const metadata = createMetadata({
58
+ * title: 'Home Page',
59
+ * description: 'Welcome to our site',
60
+ * applicationName: 'My App',
61
+ * image: '/og-image.png',
62
+ * author: { name: 'John Doe', url: 'https://johndoe.com' },
63
+ * publisher: 'My Company',
64
+ * twitterHandle: '@myapp'
65
+ * });
66
+ * ```
67
+ */
68
+ const createMetadata = ({ applicationName = "Application", author = {
69
+ name: "Author",
70
+ url: "https://example.com"
71
+ }, description, image, publisher = "Publisher", title, twitterHandle = "@site", ...properties }) => {
72
+ const protocol = getProtocol();
73
+ const envVars = safeEnv();
74
+ const productionUrl = envVars.VERCEL_PROJECT_PRODUCTION_URL ?? envVars.NEXT_PUBLIC_URL;
75
+ const parsedTitle = `${title} | ${applicationName}`;
76
+ const metadata = merge({
77
+ appleWebApp: {
78
+ capable: true,
79
+ statusBarStyle: "default",
80
+ title: parsedTitle
81
+ },
82
+ applicationName,
83
+ authors: author ? Array.isArray(author) ? author : [author] : void 0,
84
+ creator: author ? Array.isArray(author) ? author[0]?.name : author.name : void 0,
85
+ description,
86
+ formatDetection: { telephone: false },
87
+ metadataBase: safeCreateUrl(productionUrl, protocol),
88
+ openGraph: {
89
+ description,
90
+ locale: "en_US",
91
+ siteName: applicationName,
92
+ title: parsedTitle,
93
+ type: "website"
94
+ },
95
+ publisher,
96
+ title: parsedTitle,
97
+ twitter: {
98
+ card: "summary_large_image",
99
+ creator: twitterHandle,
100
+ site: twitterHandle
101
+ }
102
+ }, properties);
103
+ if (image && metadata.openGraph) metadata.openGraph.images = [{
104
+ alt: title,
105
+ height: OG_IMAGE_HEIGHT,
106
+ url: image,
107
+ width: OG_IMAGE_WIDTH
108
+ }];
109
+ return metadata;
110
+ };
111
+ /**
112
+ * Centralized SEO manager for consistent metadata generation
113
+ *
114
+ * The SEOManager class provides a centralized way to manage SEO configuration
115
+ * across your application. It ensures consistent metadata generation and
116
+ * reduces duplication by storing common values like application name, author,
117
+ * and publisher information.
118
+ *
119
+ * @example
120
+ * ```typescript
121
+ * const seoManager = new SEOManager({
122
+ * applicationName: 'My App',
123
+ * author: { name: 'John Doe', url: 'https://johndoe.com' },
124
+ * publisher: 'My Company',
125
+ * twitterHandle: '@myapp',
126
+ * keywords: ['app', 'software'],
127
+ * locale: 'en_US',
128
+ * themeColor: '#0070f3'
129
+ * });
130
+ *
131
+ * // In your pages
132
+ * export async function generateMetadata() {
133
+ * return seoManager.createMetadata({
134
+ * title: 'Home Page',
135
+ * description: 'Welcome to our app',
136
+ * image: '/og-image.png'
137
+ * });
138
+ * }
139
+ * ```
140
+ */
141
+ var SEOManager = class {
142
+ config;
143
+ constructor(config) {
144
+ this.config = config;
145
+ }
146
+ /**
147
+ * Generate metadata for error pages (404, 500, etc.)
148
+ *
149
+ * @param statusCode - HTTP status code (404, 500, 503)
150
+ * @returns Metadata with noIndex and noFollow enabled
151
+ *
152
+ * @example
153
+ * ```typescript
154
+ * export const metadata = seoManager.createErrorMetadata(404);
155
+ * ```
156
+ */
157
+ createErrorMetadata(statusCode) {
158
+ return this.createMetadata({
159
+ description: `Error ${statusCode}`,
160
+ noFollow: true,
161
+ noIndex: true,
162
+ title: {
163
+ 404: "Page Not Found",
164
+ 500: "Internal Server Error",
165
+ 503: "Service Unavailable"
166
+ }[statusCode] ?? "Error"
167
+ });
168
+ }
169
+ createMetadata(options) {
170
+ const { alternates, article, canonical, description, image, keywords = [], noFollow = false, noIndex = false, title, ...additionalProperties } = options;
171
+ const protocol = getProtocol();
172
+ const productionUrl = env?.VERCEL_PROJECT_PRODUCTION_URL ?? env?.NEXT_PUBLIC_URL;
173
+ const parsedTitle = `${title} | ${this.config.applicationName}`;
174
+ const allKeywords = [...this.config.keywords ?? [], ...keywords];
175
+ const defaultMetadata = {
176
+ alternates: alternates ?? (canonical ? { canonical } : void 0),
177
+ appleWebApp: {
178
+ capable: true,
179
+ statusBarStyle: "default",
180
+ title
181
+ },
182
+ applicationName: this.config.applicationName,
183
+ appLinks: {},
184
+ authors: [{
185
+ name: this.config.author.name,
186
+ url: this.config.author.url
187
+ }],
188
+ creator: this.config.author.name,
189
+ description,
190
+ formatDetection: {
191
+ address: false,
192
+ date: false,
193
+ email: false,
194
+ telephone: false
195
+ },
196
+ keywords: allKeywords,
197
+ metadataBase: safeCreateUrl(productionUrl, protocol),
198
+ openGraph: {
199
+ description,
200
+ locale: this.config.locale ?? "en_US",
201
+ siteName: this.config.applicationName,
202
+ title: parsedTitle,
203
+ type: article ? "article" : "website",
204
+ ...article && { article: {
205
+ ...article.authors && { authors: article.authors },
206
+ ...article.expirationTime && { expirationTime: article.expirationTime },
207
+ ...article.modifiedTime && { modifiedTime: article.modifiedTime },
208
+ ...article.publishedTime && { publishedTime: article.publishedTime },
209
+ ...article.section && { section: article.section },
210
+ ...article.tags && { tags: article.tags }
211
+ } }
212
+ },
213
+ publisher: this.config.publisher,
214
+ robots: {
215
+ follow: !noFollow,
216
+ googleBot: {
217
+ follow: !noFollow,
218
+ index: !noIndex,
219
+ "max-image-preview": "large",
220
+ "max-snippet": -1,
221
+ "max-video-preview": -1
222
+ },
223
+ index: !noIndex
224
+ },
225
+ title: {
226
+ default: parsedTitle,
227
+ template: `%s | ${this.config.applicationName}`
228
+ },
229
+ twitter: {
230
+ card: "summary_large_image",
231
+ creator: this.config.twitterHandle,
232
+ description,
233
+ site: this.config.twitterHandle,
234
+ title: parsedTitle
235
+ },
236
+ verification: {}
237
+ };
238
+ if (image) {
239
+ const imageData = typeof image === "string" ? {
240
+ alt: title,
241
+ height: OG_IMAGE_HEIGHT,
242
+ url: image,
243
+ width: OG_IMAGE_WIDTH
244
+ } : {
245
+ alt: title,
246
+ height: OG_IMAGE_HEIGHT,
247
+ width: OG_IMAGE_WIDTH,
248
+ ...image
249
+ };
250
+ if (defaultMetadata.openGraph) defaultMetadata.openGraph.images = [imageData];
251
+ if (defaultMetadata.twitter) defaultMetadata.twitter.images = [imageData.url];
252
+ }
253
+ if (this.config.themeColor) defaultMetadata.themeColor = [{
254
+ color: this.config.themeColor,
255
+ media: "(prefers-color-scheme: light)"
256
+ }, {
257
+ color: this.config.themeColor,
258
+ media: "(prefers-color-scheme: dark)"
259
+ }];
260
+ return merge(defaultMetadata, additionalProperties);
261
+ }
262
+ };
263
+ const metadataTemplates = {
264
+ product: (product) => ({
265
+ title: product.name,
266
+ description: product.description,
267
+ openGraph: {
268
+ title: product.name,
269
+ description: product.description,
270
+ type: "website",
271
+ images: [product.image]
272
+ },
273
+ twitter: {
274
+ card: "summary_large_image",
275
+ title: product.name,
276
+ description: product.description,
277
+ images: [product.image]
278
+ },
279
+ other: {
280
+ "product:price:amount": product.price.toString(),
281
+ "product:price:currency": product.currency,
282
+ "product:availability": product.availability,
283
+ ...product.brand && { "product:brand": product.brand }
284
+ }
285
+ }),
286
+ article: (article) => ({
287
+ title: article.title,
288
+ description: article.description,
289
+ authors: [{ name: article.author }],
290
+ openGraph: {
291
+ title: article.title,
292
+ description: article.description,
293
+ type: "article",
294
+ publishedTime: article.publishedTime.toISOString(),
295
+ modifiedTime: article.modifiedTime?.toISOString(),
296
+ authors: [article.author],
297
+ images: [article.image],
298
+ tags: article.tags,
299
+ section: article.section
300
+ },
301
+ twitter: {
302
+ card: "summary_large_image",
303
+ title: article.title,
304
+ description: article.description,
305
+ images: [article.image]
306
+ }
307
+ }),
308
+ profile: (profile) => ({
309
+ title: profile.name,
310
+ description: profile.bio,
311
+ openGraph: {
312
+ title: profile.name,
313
+ description: profile.bio,
314
+ type: "profile",
315
+ images: [profile.image],
316
+ firstName: profile.firstName,
317
+ lastName: profile.lastName,
318
+ username: profile.username
319
+ },
320
+ twitter: {
321
+ card: "summary",
322
+ title: profile.name,
323
+ description: profile.bio,
324
+ images: [profile.image]
325
+ }
326
+ })
327
+ };
328
+ const viewportPresets = {
329
+ default: {
330
+ width: "device-width",
331
+ initialScale: 1,
332
+ maximumScale: 5,
333
+ userScalable: true,
334
+ viewportFit: "cover"
335
+ },
336
+ mobileOptimized: {
337
+ width: "device-width",
338
+ initialScale: 1,
339
+ maximumScale: 1,
340
+ userScalable: false,
341
+ viewportFit: "cover"
342
+ },
343
+ tablet: {
344
+ width: "device-width",
345
+ initialScale: 1,
346
+ maximumScale: 3,
347
+ userScalable: true,
348
+ viewportFit: "auto"
349
+ },
350
+ desktop: {
351
+ width: "device-width",
352
+ initialScale: 1,
353
+ userScalable: true,
354
+ viewportFit: "auto"
355
+ }
356
+ };
357
+ function generateViewport(userAgent) {
358
+ if (!userAgent) return viewportPresets.default;
359
+ const isMobile = /mobile/i.test(userAgent);
360
+ const isTablet = /tablet|ipad/i.test(userAgent);
361
+ if (isMobile && !isTablet) return viewportPresets.mobileOptimized;
362
+ if (isTablet) return viewportPresets.tablet;
363
+ return viewportPresets.desktop;
364
+ }
365
+ function generateI18nSitemap(routes, locales, defaultLocale = "en") {
366
+ const sitemapEntries = [];
367
+ routes.forEach((route) => {
368
+ locales.forEach((locale) => {
369
+ const entry = {
370
+ url: locale === defaultLocale ? route.url : route.url.replace(/^https?:\/\/[^/]+/, (match) => `${match}/${locale}`),
371
+ lastModified: route.lastModified ?? /* @__PURE__ */ new Date(),
372
+ changeFrequency: route.changeFrequency ?? "weekly",
373
+ priority: route.priority ?? .5
374
+ };
375
+ if (route.images) entry.images = route.images.map((img) => img.url);
376
+ if (route.videos) entry.videos = route.videos.map((video) => ({
377
+ ...video,
378
+ thumbnail_loc: video.thumbnail_url,
379
+ thumbnail_url: void 0
380
+ }));
381
+ sitemapEntries.push(entry);
382
+ });
383
+ });
384
+ return sitemapEntries;
385
+ }
386
+ function generatePreviewMetadata(isDraft, metadata, options) {
387
+ if (!isDraft) return metadata;
388
+ const draftIndicator = options?.draftIndicator ?? "[DRAFT]";
389
+ const noIndexDrafts = options?.noIndexDrafts ?? true;
390
+ return {
391
+ ...metadata,
392
+ title: `${draftIndicator} ${metadata.title}`,
393
+ robots: noIndexDrafts ? {
394
+ index: false,
395
+ follow: false,
396
+ ...typeof metadata.robots === "object" && metadata.robots !== null ? metadata.robots : {}
397
+ } : metadata.robots,
398
+ openGraph: metadata.openGraph ? {
399
+ ...metadata.openGraph,
400
+ title: `${draftIndicator} ${metadata.openGraph.title ?? metadata.title}`
401
+ } : void 0
402
+ };
403
+ }
404
+ async function generateMetadataAsync({ params, searchParams, generator }) {
405
+ const [resolvedParams, resolvedSearchParams] = await Promise.all([params, searchParams]);
406
+ return generator(resolvedParams, resolvedSearchParams);
407
+ }
408
+ /**
409
+ * Edge-compatible metadata generation for edge runtime environments
410
+ *
411
+ * @param request - Request object with URL
412
+ * @param options - Metadata options
413
+ * @param options.title - Page title
414
+ * @param options.description - Page description
415
+ * @param options.image - Optional OG image URL
416
+ * @param options.siteName - Optional site name (defaults to 'Your Site')
417
+ * @returns Metadata object for Next.js
418
+ * @throws Error if request URL is invalid and cannot be parsed
419
+ *
420
+ * @example
421
+ * ```typescript
422
+ * export const runtime = 'edge';
423
+ *
424
+ * export async function GET(request: Request) {
425
+ * const metadata = await generateMetadataEdge(request, {
426
+ * title: 'Edge Page',
427
+ * description: 'Generated on edge',
428
+ * image: '/og-image.png',
429
+ * siteName: 'My Site'
430
+ * });
431
+ * return NextResponse.json(metadata);
432
+ * }
433
+ * ```
434
+ */
435
+ const generateMetadataEdge = async (request, options) => {
436
+ let url;
437
+ try {
438
+ url = new URL(request.url);
439
+ } catch (error) {
440
+ const baseUrl = env?.NEXT_PUBLIC_URL ?? env?.SITE_URL;
441
+ if (!baseUrl) throw new Error("[SEO] Invalid request URL and no base URL configured. Set NEXT_PUBLIC_URL or SITE_URL environment variable.");
442
+ logWarn("[SEO] Invalid request URL, attempting fallback to configured base URL", { error });
443
+ const fallbackUrl = safeCreateUrl(baseUrl, getProtocol());
444
+ if (!fallbackUrl) throw new Error("[SEO] Invalid request URL and configured base URL is also invalid. Please check NEXT_PUBLIC_URL or SITE_URL environment variable.");
445
+ url = fallbackUrl;
446
+ }
447
+ return {
448
+ title: options.title,
449
+ description: options.description,
450
+ metadataBase: safeCreateUrl(url.origin, getProtocol()),
451
+ openGraph: {
452
+ title: options.title,
453
+ description: options.description,
454
+ url: url.href,
455
+ siteName: options.siteName ?? env?.NEXT_PUBLIC_SITE_NAME ?? "Site",
456
+ locale: "en_US",
457
+ type: "website",
458
+ ...options.image && { images: [options.image] }
459
+ },
460
+ twitter: {
461
+ card: options.image ? "summary_large_image" : "summary",
462
+ title: options.title,
463
+ description: options.description,
464
+ ...options.image && { images: [options.image] }
465
+ }
466
+ };
467
+ };
468
+ function createSitemapConfig(config) {
469
+ const baseUrl = config.siteUrl ?? env?.NEXT_PUBLIC_URL ?? env?.SITE_URL;
470
+ if (!baseUrl) throw new Error("SITE_URL or NEXT_PUBLIC_URL environment variable is required for SEO package. Please set one of these variables in your environment configuration.");
471
+ return {
472
+ ...config,
473
+ siteUrl: baseUrl,
474
+ generateRobotsTxt: config.generateRobotsTxt ?? true,
475
+ robotsTxtOptions: {
476
+ policies: [{
477
+ userAgent: "*",
478
+ allow: "/"
479
+ }],
480
+ ...config.robotsTxtOptions
481
+ },
482
+ exclude: [
483
+ "/404",
484
+ "/500",
485
+ "/api/*",
486
+ "/admin/*",
487
+ "*/edit",
488
+ "*/settings",
489
+ ...config.exclude ?? []
490
+ ],
491
+ generateIndexSitemap: config.generateIndexSitemap ?? true,
492
+ sitemapSize: config.sitemapSize ?? 7e3,
493
+ changefreq: config.changefreq ?? "daily",
494
+ priority: config.priority ?? .7
495
+ };
496
+ }
497
+ function generateSitemapObject(routes) {
498
+ return routes.map((route) => {
499
+ const entry = {
500
+ url: route.url,
501
+ lastModified: route.lastModified ?? /* @__PURE__ */ new Date(),
502
+ changeFrequency: route.changeFrequency ?? "weekly",
503
+ priority: route.priority ?? .5
504
+ };
505
+ if (route.images) entry.images = route.images.map((img) => img.url);
506
+ if (route.videos) entry.videos = route.videos.filter((video) => video.thumbnail_url).map((video) => {
507
+ const { thumbnail_url: _thumbnail_url, ...videoWithoutThumbnail } = video;
508
+ return {
509
+ ...videoWithoutThumbnail,
510
+ thumbnail_loc: video.thumbnail_url
511
+ };
512
+ });
513
+ return entry;
514
+ });
515
+ }
516
+ function convertToNextSitemap(nextjsSitemap) {
517
+ return nextjsSitemap.map((item) => ({
518
+ loc: item.url,
519
+ lastmod: item.lastModified?.toISOString(),
520
+ changefreq: item.changeFrequency,
521
+ priority: item.priority
522
+ }));
523
+ }
524
+ function createIntegratedSitemapConfig(config) {
525
+ const baseConfig = createSitemapConfig(config);
526
+ if (config.appDirSitemaps && config.appDirSitemaps.length > 0) baseConfig.robotsTxtOptions = {
527
+ ...baseConfig.robotsTxtOptions,
528
+ additionalSitemaps: [...baseConfig.robotsTxtOptions?.additionalSitemaps ?? [], ...config.appDirSitemaps.map((path) => `${config.siteUrl}${path.startsWith("/") ? path : `/${path}`}`)]
529
+ };
530
+ if (config.mergeAppDirRoutes && config.additionalPaths) {
531
+ const originalAdditionalPaths = config.additionalPaths;
532
+ baseConfig.additionalPaths = async (context) => {
533
+ return await originalAdditionalPaths(context);
534
+ };
535
+ }
536
+ return baseConfig;
537
+ }
538
+
539
+ //#endregion
540
+ export { SEOManager, convertToNextSitemap, createIntegratedSitemapConfig, createMetadata, createSitemapConfig, createStructuredData, generateI18nSitemap, generateMetadataAsync, generateMetadataEdge, generatePreviewMetadata, generateSitemapObject, generateViewport, metadataTemplates, structuredData, viewport, viewportPresets };
541
+ //# sourceMappingURL=server-next.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server-next.mjs","names":[],"sources":["../src/server-next.ts"],"sourcesContent":["/**\n * @fileoverview Server-side SEO exports for Next.js\n *\n * This file provides server-side SEO functionality specifically for Next.js applications.\n * Includes Next.js metadata generation, OpenGraph, Twitter Cards, and structured data.\n *\n * @module @repo/seo/server/next\n */\n\nimport { type Metadata, type MetadataRoute, type Viewport } from 'next';\n\nimport { logWarn } from '@repo/shared/logger';\nimport merge from 'lodash.merge';\nimport 'server-only';\n\nimport { env, getProtocol, safeEnv } from '../env';\n\nimport { validateUrls } from './utils/validation';\n\n// Type extensions for Next.js metadata properties that should exist but might be missing\ndeclare module 'next' {\n interface OpenGraphMetadata {\n type?:\n | 'website'\n | 'article'\n | 'profile'\n | 'book'\n | 'music.song'\n | 'music.album'\n | 'video.movie'\n | 'video.episode'\n | 'video.tv_show'\n | 'video.other';\n publishedTime?: string;\n modifiedTime?: string;\n expirationTime?: string;\n authors?: string | string[];\n section?: string;\n tags?: string | string[];\n username?: string;\n firstName?: string;\n lastName?: string;\n article?: {\n authors?: string[];\n expirationTime?: string;\n modifiedTime?: string;\n publishedTime?: string;\n section?: string;\n tags?: string[];\n };\n }\n\n interface TwitterMetadata {\n card?: 'summary' | 'summary_large_image' | 'app' | 'player';\n }\n}\n\n// Re-export all server-side utilities\nexport { createStructuredData, structuredData } from './server';\nexport type { StructuredDataType } from './server';\n\n// Enhanced viewport configuration for better mobile experience\nexport const viewport: Viewport = {\n initialScale: 1,\n maximumScale: 5,\n userScalable: true,\n viewportFit: 'cover',\n width: 'device-width',\n};\n\n// Constants for Open Graph image dimensions (recommended by OG spec)\nconst OG_IMAGE_WIDTH = 1200;\nconst OG_IMAGE_HEIGHT = 630;\n\n/**\n * Safely creates a URL object with error handling\n */\nfunction safeCreateUrl(url: string | undefined, protocol: string): URL | undefined {\n if (!url) return undefined;\n\n try {\n const fullUrl = url.startsWith('http') ? url : `${protocol}://${url}`;\n if (!validateUrls([fullUrl])) {\n logWarn(`[SEO] Invalid URL provided: ${url}, skipping metadataBase`, { url });\n return undefined;\n }\n return new URL(fullUrl);\n } catch (error) {\n logWarn(`[SEO] Failed to create URL from ${url}`, { error, url });\n return undefined;\n }\n}\n\n/**\n * Metadata generator options\n */\ntype MetadataGenerator = Omit<Metadata, 'description' | 'title'> & {\n applicationName?: string;\n author?: Metadata['authors'];\n description: string;\n image?: string;\n publisher?: string;\n title: string;\n twitterHandle?: string;\n};\n\n/**\n * Simple metadata generator for Next.js applications\n *\n * Creates a complete metadata object with sensible defaults for Open Graph,\n * Twitter Cards, and other meta tags.\n *\n * @param options - Metadata configuration options\n * @param options.title - Page title (will be combined with applicationName)\n * @param options.description - Page description\n * @param options.image - Optional Open Graph image URL\n * @param options.applicationName - Application name (defaults to 'Application')\n * @param options.author - Author information\n * @param options.publisher - Publisher name\n * @param options.twitterHandle - Twitter handle (e.g., '@username')\n * @returns Complete Next.js Metadata object\n *\n * @example\n * ```typescript\n * export const metadata = createMetadata({\n * title: 'Home Page',\n * description: 'Welcome to our site',\n * applicationName: 'My App',\n * image: '/og-image.png',\n * author: { name: 'John Doe', url: 'https://johndoe.com' },\n * publisher: 'My Company',\n * twitterHandle: '@myapp'\n * });\n * ```\n */\nexport const createMetadata = ({\n applicationName = 'Application',\n author = { name: 'Author', url: 'https://example.com' },\n description,\n image,\n publisher = 'Publisher',\n title,\n twitterHandle = '@site',\n ...properties\n}: MetadataGenerator): Metadata => {\n const protocol = getProtocol();\n const envVars = safeEnv();\n const productionUrl = envVars.VERCEL_PROJECT_PRODUCTION_URL ?? envVars.NEXT_PUBLIC_URL;\n const parsedTitle = `${title} | ${applicationName}`;\n const defaultMetadata: Metadata = {\n appleWebApp: {\n capable: true,\n statusBarStyle: 'default',\n title: parsedTitle,\n },\n applicationName,\n authors: author ? (Array.isArray(author) ? author : [author]) : undefined,\n creator: author ? (Array.isArray(author) ? author[0]?.name : author.name) : undefined,\n description,\n formatDetection: {\n telephone: false,\n },\n metadataBase: safeCreateUrl(productionUrl, protocol),\n openGraph: {\n description,\n locale: 'en_US',\n siteName: applicationName,\n title: parsedTitle,\n type: 'website',\n },\n publisher,\n title: parsedTitle,\n twitter: {\n card: 'summary_large_image',\n creator: twitterHandle,\n site: twitterHandle,\n },\n };\n\n const metadata: Metadata = merge(defaultMetadata, properties);\n\n if (image && metadata.openGraph) {\n metadata.openGraph.images = [\n {\n alt: title,\n height: OG_IMAGE_HEIGHT,\n url: image,\n width: OG_IMAGE_WIDTH,\n },\n ];\n }\n\n return metadata;\n};\n\n// Enhanced metadata options\ninterface MetadataGeneratorOptions {\n alternates?: {\n canonical?: string;\n languages?: Record<string, string>;\n };\n article?: {\n authors?: string[];\n expirationTime?: string;\n modifiedTime?: string;\n publishedTime?: string;\n section?: string;\n tags?: string[];\n };\n canonical?: string;\n description: string;\n image?: string | { alt?: string; height?: number; url: string; width?: number };\n keywords?: string[];\n noFollow?: boolean;\n noIndex?: boolean;\n title: string;\n}\n\n/**\n * SEO configuration for the SEOManager\n */\ninterface SEOConfig {\n /** Application name used in all metadata */\n applicationName: string;\n /** Author information */\n author: {\n name: string;\n url: string;\n };\n /** Default keywords for all pages */\n keywords?: string[];\n /** Default locale (e.g., 'en_US') */\n locale?: string;\n /** Publisher name */\n publisher: string;\n /** Theme color for browser UI */\n themeColor?: string;\n /** Twitter handle (e.g., '@username') */\n twitterHandle: string;\n}\n\n/**\n * Centralized SEO manager for consistent metadata generation\n *\n * The SEOManager class provides a centralized way to manage SEO configuration\n * across your application. It ensures consistent metadata generation and\n * reduces duplication by storing common values like application name, author,\n * and publisher information.\n *\n * @example\n * ```typescript\n * const seoManager = new SEOManager({\n * applicationName: 'My App',\n * author: { name: 'John Doe', url: 'https://johndoe.com' },\n * publisher: 'My Company',\n * twitterHandle: '@myapp',\n * keywords: ['app', 'software'],\n * locale: 'en_US',\n * themeColor: '#0070f3'\n * });\n *\n * // In your pages\n * export async function generateMetadata() {\n * return seoManager.createMetadata({\n * title: 'Home Page',\n * description: 'Welcome to our app',\n * image: '/og-image.png'\n * });\n * }\n * ```\n */\nexport class SEOManager {\n private config: SEOConfig;\n\n constructor(config: SEOConfig) {\n this.config = config;\n }\n\n /**\n * Generate metadata for error pages (404, 500, etc.)\n *\n * @param statusCode - HTTP status code (404, 500, 503)\n * @returns Metadata with noIndex and noFollow enabled\n *\n * @example\n * ```typescript\n * export const metadata = seoManager.createErrorMetadata(404);\n * ```\n */\n createErrorMetadata(statusCode: number): Metadata {\n const titles: Record<number, string> = {\n 404: 'Page Not Found',\n 500: 'Internal Server Error',\n 503: 'Service Unavailable',\n };\n\n return this.createMetadata({\n description: `Error ${statusCode}`,\n noFollow: true,\n noIndex: true,\n title: titles[statusCode] ?? 'Error',\n });\n }\n\n createMetadata(options: MetadataGeneratorOptions): Metadata {\n const {\n alternates,\n article,\n canonical,\n description,\n image,\n keywords = [],\n noFollow = false,\n noIndex = false,\n title,\n ...additionalProperties\n } = options;\n\n const protocol = getProtocol();\n const productionUrl = env?.VERCEL_PROJECT_PRODUCTION_URL ?? env?.NEXT_PUBLIC_URL;\n const parsedTitle = `${title} | ${this.config.applicationName}`;\n\n // Combine default keywords with page-specific ones\n const allKeywords = [...(this.config.keywords ?? []), ...keywords];\n\n const defaultMetadata: Metadata = {\n // Alternates for i18n and canonical URLs\n alternates: alternates ?? (canonical ? { canonical } : undefined),\n // Apple Web App\n appleWebApp: {\n capable: true,\n statusBarStyle: 'default',\n title,\n },\n applicationName: this.config.applicationName,\n // App Links for mobile deep linking\n appLinks: {},\n authors: [{ name: this.config.author.name, url: this.config.author.url }],\n creator: this.config.author.name,\n description,\n\n // Enhanced format detection\n formatDetection: {\n address: false,\n date: false,\n email: false,\n telephone: false,\n },\n\n keywords: allKeywords,\n\n // Metadata base for absolute URLs\n metadataBase: safeCreateUrl(productionUrl, protocol),\n\n // Open Graph\n openGraph: {\n description,\n locale: this.config.locale ?? 'en_US',\n siteName: this.config.applicationName,\n title: parsedTitle,\n type: article ? 'article' : 'website',\n ...(article && {\n article: {\n ...(article.authors && { authors: article.authors }),\n ...(article.expirationTime && { expirationTime: article.expirationTime }),\n ...(article.modifiedTime && { modifiedTime: article.modifiedTime }),\n ...(article.publishedTime && { publishedTime: article.publishedTime }),\n ...(article.section && { section: article.section }),\n ...(article.tags && { tags: article.tags }),\n },\n }),\n },\n\n publisher: this.config.publisher,\n\n // Robots directives\n robots: {\n follow: !noFollow,\n googleBot: {\n follow: !noFollow,\n index: !noIndex,\n 'max-image-preview': 'large',\n 'max-snippet': -1,\n 'max-video-preview': -1,\n },\n index: !noIndex,\n },\n\n title: {\n default: parsedTitle,\n template: `%s | ${this.config.applicationName}`,\n },\n\n // Twitter Card\n twitter: {\n card: 'summary_large_image',\n creator: this.config.twitterHandle,\n description,\n site: this.config.twitterHandle,\n title: parsedTitle,\n },\n\n // Verification (can be extended)\n verification: {},\n };\n\n // Handle image configuration\n if (image) {\n const imageData =\n typeof image === 'string'\n ? { alt: title, height: OG_IMAGE_HEIGHT, url: image, width: OG_IMAGE_WIDTH }\n : { alt: title, height: OG_IMAGE_HEIGHT, width: OG_IMAGE_WIDTH, ...image };\n\n if (defaultMetadata.openGraph) {\n defaultMetadata.openGraph.images = [imageData];\n }\n if (defaultMetadata.twitter) {\n defaultMetadata.twitter.images = [imageData.url];\n }\n }\n\n // Theme color\n if (this.config.themeColor) {\n defaultMetadata.themeColor = [\n { color: this.config.themeColor, media: '(prefers-color-scheme: light)' },\n { color: this.config.themeColor, media: '(prefers-color-scheme: dark)' },\n ];\n }\n\n return merge(defaultMetadata, additionalProperties);\n }\n}\n\n// Re-export types\nexport type { Metadata, Viewport } from 'next';\nexport type { Thing, WithContext } from 'schema-dts';\n\n// ============================================\n// Next.js 15 Enhanced Features\n// ============================================\n\n// Metadata templates for common SEO patterns\nexport const metadataTemplates = {\n // Product page metadata template\n product: (product: {\n name: string;\n description: string;\n price: number;\n currency: string;\n image: string;\n availability: 'InStock' | 'OutOfStock';\n brand?: string;\n }): Metadata => ({\n title: product.name,\n description: product.description,\n openGraph: {\n title: product.name,\n description: product.description,\n type: 'website',\n images: [product.image],\n },\n twitter: {\n card: 'summary_large_image',\n title: product.name,\n description: product.description,\n images: [product.image],\n },\n other: {\n 'product:price:amount': product.price.toString(),\n 'product:price:currency': product.currency,\n 'product:availability': product.availability,\n ...(product.brand && { 'product:brand': product.brand }),\n },\n }),\n\n // Article/blog post metadata template\n article: (article: {\n title: string;\n description: string;\n author: string;\n publishedTime: Date;\n modifiedTime?: Date;\n image: string;\n tags?: string[];\n section?: string;\n }): Metadata => ({\n title: article.title,\n description: article.description,\n authors: [{ name: article.author }],\n openGraph: {\n title: article.title,\n description: article.description,\n type: 'article',\n publishedTime: article.publishedTime.toISOString(),\n modifiedTime: article.modifiedTime?.toISOString(),\n authors: [article.author],\n images: [article.image],\n tags: article.tags,\n section: article.section,\n },\n twitter: {\n card: 'summary_large_image',\n title: article.title,\n description: article.description,\n images: [article.image],\n },\n }),\n\n // User profile metadata template\n profile: (profile: {\n name: string;\n bio: string;\n image: string;\n username?: string;\n firstName?: string;\n lastName?: string;\n }): Metadata => ({\n title: profile.name,\n description: profile.bio,\n openGraph: {\n title: profile.name,\n description: profile.bio,\n type: 'profile',\n images: [profile.image],\n firstName: profile.firstName,\n lastName: profile.lastName,\n username: profile.username,\n },\n twitter: {\n card: 'summary',\n title: profile.name,\n description: profile.bio,\n images: [profile.image],\n },\n }),\n};\n\n// Viewport presets for different device types\nexport const viewportPresets = {\n // Default responsive viewport\n default: {\n width: 'device-width',\n initialScale: 1,\n maximumScale: 5,\n userScalable: true,\n viewportFit: 'cover',\n } as Viewport,\n\n // Mobile-optimized viewport (prevents zoom)\n mobileOptimized: {\n width: 'device-width',\n initialScale: 1,\n maximumScale: 1,\n userScalable: false,\n viewportFit: 'cover',\n } as Viewport,\n\n // Tablet-optimized viewport\n tablet: {\n width: 'device-width',\n initialScale: 1,\n maximumScale: 3,\n userScalable: true,\n viewportFit: 'auto',\n } as Viewport,\n\n // Desktop viewport (allows full zoom)\n desktop: {\n width: 'device-width',\n initialScale: 1,\n userScalable: true,\n viewportFit: 'auto',\n } as Viewport,\n};\n\n// Generate viewport based on user agent\nexport function generateViewport(userAgent?: string): Viewport {\n if (!userAgent) return viewportPresets.default;\n\n const isMobile = /mobile/i.test(userAgent);\n const isTablet = /tablet|ipad/i.test(userAgent);\n\n if (isMobile && !isTablet) return viewportPresets.mobileOptimized;\n if (isTablet) return viewportPresets.tablet;\n return viewportPresets.desktop;\n}\n\n// Multi-language sitemap generation with hreflang support\nexport function generateI18nSitemap(\n routes: DynamicSitemapRoute[],\n locales: string[],\n defaultLocale: string = 'en',\n): MetadataRoute.Sitemap {\n const sitemapEntries: MetadataRoute.Sitemap = [];\n\n routes.forEach(route => {\n // Add entry for each locale\n locales.forEach(locale => {\n const localizedUrl =\n locale === defaultLocale\n ? route.url\n : route.url.replace(/^https?:\\/\\/[^/]+/, match => `${match}/${locale}`);\n\n const entry: MetadataRoute.Sitemap[number] = {\n url: localizedUrl,\n lastModified: route.lastModified ?? new Date(),\n changeFrequency: route.changeFrequency ?? 'weekly',\n priority: route.priority ?? 0.5,\n };\n\n // Next.js expects images as string array\n if (route.images) {\n entry.images = route.images.map(img => img.url);\n }\n\n if (route.videos) {\n entry.videos = route.videos.map(video => ({\n ...video,\n thumbnail_loc: video.thumbnail_url,\n thumbnail_url: undefined,\n }));\n }\n\n sitemapEntries.push(entry);\n });\n });\n\n return sitemapEntries;\n}\n\n// Preview mode metadata handling\nexport function generatePreviewMetadata(\n isDraft: boolean,\n metadata: Metadata,\n options?: {\n draftIndicator?: string;\n noIndexDrafts?: boolean;\n },\n): Metadata {\n if (!isDraft) return metadata;\n\n const draftIndicator = options?.draftIndicator ?? '[DRAFT]';\n const noIndexDrafts = options?.noIndexDrafts ?? true;\n\n return {\n ...metadata,\n title: `${draftIndicator} ${metadata.title}`,\n robots: noIndexDrafts\n ? {\n index: false,\n follow: false,\n ...(typeof metadata.robots === 'object' && metadata.robots !== null\n ? metadata.robots\n : {}),\n }\n : metadata.robots,\n openGraph: metadata.openGraph\n ? {\n ...metadata.openGraph,\n title: `${draftIndicator} ${metadata.openGraph.title ?? metadata.title}`,\n }\n : undefined,\n };\n}\n\n// Async metadata generation with Next.js 15 patterns\nexport async function generateMetadataAsync({\n params,\n searchParams,\n generator,\n}: {\n params: Promise<Record<string, string>>;\n searchParams: Promise<Record<string, string | string[] | undefined>>;\n generator: (\n params: Record<string, string>,\n searchParams: Record<string, string | string[] | undefined>,\n ) => Promise<Metadata>;\n}): Promise<Metadata> {\n const [resolvedParams, resolvedSearchParams] = await Promise.all([params, searchParams]);\n\n return generator(resolvedParams, resolvedSearchParams);\n}\n\n/**\n * Edge-compatible metadata generation for edge runtime environments\n *\n * @param request - Request object with URL\n * @param options - Metadata options\n * @param options.title - Page title\n * @param options.description - Page description\n * @param options.image - Optional OG image URL\n * @param options.siteName - Optional site name (defaults to 'Your Site')\n * @returns Metadata object for Next.js\n * @throws Error if request URL is invalid and cannot be parsed\n *\n * @example\n * ```typescript\n * export const runtime = 'edge';\n *\n * export async function GET(request: Request) {\n * const metadata = await generateMetadataEdge(request, {\n * title: 'Edge Page',\n * description: 'Generated on edge',\n * image: '/og-image.png',\n * siteName: 'My Site'\n * });\n * return NextResponse.json(metadata);\n * }\n * ```\n */\nexport const generateMetadataEdge = async (\n request: { url: string },\n options: {\n title: string;\n description: string;\n image?: string;\n siteName?: string;\n },\n): Promise<Metadata> => {\n let url: URL;\n try {\n url = new URL(request.url);\n } catch (error) {\n // In edge runtime, we need a valid URL to generate metadata\n // Try to get from environment as fallback using safe URL creation\n const baseUrl = env?.NEXT_PUBLIC_URL ?? env?.SITE_URL;\n if (!baseUrl) {\n throw new Error(\n '[SEO] Invalid request URL and no base URL configured. Set NEXT_PUBLIC_URL or SITE_URL environment variable.',\n );\n }\n logWarn('[SEO] Invalid request URL, attempting fallback to configured base URL', { error });\n\n const fallbackUrl = safeCreateUrl(baseUrl, getProtocol());\n if (!fallbackUrl) {\n throw new Error(\n '[SEO] Invalid request URL and configured base URL is also invalid. Please check NEXT_PUBLIC_URL or SITE_URL environment variable.',\n );\n }\n url = fallbackUrl;\n }\n\n return {\n title: options.title,\n description: options.description,\n metadataBase: safeCreateUrl(url.origin, getProtocol()),\n openGraph: {\n title: options.title,\n description: options.description,\n url: url.href,\n siteName: options.siteName ?? env?.NEXT_PUBLIC_SITE_NAME ?? 'Site',\n locale: 'en_US',\n type: 'website',\n ...(options.image && { images: [options.image] }),\n },\n twitter: {\n card: options.image ? 'summary_large_image' : 'summary',\n title: options.title,\n description: options.description,\n ...(options.image && { images: [options.image] }),\n },\n };\n};\n\n// Next-sitemap configuration helper (optional)\n// This provides type-safe configuration for next-sitemap package\n// Install next-sitemap separately: npm install next-sitemap\nexport interface NextSitemapConfig {\n siteUrl: string;\n generateRobotsTxt?: boolean;\n robotsTxtOptions?: {\n policies?: Array<{\n userAgent: string;\n allow?: string | string[];\n disallow?: string | string[];\n crawlDelay?: number;\n }>;\n additionalSitemaps?: string[];\n };\n exclude?: string[];\n generateIndexSitemap?: boolean;\n sitemapSize?: number;\n changefreq?: 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never';\n priority?: number;\n transform?: (\n config: NextSitemapConfig,\n url: string,\n ) => {\n loc: string;\n lastmod?: string;\n changefreq?: string;\n priority?: number;\n };\n additionalPaths?: (\n config: NextSitemapConfig,\n ) => Promise<Array<{ loc: string; lastmod?: string; changefreq?: string; priority?: number }>>;\n}\n\n// Helper function to create next-sitemap config with defaults\nexport function createSitemapConfig(config: NextSitemapConfig): NextSitemapConfig {\n const baseUrl = config.siteUrl ?? env?.NEXT_PUBLIC_URL ?? env?.SITE_URL;\n if (!baseUrl) {\n throw new Error(\n 'SITE_URL or NEXT_PUBLIC_URL environment variable is required for SEO package. ' +\n 'Please set one of these variables in your environment configuration.',\n );\n }\n\n return {\n ...config,\n siteUrl: baseUrl,\n generateRobotsTxt: config.generateRobotsTxt ?? true,\n robotsTxtOptions: {\n policies: [\n {\n userAgent: '*',\n allow: '/',\n },\n ],\n ...config.robotsTxtOptions,\n },\n exclude: [\n '/404',\n '/500',\n '/api/*',\n '/admin/*',\n '*/edit',\n '*/settings',\n ...(config.exclude ?? []),\n ],\n generateIndexSitemap: config.generateIndexSitemap ?? true,\n sitemapSize: config.sitemapSize ?? 7000,\n changefreq: config.changefreq ?? 'daily',\n priority: config.priority ?? 0.7,\n };\n}\n\n// Dynamic sitemap generation helper for App Router\nexport interface DynamicSitemapRoute {\n url: string;\n lastModified?: Date;\n changeFrequency?: 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never';\n priority?: number;\n images?: Array<{\n url: string;\n title?: string;\n alt?: string;\n caption?: string;\n geo_location?: string;\n license?: string;\n }>;\n videos?: Array<{\n thumbnail_url: string;\n title: string;\n description: string;\n content_url?: string;\n player_url?: string;\n duration?: number;\n expiration_date?: string;\n rating?: number;\n view_count?: number;\n }>;\n}\n\nexport function generateSitemapObject(routes: DynamicSitemapRoute[]): MetadataRoute.Sitemap {\n return routes.map(route => {\n const entry: MetadataRoute.Sitemap[number] = {\n url: route.url,\n lastModified: route.lastModified ?? new Date(),\n changeFrequency: route.changeFrequency ?? 'weekly',\n priority: route.priority ?? 0.5,\n };\n\n // Next.js expects images as string array\n if (route.images) {\n entry.images = route.images.map(img => img.url);\n }\n\n // Videos need to be transformed to use thumbnail_loc instead of thumbnail_url\n if (route.videos) {\n entry.videos = route.videos\n .filter(video => video.thumbnail_url) // Only include videos with thumbnail\n .map(video => {\n const { thumbnail_url: _thumbnail_url, ...videoWithoutThumbnail } = video;\n return {\n ...videoWithoutThumbnail,\n thumbnail_loc: video.thumbnail_url,\n };\n });\n }\n\n return entry;\n });\n}\n\n// Integration helper: Convert Next.js 15 sitemap to next-sitemap format\nexport function convertToNextSitemap(\n nextjsSitemap: Array<{\n url: string;\n lastModified?: Date;\n changeFrequency?: string;\n priority?: number;\n }>,\n): Array<{\n loc: string;\n lastmod?: string;\n changefreq?: string;\n priority?: number;\n}> {\n return nextjsSitemap.map(item => ({\n loc: item.url,\n lastmod: item.lastModified?.toISOString(),\n changefreq: item.changeFrequency,\n priority: item.priority,\n }));\n}\n\n// Integration helper: Use Next.js 15 dynamic routes with next-sitemap\nexport interface NextSitemapIntegrationConfig extends NextSitemapConfig {\n // Use Next.js 15 app directory sitemaps as additional sources\n appDirSitemaps?: string[];\n // Merge Next.js 15 dynamic routes\n mergeAppDirRoutes?: boolean;\n}\n\nexport function createIntegratedSitemapConfig(\n config: NextSitemapIntegrationConfig,\n): NextSitemapConfig {\n const baseConfig = createSitemapConfig(config);\n\n // If using Next.js 15 app directory sitemaps\n if (config.appDirSitemaps && config.appDirSitemaps.length > 0) {\n baseConfig.robotsTxtOptions = {\n ...baseConfig.robotsTxtOptions,\n additionalSitemaps: [\n ...(baseConfig.robotsTxtOptions?.additionalSitemaps ?? []),\n ...config.appDirSitemaps.map(\n path => `${config.siteUrl}${path.startsWith('/') ? path : `/${path}`}`,\n ),\n ],\n };\n }\n\n // If merging app directory routes\n if (config.mergeAppDirRoutes && config.additionalPaths) {\n const originalAdditionalPaths = config.additionalPaths;\n baseConfig.additionalPaths = async context => {\n const original = await originalAdditionalPaths(context);\n\n // Here you can fetch and merge routes from Next.js 15 app directory\n // This is where you'd integrate with your app/sitemap.ts exports\n\n return original;\n };\n }\n\n return baseConfig;\n}\n"],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import { a as structuredData, i as createStructuredData, t as StructuredDataType } from "./structured-data-builders-CAgdYvmz.mjs";
2
+ import { Thing, WithContext } from "schema-dts";
3
+ export { type StructuredDataType, type Thing, type WithContext, createStructuredData, structuredData };
@@ -0,0 +1,3 @@
1
+ import { n as structuredData, t as createStructuredData } from "./structured-data-builders-ByJ4KCEf.mjs";
2
+
3
+ export { createStructuredData, structuredData };