@raftlabs/raftstack 1.0.0

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.
@@ -0,0 +1,446 @@
1
+ ---
2
+ name: seo
3
+ description: Use when creating web pages, adding metadata, optimizing Core Web Vitals, implementing structured data, or when pages aren't ranking or showing rich snippets in Google
4
+ ---
5
+
6
+ # SEO Optimization
7
+
8
+ ## Overview
9
+
10
+ SEO requires three pillars: technical performance (Core Web Vitals), proper metadata, and structured data for rich snippets. All three matter for ranking.
11
+
12
+ ## When to Use
13
+
14
+ - Creating any public-facing page
15
+ - Adding metadata to Next.js pages
16
+ - Implementing structured data (JSON-LD)
17
+ - Optimizing page load performance
18
+ - Debugging missing rich snippets in Google
19
+
20
+ ## The Iron Rules
21
+
22
+ ### 1. Core Web Vitals Are Ranking Factors
23
+
24
+ | Metric | Target | What It Measures | Key Optimization |
25
+ |--------|--------|------------------|------------------|
26
+ | **LCP** (Largest Contentful Paint) | < 2.5s | Main content load time | `priority` on hero images |
27
+ | **INP** (Interaction to Next Paint) | < 200ms | Responsiveness | scheduler.yield() for long tasks |
28
+ | **CLS** (Cumulative Layout Shift) | < 0.1 | Visual stability | Always set width/height |
29
+
30
+ #### LCP Optimization with Next.js Image
31
+
32
+ ```typescript
33
+ // ❌ BAD: Unoptimized image kills LCP
34
+ <img src="/hero.jpg" alt="Hero" />
35
+
36
+ // ✅ GOOD: Next.js Image with priority for LCP
37
+ import Image from 'next/image';
38
+
39
+ <Image
40
+ src="/hero.jpg"
41
+ alt="Hero"
42
+ width={1200}
43
+ height={630}
44
+ priority // Preloads, disables lazy loading
45
+ placeholder="blur" // Shows blur during load
46
+ blurDataURL="data:image/..." // Optional blur data
47
+ sizes="(max-width: 768px) 100vw, 50vw"
48
+ />
49
+
50
+ // ✅ GOOD: Static import auto-generates blur
51
+ import heroImage from './hero.jpg';
52
+
53
+ <Image
54
+ src={heroImage}
55
+ alt="Hero"
56
+ priority
57
+ placeholder="blur" // Blur data automatically provided
58
+ />
59
+ ```
60
+
61
+ #### INP Optimization (< 200ms)
62
+
63
+ INP has three phases to optimize:
64
+
65
+ **1. Input Delay** - Time until event handler starts
66
+ **2. Processing Time** - Event handler execution
67
+ **3. Presentation Delay** - Time to next frame
68
+
69
+ ```typescript
70
+ // ❌ BAD: Long synchronous task blocks interactions
71
+ button.addEventListener('click', () => {
72
+ // Heavy computation blocks UI for 500ms
73
+ const result = expensiveCalculation();
74
+ updateUI(result);
75
+ });
76
+
77
+ // ✅ GOOD: Break up with scheduler.yield()
78
+ button.addEventListener('click', async () => {
79
+ const data = await fetchData();
80
+
81
+ // Yield to allow interactions to process
82
+ await scheduler.yield();
83
+
84
+ processData(data);
85
+
86
+ await scheduler.yield();
87
+
88
+ updateUI();
89
+ });
90
+
91
+ // ✅ GOOD: Debounce rapid interactions
92
+ const debouncedSearch = debounce((query) => {
93
+ performSearch(query);
94
+ }, 300);
95
+
96
+ input.addEventListener('input', (e) => {
97
+ debouncedSearch(e.target.value);
98
+ });
99
+ ```
100
+
101
+ **Key INP strategies:**
102
+ - Use `scheduler.yield()` in long tasks (> 50ms)
103
+ - Debounce rapid user inputs
104
+ - Lazy load below-fold interactivity
105
+ - Avoid large DOM updates on interaction
106
+
107
+ #### CLS Optimization
108
+
109
+ ```typescript
110
+ // ❌ BAD: CLS - no dimensions on dynamic content
111
+ <div className="product-list">
112
+ {products.map(p => <ProductCard key={p.id} product={p} />)}
113
+ </div>
114
+
115
+ // ✅ GOOD: Reserve space with aspect-ratio or fixed height
116
+ <div className="product-list min-h-[400px]">
117
+ {products.map(p => <ProductCard key={p.id} product={p} />)}
118
+ </div>
119
+
120
+ // ✅ GOOD: Always set dimensions on images
121
+ <Image
122
+ src="/product.jpg"
123
+ alt="Product"
124
+ width={400} // Prevents CLS
125
+ height={300}
126
+ />
127
+ ```
128
+
129
+ ### 2. Use Next.js Metadata API Correctly
130
+
131
+ ```typescript
132
+ // app/products/[slug]/page.tsx
133
+ import { Metadata } from 'next';
134
+
135
+ export async function generateMetadata({ params }): Promise<Metadata> {
136
+ const product = await getProduct(params.slug);
137
+
138
+ return {
139
+ title: `${product.name} | Brand - $${product.price}`,
140
+ description: truncate(product.description, 155), // Max 155 chars
141
+
142
+ alternates: {
143
+ canonical: `https://site.com/products/${product.slug}`,
144
+ },
145
+
146
+ // OpenGraph - use 'product' for e-commerce
147
+ openGraph: {
148
+ type: 'product', // NOT 'website' for products
149
+ title: product.name,
150
+ description: truncate(product.description, 155),
151
+ images: [{
152
+ url: product.image,
153
+ width: 1200,
154
+ height: 630,
155
+ alt: product.name,
156
+ }],
157
+ },
158
+
159
+ twitter: {
160
+ card: 'summary_large_image',
161
+ title: product.name,
162
+ description: truncate(product.description, 155),
163
+ images: [product.image],
164
+ },
165
+
166
+ robots: {
167
+ index: true,
168
+ follow: true,
169
+ },
170
+ };
171
+ }
172
+ ```
173
+
174
+ ### 3. Product Structured Data for Rich Snippets
175
+
176
+ Google shows price, availability, and reviews in search results. You MUST include:
177
+
178
+ ```typescript
179
+ // Minimum required fields for Product rich snippet
180
+ const productJsonLd = {
181
+ '@context': 'https://schema.org',
182
+ '@type': 'Product',
183
+ name: product.name,
184
+ image: product.images,
185
+ description: product.description,
186
+ sku: product.sku,
187
+ brand: {
188
+ '@type': 'Brand',
189
+ name: product.brand,
190
+ },
191
+ offers: {
192
+ '@type': 'Offer',
193
+ url: `https://site.com/products/${product.slug}`,
194
+ price: product.price,
195
+ priceCurrency: 'USD',
196
+ availability: 'https://schema.org/InStock', // or OutOfStock
197
+ priceValidUntil: '2025-12-31', // Required for validity
198
+ },
199
+ // Optional but highly recommended:
200
+ aggregateRating: product.rating ? {
201
+ '@type': 'AggregateRating',
202
+ ratingValue: product.rating.value,
203
+ reviewCount: product.rating.count,
204
+ } : undefined,
205
+ };
206
+ ```
207
+
208
+ ### 4. JSON-LD Rendering Pattern
209
+
210
+ Use the Next.js recommended pattern for safe JSON-LD rendering:
211
+
212
+ ```typescript
213
+ // NOTE: This pattern is safe because JSON.stringify escapes content
214
+ // and the replace() prevents script tag injection
215
+ function JsonLd({ data }: { data: object }) {
216
+ const safeJson = JSON.stringify(data).replace(/</g, '\\u003c');
217
+ return (
218
+ <script
219
+ type="application/ld+json"
220
+ // eslint-disable-next-line react/no-danger
221
+ dangerouslySetInnerHTML={{ __html: safeJson }}
222
+ />
223
+ );
224
+ }
225
+ ```
226
+
227
+ ### 5. Essential Structured Data Types
228
+
229
+ | Page Type | Required Schema | Rich Result |
230
+ |-----------|-----------------|-------------|
231
+ | Product | Product + Offer | Price, availability in search |
232
+ | Article | Article + Author | Article snippet |
233
+ | FAQ | FAQPage | Expandable Q&A in search |
234
+ | Recipe | Recipe | Recipe card with image |
235
+ | Local Business | LocalBusiness | Knowledge panel |
236
+ | Breadcrumbs | BreadcrumbList | Breadcrumb trail in results |
237
+
238
+ #### Article Schema
239
+
240
+ ```typescript
241
+ const articleJsonLd = {
242
+ '@context': 'https://schema.org',
243
+ '@type': 'Article',
244
+ headline: post.title,
245
+ image: post.coverImage,
246
+ datePublished: post.publishedAt,
247
+ dateModified: post.updatedAt,
248
+ author: {
249
+ '@type': 'Person',
250
+ name: post.author.name,
251
+ url: `https://site.com/authors/${post.author.slug}`,
252
+ },
253
+ publisher: {
254
+ '@type': 'Organization',
255
+ name: 'Brand Name',
256
+ logo: {
257
+ '@type': 'ImageObject',
258
+ url: 'https://site.com/logo.png',
259
+ },
260
+ },
261
+ };
262
+ ```
263
+
264
+ #### FAQPage Schema
265
+
266
+ ```typescript
267
+ const faqJsonLd = {
268
+ '@context': 'https://schema.org',
269
+ '@type': 'FAQPage',
270
+ mainEntity: faqs.map((faq) => ({
271
+ '@type': 'Question',
272
+ name: faq.question,
273
+ acceptedAnswer: {
274
+ '@type': 'Answer',
275
+ text: faq.answer,
276
+ },
277
+ })),
278
+ };
279
+ ```
280
+
281
+ ## Sitemap & Robots
282
+
283
+ ### Dynamic Sitemap Generation
284
+
285
+ ```typescript
286
+ // app/sitemap.ts
287
+ import { MetadataRoute } from 'next';
288
+
289
+ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
290
+ const products = await db.product.findMany({
291
+ select: { slug: true, updatedAt: true },
292
+ });
293
+
294
+ const productUrls = products.map((product) => ({
295
+ url: `https://site.com/products/${product.slug}`,
296
+ lastModified: product.updatedAt,
297
+ changeFrequency: 'weekly' as const,
298
+ priority: 0.8,
299
+ }));
300
+
301
+ return [
302
+ {
303
+ url: 'https://site.com',
304
+ lastModified: new Date(),
305
+ changeFrequency: 'daily',
306
+ priority: 1,
307
+ },
308
+ ...productUrls,
309
+ ];
310
+ }
311
+ ```
312
+
313
+ ### Robots.txt
314
+
315
+ ```typescript
316
+ // app/robots.ts
317
+ import { MetadataRoute } from 'next';
318
+
319
+ export default function robots(): MetadataRoute.Robots {
320
+ return {
321
+ rules: [
322
+ {
323
+ userAgent: '*',
324
+ allow: '/',
325
+ disallow: ['/admin/', '/api/'],
326
+ },
327
+ {
328
+ userAgent: 'Googlebot',
329
+ allow: '/',
330
+ crawlDelay: 0,
331
+ },
332
+ ],
333
+ sitemap: 'https://site.com/sitemap.xml',
334
+ };
335
+ }
336
+ ```
337
+
338
+ ## Quick Reference: Metadata Checklist
339
+
340
+ Every page needs:
341
+ - [ ] `title` - Unique, < 60 chars, includes primary keyword
342
+ - [ ] `description` - Compelling, < 155 chars, includes CTA
343
+ - [ ] `canonical` URL - Prevents duplicate content
344
+ - [ ] `openGraph` - For social sharing (1200x630 images)
345
+ - [ ] `twitter` card - For Twitter/X sharing
346
+ - [ ] `robots` - index/noindex directive
347
+
348
+ For e-commerce:
349
+ - [ ] OpenGraph `type: 'product'` (not 'website')
350
+ - [ ] Product JSON-LD with offers
351
+ - [ ] Breadcrumb JSON-LD
352
+
353
+ ## Testing & Measurement
354
+
355
+ ### Validation Tools (Before Deploy)
356
+
357
+ Test structured data BEFORE deploying:
358
+ - [Rich Results Test](https://search.google.com/test/rich-results) - Google's official tool
359
+ - [Schema Markup Validator](https://validator.schema.org/) - schema.org validator
360
+ - Chrome DevTools → Lighthouse → SEO audit
361
+
362
+ ### Performance Measurement
363
+
364
+ | Tool | What It Measures | Use For |
365
+ |------|------------------|---------|
366
+ | **PageSpeed Insights** | Field data (28 days) | Official Core Web Vitals scores |
367
+ | **Chrome User Experience Report (CrUX)** | Real user data | P75 scores for ranking |
368
+ | **Lighthouse (DevTools)** | Lab data (simulated) | Local testing, not for ranking |
369
+ | **Search Console** | Core Web Vitals report | Per-URL performance in field |
370
+
371
+ **Critical:** Only **Field Data** (real users) affects Google rankings. Lab data helps debug but doesn't count for SEO.
372
+
373
+ ### Core Web Vitals Testing Strategy
374
+
375
+ ```typescript
376
+ // Measure INP in production
377
+ new PerformanceObserver((list) => {
378
+ for (const entry of list.getEntries()) {
379
+ if (entry.entryType === 'event') {
380
+ const inp = entry.processingStart - entry.startTime;
381
+ if (inp > 200) {
382
+ console.warn('Slow INP:', {
383
+ duration: inp,
384
+ name: entry.name,
385
+ target: entry.target,
386
+ });
387
+ }
388
+ }
389
+ }
390
+ }).observe({ type: 'event', buffered: true });
391
+
392
+ // Log slow LCP
393
+ new PerformanceObserver((list) => {
394
+ const entries = list.getEntries();
395
+ const lcp = entries[entries.length - 1];
396
+ if (lcp.renderTime > 2500) {
397
+ console.warn('Slow LCP:', {
398
+ duration: lcp.renderTime,
399
+ element: lcp.element,
400
+ url: lcp.url,
401
+ });
402
+ }
403
+ }).observe({ type: 'largest-contentful-paint', buffered: true });
404
+ ```
405
+
406
+ ## References
407
+
408
+ - [Core Web Vitals INP Guide](https://web.dev/articles/inp) - Official INP optimization patterns
409
+ - [Optimize INP](https://web.dev/articles/optimize-inp) - Three-phase optimization approach
410
+ - [Next.js Image Component](https://nextjs.org/docs/app/api-reference/components/image) - priority, placeholder, sizes
411
+ - [Next.js Metadata API](https://nextjs.org/docs/app/api-reference/functions/generate-metadata) - generateMetadata patterns
412
+ - [Schema.org](https://schema.org/) - Structured data vocabulary
413
+
414
+ **Version Notes:**
415
+ - INP replaced FID as Core Web Vital (March 2024)
416
+ - Next.js 15: Enhanced Image component with automatic blur
417
+ - Good INP: < 200ms (improving from 500ms → 200ms = 22% engagement boost)
418
+
419
+ ## Red Flags - STOP and Fix
420
+
421
+ | Thought | Reality |
422
+ |---------|---------|
423
+ | "SEO is just meta tags" | Core Web Vitals are ranking factors. Optimize performance. |
424
+ | "I'll add structured data later" | No rich snippets = lower CTR. Add from day one. |
425
+ | "LCP doesn't matter for this page" | Every page's performance affects site-wide ranking. |
426
+ | "Using img tag is fine" | Next.js Image handles optimization. Always use it. |
427
+ | "OpenGraph type='website' is fine" | Use 'product' for products, 'article' for articles. |
428
+ | "Lab data (Lighthouse) is good enough" | Only field data counts for ranking. Test with real users. |
429
+ | "INP is too complex to optimize" | Use scheduler.yield() and debouncing. Start simple. |
430
+ | "I don't need a sitemap for small sites" | Sitemaps help discovery. Generate dynamically. |
431
+
432
+ ## Common Mistakes
433
+
434
+ | Mistake | Fix |
435
+ |---------|-----|
436
+ | HTML img instead of Next.js Image | Use `next/image` with priority for LCP |
437
+ | Missing `width`/`height` on images | Always specify to prevent CLS |
438
+ | Description > 160 chars | Truncate to 155 with ellipsis |
439
+ | No canonical URL | Add `alternates.canonical` |
440
+ | Missing `priceValidUntil` in Offer | Required for Product rich snippets |
441
+ | OpenGraph type='website' for products | Use type='product' |
442
+ | No structured data validation | Test with Rich Results Test before deploy |
443
+ | Long tasks without scheduler.yield() | Break up tasks > 50ms to improve INP |
444
+ | Testing only with Lighthouse | Use PageSpeed Insights for field data |
445
+ | No placeholder on LCP images | Add `placeholder="blur"` for perceived performance |
446
+ | Dynamic sitemap with hardcoded URLs | Fetch from database for automatic updates |
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Raftlabs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.