Initial commit
This commit is contained in:
12
.claude-plugin/plugin.json
Normal file
12
.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "revalidation-strategy-planner",
|
||||||
|
"description": "Evaluates Next.js routes and outputs optimal revalidate settings, cache tags for ISR, SSR configurations, or streaming patterns. This skill should be used when optimizing Next.js caching strategies, configuring Incremental Static Regeneration, planning cache invalidation, or choosing between SSR/ISR/SSG. Use for Next.js caching, revalidation, ISR, cache tags, on-demand revalidation, or rendering strategies.",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": {
|
||||||
|
"name": "Hope Overture",
|
||||||
|
"email": "support@worldbuilding-app-skills.dev"
|
||||||
|
},
|
||||||
|
"skills": [
|
||||||
|
"./skills"
|
||||||
|
]
|
||||||
|
}
|
||||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# revalidation-strategy-planner
|
||||||
|
|
||||||
|
Evaluates Next.js routes and outputs optimal revalidate settings, cache tags for ISR, SSR configurations, or streaming patterns. This skill should be used when optimizing Next.js caching strategies, configuring Incremental Static Regeneration, planning cache invalidation, or choosing between SSR/ISR/SSG. Use for Next.js caching, revalidation, ISR, cache tags, on-demand revalidation, or rendering strategies.
|
||||||
49
plugin.lock.json
Normal file
49
plugin.lock.json
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"$schema": "internal://schemas/plugin.lock.v1.json",
|
||||||
|
"pluginId": "gh:hopeoverture/worldbuilding-app-skills:plugins/revalidation-strategy-planner",
|
||||||
|
"normalized": {
|
||||||
|
"repo": null,
|
||||||
|
"ref": "refs/tags/v20251128.0",
|
||||||
|
"commit": "380401673469f417a3e2a797d1a969d1f15ccd2d",
|
||||||
|
"treeHash": "b057d3f4d8ddd9a58d2c9a020c3fec0bd09964a87ef26680792c2d8534a3e608",
|
||||||
|
"generatedAt": "2025-11-28T10:17:33.107449Z",
|
||||||
|
"toolVersion": "publish_plugins.py@0.2.0"
|
||||||
|
},
|
||||||
|
"origin": {
|
||||||
|
"remote": "git@github.com:zhongweili/42plugin-data.git",
|
||||||
|
"branch": "master",
|
||||||
|
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
|
||||||
|
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
|
||||||
|
},
|
||||||
|
"manifest": {
|
||||||
|
"name": "revalidation-strategy-planner",
|
||||||
|
"description": "Evaluates Next.js routes and outputs optimal revalidate settings, cache tags for ISR, SSR configurations, or streaming patterns. This skill should be used when optimizing Next.js caching strategies, configuring Incremental Static Regeneration, planning cache invalidation, or choosing between SSR/ISR/SSG. Use for Next.js caching, revalidation, ISR, cache tags, on-demand revalidation, or rendering strategies.",
|
||||||
|
"version": "1.0.0"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"path": "README.md",
|
||||||
|
"sha256": "ef84c6d61e1d4d620273babad16a87d6ecb5729edf9c647d39c524869868a56f"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": ".claude-plugin/plugin.json",
|
||||||
|
"sha256": "a488141c6f64ab3641bcd10d21a5ee318a43d3a026fe1b5031637bbb85775085"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/revalidation-strategy-planner/SKILL.md",
|
||||||
|
"sha256": "00e70f7e44cfc91ffdefea8580706b5a9c8c0b11541af5f0a15800efd81e99e1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/revalidation-strategy-planner/scripts/analyze_routes.py",
|
||||||
|
"sha256": "af471725b616c65a652dbd95e0213253cef04e8ac24ac8f81e2054016d61cf1b"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dirSha256": "b057d3f4d8ddd9a58d2c9a020c3fec0bd09964a87ef26680792c2d8534a3e608"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"scannedAt": null,
|
||||||
|
"scannerVersion": null,
|
||||||
|
"flags": []
|
||||||
|
}
|
||||||
|
}
|
||||||
482
skills/revalidation-strategy-planner/SKILL.md
Normal file
482
skills/revalidation-strategy-planner/SKILL.md
Normal file
@@ -0,0 +1,482 @@
|
|||||||
|
---
|
||||||
|
name: revalidation-strategy-planner
|
||||||
|
description: Evaluates Next.js routes and outputs optimal revalidate settings, cache tags for ISR, SSR configurations, or streaming patterns. This skill should be used when optimizing Next.js caching strategies, configuring Incremental Static Regeneration, planning cache invalidation, or choosing between SSR/ISR/SSG. Use for Next.js caching, revalidation, ISR, cache tags, on-demand revalidation, or rendering strategies.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Revalidation Strategy Planner
|
||||||
|
|
||||||
|
Analyze Next.js application routes and recommend optimal caching and revalidation strategies for performance and data freshness.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
To optimize Next.js caching strategies:
|
||||||
|
|
||||||
|
1. Analyze route characteristics (data freshness requirements, update frequency)
|
||||||
|
2. Determine appropriate rendering strategy (SSG, ISR, SSR, streaming)
|
||||||
|
3. Configure revalidation intervals for ISR routes
|
||||||
|
4. Implement cache tags for on-demand revalidation
|
||||||
|
5. Set up streaming for progressive page loading
|
||||||
|
|
||||||
|
## Rendering Strategies
|
||||||
|
|
||||||
|
### Static Site Generation (SSG)
|
||||||
|
|
||||||
|
To use SSG for rarely changing content:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// app/about/page.tsx
|
||||||
|
export default async function AboutPage() {
|
||||||
|
// Generated at build time, no revalidation
|
||||||
|
return <div>About Us</div>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Best for:**
|
||||||
|
- Marketing pages
|
||||||
|
- Documentation
|
||||||
|
- Static content that rarely changes
|
||||||
|
|
||||||
|
### Incremental Static Regeneration (ISR)
|
||||||
|
|
||||||
|
To use ISR for periodically updated content:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// app/entities/[id]/page.tsx
|
||||||
|
export const revalidate = 3600; // Revalidate every hour
|
||||||
|
|
||||||
|
export default async function EntityPage({ params }: { params: { id: string } }) {
|
||||||
|
const entity = await fetchEntity(params.id);
|
||||||
|
return <EntityDetail entity={entity} />;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Best for:**
|
||||||
|
- Entity detail pages
|
||||||
|
- Blog posts
|
||||||
|
- Product listings
|
||||||
|
- Content with predictable update patterns
|
||||||
|
|
||||||
|
### Server-Side Rendering (SSR)
|
||||||
|
|
||||||
|
To use SSR for real-time data:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// app/dashboard/page.tsx
|
||||||
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
|
export default async function Dashboard() {
|
||||||
|
const data = await fetchUserData();
|
||||||
|
return <DashboardView data={data} />;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Best for:**
|
||||||
|
- User dashboards
|
||||||
|
- Personalized content
|
||||||
|
- Real-time data displays
|
||||||
|
- Authentication-dependent pages
|
||||||
|
|
||||||
|
### Streaming
|
||||||
|
|
||||||
|
To use streaming for progressive loading:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// app/timeline/page.tsx
|
||||||
|
import { Suspense } from 'react';
|
||||||
|
|
||||||
|
export default function TimelinePage() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<TimelineHeader />
|
||||||
|
<Suspense fallback={<TimelineLoader />}>
|
||||||
|
<TimelineEvents />
|
||||||
|
</Suspense>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Best for:**
|
||||||
|
- Pages with slow data fetching
|
||||||
|
- Complex pages with multiple data sources
|
||||||
|
- Improving perceived performance
|
||||||
|
|
||||||
|
Consult `references/rendering-strategies.md` for detailed strategy comparison.
|
||||||
|
|
||||||
|
## Revalidation Configuration
|
||||||
|
|
||||||
|
### Time-Based Revalidation
|
||||||
|
|
||||||
|
To set revalidation intervals:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Revalidate every 60 seconds
|
||||||
|
export const revalidate = 60;
|
||||||
|
|
||||||
|
// Revalidate every hour
|
||||||
|
export const revalidate = 3600;
|
||||||
|
|
||||||
|
// Revalidate every day
|
||||||
|
export const revalidate = 86400;
|
||||||
|
```
|
||||||
|
|
||||||
|
### On-Demand Revalidation
|
||||||
|
|
||||||
|
To implement on-demand cache invalidation:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// app/api/revalidate/route.ts
|
||||||
|
import { revalidatePath, revalidateTag } from 'next/cache';
|
||||||
|
import { NextRequest } from 'next/server';
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
const { path, tag } = await request.json();
|
||||||
|
|
||||||
|
if (path) {
|
||||||
|
revalidatePath(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tag) {
|
||||||
|
revalidateTag(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.json({ revalidated: true, now: Date.now() });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Use from Server Actions:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
'use server';
|
||||||
|
|
||||||
|
import { revalidatePath } from 'next/cache';
|
||||||
|
|
||||||
|
export async function updateEntity(id: string, data: EntityData) {
|
||||||
|
await saveEntity(id, data);
|
||||||
|
revalidatePath(`/entities/${id}`);
|
||||||
|
revalidatePath('/entities');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cache Tags
|
||||||
|
|
||||||
|
To implement cache tag-based revalidation:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// app/entities/[id]/page.tsx
|
||||||
|
export default async function EntityPage({ params }: { params: { id: string } }) {
|
||||||
|
const entity = await fetch(`/api/entities/${params.id}`, {
|
||||||
|
next: {
|
||||||
|
tags: [`entity-${params.id}`, 'entities'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return <EntityDetail entity={entity} />;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Revalidate by tag:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { revalidateTag } from 'next/cache';
|
||||||
|
|
||||||
|
// Revalidate all pages with 'entities' tag
|
||||||
|
revalidateTag('entities');
|
||||||
|
|
||||||
|
// Revalidate specific entity
|
||||||
|
revalidateTag(`entity-${entityId}`);
|
||||||
|
```
|
||||||
|
|
||||||
|
Reference `assets/cache-tag-patterns.ts` for cache tagging patterns.
|
||||||
|
|
||||||
|
## Route Analysis
|
||||||
|
|
||||||
|
Use `scripts/analyze_routes.py` to analyze application routes and recommend strategies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/analyze_routes.py ./app
|
||||||
|
```
|
||||||
|
|
||||||
|
Output includes:
|
||||||
|
|
||||||
|
- Route path
|
||||||
|
- Recommended rendering strategy
|
||||||
|
- Suggested revalidation interval
|
||||||
|
- Appropriate cache tags
|
||||||
|
- Reasoning for recommendations
|
||||||
|
|
||||||
|
### Analysis Criteria
|
||||||
|
|
||||||
|
Consider these factors:
|
||||||
|
|
||||||
|
1. **Data Freshness Requirements**
|
||||||
|
- Real-time: SSR or very short revalidation (1-60s)
|
||||||
|
- Near real-time: ISR with short interval (60-300s)
|
||||||
|
- Periodic updates: ISR with medium interval (300-3600s)
|
||||||
|
- Rarely changes: SSG or long interval (3600s+)
|
||||||
|
|
||||||
|
2. **Update Frequency**
|
||||||
|
- Continuous: SSR
|
||||||
|
- Multiple times per hour: ISR (60-300s)
|
||||||
|
- Hourly: ISR (3600s)
|
||||||
|
- Daily: ISR (86400s)
|
||||||
|
- Weekly+: SSG
|
||||||
|
|
||||||
|
3. **Personalization**
|
||||||
|
- User-specific: SSR
|
||||||
|
- Role-based: SSR or ISR with user context
|
||||||
|
- Public: SSG or ISR
|
||||||
|
|
||||||
|
4. **Data Source Performance**
|
||||||
|
- Fast (<100ms): Any strategy
|
||||||
|
- Medium (100-500ms): Consider streaming
|
||||||
|
- Slow (>500ms): Use streaming or aggressive caching
|
||||||
|
|
||||||
|
Consult `references/decision-matrix.md` for the complete decision matrix.
|
||||||
|
|
||||||
|
## Implementation Patterns
|
||||||
|
|
||||||
|
### Entity Detail Pages
|
||||||
|
|
||||||
|
To optimize entity pages:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// app/entities/[id]/page.tsx
|
||||||
|
export const revalidate = 1800; // 30 minutes
|
||||||
|
|
||||||
|
export async function generateStaticParams() {
|
||||||
|
const entities = await fetchAllEntityIds();
|
||||||
|
return entities.map((id) => ({ id: id.toString() }));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function EntityPage({ params }: { params: { id: string } }) {
|
||||||
|
const entity = await fetchEntity(params.id, {
|
||||||
|
next: { tags: [`entity-${params.id}`, 'entities'] },
|
||||||
|
});
|
||||||
|
|
||||||
|
return <EntityDetail entity={entity} />;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### List Pages
|
||||||
|
|
||||||
|
To optimize listing pages:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// app/entities/page.tsx
|
||||||
|
export const revalidate = 300; // 5 minutes
|
||||||
|
|
||||||
|
export default async function EntitiesPage({
|
||||||
|
searchParams,
|
||||||
|
}: {
|
||||||
|
searchParams: { page?: string };
|
||||||
|
}) {
|
||||||
|
const page = parseInt(searchParams.page || '1');
|
||||||
|
const entities = await fetchEntities(page, {
|
||||||
|
next: { tags: ['entities'] },
|
||||||
|
});
|
||||||
|
|
||||||
|
return <EntityList entities={entities} />;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Timeline Pages
|
||||||
|
|
||||||
|
To optimize timeline with streaming:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// app/timeline/page.tsx
|
||||||
|
import { Suspense } from 'react';
|
||||||
|
|
||||||
|
export default function TimelinePage() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Suspense fallback={<TimelineHeaderSkeleton />}>
|
||||||
|
<TimelineHeader />
|
||||||
|
</Suspense>
|
||||||
|
<Suspense fallback={<EventsSkeleton />}>
|
||||||
|
<TimelineEvents />
|
||||||
|
</Suspense>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function TimelineEvents() {
|
||||||
|
const events = await fetchTimelineEvents({
|
||||||
|
next: { tags: ['timeline'], revalidate: 600 },
|
||||||
|
});
|
||||||
|
return <EventsList events={events} />;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dashboard Pages
|
||||||
|
|
||||||
|
To implement personalized dashboard:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// app/dashboard/page.tsx
|
||||||
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
|
export default async function DashboardPage() {
|
||||||
|
const session = await getSession();
|
||||||
|
const data = await fetchUserDashboard(session.userId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Suspense fallback={<StatsSkeleton />}>
|
||||||
|
<DashboardStats userId={session.userId} />
|
||||||
|
</Suspense>
|
||||||
|
<Suspense fallback={<ActivitySkeleton />}>
|
||||||
|
<RecentActivity userId={session.userId} />
|
||||||
|
</Suspense>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cache Invalidation Strategies
|
||||||
|
|
||||||
|
### Granular Invalidation
|
||||||
|
|
||||||
|
To invalidate specific resources:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// After entity update
|
||||||
|
revalidateTag(`entity-${entityId}`);
|
||||||
|
|
||||||
|
// After relationship change
|
||||||
|
revalidateTag(`entity-${sourceId}`);
|
||||||
|
revalidateTag(`entity-${targetId}`);
|
||||||
|
revalidateTag('relationships');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cascade Invalidation
|
||||||
|
|
||||||
|
To invalidate related resources:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async function updateEntity(id: string, data: EntityData) {
|
||||||
|
await saveEntity(id, data);
|
||||||
|
|
||||||
|
// Invalidate entity page
|
||||||
|
revalidateTag(`entity-${id}`);
|
||||||
|
|
||||||
|
// Invalidate list pages
|
||||||
|
revalidateTag('entities');
|
||||||
|
|
||||||
|
// Invalidate related pages
|
||||||
|
const relationships = await getEntityRelationships(id);
|
||||||
|
for (const rel of relationships) {
|
||||||
|
revalidateTag(`entity-${rel.targetId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Batch Invalidation
|
||||||
|
|
||||||
|
To invalidate multiple resources efficiently:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async function bulkUpdateEntities(updates: EntityUpdate[]) {
|
||||||
|
await saveBulkUpdates(updates);
|
||||||
|
|
||||||
|
// Collect unique tags
|
||||||
|
const tags = new Set<string>(['entities']);
|
||||||
|
for (const update of updates) {
|
||||||
|
tags.add(`entity-${update.id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Revalidate all at once
|
||||||
|
for (const tag of tags) {
|
||||||
|
revalidateTag(tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Optimization
|
||||||
|
|
||||||
|
### Stale-While-Revalidate
|
||||||
|
|
||||||
|
To implement SWR pattern:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const revalidate = 60; // Revalidate every minute
|
||||||
|
export const dynamic = 'force-static'; // Serve stale while revalidating
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parallel Data Fetching
|
||||||
|
|
||||||
|
To fetch data in parallel:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export default async function EntityPage({ params }: { params: { id: string } }) {
|
||||||
|
const [entity, relationships, timeline] = await Promise.all([
|
||||||
|
fetchEntity(params.id),
|
||||||
|
fetchRelationships(params.id),
|
||||||
|
fetchTimeline(params.id),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return <EntityDetailView entity={entity} relationships={relationships} timeline={timeline} />;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Selective Streaming
|
||||||
|
|
||||||
|
To stream only slow components:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export default function EntityPage({ params }: { params: { id: string } }) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<EntityHeader id={params.id} /> {/* Fast, no streaming */}
|
||||||
|
<Suspense fallback={<RelationshipsSkeleton />}>
|
||||||
|
<EntityRelationships id={params.id} /> {/* Slow, stream it */}
|
||||||
|
</Suspense>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Monitoring and Testing
|
||||||
|
|
||||||
|
To monitor cache performance:
|
||||||
|
|
||||||
|
1. **Cache Hit Rates**: Track ISR cache hits vs. regenerations
|
||||||
|
2. **Revalidation Frequency**: Monitor how often pages regenerate
|
||||||
|
3. **Response Times**: Measure time to first byte (TTFB)
|
||||||
|
4. **Stale Serving**: Track stale-while-revalidate occurrences
|
||||||
|
|
||||||
|
Use Next.js analytics or custom logging:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// middleware.ts
|
||||||
|
export function middleware(request: NextRequest) {
|
||||||
|
const start = Date.now();
|
||||||
|
|
||||||
|
return NextResponse.next({
|
||||||
|
headers: {
|
||||||
|
'x-response-time': `${Date.now() - start}ms`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Start Conservative**: Begin with shorter revalidation intervals, increase gradually
|
||||||
|
2. **Use Cache Tags**: Prefer tag-based invalidation over path-based
|
||||||
|
3. **Monitor Performance**: Track cache hit rates and response times
|
||||||
|
4. **Plan Invalidation**: Design invalidation strategy with data mutations
|
||||||
|
5. **Test Edge Cases**: Verify behavior with stale data and revalidation
|
||||||
|
6. **Document Decisions**: Record why specific intervals were chosen
|
||||||
|
7. **Consider Users**: Balance freshness with performance
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
Common issues:
|
||||||
|
|
||||||
|
- **Stale Data Persisting**: Check cache tag implementation and invalidation logic
|
||||||
|
- **Excessive Regeneration**: Increase revalidation interval or fix trigger-happy invalidation
|
||||||
|
- **Slow Page Loads**: Add streaming for slow components
|
||||||
|
- **Cache Not Working**: Verify fetch options and dynamic/static configuration
|
||||||
|
- **Development vs Production**: Remember ISR only works in production builds
|
||||||
239
skills/revalidation-strategy-planner/scripts/analyze_routes.py
Normal file
239
skills/revalidation-strategy-planner/scripts/analyze_routes.py
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Analyze Next.js routes and recommend revalidation strategies.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
|
|
||||||
|
class RouteAnalyzer:
|
||||||
|
def __init__(self, app_dir: Path):
|
||||||
|
self.app_dir = app_dir
|
||||||
|
self.routes = []
|
||||||
|
|
||||||
|
def analyze(self):
|
||||||
|
"""Analyze all routes in the app directory."""
|
||||||
|
self.routes = []
|
||||||
|
self._scan_directory(self.app_dir)
|
||||||
|
return self.routes
|
||||||
|
|
||||||
|
def _scan_directory(self, directory: Path, route_path: str = ""):
|
||||||
|
"""Recursively scan directory for routes."""
|
||||||
|
if not directory.exists():
|
||||||
|
print(f"Warning: Directory not found: {directory}")
|
||||||
|
return
|
||||||
|
|
||||||
|
for item in directory.iterdir():
|
||||||
|
if item.name.startswith('_') or item.name.startswith('.'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if item.is_dir():
|
||||||
|
# Handle route groups (groupName) and dynamic routes [param]
|
||||||
|
if item.name.startswith('(') and item.name.endswith(')'):
|
||||||
|
# Route group - doesn't affect URL
|
||||||
|
self._scan_directory(item, route_path)
|
||||||
|
elif item.name.startswith('[') and item.name.endswith(']'):
|
||||||
|
# Dynamic route segment
|
||||||
|
param = item.name
|
||||||
|
self._scan_directory(item, f"{route_path}/{param}")
|
||||||
|
else:
|
||||||
|
# Regular route segment
|
||||||
|
self._scan_directory(item, f"{route_path}/{item.name}")
|
||||||
|
|
||||||
|
elif item.name == 'page.tsx' or item.name == 'page.js':
|
||||||
|
# Found a page route
|
||||||
|
route = self._analyze_route_file(item, route_path or '/')
|
||||||
|
if route:
|
||||||
|
self.routes.append(route)
|
||||||
|
|
||||||
|
def _analyze_route_file(self, file_path: Path, route_path: str) -> Optional[Dict]:
|
||||||
|
"""Analyze a page file and determine recommendations."""
|
||||||
|
try:
|
||||||
|
with open(file_path, 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# Detect existing configuration
|
||||||
|
has_revalidate = 'export const revalidate' in content
|
||||||
|
has_dynamic = 'export const dynamic' in content
|
||||||
|
has_fetch = 'fetch(' in content or 'await' in content
|
||||||
|
has_suspense = 'Suspense' in content
|
||||||
|
has_params = '[' in route_path and ']' in route_path
|
||||||
|
has_searchparams = 'searchParams' in content
|
||||||
|
|
||||||
|
# Determine route characteristics
|
||||||
|
is_dynamic_route = has_params
|
||||||
|
is_personalized = any(
|
||||||
|
keyword in content.lower()
|
||||||
|
for keyword in ['session', 'user', 'auth', 'dashboard']
|
||||||
|
)
|
||||||
|
is_list = any(
|
||||||
|
keyword in route_path.lower()
|
||||||
|
for keyword in ['/entities', '/timeline', '/characters', '/locations']
|
||||||
|
)
|
||||||
|
is_detail = is_dynamic_route and not is_list
|
||||||
|
|
||||||
|
# Make recommendations
|
||||||
|
recommendation = self._recommend_strategy(
|
||||||
|
is_personalized=is_personalized,
|
||||||
|
is_list=is_list,
|
||||||
|
is_detail=is_detail,
|
||||||
|
has_fetch=has_fetch,
|
||||||
|
route_path=route_path,
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'path': route_path,
|
||||||
|
'file': str(file_path.relative_to(self.app_dir.parent)),
|
||||||
|
'characteristics': {
|
||||||
|
'dynamic_route': is_dynamic_route,
|
||||||
|
'personalized': is_personalized,
|
||||||
|
'list_page': is_list,
|
||||||
|
'detail_page': is_detail,
|
||||||
|
'has_data_fetching': has_fetch,
|
||||||
|
'has_suspense': has_suspense,
|
||||||
|
},
|
||||||
|
'current_config': {
|
||||||
|
'has_revalidate': has_revalidate,
|
||||||
|
'has_dynamic': has_dynamic,
|
||||||
|
},
|
||||||
|
'recommendation': recommendation,
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error analyzing {file_path}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _recommend_strategy(
|
||||||
|
self, is_personalized: bool, is_list: bool, is_detail: bool, has_fetch: bool, route_path: str
|
||||||
|
) -> Dict:
|
||||||
|
"""Recommend rendering and caching strategy."""
|
||||||
|
|
||||||
|
# Personalized content needs SSR
|
||||||
|
if is_personalized:
|
||||||
|
return {
|
||||||
|
'strategy': 'SSR',
|
||||||
|
'config': "export const dynamic = 'force-dynamic';",
|
||||||
|
'revalidate': None,
|
||||||
|
'cache_tags': [],
|
||||||
|
'reasoning': 'Personalized content requires server-side rendering for each request',
|
||||||
|
'priority': 'high',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Detail pages benefit from ISR
|
||||||
|
if is_detail:
|
||||||
|
return {
|
||||||
|
'strategy': 'ISR',
|
||||||
|
'config': 'export const revalidate = 1800; // 30 minutes',
|
||||||
|
'revalidate': 1800,
|
||||||
|
'cache_tags': [self._extract_entity_tag(route_path), 'entities'],
|
||||||
|
'reasoning': 'Detail pages can use ISR with moderate revalidation for balance of performance and freshness',
|
||||||
|
'priority': 'medium',
|
||||||
|
}
|
||||||
|
|
||||||
|
# List pages with frequent updates
|
||||||
|
if is_list:
|
||||||
|
return {
|
||||||
|
'strategy': 'ISR',
|
||||||
|
'config': 'export const revalidate = 300; // 5 minutes',
|
||||||
|
'revalidate': 300,
|
||||||
|
'cache_tags': ['entities', 'timeline', 'characters'],
|
||||||
|
'reasoning': 'List pages benefit from shorter revalidation to show recent updates',
|
||||||
|
'priority': 'medium',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Static pages
|
||||||
|
if not has_fetch:
|
||||||
|
return {
|
||||||
|
'strategy': 'SSG',
|
||||||
|
'config': '// Static page, no revalidation needed',
|
||||||
|
'revalidate': None,
|
||||||
|
'cache_tags': [],
|
||||||
|
'reasoning': 'No data fetching detected, suitable for static generation',
|
||||||
|
'priority': 'low',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Default to ISR with moderate interval
|
||||||
|
return {
|
||||||
|
'strategy': 'ISR',
|
||||||
|
'config': 'export const revalidate = 600; // 10 minutes',
|
||||||
|
'revalidate': 600,
|
||||||
|
'cache_tags': [],
|
||||||
|
'reasoning': 'Default ISR strategy provides good balance for most pages',
|
||||||
|
'priority': 'low',
|
||||||
|
}
|
||||||
|
|
||||||
|
def _extract_entity_tag(self, route_path: str) -> str:
|
||||||
|
"""Extract entity name from route for cache tagging."""
|
||||||
|
parts = route_path.split('/')
|
||||||
|
for i, part in enumerate(parts):
|
||||||
|
if part.startswith('[') and part.endswith(']'):
|
||||||
|
if i > 0:
|
||||||
|
entity_type = parts[i - 1].rstrip('s') # Remove plural 's'
|
||||||
|
return f"{entity_type}-{{id}}"
|
||||||
|
return 'entity-{id}'
|
||||||
|
|
||||||
|
def print_report(self):
|
||||||
|
"""Print analysis report."""
|
||||||
|
print(f"\n{'=' * 80}")
|
||||||
|
print(f"Next.js Revalidation Strategy Analysis")
|
||||||
|
print(f"{'=' * 80}\n")
|
||||||
|
|
||||||
|
print(f"Analyzed {len(self.routes)} routes\n")
|
||||||
|
|
||||||
|
# Group by strategy
|
||||||
|
strategies = {}
|
||||||
|
for route in self.routes:
|
||||||
|
strategy = route['recommendation']['strategy']
|
||||||
|
if strategy not in strategies:
|
||||||
|
strategies[strategy] = []
|
||||||
|
strategies[strategy].append(route)
|
||||||
|
|
||||||
|
for strategy, routes in strategies.items():
|
||||||
|
print(f"\n{strategy} Routes ({len(routes)}):")
|
||||||
|
print("-" * 80)
|
||||||
|
for route in routes:
|
||||||
|
rec = route['recommendation']
|
||||||
|
print(f"\nRoute: {route['path']}")
|
||||||
|
print(f" File: {route['file']}")
|
||||||
|
print(f" Strategy: {rec['strategy']}")
|
||||||
|
print(f" Config: {rec['config']}")
|
||||||
|
if rec['cache_tags']:
|
||||||
|
print(f" Cache Tags: {', '.join(rec['cache_tags'])}")
|
||||||
|
print(f" Reasoning: {rec['reasoning']}")
|
||||||
|
|
||||||
|
def export_json(self, output_path: Path):
|
||||||
|
"""Export analysis to JSON file."""
|
||||||
|
with open(output_path, 'w') as f:
|
||||||
|
json.dump(self.routes, f, indent=2)
|
||||||
|
print(f"\nExported analysis to: {output_path}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description='Analyze Next.js routes for revalidation strategies')
|
||||||
|
parser.add_argument('app_dir', help='Path to Next.js app directory')
|
||||||
|
parser.add_argument('--output', '-o', help='Output JSON file path')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
app_dir = Path(args.app_dir)
|
||||||
|
if not app_dir.exists():
|
||||||
|
print(f"Error: Directory not found: {app_dir}")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
analyzer = RouteAnalyzer(app_dir)
|
||||||
|
analyzer.analyze()
|
||||||
|
analyzer.print_report()
|
||||||
|
|
||||||
|
if args.output:
|
||||||
|
analyzer.export_json(Path(args.output))
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
exit(main())
|
||||||
Reference in New Issue
Block a user