@raftlabs/raftstack 1.7.0 → 1.7.2
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/.claude/skills/backend/SKILL.md +132 -1
- package/.claude/skills/code-quality/SKILL.md +53 -38
- package/.claude/skills/database/SKILL.md +55 -6
- package/.claude/skills/react/SKILL.md +112 -2
- package/.claude/skills/seo/SKILL.md +117 -5
- package/dist/cli.js +11 -2
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: backend
|
|
3
|
-
description: Use when writing
|
|
3
|
+
description: Use when writing Lambda functions, API routes, Hono handlers, Express routes, serverless endpoints, or backend services. Use when creating API validation with Zod, implementing service layers, or structuring handler code.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Backend Development
|
|
@@ -578,6 +578,135 @@ export const handler = compose(
|
|
|
578
578
|
)(baseHandler);
|
|
579
579
|
```
|
|
580
580
|
|
|
581
|
+
## Hono.js Patterns
|
|
582
|
+
|
|
583
|
+
Hono is a fast, lightweight framework for serverless and edge. Same patterns apply - layer separation, DI, Zod validation.
|
|
584
|
+
|
|
585
|
+
### Basic Hono Handler with Zod
|
|
586
|
+
|
|
587
|
+
```typescript
|
|
588
|
+
import { Hono } from 'hono';
|
|
589
|
+
import { zValidator } from '@hono/zod-validator';
|
|
590
|
+
import { z } from 'zod';
|
|
591
|
+
|
|
592
|
+
const app = new Hono();
|
|
593
|
+
|
|
594
|
+
const CreateUserSchema = z.object({
|
|
595
|
+
email: z.string().email(),
|
|
596
|
+
name: z.string().min(1),
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
// ✅ GOOD: Validation middleware + typed body
|
|
600
|
+
app.post(
|
|
601
|
+
'/users',
|
|
602
|
+
zValidator('json', CreateUserSchema),
|
|
603
|
+
async (c) => {
|
|
604
|
+
const body = c.req.valid('json'); // Typed as { email: string; name: string }
|
|
605
|
+
const user = await userService.createUser(body);
|
|
606
|
+
return c.json(user, 201);
|
|
607
|
+
}
|
|
608
|
+
);
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
### Hono with Dependency Injection
|
|
612
|
+
|
|
613
|
+
```typescript
|
|
614
|
+
// ✅ GOOD: Factory pattern for testable Hono apps
|
|
615
|
+
import { Hono } from 'hono';
|
|
616
|
+
|
|
617
|
+
export function createApp(deps: {
|
|
618
|
+
userService: UserService;
|
|
619
|
+
authService: AuthService;
|
|
620
|
+
}) {
|
|
621
|
+
const app = new Hono();
|
|
622
|
+
|
|
623
|
+
app.post('/users', async (c) => {
|
|
624
|
+
const body = await c.req.json();
|
|
625
|
+
const user = await deps.userService.createUser(body);
|
|
626
|
+
return c.json(user, 201);
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
app.post('/login', async (c) => {
|
|
630
|
+
const { email, password } = await c.req.json();
|
|
631
|
+
const token = await deps.authService.login(email, password);
|
|
632
|
+
return c.json({ token });
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
return app;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// Production: inject real services
|
|
639
|
+
const app = createApp({
|
|
640
|
+
userService: createUserService(db),
|
|
641
|
+
authService: createAuthService(db),
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
export default app;
|
|
645
|
+
|
|
646
|
+
// Test: inject mocks
|
|
647
|
+
const testApp = createApp({
|
|
648
|
+
userService: { createUser: vi.fn() },
|
|
649
|
+
authService: { login: vi.fn() },
|
|
650
|
+
});
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
### Hono Error Handling Middleware
|
|
654
|
+
|
|
655
|
+
```typescript
|
|
656
|
+
import { Hono } from 'hono';
|
|
657
|
+
import { HTTPException } from 'hono/http-exception';
|
|
658
|
+
|
|
659
|
+
const app = new Hono();
|
|
660
|
+
|
|
661
|
+
// Global error handler
|
|
662
|
+
app.onError((err, c) => {
|
|
663
|
+
if (err instanceof AppError) {
|
|
664
|
+
return c.json(
|
|
665
|
+
{ error: err.message, code: err.code },
|
|
666
|
+
err.statusCode
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
if (err instanceof HTTPException) {
|
|
671
|
+
return c.json({ error: err.message }, err.status);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
console.error('Unexpected error:', err);
|
|
675
|
+
return c.json({ error: 'Internal server error' }, 500);
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
// Route handlers throw AppError
|
|
679
|
+
app.get('/users/:id', async (c) => {
|
|
680
|
+
const user = await userService.getUser(c.req.param('id'));
|
|
681
|
+
if (!user) {
|
|
682
|
+
throw new NotFoundError('User not found');
|
|
683
|
+
}
|
|
684
|
+
return c.json(user);
|
|
685
|
+
});
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
### Hono Middleware Composition
|
|
689
|
+
|
|
690
|
+
```typescript
|
|
691
|
+
import { Hono } from 'hono';
|
|
692
|
+
import { cors } from 'hono/cors';
|
|
693
|
+
import { logger } from 'hono/logger';
|
|
694
|
+
import { jwt } from 'hono/jwt';
|
|
695
|
+
|
|
696
|
+
const app = new Hono();
|
|
697
|
+
|
|
698
|
+
// Apply middleware in order
|
|
699
|
+
app.use('*', logger());
|
|
700
|
+
app.use('*', cors());
|
|
701
|
+
app.use('/api/*', jwt({ secret: process.env.JWT_SECRET }));
|
|
702
|
+
|
|
703
|
+
// Protected routes
|
|
704
|
+
app.get('/api/me', (c) => {
|
|
705
|
+
const payload = c.get('jwtPayload');
|
|
706
|
+
return c.json({ userId: payload.sub });
|
|
707
|
+
});
|
|
708
|
+
```
|
|
709
|
+
|
|
581
710
|
## Testing Strategy
|
|
582
711
|
|
|
583
712
|
| What to Test | How |
|
|
@@ -735,6 +864,7 @@ describe('Handler', () => {
|
|
|
735
864
|
## References
|
|
736
865
|
|
|
737
866
|
- [Zod Documentation](https://zod.dev) - Validation, transforms, error formatting, branded types
|
|
867
|
+
- [Hono Documentation](https://hono.dev) - Lightweight framework for serverless and edge
|
|
738
868
|
- [Vitest Documentation](https://vitest.dev) - Testing, mocking, vi.fn(), vi.spyOn()
|
|
739
869
|
- [AWS Lambda Cold Starts](https://aws.amazon.com/blogs/compute/understanding-and-remediating-cold-starts-an-aws-lambda-perspective/) - Official optimization guide
|
|
740
870
|
- [AWS Lambda Performance](https://aws.amazon.com/blogs/compute/operating-lambda-performance-optimization-part-1/) - Best practices
|
|
@@ -742,6 +872,7 @@ describe('Handler', () => {
|
|
|
742
872
|
**Version Notes:**
|
|
743
873
|
- Zod v3.24+: Improved error formatting, discriminated unions, branded types
|
|
744
874
|
- Zod v4.0+: prefault(), enhanced pipe(), performance improvements
|
|
875
|
+
- Hono v4+: Stable, edge-ready, built-in middleware
|
|
745
876
|
- Vitest v3+: mockResolvedValue, mockRejectedValue patterns
|
|
746
877
|
- AWS Lambda: Node.js 20.x has faster cold starts than 18.x
|
|
747
878
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: code-quality
|
|
3
|
-
description: Use when
|
|
3
|
+
description: Use when refactoring functions, extracting helpers, splitting large files, improving naming conventions, or reducing complexity. Use when functions exceed 30 lines, have too many parameters, or contain magic numbers. NOT for React/backend/database-specific patterns.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Code Quality
|
|
@@ -141,46 +141,59 @@ if (user.role === 'admin') { }
|
|
|
141
141
|
|
|
142
142
|
## Automated Enforcement
|
|
143
143
|
|
|
144
|
-
### ESLint Configuration
|
|
144
|
+
### ESLint Configuration (Flat Config - v9+)
|
|
145
145
|
|
|
146
|
-
Enforce code quality
|
|
146
|
+
ESLint 9+ uses flat config (`eslint.config.js`). Enforce code quality automatically:
|
|
147
147
|
|
|
148
148
|
```javascript
|
|
149
|
-
// .
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
149
|
+
// eslint.config.js (ESLint v9+ flat config)
|
|
150
|
+
import js from '@eslint/js';
|
|
151
|
+
import tseslint from 'typescript-eslint';
|
|
152
|
+
|
|
153
|
+
export default tseslint.config(
|
|
154
|
+
js.configs.recommended,
|
|
155
|
+
...tseslint.configs.recommended,
|
|
156
|
+
{
|
|
157
|
+
rules: {
|
|
158
|
+
// Max function length
|
|
159
|
+
'max-lines-per-function': ['error', {
|
|
160
|
+
max: 30,
|
|
161
|
+
skipBlankLines: true,
|
|
162
|
+
skipComments: true,
|
|
163
|
+
}],
|
|
164
|
+
|
|
165
|
+
// Cyclomatic complexity
|
|
166
|
+
'complexity': ['error', { max: 10 }],
|
|
167
|
+
|
|
168
|
+
// Max file length
|
|
169
|
+
'max-lines': ['error', {
|
|
170
|
+
max: 300,
|
|
171
|
+
skipBlankLines: true,
|
|
172
|
+
skipComments: true,
|
|
173
|
+
}],
|
|
174
|
+
|
|
175
|
+
// Max function params
|
|
176
|
+
'max-params': ['error', 3],
|
|
177
|
+
|
|
178
|
+
// Max nested callbacks
|
|
179
|
+
'max-nested-callbacks': ['error', 2],
|
|
180
|
+
|
|
181
|
+
// No magic numbers
|
|
182
|
+
'no-magic-numbers': ['error', {
|
|
183
|
+
ignore: [0, 1, -1],
|
|
184
|
+
ignoreArrayIndexes: true,
|
|
185
|
+
}],
|
|
186
|
+
},
|
|
180
187
|
},
|
|
181
|
-
|
|
188
|
+
{
|
|
189
|
+
// Ignore patterns (replaces .eslintignore)
|
|
190
|
+
ignores: ['node_modules/', 'dist/', '*.config.js'],
|
|
191
|
+
}
|
|
192
|
+
);
|
|
182
193
|
```
|
|
183
194
|
|
|
195
|
+
**Legacy config?** ESLint 9+ still supports `.eslintrc.js` via `ESLINT_USE_FLAT_CONFIG=false`, but flat config is the future.
|
|
196
|
+
|
|
184
197
|
### TypeScript-Specific Patterns
|
|
185
198
|
|
|
186
199
|
```typescript
|
|
@@ -294,14 +307,16 @@ When someone says "just make it work fast":
|
|
|
294
307
|
|
|
295
308
|
## References
|
|
296
309
|
|
|
310
|
+
- [ESLint Flat Config](https://eslint.org/docs/latest/use/configure/configuration-files-new) - ESLint 9+ configuration
|
|
297
311
|
- [ESLint Complexity Rule](https://eslint.org/docs/latest/rules/complexity) - Cyclomatic complexity enforcement
|
|
298
|
-
- [
|
|
312
|
+
- [typescript-eslint](https://typescript-eslint.io/) - TypeScript ESLint integration
|
|
299
313
|
- [TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/) - Advanced type patterns
|
|
300
314
|
- [Clean Code (Martin)](https://www.amazon.com/Clean-Code-Handbook-Software-Craftsmanship/dp/0132350882) - Function length rationale
|
|
301
315
|
|
|
302
316
|
**Version Notes:**
|
|
303
|
-
- ESLint 9+: Flat config
|
|
304
|
-
- TypeScript 5+: Improved discriminated union narrowing
|
|
317
|
+
- ESLint 9+: Flat config (`eslint.config.js`), replaces `.eslintrc.*`
|
|
318
|
+
- TypeScript 5+: Improved discriminated union narrowing, const type parameters
|
|
319
|
+
- typescript-eslint 8+: Native flat config support
|
|
305
320
|
- Cyclomatic complexity: Default threshold 20, recommended 10
|
|
306
321
|
|
|
307
322
|
## Common Mistakes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: database
|
|
3
|
-
description: Use when
|
|
3
|
+
description: Use when writing Drizzle ORM schemas, creating database tables, adding indexes, writing SQL migrations, defining relations, or optimizing PostgreSQL queries. Use for foreign key indexes, JSONB columns, composite indexes, or prepared statements.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Database Development
|
|
@@ -125,20 +125,66 @@ export const users = pgTable("users", {
|
|
|
125
125
|
| Strategy | Use When | Pros | Cons |
|
|
126
126
|
|----------|----------|------|------|
|
|
127
127
|
| **UUID** | Distributed systems, public IDs | No collisions, hide growth | Larger, slower indexes |
|
|
128
|
-
| **
|
|
128
|
+
| **Identity** | Single DB, internal IDs (2025+) | Compact, fast, SQL standard | Exposes growth |
|
|
129
129
|
| **ULID/NanoID** | Need sortability + randomness | Sortable, compact | Custom generation |
|
|
130
130
|
|
|
131
131
|
```typescript
|
|
132
|
-
// UUID (default
|
|
132
|
+
// UUID (default for distributed/public IDs)
|
|
133
133
|
id: uuid("id").defaultRandom().primaryKey(),
|
|
134
134
|
|
|
135
|
-
//
|
|
136
|
-
id:
|
|
135
|
+
// ✅ GOOD: Identity columns (PostgreSQL 10+, preferred over serial)
|
|
136
|
+
id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
|
|
137
|
+
|
|
138
|
+
// For bigint IDs
|
|
139
|
+
id: bigint("id", { mode: "number" }).primaryKey().generatedAlwaysAsIdentity(),
|
|
140
|
+
|
|
141
|
+
// ❌ AVOID: Serial (legacy, has sequence ownership issues)
|
|
142
|
+
id: serial("id").primaryKey(), // Use identity instead
|
|
137
143
|
|
|
138
144
|
// ULID for sortable + random
|
|
139
145
|
id: varchar("id", { length: 26 }).primaryKey().$default(() => ulid()),
|
|
140
146
|
```
|
|
141
147
|
|
|
148
|
+
**Why Identity over Serial:**
|
|
149
|
+
- SQL standard (serial is PostgreSQL-specific)
|
|
150
|
+
- No sequence ownership issues during migrations
|
|
151
|
+
- Better `COPY` and `pg_dump` behavior
|
|
152
|
+
- Cannot be overwritten accidentally (`GENERATED ALWAYS`)
|
|
153
|
+
|
|
154
|
+
### 7. Full-Text Search with GIN
|
|
155
|
+
|
|
156
|
+
For searchable text columns, use PostgreSQL's built-in full-text search:
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
import { sql } from "drizzle-orm";
|
|
160
|
+
import { index, pgTable, text, tsvector } from "drizzle-orm/pg-core";
|
|
161
|
+
|
|
162
|
+
export const products = pgTable("products", {
|
|
163
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
164
|
+
name: varchar("name", { length: 255 }).notNull(),
|
|
165
|
+
description: text("description"),
|
|
166
|
+
// Generated tsvector column for search
|
|
167
|
+
searchVector: tsvector("search_vector").generatedAlwaysAs(
|
|
168
|
+
sql`to_tsvector('english', coalesce(${products.name}, '') || ' ' || coalesce(${products.description}, ''))`
|
|
169
|
+
),
|
|
170
|
+
}, (table) => ({
|
|
171
|
+
// GIN index for fast full-text search
|
|
172
|
+
searchIdx: index("products_search_idx").using("gin", table.searchVector),
|
|
173
|
+
}));
|
|
174
|
+
|
|
175
|
+
// Query with full-text search
|
|
176
|
+
const results = await db
|
|
177
|
+
.select()
|
|
178
|
+
.from(products)
|
|
179
|
+
.where(sql`${products.searchVector} @@ plainto_tsquery('english', ${query})`);
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
**Key patterns:**
|
|
183
|
+
- Use `tsvector` for searchable content
|
|
184
|
+
- `generatedAlwaysAs` auto-updates on row changes
|
|
185
|
+
- GIN index makes searches fast (vs sequential scan)
|
|
186
|
+
- `plainto_tsquery` for simple user queries
|
|
187
|
+
|
|
142
188
|
## Migration Strategy
|
|
143
189
|
|
|
144
190
|
### Generate vs Push
|
|
@@ -429,11 +475,14 @@ if (duration > 100) {
|
|
|
429
475
|
|
|
430
476
|
- [Drizzle ORM Documentation](https://orm.drizzle.team/docs/overview) - Relations, prepared statements, migrations
|
|
431
477
|
- [PostgreSQL Indexes](https://www.postgresql.org/docs/current/indexes.html) - Index types and usage
|
|
478
|
+
- [PostgreSQL Full-Text Search](https://www.postgresql.org/docs/current/textsearch.html) - tsvector, GIN indexes
|
|
432
479
|
- [Drizzle Kit](https://orm.drizzle.team/docs/kit-overview) - Migration management
|
|
433
480
|
|
|
434
481
|
**Version Notes:**
|
|
435
|
-
- Drizzle ORM 0.30+: Improved relations API
|
|
482
|
+
- Drizzle ORM 0.30+: Improved relations API, identity column support
|
|
436
483
|
- Drizzle Kit 0.20+: Enhanced migration generation
|
|
484
|
+
- PostgreSQL 10+: Identity columns (prefer over serial)
|
|
485
|
+
- PostgreSQL 15+: Improved JSON and full-text performance
|
|
437
486
|
|
|
438
487
|
## Red Flags - STOP and Add Indexes
|
|
439
488
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: react
|
|
3
|
-
description: Use when writing React components,
|
|
3
|
+
description: Use when writing React components, creating hooks, using useState/useEffect/useReducer, building Next.js pages, implementing Server Components or Client Components, working with Remix loaders, Astro islands, or Vite SPA. Use for React performance issues, re-render problems, or component refactoring.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# React Development
|
|
@@ -229,6 +229,60 @@ export function SignupForm() {
|
|
|
229
229
|
|
|
230
230
|
## React 19 Features
|
|
231
231
|
|
|
232
|
+
### React Compiler (Auto-Memoization)
|
|
233
|
+
|
|
234
|
+
React Compiler automatically adds memoization - no more manual `useMemo`, `useCallback`, or `React.memo`.
|
|
235
|
+
|
|
236
|
+
```typescript
|
|
237
|
+
// ❌ OLD: Manual memoization everywhere
|
|
238
|
+
const MemoizedComponent = React.memo(({ user }) => {
|
|
239
|
+
const formattedName = useMemo(() => formatName(user), [user]);
|
|
240
|
+
const handleClick = useCallback(() => saveUser(user), [user]);
|
|
241
|
+
return <button onClick={handleClick}>{formattedName}</button>;
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// ✅ NEW: React Compiler handles it automatically
|
|
245
|
+
function UserButton({ user }) {
|
|
246
|
+
const formattedName = formatName(user);
|
|
247
|
+
const handleClick = () => saveUser(user);
|
|
248
|
+
return <button onClick={handleClick}>{formattedName}</button>;
|
|
249
|
+
}
|
|
250
|
+
// Compiler adds memoization during build - 25-40% fewer re-renders
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
**Key benefits:**
|
|
254
|
+
- No manual memoization needed
|
|
255
|
+
- Reduces bundle size (no memo wrappers)
|
|
256
|
+
- 25-40% fewer re-renders in typical apps
|
|
257
|
+
- Works with existing React 19 codebases
|
|
258
|
+
|
|
259
|
+
**Enable in Next.js 15+:**
|
|
260
|
+
```javascript
|
|
261
|
+
// next.config.js
|
|
262
|
+
module.exports = {
|
|
263
|
+
experimental: {
|
|
264
|
+
reactCompiler: true,
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Server/Client Component Decision Tree
|
|
270
|
+
|
|
271
|
+
```
|
|
272
|
+
Need this in the component?
|
|
273
|
+
├── Event handlers (onClick, onChange) → 'use client'
|
|
274
|
+
├── useState, useEffect, useReducer → 'use client'
|
|
275
|
+
├── Browser APIs (localStorage, window) → 'use client'
|
|
276
|
+
├── Third-party client libs (charts, maps) → 'use client'
|
|
277
|
+
│
|
|
278
|
+
├── Data fetching from DB/API → Server Component ✅
|
|
279
|
+
├── Access backend resources → Server Component ✅
|
|
280
|
+
├── Keep sensitive data (tokens, keys) → Server Component ✅
|
|
281
|
+
└── Reduce client bundle → Server Component ✅
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
**Rule of thumb:** Start with Server Components. Only add 'use client' when you hit a boundary that requires it.
|
|
285
|
+
|
|
232
286
|
### use() Hook - Read Promises/Context
|
|
233
287
|
|
|
234
288
|
```typescript
|
|
@@ -279,6 +333,59 @@ function TodoList({ todos }: { todos: Todo[] }) {
|
|
|
279
333
|
|
|
280
334
|
**Auto-reverts on error** - no manual rollback needed.
|
|
281
335
|
|
|
336
|
+
### Partial Pre-rendering (PPR) - Next.js 15+
|
|
337
|
+
|
|
338
|
+
Combine static shell with dynamic content in a single request:
|
|
339
|
+
|
|
340
|
+
```typescript
|
|
341
|
+
// app/products/[id]/page.tsx
|
|
342
|
+
import { Suspense } from 'react';
|
|
343
|
+
|
|
344
|
+
// Static shell - pre-rendered at build time
|
|
345
|
+
export default async function ProductPage({ params }) {
|
|
346
|
+
const product = await getProduct(params.id); // Cached/static
|
|
347
|
+
|
|
348
|
+
return (
|
|
349
|
+
<div>
|
|
350
|
+
{/* Static content - instant load */}
|
|
351
|
+
<h1>{product.name}</h1>
|
|
352
|
+
<p>{product.description}</p>
|
|
353
|
+
|
|
354
|
+
{/* Dynamic content - streams in */}
|
|
355
|
+
<Suspense fallback={<PriceSkeleton />}>
|
|
356
|
+
<DynamicPrice productId={params.id} />
|
|
357
|
+
</Suspense>
|
|
358
|
+
|
|
359
|
+
<Suspense fallback={<InventorySkeleton />}>
|
|
360
|
+
<InventoryStatus productId={params.id} />
|
|
361
|
+
</Suspense>
|
|
362
|
+
</div>
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Dynamic component - fetches real-time data
|
|
367
|
+
async function DynamicPrice({ productId }) {
|
|
368
|
+
const price = await getCurrentPrice(productId); // Always fresh
|
|
369
|
+
return <span className="price">${price}</span>;
|
|
370
|
+
}
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
**Enable PPR:**
|
|
374
|
+
```javascript
|
|
375
|
+
// next.config.js
|
|
376
|
+
module.exports = {
|
|
377
|
+
experimental: {
|
|
378
|
+
ppr: true,
|
|
379
|
+
},
|
|
380
|
+
};
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
**Benefits:**
|
|
384
|
+
- Static shell loads instantly (like SSG)
|
|
385
|
+
- Dynamic parts stream in (like SSR)
|
|
386
|
+
- Best of both worlds in one request
|
|
387
|
+
- No full-page waterfall
|
|
388
|
+
|
|
282
389
|
## Streaming & Suspense
|
|
283
390
|
|
|
284
391
|
### Full Page Streaming with loading.tsx
|
|
@@ -384,12 +491,15 @@ test('shows fallback then content', async () => {
|
|
|
384
491
|
|
|
385
492
|
- [Next.js Documentation](https://nextjs.org/docs) - App Router, Server Components, Server Actions
|
|
386
493
|
- [React 19 Release](https://react.dev/blog/2024/12/05/react-19) - use(), useActionState, useOptimistic
|
|
494
|
+
- [React Compiler](https://react.dev/learn/react-compiler) - Auto-memoization setup and usage
|
|
387
495
|
- [React Server Components](https://react.dev/reference/rsc/server-components) - Official RSC guide
|
|
388
496
|
|
|
389
497
|
**Version Notes:**
|
|
390
498
|
- Next.js 14+: App Router stable, Server Actions stable
|
|
391
|
-
- Next.js 15:
|
|
499
|
+
- Next.js 15: Turbopack stable, React Compiler support, PPR experimental
|
|
392
500
|
- React 19: use(), Actions, useOptimistic, useActionState
|
|
501
|
+
- React 19.2+: Partial Pre-rendering (PPR), enhanced Suspense
|
|
502
|
+
- React Compiler: Auto-memoization, 25-40% fewer re-renders
|
|
393
503
|
|
|
394
504
|
## Red Flags - STOP and Restructure
|
|
395
505
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: seo
|
|
3
|
-
description: Use when
|
|
3
|
+
description: Use when adding page metadata, implementing OpenGraph tags, creating JSON-LD structured data, generating sitemaps, optimizing LCP/INP/CLS, or configuring robots.txt. Use for Next.js Metadata API, Article/Product/FAQ schemas, or image optimization with next/image.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# SEO Optimization
|
|
@@ -128,6 +128,35 @@ input.addEventListener('input', (e) => {
|
|
|
128
128
|
|
|
129
129
|
### 2. Use Next.js Metadata API Correctly
|
|
130
130
|
|
|
131
|
+
#### Root Layout: metadataBase + title.template
|
|
132
|
+
|
|
133
|
+
**Critical:** Set `metadataBase` in your root layout. Without it, relative OG image URLs break.
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
// app/layout.tsx
|
|
137
|
+
import { Metadata } from 'next';
|
|
138
|
+
|
|
139
|
+
export const metadata: Metadata = {
|
|
140
|
+
metadataBase: new URL('https://site.com'), // REQUIRED for OG images
|
|
141
|
+
title: {
|
|
142
|
+
default: 'Brand Name',
|
|
143
|
+
template: '%s | Brand Name', // Auto-appends to all pages
|
|
144
|
+
},
|
|
145
|
+
description: 'Default site description',
|
|
146
|
+
openGraph: {
|
|
147
|
+
siteName: 'Brand Name',
|
|
148
|
+
locale: 'en_US',
|
|
149
|
+
type: 'website',
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**Why metadataBase matters:**
|
|
155
|
+
- Without it: `images: ['/og.png']` → broken URL
|
|
156
|
+
- With it: `images: ['/og.png']` → `https://site.com/og.png`
|
|
157
|
+
|
|
158
|
+
#### Page-Level Metadata
|
|
159
|
+
|
|
131
160
|
```typescript
|
|
132
161
|
// app/products/[slug]/page.tsx
|
|
133
162
|
import { Metadata } from 'next';
|
|
@@ -136,11 +165,11 @@ export async function generateMetadata({ params }): Promise<Metadata> {
|
|
|
136
165
|
const product = await getProduct(params.slug);
|
|
137
166
|
|
|
138
167
|
return {
|
|
139
|
-
title:
|
|
168
|
+
title: product.name, // Becomes "Product Name | Brand Name" via template
|
|
140
169
|
description: truncate(product.description, 155), // Max 155 chars
|
|
141
170
|
|
|
142
171
|
alternates: {
|
|
143
|
-
canonical:
|
|
172
|
+
canonical: `/products/${product.slug}`, // Relative OK with metadataBase
|
|
144
173
|
},
|
|
145
174
|
|
|
146
175
|
// OpenGraph - use 'product' for e-commerce
|
|
@@ -278,6 +307,87 @@ const faqJsonLd = {
|
|
|
278
307
|
};
|
|
279
308
|
```
|
|
280
309
|
|
|
310
|
+
#### BreadcrumbList Schema
|
|
311
|
+
|
|
312
|
+
Shows breadcrumb trail in search results:
|
|
313
|
+
|
|
314
|
+
```typescript
|
|
315
|
+
const breadcrumbJsonLd = {
|
|
316
|
+
'@context': 'https://schema.org',
|
|
317
|
+
'@type': 'BreadcrumbList',
|
|
318
|
+
itemListElement: [
|
|
319
|
+
{
|
|
320
|
+
'@type': 'ListItem',
|
|
321
|
+
position: 1,
|
|
322
|
+
name: 'Home',
|
|
323
|
+
item: 'https://site.com',
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
'@type': 'ListItem',
|
|
327
|
+
position: 2,
|
|
328
|
+
name: 'Products',
|
|
329
|
+
item: 'https://site.com/products',
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
'@type': 'ListItem',
|
|
333
|
+
position: 3,
|
|
334
|
+
name: product.name,
|
|
335
|
+
item: `https://site.com/products/${product.slug}`,
|
|
336
|
+
},
|
|
337
|
+
],
|
|
338
|
+
};
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
### 6. Answer Engine Optimization (AEO)
|
|
342
|
+
|
|
343
|
+
AI search engines (ChatGPT, Perplexity, Claude) are becoming traffic sources. Optimize for them:
|
|
344
|
+
|
|
345
|
+
**Key AEO strategies:**
|
|
346
|
+
|
|
347
|
+
| Strategy | Implementation |
|
|
348
|
+
|----------|----------------|
|
|
349
|
+
| **FAQ sections** | Add FAQPage schema - AI pulls from structured Q&A |
|
|
350
|
+
| **Direct answers** | Start content with clear, factual statements |
|
|
351
|
+
| **Structured data** | Schema.org markup helps AI understand content |
|
|
352
|
+
| **Topic authority** | Comprehensive coverage on topic clusters |
|
|
353
|
+
| **Citation-friendly** | Include stats, dates, sources that AI can cite |
|
|
354
|
+
|
|
355
|
+
```typescript
|
|
356
|
+
// ✅ GOOD: Content structure for AI search
|
|
357
|
+
function ProductPage({ product }) {
|
|
358
|
+
return (
|
|
359
|
+
<>
|
|
360
|
+
{/* Direct answer for AI to extract */}
|
|
361
|
+
<p className="lead">
|
|
362
|
+
The {product.name} is a {product.category} that {product.keyBenefit}.
|
|
363
|
+
Priced at ${product.price}, it's ideal for {product.targetAudience}.
|
|
364
|
+
</p>
|
|
365
|
+
|
|
366
|
+
{/* FAQ section with schema */}
|
|
367
|
+
<section>
|
|
368
|
+
<h2>Frequently Asked Questions</h2>
|
|
369
|
+
{product.faqs.map(faq => (
|
|
370
|
+
<details key={faq.id}>
|
|
371
|
+
<summary>{faq.question}</summary>
|
|
372
|
+
<p>{faq.answer}</p>
|
|
373
|
+
</details>
|
|
374
|
+
))}
|
|
375
|
+
</section>
|
|
376
|
+
|
|
377
|
+
{/* Structured data for both Google and AI */}
|
|
378
|
+
<JsonLd data={productJsonLd} />
|
|
379
|
+
<JsonLd data={faqJsonLd} />
|
|
380
|
+
</>
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
**Why AEO matters:**
|
|
386
|
+
- 40% of Gen Z uses TikTok/AI for search over Google
|
|
387
|
+
- AI search engines cite well-structured content
|
|
388
|
+
- FAQ sections appear in AI answers
|
|
389
|
+
- Structured data = machine-readable = AI-friendly
|
|
390
|
+
|
|
281
391
|
## Sitemap & Robots
|
|
282
392
|
|
|
283
393
|
### Dynamic Sitemap Generation
|
|
@@ -408,13 +518,15 @@ new PerformanceObserver((list) => {
|
|
|
408
518
|
- [Core Web Vitals INP Guide](https://web.dev/articles/inp) - Official INP optimization patterns
|
|
409
519
|
- [Optimize INP](https://web.dev/articles/optimize-inp) - Three-phase optimization approach
|
|
410
520
|
- [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
|
|
521
|
+
- [Next.js Metadata API](https://nextjs.org/docs/app/api-reference/functions/generate-metadata) - generateMetadata, metadataBase
|
|
412
522
|
- [Schema.org](https://schema.org/) - Structured data vocabulary
|
|
523
|
+
- [Rich Results Test](https://search.google.com/test/rich-results) - Validate structured data
|
|
413
524
|
|
|
414
525
|
**Version Notes:**
|
|
415
526
|
- INP replaced FID as Core Web Vital (March 2024)
|
|
416
|
-
- Next.js 15: Enhanced Image component
|
|
527
|
+
- Next.js 15: Enhanced Image component, metadataBase required for OG
|
|
417
528
|
- Good INP: < 200ms (improving from 500ms → 200ms = 22% engagement boost)
|
|
529
|
+
- AEO emerging: AI search engines (ChatGPT, Perplexity) use structured data
|
|
418
530
|
|
|
419
531
|
## Red Flags - STOP and Fix
|
|
420
532
|
|