@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
package/src/server.ts ADDED
@@ -0,0 +1,27 @@
1
+ /**
2
+ * @fileoverview Server-side SEO exports (non-Next.js)
3
+ *
4
+ * This file provides server-side SEO functionality for non-Next.js environments.
5
+ * For Next.js applications, use '@repo/seo/server/next' instead.
6
+ *
7
+ * @module @repo/seo/server
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * import { createStructuredData, structuredData } from '@repo/seo/server';
12
+ *
13
+ * const article = structuredData.article({
14
+ * headline: 'My Article',
15
+ * author: 'John Doe',
16
+ * datePublished: '2024-01-01',
17
+ * publisher: { name: 'Publisher', logo: '/logo.png' }
18
+ * });
19
+ * ```
20
+ */
21
+
22
+ // Re-export everything from the shared builders
23
+ export { createStructuredData, structuredData } from './utils/structured-data-builders';
24
+
25
+ // Re-export types
26
+ export type { Thing, WithContext } from 'schema-dts';
27
+ export type { StructuredDataType } from './utils/structured-data-builders';
@@ -0,0 +1,85 @@
1
+ /**
2
+ * @fileoverview Metadata Type Definitions
3
+ *
4
+ * Provides TypeScript types for metadata and OpenGraph structures.
5
+ * Used for Next.js metadata generation and validation.
6
+ *
7
+ * @module @repo/seo/types/metadata
8
+ */
9
+
10
+ /**
11
+ * Enhanced metadata options for SEO generators
12
+ */
13
+ export interface MetadataOptions {
14
+ alternates?: {
15
+ canonical?: string;
16
+ languages?: Record<string, string>;
17
+ };
18
+ article?: {
19
+ authors?: string[];
20
+ expirationTime?: string;
21
+ modifiedTime?: string;
22
+ publishedTime?: string;
23
+ section?: string;
24
+ tags?: string[];
25
+ };
26
+ canonical?: string;
27
+ description: string;
28
+ image?: string | { alt?: string; height?: number; url: string; width?: number };
29
+ keywords?: string[];
30
+ noFollow?: boolean;
31
+ noIndex?: boolean;
32
+ title: string;
33
+ }
34
+
35
+ /**
36
+ * OpenGraph data structure
37
+ */
38
+ export interface OpenGraphData {
39
+ title: string;
40
+ description: string;
41
+ type: 'website' | 'article' | 'profile' | 'product';
42
+ url?: string;
43
+ siteName?: string;
44
+ images?: Array<{
45
+ url: string;
46
+ width?: number;
47
+ height?: number;
48
+ alt?: string;
49
+ }>;
50
+ locale?: string;
51
+ publishedTime?: string;
52
+ modifiedTime?: string;
53
+ authors?: string[];
54
+ tags?: string[];
55
+ section?: string;
56
+ }
57
+
58
+ /**
59
+ * Twitter Card data structure
60
+ */
61
+ export interface TwitterCardData {
62
+ card: 'summary' | 'summary_large_image' | 'app' | 'player';
63
+ site?: string;
64
+ creator?: string;
65
+ title?: string;
66
+ description?: string;
67
+ images?: string[];
68
+ }
69
+
70
+ /**
71
+ * Complete metadata structure
72
+ */
73
+ export interface CompleteMetadata {
74
+ title: string;
75
+ description: string;
76
+ keywords?: string[];
77
+ canonical?: string;
78
+ openGraph?: OpenGraphData;
79
+ twitter?: TwitterCardData;
80
+ robots?: {
81
+ index?: boolean;
82
+ follow?: boolean;
83
+ };
84
+ themeColor?: string;
85
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * @fileoverview Core SEO Type Definitions
3
+ *
4
+ * Provides TypeScript types for SEO functionality and configurations.
5
+ * Includes structured data types and SEO configuration interfaces.
6
+ *
7
+ * @module @repo/seo/types/seo
8
+ */
9
+
10
+ // Re-export from schema-dts for convenience
11
+ export type { Thing, WithContext } from 'schema-dts';
12
+
13
+ /**
14
+ * Common structured data types with better type safety
15
+ */
16
+ export type StructuredDataType =
17
+ | 'Article'
18
+ | 'BlogPosting'
19
+ | 'BreadcrumbList'
20
+ | 'Course'
21
+ | 'Event'
22
+ | 'FAQPage'
23
+ | 'HowTo'
24
+ | 'LocalBusiness'
25
+ | 'Organization'
26
+ | 'Person'
27
+ | 'Product'
28
+ | 'Recipe'
29
+ | 'SoftwareApplication'
30
+ | 'VideoObject'
31
+ | 'WebSite';
32
+
33
+ /**
34
+ * Configuration for SEO setup
35
+ */
36
+ export interface SEOConfig {
37
+ applicationName: string;
38
+ author: {
39
+ name: string;
40
+ url: string;
41
+ };
42
+ keywords?: string[];
43
+ locale?: string;
44
+ publisher: string;
45
+ themeColor?: string;
46
+ twitterHandle: string;
47
+ }
48
+
49
+ /**
50
+ * Basic SEO metadata structure
51
+ */
52
+ export interface SEOMetadata {
53
+ title: string;
54
+ description: string;
55
+ image?: string;
56
+ keywords?: string[];
57
+ canonical?: string;
58
+ noIndex?: boolean;
59
+ noFollow?: boolean;
60
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * @fileoverview Structured Data Type Definitions
3
+ *
4
+ * Provides TypeScript types for schema.org structured data.
5
+ * Includes input types for common structured data schemas.
6
+ *
7
+ * @module @repo/seo/types/structured-data
8
+ */
9
+
10
+ // Re-export from schema-dts for convenience
11
+ export type { Thing, WithContext } from 'schema-dts';
12
+
13
+ /**
14
+ * Article structured data input
15
+ */
16
+ export interface ArticleData {
17
+ author: string | { name: string; url?: string };
18
+ dateModified?: string;
19
+ datePublished: string;
20
+ description?: string;
21
+ headline: string;
22
+ image?: string | string[];
23
+ mainEntityOfPage?: string;
24
+ publisher: {
25
+ logo?: string;
26
+ name: string;
27
+ };
28
+ }
29
+
30
+ /**
31
+ * Product structured data input
32
+ */
33
+ export interface ProductData {
34
+ aggregateRating?: {
35
+ ratingValue: number;
36
+ reviewCount: number;
37
+ };
38
+ brand?: string;
39
+ description?: string;
40
+ image?: string | string[];
41
+ name: string;
42
+ offers?: {
43
+ availability?: string;
44
+ price: string;
45
+ priceCurrency: string;
46
+ seller?: string;
47
+ };
48
+ }
49
+
50
+ /**
51
+ * Organization structured data input
52
+ */
53
+ export interface OrganizationData {
54
+ contactPoint?: {
55
+ areaServed?: string | string[];
56
+ availableLanguage?: string | string[];
57
+ contactType: string;
58
+ telephone: string;
59
+ };
60
+ description?: string;
61
+ logo?: string;
62
+ name: string;
63
+ sameAs?: string[];
64
+ url: string;
65
+ }
66
+
67
+ /**
68
+ * Website structured data input
69
+ */
70
+ export interface WebsiteData {
71
+ description?: string;
72
+ name: string;
73
+ potentialAction?: {
74
+ queryInput: string;
75
+ target: string;
76
+ };
77
+ url: string;
78
+ }
79
+
80
+ /**
81
+ * Breadcrumb item
82
+ */
83
+ export interface BreadcrumbItem {
84
+ name: string;
85
+ url: string;
86
+ }
87
+
88
+ /**
89
+ * FAQ item
90
+ */
91
+ export interface FAQItem {
92
+ answer: string;
93
+ question: string;
94
+ }
@@ -0,0 +1,148 @@
1
+ /**
2
+ * @fileoverview i18n-enhanced SEO utilities
3
+ *
4
+ * Provides internationalization-aware SEO metadata generation.
5
+ * Extends SEOManager with locale-specific metadata and translations support.
6
+ *
7
+ * @module @repo/seo/utils/i18n-enhanced
8
+ */
9
+
10
+ import { type Metadata } from 'next';
11
+
12
+ import { SEOManager } from './metadata-enhanced';
13
+
14
+ interface I18nSEOConfig {
15
+ defaultLocale: string;
16
+ localeNames: Record<string, string>;
17
+ locales: string[];
18
+ rtlLocales?: string[];
19
+ }
20
+
21
+ export class I18nSEOManager extends SEOManager {
22
+ private i18nConfig: I18nSEOConfig;
23
+
24
+ constructor(config: ConstructorParameters<typeof SEOManager>[0] & { i18n: I18nSEOConfig }) {
25
+ super(config);
26
+ this.i18nConfig = config.i18n;
27
+ }
28
+
29
+ createI18nMetadata(
30
+ options: Parameters<SEOManager['createMetadata']>[0] & {
31
+ locale: string;
32
+ translations?: Record<string, { description: string; title: string }>;
33
+ },
34
+ ): Metadata {
35
+ const { locale, translations, ...baseOptions } = options;
36
+
37
+ // Use translated title and description if available
38
+ const localizedTitle = translations?.[locale]?.title ?? baseOptions.title;
39
+ const localizedDescription = translations?.[locale]?.description ?? baseOptions.description;
40
+
41
+ // Generate language alternates automatically
42
+ const languageAlternates: Record<string, string> = {};
43
+ this.i18nConfig.locales.forEach(loc => {
44
+ if (baseOptions.alternates?.canonical) {
45
+ const basePath = baseOptions.alternates.canonical.replace(/^\/[a-z]{2}\//, '/');
46
+ languageAlternates[loc] = `/${loc}${basePath}`;
47
+ }
48
+ });
49
+
50
+ // Determine OpenGraph locale format
51
+ const ogLocale = this.formatOpenGraphLocale(locale);
52
+
53
+ // Get alternate locales for OpenGraph
54
+ const alternateLocales = this.i18nConfig.locales
55
+ .filter(loc => loc !== locale)
56
+ .map(loc => this.formatOpenGraphLocale(loc));
57
+
58
+ const metadata = super.createMetadata({
59
+ ...baseOptions,
60
+ alternates: {
61
+ ...baseOptions.alternates,
62
+ languages: languageAlternates,
63
+ },
64
+ description: localizedDescription,
65
+ title: localizedTitle,
66
+ });
67
+
68
+ // Enhance OpenGraph with locale information
69
+ if (metadata.openGraph) {
70
+ metadata.openGraph.locale = ogLocale;
71
+ metadata.openGraph.alternateLocale = alternateLocales;
72
+ }
73
+
74
+ // Add language meta tag
75
+ const otherMetadata: Record<string, (number | string)[] | number | string> = {
76
+ 'content-language': locale,
77
+ };
78
+
79
+ // Handle RTL languages
80
+ if (this.i18nConfig.rtlLocales?.includes(locale)) {
81
+ otherMetadata.direction = 'rtl';
82
+ }
83
+
84
+ // Merge with existing metadata.other
85
+ if (metadata.other) {
86
+ Object.entries(metadata.other).forEach(([key, value]) => {
87
+ otherMetadata[key] = value;
88
+ });
89
+ }
90
+
91
+ metadata.other = otherMetadata;
92
+
93
+ return metadata;
94
+ }
95
+
96
+ // Create localized structured data
97
+ createLocalizedStructuredData<T>(
98
+ type: string,
99
+ data: T,
100
+ locale: string,
101
+ translations?: Record<string, Partial<T>>,
102
+ ): T {
103
+ const localizedData = translations?.[locale] ? { ...data, ...translations[locale] } : data;
104
+
105
+ return {
106
+ ...localizedData,
107
+ '@context': 'https://schema.org',
108
+ '@type': type,
109
+ inLanguage: locale,
110
+ } as T;
111
+ }
112
+
113
+ // Generate hreflang tags for international SEO
114
+
115
+ generateHreflangTags(currentPath: string, _currentLocale: string): Record<string, string> {
116
+ const hreflangTags: Record<string, string> = {};
117
+
118
+ // Add x-default for default locale
119
+ const basePath = currentPath.replace(/^\/[a-z]{2}\//, '/');
120
+ hreflangTags['x-default'] = `/${this.i18nConfig.defaultLocale}${basePath}`;
121
+
122
+ // Add all locale variations
123
+ this.i18nConfig.locales.forEach(locale => {
124
+ hreflangTags[locale] = `/${locale}${basePath}`;
125
+ });
126
+
127
+ return hreflangTags;
128
+ }
129
+
130
+ // Format locale for OpenGraph (e.g., 'en' -> 'en_US', 'fr' -> 'fr_FR')
131
+ private formatOpenGraphLocale(locale: string): string {
132
+ const localeMap: Record<string, string> = {
133
+ ar: 'ar_SA',
134
+ de: 'de_DE',
135
+ en: 'en_US',
136
+ es: 'es_ES',
137
+ fr: 'fr_FR',
138
+ it: 'it_IT',
139
+ ja: 'ja_JP',
140
+ ko: 'ko_KR',
141
+ nl: 'nl_NL',
142
+ pt: 'pt_BR',
143
+ ru: 'ru_RU',
144
+ zh: 'zh_CN',
145
+ };
146
+ return localeMap[locale] ?? `${locale}_${locale.toUpperCase()}`;
147
+ }
148
+ }
@@ -0,0 +1,238 @@
1
+ /**
2
+ * @fileoverview Enhanced metadata utilities for SEO
3
+ *
4
+ * Provides advanced metadata generation with enhanced features including
5
+ * SEO manager class, viewport configuration, and comprehensive metadata merging.
6
+ *
7
+ * @module @repo/seo/utils/metadata-enhanced
8
+ */
9
+
10
+ import { type Metadata, type Viewport } 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
+ // Constants for Open Graph image dimensions (recommended by OG spec)
20
+ const OG_IMAGE_WIDTH = 1200;
21
+ const OG_IMAGE_HEIGHT = 630;
22
+
23
+ /**
24
+ * Safely creates a URL object with error handling
25
+ */
26
+ function safeCreateUrl(url: string | undefined, protocol: string): URL | undefined {
27
+ if (!url) return undefined;
28
+
29
+ try {
30
+ const fullUrl = url.startsWith('http') ? url : `${protocol}://${url}`;
31
+ if (!validateUrls([fullUrl])) {
32
+ logWarn(`[SEO] Invalid URL provided: ${url}, skipping metadataBase`, { url });
33
+ return undefined;
34
+ }
35
+ return new URL(fullUrl);
36
+ } catch (error) {
37
+ logWarn(`[SEO] Failed to create URL from ${url}:`, { error });
38
+ return undefined;
39
+ }
40
+ }
41
+
42
+ interface MetadataGeneratorOptions {
43
+ alternates?: {
44
+ canonical?: string;
45
+ languages?: Record<string, string>;
46
+ };
47
+ article?: {
48
+ authors?: string[];
49
+ expirationTime?: string;
50
+ modifiedTime?: string;
51
+ publishedTime?: string;
52
+ section?: string;
53
+ tags?: string[];
54
+ };
55
+ canonical?: string;
56
+ description: string;
57
+ image?: string | { alt?: string; height?: number; url: string; width?: number };
58
+ keywords?: string[];
59
+ noFollow?: boolean;
60
+ noIndex?: boolean;
61
+ other?: Record<string, string | number | (string | number)[]>;
62
+ title: string;
63
+ }
64
+
65
+ interface SEOConfig {
66
+ applicationName: string;
67
+ author: {
68
+ name: string;
69
+ url: string;
70
+ };
71
+ keywords?: string[];
72
+ locale?: string;
73
+ publisher: string;
74
+ themeColor?: string;
75
+ twitterHandle: string;
76
+ }
77
+
78
+ // Enhanced viewport configuration for better mobile experience
79
+ export const viewport: Viewport = {
80
+ initialScale: 1,
81
+ maximumScale: 5,
82
+ userScalable: true,
83
+ viewportFit: 'cover',
84
+ width: 'device-width',
85
+ };
86
+
87
+ export class SEOManager {
88
+ private config: SEOConfig;
89
+
90
+ constructor(config: SEOConfig) {
91
+ this.config = config;
92
+ }
93
+
94
+ // Generate metadata for error pages
95
+ createErrorMetadata(statusCode: number): Metadata {
96
+ const titles: Record<number, string> = {
97
+ 404: 'Page Not Found',
98
+ 500: 'Internal Server Error',
99
+ 503: 'Service Unavailable',
100
+ };
101
+
102
+ return this.createMetadata({
103
+ description: `Error ${statusCode}`,
104
+ noFollow: true,
105
+ noIndex: true,
106
+ title: titles[statusCode] ?? 'Error',
107
+ });
108
+ }
109
+
110
+ createMetadata(options: MetadataGeneratorOptions): Metadata {
111
+ const {
112
+ alternates,
113
+ article,
114
+ canonical,
115
+ description,
116
+ image,
117
+ keywords = [],
118
+ noFollow = false,
119
+ noIndex = false,
120
+ title,
121
+ ...additionalProperties
122
+ } = options;
123
+
124
+ const protocol = getProtocol();
125
+ const envVars = safeEnv();
126
+ const productionUrl = envVars.VERCEL_PROJECT_PRODUCTION_URL ?? envVars.NEXT_PUBLIC_URL;
127
+ const parsedTitle = `${title} | ${this.config.applicationName}`;
128
+
129
+ // Combine default keywords with page-specific ones
130
+ const allKeywords = [...(this.config.keywords ?? []), ...keywords];
131
+
132
+ const defaultMetadata: Metadata = {
133
+ // Alternates for i18n and canonical URLs
134
+ alternates: alternates ?? (canonical ? { canonical } : undefined),
135
+ // Apple Web App
136
+ appleWebApp: {
137
+ capable: true,
138
+ statusBarStyle: 'default',
139
+ title,
140
+ },
141
+ applicationName: this.config.applicationName,
142
+ // App Links for mobile deep linking
143
+ appLinks: {},
144
+ authors: [{ name: this.config.author.name, url: this.config.author.url }],
145
+ creator: this.config.author.name,
146
+ description,
147
+
148
+ // Enhanced format detection
149
+ formatDetection: {
150
+ address: false,
151
+ date: false,
152
+ email: false,
153
+ telephone: false,
154
+ },
155
+
156
+ keywords: allKeywords,
157
+
158
+ // Metadata base for absolute URLs
159
+ metadataBase: safeCreateUrl(productionUrl, protocol),
160
+
161
+ // Open Graph
162
+ openGraph: {
163
+ description,
164
+ locale: this.config.locale ?? 'en_US',
165
+ siteName: this.config.applicationName,
166
+ title: parsedTitle,
167
+ type: article ? 'article' : 'website',
168
+ ...(article && {
169
+ article: {
170
+ ...(article.authors && { authors: article.authors }),
171
+ ...(article.expirationTime && { expirationTime: article.expirationTime }),
172
+ ...(article.modifiedTime && { modifiedTime: article.modifiedTime }),
173
+ ...(article.publishedTime && { publishedTime: article.publishedTime }),
174
+ ...(article.section && { section: article.section }),
175
+ ...(article.tags && { tags: article.tags }),
176
+ },
177
+ }),
178
+ },
179
+
180
+ publisher: this.config.publisher,
181
+
182
+ // Robots directives
183
+ robots: {
184
+ follow: !noFollow,
185
+ googleBot: {
186
+ follow: !noFollow,
187
+ index: !noIndex,
188
+ 'max-image-preview': 'large',
189
+ 'max-snippet': -1,
190
+ 'max-video-preview': -1,
191
+ },
192
+ index: !noIndex,
193
+ },
194
+
195
+ title: {
196
+ default: parsedTitle,
197
+ template: `%s | ${this.config.applicationName}`,
198
+ },
199
+
200
+ // Twitter Card
201
+ twitter: {
202
+ card: 'summary_large_image',
203
+ creator: this.config.twitterHandle,
204
+ description,
205
+ site: this.config.twitterHandle,
206
+ title: parsedTitle,
207
+ },
208
+
209
+ // Verification (can be extended)
210
+ verification: {},
211
+ };
212
+
213
+ // Handle image configuration
214
+ if (image) {
215
+ const imageData =
216
+ typeof image === 'string'
217
+ ? { alt: title, height: OG_IMAGE_HEIGHT, url: image, width: OG_IMAGE_WIDTH }
218
+ : { alt: title, height: OG_IMAGE_HEIGHT, width: OG_IMAGE_WIDTH, ...image };
219
+
220
+ if (defaultMetadata.openGraph) {
221
+ defaultMetadata.openGraph.images = [imageData];
222
+ }
223
+ if (defaultMetadata.twitter) {
224
+ defaultMetadata.twitter.images = [imageData.url];
225
+ }
226
+ }
227
+
228
+ // Theme color
229
+ if (this.config.themeColor) {
230
+ defaultMetadata.themeColor = [
231
+ { color: this.config.themeColor, media: '(prefers-color-scheme: light)' },
232
+ { color: this.config.themeColor, media: '(prefers-color-scheme: dark)' },
233
+ ];
234
+ }
235
+
236
+ return merge(defaultMetadata, additionalProperties);
237
+ }
238
+ }