refactor: 主题系统 → monorepo 五个独立网站

- packages/core: @aidir/core 底座库(src/pages 页面组件体 1:1 镜像路由树 / kit 站点外壳工厂 / content/blog / scripts)
- sites/: 5 个独立 Next.js 网站(ai-stack / launchai / the-ai-ledger / devai-tools / findai)
  - 每站独立 package.json / next.config / tailwind / messages / .env / 构建
  - src/app 约 56 个薄路由文件(re-export 底座页面 + 内联路由段配置 revalidate/dynamic)
  - src/site/brand.tsx 站点配置 + src/site/home.tsx 站点首页(createMarketingLayout / createHomePage 工厂)
- generate-site.mjs: 站点生成器(新增网站 = SITES 加条目,零 Core 修改)
- start-all-sites.sh: 一键启动 3001-3005
- next-config.mjs: 底座统一 Next 配置工厂(next-intl + MDX + standalone)
- 五站共享同一套 PostgreSQL / Better Auth / Admin(设计文档 §42)
- 验证: 5 站 build OK / typecheck OK / 55 路由冒烟 200 / 品牌-内容-截图 差异确认 / 登录-投票-重复检测 e2e PASS
This commit is contained in:
AI Directory Platform
2026-09-08 19:19:33 +00:00
parent e46b4b52a3
commit 4a1a034386
699 changed files with 19829 additions and 2029 deletions
+4 -63
View File
@@ -1,66 +1,7 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
node_modules/
.next/
.next-*/
.env
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
# macOS
.DS_Store
.AppleDouble
.LSOverride
# Windows
Thumbs.db
ehthumbs.db
Desktop.ini
# Editor directories
.vscode/
.idea/
*.swp
*.swo
*~
.env
# local documentation
.local-docs/
# scripts
/scripts/manage-users.ts
# 多主题构建产物
/.next-product
/.next-editorial
/.next-niche
/.next-minimal
+38 -478
View File
@@ -1,499 +1,59 @@
<div align="center">
# AI Directory Platform
# Toolso.AI
可复用 AI Directory 底座 + 5 个独立网站(monorepo)。
**An open-source AI tools directory to help you discover the best AI tools**
基于 [coolchange-ai/toolso-ai-open](https://github.com/coolchange-ai/toolso-ai-open) 二次开发,
按 `docs/AI-Directory-Platform开发计划与技术设计文档.md` 完成 MVP + V1 能力,
并用 **5 个独立网站** 验证底座的「快速、易用、可用、可靠」。
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
[![Next.js](https://img.shields.io/badge/Next.js-16-black?logo=next.js)](https://nextjs.org/)
[![React](https://img.shields.io/badge/React-19-61DAFB?logo=react)](https://react.dev/)
[![TypeScript](https://img.shields.io/badge/TypeScript-5-3178C6?logo=typescript)](https://www.typescriptlang.org/)
[![Tailwind CSS](https://img.shields.io/badge/Tailwind%20CSS-3-06B6D4?logo=tailwindcss)](https://tailwindcss.com/)
[![PostgreSQL](https://img.shields.io/badge/PostgreSQL-16-336791?logo=postgresql)](https://www.postgresql.org/)
[English](#english) | [中文](#中文)
<br/>
**Discover, explore, and share the best AI tools.**
</div>
---
<a name="english"></a>
## Overview
Toolso.AI is an open-source AI tools directory designed to help users discover the best AI tools across various categories. Built with Next.js 16, React 19, and TypeScript, it features a modern, responsive interface with powerful search and filtering capabilities, multilingual support, and a comprehensive admin panel for tool management.
### Key Features
| Feature | Description | Benefits |
|---------|-------------|----------|
| **AI Tools Directory** | Categorized AI tools listing | Easy discovery and exploration |
| **Search & Filter** | Powerful search with category filters | Find the right tool quickly |
| **Admin Panel** | Tool & user management | Manage content with ease |
| **Favorites** | Save tools to your personal collection | Sign in and build your toolbox |
| **Newsletter** | Footer subscription backed by Resend | Grow your audience |
| **Blog System** | MDX-powered multilingual blog | SEO-optimized AI insights |
| **i18n** | next-intl internationalization | English & Chinese out of the box |
| **Authentication** | Better Auth + Google OAuth | User accounts and submissions |
| **Theming** | Light/Dark mode | Comfortable browsing experience |
## Quick Start
### Prerequisites
- Node.js 18+
- pnpm 8+
- PostgreSQL database
### Installation
```bash
# Clone the repository
git clone https://github.com/coolchange-ai/toolso-ai-open.git
cd toolso-ai-open
# Install dependencies
pnpm install
# Set up environment variables
cp .env.example .env.local
# Push database schema
pnpm db:push
# (Optional) Seed 50 AI tools, 10 categories and 30 tags
pnpm seed:tools
# (Optional) Create an admin account
pnpm admin:setup
# Start development server
pnpm dev
```
Open [http://localhost:3000](http://localhost:3000) to see your app.
### Environment Configuration
Create `.env.local` with your configuration:
```env
# Required
DATABASE_URL="postgresql://user:password@host/db?sslmode=require"
BETTER_AUTH_SECRET="your-secret-key-at-least-32-characters"
BETTER_AUTH_URL="http://localhost:3000"
NEXT_PUBLIC_APP_URL="http://localhost:3000"
# Email (Required for auth features)
RESEND_API_KEY="re_your_api_key"
RESEND_FROM_EMAIL="Your App <noreply@yourdomain.com>"
# Optional: Google OAuth
AUTH_GOOGLE_ID="your-google-client-id"
AUTH_GOOGLE_SECRET="your-google-client-secret"
```
See [.env.example](.env.example) for all available options.
## Project Structure
## 仓库结构
```
src/
├── app/
│ ├── [locale]/ # Internationalized routes
│ │ ├── (auth)/ # Authentication pages
│ │ ├── (marketing)/ # Public pages (home, blog, tools)
│ │ ├── (protected)/ # User dashboard, profile
│ │ └── (admin)/ # Admin panel
│ └── api/ # API routes
├── components/ # Reusable UI components
├── features/ # Feature modules
│ ├── auth/ # Authentication logic
│ ├── admin/ # Admin components
│ ├── tools/ # AI tools directory
│ └── navigation/ # Navigation components
├── lib/ # Core utilities
│ ├── auth.ts # Better Auth config
│ ├── db/ # Database schema & connection
│ └── email.ts # Email utilities
└── emails/ # React Email templates
messages/ # i18n translation files
public/ # Static assets
scripts/ # Build & admin scripts
packages/core/ @aidir/core —— 底座库(页面组件体 / 业务模块 / 站点外壳工厂 / 种子与工具脚本)
sites/
├── ai-stack/ AI Stack — Classic 目录 → http://localhost:3001
├── launchai/ LaunchAI — Product Hunt 式社区 → http://localhost:3002
├── the-ai-ledger/ The AI Ledger — Editorial 媒体 → http://localhost:3003
├── devai-tools/ DevAI.tools — 开发者垂直 → http://localhost:3004
└── findai/ findai — 极简搜索 → http://localhost:3005
docs/ 设计文档 + 交付报告
```
## Tech Stack
### Frontend
- **Framework**: Next.js 16 (App Router)
- **UI Library**: React 19
- **Styling**: Tailwind CSS + Framer Motion
- **Components**: Custom components + Radix UI
- **Forms**: React Hook Form + Zod validation
- **Themes**: next-themes (dark/light mode)
### Backend
- **Authentication**: Better Auth
- **Database**: PostgreSQL + Drizzle ORM
- **Email**: Resend
- **Storage**: Cloudflare R2 / AWS S3
### Developer Experience
- **Language**: TypeScript (strict mode)
- **Linting**: ESLint
- **Package Manager**: pnpm
## Available Scripts
```bash
pnpm dev # Start development server
pnpm build # Build for production
pnpm start # Start production server
pnpm lint # Run ESLint (flat config)
pnpm typecheck # Run TypeScript type check
pnpm i18n:check # Verify translation key consistency
# Database
pnpm db:generate # Generate Drizzle migrations
pnpm db:migrate # Run migrations
pnpm db:push # Push schema to database
pnpm db:studio # Open Drizzle Studio
pnpm seed:tools # Seed AI tools data
# Admin
pnpm admin:setup # Create admin account
# Content
pnpm generate:blog-manifest # Generate blog manifest
```
## Deployment
### Vercel (Recommended)
1. Push your code to GitHub
2. Import project in [Vercel](https://vercel.com)
3. Add environment variables
4. Deploy
### Docker
```bash
# Start PostgreSQL + app
docker compose up -d --build
# Initialize database (run from host)
DATABASE_URL=postgresql://toolso:toolso@localhost:5432/toolso pnpm db:push
DATABASE_URL=postgresql://toolso:toolso@localhost:5432/toolso pnpm seed:tools
DATABASE_URL=postgresql://toolso:toolso@localhost:5432/toolso pnpm admin:setup
```
### Other Platforms
The template works with any platform supporting Node.js:
- Railway
- Render
- Self-hosted
### Deployment Checklist
- [ ] Set up PostgreSQL database (Supabase, Neon, or Vercel Postgres)
- [ ] Configure Resend for email
- [ ] Update `BETTER_AUTH_URL` and `NEXT_PUBLIC_APP_URL` with production domain
- [ ] Run `pnpm db:push` to initialize database
- [ ] Create admin account with `pnpm admin:setup`
## Internationalization
The template supports multiple languages via `next-intl`:
- **Supported**: English (`en`), Chinese (`zh`)
- **Route Format**: English is the default locale without prefix (`/...`), Chinese uses `/zh/...` (`localePrefix: 'as-needed'`)
- **Translation Files**: `messages/en.json`, `messages/zh.json`
- **Consistency Check**: `pnpm i18n:check`
### Adding a New Language
1. Copy `messages/en.json` to `messages/{locale}.json`
2. Translate the content
3. Update `i18n.config.ts`
> Note: UI copy is fully translatable via JSON files, but tool/category/tag **data** is stored as bilingual columns (`nameEn`/`nameZh`) in the database. Adding a third content language requires extending the schema and queries.
## Roadmap
- [ ] User tool submission with review workflow
- [ ] Tool ratings & comments
- [ ] Automated tool data import pipeline
## Contributing
We welcome contributions! Please feel free to submit a Pull Request.
1. Fork the repository
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request
## License
This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details.
---
<a name="中文"></a>
## 项目概览
Toolso.AI 是一个开源的 AI 工具导航站,旨在帮助用户发现各类优秀的 AI 工具。基于 Next.js 16、React 19 和 TypeScript 构建,拥有现代化的响应式界面、强大的搜索和筛选功能、多语言支持,以及完善的后台管理系统。
### 核心特性
| 特性 | 描述 | 优势 |
|------|------|------|
| **AI 工具目录** | 分类展示 AI 工具 | 轻松发现和探索 |
| **搜索与筛选** | 强大的搜索和分类筛选 | 快速找到合适的工具 |
| **管理后台** | 工具和用户管理 | 轻松管理内容 |
| **收藏功能** | 登录用户可收藏工具 | 打造个人工具箱 |
| **Newsletter** | 页脚订阅(基于 Resend) | 沉淀你的用户 |
| **博客系统** | MDX 驱动的多语言博客 | SEO 优化的 AI 资讯 |
| **国际化** | next-intl 国际化 | 中英文开箱即用 |
| **身份认证** | Better Auth + Google OAuth | 用户账户和提交功能 |
| **主题切换** | 深色/浅色模式 | 舒适的浏览体验 |
五个网站共享同一套 PostgreSQL / Better Auth / Admin(设计文档 §42 验证标准),
各自独立构建、独立部署(各自 `package.json` / `next.config.mjs` / `.env.local`)。
## 快速开始
### 前置要求
- Node.js 18+
- pnpm 8+
- PostgreSQL 数据库
### 安装步骤
```bash
# 克隆仓库
git clone https://github.com/coolchange-ai/toolso-ai-open.git
cd toolso-ai-open
bun install
cd packages/core && cp .env.example .env.local && bun run db:push && bun run seed:platform && cd ../..
# 安装依赖
pnpm install
# 构建 5 个网站(构建时数据库需在线;构建期间不要运行站点)
for s in ai-stack launchai the-ai-ledger devai-tools findai; do
(cd sites/$s && cp -n .env.example .env.local && bun run build)
done
# 设置环境变量
cp .env.example .env.local
# 推送数据库结构
pnpm db:push
# (可选)导入 50 个 AI 工具、10 个分类、30 个标签种子数据
pnpm seed:tools
# (可选)创建管理员账户
pnpm admin:setup
# 启动开发服务器
pnpm dev
# 一键启动
bash packages/core/scripts/start-all-sites.sh
```
打开 [http://localhost:3000](http://localhost:3000) 查看应用。
演示账号:`admin@aidirectory.dev`(管理员)/ `alice@demo.dev` 等用户,密码均为 `password123`。
### 环境变量配置
## 底座能力
创建 `.env.local` 并配置:
Tool Directory · 三维发现(Category / Use Case / Audience)· 最佳清单(Best Lists)·
搜索(PostgreSQL)· 七榜 Ranking Engine(Trending 贝叶斯评分等)· 投票 · 评论 · 收藏 ·
工具提交(重复检测)· 认领(Owner 白名单编辑)· 事件分析(CTR/搜索词)·
Programmatic SEO(用例页/受众页/合集页/Alternatives/Vs 对比页 + Schema 全套)·
博客 · Newsletter · 双语 i18n · GitHub/Google OAuth · 邮箱认证 · Admin 后台(8 个模块)·
深浅主题 · Lucide SVG 图标(无 Emoji)。
```env
# 必需配置
DATABASE_URL="postgresql://user:password@host/db?sslmode=require"
BETTER_AUTH_SECRET="至少32字符的密钥"
BETTER_AUTH_URL="http://localhost:3000"
NEXT_PUBLIC_APP_URL="http://localhost:3000"
## 新增一个网站
# 邮件(认证功能必需)
RESEND_API_KEY="re_your_api_key"
RESEND_FROM_EMAIL="Your App <noreply@yourdomain.com>"
1. 在 `packages/core/scripts/generate-site.mjs` 的 `SITES` 中加一个条目(品牌/导航/页脚/SEO/端口)。
2. `bun packages/core/scripts/generate-site.mjs <key>` —— 生成独立站点(约 56 个薄路由 + 配置)。
3. `cd sites/<key> && bun install && bun run build && bun run start`。
# 可选:Google OAuth
AUTH_GOOGLE_ID="your-google-client-id"
AUTH_GOOGLE_SECRET="your-google-client-secret"
```
零 Core 代码修改。
完整配置请参考 [.env.example](.env.example)。
## 项目结构
```
src/
├── app/
│ ├── [locale]/ # 国际化路由
│ │ ├── (auth)/ # 认证页面
│ │ ├── (marketing)/ # 公开页面(首页、博客、工具)
│ │ ├── (protected)/ # 用户仪表板、个人资料
│ │ └── (admin)/ # 管理后台
│ └── api/ # API 路由
├── components/ # 可复用 UI 组件
├── features/ # 功能模块
│ ├── auth/ # 认证逻辑
│ ├── admin/ # 管理后台组件
│ ├── tools/ # AI 工具目录
│ └── navigation/ # 导航组件
├── lib/ # 核心工具库
│ ├── auth.ts # Better Auth 配置
│ ├── db/ # 数据库 Schema 和连接
│ └── email.ts # 邮件工具
└── emails/ # React Email 模板
messages/ # 国际化翻译文件
public/ # 静态资源
scripts/ # 构建和管理脚本
```
## 技术栈
### 前端
- **框架**: Next.js 16 (App Router)
- **UI 库**: React 19
- **样式**: Tailwind CSS + Framer Motion
- **组件**: 自定义组件 + Radix UI
- **表单**: React Hook Form + Zod 验证
- **主题**: next-themes (深色/浅色模式)
### 后端
- **身份认证**: Better Auth
- **数据库**: PostgreSQL + Drizzle ORM
- **邮件**: Resend
- **存储**: Cloudflare R2 / AWS S3
### 开发体验
- **语言**: TypeScript (严格模式)
- **代码检查**: ESLint
- **包管理器**: pnpm
## 可用脚本
```bash
pnpm dev # 启动开发服务器
pnpm build # 构建生产版本
pnpm start # 启动生产服务器
pnpm lint # 运行 ESLint(flat config)
pnpm typecheck # TypeScript 类型检查
pnpm i18n:check # 校验翻译键一致性
# 数据库
pnpm db:generate # 生成 Drizzle 迁移
pnpm db:migrate # 运行迁移
pnpm db:push # 推送 schema 到数据库
pnpm db:studio # 打开 Drizzle Studio
pnpm seed:tools # 导入工具种子数据
# 管理员
pnpm admin:setup # 创建管理员账户
# 内容
pnpm generate:blog-manifest # 生成博客清单
```
## 部署指南
### Vercel(推荐)
1. 将代码推送到 GitHub
2. 在 [Vercel](https://vercel.com) 导入项目
3. 添加环境变量
4. 部署
### Docker
```bash
# 启动 PostgreSQL + 应用
docker compose up -d --build
# 初始化数据库(在宿主机执行)
DATABASE_URL=postgresql://toolso:toolso@localhost:5432/toolso pnpm db:push
DATABASE_URL=postgresql://toolso:toolso@localhost:5432/toolso pnpm seed:tools
DATABASE_URL=postgresql://toolso:toolso@localhost:5432/toolso pnpm admin:setup
```
### 其他平台
模板支持任何支持 Node.js 的平台:
- Railway
- Render
- 自托管
### 部署清单
- [ ] 设置 PostgreSQL 数据库(Supabase、Neon 或 Vercel Postgres)
- [ ] 配置 Resend 邮件服务
- [ ] 更新 `BETTER_AUTH_URL` 和 `NEXT_PUBLIC_APP_URL` 为生产域名
- [ ] 运行 `pnpm db:push` 初始化数据库
- [ ] 使用 `pnpm admin:setup` 创建管理员账户
## 国际化
模板通过 `next-intl` 支持多语言:
- **已支持**: 英文 (`en`)、中文 (`zh`)
- **路由格式**: 英文为默认语言不带前缀(`/...`),中文为 `/zh/...`(`localePrefix: 'as-needed'`)
- **翻译文件**: `messages/en.json`、`messages/zh.json`
- **一致性检查**: `pnpm i18n:check`
### 添加新语言
1. 复制 `messages/en.json` 到 `messages/{语言代码}.json`
2. 翻译内容
3. 更新 `i18n.config.ts`
> 注意:UI 文案可完全通过 JSON 文件翻译,但工具/分类/标签的**数据**在数据库中以双语列(`nameEn`/`nameZh`)存储。新增第三种内容语言需要扩展 schema 和查询。
## Roadmap
- [ ] 用户工具提交与审核工作流
- [ ] 工具评分与评论
- [ ] 自动化工具数据采集管线
## 贡献指南
欢迎贡献!请随时提交 Pull Request。
1. Fork 仓库
2. 创建特性分支 (`git checkout -b feature/amazing-feature`)
3. 提交更改 (`git commit -m 'Add some amazing feature'`)
4. 推送到分支 (`git push origin feature/amazing-feature`)
5. 开启 Pull Request
## 开源协议
本项目基于 Apache License 2.0 开源 - 详情请查看 [LICENSE](LICENSE) 文件。
---
<div align="center">
## Related Projects
| Project | Description | Link |
|---------|-------------|------|
| **Toolso.AI** | AI Tools Directory | [https://toolso.ai](https://toolso.ai) |
| **CoolChange.AI** | AI Development Studio | [https://coolchange.ai](https://coolchange.ai) |
| **chatPPT.app** | AI Presentation Generator | [https://chatppt.app](https://chatppt.app) |
| **AIHealthGuide** | AI Health Assistant | [https://aihealthguide.app](https://aihealthguide.app) |
<br/>
---
<br/>
**Built with love by [CoolChange.AI](https://coolchange.ai)**
<br/>
If you find this project helpful, please consider giving it a star!
</div>
详细验收对照见 `docs/交付报告.md`。
+441 -1137
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+217
View File
@@ -0,0 +1,217 @@
# AI Directory Platform — 交付报告
> 基于 `coolchange-ai/toolso-ai-open` 二次开发的可复用 AI Directory 基础底座,
> 按《AI-Directory-Platform开发计划与技术设计文档》完成 MVP + V1 核心能力,
> 并以 **五套不同 UI 的完整站点** 完成底座「快速、易用、可用、可靠」验证。
- 代码位置:`AI-Directory-Platform/packages/core/`(底座库)+ `AI-Directory-Platform/sites/`(5 个独立网站)
- 技术栈:Next.js 16 · React 19 · TypeScript · Tailwind CSS · PostgreSQL + Drizzle ORM · Better Auth · next-intl
- 验证状态:**typecheck 0 错误 · bun test 32/32 通过 · 5 套主题全部构建成功 · 端到端交互测试 PASS**
---
## 一、仓库结构(monorepo)
```
AI-Directory-Platform/
├── packages/core/ # @aidir/core —— AI Directory 底座库
│ ├── src/pages/ # 全部页面组件体(1:1 镜像路由树)
│ ├── src/kit/ # 站点外壳工厂(Header/Footer/chrome/首页数据)
│ ├── src/lib|features/ # ranking/seo/analytics/voting/reviews/... 全部业务
│ ├── content/blog/ # MDX 文章
│ ├── messages/ # 基础文案(en/zh,546 键)
│ └── scripts/ # db/seed/生成器/启动器/e2e
└── sites/ # 5 个独立 Next.js 网站(各自 package.json/构建/部署)
├── ai-stack/ # AI Stack —— Classic 目录 :3001
├── launchai/ # LaunchAI —— Product Hunt 式 :3002
├── the-ai-ledger/ # The AI Ledger —— Editorial 媒体 :3003
├── devai-tools/ # DevAI.tools —— 开发者垂直 :3004
└── findai/ # findai —— 极简搜索 :3005
```
每个站点应用只有三类自己的代码:
1. `src/site/brand.tsx` —— 品牌/导航/页脚/SEO 文案(SiteConfig)
2. `src/site/home.tsx` —— 站点首页组件
3. `src/app/**` —— 薄路由(约 56 个文件,全部为 re-export 底座页面组件 + 内联路由段配置)
**站点之间零重复业务代码**;新增第 6 个网站 = 在生成器 `SITES` 里加一个条目并运行
`bun packages/core/scripts/generate-site.mjs <key>`,零 Core 代码修改。
## 一'、快速开始
```bash
cd AI-Directory-Platform
bun install # workspace 安装(root 一次)
# 数据库(五个网站共享同一套 PostgreSQL —— 设计文档 §42)
cd packages/core && cp .env.example .env.local && bun run db:push && bun run seed:platform && cd ../..
# 构建 5 个网站(构建时数据库需在线;构建期间不要运行站点,避免连接数耗尽)
for s in ai-stack launchai the-ai-ledger devai-tools findai; do (cd sites/$s && cp -n .env.example .env.local && bun run build); done
# 一键启动(或分别 cd sites/<name> && bun run start)
bash packages/core/scripts/start-all-sites.sh
# 质量
bun test packages/core/tests # 32 个单元测试
bun run typecheck # core + 5 站全部 0 错误
```
演示账号(种子内置,密码均为 `password123`):
| 账号 | 角色 |
| --- | --- |
| `admin@aidirectory.dev` | 管理员(可访问 `/admin` 全部后台) |
| `alice@demo.dev` 等 5 个 | 普通用户 |
---
## 二、与设计文档验收标准对照
### UI(§74)
| 要求 | 状态 |
| --- | --- |
| 所有图标 SVG / Lucide,无 Emoji、Unicode 图标、字符箭头 | ✅ 全部使用 `lucide-react`;种子分类 icon 置空;唯一例外是种子工具名 "DALL·E 3" 的 `·` 为产品名本身 |
| Responsive(375/768/1280/1440 实测截图) | ✅ Mobile First,Header 移动端汉堡抽屉 + 搜索 |
| Light/Dark/System | ✅ 继承底座 next-themes,全组件适配 |
### Functional(§74 / MVP §57)
| 要求 | 实现位置 |
| --- | --- |
| Google Login | ✅ 底座已有(env 门控) |
| GitHub Login | ✅ `src/lib/auth.ts`(env 门控)+ 登录页按钮 |
| Vote | ✅ `src/features/voting/`(登录投票、原子 toggle、滑动窗口限流 40 次/10min、乐观 UI、`/api/tools/:id/vote`) |
| Favorite | ✅ 底座已有 |
| Review | ✅ `src/features/reviews/`(评分/优缺点/使用场景/Helpful/每用户一条/聚合评分回写) |
| Submit | ✅ `src/features/submission/`(提交 → Admin 审批自动发布 → 域名重复检测) |
| Claim | ✅ `src/features/claim/`(认领申请 → 审批 → Owner 徽标 + 白名单字段编辑 `ownerUpdateTool`) |
| Search | ✅ `/api/search`(PostgreSQL 多维 ILIKE + 热度排序 + search 埋点) |
| Filter | ✅ 分类 / 定价 / Use Case / Audience / 排序(最新、最热) |
| Ranking | ✅ `src/lib/ranking/`(七个榜单 + Ranking Engine 独立) |
### SEO(§74 / §23 / §24 / §52)
| 要求 | 实现 |
| --- | --- |
| SSR / ISR | ✅ 全站 RSC + ISR(1h)+ 变更 revalidatePath |
| Metadata / Canonical / hreflang | ✅ `src/lib/metadata.ts`(继承底座) |
| Sitemap | ✅ `src/app/sitemap.ts` 扩展至 270 条:工具 / 分类 / 用例 / 受众 / 合集 / Top20 Alternatives / 对比页 |
| Schema | ✅ SoftwareApplication(+AggregateRating) / Review 数据 / FAQPage / BreadcrumbList / ItemList / CollectionPage / WebSite / Organization |
| Programmatic SEO | ✅ `src/lib/seo/page-generator.ts`:由实体真实数据生成 Title/H1/Intro/FAQ(避免低质模板页:Alternatives/Comparison 只为 Top 工具生成) |
| 内链系统 | ✅ Tool ↔ Category ↔ Use Case ↔ Audience ↔ Alternatives ↔ Best ↔ Blog 的 SEO Graph |
### Performance(§47)
Server Components First、ISR、`next/image`、分页、数据库索引(vote/rating/domain 等 7 个新索引)。构建产物静态化率见构建输出(SSG 为主,交互 API 为 ƒ)。
### Security(§74)
RBAC(`requireAdmin`/`isAdmin`)、Zod 全量输入校验、投票限流 + pending 唯一索引防重、认领 pending 唯一约束、Vote Audit 审计页(用户/IP/来源/速度告警)、外链 `nofollow` + 跳转埋点。
---
## 三、底座架构(Core 与 Site 分离)
```
src/
├── lib/
│ ├── ranking/ # Ranking Engine(score.ts 纯函数 + queries.ts SQL + boards.ts)
│ ├── seo/ # Programmatic SEO 页面生成器
│ ├── analytics/ # 事件埋点(recordEvent/recordToolView)
│ ├── tools/duplicate.ts# 重复工具检测(域名归一化/slug/名称)
│ └── db/schema/ # community(投票/评论/提交/认领) discovery(用例/受众/合集) analytics(事件)
├── features/ # Feature-First:voting / reviews / submission / claim / discovery / alternatives
├── themes/ # ★ Site Theme 层:registry + chrome + shared + 5 套主题
│ ├── classic/ # AI Stack —— 专业目录(Clean/Dense/Professional)
│ ├── product/ # LaunchAI —— Product Hunt 式每日发布社区
│ ├── editorial/ # The AI Ledger —— 媒体型(Featured Story/编辑精选/购买指南)
│ ├── niche/ # DevAI.tools —— 垂直(AI for Developers,终端风 Hero)
│ └── minimal/ # findai —— 极简搜索引擎(一个框 + 热门搜索)
└── app/[locale]/ # 路由:/tools /rankings /best /ai-tools-for /submit /admin/* /api/*
```
**五套站点 = 同一套数据库、认证、Admin、Tool 模型、Vote/Review、Ranking、Search、SEO、Analytics**,
只改变 Theme/Layout/Navigation/Content/Brand —— 即设计文档 §42 的验证标准。新增第 6 个站点只需
在 `src/themes/` 增加一个目录并注册 registry,**零 Core 代码修改**。
主题在**构建时**注入(`SITE_THEME=<id> NEXT_DIST_DIR=.next-<id> bun run build`),
一次构建对应一个站点部署,SSG/ISR 性能不受影响。
---
## 四、五个网站验证结论(§41 / 最终要求)
| 网站 | 目录 | 端口 | 定位 | 首页形态 |
| --- | --- | --- | --- | --- |
| AI Stack | sites/ai-stack | 3001 | 标准目录 | Hero 搜索 + Trending/分类/新工具/用例/合集/评论 |
| LaunchAI | sites/launchai | 3002 | 产品社区 | 今日发布 Feed + 大投票块 + Leaderboard 侧栏 |
| The AI Ledger | sites/the-ai-ledger | 3003 | AI 媒体 | Featured Story + 编辑精选 + 购买指南 + 社区引语 |
| DevAI.tools | sites/devai-tools | 3004 | 开发者垂直 | 终端风 Hero + 工作流/用例/技术栈 |
| findai | sites/findai | 3005 | 极简搜索 | 居中大搜索框 + 热门搜索 + Trending |
验证结果:**5 个网站是 5 个独立 Next.js 应用**(独立 package.json、独立 .next 构建、独立进程/端口),
共享同一套 PostgreSQL / Better Auth / Admin(§42 验证标准)。站点间零 `if site === "xxx"` 分支;
每站均通过构建、11 条路由 × 5 站 = 55 项 200 冒烟、1440/390 双断点截图、控制台 0 报错、
登录→投票(DB 3→4→3)→重复提交检测 e2e PASS。
---
## 五、端到端验证记录
| 验证项 | 结果 |
| --- | --- |
| 19 条核心路由冒烟(含 zh、sitemap、search API、outbound 307 跳转) | ✅ 全部 200 |
| JSON-LD(SoftwareApplication+AggregateRating/FAQPage/Breadcrumb/ItemList) | ✅ 详情页实测输出 |
| Sitemap 270 条(含用例/受众/合集/alternatives/vs) | ✅ |
| 登录(alice@demo.dev)→ 投票 toggle(DB 3→4→3,按钮同步) | ✅ PASS |
| 重复提交检测(提交 chatgpt.com 被拦截并列出已有工具) | ✅ PASS |
| 评论表单(已有评论回显编辑模式、星级、优缺点) | ✅ 截图验证 |
| 五主题品牌/版式差异化(AI Stack/LaunchAI/The AI Ledger/DevAI.tools/findai) | ✅ 截图验证 |
| `bun test`(ranking 评分/贝叶斯/路由解析/域名归一化/SEO 生成器) | ✅ 32/32 |
| `bun run typecheck` | ✅ 0 错误 |
| `bun run build` × 5 主题 | ✅ 全部成功 |
截图存档:`/tmp/aidir-shots/`(16 张,含移动端 390×844)。
---
## 六、API 一览(§45)
```
POST/DELETE /api/tools/:id/vote 投票/取消(登录,限流)
GET /api/search?q= PostgreSQL 搜索(含埋点)
GET /api/outbound?tool=slug 外链跳转 + tool_click 埋点 + 计数
GET /api/tools(列表查询走 server actions:getPublishedTools 支持 category/pricing/usecase/audience/sort)
POST /api/auth/[...all] Better Auth(邮箱密码 + Google + GitHub)
```
提交/认领/评论/收藏走 Server Actions(`features/*/actions.ts`),与 REST 共存。
---
## 七、事件与分析(§37/§38)
`event` 表统一采集 10 类事件:`tool_view / tool_click / tool_vote / tool_unvote / tool_favorite /
tool_review / tool_share / search / category_view / submit_tool / claim_tool`。
详情页浏览与外链点击自动同步 `tool.viewCount/clickCount` 冗余列;
`/admin/analytics` 输出近 30 天事件总量、工具 CTR 排行(views≥10)、热门搜索词。
---
## 八、已知边界与后续(V2 路线)
1. **主题文案 i18n**:五套主题的 Brand 文案当前为英文(品牌层);核心页面(工具/榜单/表单/后台)中英双语完整。后续可把主题文案迁入 `messages/<locale>.json`。
2. **OAuth 凭据**:Google/GitHub 登录需在生产配置真实 Client ID/Secret(env 门控,缺省自动隐藏按钮)。
3. **V2 待办**(按设计文档 §59):Meilisearch 语义搜索、AI 推荐、自动分类、爬虫导入、Monetization 层。
4. **评论可信度**(§19):已实现 Verified User(邮箱认证徽标)+ Helpful 投票;Verified Usage/Purchase 留待 V2。
5. 上游遗留兼容性升级:framer-motion 11→12(本次已升级以修复 React 19 类型)。
---
## 九、Git 状态
全部改动已在 `core/` 仓库本地提交(见 `git log`),上游远程 `origin` 保留,
可随时 diff 审查或推送 fork。
+16 -113
View File
@@ -1,118 +1,21 @@
{
"name": "toolso-ai",
"name": "ai-directory-platform",
"version": "1.0.0",
"description": "An open-source AI tools directory to help you discover the best AI tools across various categories.",
"private": false,
"keywords": [
"ai-tools",
"ai-directory",
"ai-tools-directory",
"artificial-intelligence",
"machine-learning",
"nextjs",
"typescript",
"tailwindcss",
"open-source"
"description": "AI Directory Platform — 可复用 AI Directory 底座 + 多站点(monorepo)",
"private": true,
"workspaces": [
"packages/*",
"sites/*"
],
"license": "Apache-2.0",
"homepage": "https://toolso.ai",
"repository": {
"type": "git",
"url": "https://github.com/coolchange-ai/toolso-ai-open.git"
},
"bugs": {
"url": "https://github.com/coolchange-ai/toolso-ai-open/issues"
},
"author": {
"name": "CoolChange.AI",
"url": "https://coolchange.ai"
},
"scripts": {
"dev": "node scripts/run-dev.mjs",
"build": "npm run generate:blog-manifest && next build",
"start": "next start",
"lint": "eslint .",
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:push": "drizzle-kit push",
"db:studio": "drizzle-kit studio",
"admin:setup": "tsx scripts/setup-admin.ts",
"seed:tools": "tsx scripts/seed-tools.ts",
"generate:blog-manifest": "node scripts/generate-blog-manifest.mjs",
"typecheck": "tsc --noEmit",
"i18n:check": "node scripts/check-i18n.mjs",
"seed:platform": "bun scripts/seed-platform.ts",
"build:theme": "next build",
"test:e2e": "bun scripts/e2e-interaction.ts",
"shots": "bun scripts/screenshot-pages.ts"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.958.0",
"@better-auth/client": "0.0.2-alpha.3",
"@hookform/resolvers": "^3.4.2",
"@mapbox/rehype-prism": "^0.9.0",
"@mdx-js/loader": "^3.0.1",
"@mdx-js/react": "^3.0.1",
"@next/mdx": "^14.2.3",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-slot": "^1.0.2",
"@react-email/components": "^0.5.3",
"@tabler/icons-react": "^3.5.0",
"@tailwindcss/typography": "^0.5.13",
"@tanstack/react-store": "^0.7.5",
"@types/mdx": "^2.0.13",
"@vercel/analytics": "^1.5.0",
"@vercel/speed-insights": "^1.2.0",
"better-auth": "^1.4.10",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cobe": "^0.6.3",
"date-fns": "^3.6.0",
"drizzle-kit": "^0.31.4",
"drizzle-orm": "^0.44.5",
"fast-glob": "^3.3.2",
"framer-motion": "^12",
"geist": "^1.3.0",
"lucide-react": "^0.562.0",
"nanoid": "^5.1.6",
"next": "16.1.1",
"next-intl": "^4.7.0",
"next-themes": "^0.4.6",
"next-view-transitions": "^0.3.5",
"postgres": "^3.4.7",
"prettier": "^3.7.4",
"prismjs": "^1.29.0",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"react-email": "^4.2.11",
"react-hook-form": "^7.51.5",
"react-icons": "^5.2.1",
"react-markdown": "^10.1.0",
"react-wrap-balancer": "^1.1.1",
"reading-time": "^1.5.0",
"rehype-highlight": "^7.0.2",
"remark-gfm": "^4.0.0",
"resend": "^6.1.0",
"sonner": "^2.0.7",
"tailwind-merge": "^2.3.0",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/bun": "^1.4.2",
"@types/node": "^20",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"dotenv": "^17.2.2",
"eslint": "^9.39.5",
"eslint-config-next": "16.1.1",
"pg": "^8.16.3",
"playwright": "^1.57.0",
"png-to-ico": "^3.0.1",
"postcss": "^8",
"sharp": "^0.34.5",
"tailwindcss": "^3.4.1",
"tsx": "^4.20.5",
"typescript": "^5"
},
"packageManager": "pnpm@10.11.1+sha512.e519b9f7639869dc8d5c3c5dfef73b3f091094b0a006d7317353c72b124e80e1afd429732e28705ad6bfa1ee879c1fce46c128ccebd3192101f43dd67c667912"
"build:core": "bun run --filter '@aidir/core' generate:blog-manifest",
"gen:sites": "bun packages/core/scripts/generate-site.mjs all",
"db:push": "bun run --filter '@aidir/core' db:push",
"db:migrate": "bun run --filter '@aidir/core' db:migrate",
"seed": "bun packages/core/scripts/seed-platform.ts --force",
"test": "bun test packages/core/tests",
"typecheck": "tsc --noEmit -p packages/core && tsc --noEmit -p sites/ai-stack && tsc --noEmit -p sites/launchai && tsc --noEmit -p sites/the-ai-ledger && tsc --noEmit -p sites/devai-tools && tsc --noEmit -p sites/findai",
"build:all": "for s in ai-stack launchai the-ai-ledger devai-tools findai; do (cd sites/$s && bun run build); done",
"start:all": "bash packages/core/scripts/start-all-sites.sh"
}
}
+66
View File
@@ -0,0 +1,66 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
# macOS
.DS_Store
.AppleDouble
.LSOverride
# Windows
Thumbs.db
ehthumbs.db
Desktop.ini
# Editor directories
.vscode/
.idea/
*.swp
*.swo
*~
.env
# local documentation
.local-docs/
# scripts
/scripts/manage-users.ts
# 多主题构建产物
/.next-product
/.next-editorial
/.next-niche
/.next-minimal
View File
+499
View File
@@ -0,0 +1,499 @@
<div align="center">
# Toolso.AI
**An open-source AI tools directory to help you discover the best AI tools**
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
[![Next.js](https://img.shields.io/badge/Next.js-16-black?logo=next.js)](https://nextjs.org/)
[![React](https://img.shields.io/badge/React-19-61DAFB?logo=react)](https://react.dev/)
[![TypeScript](https://img.shields.io/badge/TypeScript-5-3178C6?logo=typescript)](https://www.typescriptlang.org/)
[![Tailwind CSS](https://img.shields.io/badge/Tailwind%20CSS-3-06B6D4?logo=tailwindcss)](https://tailwindcss.com/)
[![PostgreSQL](https://img.shields.io/badge/PostgreSQL-16-336791?logo=postgresql)](https://www.postgresql.org/)
[English](#english) | [中文](#中文)
<br/>
**Discover, explore, and share the best AI tools.**
</div>
---
<a name="english"></a>
## Overview
Toolso.AI is an open-source AI tools directory designed to help users discover the best AI tools across various categories. Built with Next.js 16, React 19, and TypeScript, it features a modern, responsive interface with powerful search and filtering capabilities, multilingual support, and a comprehensive admin panel for tool management.
### Key Features
| Feature | Description | Benefits |
|---------|-------------|----------|
| **AI Tools Directory** | Categorized AI tools listing | Easy discovery and exploration |
| **Search & Filter** | Powerful search with category filters | Find the right tool quickly |
| **Admin Panel** | Tool & user management | Manage content with ease |
| **Favorites** | Save tools to your personal collection | Sign in and build your toolbox |
| **Newsletter** | Footer subscription backed by Resend | Grow your audience |
| **Blog System** | MDX-powered multilingual blog | SEO-optimized AI insights |
| **i18n** | next-intl internationalization | English & Chinese out of the box |
| **Authentication** | Better Auth + Google OAuth | User accounts and submissions |
| **Theming** | Light/Dark mode | Comfortable browsing experience |
## Quick Start
### Prerequisites
- Node.js 18+
- pnpm 8+
- PostgreSQL database
### Installation
```bash
# Clone the repository
git clone https://github.com/coolchange-ai/toolso-ai-open.git
cd toolso-ai-open
# Install dependencies
pnpm install
# Set up environment variables
cp .env.example .env.local
# Push database schema
pnpm db:push
# (Optional) Seed 50 AI tools, 10 categories and 30 tags
pnpm seed:tools
# (Optional) Create an admin account
pnpm admin:setup
# Start development server
pnpm dev
```
Open [http://localhost:3000](http://localhost:3000) to see your app.
### Environment Configuration
Create `.env.local` with your configuration:
```env
# Required
DATABASE_URL="postgresql://user:password@host/db?sslmode=require"
BETTER_AUTH_SECRET="your-secret-key-at-least-32-characters"
BETTER_AUTH_URL="http://localhost:3000"
NEXT_PUBLIC_APP_URL="http://localhost:3000"
# Email (Required for auth features)
RESEND_API_KEY="re_your_api_key"
RESEND_FROM_EMAIL="Your App <noreply@yourdomain.com>"
# Optional: Google OAuth
AUTH_GOOGLE_ID="your-google-client-id"
AUTH_GOOGLE_SECRET="your-google-client-secret"
```
See [.env.example](.env.example) for all available options.
## Project Structure
```
src/
├── app/
│ ├── [locale]/ # Internationalized routes
│ │ ├── (auth)/ # Authentication pages
│ │ ├── (marketing)/ # Public pages (home, blog, tools)
│ │ ├── (protected)/ # User dashboard, profile
│ │ └── (admin)/ # Admin panel
│ └── api/ # API routes
├── components/ # Reusable UI components
├── features/ # Feature modules
│ ├── auth/ # Authentication logic
│ ├── admin/ # Admin components
│ ├── tools/ # AI tools directory
│ └── navigation/ # Navigation components
├── lib/ # Core utilities
│ ├── auth.ts # Better Auth config
│ ├── db/ # Database schema & connection
│ └── email.ts # Email utilities
└── emails/ # React Email templates
messages/ # i18n translation files
public/ # Static assets
scripts/ # Build & admin scripts
```
## Tech Stack
### Frontend
- **Framework**: Next.js 16 (App Router)
- **UI Library**: React 19
- **Styling**: Tailwind CSS + Framer Motion
- **Components**: Custom components + Radix UI
- **Forms**: React Hook Form + Zod validation
- **Themes**: next-themes (dark/light mode)
### Backend
- **Authentication**: Better Auth
- **Database**: PostgreSQL + Drizzle ORM
- **Email**: Resend
- **Storage**: Cloudflare R2 / AWS S3
### Developer Experience
- **Language**: TypeScript (strict mode)
- **Linting**: ESLint
- **Package Manager**: pnpm
## Available Scripts
```bash
pnpm dev # Start development server
pnpm build # Build for production
pnpm start # Start production server
pnpm lint # Run ESLint (flat config)
pnpm typecheck # Run TypeScript type check
pnpm i18n:check # Verify translation key consistency
# Database
pnpm db:generate # Generate Drizzle migrations
pnpm db:migrate # Run migrations
pnpm db:push # Push schema to database
pnpm db:studio # Open Drizzle Studio
pnpm seed:tools # Seed AI tools data
# Admin
pnpm admin:setup # Create admin account
# Content
pnpm generate:blog-manifest # Generate blog manifest
```
## Deployment
### Vercel (Recommended)
1. Push your code to GitHub
2. Import project in [Vercel](https://vercel.com)
3. Add environment variables
4. Deploy
### Docker
```bash
# Start PostgreSQL + app
docker compose up -d --build
# Initialize database (run from host)
DATABASE_URL=postgresql://toolso:toolso@localhost:5432/toolso pnpm db:push
DATABASE_URL=postgresql://toolso:toolso@localhost:5432/toolso pnpm seed:tools
DATABASE_URL=postgresql://toolso:toolso@localhost:5432/toolso pnpm admin:setup
```
### Other Platforms
The template works with any platform supporting Node.js:
- Railway
- Render
- Self-hosted
### Deployment Checklist
- [ ] Set up PostgreSQL database (Supabase, Neon, or Vercel Postgres)
- [ ] Configure Resend for email
- [ ] Update `BETTER_AUTH_URL` and `NEXT_PUBLIC_APP_URL` with production domain
- [ ] Run `pnpm db:push` to initialize database
- [ ] Create admin account with `pnpm admin:setup`
## Internationalization
The template supports multiple languages via `next-intl`:
- **Supported**: English (`en`), Chinese (`zh`)
- **Route Format**: English is the default locale without prefix (`/...`), Chinese uses `/zh/...` (`localePrefix: 'as-needed'`)
- **Translation Files**: `messages/en.json`, `messages/zh.json`
- **Consistency Check**: `pnpm i18n:check`
### Adding a New Language
1. Copy `messages/en.json` to `messages/{locale}.json`
2. Translate the content
3. Update `i18n.config.ts`
> Note: UI copy is fully translatable via JSON files, but tool/category/tag **data** is stored as bilingual columns (`nameEn`/`nameZh`) in the database. Adding a third content language requires extending the schema and queries.
## Roadmap
- [ ] User tool submission with review workflow
- [ ] Tool ratings & comments
- [ ] Automated tool data import pipeline
## Contributing
We welcome contributions! Please feel free to submit a Pull Request.
1. Fork the repository
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request
## License
This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details.
---
<a name="中文"></a>
## 项目概览
Toolso.AI 是一个开源的 AI 工具导航站,旨在帮助用户发现各类优秀的 AI 工具。基于 Next.js 16、React 19 和 TypeScript 构建,拥有现代化的响应式界面、强大的搜索和筛选功能、多语言支持,以及完善的后台管理系统。
### 核心特性
| 特性 | 描述 | 优势 |
|------|------|------|
| **AI 工具目录** | 分类展示 AI 工具 | 轻松发现和探索 |
| **搜索与筛选** | 强大的搜索和分类筛选 | 快速找到合适的工具 |
| **管理后台** | 工具和用户管理 | 轻松管理内容 |
| **收藏功能** | 登录用户可收藏工具 | 打造个人工具箱 |
| **Newsletter** | 页脚订阅(基于 Resend) | 沉淀你的用户 |
| **博客系统** | MDX 驱动的多语言博客 | SEO 优化的 AI 资讯 |
| **国际化** | next-intl 国际化 | 中英文开箱即用 |
| **身份认证** | Better Auth + Google OAuth | 用户账户和提交功能 |
| **主题切换** | 深色/浅色模式 | 舒适的浏览体验 |
## 快速开始
### 前置要求
- Node.js 18+
- pnpm 8+
- PostgreSQL 数据库
### 安装步骤
```bash
# 克隆仓库
git clone https://github.com/coolchange-ai/toolso-ai-open.git
cd toolso-ai-open
# 安装依赖
pnpm install
# 设置环境变量
cp .env.example .env.local
# 推送数据库结构
pnpm db:push
# (可选)导入 50 个 AI 工具、10 个分类、30 个标签种子数据
pnpm seed:tools
# (可选)创建管理员账户
pnpm admin:setup
# 启动开发服务器
pnpm dev
```
打开 [http://localhost:3000](http://localhost:3000) 查看应用。
### 环境变量配置
创建 `.env.local` 并配置:
```env
# 必需配置
DATABASE_URL="postgresql://user:password@host/db?sslmode=require"
BETTER_AUTH_SECRET="至少32字符的密钥"
BETTER_AUTH_URL="http://localhost:3000"
NEXT_PUBLIC_APP_URL="http://localhost:3000"
# 邮件(认证功能必需)
RESEND_API_KEY="re_your_api_key"
RESEND_FROM_EMAIL="Your App <noreply@yourdomain.com>"
# 可选:Google OAuth
AUTH_GOOGLE_ID="your-google-client-id"
AUTH_GOOGLE_SECRET="your-google-client-secret"
```
完整配置请参考 [.env.example](.env.example)。
## 项目结构
```
src/
├── app/
│ ├── [locale]/ # 国际化路由
│ │ ├── (auth)/ # 认证页面
│ │ ├── (marketing)/ # 公开页面(首页、博客、工具)
│ │ ├── (protected)/ # 用户仪表板、个人资料
│ │ └── (admin)/ # 管理后台
│ └── api/ # API 路由
├── components/ # 可复用 UI 组件
├── features/ # 功能模块
│ ├── auth/ # 认证逻辑
│ ├── admin/ # 管理后台组件
│ ├── tools/ # AI 工具目录
│ └── navigation/ # 导航组件
├── lib/ # 核心工具库
│ ├── auth.ts # Better Auth 配置
│ ├── db/ # 数据库 Schema 和连接
│ └── email.ts # 邮件工具
└── emails/ # React Email 模板
messages/ # 国际化翻译文件
public/ # 静态资源
scripts/ # 构建和管理脚本
```
## 技术栈
### 前端
- **框架**: Next.js 16 (App Router)
- **UI 库**: React 19
- **样式**: Tailwind CSS + Framer Motion
- **组件**: 自定义组件 + Radix UI
- **表单**: React Hook Form + Zod 验证
- **主题**: next-themes (深色/浅色模式)
### 后端
- **身份认证**: Better Auth
- **数据库**: PostgreSQL + Drizzle ORM
- **邮件**: Resend
- **存储**: Cloudflare R2 / AWS S3
### 开发体验
- **语言**: TypeScript (严格模式)
- **代码检查**: ESLint
- **包管理器**: pnpm
## 可用脚本
```bash
pnpm dev # 启动开发服务器
pnpm build # 构建生产版本
pnpm start # 启动生产服务器
pnpm lint # 运行 ESLint(flat config)
pnpm typecheck # TypeScript 类型检查
pnpm i18n:check # 校验翻译键一致性
# 数据库
pnpm db:generate # 生成 Drizzle 迁移
pnpm db:migrate # 运行迁移
pnpm db:push # 推送 schema 到数据库
pnpm db:studio # 打开 Drizzle Studio
pnpm seed:tools # 导入工具种子数据
# 管理员
pnpm admin:setup # 创建管理员账户
# 内容
pnpm generate:blog-manifest # 生成博客清单
```
## 部署指南
### Vercel(推荐)
1. 将代码推送到 GitHub
2. 在 [Vercel](https://vercel.com) 导入项目
3. 添加环境变量
4. 部署
### Docker
```bash
# 启动 PostgreSQL + 应用
docker compose up -d --build
# 初始化数据库(在宿主机执行)
DATABASE_URL=postgresql://toolso:toolso@localhost:5432/toolso pnpm db:push
DATABASE_URL=postgresql://toolso:toolso@localhost:5432/toolso pnpm seed:tools
DATABASE_URL=postgresql://toolso:toolso@localhost:5432/toolso pnpm admin:setup
```
### 其他平台
模板支持任何支持 Node.js 的平台:
- Railway
- Render
- 自托管
### 部署清单
- [ ] 设置 PostgreSQL 数据库(Supabase、Neon 或 Vercel Postgres)
- [ ] 配置 Resend 邮件服务
- [ ] 更新 `BETTER_AUTH_URL` 和 `NEXT_PUBLIC_APP_URL` 为生产域名
- [ ] 运行 `pnpm db:push` 初始化数据库
- [ ] 使用 `pnpm admin:setup` 创建管理员账户
## 国际化
模板通过 `next-intl` 支持多语言:
- **已支持**: 英文 (`en`)、中文 (`zh`)
- **路由格式**: 英文为默认语言不带前缀(`/...`),中文为 `/zh/...`(`localePrefix: 'as-needed'`)
- **翻译文件**: `messages/en.json`、`messages/zh.json`
- **一致性检查**: `pnpm i18n:check`
### 添加新语言
1. 复制 `messages/en.json` 到 `messages/{语言代码}.json`
2. 翻译内容
3. 更新 `i18n.config.ts`
> 注意:UI 文案可完全通过 JSON 文件翻译,但工具/分类/标签的**数据**在数据库中以双语列(`nameEn`/`nameZh`)存储。新增第三种内容语言需要扩展 schema 和查询。
## Roadmap
- [ ] 用户工具提交与审核工作流
- [ ] 工具评分与评论
- [ ] 自动化工具数据采集管线
## 贡献指南
欢迎贡献!请随时提交 Pull Request。
1. Fork 仓库
2. 创建特性分支 (`git checkout -b feature/amazing-feature`)
3. 提交更改 (`git commit -m 'Add some amazing feature'`)
4. 推送到分支 (`git push origin feature/amazing-feature`)
5. 开启 Pull Request
## 开源协议
本项目基于 Apache License 2.0 开源 - 详情请查看 [LICENSE](LICENSE) 文件。
---
<div align="center">
## Related Projects
| Project | Description | Link |
|---------|-------------|------|
| **Toolso.AI** | AI Tools Directory | [https://toolso.ai](https://toolso.ai) |
| **CoolChange.AI** | AI Development Studio | [https://coolchange.ai](https://coolchange.ai) |
| **chatPPT.app** | AI Presentation Generator | [https://chatppt.app](https://chatppt.app) |
| **AIHealthGuide** | AI Health Assistant | [https://aihealthguide.app](https://aihealthguide.app) |
<br/>
---
<br/>
**Built with love by [CoolChange.AI](https://coolchange.ai)**
<br/>
If you find this project helpful, please consider giving it a star!
</div>
File diff suppressed because it is too large Load Diff
+54
View File
@@ -0,0 +1,54 @@
// AI Directory Platform — 站点 Next.js 配置工厂
// 说明:站点 next.config.mjs 只需 `export { createSiteConfig as default } from "@aidir/core/next-config"`
// MDX/intl/turbopack 等构建依赖由底座包解析,站点零构建依赖。
import fs from "node:fs";
import path from "node:path";
import rehypePrism from "@mapbox/rehype-prism";
import nextMDX from "@next/mdx";
import remarkGfm from "remark-gfm";
import createNextIntlPlugin from "next-intl/plugin";
/**
* # 函数 createSiteConfig
* 说明:底座统一的站点 Next 配置(next-intl + MDX + 图片域名 + standalone)。
* 参数:opts.requestPath 站点 i18n request 文件相对路径
* 返回值:完整 Next 配置
*/
export function createSiteConfig({
requestPath = "./src/i18n/request.ts",
} = {}) {
const withNextIntl = createNextIntlPlugin(requestPath);
/** @type {import('next').NextConfig} */
const nextConfig = {
output: "standalone",
transpilePackages: ["@aidir/core"],
images: {
// 工具 logo 指向各官网 favicon,域名不可枚举,放开 https 通配
remotePatterns: [{ protocol: "https", hostname: "**" }],
},
pageExtensions: ["ts", "tsx", "mdx"],
turbopack: {
rules: {
"*.mdx": {
loaders: ["@mdx-js/loader"],
as: "*.js",
},
},
},
};
const withMDX = nextMDX({
extension: /\.mdx?$/,
options: {
remarkPlugins: [remarkGfm],
rehypePlugins: [rehypePrism],
},
});
return withNextIntl(withMDX(nextConfig));
}
export default createSiteConfig;
+124
View File
@@ -0,0 +1,124 @@
{
"name": "@aidir/core",
"version": "1.0.0",
"description": "AI Directory Platform core — shared library for all directory sites",
"private": true,
"keywords": [
"ai-tools",
"ai-directory",
"ai-tools-directory",
"artificial-intelligence",
"machine-learning",
"nextjs",
"typescript",
"tailwindcss",
"open-source"
],
"license": "Apache-2.0",
"homepage": "https://toolso.ai",
"repository": {
"type": "git",
"url": "https://github.com/coolchange-ai/toolso-ai-open.git"
},
"bugs": {
"url": "https://github.com/coolchange-ai/toolso-ai-open/issues"
},
"author": {
"name": "CoolChange.AI",
"url": "https://coolchange.ai"
},
"scripts": {
"lint": "eslint .",
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:push": "drizzle-kit push",
"db:studio": "drizzle-kit studio",
"admin:setup": "tsx scripts/setup-admin.ts",
"seed:tools": "tsx scripts/seed-tools.ts",
"generate:blog-manifest": "node scripts/generate-blog-manifest.mjs",
"typecheck": "tsc --noEmit",
"i18n:check": "node scripts/check-i18n.mjs",
"seed:platform": "bun scripts/seed-platform.ts",
"test:e2e": "bun scripts/e2e-interaction.ts",
"shots": "bun scripts/screenshot-pages.ts"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.958.0",
"@better-auth/client": "0.0.2-alpha.3",
"@hookform/resolvers": "^3.4.2",
"@mapbox/rehype-prism": "^0.9.0",
"@mdx-js/loader": "^3.0.1",
"@mdx-js/react": "^3.0.1",
"@next/mdx": "^14.2.3",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-slot": "^1.0.2",
"@react-email/components": "^0.5.3",
"@tabler/icons-react": "^3.5.0",
"@tailwindcss/typography": "^0.5.13",
"@tanstack/react-store": "^0.7.5",
"@types/mdx": "^2.0.13",
"@vercel/analytics": "^1.5.0",
"@vercel/speed-insights": "^1.2.0",
"better-auth": "^1.4.10",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cobe": "^0.6.3",
"date-fns": "^3.6.0",
"drizzle-kit": "^0.31.4",
"drizzle-orm": "^0.44.5",
"fast-glob": "^3.3.2",
"framer-motion": "^12",
"geist": "^1.3.0",
"lucide-react": "^0.562.0",
"nanoid": "^5.1.6",
"next": "16.1.1",
"next-intl": "^4.7.0",
"next-themes": "^0.4.6",
"next-view-transitions": "^0.3.5",
"postgres": "^3.4.7",
"prettier": "^3.7.4",
"prismjs": "^1.29.0",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"react-email": "^4.2.11",
"react-hook-form": "^7.51.5",
"react-icons": "^5.2.1",
"react-markdown": "^10.1.0",
"react-wrap-balancer": "^1.1.1",
"reading-time": "^1.5.0",
"rehype-highlight": "^7.0.2",
"remark-gfm": "^4.0.0",
"resend": "^6.1.0",
"sonner": "^2.0.7",
"tailwind-merge": "^2.3.0",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/bun": "^1.4.2",
"@types/node": "^20",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"dotenv": "^17.2.2",
"eslint": "^9.39.5",
"eslint-config-next": "16.1.1",
"pg": "^8.16.3",
"playwright": "^1.57.0",
"png-to-ico": "^3.0.1",
"postcss": "^8",
"sharp": "^0.34.5",
"tailwindcss": "^3.4.1",
"tsx": "^4.20.5",
"typescript": "^5"
},
"packageManager": "pnpm@10.11.1+sha512.e519b9f7639869dc8d5c3c5dfef73b3f091094b0a006d7317353c72b124e80e1afd429732e28705ad6bfa1ee879c1fce46c128ccebd3192101f43dd67c667912",
"exports": {
"./pages/*": "./src/pages/*",
"./kit/*": "./src/kit/*",
"./src/*": "./src/*",
"./styles/globals.css": "./src/styles/globals.css",
"./messages/*": "./messages/*",
"./proxy": "./src/proxy.ts",
"./i18n.config": "./src/i18n.config.ts",
"./next-config": "./next-config.mjs"
}
}
View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Before

Width:  |  Height:  |  Size: 293 KiB

After

Width:  |  Height:  |  Size: 293 KiB

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Before

Width:  |  Height:  |  Size: 715 KiB

After

Width:  |  Height:  |  Size: 715 KiB

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Before

Width:  |  Height:  |  Size: 574 KiB

After

Width:  |  Height:  |  Size: 574 KiB

Before

Width:  |  Height:  |  Size: 336 KiB

After

Width:  |  Height:  |  Size: 336 KiB

@@ -0,0 +1,63 @@
/**
* # 脚本 e2e-interaction
* 说明:端到端交互验证 —— 登录 → 投票(DB 前后对比)→ 重复提交检测。
* 在 monorepo 多站点架构下运行(共享同一套 Auth/DB)。
*/
import { chromium } from "playwright";
import postgres from "postgres";
const BASE = process.env.E2E_BASE || "http://localhost:3001";
const sql = postgres(
process.env.DATABASE_URL || "postgresql://postgres:postgres@127.0.0.1:15433/ai_directory",
{ prepare: false }
);
const results: string[] = [];
const browser = await chromium.launch();
const context = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await context.newPage();
const countClaudeVotes = async () =>
Number((await sql`SELECT COUNT(*) FROM vote WHERE tool_id='tool_claude'`)[0].count);
// 1) 登录
await page.goto(`${BASE}/login`, { waitUntil: "domcontentloaded" });
await page.fill('input[type="email"]', "bob@demo.dev");
await page.fill('input[type="password"]', "password123");
await page.click('button[type="submit"]');
await page.waitForTimeout(2500);
results.push(`1) 登录后 URL: ${page.url()}`);
await page.screenshot({ path: "/tmp/aidir-mono/e2e-after-login.png" });
results.push(`1b) 登录后用户菜单存在: ${await page.locator("header img, header button").count() > 0}`);
// 2) 投票 toggle(DB 前后对比)
const before = await countClaudeVotes();
await page.goto(`${BASE}/tools/claude`, { waitUntil: "domcontentloaded" });
await page.waitForTimeout(1500);
await page.screenshot({ path: "/tmp/aidir-mono/e2e-detail.png" });
const btnCount = await page.locator("button", { hasText: /Upvote|Upvoted/ }).count();
results.push(`2a) 找到投票按钮数: ${btnCount}`);
const btn = page.locator("button", { hasText: /Upvote|Upvoted/ }).first();
await btn.click();
await page.waitForTimeout(2500);
const after = await countClaudeVotes();
results.push(`2) 投票: DB ${before}→${after},按钮 "${((await btn.textContent()) ?? "").trim()}" → ${before !== after ? "PASS" : "FAIL"}`);
await btn.click();
await page.waitForTimeout(2500);
const restored = await countClaudeVotes();
results.push(`3) 取消投票: DB 回到 ${restored} → ${restored === before ? "PASS" : "FAIL"}`);
// 4) 重复提交检测
await page.goto(`${BASE}/submit`, { waitUntil: "domcontentloaded" });
await page.fill('input[placeholder="ChatGPT"]', "ChatGPT");
await page.fill('input[type="url"]', "https://chatgpt.com");
await page.locator('input[type="url"]').blur();
await page.waitForTimeout(1500);
const dupVisible = await page.locator("text=already be listed").count();
results.push(`4) 重复检测: ${dupVisible > 0 ? "PASS" : "FAIL"}`);
await page.screenshot({ path: "/tmp/aidir-mono/e2e-duplicate.png" });
await browser.close();
await sql.end();
console.log(results.join("\n"));
@@ -3,7 +3,7 @@ import path from "node:path";
import glob from "fast-glob";
async function main() {
const blogRoot = path.join(process.cwd(), "src/app/[locale]/(marketing)/blog");
const blogRoot = path.join(process.cwd(), "src/content/blog");
const entries = await glob("**/*.mdx", { cwd: blogRoot });
const manifest = new Map();
@@ -51,9 +51,7 @@ async function main() {
// 使用 path.posix 确保导入路径始终使用 / (跨平台兼容)
const importPath = path.posix.join(
"..",
"app",
"[locale]",
"(marketing)",
"content",
"blog",
slug,
`${locale}.mdx`
+705
View File
@@ -0,0 +1,705 @@
/**
* # 脚本 generate-site
* 说明:从底座生成一个独立网站应用(sites/<id>/)。
* - src/app/**:全部为薄路由(re-export core/src/pages 的同名模块)
* - (marketing)/layout + 首页:站点组合器(品牌 + 站点首页组件)
* - messages/:core 基础文案 + 站点品牌 SEO 覆盖
* - 配置:独立 package.json / next.config / tsconfig / tailwind / postcss
*
* 用法:bun scripts/generate-site.mjs <siteKey>
* 站点定义见本文件 SITES 常量。
*/
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const CORE_ROOT = path.resolve(__dirname, "..");
const REPO_ROOT = path.resolve(CORE_ROOT, "..", "..");
const SITES_ROOT = path.join(REPO_ROOT, "sites");
// ====================================================================
// 站点定义:一个条目 = 一个独立网站(品牌 / 导航 / 页脚 / 首页 / 端口)
// ====================================================================
const SITES = {
"ai-stack": {
port: 3001,
theme: "classic",
brand: {
name: "AI Stack",
tagline: "The professional AI tools directory — ranked by the community.",
headline: "Find the best AI tools, ranked by real users",
subheadline:
"1,000+ AI tools across 14 categories. Compare votes, reviews and pricing to pick the right one.",
},
navItems: [
{ label: "Discover", href: "/tools" },
{ label: "Use Cases", href: "/ai-tools-for" },
{ label: "Best Lists", href: "/best" },
{ label: "Rankings", href: "/rankings" },
{ label: "Blog", href: "/blog" },
{ label: "Submit", href: "/submit" },
],
footerGroups: [
{
title: "Discover",
links: [
{ label: "All Tools", href: "/tools" },
{ label: "Use Cases", href: "/ai-tools-for" },
{ label: "Best Lists", href: "/best" },
{ label: "Rankings", href: "/rankings" },
],
},
{
title: "Community",
links: [
{ label: "Submit a Tool", href: "/submit" },
{ label: "Blog", href: "/blog" },
{ label: "Dashboard", href: "/dashboard" },
],
},
{
title: "Legal",
links: [
{ label: "Terms", href: "/terms" },
{ label: "Privacy", href: "/privacy" },
{ label: "Contact", href: "/contact" },
],
},
],
searchPlaceholder: "Search AI tools...",
seo: {
en: {
title: "AI Stack — Find the Best AI Tools, Ranked by Real Users",
description:
"The professional AI tools directory. 1,000+ AI tools across 14 categories ranked by community votes, reviews and engagement.",
},
zh: {
title: "AI Stack — 由真实用户排名的 AI 工具目录",
description: "专业 AI 工具导航:14 个分类、1000+ 工具,由社区投票、评论与互动数据实时排名。",
},
},
},
launchai: {
port: 3002,
theme: "product",
brand: {
name: "LaunchAI",
tagline: "Discover and upvote today's AI products.",
headline: "Today's AI product launches",
subheadline: "Fresh AI products, every day. Upvote the best ones to the top.",
},
navItems: [
{ label: "Today", href: "/" },
{ label: "Leaderboard", href: "/rankings" },
{ label: "Categories", href: "/tools" },
{ label: "Collections", href: "/best" },
{ label: "Blog", href: "/blog" },
{ label: "Submit", href: "/submit" },
],
footerGroups: [
{
title: "Community",
links: [
{ label: "Submit a Product", href: "/submit" },
{ label: "Leaderboard", href: "/rankings" },
{ label: "Dashboard", href: "/dashboard" },
],
},
{
title: "Explore",
links: [
{ label: "All Products", href: "/tools" },
{ label: "Use Cases", href: "/ai-tools-for" },
{ label: "Collections", href: "/best" },
],
},
{
title: "About",
links: [
{ label: "Blog", href: "/blog" },
{ label: "Terms", href: "/terms" },
{ label: "Privacy", href: "/privacy" },
],
},
],
searchPlaceholder: "Search AI products...",
seo: {
en: {
title: "LaunchAI — Discover and Upvote Today's AI Products",
description:
"A daily launch feed for AI products. Upvote the best AI tools, follow makers and climb the leaderboard.",
},
zh: {
title: "LaunchAI — 发现并投票今日 AI 新品",
description: "AI 产品每日发布社区:为最好的 AI 工具投票,登上社区排行榜。",
},
},
},
"the-ai-ledger": {
port: 3003,
theme: "editorial",
brand: {
name: "The AI Ledger",
tagline: "Independent journalism on AI tools, with a community-ranked directory.",
headline: "AI tools, reviewed and ranked",
subheadline:
"Guides, comparisons and a community-powered directory of the best AI tools.",
},
navItems: [
{ label: "Stories", href: "/blog" },
{ label: "Guides", href: "/ai-tools-for" },
{ label: "Best Picks", href: "/best" },
{ label: "Directory", href: "/tools" },
{ label: "Rankings", href: "/rankings" },
{ label: "Submit", href: "/submit" },
],
footerGroups: [
{
title: "Read",
links: [
{ label: "Latest Stories", href: "/blog" },
{ label: "Buying Guides", href: "/best" },
{ label: "Use Case Guides", href: "/ai-tools-for" },
],
},
{
title: "Directory",
links: [
{ label: "All Tools", href: "/tools" },
{ label: "Community Rankings", href: "/rankings" },
{ label: "Submit a Tool", href: "/submit" },
],
},
{
title: "Company",
links: [
{ label: "Contact", href: "/contact" },
{ label: "Terms", href: "/terms" },
{ label: "Privacy", href: "/privacy" },
],
},
],
searchPlaceholder: "Search the directory...",
seo: {
en: {
title: "The AI Ledger — AI Tools, Reviewed and Ranked",
description:
"Independent journalism on AI: buying guides, comparisons and a community-ranked directory of the best AI tools.",
},
zh: {
title: "The AI Ledger — AI 工具评测与排名",
description: "独立 AI 媒体:深度指南、横向对比,以及由社区排名的 AI 工具目录。",
},
},
},
"devai-tools": {
port: 3004,
theme: "niche",
brand: {
name: "DevAI.tools",
tagline: "AI tools for developers — curated, tested, ranked by the community.",
headline: "AI tools for developers",
subheadline:
"Code assistants, DevOps automation, testing copilots and everything else you ship with.",
},
navItems: [
{ label: "Tools", href: "/tools" },
{ label: "Workflows", href: "/best" },
{ label: "Use Cases", href: "/ai-tools-for" },
{ label: "Rankings", href: "/rankings" },
{ label: "Submit", href: "/submit" },
],
footerGroups: [
{
title: "Directory",
links: [
{ label: "All Tools", href: "/tools" },
{ label: "Rankings", href: "/rankings" },
{ label: "Use Cases", href: "/ai-tools-for" },
],
},
{
title: "Build",
links: [
{ label: "Workflows", href: "/best" },
{ label: "Submit a Tool", href: "/submit" },
{ label: "Blog", href: "/blog" },
],
},
{
title: "Legal",
links: [
{ label: "Terms", href: "/terms" },
{ label: "Privacy", href: "/privacy" },
],
},
],
searchPlaceholder: "Search dev tools...",
seo: {
en: {
title: "DevAI.tools — AI Tools for Developers",
description:
"Code assistants, DevOps automation and testing copilots — the AI tools developers actually upvote, ranked daily.",
},
zh: {
title: "DevAI.tools — 开发者 AI 工具",
description: "面向开发者的 AI 工具目录:代码助手、运维自动化、测试副驾驶,由社区每日排名。",
},
},
},
findai: {
port: 3005,
theme: "minimal",
brand: {
name: "findai",
tagline: "The minimal AI tool search engine.",
headline: "What are you trying to do?",
subheadline: "",
},
navItems: [
{ label: "Search", href: "/tools" },
{ label: "Use Cases", href: "/ai-tools-for" },
{ label: "Rankings", href: "/rankings" },
{ label: "Submit", href: "/submit" },
],
footerGroups: [
{
title: "Search",
links: [
{ label: "All Tools", href: "/tools" },
{ label: "Use Cases", href: "/ai-tools-for" },
{ label: "Rankings", href: "/rankings" },
],
},
{
title: "About",
links: [
{ label: "Submit a Tool", href: "/submit" },
{ label: "Privacy", href: "/privacy" },
{ label: "Terms", href: "/terms" },
],
},
],
searchPlaceholder: "What do you need?",
seo: {
en: {
title: "findai — The Minimal AI Tool Search Engine",
description:
"One search box, every AI tool. Describe what you're trying to do and find the right AI tool, ranked by the community.",
},
zh: {
title: "findai — 极简 AI 工具搜索引擎",
description: "一个搜索框,找遍全部 AI 工具。说出你想做的事,社区排名替你选。",
},
},
},
};
// ====================================================================
// 生成逻辑
// ====================================================================
/** 遍历 core/src/pages 下所有 page.tsx / route.ts,返回相对路由信息 */
function collectRoutes() {
const pagesRoot = path.join(CORE_ROOT, "src", "pages");
const routes = [];
function walk(dir) {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
walk(full);
} else if (
entry.name === "page.tsx" ||
entry.name === "route.ts" ||
entry.name === "layout.tsx" ||
entry.name === "not-found.tsx" ||
entry.name === "manifest.ts" ||
entry.name === "sitemap.ts"
) {
const rel = path.relative(pagesRoot, full);
const kind =
entry.name === "route.ts"
? "route"
: entry.name === "layout.tsx"
? "layout"
: "page";
routes.push({ rel, kind, full });
}
}
}
walk(pagesRoot);
return routes;
}
/** 从 core 页面模块源码中提取可 re-export 的导出名 */
/**
* 从 core 页面模块源码中提取导出:
* - 函数导出(default/generateMetadata/generateStaticParams...)→ re-export
* - 路由段配置常量(revalidate/dynamic...)→ 内联字面量(Next 要求静态可解析,不能 re-export)
*/
function extractExports(file, kind) {
const srcText = fs.readFileSync(file, "utf8");
const names = new Set();
const inlineConfigs = [];
if (kind !== "route" && /export\s+default/.test(srcText)) {
names.add("default");
}
const fnRe = /export\s+(?:async\s+)?function\s+([A-Za-z0-9_]+)/g;
for (const m of srcText.matchAll(fnRe)) names.add(m[1]);
const configNames = ["revalidate", "dynamic", "runtime", "fetchCache", "maxDuration", "dynamicParams"];
const constRe = /export\s+const\s+([A-Za-z0-9_]+)\s*=\s*([^;\n]+);/g;
for (const m of srcText.matchAll(constRe)) {
const full = m[0];
const name = m[1];
if (configNames.includes(name)) {
inlineConfigs.push(full);
}
}
// route 的解构导出(如 export const { POST, GET } = ...)→ 整句 re-export
const destructureRe = /export\s+const\s*\{([^}]+)\}\s*=[^;\n]+;/g;
for (const m of srcText.matchAll(destructureRe)) {
for (const raw of m[1].split(",")) {
const name = raw.trim().split(":")[0].trim();
if (/^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)$/.test(name)) names.add(name);
}
}
return {
names: [...names].filter((n) => !n.startsWith("type")),
inlineConfigs,
};
}
function writeFileSafe(file, content) {
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, content);
}
function generateSite(siteKey) {
const site = SITES[siteKey];
if (!site) throw new Error(`Unknown site: ${siteKey}. 可选: ${Object.keys(SITES).join(", ")}`);
const siteRoot = path.join(SITES_ROOT, siteKey);
console.log(`\n=== 生成站点 ${siteKey} (port ${site.port}, theme ${site.theme}) ===`);
fs.mkdirSync(siteRoot, { recursive: true });
// 清空生成物,但保留本地环境与依赖
for (const entry of fs.readdirSync(siteRoot)) {
if (entry === ".env.local" || entry === "node_modules") continue;
fs.rmSync(path.join(siteRoot, entry), { recursive: true, force: true });
}
const corePkg = "@aidir/core";
// ---------- 1) 薄路由 ----------
const skipRoutes = new Set([
// 站点组合器文件由下方单独生成(营销布局 + 首页)
"[locale]/(marketing)/layout.tsx",
"[locale]/(marketing)/page.tsx",
// 根布局单独生成(需引入站点样式)
"layout.tsx",
]);
let routeCount = 0;
for (const route of collectRoutes()) {
if (skipRoutes.has(route.rel)) continue;
const rel = route.rel.replace(/\\/g, "/");
const target = path.join(siteRoot, "src", "app", rel);
const importPath = `${corePkg}/pages/${rel.replace(/\.tsx?$/, "")}`;
const { names, inlineConfigs } = extractExports(route.full, route.kind);
if (names.length === 0 && inlineConfigs.length === 0) continue;
let content = `// 自动生成:${rel} 的薄路由\n`;
for (const cfg of inlineConfigs) content += `${cfg}\n`;
if (names.length > 0) {
content += `export { ${names.join(", ")} } from "${importPath}";\n`;
}
writeFileSafe(target, content);
routeCount++;
}
// ---------- 2) 根布局(css + 透传) ----------
writeFileSafe(
path.join(siteRoot, "src/app/layout.tsx"),
`import "${corePkg}/styles/globals.css";\n\nexport { default } from "${corePkg}/pages/app-layout";\n`
);
// ---------- 3) 营销布局 + 首页(站点组合器) ----------
writeFileSafe(
path.join(siteRoot, "src/app/[locale]/(marketing)/layout.tsx"),
`import { createMarketingLayout } from "${corePkg}/kit/create-marketing-layout";\nimport { siteConfig } from "@/site/brand";\n\nexport default createMarketingLayout(siteConfig);\n`
);
writeFileSafe(
path.join(siteRoot, "src/app/[locale]/(marketing)/page.tsx"),
`import { createHomePage, createHomeMetadata } from "${corePkg}/kit/create-home";\nimport { siteConfig } from "@/site/brand";\nimport { SiteHome } from "@/site/home";\n\n// 首页内容变化不频繁,ISR 每小时再生\nexport const revalidate = 3600;\n\nexport const generateMetadata = createHomeMetadata(siteConfig);\nexport default createHomePage(siteConfig, SiteHome);\n`
);
// ---------- 4) 站点品牌 + 首页组件 ----------
const themeSrc = fs.readFileSync(
path.join(CORE_ROOT, "src/kit/site-themes", site.theme, "index.tsx"),
"utf8"
);
// 改写 import:kit 路径 + 站点品牌
let homeSrc = themeSrc
.replace(/from "[.\/]*shared\/(section|home-data)"/g, 'from "@/kit/$1"')
.replace(/from "[.\/]*shared\/theme-header"/g, 'from "@/kit/theme-header"')
.replace(/from "[.\/]*shared\/theme-footer"/g, 'from "@/kit/theme-footer"')
.replace(/import type \{ ThemeModule \} from "[.\/]*types";\n/g, "");
// 去掉主题文件里的 brand 常量与尾部主题注册导出(品牌来自 brand.tsx)
homeSrc = homeSrc.replace(/const brand = \{[\s\S]*?\};/, "");
const tailIdx = homeSrc.indexOf("\nexport const ");
if (tailIdx !== -1) homeSrc = homeSrc.slice(0, tailIdx);
// 统一导出名为 SiteHome(原函数如 ClassicHome / ProductHome...)
homeSrc = homeSrc.replace(
/(async\s+function\s+)(\w+Home)(\s*\()/,
"$1SiteHome$3"
);
homeSrc += "\nexport { SiteHome };\n";
// 未使用的 brand 引用替换
homeSrc = homeSrc.replace(/\bbrand\.name\b/g, "siteConfig.brand.name")
.replace(/\bbrand\.headline\b/g, "siteConfig.brand.headline")
.replace(/\bbrand\.subheadline\b/g, "siteConfig.brand.subheadline")
.replace(/\bbrand\.tagline\b/g, "siteConfig.brand.tagline");
// 追加 import siteConfig
const firstImport = homeSrc.indexOf('import ');
homeSrc =
homeSrc.slice(0, firstImport) +
'import { siteConfig } from "@/site/brand";\n' +
homeSrc.slice(firstImport);
homeSrc =
`// 自动生成:站点首页组件(源自底座 ${site.theme} 形态,品牌来自 @/site/brand)\n` +
homeSrc;
writeFileSafe(path.join(siteRoot, "src/site/home.tsx"), homeSrc);
// brand.tsx(SiteConfig,不含 theme 生成元数据)
const { theme: _theme, ...siteConfigData } = site;
siteConfigData.id = siteKey;
writeFileSafe(
path.join(siteRoot, "src/site/brand.tsx"),
`import type { SiteConfig } from "@/kit/types";\n\n// ${site.brand.name} —— 独立站点的全部品牌/导航/页脚配置\nexport const siteConfig: SiteConfig = ${JSON.stringify(siteConfigData, null, 2)};\n`
);
// ---------- 5) i18n request(站点文案覆盖) ----------
writeFileSafe(
path.join(siteRoot, "src/i18n/request.ts"),
`import { getRequestConfig } from "next-intl/server";
import { notFound } from "next/navigation";
import { locales, defaultLocale, type Locale } from "${corePkg}/i18n.config";
/** 站点品牌 SEO 文案覆盖(每个网站不同) */
const brandMessages = {
en: {
seo: { home: ${JSON.stringify({ title: site.seo.en.title, description: site.seo.en.description, ogImage: "/og-image.png" })} },
hero: { title: ${JSON.stringify(site.seo.en.title)}, description: ${JSON.stringify(site.seo.en.description)} },
common: { brand: { name: ${JSON.stringify(site.brand.name)}, tagline: ${JSON.stringify(site.brand.tagline)} } },
},
zh: {
seo: { home: ${JSON.stringify({ title: site.seo.zh.title, description: site.seo.zh.description, ogImage: "/og-image.png" })} },
hero: { title: ${JSON.stringify(site.seo.zh.title)}, description: ${JSON.stringify(site.seo.zh.description)} },
common: { brand: { name: ${JSON.stringify(site.brand.name)}, tagline: ${JSON.stringify(site.brand.tagline)} } },
},
} as const;
export default getRequestConfig(async ({ requestLocale }) => {
let locale = await requestLocale;
if (!locale || !locales.includes(locale as Locale)) {
locale = defaultLocale;
}
try {
// 底座基础文案 + 站点品牌覆盖(浅合并到顶层命名空间)
const base = (await import(\`${corePkg}/messages/\${locale}.json\`)).default;
const override = brandMessages[locale as keyof typeof brandMessages];
const messages = { ...base };
for (const [ns, value] of Object.entries(override)) {
messages[ns] = { ...(messages[ns] ?? {}), ...value };
}
return { locale, messages };
} catch (error) {
console.error("Error loading messages for locale:", locale, error);
notFound();
}
});
`
);
// ---------- 6) proxy / mdx-components ----------
writeFileSafe(
path.join(siteRoot, "src/proxy.ts"),
`import { proxy } from "${corePkg}/proxy";\n\nexport default proxy;\n\nexport const config = {\n matcher: [\n "/",\n "/((?!api|_next|_vercel|.*\\\\..*).*)"\n ]\n};\n`
);
writeFileSafe(
path.join(siteRoot, "mdx-components.tsx"),
`export { useMDXComponents } from "${corePkg}/src/mdx-components";\n`
);
// ---------- 7) 配置文件 ----------
writeFileSafe(
path.join(siteRoot, "package.json"),
JSON.stringify(
{
name: `site-${siteKey}`,
version: "1.0.0",
description: `${site.brand.name} — AI directory site powered by @aidir/core`,
private: true,
scripts: {
dev: `next dev -p ${site.port}`,
build: "next build",
start: `next start -p ${site.port}`,
typecheck: "tsc --noEmit",
},
dependencies: {
"@aidir/core": "workspace:*",
next: "16.1.1",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"next-intl": "^4.7.0",
"next-themes": "^0.4.6",
"next-view-transitions": "^0.3.5",
"sonner": "^2.0.7",
"geist": "^1.3.0",
"lucide-react": "^0.562.0",
"framer-motion": "^12.43.0",
},
devDependencies: {
"@types/node": "^20",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@types/mdx": "^2.0.13",
"typescript": "^5",
"tailwindcss": "^3.4.1",
"postcss": "^8",
},
},
null,
2
) + "\n"
);
writeFileSafe(
path.join(siteRoot, "next.config.mjs"),
`import createSiteConfig from "@aidir/core/next-config";\n\nexport default createSiteConfig({\n requestPath: "./src/i18n/request.ts",\n});\n`
);
writeFileSafe(
path.join(siteRoot, "tsconfig.json"),
JSON.stringify(
{
compilerOptions: {
target: "ES2017",
lib: ["dom", "dom.iterable", "esnext"],
allowJs: true,
skipLibCheck: true,
strict: true,
noEmit: true,
esModuleInterop: true,
module: "esnext",
moduleResolution: "bundler",
resolveJsonModule: true,
isolatedModules: true,
jsx: "preserve",
incremental: true,
plugins: [{ name: "next" }],
paths: {
"@/*": ["./src/*", "../../packages/core/src/*"],
"@aidir/core/pages/*": ["../../packages/core/src/pages/*"],
"@aidir/core/kit/*": ["../../packages/core/src/kit/*"],
"@aidir/core/src/*": ["../../packages/core/src/*"],
"@aidir/core/proxy": ["../../packages/core/src/proxy.ts"],
"@aidir/core/i18n.config": ["../../packages/core/src/i18n.config.ts"],
"@aidir/core/styles/globals.css": ["../../packages/core/src/styles/globals.css"],
"@aidir/core/*": ["../../packages/core/src/*", "../../packages/core/*"],
},
},
include: ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
exclude: ["node_modules"],
},
null,
2
) + "\n"
);
writeFileSafe(
path.join(siteRoot, "postcss.config.mjs"),
`const config = {\n plugins: {\n tailwindcss: {},\n },\n};\n\nexport default config;\n`
);
// tailwind:content 扫描站点 + 底座;主题/插件来自底座 preset
writeFileSafe(
path.join(siteRoot, "tailwind.config.ts"),
`import type { Config } from "tailwindcss";\n\nconst config: Config = {\n content: [\n "./src/**/*.{js,ts,jsx,tsx,mdx}",\n "../../packages/core/src/**/*.{js,ts,jsx,tsx,mdx}",\n ],\n presets: [require("../../packages/core/tailwind.preset.cjs")],\n};\n\nexport default config;\n`
);
// ---------- 8) messages(base 由 i18n request 运行时合并;站点仍保留一份覆盖说明) ----------
// 静态拷贝 core messages 便于独立部署检查
for (const locale of ["en", "zh"]) {
const base = JSON.parse(fs.readFileSync(path.join(CORE_ROOT, "messages", `${locale}.json`), "utf8"));
// 品牌覆盖(静态合并进 messages,运行时 request.ts 仍会覆盖以双保险)
base.common = base.common ?? {};
base.common.brand = { ...(base.common.brand ?? {}), name: site.brand.name, tagline: site.brand.tagline };
base.seo = base.seo ?? {};
base.seo.home = { ...(base.seo.home ?? {}), ...site.seo[locale] };
base.hero = { ...(base.hero ?? {}), title: site.seo[locale].title, description: site.seo[locale].description };
fs.mkdirSync(path.join(siteRoot, "messages"), { recursive: true });
fs.writeFileSync(
path.join(siteRoot, "messages", `${locale}.json`),
JSON.stringify(base, null, 2) + "\n"
);
}
// 但 request.ts 目前从 core 读基础文案 —— 改为读本站 messages(保证站点内容独立可改)
let requestSrc = fs.readFileSync(path.join(siteRoot, "src/i18n/request.ts"), "utf8");
requestSrc = requestSrc.replace(
'const base = (await import(`@aidir/core/messages/${locale}.json`)).default;',
'const base = (await import(`../../messages/${locale}.json`)).default;'
);
writeFileSafe(path.join(siteRoot, "src/i18n/request.ts"), requestSrc);
// ---------- 9) public + env ----------
fs.cpSync(path.join(CORE_ROOT, "public"), path.join(siteRoot, "public"), { recursive: true });
// 品牌首字母 favicon(SVG,无 Emoji)
const initial = site.brand.name.slice(0, 1).toUpperCase();
writeFileSafe(
path.join(siteRoot, "public", "icon.svg"),
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="14" fill="currentColor"/><text x="32" y="44" font-family="system-ui,sans-serif" font-size="36" font-weight="700" text-anchor="middle" fill="#fff">${initial}</text></svg>\n`
);
writeFileSafe(
path.join(siteRoot, ".env.example"),
`# ${site.brand.name} 环境变量(与底座共享同一套 PostgreSQL / Better Auth / Admin)
DATABASE_URL="postgresql://postgres:postgres@127.0.0.1:15433/ai_directory"
BETTER_AUTH_SECRET="dev-secret-ai-directory-platform-0123456789abcdef"
BETTER_AUTH_URL="http://localhost:${site.port}"
AUTH_GOOGLE_ID=""
AUTH_GOOGLE_SECRET=""
NEXT_PUBLIC_AUTH_GOOGLE_ENABLED="false"
AUTH_GITHUB_ID=""
AUTH_GITHUB_SECRET=""
NEXT_PUBLIC_AUTH_GITHUB_ENABLED="false"
RESEND_API_KEY=""
RESEND_FROM_EMAIL="${site.brand.name} <noreply@localhost>"
NEXT_PUBLIC_APP_URL="http://localhost:${site.port}"
NEXT_PUBLIC_APP_NAME="${site.brand.name}"
`
);
// ---------- 10) .gitignore ----------
writeFileSafe(
path.join(siteRoot, ".gitignore"),
"/node_modules\n/.next\n/.env*.local\nnext-env.d.ts\n*.tsbuildinfo\n"
);
console.log(`路由薄文件: ${routeCount + 2}(含根布局/营销布局/首页组合器)`);
console.log(`输出: sites/${siteKey}`);
}
// main
const key = process.argv[2];
if (!key || key === "all") {
for (const k of Object.keys(SITES)) generateSite(k);
} else {
generateSite(key);
}
+25
View File
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
# 启动五个独立网站(monorepo sites/*,共享同一套数据库/Auth/Admin)
# ai-stack:3001 launchai:3002 the-ai-ledger:3003 devai-tools:3004 findai:3005
set -e
cd "$(dirname "$0")/../../.."
SITES=(ai-stack:3001 launchai:3002 the-ai-ledger:3003 devai-tools:3004 findai:3005)
for entry in "${SITES[@]}"; do
site="${entry%%:*}"
port="${entry##*:}"
(cd "sites/$site" && bun run start > "/tmp/site-$site.log" 2>&1) &
echo "$site → http://localhost:$port (pid $!)"
done
echo "等待启动..."
sleep 5
for entry in "${SITES[@]}"; do
site="${entry%%:*}"
port="${entry##*:}"
code=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:$port/" || echo "000")
echo "$site [$port] → HTTP $code"
done
wait

Some files were not shown because too many files have changed in this diff Show More