fix: 修复 Next.js 15 async params 兼容性问题并更新文档

- 修复关键词词云页面和 API 路由中的 async params 处理
- 将 params 和 searchParams 正确声明为 Promise 类型并添加 await
- 在 CLAUDE.md 中添加 Next.js 15 Breaking Change 说明
- 添加 ESLint/Prettier 配置文档
- 添加 Next.js 15 页面创建指南
- 补充故障排查部分的相关错误处理说明

Co-Authored-By: Claude (glm-4.7) <noreply@anthropic.com>
This commit is contained in:
2026-01-29 17:21:55 +08:00
co-authored by Claude
parent e1f250d397
commit 7955cabea8
3 changed files with 89 additions and 10 deletions
+83 -1
View File
@@ -135,6 +135,22 @@ When updating an existing project, the webhook:
- **tsconfig.json**: ES2017 target, strict mode enabled, noUncheckedIndexedAccess enabled
- **Testing**: Vitest for unit tests, Playwright for E2E tests (configured but not extensively used yet)
#### Next.js 15 Breaking Change: Async Params
**Critical**: Next.js 15 changed `params` and `searchParams` to be **async** (Promise type).
- In page components: `params: Promise<{ locale: string }>` and `searchParams: Promise<{ key?: string }>`
- In API routes: `{ params }: { params: Promise<{ id: string }> }`
- **Always await these params before using them**:
```typescript
// ✅ Correct
const { locale } = await params;
const resolvedSearchParams = await searchParams;
const quarter = resolvedSearchParams.quarter || 'default';
// ❌ Wrong (causes runtime errors in Next.js 15)
const { locale } = params; // params is a Promise, not an object
const quarter = searchParams.quarter;
```
### Project Discovery System
项目发现系统是自动化探索和收录 AI 项目的核心功能,采用**双 Agent 协作架构**实现上下文隔离:
@@ -233,10 +249,23 @@ API Submitter Agent (提交到生产环境 API)
- `/[locale]/keyword-cloud`: 词云展示页面
#### 前端组件
- **Location**: `src/app/[locale]/keyword-cloud/components/`
- `CloudWord`: 单个词汇组件(支持颜色、大小、边框、旋转、悬停弹出框)
- `QuarterNavigator`: 季度导航组件(前后切换)
- `ProgressIndicator`: 进度条组件(显示季度进度)
- `KeywordCloud`: 主容器组件(集成所有子组件)
- `KeywordCloud`: 主容器组件(集成所有子组件,客户端组件
#### 国际化支持
- **消息键**: `src/messages/{locale}.json` 中的 `keywordCloud` 命名空间
- **支持的字段**:
- `metaTitle`/`metaDescription`: SEO 元数据
- `badge`: 页面徽章文本
- `title`/`titleHighlight`: 主标题(支持高亮)
- `subtitle`: 副标题描述
- `loading`/`loadFailed`/`retry`: 加载状态文本
- `hotKeyword`: 热门词汇提示(支持 `{word}` 参数替换)
- **导航菜单**: `navigation.keywordCloud` 键控制导航栏显示
- **添加新翻译**: 更新 `src/messages/zh.json` 和 `src/messages/en.json` 中的 `keywordCloud` 部分
#### 数据访问层
- **Location**: `src/hooks/useKeywordCloud.ts` (服务器端函数)
@@ -332,6 +361,47 @@ Git 提交信息遵循约定式提交格式(详见上方 Code Quality & Standa
- For project data, prefer Chinese as primary language with English as optional
- When creating content, avoid mechanical translation - write naturally for each locale
### Creating New Pages (Next.js 15)
When adding new pages in the App Router, remember that **params and searchParams are Promises**:
```typescript
// Page component structure
interface PageProps {
params: Promise<{ locale: string; id?: string }>;
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}
export default async function MyPage({ params, searchParams }: PageProps) {
// MUST await params and searchParams
const { locale, id } = await params;
const { filter, sort } = await searchParams;
// Now you can use the values
// ...
}
// For generateMetadata
export async function generateMetadata({
params
}: {
params: Promise<{ locale: string }>
}) {
const { locale } = await params;
// ...
}
```
For API routes with dynamic parameters:
```typescript
export async function GET(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
// ...
}
```
## Code Quality & Standards
### TypeScript Configuration
@@ -339,6 +409,12 @@ Git 提交信息遵循约定式提交格式(详见上方 Code Quality & Standa
- Path alias: `@/*` maps to `./src/*`
- Target: ES2017 for modern browser support
### ESLint Configuration
- **Config**: `.eslintrc.json` extends `next/core-web-vitals` and `prettier`
- **Console rules**: `no-console` warns on `console.log` but allows `console.warn` and `console.error`
- **Prettier integration**: `eslint-config-prettier` disables conflicting ESLint rules
- **Prettier config**: `.prettierrc.json` for code formatting
### Validation & Security
- **API Authentication**: Webhook uses timing-safe comparison (`crypto.timingSafeEqual`) to prevent timing attacks
- **Input Validation**: All API inputs use Zod schemas with detailed error messages
@@ -411,6 +487,12 @@ Git 提交信息遵循约定式提交格式(详见上方 Code Quality & Standa
- Restart dev server after schema changes
- Check for TypeScript errors in generated types
**Next.js 15 Async Params Errors**
- If you see errors like `params.locale is undefined` or `Cannot read properties of undefined`, you likely forgot to `await` the params
- Check that all page components properly await: `const { locale } = await params;`
- Check that all API routes properly await: `const { id } = await params;`
- See "Next.js Configuration → Next.js 15 Breaking Change" above for examples
**Discovery Task Failures**
- Check `src/app/api/discovery/lib/discovery-service.ts:16-80` for deduplication logic
- Verify `WEBHOOK_API_KEY` is set for api-submitter-agent
+4 -7
View File
@@ -2,12 +2,8 @@ import { KeywordCloud } from './components/KeywordCloud';
import { getTranslations } from 'next-intl/server';
interface PageProps {
params: {
locale: string;
};
searchParams: {
quarter?: string;
};
params: Promise<{ locale: string }>;
searchParams: Promise<{ quarter?: string }>;
}
export async function generateMetadata({
@@ -26,10 +22,11 @@ export async function generateMetadata({
export default async function KeywordCloudPage({ searchParams, params }: PageProps) {
const { locale } = await params;
const resolvedSearchParams = await searchParams;
const t = await getTranslations('keywordCloud');
// 如果 URL 中有 quarter 参数,使用它;否则使用默认季度
const quarter = searchParams.quarter || '2024-Q1';
const quarter = resolvedSearchParams.quarter || '2024-Q1';
const texts = {
loading: t('loading'),
@@ -9,10 +9,10 @@ export const dynamic = 'force-dynamic';
*/
export async function GET(
request: Request,
{ params }: { params: { quarter: string } }
{ params }: { params: Promise<{ quarter: string }> }
) {
try {
const { quarter } = params;
const { quarter } = await params;
// 验证 quarter 格式
if (!/^\d{4}-Q[1-4]$/.test(quarter)) {