@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
package/README.md
ADDED
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
# @repo/seo
|
|
2
|
+
|
|
3
|
+
Enterprise-grade SEO utilities for Next.js applications with comprehensive metadata generation, structured data, and
|
|
4
|
+
sitemap support.
|
|
5
|
+
|
|
6
|
+
## Status
|
|
7
|
+
|
|
8
|
+
- **Build**: ✅ TypeScript source package (no build step required)
|
|
9
|
+
- **Tests**: ✅ 19 test files with comprehensive coverage
|
|
10
|
+
- **Type Safety**: ✅ Strict TypeScript with schema-dts integration
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pnpm add @repo/seo
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Quick Start
|
|
19
|
+
|
|
20
|
+
### Next.js 15 Metadata (App Router)
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
// app/layout.tsx
|
|
24
|
+
import { createMetadata } from "@repo/seo/server/next";
|
|
25
|
+
|
|
26
|
+
export const metadata = createMetadata({
|
|
27
|
+
title: "Home",
|
|
28
|
+
description: "Welcome to our application",
|
|
29
|
+
image: "/og-image.png",
|
|
30
|
+
applicationName: "My App",
|
|
31
|
+
author: { name: "Your Name", url: "https://example.com" },
|
|
32
|
+
publisher: "Your Company",
|
|
33
|
+
twitterHandle: "@yourhandle"
|
|
34
|
+
});
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Structured Data (JSON-LD)
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
// app/products/[id]/page.tsx
|
|
41
|
+
import { JsonLd } from "@repo/seo/client/next";
|
|
42
|
+
import { structuredData } from "@repo/seo/server/next";
|
|
43
|
+
|
|
44
|
+
export default function ProductPage({ product }) {
|
|
45
|
+
const productSchema = structuredData.product({
|
|
46
|
+
name: product.title,
|
|
47
|
+
description: product.description,
|
|
48
|
+
image: product.image,
|
|
49
|
+
offers: {
|
|
50
|
+
price: product.price.toString(),
|
|
51
|
+
priceCurrency: "USD",
|
|
52
|
+
availability: "https://schema.org/InStock",
|
|
53
|
+
},
|
|
54
|
+
brand: product.brand,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<>
|
|
59
|
+
<JsonLd data={productSchema} />
|
|
60
|
+
{/* Your product content */}
|
|
61
|
+
</>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Dynamic Sitemap
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
// app/sitemap.ts
|
|
70
|
+
import { generateSitemapObject } from "@repo/seo/server/next";
|
|
71
|
+
import { getBaseUrl } from "@repo/seo/env";
|
|
72
|
+
|
|
73
|
+
export default async function sitemap() {
|
|
74
|
+
const products = await getAllProducts();
|
|
75
|
+
const baseUrl = getBaseUrl({ required: true });
|
|
76
|
+
|
|
77
|
+
return generateSitemapObject([
|
|
78
|
+
{
|
|
79
|
+
url: baseUrl,
|
|
80
|
+
changeFrequency: "daily",
|
|
81
|
+
priority: 1,
|
|
82
|
+
lastModified: new Date()
|
|
83
|
+
},
|
|
84
|
+
...products.map((product) => ({
|
|
85
|
+
url: `${baseUrl}/products/${product.slug}`,
|
|
86
|
+
lastModified: product.updatedAt,
|
|
87
|
+
changeFrequency: "weekly" as const,
|
|
88
|
+
priority: 0.8,
|
|
89
|
+
images: [{ url: product.image, title: product.title }]
|
|
90
|
+
}))
|
|
91
|
+
]);
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Package Exports
|
|
96
|
+
|
|
97
|
+
This package uses the modern exports field for precise imports:
|
|
98
|
+
|
|
99
|
+
- **`@repo/seo`** - Main entry point (convenience re-exports)
|
|
100
|
+
- **`@repo/seo/client`** - Client-side components (generic)
|
|
101
|
+
- **`@repo/seo/server`** - Server-side utilities (generic)
|
|
102
|
+
- **`@repo/seo/client/next`** - Next.js client components
|
|
103
|
+
- **`@repo/seo/server/next`** - Next.js server utilities
|
|
104
|
+
- **`@repo/seo/structured-data`** - Structured data components
|
|
105
|
+
|
|
106
|
+
## Core Features
|
|
107
|
+
|
|
108
|
+
### 1. Next.js 15 Integration
|
|
109
|
+
|
|
110
|
+
Full support for Next.js 15 App Router metadata API:
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
import { SEOManager } from "@repo/seo/server/next";
|
|
114
|
+
|
|
115
|
+
const seoManager = new SEOManager({
|
|
116
|
+
applicationName: "My App",
|
|
117
|
+
author: { name: "Author Name", url: "https://author.com" },
|
|
118
|
+
publisher: "Publisher Name",
|
|
119
|
+
twitterHandle: "@handle",
|
|
120
|
+
keywords: ["keyword1", "keyword2"],
|
|
121
|
+
locale: "en_US",
|
|
122
|
+
themeColor: "#000000"
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Generate metadata for any page
|
|
126
|
+
export async function generateMetadata({ params }) {
|
|
127
|
+
return seoManager.createMetadata({
|
|
128
|
+
title: "Page Title",
|
|
129
|
+
description: "Page description",
|
|
130
|
+
image: "/image.png",
|
|
131
|
+
keywords: ["page-specific", "keywords"],
|
|
132
|
+
canonical: "https://example.com/canonical-url"
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Error pages
|
|
137
|
+
export const metadata = seoManager.createErrorMetadata(404);
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### 2. Metadata Templates
|
|
141
|
+
|
|
142
|
+
Pre-built templates for common page types:
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
import { metadataTemplates } from "@repo/seo/server/next";
|
|
146
|
+
|
|
147
|
+
// Product page
|
|
148
|
+
export const metadata = metadataTemplates.product({
|
|
149
|
+
name: "Product Name",
|
|
150
|
+
description: "Product description",
|
|
151
|
+
price: 99.99,
|
|
152
|
+
currency: "USD",
|
|
153
|
+
image: "/product.png",
|
|
154
|
+
availability: "InStock",
|
|
155
|
+
brand: "Brand Name"
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Article/blog post
|
|
159
|
+
export const metadata = metadataTemplates.article({
|
|
160
|
+
title: "Article Title",
|
|
161
|
+
description: "Article description",
|
|
162
|
+
author: "Author Name",
|
|
163
|
+
publishedTime: new Date("2024-01-01"),
|
|
164
|
+
modifiedTime: new Date("2024-01-15"),
|
|
165
|
+
image: "/article.png",
|
|
166
|
+
tags: ["tag1", "tag2"],
|
|
167
|
+
section: "Technology"
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// User profile
|
|
171
|
+
export const metadata = metadataTemplates.profile({
|
|
172
|
+
name: "User Name",
|
|
173
|
+
bio: "User biography",
|
|
174
|
+
image: "/avatar.png",
|
|
175
|
+
username: "username",
|
|
176
|
+
firstName: "First",
|
|
177
|
+
lastName: "Last"
|
|
178
|
+
});
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### 3. Structured Data Builders
|
|
182
|
+
|
|
183
|
+
Type-safe Schema.org structured data generation:
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
import { structuredData } from "@repo/seo/server/next";
|
|
187
|
+
import { JsonLd } from "@repo/seo/client/next";
|
|
188
|
+
|
|
189
|
+
// Article schema
|
|
190
|
+
const articleSchema = structuredData.article({
|
|
191
|
+
headline: "Article Title",
|
|
192
|
+
author: { name: "Author Name", url: "https://author.com" },
|
|
193
|
+
datePublished: "2024-01-01",
|
|
194
|
+
dateModified: "2024-01-15",
|
|
195
|
+
image: ["/article.png"],
|
|
196
|
+
publisher: { name: "Publisher", logo: "/logo.png" },
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// Organization schema
|
|
200
|
+
const orgSchema = structuredData.organization({
|
|
201
|
+
name: "Company Name",
|
|
202
|
+
url: "https://company.com",
|
|
203
|
+
logo: "/logo.png",
|
|
204
|
+
description: "Company description",
|
|
205
|
+
sameAs: ["https://twitter.com/company", "https://linkedin.com/company"],
|
|
206
|
+
contactPoint: {
|
|
207
|
+
contactType: "Customer Service",
|
|
208
|
+
telephone: "+1-555-1234",
|
|
209
|
+
areaServed: "US",
|
|
210
|
+
availableLanguage: ["English"],
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// FAQ schema
|
|
215
|
+
const faqSchema = structuredData.faq([
|
|
216
|
+
{ question: "What is this?", answer: "This is the answer." },
|
|
217
|
+
{ question: "How do I use it?", answer: "Follow the instructions." },
|
|
218
|
+
]);
|
|
219
|
+
|
|
220
|
+
// Breadcrumbs
|
|
221
|
+
const breadcrumbsSchema = structuredData.breadcrumbs([
|
|
222
|
+
{ name: "Home", url: "https://example.com" },
|
|
223
|
+
{ name: "Category", url: "https://example.com/category" },
|
|
224
|
+
{ name: "Product", url: "https://example.com/category/product" },
|
|
225
|
+
]);
|
|
226
|
+
|
|
227
|
+
// Render in component
|
|
228
|
+
<JsonLd data={[articleSchema, orgSchema, breadcrumbsSchema]} />;
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### 4. Internationalization (i18n)
|
|
232
|
+
|
|
233
|
+
Multi-language SEO with hreflang support:
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
import { generateI18nSitemap } from "@repo/seo/server/next";
|
|
237
|
+
|
|
238
|
+
const routes = [
|
|
239
|
+
{ url: "https://example.com", priority: 1, changeFrequency: "daily" as const },
|
|
240
|
+
{ url: "https://example.com/about", priority: 0.8, changeFrequency: "monthly" as const }
|
|
241
|
+
];
|
|
242
|
+
|
|
243
|
+
const locales = ["en", "es", "fr", "de"];
|
|
244
|
+
const sitemap = generateI18nSitemap(routes, locales, "en");
|
|
245
|
+
|
|
246
|
+
// Generates:
|
|
247
|
+
// https://example.com
|
|
248
|
+
// https://example.com/es
|
|
249
|
+
// https://example.com/fr
|
|
250
|
+
// https://example.com/de
|
|
251
|
+
// https://example.com/about
|
|
252
|
+
// https://example.com/es/about
|
|
253
|
+
// ... etc
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### 5. Viewport Configuration
|
|
257
|
+
|
|
258
|
+
Device-specific viewport presets:
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
import { viewportPresets, generateViewport } from "@repo/seo/server/next";
|
|
262
|
+
|
|
263
|
+
// Static viewport
|
|
264
|
+
export const viewport = viewportPresets.default; // Responsive
|
|
265
|
+
export const viewport = viewportPresets.mobileOptimized; // Mobile (no zoom)
|
|
266
|
+
export const viewport = viewportPresets.tablet; // Tablet
|
|
267
|
+
export const viewport = viewportPresets.desktop; // Desktop
|
|
268
|
+
|
|
269
|
+
// Dynamic viewport based on user agent
|
|
270
|
+
export function generateViewport() {
|
|
271
|
+
const userAgent = headers().get("user-agent") || "";
|
|
272
|
+
return generateViewport(userAgent);
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### 6. Advanced Features
|
|
277
|
+
|
|
278
|
+
#### Preview Mode Metadata
|
|
279
|
+
|
|
280
|
+
```typescript
|
|
281
|
+
import { generatePreviewMetadata } from "@repo/seo/server/next";
|
|
282
|
+
|
|
283
|
+
const isDraft = true;
|
|
284
|
+
const baseMetadata = createMetadata({ title: "My Post", description: "..." });
|
|
285
|
+
|
|
286
|
+
const metadata = generatePreviewMetadata(isDraft, baseMetadata, {
|
|
287
|
+
draftIndicator: "[DRAFT]",
|
|
288
|
+
noIndexDrafts: true
|
|
289
|
+
});
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
#### Edge Runtime Support
|
|
293
|
+
|
|
294
|
+
```typescript
|
|
295
|
+
import { generateMetadataEdge } from "@repo/seo/server/next";
|
|
296
|
+
|
|
297
|
+
export const runtime = "edge";
|
|
298
|
+
|
|
299
|
+
export async function GET(request: Request) {
|
|
300
|
+
const metadata = await generateMetadataEdge(request, {
|
|
301
|
+
title: "Edge Page",
|
|
302
|
+
description: "Generated on the edge",
|
|
303
|
+
image: "/edge-og.png"
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
return NextResponse.json(metadata);
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
#### Streaming JSON-LD
|
|
311
|
+
|
|
312
|
+
```typescript
|
|
313
|
+
import { StreamingJsonLd } from "@repo/seo/client/next";
|
|
314
|
+
|
|
315
|
+
export default function Page() {
|
|
316
|
+
const dataPromise = fetch("/api/product-schema").then((r) => r.json());
|
|
317
|
+
|
|
318
|
+
return (
|
|
319
|
+
<StreamingJsonLd
|
|
320
|
+
dataPromise={dataPromise}
|
|
321
|
+
fallback={{ "@context": "https://schema.org", "@type": "Product" }}
|
|
322
|
+
/>
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
## Environment Configuration
|
|
328
|
+
|
|
329
|
+
The package uses `@t3-oss/env-core` for type-safe environment validation:
|
|
330
|
+
|
|
331
|
+
```bash
|
|
332
|
+
# Required environment variables (at least one)
|
|
333
|
+
NEXT_PUBLIC_URL=https://example.com
|
|
334
|
+
SITE_URL=https://example.com
|
|
335
|
+
VERCEL_PROJECT_PRODUCTION_URL=example.com
|
|
336
|
+
|
|
337
|
+
# Optional
|
|
338
|
+
NODE_ENV=production
|
|
339
|
+
VERCEL_ENV=production
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
```typescript
|
|
343
|
+
import { env, getProtocol, getBaseUrl, isProduction } from "@repo/seo/env";
|
|
344
|
+
|
|
345
|
+
// Access validated environment variables
|
|
346
|
+
const protocol = getProtocol(); // 'https' in production
|
|
347
|
+
const baseUrl = getBaseUrl(); // Falls back to env vars
|
|
348
|
+
const isProd = isProduction(); // true if NODE_ENV === 'production'
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
## Validation Utilities
|
|
352
|
+
|
|
353
|
+
```typescript
|
|
354
|
+
import {
|
|
355
|
+
validateMetadata,
|
|
356
|
+
validateOpenGraph,
|
|
357
|
+
validateSchemaOrg,
|
|
358
|
+
validateUrls,
|
|
359
|
+
validateImages
|
|
360
|
+
} from "@repo/seo/validation";
|
|
361
|
+
|
|
362
|
+
// Validate metadata has required fields
|
|
363
|
+
const missing = validateMetadata({ title: "My Page" }); // ['description']
|
|
364
|
+
|
|
365
|
+
// Validate Open Graph data
|
|
366
|
+
const isValid = validateOpenGraph({ title: "Title", description: "Desc" }); // true
|
|
367
|
+
|
|
368
|
+
// Validate Schema.org structured data
|
|
369
|
+
const isValidSchema = validateSchemaOrg({
|
|
370
|
+
"@context": "https://schema.org",
|
|
371
|
+
"@type": "Article"
|
|
372
|
+
}); // true
|
|
373
|
+
|
|
374
|
+
// Validate URLs
|
|
375
|
+
const validUrls = validateUrls(["https://example.com", "invalid"]); // false
|
|
376
|
+
|
|
377
|
+
// Validate image URLs
|
|
378
|
+
const validImages = validateImages(["https://example.com/image.jpg", "https://example.com/logo.png"]); // true
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
## Security Considerations
|
|
382
|
+
|
|
383
|
+
### XSS Prevention
|
|
384
|
+
|
|
385
|
+
The package includes built-in XSS protection:
|
|
386
|
+
|
|
387
|
+
```typescript
|
|
388
|
+
// ✅ SAFE: JSON.stringify escapes content
|
|
389
|
+
<JsonLd data={structuredData.article({ headline: userInput })} />
|
|
390
|
+
|
|
391
|
+
// ✅ SAFE: useOpenGraphPreview escapes HTML
|
|
392
|
+
const { generatePreviewHtml } = useOpenGraphPreview({
|
|
393
|
+
title: userInput, // Automatically escaped
|
|
394
|
+
description: userInput,
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
// ✅ SAFE: URL validation prevents malicious URLs
|
|
398
|
+
createMetadata({
|
|
399
|
+
image: userProvidedUrl, // Validated before use
|
|
400
|
+
});
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
### Environment Variables
|
|
404
|
+
|
|
405
|
+
- Never expose server-side environment variables to the client
|
|
406
|
+
- Use `NEXT_PUBLIC_` prefix only for public URLs
|
|
407
|
+
- Environment validation runs on package import
|
|
408
|
+
|
|
409
|
+
## Best Practices
|
|
410
|
+
|
|
411
|
+
### 1. Use SEOManager for Consistency
|
|
412
|
+
|
|
413
|
+
```typescript
|
|
414
|
+
// ✅ Recommended: Centralized configuration
|
|
415
|
+
const seoManager = new SEOManager({ ...config });
|
|
416
|
+
export const metadata = seoManager.createMetadata({ ...pageData });
|
|
417
|
+
|
|
418
|
+
// ❌ Avoid: Inconsistent metadata across pages
|
|
419
|
+
export const metadata = createMetadata({ ...everythingEachTime });
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
### 2. Combine Metadata with Structured Data
|
|
423
|
+
|
|
424
|
+
```typescript
|
|
425
|
+
export const metadata = metadataTemplates.article({ ...articleData });
|
|
426
|
+
|
|
427
|
+
export default function ArticlePage() {
|
|
428
|
+
const schema = structuredData.article({ ...articleData });
|
|
429
|
+
return <><JsonLd data={schema} />{/* content */}</>;
|
|
430
|
+
}
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
### 3. Validate URLs Before Use
|
|
434
|
+
|
|
435
|
+
```typescript
|
|
436
|
+
import { validateUrls } from "@repo/seo/utils/validation";
|
|
437
|
+
|
|
438
|
+
const imageUrl = getUserUploadedImage();
|
|
439
|
+
if (!validateUrls([imageUrl])) {
|
|
440
|
+
// Use fallback image
|
|
441
|
+
imageUrl = "/default-og-image.png";
|
|
442
|
+
}
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
### 4. Use TypeScript for Type Safety
|
|
446
|
+
|
|
447
|
+
```typescript
|
|
448
|
+
import type { Metadata, SEOConfig } from "@repo/seo/server/next";
|
|
449
|
+
|
|
450
|
+
const config: SEOConfig = {
|
|
451
|
+
applicationName: "My App",
|
|
452
|
+
author: { name: "Name", url: "https://example.com" },
|
|
453
|
+
publisher: "Publisher",
|
|
454
|
+
twitterHandle: "@handle"
|
|
455
|
+
};
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
## Testing
|
|
459
|
+
|
|
460
|
+
```bash
|
|
461
|
+
pnpm test # Run all tests
|
|
462
|
+
pnpm test:watch # Watch mode
|
|
463
|
+
pnpm test:coverage # Coverage report
|
|
464
|
+
pnpm test:ui # Interactive UI
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
## API Reference
|
|
468
|
+
|
|
469
|
+
### Server Utilities (Next.js)
|
|
470
|
+
|
|
471
|
+
- `createMetadata(options)` - Simple metadata generation
|
|
472
|
+
- `SEOManager` - Centralized SEO configuration
|
|
473
|
+
- `metadataTemplates` - Pre-built templates (product, article, profile)
|
|
474
|
+
- `structuredData` - Schema.org builders (article, product, organization, FAQ, breadcrumbs, website)
|
|
475
|
+
- `generateSitemapObject(routes)` - Dynamic sitemap generation
|
|
476
|
+
- `generateI18nSitemap(routes, locales)` - Multi-language sitemaps
|
|
477
|
+
- `viewportPresets` - Device-specific viewport configs
|
|
478
|
+
- `generateViewport(userAgent)` - Dynamic viewport selection
|
|
479
|
+
- `generatePreviewMetadata(isDraft, metadata)` - Draft/preview handling
|
|
480
|
+
- `generateMetadataEdge(request, options)` - Edge runtime support
|
|
481
|
+
- `createSitemapConfig(config)` - next-sitemap integration
|
|
482
|
+
|
|
483
|
+
### Client Components
|
|
484
|
+
|
|
485
|
+
- `<JsonLd data={...} />` - Render JSON-LD structured data
|
|
486
|
+
- `<OptimizedJsonLd data={...} strategy="..." />` - Performance-optimized JSON-LD
|
|
487
|
+
- `<StreamingJsonLd dataPromise={...} />` - Async data loading
|
|
488
|
+
- `useOpenGraphPreview(metadata)` - Dynamic OG preview generation
|
|
489
|
+
|
|
490
|
+
### Validation
|
|
491
|
+
|
|
492
|
+
- `validateMetadata(metadata)` - Check required fields
|
|
493
|
+
- `validateOpenGraph(og)` - Validate OG data
|
|
494
|
+
- `validateSchemaOrg(schema)` - Validate structured data
|
|
495
|
+
- `validateUrls(urls)` - URL format validation
|
|
496
|
+
- `validateImages(images)` - Image URL validation
|
|
497
|
+
|
|
498
|
+
## Migration Guide
|
|
499
|
+
|
|
500
|
+
### From next-seo
|
|
501
|
+
|
|
502
|
+
```typescript
|
|
503
|
+
// Before (next-seo)
|
|
504
|
+
import { NextSeo } from 'next-seo';
|
|
505
|
+
|
|
506
|
+
export default function Page() {
|
|
507
|
+
return <NextSeo title="..." description="..." openGraph={{ ... }} />;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// After (@repo/seo)
|
|
511
|
+
import { createMetadata } from '@repo/seo/server/next';
|
|
512
|
+
|
|
513
|
+
export const metadata = createMetadata({
|
|
514
|
+
title: "...",
|
|
515
|
+
description: "...",
|
|
516
|
+
image: "...",
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
export default function Page() {
|
|
520
|
+
return <>{/* content */}</>;
|
|
521
|
+
}
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
### From Manual Metadata
|
|
525
|
+
|
|
526
|
+
```typescript
|
|
527
|
+
// Before
|
|
528
|
+
export const metadata = {
|
|
529
|
+
title: "...",
|
|
530
|
+
description: "...",
|
|
531
|
+
openGraph: {
|
|
532
|
+
/* manual configuration */
|
|
533
|
+
},
|
|
534
|
+
twitter: {
|
|
535
|
+
/* manual configuration */
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
// After
|
|
540
|
+
import { SEOManager } from "@repo/seo/server/next";
|
|
541
|
+
|
|
542
|
+
const seo = new SEOManager({ ...appConfig });
|
|
543
|
+
export const metadata = seo.createMetadata({ title: "...", description: "..." });
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
## Troubleshooting
|
|
547
|
+
|
|
548
|
+
### Metadata not appearing
|
|
549
|
+
|
|
550
|
+
1. Check that `metadataBase` is set correctly
|
|
551
|
+
2. Verify environment variables are configured
|
|
552
|
+
3. Ensure metadata is exported from page/layout
|
|
553
|
+
|
|
554
|
+
### Structured data not validating
|
|
555
|
+
|
|
556
|
+
1. Use Google's Rich Results Test
|
|
557
|
+
2. Validate against Schema.org schemas
|
|
558
|
+
3. Check console for validation errors
|
|
559
|
+
|
|
560
|
+
### Type errors
|
|
561
|
+
|
|
562
|
+
1. Update `@types/react` to latest version
|
|
563
|
+
2. Ensure `next` version is 15.0.0+
|
|
564
|
+
3. Check TypeScript is 5.0+
|
|
565
|
+
|
|
566
|
+
## Related Packages
|
|
567
|
+
|
|
568
|
+
- `@repo/utils` - Shared utilities (cn, formatters, validators)
|
|
569
|
+
- `@repo/types` - TypeScript type utilities
|
|
570
|
+
|
|
571
|
+
## Contributing
|
|
572
|
+
|
|
573
|
+
See the main monorepo [CONTRIBUTING.md](../../../CONTRIBUTING.md) for guidelines.
|
|
574
|
+
|
|
575
|
+
## License
|
|
576
|
+
|
|
577
|
+
MIT - See [LICENSE](../../../LICENSE) for details.
|
|
578
|
+
|
|
579
|
+
## 📚 Comprehensive Documentation
|
|
580
|
+
|
|
581
|
+
For detailed documentation, see:
|
|
582
|
+
|
|
583
|
+
- **[Audit Reports](../../apps/docs/content/docs/audits/seo/)** - Comprehensive audits, fixes, and security reviews
|
|
584
|
+
- **[Technical Guides](../../apps/docs/content/docs/packages/seo/)** - Implementation guides and best practices
|
|
585
|
+
|
|
586
|
+
All comprehensive documentation has been centralized in the docs app.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Thing, Thing as Thing$1, WithContext, WithContext as WithContext$1 } from "schema-dts";
|
|
2
|
+
import * as react_jsx_runtime0 from "react/jsx-runtime";
|
|
3
|
+
export * from "schema-dts";
|
|
4
|
+
|
|
5
|
+
//#region src/client-next.d.ts
|
|
6
|
+
interface JsonLdProps {
|
|
7
|
+
data: WithContext$1<Thing$1> | WithContext$1<Thing$1>[];
|
|
8
|
+
id?: string;
|
|
9
|
+
strategy?: 'afterInteractive' | 'lazyOnload' | 'beforeInteractive' | 'worker';
|
|
10
|
+
}
|
|
11
|
+
declare function JsonLd({
|
|
12
|
+
data,
|
|
13
|
+
id
|
|
14
|
+
}: JsonLdProps): react_jsx_runtime0.JSX.Element;
|
|
15
|
+
declare function OptimizedJsonLd({
|
|
16
|
+
data,
|
|
17
|
+
id
|
|
18
|
+
}: JsonLdProps): react_jsx_runtime0.JSX.Element;
|
|
19
|
+
interface StreamingJsonLdProps {
|
|
20
|
+
dataPromise: Promise<WithContext$1<Thing$1> | WithContext$1<Thing$1>[]>;
|
|
21
|
+
id?: string;
|
|
22
|
+
fallback?: WithContext$1<Thing$1>;
|
|
23
|
+
}
|
|
24
|
+
declare function StreamingJsonLd({
|
|
25
|
+
dataPromise,
|
|
26
|
+
id,
|
|
27
|
+
fallback
|
|
28
|
+
}: StreamingJsonLdProps): react_jsx_runtime0.JSX.Element;
|
|
29
|
+
declare function useOpenGraphPreview(metadata: {
|
|
30
|
+
title: string;
|
|
31
|
+
description: string;
|
|
32
|
+
image?: string;
|
|
33
|
+
url?: string;
|
|
34
|
+
}): {
|
|
35
|
+
preview: {
|
|
36
|
+
title: string;
|
|
37
|
+
description: string;
|
|
38
|
+
image?: string;
|
|
39
|
+
url?: string;
|
|
40
|
+
};
|
|
41
|
+
updatePreview: (updates: Partial<typeof metadata>) => void;
|
|
42
|
+
generatePreviewHtml: string;
|
|
43
|
+
};
|
|
44
|
+
//#endregion
|
|
45
|
+
export { JsonLd, OptimizedJsonLd, StreamingJsonLd, type Thing, type WithContext, useOpenGraphPreview };
|
|
46
|
+
//# sourceMappingURL=client-next.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client-next.d.mts","names":[],"sources":["../src/client-next.tsx"],"mappings":";;;;;UAeU,WAAA;EACR,IAAA,EAAM,aAAA,CAAY,OAAA,IAAS,aAAA,CAAY,OAAA;EACvC,EAAA;EACA,QAAA;AAAA;AAAA,iBAIc,MAAA,CAAA;EAAS,IAAA;EAAM;AAAA,GAAM,WAAA,GAAW,kBAAA,CAAA,GAAA,CAAA,OAAA;AAAA,iBAoBhC,eAAA,CAAA;EAAkB,IAAA;EAAM;AAAA,GAAM,WAAA,GAAW,kBAAA,CAAA,GAAA,CAAA,OAAA;AAAA,UAqB/C,oBAAA;EACR,WAAA,EAAa,OAAA,CAAQ,aAAA,CAAY,OAAA,IAAS,aAAA,CAAY,OAAA;EACtD,EAAA;EACA,QAAA,GAAW,aAAA,CAAY,OAAA;AAAA;AAAA,iBAGT,eAAA,CAAA;EAAkB,WAAA;EAAa,EAAA;EAAI;AAAA,GAAY,oBAAA,GAAoB,kBAAA,CAAA,GAAA,CAAA,OAAA;AAAA,iBAkCnE,mBAAA,CAAoB,QAAA;EAClC,KAAA;EACA,WAAA;EACA,KAAA;EACA,GAAA;AAAA;;;;;;;2BAIkD,OAAA,QAAe,QAAA"}
|