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
@@ -1,66 +1,7 @@
|
|||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
node_modules/
|
||||||
|
.next/
|
||||||
# dependencies
|
.next-*/
|
||||||
/node_modules
|
.env
|
||||||
/.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
|
.env*.local
|
||||||
|
|
||||||
# vercel
|
|
||||||
.vercel
|
|
||||||
|
|
||||||
# typescript
|
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
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
|
|
||||||
|
|||||||
@@ -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 个独立网站** 验证底座的「快速、易用、可用、可靠」。
|
||||||
|
|
||||||
[](https://opensource.org/licenses/Apache-2.0)
|
## 仓库结构
|
||||||
[](https://nextjs.org/)
|
|
||||||
[](https://react.dev/)
|
|
||||||
[](https://www.typescriptlang.org/)
|
|
||||||
[](https://tailwindcss.com/)
|
|
||||||
[](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/
|
packages/core/ @aidir/core —— 底座库(页面组件体 / 业务模块 / 站点外壳工厂 / 种子与工具脚本)
|
||||||
├── app/
|
sites/
|
||||||
│ ├── [locale]/ # Internationalized routes
|
├── ai-stack/ AI Stack — Classic 目录 → http://localhost:3001
|
||||||
│ │ ├── (auth)/ # Authentication pages
|
├── launchai/ LaunchAI — Product Hunt 式社区 → http://localhost:3002
|
||||||
│ │ ├── (marketing)/ # Public pages (home, blog, tools)
|
├── the-ai-ledger/ The AI Ledger — Editorial 媒体 → http://localhost:3003
|
||||||
│ │ ├── (protected)/ # User dashboard, profile
|
├── devai-tools/ DevAI.tools — 开发者垂直 → http://localhost:3004
|
||||||
│ │ └── (admin)/ # Admin panel
|
└── findai/ findai — 极简搜索 → http://localhost:3005
|
||||||
│ └── api/ # API routes
|
docs/ 设计文档 + 交付报告
|
||||||
├── 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
|
五个网站共享同一套 PostgreSQL / Better Auth / Admin(设计文档 §42 验证标准),
|
||||||
|
各自独立构建、独立部署(各自 `package.json` / `next.config.mjs` / `.env.local`)。
|
||||||
### 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
|
```bash
|
||||||
# 克隆仓库
|
bun install
|
||||||
git clone https://github.com/coolchange-ai/toolso-ai-open.git
|
cd packages/core && cp .env.example .env.local && bun run db:push && bun run seed:platform && cd ../..
|
||||||
cd toolso-ai-open
|
|
||||||
|
|
||||||
# 安装依赖
|
# 构建 5 个网站(构建时数据库需在线;构建期间不要运行站点)
|
||||||
pnpm install
|
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
|
bash packages/core/scripts/start-all-sites.sh
|
||||||
|
|
||||||
# 推送数据库结构
|
|
||||||
pnpm db:push
|
|
||||||
|
|
||||||
# (可选)导入 50 个 AI 工具、10 个分类、30 个标签种子数据
|
|
||||||
pnpm seed:tools
|
|
||||||
|
|
||||||
# (可选)创建管理员账户
|
|
||||||
pnpm admin:setup
|
|
||||||
|
|
||||||
# 启动开发服务器
|
|
||||||
pnpm dev
|
|
||||||
```
|
```
|
||||||
|
|
||||||
打开 [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"
|
|
||||||
|
|
||||||
# 邮件(认证功能必需)
|
1. 在 `packages/core/scripts/generate-site.mjs` 的 `SITES` 中加一个条目(品牌/导航/页脚/SEO/端口)。
|
||||||
RESEND_API_KEY="re_your_api_key"
|
2. `bun packages/core/scripts/generate-site.mjs <key>` —— 生成独立站点(约 56 个薄路由 + 配置)。
|
||||||
RESEND_FROM_EMAIL="Your App <noreply@yourdomain.com>"
|
3. `cd sites/<key> && bun install && bun run build && bun run start`。
|
||||||
|
|
||||||
# 可选:Google OAuth
|
零 Core 代码修改。
|
||||||
AUTH_GOOGLE_ID="your-google-client-id"
|
|
||||||
AUTH_GOOGLE_SECRET="your-google-client-secret"
|
|
||||||
```
|
|
||||||
|
|
||||||
完整配置请参考 [.env.example](.env.example)。
|
详细验收对照见 `docs/交付报告.md`。
|
||||||
|
|
||||||
## 项目结构
|
|
||||||
|
|
||||||
```
|
|
||||||
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>
|
|
||||||
|
|||||||
@@ -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。
|
||||||
@@ -1,118 +1,21 @@
|
|||||||
{
|
{
|
||||||
"name": "toolso-ai",
|
"name": "ai-directory-platform",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "An open-source AI tools directory to help you discover the best AI tools across various categories.",
|
"description": "AI Directory Platform — 可复用 AI Directory 底座 + 多站点(monorepo)",
|
||||||
"private": false,
|
"private": true,
|
||||||
"keywords": [
|
"workspaces": [
|
||||||
"ai-tools",
|
"packages/*",
|
||||||
"ai-directory",
|
"sites/*"
|
||||||
"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": {
|
"scripts": {
|
||||||
"dev": "node scripts/run-dev.mjs",
|
"build:core": "bun run --filter '@aidir/core' generate:blog-manifest",
|
||||||
"build": "npm run generate:blog-manifest && next build",
|
"gen:sites": "bun packages/core/scripts/generate-site.mjs all",
|
||||||
"start": "next start",
|
"db:push": "bun run --filter '@aidir/core' db:push",
|
||||||
"lint": "eslint .",
|
"db:migrate": "bun run --filter '@aidir/core' db:migrate",
|
||||||
"db:generate": "drizzle-kit generate",
|
"seed": "bun packages/core/scripts/seed-platform.ts --force",
|
||||||
"db:migrate": "drizzle-kit migrate",
|
"test": "bun test packages/core/tests",
|
||||||
"db:push": "drizzle-kit push",
|
"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",
|
||||||
"db:studio": "drizzle-kit studio",
|
"build:all": "for s in ai-stack launchai the-ai-ledger devai-tools findai; do (cd sites/$s && bun run build); done",
|
||||||
"admin:setup": "tsx scripts/setup-admin.ts",
|
"start:all": "bash packages/core/scripts/start-all-sites.sh"
|
||||||
"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"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -0,0 +1,499 @@
|
|||||||
|
<div align="center">
|
||||||
|
|
||||||
|
# Toolso.AI
|
||||||
|
|
||||||
|
**An open-source AI tools directory to help you discover the best AI tools**
|
||||||
|
|
||||||
|
[](https://opensource.org/licenses/Apache-2.0)
|
||||||
|
[](https://nextjs.org/)
|
||||||
|
[](https://react.dev/)
|
||||||
|
[](https://www.typescriptlang.org/)
|
||||||
|
[](https://tailwindcss.com/)
|
||||||
|
[](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>
|
||||||
@@ -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;
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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";
|
import glob from "fast-glob";
|
||||||
|
|
||||||
async function main() {
|
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 entries = await glob("**/*.mdx", { cwd: blogRoot });
|
||||||
const manifest = new Map();
|
const manifest = new Map();
|
||||||
|
|
||||||
@@ -51,9 +51,7 @@ async function main() {
|
|||||||
// 使用 path.posix 确保导入路径始终使用 / (跨平台兼容)
|
// 使用 path.posix 确保导入路径始终使用 / (跨平台兼容)
|
||||||
const importPath = path.posix.join(
|
const importPath = path.posix.join(
|
||||||
"..",
|
"..",
|
||||||
"app",
|
"content",
|
||||||
"[locale]",
|
|
||||||
"(marketing)",
|
|
||||||
"blog",
|
"blog",
|
||||||
slug,
|
slug,
|
||||||
`${locale}.mdx`
|
`${locale}.mdx`
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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
|
||||||