This commit is contained in:
砂糖
2025-11-21 13:36:06 +08:00
commit 7cd50654ed
112 changed files with 14246 additions and 0 deletions

42
.env.example Normal file
View File

@@ -0,0 +1,42 @@
# -----------------------------------------------------------------------------
# Site info
# -----------------------------------------------------------------------------
NEXT_PUBLIC_SITE_URL=http://localhost:3000
NEXT_PUBLIC_LOCALE_DETECTION=false
# -----------------------------------------------------------------------------
# Discord
# -----------------------------------------------------------------------------
NEXT_PUBLIC_DISCORD_INVITE_URL="https://discord.com/invite/R7bUxWKRqZ"
# -----------------------------------------------------------------------------
# Analytics
# Google Analytics: https://analytics.google.com/analytics/web/
# Baidu Tongji: https://tongji.baidu.com/
# Plausible: https://plausible.io/
# -----------------------------------------------------------------------------
NEXT_PUBLIC_GOOGLE_ID=
NEXT_PUBLIC_BAIDU_TONGJI=
NEXT_PUBLIC_PLAUSIBLE_DOMAIN=
NEXT_PUBLIC_PLAUSIBLE_SRC=
#------------------------------------------------------------------------
# Ads
# Google Adsense: https://www.google.com/adsense/
#------------------------------------------------------------------------
NEXT_PUBLIC_GOOGLE_ADSENSE_ID=
#------------------------------------------------------------------------
# Resend: https://resend.com/
#------------------------------------------------------------------------
RESEND_API_KEY=
ADMIN_EMAIL=
RESEND_AUDIENCE_ID=
#------------------------------------------------------------------------
# Upstash: https://upstash.com/
#------------------------------------------------------------------------
UPSTASH_REDIS_REST_URL=
UPSTASH_REDIS_REST_TOKEN=
UPSTASH_REDIS_NEWSLETTER_RATE_LIMIT_KEY=newsletter-rate-limit
DAY_MAX_SUBMISSIONS=

3
.eslintrc.json Normal file
View File

@@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}

133
.gitignore vendored Normal file
View File

@@ -0,0 +1,133 @@
### Node template
.idea
.DS_Store
dist
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.temp
yarn.lock
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.local
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the assets line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# assets
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# pnpm
.pnpm-store/
/.vuepress/dist/
# sitemap
*/sitemap*.xml
*/robots.txt

4
.npmrc Normal file
View File

@@ -0,0 +1,4 @@
# if use pnpm
enable-pre-post-scripts=true
public-hoist-pattern[]=*@nextui-org/*
registry=https://registry.npmmirror.com/

18
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,18 @@
{
"css.validate": false,
"editor.formatOnSave": true,
"editor.tabSize": 2,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.organizeImports": "explicit"
},
"headwind.runOnSave": false,
"typescript.preferences.importModuleSpecifier": "non-relative",
"eslint.validate": ["javascript", "javascriptreact", "typescript"],
"typescript.tsdk": "node_modules/typescript/lib",
"commentTranslate.source": "Bing",
"cSpell.words": [
"contentlayer",
"lemonsqueezy"
]
}

293
README.md Normal file
View File

@@ -0,0 +1,293 @@
---
Ship your SaaS faster with Nexty
Nexty.dev is a comprehensive Next.js SaaS boilerplate that provides everything developers need to rapidly build and launch modern AI web applications
Try [Nexty.dev today](https://nexty.dev?utm_source=github-nextjs-starter)
---
[<img src="/public/try-nexty.webp">](https://nexty.dev?utm_source=github-nextjs-starter)
🌍 *[English](README.md) ∙ [简体中文](README_zh.md) ∙ [日本語](README_ja.md)*
# Next Forge - Multilingual Next.js 16 Starter
A feature-rich Next.js 16 multilingual starter template to help you quickly build globally-ready websites.
- [👉 Source Code](https://github.com/weijunext/nextjs-starter)
- [👉 Live Demo](https://nextforge.dev/)
**🚀 Looking for a full-featured SaaS Starter Template? [Check out the complete version](https://nexty.dev)**
## ✨ Features
- 🌐 Built-in i18n support (English, Chinese, Japanese)
- 🎨 Modern UI design with Tailwind CSS
- 🌙 Dark/Light theme toggle
- 📱 Responsive layout
- 📝 MDX blog system
- 🔍 SEO optimization
- 📊 Integrated analytics tools
- Google Analytics
- Baidu Analytics
- Google Adsense
- Vercel Analytics
## 🚀 Quick Start
### Prerequisites
- Node.js 20.9 or higher
- pnpm 9.0 or higher (recommended)
> **Note**: The project has configured `packageManager` field, we recommend using pnpm for the best experience.
### Installation
1. Clone the repository:
```bash
git clone https://github.com/weijunext/nextjs-starter.git
cd nextjs-starter
```
2. Enable Corepack (recommended):
```bash
corepack enable
```
3. Install dependencies:
```bash
pnpm install
# or use other package managers
npm install
yarn
```
4. Copy environment variables:
```bash
cp .env.example .env
```
5. Start the development server:
```bash
pnpm dev
# or npm run dev
```
Visit http://localhost:3000 to view your application.
## ⚙️ Configuration
1. Basic Setup
- Edit `config/site.ts` for website information
- Update icons and logo in `public/`
- Configure `app/sitemap.ts` for sitemap
- Update `app/robots.ts` for robots.txt
2. i18n Setup
- Add/modify language files in `i18n/messages/`
- Configure supported languages in `i18n/routing.ts`
- Set up i18n routing in `middleware.ts`
- Create pages under `app/[locale]/`
- Use the `Link` component from `i18n/routing.ts` instead of Next.js default
## 📝 Content Management
### Blog Posts
Create MDX files in `blog/[locale]` with the following format:
```markdown
---
title: Post Title
description: Post Description
image: /image.png
slug: /url-path
tags: tag1,tag2
date: 2025-02-20
visible: published
pin: true
---
Post content...
```
Reference `types/blog.ts` for supported fields.
### Static Pages
Manage static page content in `content/[page]/[locale].mdx`.
## 🔍 SEO Optimization
Built-in comprehensive SEO features:
- Server-side rendering and static generation
- Automatic sitemap.xml generation
- robots.txt configuration
- Optimized metadata
- Open Graph support
- Multilingual SEO support
## 📊 Analytics
Enable analytics by adding IDs in `.env`:
```
NEXT_PUBLIC_GOOGLE_ANALYTICS=
NEXT_PUBLIC_BAIDU_TONGJI=
NEXT_PUBLIC_GOOGLE_ADSENSE=
```
## 📁 Project Structure
```
nextjs-starter/
├── app/ # App directory
│ ├── [locale]/ # Internationalized routes
│ │ ├── about/ # About page
│ │ ├── blog/ # Blog pages
│ │ └── ... # Other pages
│ ├── api/ # API routes
│ └── globals/ # Global components
├── blog/ # Blog content (MDX)
│ ├── en/ # English blog
│ ├── ja/ # Japanese blog
│ └── zh/ # Chinese blog
├── components/ # Reusable components
│ ├── ui/ # Base UI components
│ ├── header/ # Header components
│ ├── footer/ # Footer components
│ └── ... # Other components
├── config/ # Configuration files
├── content/ # Static content (MDX)
├── i18n/ # Internationalization
│ ├── messages/ # Translation files
│ ├── routing.ts # Routing configuration
│ └── request.ts # Request configuration
├── lib/ # Utility functions
├── public/ # Static assets
└── types/ # Type definitions
```
## 🛠️ Tech Stack
- **Framework**: Next.js 16 (App Router)
- **Language**: TypeScript
- **Styling**: Tailwind CSS + Shadcn/ui
- **Internationalization**: next-intl
- **Content**: MDX
- **State Management**: Zustand
- **Deployment**: Vercel
- **Package Manager**: pnpm (recommended)
## 🚀 Deployment
### One-Click Deploy
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/weijunext/nextjs-starter&project-name=&repository-name=nextjs-starter&demo-title=NextjsStarter&demo-description=Nextjs%2015%20starter.&demo-url=https://nextforge.dev&demo-image=https://nextforge.dev/og.png)
### Manual Deployment to Vercel
1. Push your code to GitHub
2. Import project in Vercel
3. Configure environment variables
4. Deploy
### Other Platforms
```bash
# Build for production
pnpm build
# Start production server
pnpm start
```
## 💡 Development Best Practices
### Package Manager
- Project configured with `packageManager: "pnpm@10.12.4"`
- Enable Corepack: `corepack enable`
- Team members should use the same pnpm version
### Code Quality
```bash
# Lint code
pnpm lint
# Type checking
pnpm type-check
```
### Internationalization Development
1. Adding new language support:
- Add new language files in `i18n/messages/`
- Update `i18n/routing.ts` configuration
- Create corresponding language directories in `blog/` and `content/`
2. Using translations:
```tsx
import { useTranslations } from 'next-intl';
export default function MyComponent() {
const t = useTranslations('namespace');
return <h1>{t('title')}</h1>;
}
```
## 🔧 Troubleshooting
### Common Issues
**1. Package manager version mismatch**
```bash
# Remove node_modules and lockfile
rm -rf node_modules pnpm-lock.yaml
# Reinstall
pnpm install
```
**2. MDX files not displaying**
- Check file path is correct
- Verify frontmatter format
- Ensure `visible` field is set to `published`
**3. Internationalization routing issues**
- Use `Link` component from `i18n/routing.ts`
- Check `middleware.ts` configuration
**4. Styles not working**
- Verify Tailwind CSS class names are correct
- Try restarting development server
### Environment Variables
Ensure `.env` file contains necessary configuration:
```bash
# Copy example config
cp .env.example .env
# Modify as needed
```
## 📄 License
MIT
## 🤝 Contributing
Issues and Pull Requests are welcome!
## About the Author
Next.js full-stack specialist providing expert services in project development, performance optimization, and SEO improvement.
For consulting and training opportunities, reach out at weijunext@gmail.com
- [Github](https://github.com/weijunext)
- [Bento](https://bento.me/weijunext)
- [Twitter/X](https://twitter.com/judewei_dev)
<a href="https://www.buymeacoffee.com/weijunext" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" ></a>
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/G2G6TWWMG)

290
README_ja.md Normal file
View File

@@ -0,0 +1,290 @@
---
Nexty で SaaS を迅速に立ち上げよう - ビジュアル料金ダッシュボード、AI プレイグラウンド、エンタープライズレベル CMS を備えた唯一の Next.js ボイラープレート。多言語対応、認証、決済、メール機能、SEO 最適化も完備。
[Nexty.dev を今すぐ試す](https://nexty.dev?utm_source=github-nextjs-starter)
---
[<img src="/public/try-nexty.webp">](https://nexty.dev?utm_source=github-nextjs-starter)
🌍 *[English](README.md) ∙ [简体中文](README_zh.md) ∙ [日本語](README_ja.md)*
# Next Forge - 多言語対応 Next.js 16 スターター
グローバル対応のウェブサイトを素早く構築するための、機能豊富なNext.js 15多言語スターターテンプレートです。
- [👉 ソースコード](https://github.com/weijunext/nextjs-starter)
- [👉 デモサイト](https://nextforge.dev/)
**🚀 多機能で使いやすいフルスタックの起動テンプレートをお探しですか? ぜひ、当社の[アドバンス版](https://nexty.dev)をお試しください。**
## ✨ 主な機能
- 🌐 多言語対応(英語・中国語・日本語)
- 🎨 Tailwind CSSによるモダンなUI
- 🌙 ダーク/ライトテーマ切り替え
- 📱 レスポンシブデザイン
- 📝 MDXブログシステム
- 🔍 SEO最適化
- 📊 アナリティクスツール統合
- Google Analytics
- Baidu Analytics
- Google Adsense
- Vercel Analytics
## 🚀 クイックスタート
### 必要な環境
- Node.js 20.9 以上
- pnpm 9.0 以上(推奨)
> **注意**: プロジェクトには `packageManager` フィールドが設定されており、最適な体験のために pnpm の使用を推奨しています。
### インストール手順
1. リポジトリのクローン:
```bash
git clone https://github.com/weijunext/nextjs-starter.git
cd nextjs-starter
```
2. Corepack の有効化(推奨):
```bash
corepack enable
```
3. 依存関係のインストール:
```bash
pnpm install
# または他のパッケージマネージャーを使用
npm install
yarn
```
4. 環境変数の設定:
```bash
cp .env.example .env
```
5. 開発サーバーの起動:
```bash
pnpm dev
# または npm run dev
```
http://localhost:3000 にアクセスして確認できます。
## ⚙️ 設定方法
1. 基本設定
- `config/site.ts`でウェブサイト情報を編集
- `public/`内のアイコンとロゴを更新
- `app/sitemap.ts`でサイトマップを設定
- `app/robots.ts`でrobots.txtを更新
2. 多言語設定
- `i18n/messages/`内の言語ファイルを追加/編集
- `i18n/routing.ts`でサポートする言語を設定
- `middleware.ts`で多言語ルーティングを設定
- `app/[locale]/`配下にページを作成
- Next.jsデフォルトの代わりに`i18n/routing.ts``Link`コンポーネントを使用
## 📝 コンテンツ管理
### ブログ投稿
`blog/[locale]`にMDXファイルを以下のフォーマットで作成
```markdown
---
title: 投稿タイトル
description: 投稿の説明
image: /image.png
slug: /url-path
tags: tag1,tag2
date: 2025-02-20
visible: published
pin: true
---
投稿内容...
```
対応フィールドについては`types/blog.ts`を参照してください。
### 静的ページ
`content/[page]/[locale].mdx`で静的ページのコンテンツを管理します。
## 🔍 SEO最適化
包括的なSEO機能を搭載
- サーバーサイドレンダリングと静的生成
- sitemap.xml自動生成
- robots.txt設定
- 最適化されたメタデータ
- OGP対応
- 多言語SEOサポート
## 📊 アナリティクス
`.env`にIDを追加して有効化
```
NEXT_PUBLIC_GOOGLE_ANALYTICS=
NEXT_PUBLIC_BAIDU_TONGJI=
NEXT_PUBLIC_GOOGLE_ADSENSE=
```
## 📁 プロジェクト構成
```
nextjs-starter/
├── app/ # アプリディレクトリ
│ ├── [locale]/ # 多言語ルート
│ │ ├── about/ # Aboutページ
│ │ ├── blog/ # ブログページ
│ │ └── ... # その他のページ
│ ├── api/ # APIルート
│ └── globals/ # グローバルコンポーネント
├── blog/ # ブログコンテンツMDX
│ ├── en/ # 英語ブログ
│ ├── ja/ # 日本語ブログ
│ └── zh/ # 中国語ブログ
├── components/ # 再利用可能なコンポーネント
│ ├── ui/ # ベースUIコンポーネント
│ ├── header/ # ヘッダーコンポーネント
│ ├── footer/ # フッターコンポーネント
│ └── ... # その他のコンポーネント
├── config/ # 設定ファイル
├── content/ # 静的コンテンツMDX
├── i18n/ # 国際化設定
│ ├── messages/ # 翻訳ファイル
│ ├── routing.ts # ルーティング設定
│ └── request.ts # リクエスト設定
├── lib/ # ユーティリティ関数
├── public/ # 静的アセット
└── types/ # 型定義
```
## 🛠️ 技術スタック
- **フレームワーク**: Next.js 16 (App Router)
- **言語**: TypeScript
- **スタイリング**: Tailwind CSS + Shadcn/ui
- **国際化**: next-intl
- **コンテンツ**: MDX
- **状態管理**: Zustand
- **デプロイ**: Vercel
- **パッケージマネージャー**: pnpm推奨
## 🚀 デプロイ
### ワンクリックデプロイ
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/weijunext/nextjs-starter&project-name=&repository-name=nextjs-starter&demo-title=NextjsStarter&demo-description=Nextjs%2015%20starter.&demo-url=https://nextforge.dev&demo-image=https://nextforge.dev/og.png)
### Vercelへの手動デプロイ
1. GitHubにコードをプッシュ
2. Vercelでプロジェクトをインポート
3. 環境変数を設定
4. デプロイ
### その他のプラットフォーム
```bash
# プロダクション用ビルド
pnpm build
# プロダクションサーバーを起動
pnpm start
```
## 💡 開発のベストプラクティス
### パッケージマネージャー使用
- プロジェクトは `packageManager: "pnpm@10.12.4"` で設定済み
- Corepack を有効化: `corepack enable`
- チームメンバーは同じ pnpm バージョンを使用すべき
### コード品質
```bash
# コードリント
pnpm lint
# 型チェック
pnpm type-check
```
### 多言語開発
1. 新しい言語サポートの追加:
- `i18n/messages/` に新しい言語ファイルを追加
- `i18n/routing.ts` 設定を更新
- `blog/``content/` に対応する言語ディレクトリを作成
2. 翻訳の使用:
```tsx
import { useTranslations } from 'next-intl';
export default function MyComponent() {
const t = useTranslations('namespace');
return <h1>{t('title')}</h1>;
}
```
## 🔧 トラブルシューティング
### よくある問題
**1. パッケージマネージャーのバージョン不一致**
```bash
# node_modules と lockfile を削除
rm -rf node_modules pnpm-lock.yaml
# 再インストール
pnpm install
```
**2. MDXファイルが表示されない**
- ファイルパスが正しいか確認
- frontmatter のフォーマットが正しいか確認
- `visible` フィールドが `published` に設定されているか確認
**3. 多言語ルーティングの問題**
- `i18n/routing.ts``Link` コンポーネントを使用
- `middleware.ts` の設定を確認
**4. スタイルが効かない**
- Tailwind CSS のクラス名が正しいか確認
- 開発サーバーの再起動を試す
### 環境変数の問題
`.env` ファイルに必要な設定が含まれていることを確認:
```bash
# サンプル設定をコピー
cp .env.example .env
# 必要に応じて設定を変更
```
## 📄 ライセンス
MIT
## 🤝 コントリビューション
Issue、PRは大歓迎です
## 作者について
Next.jsのフルスタックスペシャリストとして、プロジェクト開発、パフォーマンス最適化、SEO改善のエキスパートサービスを提供しています。
コンサルティングやトレーニングについては、 weijunext@gmail.com までご連絡ください。
- [Github](https://github.com/weijunext)
- [Bento](https://bento.me/weijunext)
- [Twitter/X](https://twitter.com/judewei_dev)
<a href="https://www.buymeacoffee.com/weijunext" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" ></a>
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/G2G6TWWMG)

297
README_zh.md Normal file
View File

@@ -0,0 +1,297 @@
---
使用 Nexty 加速 SaaS 开发 - 唯一一个集成可视化定价面板、AI 开发环境和企业级 CMS 的 Next.js 模板,还包含多语言支持、身份验证、支付系统、电子邮件功能和 SEO 优化。
立即体验 [Nexty.dev](https://nexty.dev/?utm_source=github-nextjs-starter)
---
[<img src="/public/try-nexty.webp">](https://nexty.dev/?utm_source=github-nextjs-starter)
🌍 *[English](README.md) ∙ [简体中文](README_zh.md) ∙ [日本语](README_ja.md)*
# Next Forge - 多语言 Next.js 16 启动模板
一个轻量的 Next.js 16 多语言启动模板,帮助你快速构建面向全球的网站。
- [👉 源码地址](https://github.com/weijunext/nextjs-starter)
- [👉 在线预览](https://nextforge.dev/)
**🚀 如果你正在寻找功能完备的全栈启动模板,请了解我们的[高级版](https://nexty.dev/?utm_source=github-nextjs-starter)**
## ✨ 特性
- 🌐 内置多语言支持 (中文、英文、日语)
- 🎨 基于 Tailwind CSS 的现代 UI 设计
- 🌙 深色/浅色主题切换
- 📱 响应式布局
- 📝 MDX 博客系统
- 🔍 SEO 优化
- 📊 集成多个统计分析工具
- Google Analytics
- Baidu Analytics
- Google Adsense
- Vercel Analytics
## 🚀 快速开始
### 环境要求
- Node.js 20.9 或更高版本
- pnpm 9.0 或更高版本(推荐)
> **注意**: 项目已配置 `packageManager` 字段,推荐使用 pnpm 以获得最佳体验。
### 安装步骤
1. 克隆项目:
```bash
git clone https://github.com/weijunext/nextjs-starter.git
cd nextjs-starter
```
2. 启用 Corepack (推荐):
```bash
corepack enable
```
3. 安装依赖:
```bash
pnpm install
# 或使用其他包管理器
npm install
yarn
```
4. 复制环境变量文件:
```bash
cp .env.example .env
```
5. 启动开发服务器:
```bash
pnpm dev
# 或 npm run dev
```
访问 http://localhost:3000 查看你的应用。
## ⚙️ 配置
1. 基础配置
- 修改 `config/site.ts` 配置网站信息
- 修改 `public/` 下的图标和 logo
- 更新 `app/sitemap.ts` 配置站点地图
- 更新 `app/robots.ts` 配置 robots.txt
2. 多语言配置
-`i18n/messages/` 下添加或修改语言文件
-`i18n/routing.ts` 中配置支持的语言
-`middleware.ts` 中配置多语言路由
-`app/[locale]/` 目录下创建页面
- 多语言页面使用 `i18n/routing.ts` 导出的 `Link` 组件替代 next.js 的
## 📝 内容管理
### 博客文章
`blog/[locale]` 目录下创建 MDX 文件,支持以下格式:
```markdown
---
title: 文章标题
description: 文章描述
image: /image.png
slug: /url-path
tags: tag1,tag2
date: 2025-02-20
visible: published
pin: true
---
文章内容...
```
可参考类型定义 `types/blog.ts` 确认支持的字段。
### 静态页面
`content/[page]/[locale].mdx` 下管理静态页面内容。
## 🔍 SEO 优化
模板内置了完整的 SEO 优化方案:
- 服务端渲染和静态生成
- 自动生成 sitemap.xml
- 配置 robots.txt
- 优化的 metadata
- 支持 Open Graph
- 多语言 SEO 支持
## 📊 统计分析
`.env` 文件中配置相应的 ID 即可启用:
```
NEXT_PUBLIC_GOOGLE_ANALYTICS=
NEXT_PUBLIC_BAIDU_TONGJI=
NEXT_PUBLIC_GOOGLE_ADSENSE=
```
## 📁 项目结构
```
nextjs-starter/
├── app/ # 应用路由目录
│ ├── [locale]/ # 多语言路由
│ │ ├── about/ # 关于页面
│ │ ├── blog/ # 博客页面
│ │ └── ... # 其他页面
│ ├── api/ # API 路由
│ └── globals/ # 全局组件
├── blog/ # 博客内容 (MDX)
│ ├── en/ # 英文博客
│ ├── ja/ # 日文博客
│ └── zh/ # 中文博客
├── components/ # 可复用组件
│ ├── ui/ # 基础 UI 组件
│ ├── header/ # 头部组件
│ ├── footer/ # 底部组件
│ └── ... # 其他组件
├── config/ # 配置文件
├── content/ # 静态内容 (MDX)
├── i18n/ # 国际化配置
│ ├── messages/ # 翻译文件
│ ├── routing.ts # 路由配置
│ └── request.ts # 请求配置
├── lib/ # 工具函数
├── public/ # 静态资源
└── types/ # 类型定义
```
## 🛠️ 技术栈
- **框架**: Next.js 16 (App Router)
- **语言**: TypeScript
- **样式**: Tailwind CSS + Shadcn/ui
- **国际化**: next-intl
- **内容**: MDX
- **状态管理**: Zustand
- **部署**: Vercel
- **包管理器**: pnpm (推荐)
## 🚀 部署
### 一键部署
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/weijunext/nextjs-starter&project-name=&repository-name=nextjs-starter&demo-title=NextjsStarter&demo-description=Nextjs%2015%20starter.&demo-url=https://nextforge.dev&demo-image=https://nextforge.dev/og.png)
### 手动部署到 Vercel
1. 推送代码到 GitHub
2. 在 Vercel 中导入项目
3. 配置环境变量
4. 部署
### 其他平台部署
```bash
# 构建生产版本
pnpm build
# 启动生产服务器
pnpm start
```
## 💡 开发最佳实践
### 包管理器使用
- 项目已配置 `packageManager: "pnpm@10.12.4"`
- 建议启用 Corepack: `corepack enable`
- 团队成员应使用相同版本的 pnpm
### 代码规范
```bash
# 代码检查
pnpm lint
# 类型检查
pnpm type-check
```
### 多语言开发
1. 新增语言支持:
-`i18n/messages/` 添加新的语言文件
- 更新 `i18n/routing.ts` 配置
-`blog/``content/` 下创建对应语言目录
2. 使用翻译:
```tsx
import { useTranslations } from 'next-intl';
export default function MyComponent() {
const t = useTranslations('namespace');
return <h1>{t('title')}</h1>;
}
```
## 🔧 故障排除
### 常见问题
**1. 包管理器版本不一致**
```bash
# 删除 node_modules 和 lockfile
rm -rf node_modules pnpm-lock.yaml
# 重新安装
pnpm install
```
**2. MDX 文件不显示**
- 检查文件路径是否正确
- 确认 frontmatter 格式正确
- 检查 `visible` 字段是否设置为 `published`
**3. 多语言路由问题**
- 确保使用 `i18n/routing.ts` 中的 `Link` 组件
- 检查 `middleware.ts` 配置
**4. 样式不生效**
- 确认 Tailwind CSS 类名拼写正确
- 检查是否需要重启开发服务器
### 环境变量问题
确保 `.env` 文件包含必要的配置:
```bash
# 复制示例配置
cp .env.example .env
# 根据需要修改配置
```
## 📄 许可证
MIT
## 🤝 贡献
欢迎提交 Issue 和 Pull Request
## 关于作者
专注于 Next.js 全栈开发欢迎探讨开发、咨询与培训等合作机会联系微信bigye_chengpu
- [Github](https://github.com/weijunext)
- [Twitter/X](https://twitter.com/weijunext)
- [博客 - J实验室](https://weijunext.com)
- [Medium](https://medium.com/@weijunext)
- [掘金](https://juejin.cn/user/26044008768029)
- [知乎](https://www.zhihu.com/people/mo-mo-mo-89-12-11)
<a href="https://www.buymeacoffee.com/weijunext" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" ></a>
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/G2G6TWWMG)
<img src="./public/zs.jpeg" alt="赞赏作者" style="height: 200px; width: 200px">

125
actions/newsletter.ts Normal file
View File

@@ -0,0 +1,125 @@
import { normalizeEmail, validateEmail } from '@/lib/email';
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
import { headers } from 'next/headers';
import { Resend } from 'resend';
// initialize resend
const resend = new Resend(process.env.RESEND_API_KEY);
// Resend Audience ID
const AUDIENCE_ID = process.env.RESEND_AUDIENCE_ID!;
// initialize Redis
const redis = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL!,
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
});
const REDIS_RATE_LIMIT_KEY = process.env.UPSTASH_REDIS_NEWSLETTER_RATE_LIMIT_KEY!;
const DAY_MAX_SUBMISSIONS = parseInt(process.env.DAY_MAX_SUBMISSIONS || '10');
// create rate limiter
const limiter = new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(DAY_MAX_SUBMISSIONS, '1d'),
prefix: REDIS_RATE_LIMIT_KEY,
});
// Shared rate limit check
async function checkRateLimit() {
const headersList = await headers();
const ip = headersList.get('x-real-ip') ||
headersList.get('x-forwarded-for') ||
'unknown';
const { success } = await limiter.limit(ip);
if (!success) {
throw new Error('Too many submissions, please try again later');
}
}
export async function subscribeToNewsletter(email: string) {
try {
await checkRateLimit();
const normalizedEmail = normalizeEmail(email);
const { isValid, error } = validateEmail(normalizedEmail);
if (!isValid) {
throw new Error(error || 'Invalid email address');
}
// Check if already subscribed
// const list = await resend.contacts.list({ audienceId: AUDIENCE_ID });
// const user = list.data?.data.find((item) => item.email === normalizedEmail);
// if (user) {
// return { success: true, alreadySubscribed: true };
// }
// Add to audience
await resend.contacts.create({
audienceId: AUDIENCE_ID,
email: normalizedEmail,
});
// Send welcome email
const unsubscribeToken = Buffer.from(normalizedEmail).toString('base64');
const unsubscribeLink = `${process.env.NEXT_PUBLIC_SITE_URL}/unsubscribe?token=${unsubscribeToken}`;
await resend.emails.send({
from: 'NextForge <' + process.env.ADMIN_EMAIL! + '>',
to: normalizedEmail,
subject: 'Welcome to Next Forge',
html: `
<h2>Welcome to Next Forge</h2>
<p>Thank you for subscribing to the newsletter. You will receive the latest updates and news.</p>
<p style="margin-top: 20px; font-size: 12px; color: #666;">
If you wish to unsubscribe, please <a href="${unsubscribeLink}">click here</a>
</p>
`,
headers: {
"List-Unsubscribe": `<${unsubscribeLink}>`,
"List-Unsubscribe-Post": "List-Unsubscribe=One-Click"
}
});
return { success: true };
} catch (error) {
console.error('Newsletter subscription failed:', error);
throw error;
}
}
export async function unsubscribeFromNewsletter(token: string) {
try {
await checkRateLimit();
const email = Buffer.from(token, 'base64').toString();
const normalizedEmail = normalizeEmail(email);
const { isValid, error } = validateEmail(normalizedEmail);
if (!isValid) {
throw new Error(error || 'Invalid email address');
}
// Check if subscribed
const list = await resend.contacts.list({ audienceId: AUDIENCE_ID });
const user = list.data?.data.find((item) => item.email === normalizedEmail);
if (!user) {
throw new Error('This email is not subscribed to our notifications');
}
// Remove from audience
await resend.contacts.remove({
audienceId: AUDIENCE_ID,
email: normalizedEmail,
});
return { success: true, email: normalizedEmail };
} catch (error) {
console.error('Newsletter unsubscribe failed:', error);
throw error;
}
}

33
app/BaiDuAnalytics.tsx Normal file
View File

@@ -0,0 +1,33 @@
"use client";
import Script from "next/script";
const BaiDuAnalytics = () => {
return (
<>
{process.env.NEXT_PUBLIC_BAIDU_TONGJI ? (
<>
<Script
id="baidu-tongji"
strategy="afterInteractive"
dangerouslySetInnerHTML={{
__html: `
var _hmt = _hmt || [];
(function() {
var hm = document.createElement("script");
hm.src = "https://hm.baidu.com/hm.js?${process.env.NEXT_PUBLIC_BAIDU_TONGJI}";
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(hm, s);
})();
`,
}}
/>
</>
) : (
<></>
)}
</>
);
};
export default BaiDuAnalytics;

24
app/GoogleAdsense.tsx Normal file
View File

@@ -0,0 +1,24 @@
"use client";
import Script from "next/script";
const GoogleAdsense = () => {
return (
<>
{process.env.NEXT_PUBLIC_GOOGLE_ADSENSE_ID ? (
<>
<Script
async
src={`https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-${process.env.NEXT_PUBLIC_GOOGLE_ADSENSE_ID}`}
crossOrigin="anonymous"
strategy="afterInteractive"
/>
</>
) : (
<></>
)}
</>
);
};
export default GoogleAdsense;

37
app/GoogleAnalytics.tsx Normal file
View File

@@ -0,0 +1,37 @@
"use client";
import Script from "next/script";
import * as gtag from "../gtag.js";
const GoogleAnalytics = () => {
return (
<>
{gtag.GA_TRACKING_ID ? (
<>
<Script
strategy="afterInteractive"
src={`https://www.googletagmanager.com/gtag/js?id=${gtag.GA_TRACKING_ID}`}
/>
<Script
id="gtag-init"
strategy="afterInteractive"
dangerouslySetInnerHTML={{
__html: `
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${gtag.GA_TRACKING_ID}', {
page_path: window.location.pathname,
});
`,
}}
/>
</>
) : (
<></>
)}
</>
);
};
export default GoogleAnalytics;

View File

@@ -0,0 +1,39 @@
"use client";
import Script from "next/script";
// 你可以将域名放在环境变量中,这里先直接使用你提供的域名
const PLAUSIBLE_DOMAIN = process.env.NEXT_PUBLIC_PLAUSIBLE_DOMAIN;
const PLAUSIBLE_SRC = process.env.NEXT_PUBLIC_PLAUSIBLE_SRC;
const PlausibleAnalytics = () => {
return (
<>
{PLAUSIBLE_DOMAIN ? (
<>
<Script
strategy="afterInteractive"
data-domain={PLAUSIBLE_DOMAIN}
src={PLAUSIBLE_SRC}
defer
/>
<Script
id="plausible-init"
strategy="afterInteractive"
dangerouslySetInnerHTML={{
__html: `
window.plausible = window.plausible || function() {
(window.plausible.q = window.plausible.q || []).push(arguments)
}
`,
}}
/>
</>
) : (
<></>
)}
</>
);
};
export default PlausibleAnalytics;

View File

@@ -0,0 +1,78 @@
import MDXComponents from "@/components/mdx/MDXComponents";
import { Locale, LOCALES } from "@/i18n/routing";
import { constructMetadata } from "@/lib/metadata";
import fs from "fs/promises";
import { Metadata } from "next";
import { getTranslations } from "next-intl/server";
import { MDXRemote } from "next-mdx-remote-client/rsc";
import path from "path";
import remarkGfm from "remark-gfm";
const options = {
parseFrontmatter: true,
mdxOptions: {
remarkPlugins: [remarkGfm],
rehypePlugins: [],
},
};
async function getMDXContent(locale: string) {
const filePath = path.join(
process.cwd(),
"content",
"about",
`${locale}.mdx`
);
try {
const content = await fs.readFile(filePath, "utf-8");
return content;
} catch (error) {
console.error(`Error reading MDX file: ${error}`);
return "";
}
}
type Params = Promise<{
locale: string;
}>;
type MetadataProps = {
params: Params;
};
export async function generateMetadata({
params,
}: MetadataProps): Promise<Metadata> {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: "About" });
return constructMetadata({
page: "About",
title: t("title"),
description: t("description"),
locale: locale as Locale,
path: `/about`,
canonicalUrl: `/about`,
});
}
export default async function AboutPage({ params }: { params: Params }) {
const { locale } = await params;
const content = await getMDXContent(locale);
return (
<article className="w-full md:w-3/5 px-2 md:px-12">
<MDXRemote
source={content}
components={MDXComponents}
options={options}
/>
</article>
);
}
export async function generateStaticParams() {
return LOCALES.map((locale) => ({
locale,
}));
}

View File

@@ -0,0 +1,31 @@
import { Link as I18nLink } from "@/i18n/routing";
import { BlogPost } from "@/types/blog";
import dayjs from "dayjs";
import Image from "next/image";
export function BlogCard({ post, locale }: { post: BlogPost; locale: string }) {
return (
<I18nLink
href={`/blog${post.slug}`}
prefetch={false}
className="bg-transparent rounded-lg hover:underline"
>
<div className="relative rounded shadow-md pt-[56.25%]">
<Image
src={post.image || "/placeholder.svg"}
alt={post.title}
fill
className="object-cover shadow-sm w-full rounded hover:shadow-lg transition-shadow duration-200 h-[200p]"
/>
</div>
<div className="py-3 flex-1 flex flex-col">
<h2 className="text-lg font-500 line-clamp-2 flex-grow">
{post.title}
</h2>
<p className="text-gray-600 dark:text-gray-400 text-sm mt-2">
{dayjs(post.date).format("YYYY-MM-DD")}
</p>
</div>
</I18nLink>
);
}

View File

@@ -0,0 +1,103 @@
import { Callout } from "@/components/mdx/Callout";
import MDXComponents from "@/components/mdx/MDXComponents";
import { Locale, LOCALES } from "@/i18n/routing";
import { getPosts } from "@/lib/getBlogs";
import { constructMetadata } from "@/lib/metadata";
import { BlogPost } from "@/types/blog";
import { Metadata } from "next";
import { MDXRemote } from "next-mdx-remote-client/rsc";
import { notFound } from "next/navigation";
type Params = Promise<{
locale: string;
slug: string;
}>;
type MetadataProps = {
params: Params;
};
export async function generateMetadata({
params,
}: MetadataProps): Promise<Metadata> {
const { locale, slug } = await params;
let { posts }: { posts: BlogPost[] } = await getPosts(locale);
const post = posts.find((post) => post.slug === "/" + slug);
if (!post) {
return constructMetadata({
title: "404",
description: "Page not found",
noIndex: true,
locale: locale as Locale,
path: `/blog/${slug}`,
canonicalUrl: `/blog/${slug}`,
});
}
return constructMetadata({
page: "blog",
title: post.title,
description: post.description,
images: post.image ? [post.image] : [],
locale: locale as Locale,
path: `/blog/${slug}`,
canonicalUrl: `/blog/${slug}`,
});
}
export default async function BlogPage({ params }: { params: Params }) {
const { locale, slug } = await params;
let { posts }: { posts: BlogPost[] } = await getPosts(locale);
const post = posts.find((item) => item.slug === "/" + slug);
if (!post) {
return notFound();
}
return (
<div className="w-full md:w-3/5 px-2 md:px-12">
<h1 className="break-words text-4xl font-bold mt-6 mb-4">{post.title}</h1>
{post.image && (
<img src={post.image} alt={post.title} className="rounded-sm" />
)}
{post.tags && post.tags.split(",").length ? (
<div className="flex flex-wrap gap-2">
{post.tags.split(",").map((tag) => {
return (
<div
key={tag}
className={`rounded-md bg-gray-200 hover:!no-underline dark:bg-[#24272E] flex px-2.5 py-1.5 text-sm font-medium transition-colors hover:text-black hover:dark:bg-[#15AFD04C] hover:dark:text-[#82E9FF] text-gray-500 dark:text-[#7F818C] outline-none focus-visible:ring transition`}
>
{tag.trim()}
</div>
);
})}
</div>
) : (
<></>
)}
{post.description && <Callout>{post.description}</Callout>}
<MDXRemote source={post?.content || ""} components={MDXComponents} />
</div>
);
}
export async function generateStaticParams() {
let posts = (await getPosts()).posts;
// Filter out posts without a slug
posts = posts.filter((post) => post.slug);
return LOCALES.flatMap((locale) =>
posts.map((post) => {
const slugPart = post.slug.replace(/^\//, "").replace(/^blog\//, "");
return {
locale,
slug: slugPart,
};
})
);
}

View File

@@ -0,0 +1,51 @@
import { BlogCard } from "@/app/[locale]/blog/BlogCard";
import { Locale, LOCALES } from "@/i18n/routing";
import { getPosts } from "@/lib/getBlogs";
import { constructMetadata } from "@/lib/metadata";
import { Metadata } from "next";
import { getTranslations } from "next-intl/server";
type Params = Promise<{ locale: string }>;
type MetadataProps = {
params: Params;
};
export async function generateMetadata({
params,
}: MetadataProps): Promise<Metadata> {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: "Blog" });
return constructMetadata({
page: "Blog",
title: t("title"),
description: t("description"),
locale: locale as Locale,
path: `/blog`,
canonicalUrl: `/blog`,
});
}
export default async function Page({ params }: { params: Params }) {
const { locale } = await params;
const { posts } = await getPosts(locale);
const t = await getTranslations("Blog");
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-4xl font-bold mb-8 text-center">{t("title")}</h1>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{posts.map((post) => (
<BlogCard key={post.slug} locale={locale} post={post} />
))}
</div>
</div>
);
}
export async function generateStaticParams() {
return LOCALES.map((locale) => ({ locale }));
}

2
app/[locale]/globals.css Normal file
View File

@@ -0,0 +1,2 @@
@import url(../../styles/globals.css);
@import url(../../styles/loading.css);

110
app/[locale]/layout.tsx Normal file
View File

@@ -0,0 +1,110 @@
import BaiDuAnalytics from "@/app/BaiDuAnalytics";
import GoogleAdsense from "@/app/GoogleAdsense";
import GoogleAnalytics from "@/app/GoogleAnalytics";
import PlausibleAnalytics from "@/app/PlausibleAnalytics";
import Footer from "@/components/footer/Footer";
import Header from "@/components/header/Header";
import { LanguageDetectionAlert } from "@/components/LanguageDetectionAlert";
import { TailwindIndicator } from "@/components/TailwindIndicator";
import { siteConfig } from "@/config/site";
import { DEFAULT_LOCALE, Locale, routing } from "@/i18n/routing";
import { constructMetadata } from "@/lib/metadata";
import { cn } from "@/lib/utils";
import "@/styles/globals.css";
import "@/styles/loading.css";
import { Analytics } from "@vercel/analytics/react";
import { Metadata, Viewport } from "next";
import { hasLocale, NextIntlClientProvider } from "next-intl";
import {
getMessages,
getTranslations,
setRequestLocale,
} from "next-intl/server";
import { ThemeProvider } from "next-themes";
import { notFound } from "next/navigation";
// import './globals.css';
type MetadataProps = {
params: Promise<{ locale: string }>;
};
export async function generateMetadata({
params,
}: MetadataProps): Promise<Metadata> {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: "Home" });
return constructMetadata({
page: "Home",
title: t("title"),
description: t("description"),
locale: locale as Locale,
path: `/`,
canonicalUrl: `/`,
});
}
export const viewport: Viewport = {
themeColor: siteConfig.themeColors,
};
export default async function LocaleLayout({
children,
params,
}: {
children: React.ReactNode;
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
// Ensure that the incoming `locale` is valid
if (!hasLocale(routing.locales, locale)) {
notFound();
}
setRequestLocale(locale);
// Providing all messages to the client
// side is the easiest way to get started
const messages = await getMessages();
return (
<html lang={locale || DEFAULT_LOCALE} suppressHydrationWarning>
<head />
<body
className={cn(
"min-h-screen bg-background flex flex-col font-sans antialiased"
)}
>
<NextIntlClientProvider messages={messages}>
<ThemeProvider
attribute="class"
defaultTheme={siteConfig.defaultNextTheme}
enableSystem
>
{messages.LanguageDetection && <LanguageDetectionAlert />}
{messages.Header && <Header />}
<main className="flex-1 flex flex-col items-center">
{children}
</main>
{messages.Footer && <Footer />}
</ThemeProvider>
</NextIntlClientProvider>
<TailwindIndicator />
{process.env.NODE_ENV === "development" ? (
<></>
) : (
<>
<Analytics />
<BaiDuAnalytics />
<GoogleAnalytics />
<GoogleAdsense />
<PlausibleAnalytics />
</>
)}
</body>
</html>
);
}

15
app/[locale]/page.tsx Normal file
View File

@@ -0,0 +1,15 @@
import HomeComponent from "@/components/home";
// export const dynamic = "force-static";
export default function Home() {
return <HomeComponent />;
}
// export async function generateStaticParams() {
// return [
// { locale: 'en' },
// { locale: 'zh' },
// { locale: 'ja' },
// ]
// }

View File

@@ -0,0 +1,78 @@
import MDXComponents from "@/components/mdx/MDXComponents";
import { Locale, LOCALES } from "@/i18n/routing";
import { constructMetadata } from "@/lib/metadata";
import fs from "fs/promises";
import { Metadata } from "next";
import { getTranslations } from "next-intl/server";
import { MDXRemote } from "next-mdx-remote-client/rsc";
import path from "path";
import remarkGfm from "remark-gfm";
const options = {
parseFrontmatter: true,
mdxOptions: {
remarkPlugins: [remarkGfm],
rehypePlugins: [],
},
};
async function getMDXContent(locale: string) {
const filePath = path.join(
process.cwd(),
"content",
"privacy-policy",
`${locale}.mdx`
);
try {
const content = await fs.readFile(filePath, "utf-8");
return content;
} catch (error) {
console.error(`Error reading MDX file: ${error}`);
return "";
}
}
type Params = Promise<{
locale: string;
}>;
type MetadataProps = {
params: Params;
};
export async function generateMetadata({
params,
}: MetadataProps): Promise<Metadata> {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: "PrivacyPolicy" });
return constructMetadata({
page: "PrivacyPolicy",
title: t("title"),
description: t("description"),
locale: locale as Locale,
path: `/privacy-policy`,
canonicalUrl: `/privacy-policy`,
});
}
export default async function AboutPage({ params }: { params: Params }) {
const { locale } = await params;
const content = await getMDXContent(locale);
return (
<article className="w-full md:w-3/5 px-2 md:px-12">
<MDXRemote
source={content}
components={MDXComponents}
options={options}
/>
</article>
);
}
export async function generateStaticParams() {
return LOCALES.map((locale) => ({
locale,
}));
}

View File

@@ -0,0 +1,78 @@
import MDXComponents from "@/components/mdx/MDXComponents";
import { Locale, LOCALES } from "@/i18n/routing";
import { constructMetadata } from "@/lib/metadata";
import fs from "fs/promises";
import { Metadata } from "next";
import { getTranslations } from "next-intl/server";
import { MDXRemote } from "next-mdx-remote-client/rsc";
import path from "path";
import remarkGfm from "remark-gfm";
const options = {
parseFrontmatter: true,
mdxOptions: {
remarkPlugins: [remarkGfm],
rehypePlugins: [],
},
};
async function getMDXContent(locale: string) {
const filePath = path.join(
process.cwd(),
"content",
"terms-of-service",
`${locale}.mdx`
);
try {
const content = await fs.readFile(filePath, "utf-8");
return content;
} catch (error) {
console.error(`Error reading MDX file: ${error}`);
return "";
}
}
type Params = Promise<{
locale: string;
}>;
type MetadataProps = {
params: Params;
};
export async function generateMetadata({
params,
}: MetadataProps): Promise<Metadata> {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: "TermsOfService" });
return constructMetadata({
page: "TermsOfService",
title: t("title"),
description: t("description"),
locale: locale as Locale,
path: `/terms-of-service`,
canonicalUrl: `/terms-of-service`,
});
}
export default async function AboutPage({ params }: { params: Params }) {
const { locale } = await params;
const content = await getMDXContent(locale);
return (
<article className="w-full md:w-3/5 px-2 md:px-12">
<MDXRemote
source={content}
components={MDXComponents}
options={options}
/>
</article>
);
}
export async function generateStaticParams() {
return LOCALES.map((locale) => ({
locale,
}));
}

31
app/robots.ts Normal file
View File

@@ -0,0 +1,31 @@
import { siteConfig } from '@/config/site'
import type { MetadataRoute } from 'next'
const siteUrl = siteConfig.url
// export const dynamic = "force-static"
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: '*',
allow: '/',
disallow: [
'/private/',
'/api/',
'/auth/',
'/dashboard/',
'/_next/',
'/assets/',
'/error',
'/*/404',
'/*/500',
'/*/403',
'/*/401',
'/*/400',
'/cdn-cgi/',
],
},
sitemap: `${siteUrl}/sitemap.xml`
}
}

48
app/sitemap.ts Normal file
View File

@@ -0,0 +1,48 @@
import { siteConfig } from '@/config/site'
import { DEFAULT_LOCALE, LOCALES } from '@/i18n/routing'
import { getPosts } from '@/lib/getBlogs'
import { MetadataRoute } from 'next'
const siteUrl = siteConfig.url
type ChangeFrequency = 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never' | undefined
// export const dynamic = "force-static"
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
// Static pages
const staticPages = [
'',
'/blog',
'/about',
'/privacy-policy',
'/terms-of-service',
]
// Generate multilingual pages
const pages = LOCALES.flatMap(locale => {
return staticPages.map(page => ({
url: `${siteUrl}${locale === DEFAULT_LOCALE ? '' : `/${locale}`}${page}`,
lastModified: new Date(),
changeFrequency: 'daily' as ChangeFrequency,
priority: page === '' ? 1.0 : 0.8,
}))
})
const blogPosts = await Promise.all(
LOCALES.map(async (locale) => {
const { posts } = await getPosts(locale)
return posts.map(post => ({
url: `${siteUrl}${locale === DEFAULT_LOCALE ? '' : `/${locale}`}/blog${post.slug}`,
lastModified: post.metadata.updatedAt || post.date,
changeFrequency: 'daily' as const,
priority: 0.7,
}))
})
).then(results => results.flat())
return [
...pages,
...blogPosts,
]
}

18
blogs/en/1.demo.mdx Normal file
View File

@@ -0,0 +1,18 @@
---
title: demo
description: it is a description
slug: /demo
tags: nextjs,i18n,mdx,starter,robots,sitemap
date: 2025-02-16
visible: published
# visible: draft/invisible/published (published is default)
pin: pin
---
## Introduction
demo
## How to use
demo

18
blogs/ja/1.demo.mdx Normal file
View File

@@ -0,0 +1,18 @@
---
title: デモ
description: これはデモプロジェクトの説明です
slug: /demo
tags: nextjs,i18n,mdx,starter,robots,sitemap
date: 2025-02-16
visible: published
# visible: draft/invisible/published、デフォルトはpublished
# pin: pin
---
## はじめに
デモ
## 使い方
デモ

18
blogs/zh/1.demo.mdx Normal file
View File

@@ -0,0 +1,18 @@
---
title: 示例
description: 这是一个描述
slug: /demo
tags: nextjs,i18n,mdx,starter,robots,sitemap
date: 2025-02-16
visible: published
# visible: draft/invisible/published默认为published
pin: true
---
## 简介
示例
## 如何使用
示例

18
blogs/zh/2.demo2.mdx Normal file
View File

@@ -0,0 +1,18 @@
---
title: 示例2
description: 这是一个描述2
slug: /demo2
tags: nextjs,i18n,mdx,starter,robots,sitemap
date: 2025-02-17
visible: published
# visible: draft/invisible/published默认为published
# pin: true
---
## 简介
示例2
## 如何使用
示例2

21
components.json Normal file
View File

@@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "styles/globals.css",
"baseColor": "zinc",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

View File

@@ -0,0 +1,44 @@
import { buttonVariants } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import Link from "next/link";
// Nexty.dev Affiliate Link: https://affiliates.nexty.dev/
// sign up and use your affiliate link on BuiltWithButton to earn money
export default function BuiltWithButton() {
return (
<Link
href="https://nexty.dev"
title="Built with Nexty.dev"
prefetch={false}
target="_blank"
rel="noopener noreferrer"
className={cn(
buttonVariants({ variant: "outline", size: "sm" }),
"px-4 rounded-md bg-transparent border-gray-500 hover:bg-gray-950 text-white hover:text-gray-100"
)}
>
<span>Built with</span>
<span>
<LogoNexty className="size-4 rounded-full" />
</span>
<span className="font-bold text-base-content flex gap-0.5 items-center tracking-tight">
Nexty.dev
</span>
</Link>
);
}
function LogoNexty({ className }: { className?: string }) {
return (
<img
src="/logo_nexty.png"
alt="Logo"
title="Logo"
loading="lazy"
width={96}
height={96}
className={cn("size-8 rounded-md", className)}
/>
);
}

View File

@@ -0,0 +1,144 @@
"use client";
import { Button } from "@/components/ui/button";
import { Link as I18nLink, LOCALE_NAMES, routing } from "@/i18n/routing";
import { cn } from "@/lib/utils";
import { useLocaleStore } from "@/stores/localeStore";
import { ArrowRight, Globe, X } from "lucide-react";
import { useLocale } from "next-intl";
import { useCallback, useEffect, useState } from "react";
export function LanguageDetectionAlert() {
const [countdown, setCountdown] = useState(10); // countdown 10s and dismiss
const [isVisible, setIsVisible] = useState(false);
const locale = useLocale();
const [detectedLocale, setDetectedLocale] = useState<string | null>(null);
const {
showLanguageAlert,
setShowLanguageAlert,
dismissLanguageAlert,
getLangAlertDismissed,
} = useLocaleStore();
const handleDismiss = useCallback(() => {
setIsVisible(false);
setTimeout(() => {
dismissLanguageAlert();
}, 300);
}, [dismissLanguageAlert]);
const handleSwitchLanguage = useCallback(() => {
dismissLanguageAlert();
}, [dismissLanguageAlert]);
useEffect(() => {
const detectedLang = navigator.language; // Get full language code, e.g., zh_HK
const storedDismiss = getLangAlertDismissed();
if (!storedDismiss) {
let supportedLang = routing.locales.find((l) => l === detectedLang);
if (!supportedLang) {
const mainLang = detectedLang.split("-")[0];
supportedLang = routing.locales.find((l) => l.startsWith(mainLang));
}
if (supportedLang && supportedLang !== locale) {
setDetectedLocale(supportedLang);
setShowLanguageAlert(true);
setTimeout(() => setIsVisible(true), 100);
}
}
}, [locale, getLangAlertDismissed, setShowLanguageAlert]);
useEffect(() => {
let timer: NodeJS.Timeout;
if (showLanguageAlert && countdown > 0) {
timer = setInterval(() => {
setCountdown((prev) => prev - 1);
}, 1000);
}
return () => {
if (timer) clearInterval(timer);
};
}, [showLanguageAlert, countdown]);
useEffect(() => {
if (countdown === 0 && showLanguageAlert) {
handleDismiss();
}
}, [countdown, showLanguageAlert, handleDismiss]);
if (!showLanguageAlert || !detectedLocale) return null;
const messages = require(`@/i18n/messages/${detectedLocale}.json`);
const alertMessages = messages.LanguageDetection;
return (
<div
className={cn(
"fixed top-16 right-4 z-50 max-w-sm w-full mx-4 sm:mx-0 sm:w-96",
"transform transition-all duration-300 ease-in-out",
isVisible
? "translate-x-0 translate-y-0 opacity-100"
: "translate-x-full opacity-0"
)}
role="banner"
aria-live="polite"
aria-label="Language detection alert"
>
<div className="bg-background/95 backdrop-blur-md border border-border rounded-xl shadow-lg p-4 relative">
<Button
variant="ghost"
size="icon"
className="absolute right-2 top-2 h-6 w-6 opacity-50 hover:opacity-100"
onClick={handleDismiss}
aria-label="Dismiss language suggestion"
>
<X className="h-4 w-4" />
</Button>
<div className="pr-8">
<div className="flex items-center gap-2 mb-3">
<Globe className="h-4 w-4 text-primary" />
<h3 className="font-medium text-sm text-foreground">
{alertMessages.title}
</h3>
</div>
<p className="text-xs text-muted-foreground mb-4 leading-relaxed">
{alertMessages.description}
</p>
<div className="flex items-center justify-between">
<Button asChild onClick={handleSwitchLanguage}>
<I18nLink
href="/"
title={`${alertMessages.switchTo} ${LOCALE_NAMES[detectedLocale]}`}
locale={detectedLocale as any}
className={cn(
"flex items-center gap-2 px-3 py-2 rounded-lg",
"bg-primary text-primary-foreground hover:bg-primary/90",
"text-sm font-medium transition-colors",
"group focus:outline-none focus:ring-2 focus:ring-primary/50"
)}
aria-label={`${alertMessages.switchTo} ${LOCALE_NAMES[detectedLocale]}`}
>
<span>
{alertMessages.switchTo} {LOCALE_NAMES[detectedLocale]}
</span>
<ArrowRight className="h-3 w-3 transition-transform group-hover:translate-x-0.5" />
</I18nLink>
</Button>
<span className="text-xs text-muted-foreground">{countdown}s</span>
</div>
</div>
<div className="absolute inset-0 rounded-xl bg-gradient-to-r from-primary/10 to-transparent pointer-events-none opacity-50" />
</div>
</div>
);
}

View File

@@ -0,0 +1,71 @@
"use client";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Locale,
LOCALE_NAMES,
routing,
usePathname,
useRouter,
} from "@/i18n/routing";
import { useLocaleStore } from "@/stores/localeStore";
import { Globe } from "lucide-react";
import { useLocale } from "next-intl";
import { useParams } from "next/navigation";
import { useEffect, useState, useTransition } from "react";
export default function LocaleSwitcher() {
const router = useRouter();
const pathname = usePathname();
const params = useParams();
const locale = useLocale();
const { dismissLanguageAlert } = useLocaleStore();
const [, startTransition] = useTransition();
const [currentLocale, setCurrentLocale] = useState("locale");
useEffect(() => {
setCurrentLocale(locale);
}, [locale, setCurrentLocale]);
function onSelectChange(nextLocale: Locale) {
setCurrentLocale(nextLocale);
dismissLanguageAlert();
startTransition(() => {
router.replace(
// @ts-expect-error -- TypeScript will validate that only known `params`
// are used in combination with a given `pathname`. Since the two will
// always match for the current route, we can skip runtime checks.
// { pathname: "/", params: params || {} }, // if your want to redirect to the home page
{ pathname, params: params || {} }, // if your want to redirect to the current page
{ locale: nextLocale }
);
});
}
return (
<Select
defaultValue={locale}
value={currentLocale}
onValueChange={onSelectChange}
>
<SelectTrigger className="w-fit">
<Globe className="w-4 h-4 mr-1" />
<SelectValue placeholder="Language" />
</SelectTrigger>
<SelectContent>
{routing.locales.map((cur) => (
<SelectItem key={cur} value={cur}>
{LOCALE_NAMES[cur]}
</SelectItem>
))}
</SelectContent>
</Select>
);
}

View File

@@ -0,0 +1,14 @@
export function TailwindIndicator() {
if (process.env.NODE_ENV === "production") return null
return (
<div className="fixed bottom-1 left-1 z-50 flex h-6 w-6 items-center justify-center rounded-full bg-gray-800 p-3 font-mono text-xs text-white">
<div className="sm:hidden">xs</div>
<div className="hidden sm:block md:hidden">sm</div>
<div className="hidden md:block lg:hidden">md</div>
<div className="hidden lg:block xl:hidden">lg</div>
<div className="hidden xl:block 2xl:hidden">xl</div>
<div className="hidden 2xl:block">2xl</div>
</div>
)
}

View File

@@ -0,0 +1,39 @@
"use client";
import { Moon, Sun } from "lucide-react";
import { useTheme } from "next-themes";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
export function ThemeToggle() {
const { setTheme } = useTheme();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon">
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme("light")}>
Light
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")}>
Dark
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("system")}>
System
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}

108
components/WebsiteLogo.tsx Normal file
View File

@@ -0,0 +1,108 @@
"use client";
import { getDomain } from "@/lib/utils";
import { useEffect, useState } from "react";
interface IProps {
url: string;
size?: number;
className?: string;
timeout?: number;
}
const WebsiteLogo = ({
url,
size = 32,
className = "",
timeout = 1000, // 1 second
}: IProps) => {
const domain = getDomain(url);
const [imgSrc, setImgSrc] = useState(`https://${domain}/logo.svg`);
const [fallbackIndex, setFallbackIndex] = useState(0);
const [isLoading, setIsLoading] = useState(true);
const [hasError, setHasError] = useState(false);
const fallbackSources = [
`https://${domain}/logo.svg`,
`https://${domain}/logo.png`,
`https://${domain}/apple-touch-icon.png`,
`https://${domain}/apple-touch-icon-precomposed.png`,
`https://www.google.com/s2/favicons?domain=${domain}&sz=64`,
`https://icons.duckduckgo.com/ip3/${domain}.ico`,
`https://${domain}/favicon.ico`,
];
useEffect(() => {
let timeoutId: any;
if (isLoading) {
timeoutId = setTimeout(() => {
handleError();
}, timeout);
}
return () => {
if (timeoutId) {
clearTimeout(timeoutId);
}
};
}, [imgSrc, isLoading]);
const handleError = () => {
const nextIndex = fallbackIndex + 1;
if (nextIndex < fallbackSources.length) {
setFallbackIndex(nextIndex);
setImgSrc(fallbackSources[nextIndex]);
setIsLoading(true);
} else {
setHasError(true);
setIsLoading(false);
}
};
const handleLoad = () => {
setIsLoading(false);
setHasError(false);
};
return (
<div
className={`relative inline-block ${className}`}
style={{ width: size, height: size }}
>
{/* placeholder */}
{isLoading && (
<div className="absolute inset-0 animate-pulse">
<div className="w-full h-full rounded-md bg-gray-200/60" />
</div>
)}
<img
src={imgSrc}
alt={`${domain} logo`}
width={size}
height={size}
onError={handleError}
onLoad={handleLoad}
className={`inline-block transition-opacity duration-300 ${
isLoading ? "opacity-0" : "opacity-100"
}`}
style={{
objectFit: "contain",
display: hasError ? "none" : "inline-block",
}}
/>
{/* Fallback: Display first letter of domain when all image sources fail */}
{hasError && (
<div
className="w-full h-full flex items-center justify-center bg-gray-100 rounded-md"
style={{ fontSize: `${size * 0.5}px` }}
>
{domain.charAt(0).toUpperCase()}
</div>
)}
</div>
);
};
export default WebsiteLogo;

View File

@@ -0,0 +1,20 @@
"use client";
export default function Badges() {
return (
<div className="py-6 flex justify-center w-full flex-wrap gap-2">
<a
href="https://dofollow.tools"
title="Featured on Dofollow.Tools"
target="_blank"
>
<img
src="https://dofollow.tools/badge/badge_light.svg"
alt="Featured on Dofollow.Tools"
width="200"
height="54"
/>
</a>
</div>
);
}

View File

@@ -0,0 +1,188 @@
import BuiltWithButton from "@/components/BuiltWithButton";
import { TwitterX } from "@/components/social-icons/icons";
import { siteConfig } from "@/config/site";
import { Link as I18nLink } from "@/i18n/routing";
import { FooterLink } from "@/types/common";
import { GithubIcon, MailIcon } from "lucide-react";
import { getMessages, getTranslations } from "next-intl/server";
import Link from "next/link";
import { SiBluesky, SiDiscord } from "react-icons/si";
export default async function Footer() {
const messages = await getMessages();
const t = await getTranslations("Home");
const tFooter = await getTranslations("Footer");
const footerLinks: FooterLink[] = tFooter.raw("Links.groups");
footerLinks.forEach((group) => {
const pricingLink = group.links.find((link) => link.id === "pricing");
if (pricingLink) {
pricingLink.href = process.env.NEXT_PUBLIC_PRICING_PATH!;
}
});
return (
<div className="bg-gray-900 text-gray-300">
<footer className="py-2 border-t border-gray-700">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-8 py-12 lg:grid-cols-6">
<div className="w-full flex flex-col sm:flex-row lg:flex-col gap-4 col-span-full md:col-span-2">
<div className="space-y-4 flex-1">
<div className="items-center space-x-2 flex">
<h2 className="highlight-text text-2xl font-bold">
{t("title")}
</h2>
</div>
<p className="text-sm p4-4 md:pr-12">{t("tagLine")}</p>
<div className="flex items-center gap-2">
{siteConfig.socialLinks?.github && (
<Link
href={siteConfig.socialLinks.github}
prefetch={false}
target="_blank"
rel="noreferrer nofollow noopener"
aria-label="GitHub"
title="View on GitHub"
className="inline-flex h-8 w-8 items-center justify-center rounded-md hover:bg-accent hover:text-accent-foreground"
>
<GithubIcon className="size-4" aria-hidden="true" />
</Link>
)}
{siteConfig.socialLinks?.discord && (
<Link
href={siteConfig.socialLinks.discord}
prefetch={false}
target="_blank"
rel="noreferrer nofollow noopener"
aria-label="Discord"
title="Join Discord"
className="inline-flex h-8 w-8 items-center justify-center rounded-md hover:bg-accent hover:text-accent-foreground"
>
<SiDiscord className="w-4 h-4" aria-hidden="true" />
</Link>
)}
{siteConfig.socialLinks?.twitter && (
<Link
href={siteConfig.socialLinks.twitter}
prefetch={false}
target="_blank"
rel="noreferrer nofollow noopener"
aria-label="Twitter"
title="View on Twitter"
className="inline-flex h-8 w-8 items-center justify-center rounded-md hover:bg-accent hover:text-accent-foreground"
>
<TwitterX className="w-4 h-4" aria-hidden="true" />
</Link>
)}
{siteConfig.socialLinks?.bluesky && (
<Link
href={siteConfig.socialLinks.bluesky}
prefetch={false}
target="_blank"
rel="noreferrer nofollow noopener"
aria-label="Blue Sky"
title="View on Bluesky"
className="inline-flex h-8 w-8 items-center justify-center rounded-md hover:bg-accent hover:text-accent-foreground"
>
<SiBluesky className="w-4 h-4" aria-hidden="true" />
</Link>
)}
{siteConfig.socialLinks?.email && (
<Link
href={`mailto:${siteConfig.socialLinks.email}`}
prefetch={false}
target="_blank"
rel="noreferrer nofollow noopener"
aria-label="Email"
title="Email"
className="inline-flex h-8 w-8 items-center justify-center rounded-md hover:bg-accent hover:text-accent-foreground"
>
<MailIcon className="w-4 h-4" />
</Link>
)}
</div>
<BuiltWithButton />
</div>
</div>
{footerLinks.map((section) => (
<div key={section.title} className="flex-1">
<h3 className="text-white text-lg font-semibold mb-4">
{section.title}
</h3>
<ul className="space-y-2 text-sm">
{section.links.map((link) => (
<li key={link.href}>
{link.href.startsWith("/") && !link.useA ? (
<I18nLink
href={link.href}
title={link.name}
prefetch={false}
className="hover:text-white transition-colors"
target={link.target || ""}
rel={link.rel || ""}
>
{link.name}
</I18nLink>
) : (
<Link
href={link.href}
title={link.name}
prefetch={false}
className="hover:text-white transition-colors"
target={link.target || ""}
rel={link.rel || ""}
>
{link.name}
</Link>
)}
</li>
))}
</ul>
</div>
))}
{/* {messages.Footer.Newsletter && (
<div className="w-full flex-1">
<Newsletter />
</div>
)} */}
</div>
<div className="border-t border-gray-800 py-6 flex flex-col md:flex-row justify-between items-center">
<p className="text-gray-400 text-sm">
{tFooter("Copyright", {
year: new Date().getFullYear(),
name: siteConfig.name,
})}
</p>
<div className="flex space-x-6 mt-4 md:mt-0">
<I18nLink
href="/privacy-policy"
title={tFooter("PrivacyPolicy")}
prefetch={false}
className="text-gray-400 hover:text-white text-sm"
>
{tFooter("PrivacyPolicy")}
</I18nLink>
<I18nLink
href="/terms-of-service"
title={tFooter("TermsOfService")}
prefetch={false}
className="text-gray-400 hover:text-white text-sm"
>
{tFooter("TermsOfService")}
</I18nLink>
</div>
</div>
</div>
{/* <Badges /> */}
</footer>
</div>
);
}

View File

@@ -0,0 +1,93 @@
"use client";
import { Button } from "@/components/ui/button";
import { normalizeEmail, validateEmail } from "@/lib/email";
import { Send } from "lucide-react";
import { useTranslations } from "next-intl";
import { useState } from "react";
export function Newsletter() {
const [email, setEmail] = useState("");
const [subscribeStatus, setSubscribeStatus] = useState<
"idle" | "loading" | "success" | "error"
>("idle");
const [errorMessage, setErrorMessage] = useState("");
const t = useTranslations("Footer.Newsletter");
const handleSubscribe = async (e: React.FormEvent) => {
e.preventDefault();
if (!email) return;
const normalizedEmailAddress = normalizeEmail(email);
const { isValid, error } = validateEmail(normalizedEmailAddress);
if (!isValid) {
setSubscribeStatus("error");
setErrorMessage(error || t("defaultErrorMessage"));
setTimeout(() => setSubscribeStatus("idle"), 5000);
return;
}
try {
setSubscribeStatus("loading");
const response = await fetch("/api/newsletter", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: normalizedEmailAddress }),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || t("errorMessage"));
}
setSubscribeStatus("success");
setEmail("");
setErrorMessage("");
setTimeout(() => setSubscribeStatus("idle"), 5000);
} catch (error) {
setSubscribeStatus("error");
setErrorMessage(
error instanceof Error ? error.message : t("errorMessage2")
);
setTimeout(() => setSubscribeStatus("idle"), 5000);
}
};
return (
<div className="">
<h4 className="mb-3 font-semibold">{t("title")}</h4>
<p className="text-sm mb-3">{t("description")}</p>
<form onSubmit={handleSubscribe} className="flex flex-col gap-2 max-w-64">
<div className="relative">
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="your@email.com"
required
className="w-full px-3 py-2 bg-gray-200 text-black text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-primary"
disabled={subscribeStatus === "loading"}
/>
</div>
<Button type="submit" disabled={subscribeStatus === "loading"}>
{subscribeStatus === "loading" ? (
t("subscribing")
) : (
<>
{t("subscribe")} <Send className="w-3.5 h-3.5" />
</>
)}
</Button>
{subscribeStatus === "success" && (
<p className="text-xs text-green-600 mt-1">{t("subscribed")}</p>
)}
{subscribeStatus === "error" && (
<p className="text-xs text-red-600 mt-1">{errorMessage}</p>
)}
</form>
</div>
);
}

View File

@@ -0,0 +1,51 @@
import HeaderLinks from "@/components/header/HeaderLinks";
import MobileMenu from "@/components/header/MobileMenu";
import LocaleSwitcher from "@/components/LocaleSwitcher";
import { ThemeToggle } from "@/components/ThemeToggle";
import { siteConfig } from "@/config/site";
import { Link as I18nLink } from "@/i18n/routing";
import { useTranslations } from "next-intl";
import Image from "next/image";
const Header = () => {
const t = useTranslations("Home");
return (
<header className="py-2 px-6 backdrop-blur-md sticky top-0 z-50">
<nav className="flex justify-between items-center w-full mx-auto">
<div className="flex items-center space-x-6 md:space-x-12">
<I18nLink
href="/"
prefetch={false}
className="flex items-center space-x-1 font-bold"
>
<Image
alt={siteConfig.name}
src="/logo.svg"
className="w-6 h-6"
width={32}
height={32}
/>
<span className="text-gray-800 dark:text-gray-200">
{t("title")}
</span>
</I18nLink>
<HeaderLinks />
</div>
<div className="flex items-center gap-x-2 md:gap-x-4 lg:gap-x-6 flex-1 justify-end">
{/* PC */}
<div className="hidden md:flex items-center gap-x-4">
<LocaleSwitcher />
<ThemeToggle />
</div>
{/* Mobile */}
<MobileMenu />
</div>
</nav>
</header>
);
};
export default Header;

View File

@@ -0,0 +1,42 @@
"use client";
import { Link as I18nLink, usePathname } from "@/i18n/routing";
import { cn } from "@/lib/utils";
import { HeaderLink } from "@/types/common";
import { ExternalLink } from "lucide-react";
import { useTranslations } from "next-intl";
const HeaderLinks = () => {
const tHeader = useTranslations("Header");
const pathname = usePathname();
const headerLinks: HeaderLink[] = tHeader.raw("links");
return (
<div className="hidden md:flex flex-row items-center gap-x-2 text-sm font-medium text-muted-500">
{headerLinks.map((link) => (
<I18nLink
key={link.name}
href={link.href}
title={link.name}
prefetch={link.target && link.target === "_blank" ? false : true}
target={link.target || "_self"}
rel={link.rel || undefined}
className={cn(
"rounded-xl px-4 py-2 flex items-center gap-x-1 hover:bg-accent-foreground/10 hover:text-accent-foreground",
pathname === link.href && "font-semibold text-accent-foreground"
)}
>
{link.name}
{link.target && link.target === "_blank" && (
<span className="text-xs">
<ExternalLink className="w-4 h-4" />
</span>
)}
</I18nLink>
))}
</div>
);
};
export default HeaderLinks;

View File

@@ -0,0 +1,74 @@
"use client";
import LocaleSwitcher from "@/components/LocaleSwitcher";
import { ThemeToggle } from "@/components/ThemeToggle";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Link as I18nLink } from "@/i18n/routing";
import { HeaderLink } from "@/types/common";
import { Menu } from "lucide-react";
import { useTranslations } from "next-intl";
import Image from "next/image";
export default function MobileMenu() {
const t = useTranslations("Home");
const tHeader = useTranslations("Header");
const headerLinks: HeaderLink[] = tHeader.raw("links");
return (
<div className="flex items-center gap-1 md:hidden">
<LocaleSwitcher />
<ThemeToggle />
<DropdownMenu>
<DropdownMenuTrigger className="p-2">
<Menu className="h-5 w-5" />
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-64">
<DropdownMenuLabel>
<I18nLink
href="/"
title={t("title")}
prefetch={true}
className="flex items-center space-x-1 font-bold"
>
<Image
alt={t("title")}
src="/logo.svg"
className="w-6 h-6"
width={32}
height={32}
/>
<span className="highlight-text">{t("title")}</span>
</I18nLink>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
{headerLinks.map((link) => (
<DropdownMenuItem key={link.name}>
<I18nLink
href={link.href}
title={link.name}
prefetch={
link.target && link.target === "_blank" ? false : true
}
target={link.target || "_self"}
rel={link.rel || undefined}
>
{link.name}
</I18nLink>
</DropdownMenuItem>
))}
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
}

76
components/home/index.tsx Normal file
View File

@@ -0,0 +1,76 @@
'use client'
import { Button } from "@/components/ui/button";
import { siteConfig } from "@/config/site";
import { MousePointerClick } from "lucide-react";
import { useTranslations } from "next-intl";
import Link from "next/link";
import { SiDiscord } from "react-icons/si";
export default function HomeComponent() {
const t = useTranslations("Home");
return (
<>
<section className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 pb-16 pt-24 text-center">
<h1 className="mx-auto max-w-4xl font-display text-5xl font-bold tracking-tight text-slate-900 sm:text-7xl dark:text-gray-200">
Next.js{" "}
<span className="relative whitespace-nowrap text-blue-600">
<svg
aria-hidden="true"
viewBox="0 0 418 42"
className="absolute left-0 top-2/3 h-[0.58em] w-full fill-blue-300/70"
preserveAspectRatio="none"
>
<path d="M203.371.916c-26.013-2.078-76.686 1.963-124.73 9.946L67.3 12.749C35.421 18.062 18.2 21.766 6.004 25.934 1.244 27.561.828 27.778.874 28.61c.07 1.214.828 1.121 9.595-1.176 9.072-2.377 17.15-3.92 39.246-7.496C123.565 7.986 157.869 4.492 195.942 5.046c7.461.108 19.25 1.696 19.17 2.582-.107 1.183-7.874 4.31-25.75 10.366-21.992 7.45-35.43 12.534-36.701 13.884-2.173 2.308-.202 4.407 4.442 4.734 2.654.187 3.263.157 15.593-.78 35.401-2.686 57.944-3.488 88.365-3.143 46.327.526 75.721 2.23 130.788 7.584 19.787 1.924 20.814 1.98 24.557 1.332l.066-.011c1.201-.203 1.53-1.825.399-2.335-2.911-1.31-4.893-1.604-22.048-3.261-57.509-5.556-87.871-7.36-132.059-7.842-23.239-.254-33.617-.116-50.627.674-11.629.54-42.371 2.494-46.696 2.967-2.359.259 8.133-3.625 26.504-9.81 23.239-7.825 27.934-10.149 28.304-14.005.417-4.348-3.529-6-16.878-7.066Z"></path>
</svg>
<span className="relative"> i18n </span>{" "}
</span>
Starter
</h1>
<p className="mx-auto mt-6 max-w-2xl text-2xl tracking-tight text-slate-700 dark:text-slate-500">
{t("description")}
</p>
<div className="mt-10 flex items-center justify-center gap-2">
<Button
className="h-11 rounded-xl px-8 py-2 bg-white text-indigo-500 hover:text-indigo-600 border-2 border-indigo-500"
variant="outline"
asChild
>
<Link
href="https://nexty.dev/"
target="_blank"
rel="noopener noreferrer"
title="Get SaaS Version - Nexty.dev"
prefetch={false}
className="flex items-center gap-2"
>
<MousePointerClick className="w-4 h-4 text-indigo-500" />
Get SaaS Version
</Link>
</Button>
<Button
className="h-11 rounded-xl px-8 py-2 bg-white text-indigo-500 hover:text-indigo-600 border-2 border-indigo-500"
variant="outline"
asChild
>
<Link
href={
siteConfig.socialLinks?.discord ||
"https://discord.com/invite/R7bUxWKRqZ"
}
target="_blank"
rel="noopener noreferrer nofollow"
title="Join Discord"
prefetch={false}
className="flex items-center gap-2"
>
<SiDiscord className="w-4 h-4 text-indigo-500" />
Join Discord
</Link>
</Button>
</div>
</section>
</>
);
}

View File

@@ -0,0 +1,36 @@
export default function ExpandingArrow({ className }: { className?: string }) {
return (
<div className="group relative flex items-center">
<svg
className={`${
className ? className : "h-4 w-4"
} absolute transition-all group-hover:translate-x-1 group-hover:opacity-0`}
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
viewBox="0 0 16 16"
width="16"
height="16"
>
<path
fillRule="evenodd"
d="M6.22 3.22a.75.75 0 011.06 0l4.25 4.25a.75.75 0 010 1.06l-4.25 4.25a.75.75 0 01-1.06-1.06L9.94 8 6.22 4.28a.75.75 0 010-1.06z"
></path>
</svg>
<svg
className={`${
className ? className : "h-4 w-4"
} absolute opacity-0 transition-all group-hover:translate-x-1 group-hover:opacity-100`}
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
viewBox="0 0 16 16"
width="16"
height="16"
>
<path
fillRule="evenodd"
d="M8.22 2.97a.75.75 0 011.06 0l4.25 4.25a.75.75 0 010 1.06l-4.25 4.25a.75.75 0 01-1.06-1.06l2.97-2.97H3.75a.75.75 0 010-1.5h7.44L8.22 4.03a.75.75 0 010-1.06z"
></path>
</svg>
</div>
);
}

24
components/icons/eye.tsx Normal file
View File

@@ -0,0 +1,24 @@
export default function TablerEyeFilled(props: any) {
return (
<svg
width="1em"
height="1em"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<g
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
>
<path d="M0 0h24v24H0z"></path>
<path
fill="#000000"
d="M12 4c4.29 0 7.863 2.429 10.665 7.154l.22.379l.045.1l.03.083l.014.055l.014.082l.011.1v.11l-.014.111a.992.992 0 0 1-.026.11l-.039.108l-.036.075l-.016.03c-2.764 4.836-6.3 7.38-10.555 7.499L12 20c-4.396 0-8.037-2.549-10.868-7.504a1 1 0 0 1 0-.992C3.963 6.549 7.604 4 12 4zm0 5a3 3 0 1 0 0 6a3 3 0 0 0 0-6z"
></path>
</g>
</svg>
);
}

View File

@@ -0,0 +1,4 @@
export { default as LoadingDots } from "./loading-dots";
export { default as LoadingCircle } from "./loading-circle";
export { default as LoadingSpinner } from "./loading-spinner";
export { default as ExpandingArrow } from "./expanding-arrow";

View File

@@ -0,0 +1,20 @@
export default function LoadingCircle() {
return (
<svg
aria-hidden="true"
className="h-4 w-4 animate-spin fill-gray-600 text-gray-200"
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="currentColor"
/>
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="currentFill"
/>
</svg>
);
}

View File

@@ -0,0 +1,40 @@
.loading {
display: inline-flex;
align-items: center;
}
.loading .spacer {
margin-right: 2px;
}
.loading span {
animation-name: blink;
animation-duration: 1.4s;
animation-iteration-count: infinite;
animation-fill-mode: both;
width: 5px;
height: 5px;
border-radius: 50%;
display: inline-block;
margin: 0 1px;
}
.loading span:nth-of-type(2) {
animation-delay: 0.2s;
}
.loading span:nth-of-type(3) {
animation-delay: 0.4s;
}
@keyframes blink {
0% {
opacity: 0.2;
}
20% {
opacity: 1;
}
100% {
opacity: 0.2;
}
}

View File

@@ -0,0 +1,13 @@
import styles from "./loading-dots.module.css";
const LoadingDots = ({ color = "#000" }: { color?: string }) => {
return (
<span className={styles.loading}>
<span style={{ backgroundColor: color }} />
<span style={{ backgroundColor: color }} />
<span style={{ backgroundColor: color }} />
</span>
);
};
export default LoadingDots;

View File

@@ -0,0 +1,79 @@
.spinner {
color: gray;
display: inline-block;
position: relative;
width: 80px;
height: 80px;
transform: scale(0.3) translateX(-95px);
}
.spinner div {
transform-origin: 40px 40px;
animation: spinner 1.2s linear infinite;
}
.spinner div:after {
content: " ";
display: block;
position: absolute;
top: 3px;
left: 37px;
width: 6px;
height: 20px;
border-radius: 20%;
background: black;
}
.spinner div:nth-child(1) {
transform: rotate(0deg);
animation-delay: -1.1s;
}
.spinner div:nth-child(2) {
transform: rotate(30deg);
animation-delay: -1s;
}
.spinner div:nth-child(3) {
transform: rotate(60deg);
animation-delay: -0.9s;
}
.spinner div:nth-child(4) {
transform: rotate(90deg);
animation-delay: -0.8s;
}
.spinner div:nth-child(5) {
transform: rotate(120deg);
animation-delay: -0.7s;
}
.spinner div:nth-child(6) {
transform: rotate(150deg);
animation-delay: -0.6s;
}
.spinner div:nth-child(7) {
transform: rotate(180deg);
animation-delay: -0.5s;
}
.spinner div:nth-child(8) {
transform: rotate(210deg);
animation-delay: -0.4s;
}
.spinner div:nth-child(9) {
transform: rotate(240deg);
animation-delay: -0.3s;
}
.spinner div:nth-child(10) {
transform: rotate(270deg);
animation-delay: -0.2s;
}
.spinner div:nth-child(11) {
transform: rotate(300deg);
animation-delay: -0.1s;
}
.spinner div:nth-child(12) {
transform: rotate(330deg);
animation-delay: 0s;
}
@keyframes spinner {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}

View File

@@ -0,0 +1,20 @@
import styles from "./loading-spinner.module.css";
export default function LoadingSpinner() {
return (
<div className={styles.spinner}>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
</div>
);
}

16
components/icons/moon.tsx Normal file
View File

@@ -0,0 +1,16 @@
export default function PhMoonFill(props: any) {
return (
<svg
width="1.5em"
height="1.5em"
viewBox="0 0 256 256"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
fill="#000000"
d="M235.54 150.21a104.84 104.84 0 0 1-37 52.91A104 104 0 0 1 32 120a103.09 103.09 0 0 1 20.88-62.52a104.84 104.84 0 0 1 52.91-37a8 8 0 0 1 10 10a88.08 88.08 0 0 0 109.8 109.8a8 8 0 0 1 10 10Z"
></path>
</svg>
);
}

16
components/icons/sun.tsx Normal file
View File

@@ -0,0 +1,16 @@
export default function PhSunBold(props: any) {
return (
<svg
width="1.5em"
height="1.5em"
viewBox="0 0 256 256"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
fill="#ffffff"
d="M116 32V16a12 12 0 0 1 24 0v16a12 12 0 0 1-24 0Zm80 96a68 68 0 1 1-68-68a68.07 68.07 0 0 1 68 68Zm-24 0a44 44 0 1 0-44 44a44.05 44.05 0 0 0 44-44ZM51.51 68.49a12 12 0 1 0 17-17l-12-12a12 12 0 0 0-17 17Zm0 119l-12 12a12 12 0 0 0 17 17l12-12a12 12 0 1 0-17-17ZM196 72a12 12 0 0 0 8.49-3.51l12-12a12 12 0 0 0-17-17l-12 12A12 12 0 0 0 196 72Zm8.49 115.51a12 12 0 0 0-17 17l12 12a12 12 0 0 0 17-17ZM44 128a12 12 0 0 0-12-12H16a12 12 0 0 0 0 24h16a12 12 0 0 0 12-12Zm84 84a12 12 0 0 0-12 12v16a12 12 0 0 0 24 0v-16a12 12 0 0 0-12-12Zm112-96h-16a12 12 0 0 0 0 24h16a12 12 0 0 0 0-24Z"
></path>
</svg>
);
}

29
components/mdx/Aside.tsx Normal file
View File

@@ -0,0 +1,29 @@
import { cn } from "@/lib/utils";
interface AsideProps {
icon?: string;
children?: React.ReactNode;
type?: "default" | "warning" | "danger";
}
export function Aside({
children,
icon,
type = "default",
...props
}: AsideProps) {
return (
<div
className={cn(
"flex border-5 py-3 px-4 ms-2 ms-md-0 my-10 rounded rounded-1 shadow",
"bg-[#6edff633] border-[#6edff633] border-l-[#f6ef6e]"
)}
{...props}
>
<div className="rounded rounded-1 text-center h-8 w-8 bg-[#6edff6] text-2xl relative top-[-30px] left-[-30px]">
{icon || "💡"}
</div>
<div>{children}</div>
</div>
);
}

View File

@@ -0,0 +1,27 @@
import { cn } from "@/lib/utils";
interface CalloutProps {
icon?: string;
children?: React.ReactNode;
type?: "default" | "warning" | "danger";
}
export function Callout({
children,
icon,
type = "default",
...props
}: CalloutProps) {
return (
<div
className={cn("my-6 flex items-start rounded-md border border-l-4 p-4", {
"border-red-900 bg-red-50": type === "danger",
"border-yellow-900 bg-yellow-50": type === "warning",
})}
{...props}
>
{icon && <span className="mr-4 text-2xl">{icon}</span>}
<div>{children}</div>
</div>
);
}

View File

@@ -0,0 +1,125 @@
import { Aside } from "@/components/mdx/Aside";
import { Callout } from "@/components/mdx/Callout";
import { MdxCard } from "@/components/mdx/MdxCard";
import React, { ReactNode } from "react";
interface HeadingProps {
level: 1 | 2 | 3 | 4 | 5 | 6;
className: string;
children: ReactNode;
}
const Heading: React.FC<HeadingProps> = ({ level, className, children }) => {
const HeadingTag = `h${level}` as keyof React.ElementType;
const headingId = children?.toString() ?? "";
return React.createElement(
HeadingTag,
{ id: headingId, className },
children
);
};
interface MDXComponentsProps {
[key: string]: React.FC<any>;
}
const MDXComponents: MDXComponentsProps = {
h1: (props) => (
<Heading level={1} className="text-4xl font-bold mt-8 mb-6" {...props} />
),
h2: (props) => (
<Heading
level={2}
className="text-3xl font-semibold mt-8 mb-6 border-b-2 border-gray-200 pb-2"
{...props}
/>
),
h3: (props) => (
<Heading
level={3}
className="text-2xl font-semibold mt-6 mb-4"
{...props}
/>
),
h4: (props) => (
<Heading level={4} className="text-xl font-semibold mt-6 mb-4" {...props} />
),
h5: (props) => (
<Heading level={5} className="text-lg font-semibold mt-6 mb-4" {...props} />
),
h6: (props) => (
<Heading
level={6}
className="text-base font-semibold mt-6 mb-4"
{...props}
/>
),
hr: (props) => <hr className="border-t border-gray-200 my-8" {...props} />,
p: (props) => (
<p
className="mt-6 mb-6 leading-relaxed text-gray-700 dark:text-gray-300"
{...props}
/>
),
a: (props) => (
<a
className="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 transition-colors underline underline-offset-4"
target="_blank"
{...props}
/>
),
ul: (props) => <ul className="list-disc pl-6 mt-0 mb-6" {...props} />,
ol: (props) => <ol className="list-decimal pl-6 mt-0 mb-6" {...props} />,
li: (props) => (
<li className="mb-3 text-gray-700 dark:text-gray-300" {...props} />
),
code: (props) => (
<code
className="bg-gray-100 dark:bg-gray-700 rounded px-2 py-1 font-mono text-sm"
{...props}
/>
),
pre: (props) => (
<pre
className="rounded-lg p-4 overflow-x-auto my-4 bg-gray-100 dark:bg-gray-800"
{...props}
/>
),
blockquote: (props) => (
<blockquote
className="pl-6 border-l-4 my-6 text-gray-600 dark:text-gray-400 italic"
{...props}
/>
),
img: (props) => (
<img className="rounded-lg border-2 border-gray-200 my-6" {...props} />
),
strong: (props) => <strong className="font-bold" {...props} />,
table: (props) => (
<div className="my-8 w-full overflow-x-auto">
<table
className="w-full shadow-sm rounded-lg overflow-hidden"
{...props}
/>
</div>
),
tr: (props) => <tr className="border-t border-gray-200" {...props} />,
th: (props) => (
<th
className="px-6 py-3 font-bold text-left bg-gray-100 dark:bg-gray-700 [&[align=center]]:text-center [&[align=right]]:text-right"
{...props}
/>
),
td: (props) => (
<td
className="px-6 py-4 text-left border-t border-gray-100 dark:border-gray-700 [&[align=center]]:text-center [&[align=right]]:text-right"
{...props}
/>
),
Aside,
Callout,
Card: MdxCard,
};
export default MDXComponents;

View File

@@ -0,0 +1,42 @@
import Link from "next/link";
import { cn } from "@/lib/utils";
interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
href?: string;
disabled?: boolean;
}
export function MdxCard({
href,
className,
children,
disabled,
...props
}: CardProps) {
return (
<div
className={cn(
"group relative rounded-lg border p-6 shadow-md transition-shadow hover:shadow-lg",
disabled && "cursor-not-allowed opacity-60",
className
)}
{...props}
>
<div className="flex flex-col justify-between space-y-4">
<div className="space-y-2 [&>h3]:!mt-0 [&>h4]:!mt-0 [&>p]:text-muted-foreground">
{children}
</div>
</div>
{href && (
<Link
href={disabled ? "#" : href}
className="absolute inset-0"
prefetch={false}
>
<span className="sr-only">View</span>
</Link>
)}
</div>
);
}

View File

@@ -0,0 +1,114 @@
import { SVGProps } from 'react'
// Icons taken from: https://simpleicons.org/
// To add a new icon, add a new function here and add it to components in social-icons/index.tsx
export function Facebook(svgProps: SVGProps<SVGSVGElement>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...svgProps}>
<path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"></path>
</svg>
)
}
export function Github(svgProps: SVGProps<SVGSVGElement>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...svgProps}>
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"></path>
</svg>
)
}
export function Linkedin(svgProps: SVGProps<SVGSVGElement>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...svgProps}>
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"></path>
</svg>
)
}
export function Mail(svgProps: SVGProps<SVGSVGElement>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" {...svgProps}>
<path d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z"></path>
<path d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z"></path>
</svg>
)
}
export function Twitter(svgProps: SVGProps<SVGSVGElement>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...svgProps}>
<path d="M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z"></path>
</svg>
)
}
export function TwitterX(svgProps: SVGProps<SVGSVGElement>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...svgProps}>
<path
fill="currentColor"
d="M8 2H1l8.26 11.015L1.45 22H4.1l6.388-7.349L16 22h7l-8.608-11.478L21.8 2h-2.65l-5.986 6.886zm9 18L5 4h2l12 16z"
/>
</svg>
)
}
export function Youtube(svgProps: SVGProps<SVGSVGElement>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...svgProps}>
<path d="M23.499 6.203a3.008 3.008 0 00-2.089-2.089c-1.87-.501-9.4-.501-9.4-.501s-7.509-.01-9.399.501a3.008 3.008 0 00-2.088 2.09A31.258 31.26 0 000 12.01a31.258 31.26 0 00.523 5.785 3.008 3.008 0 002.088 2.089c1.869.502 9.4.502 9.4.502s7.508 0 9.399-.502a3.008 3.008 0 002.089-2.09 31.258 31.26 0 00.5-5.784 31.258 31.26 0 00-.5-5.808zm-13.891 9.4V8.407l6.266 3.604z"></path>
</svg>
)
}
export function Mastodon(svgProps: SVGProps<SVGSVGElement>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...svgProps}>
<path d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38.265-.061.527-.132.786-.213.585-.184 1.27-.39 1.774-.753a.057.057 0 0 0 .023-.043v-1.809a.052.052 0 0 0-.02-.041.053.053 0 0 0-.046-.01 20.282 20.282 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.593 5.593 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422.038-.008.077-.015.11-.024 2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545zm-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102c0-1.31.337-2.35 1.011-3.12.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164.675.77 1.012 1.81 1.012 3.12z" />
</svg>
)
}
export function Threads(svgProps: SVGProps<SVGSVGElement>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...svgProps}>
<path d="M12.186 24h-.007c-3.581-.024-6.334-1.205-8.184-3.509C2.35 18.44 1.5 15.586 1.472 12.01v-.017c.03-3.579.879-6.43 2.525-8.482C5.845 1.205 8.6.024 12.18 0h.014c2.746.02 5.043.725 6.826 2.098 1.677 1.29 2.858 3.13 3.509 5.467l-2.04.569c-1.104-3.96-3.898-5.984-8.304-6.015-2.91.022-5.11.936-6.54 2.717C4.307 6.504 3.616 8.914 3.589 12c.027 3.086.718 5.496 2.057 7.164 1.43 1.783 3.631 2.698 6.54 2.717 2.623-.02 4.358-.631 5.8-2.045 1.647-1.613 1.618-3.593 1.09-4.798-.31-.71-.873-1.3-1.634-1.75-.192 1.352-.622 2.446-1.284 3.272-.886 1.102-2.14 1.704-3.73 1.79-1.202.065-2.361-.218-3.259-.801-1.063-.689-1.685-1.74-1.752-2.964-.065-1.19.408-2.285 1.33-3.082.88-.76 2.119-1.207 3.583-1.291a13.853 13.853 0 0 1 3.02.142c-.126-.742-.375-1.332-.75-1.757-.513-.586-1.308-.883-2.359-.89h-.029c-.844 0-1.992.232-2.721 1.32L7.734 7.847c.98-1.454 2.568-2.256 4.478-2.256h.044c3.194.02 5.097 1.975 5.287 5.388.108.046.216.094.321.142 1.49.7 2.58 1.761 3.154 3.07.797 1.82.871 4.79-1.548 7.158-1.85 1.81-4.094 2.628-7.277 2.65Zm1.003-11.69c-.242 0-.487.007-.739.021-1.836.103-2.98.946-2.916 2.143.067 1.256 1.452 1.839 2.784 1.767 1.224-.065 2.818-.543 3.086-3.71a10.5 10.5 0 0 0-2.215-.221z" />
</svg>
)
}
export function Instagram(svgProps: SVGProps<SVGSVGElement>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...svgProps}>
<path d="M12 0C8.74 0 8.333.015 7.053.072 5.775.132 4.905.333 4.14.63c-.789.306-1.459.717-2.126 1.384S.935 3.35.63 4.14C.333 4.905.131 5.775.072 7.053.012 8.333 0 8.74 0 12s.015 3.667.072 4.947c.06 1.277.261 2.148.558 2.913.306.788.717 1.459 1.384 2.126.667.666 1.336 1.079 2.126 1.384.766.296 1.636.499 2.913.558C8.333 23.988 8.74 24 12 24s3.667-.015 4.947-.072c1.277-.06 2.148-.262 2.913-.558.788-.306 1.459-.718 2.126-1.384.666-.667 1.079-1.335 1.384-2.126.296-.765.499-1.636.558-2.913.06-1.28.072-1.687.072-4.947s-.015-3.667-.072-4.947c-.06-1.277-.262-2.149-.558-2.913-.306-.789-.718-1.459-1.384-2.126C21.319 1.347 20.651.935 19.86.63c-.765-.297-1.636-.499-2.913-.558C15.667.012 15.26 0 12 0zm0 2.16c3.203 0 3.585.016 4.85.071 1.17.055 1.805.249 2.227.415.562.217.96.477 1.382.896.419.42.679.819.896 1.381.164.422.36 1.057.413 2.227.057 1.266.07 1.646.07 4.85s-.015 3.585-.074 4.85c-.061 1.17-.256 1.805-.421 2.227-.224.562-.479.96-.899 1.382-.419.419-.824.679-1.38.896-.42.164-1.065.36-2.235.413-1.274.057-1.649.07-4.859.07-3.211 0-3.586-.015-4.859-.074-1.171-.061-1.816-.256-2.236-.421-.569-.224-.96-.479-1.379-.899-.421-.419-.69-.824-.9-1.38-.165-.42-.359-1.065-.42-2.235-.045-1.26-.061-1.649-.061-4.844 0-3.196.016-3.586.061-4.861.061-1.17.255-1.814.42-2.234.21-.57.479-.96.9-1.381.419-.419.81-.689 1.379-.898.42-.166 1.051-.361 2.221-.421 1.275-.045 1.65-.06 4.859-.06l.045.03zm0 3.678c-3.405 0-6.162 2.76-6.162 6.162 0 3.405 2.76 6.162 6.162 6.162 3.405 0 6.162-2.76 6.162-6.162 0-3.405-2.76-6.162-6.162-6.162zM12 16c-2.21 0-4-1.79-4-4s1.79-4 4-4 4 1.79 4 4-1.79 4-4 4zm7.846-10.405c0 .795-.646 1.44-1.44 1.44-.795 0-1.44-.646-1.44-1.44 0-.794.646-1.439 1.44-1.439.793-.001 1.44.645 1.44 1.439z" />
</svg>
)
}
export function WeChat(svgProps: SVGProps<SVGSVGElement>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...svgProps}>
<path
fill="currentColor"
d="M15.85 8.14c.39 0 .77.03 1.14.08C16.31 5.25 13.19 3 9.44 3c-4.25 0-7.7 2.88-7.7 6.43c0 2.05 1.15 3.86 2.94 5.04L3.67 16.5l2.76-1.19c.59.21 1.21.38 1.87.47c-.09-.39-.14-.79-.14-1.21c-.01-3.54 3.44-6.43 7.69-6.43M12 5.89a.96.96 0 1 1 0 1.92a.96.96 0 0 1 0-1.92M6.87 7.82a.96.96 0 1 1 0-1.92a.96.96 0 0 1 0 1.92"
/>
<path
fill="currentColor"
d="M22.26 14.57c0-2.84-2.87-5.14-6.41-5.14s-6.41 2.3-6.41 5.14s2.87 5.14 6.41 5.14c.58 0 1.14-.08 1.67-.2L20.98 21l-1.2-2.4c1.5-.94 2.48-2.38 2.48-4.03m-8.34-.32a.96.96 0 1 1 .96-.96c.01.53-.43.96-.96.96m3.85 0a.96.96 0 1 1 0-1.92a.96.96 0 0 1 0 1.92"
/>
</svg>
)
}
export function JueJin(svgProps: SVGProps<SVGSVGElement>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...svgProps}>
<path
fill="currentColor"
d="m12 14.316l7.454-5.88l-2.022-1.625L12 11.1l-.004.003l-5.432-4.288l-2.02 1.624l7.452 5.88Zm0-7.247l2.89-2.298L12 2.453l-.004-.005l-2.884 2.318l2.884 2.3Zm0 11.266l-.005.002l-9.975-7.87L0 12.088l.194.156l11.803 9.308l7.463-5.885L24 12.085l-2.023-1.624Z"
/>
</svg>
)
}

View File

@@ -0,0 +1,62 @@
import {
Facebook,
Github,
Instagram,
JueJin,
Linkedin,
Mail,
Mastodon,
Threads,
Twitter,
TwitterX,
WeChat,
Youtube
} from './icons'
const components = {
mail: Mail,
github: Github,
facebook: Facebook,
youtube: Youtube,
linkedin: Linkedin,
twitter: Twitter,
twitterX: TwitterX,
weChat: WeChat,
jueJin: JueJin,
mastodon: Mastodon,
threads: Threads,
instagram: Instagram
}
type SocialIconProps = {
kind: keyof typeof components
href: string | undefined
size?: number
}
const SocialIcon = ({ kind, href, size = 8 }: SocialIconProps) => {
if (
!href ||
(kind === 'mail' &&
!/^mailto:\w+([.-]?\w+)@\w+([.-]?\w+)(.\w{2,3})+$/.test(href))
)
return null
const SocialSvg = components[kind]
return (
<a
className="text-sm text-gray-500 transition hover:text-gray-600"
target="_blank"
rel="noopener noreferrer"
href={href}
>
<span className="sr-only">{kind}</span>
<SocialSvg
className={`fill-current text-gray-400 hover:text-primary-500 dark:text-gray-200 dark:hover:text-primary-400 h-${size} w-${size}`}
/>
</a>
)
}
export default SocialIcon

59
components/ui/alert.tsx Normal file
View File

@@ -0,0 +1,59 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
{
variants: {
variant: {
default: "bg-background text-foreground",
destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
},
},
defaultVariants: {
variant: "default",
},
}
)
const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div
ref={ref}
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
))
Alert.displayName = "Alert"
const AlertTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...props}
/>
))
AlertTitle.displayName = "AlertTitle"
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm [&_p]:leading-relaxed", className)}
{...props}
/>
))
AlertDescription.displayName = "AlertDescription"
export { Alert, AlertTitle, AlertDescription }

57
components/ui/button.tsx Normal file
View File

@@ -0,0 +1,57 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

View File

@@ -0,0 +1,201 @@
"use client"
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { Check, ChevronRight, Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
)
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}

159
components/ui/select.tsx Normal file
View File

@@ -0,0 +1,159 @@
"use client"
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown, ChevronUp } from "lucide-react"
import { cn } from "@/lib/utils"
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}

129
components/ui/toast.tsx Normal file
View File

@@ -0,0 +1,129 @@
"use client"
import * as React from "react"
import * as ToastPrimitives from "@radix-ui/react-toast"
import { cva, type VariantProps } from "class-variance-authority"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const ToastProvider = ToastPrimitives.Provider
const ToastViewport = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Viewport
ref={ref}
className={cn(
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
className
)}
{...props}
/>
))
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
const toastVariants = cva(
"group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
{
variants: {
variant: {
default: "border bg-background text-foreground",
destructive:
"destructive group border-destructive bg-destructive text-destructive-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => {
return (
<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({ variant }), className)}
{...props}
/>
)
})
Toast.displayName = ToastPrimitives.Root.displayName
const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Action
ref={ref}
className={cn(
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
className
)}
{...props}
/>
))
ToastAction.displayName = ToastPrimitives.Action.displayName
const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Close
ref={ref}
className={cn(
"absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
className
)}
toast-close=""
{...props}
>
<X className="h-4 w-4" />
</ToastPrimitives.Close>
))
ToastClose.displayName = ToastPrimitives.Close.displayName
const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title
ref={ref}
className={cn("text-sm font-semibold [&+div]:text-xs", className)}
{...props}
/>
))
ToastTitle.displayName = ToastPrimitives.Title.displayName
const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Description
ref={ref}
className={cn("text-sm opacity-90", className)}
{...props}
/>
))
ToastDescription.displayName = ToastPrimitives.Description.displayName
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
type ToastActionElement = React.ReactElement<typeof ToastAction>
export {
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
}

35
components/ui/toaster.tsx Normal file
View File

@@ -0,0 +1,35 @@
"use client"
import { useToast } from "@/hooks/use-toast"
import {
Toast,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
} from "@/components/ui/toast"
export function Toaster() {
const { toasts } = useToast()
return (
<ToastProvider>
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && (
<ToastDescription>{description}</ToastDescription>
)}
</div>
{action}
<ToastClose />
</Toast>
)
})}
<ToastViewport />
</ToastProvider>
)
}

44
config/site.ts Normal file
View File

@@ -0,0 +1,44 @@
import { SiteConfig } from "@/types/siteConfig";
export const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL || "https://nextforge.dev";
export const SOURCE_CODE_URL = "https://github.com/weijunext/nextjs-starter";
export const PRO_VERSION = "https://nexty.dev";
const TWITTER_URL = 'https://x.com/weijunext'
const BSKY_URL = 'https://bsky.app/profile/judewei.bsky.social'
const EMAIL_URL = 'mailto:weijunext@gmail.com'
const GITHUB_URL = 'https://github.com/weijunext'
const DISCORD_URL = process.env.NEXT_PUBLIC_DISCORD_INVITE_URL
export const siteConfig: SiteConfig = {
name: "福安德外贸",
tagLine: '福安德 外贸 门户网站',
description:
"福安德 外贸 门户网站,提供专业的外贸服务和解决方案。",
url: BASE_URL,
authors: [
{
name: "chao",
url: "https://chao.com",
}
],
creator: 'chao',
socialLinks: {
discord: DISCORD_URL,
twitter: TWITTER_URL,
github: GITHUB_URL,
bluesky: BSKY_URL,
email: EMAIL_URL
},
themeColors: [
{ media: '(prefers-color-scheme: light)', color: 'white' },
{ media: '(prefers-color-scheme: dark)', color: 'black' },
],
defaultNextTheme: 'system', // next-theme option: system | dark | light
icons: {
icon: "/favicon.ico",
shortcut: "/logo.png",
apple: "/logo.png", // apple-touch-icon.png
},
}

99
content/about/en.mdx Normal file
View File

@@ -0,0 +1,99 @@
---
title: About
description: About Next Forge - Multilingual Next.js 16 Starter
lastUpdated: 2025-02-19
---
> Update date: 2025-02-19
# About Next Forge
Next Forge is a feature-rich Next.js 16 multilingual starter template designed to help developers quickly build globally-ready websites. It comes with built-in i18n support, modern UI design, dark/light theme toggling, responsive layout, MDX blog system, SEO optimization, and integrated analytics tools.
## ✨ Key Features
- 🌐 **Built-in i18n Support**: Supports English, Chinese, and Japanese out of the box, making it easy to create multilingual websites.
- 🎨 **Modern UI Design**: Clean and modern UI powered by Tailwind CSS.
- 🌙 **Dark/Light Theme Toggle**: Allows users to switch between dark and light themes effortlessly.
- 📱 **Responsive Layout**: Fully responsive design ensures a great experience on both mobile and desktop devices.
- 📝 **MDX Blog System**: Write blog posts using MDX for enhanced flexibility and power.
- 🔍 **SEO Optimization**: Comprehensive SEO features including automatic sitemap.xml generation, robots.txt configuration, and optimized metadata.
- 📊 **Analytics Integration**: Integrated with Google Analytics, Baidu Analytics, Google Adsense, and Vercel Analytics for easy tracking and insights.
- 🌿 **Eco-Friendly Performance**: Achieved A+ rating on [Website Carbon](https://www.websitecarbon.com/website/nextforge-dev/), making it one of the most energy-efficient websites.
## 🚀 Quick Start
1. Clone the repository:
```bash
git clone https://github.com/weijunext/nextjs-starter.git
```
2. Install dependencies:
```bash
npm install
```
3. Copy environment variables:
```bash
cp .env.example .env
```
4. Start the development server:
```bash
npm run dev
```
Visit [http://localhost:3000](http://localhost:3000) to view your application.
## ⚙️ Configuration
1. **Basic Setup**:
- Edit `config/site.ts` to configure website information.
- Update icons and logo in the `public/` directory.
- Configure `app/sitemap.ts` and `app/robots.ts`.
2. **i18n Setup**:
- Add or modify language files in `i18n/messages/`.
- Configure supported languages in `i18n/routing.ts`.
- Set up i18n routing in `middleware.ts`.
## 📝 Content Management
### Blog Posts
Create MDX files in `blog/[locale]` with the following format:
````
---
title: Post Title
description: Post Description
image: /image.png
slug: /url-path
tags: tag1,tag2
date: 2025-02-20
visible: published
pin: true
---
Post content...
````
### Static Pages
Manage static page content in `content/[page]/[locale].mdx`.
## 📄 License
Next Forge is licensed under the MIT License, allowing you to freely use, modify, and distribute the template.
## 🤝 Contributing
We welcome issues and pull requests! Your contributions help us improve this project.
## About the Author
A Next.js full-stack specialist offering expert services in project development, performance optimization, and SEO improvement.
For consulting and training opportunities, reach out at weijunext@gmail.com
- [Github](https://github.com/weijunext)
- [Bento](https://bento.me/weijunext)
- [Twitter/X](https://twitter.com/judewei_dev)

99
content/about/ja.mdx Normal file
View File

@@ -0,0 +1,99 @@
---
title: サイトについて
description: Next Forge - 多言語対応 Next.js 16 スターターについて
lastUpdated: 2025-02-19
---
> 更新日: 2025-02-19
# Next Forge について
Next Forge は、グローバル対応のウェブサイトを素早く構築するための、機能豊富な Next.js 16 多言語スターターテンプレートです。多言語対応、モダンな UI デザイン、ダーク/ライトテーマ切り替え、レスポンシブデザイン、MDX ブログシステム、SEO 最適化、アナリティクス統合など、多くの機能を備えています。
## ✨ 主な機能
- 🌐 **多言語対応**: 英語、中国語、日本語をサポートし、多言語サイトを簡単に構築できます。
- 🎨 **モダンな UI デザイン**: Tailwind CSS を利用したクリーンでモダンな UI デザイン。
- 🌙 **ダーク/ライトテーマ切り替え**: ユーザーが簡単にダークモードとライトモードを切り替えられる機能。
- 📱 **レスポンシブデザイン**: モバイルとデスクトップの両方で最適なユーザー体験を提供します。
- 📝 **MDX ブログシステム**: MDX を使用してブログ記事を執筆し、柔軟性とパワーを兼ね備えています。
- 🔍 **SEO 最適化**: sitemap.xml の自動生成、robots.txt の設定、最適化されたメタデータなど、包括的な SEO 機能を搭載。
- 📊 **アナリティクス統合**: Google Analytics、Baidu Analytics、Google Adsense、Vercel Analytics を統合し、簡単にデータを追跡できます。
- 🌿 **エコフレンドリーなパフォーマンス**: [Website Carbon](https://www.websitecarbon.com/website/nextforge-dev/) で A+ 評価を獲得し、最もエネルギー効率の良いウェブサイトの一つです。
## 🚀 クイックスタート
1. リポジトリのクローン:
```bash
git clone https://github.com/weijunext/nextjs-starter.git
```
2. 依存関係のインストール:
```bash
npm install
```
3. 環境変数の設定:
```bash
cp .env.example .env
```
4. 開発サーバーの起動:
```bash
npm run dev
```
[http://localhost:3000](http://localhost:3000) にアクセスしてアプリケーションを確認できます。
## ⚙️ 設定方法
1. **基本設定**:
- `config/site.ts` を編集してウェブサイト情報を設定します。
- `public/` ディレクトリ内のアイコンとロゴを更新します。
- `app/sitemap.ts` と `app/robots.ts` を設定します。
2. **多言語設定**:
- `i18n/messages/` 内の言語ファイルを追加または編集します。
- `i18n/routing.ts` でサポートする言語を設定します。
- `middleware.ts` で多言語ルーティングを設定します。
## 📝 コンテンツ管理
### ブログ投稿
`blog/[locale]` ディレクトリに以下のフォーマットで MDX ファイルを作成します:
````
---
title: 投稿タイトル
description: 投稿の説明
image: /image.png
slug: /url-path
tags: tag1,tag2
date: 2025-02-20
visible: published
pin: true
---
投稿内容...
````
### 静的ページ
`content/[page]/[locale].mdx` で静的ページのコンテンツを管理します。
## 📄 ライセンス
Next Forge は MIT ライセンスで提供されており、自由に使用、変更、配布することができます。
## 🤝 コントリビューション
Issue や Pull Request を歓迎します!皆さんの貢献がこのプロジェクトをより良いものにします。
## 作者について
Next.js のフルスタックスペシャリストとして、プロジェクト開発、パフォーマンス最適化、SEO 改善のエキスパートサービスを提供しています。
コンサルティングやトレーニングの機会については、weijunext@gmail.com までご連絡ください。
- [Github](https://github.com/weijunext)
- [Bento](https://bento.me/weijunext)
- [Twitter/X](https://twitter.com/judewei_dev)

99
content/about/zh.mdx Normal file
View File

@@ -0,0 +1,99 @@
---
title: 关于
description: 关于 Next Forge 多语言启动模板
lastUpdated: 2025-02-19
---
> 更新日期2025-02-19
# 关于 Next Forge
Next Forge 是一个功能完备的 Next.js 16 多语言启动模板,旨在帮助开发者快速构建面向全球的网站。它内置了多语言支持、现代 UI 设计、深色/浅色主题切换、响应式布局、MDX 博客系统、SEO 优化以及多种统计分析工具。
## ✨ 主要特性
- 🌐 **多语言支持**:内置中文、英文、日语的国际化支持,轻松实现多语言网站。
- 🎨 **现代 UI 设计**:基于 Tailwind CSS 的现代 UI 设计,简洁美观。
- 🌙 **深色/浅色主题切换**:支持用户自由切换深色和浅色主题。
- 📱 **响应式布局**:适配各种设备,确保在移动端和桌面端都有良好的用户体验。
- 📝 **MDX 博客系统**:支持使用 MDX 编写博客文章,灵活且强大。
- 🔍 **SEO 优化**:内置完整的 SEO 优化方案,包括自动生成 sitemap.xml 和 robots.txt。
- 📊 **统计分析**:集成 Google Analytics、Baidu Analytics、Google Adsense 和 Vercel Analytics方便进行数据追踪。
- 🌿 **环保性能**:在 [Website Carbon](https://www.websitecarbon.com/website/nextforge-dev/) 上获得 A+ 评级,成为最节能的网站之一。
## 🚀 快速开始
1. 克隆项目:
```bash
git clone https://github.com/weijunext/nextjs-starter.git
```
2. 安装依赖:
```bash
npm install
```
3. 复制环境变量文件:
```bash
cp .env.example .env
```
4. 启动开发服务器:
```bash
npm run dev
```
访问 [http://localhost:3000](http://localhost:3000) 查看你的应用。
## ⚙️ 配置
1. **基础配置**
- 修改 `config/site.ts` 配置网站信息。
- 更新 `public/` 下的图标和 logo。
- 配置 `app/sitemap.ts` 和 `app/robots.ts`。
2. **多语言配置**
- 在 `i18n/messages/` 下添加或修改语言文件。
- 在 `i18n/routing.ts` 中配置支持的语言。
- 在 `middleware.ts` 中配置多语言路由。
## 📝 内容管理
### 博客文章
在 `blog/[locale]` 目录下创建 MDX 文件,支持以下格式:
```markdown
---
title: 文章标题
description: 文章描述
image: /image.png
slug: /url-path
tags: tag1,tag2
date: 2025-02-20
visible: published
pin: true
---
文章内容...
```
### 静态页面
在 `content/[page]/[locale].mdx` 下管理静态页面内容。
## 📄 许可证
Next Forge 采用 MIT 许可证,您可以自由使用、修改和分发。
## 🤝 贡献
欢迎提交 Issue 和 Pull Request您的贡献将帮助我们不断改进这个项目。
## 关于作者
专注于 Next.js 全栈开发欢迎探讨开发、咨询与培训等合作机会联系微信bigye_chengpu
- [Github](https://github.com/weijunext)
- [Twitter/X](https://twitter.com/weijunext)
- [博客 - J实验室](https://weijunext.com)
- [Medium](https://medium.com/@weijunext)
- [掘金](https://juejin.cn/user/26044008768029)
- [知乎](https://www.zhihu.com/people/mo-mo-mo-89-12-11)

View File

@@ -0,0 +1,37 @@
---
title: Privacy Policy
description: How we collect and use your information
lastUpdated: "2025-02-19"
---
> Updated on 2025-02-19
# Privacy Policy
This Privacy Policy describes how we collect and use your information.
## Information We Collect
We collect information that you provide directly to us:
- Account information
- Usage data
- Communication preferences
## How We Use Your Information
We use the information we collect to:
- Provide our services
- Improve user experience
- Send important updates
## Data Security
We implement appropriate security measures to protect your data.
## Contact Information
For privacy-related questions, please reach out to our team.
## Updates to This Policy
We may update this policy from time to time. Please review it periodically.

View File

@@ -0,0 +1,37 @@
---
title: プライバシーポリシー
description: お客様の情報の収集および利用方法について
lastUpdated: "2025-02-19"
---
> 更新日2025年2月19日
# プライバシーポリシー
本プライバシーポリシーでは、当社がお客様の情報をどのように収集し、利用するかについて説明いたします。
## 収集する情報
当社は、お客様から直接提供いただく以下の情報を収集いたします:
- アカウント情報
- 利用データ
- 通信設定
## 情報の利用目的
収集した情報は、以下の目的で利用させていただきます:
- サービスの提供
- ユーザー体験の向上
- 重要なお知らせの送信
## データセキュリティ
お客様のデータを保護するため、適切なセキュリティ対策を実施しております。
## お問い合わせ先
プライバシーに関するご質問は、当社サポートチームまでお問い合わせください。
## ポリシーの更新
本ポリシーは随時更新される場合がございます。定期的にご確認いただきますようお願いいたします。

View File

@@ -0,0 +1,37 @@
---
title: 隐私政策
description: 我们如何收集和使用您的信息
lastUpdated: "2025-02-19"
---
> 更新日期2025年2月19日
# 隐私政策
本隐私政策说明了我们如何收集和使用您的信息。
## 我们收集的信息
我们收集您直接提供给我们的信息:
- 账户信息
- 使用数据
- 通信偏好设置
## 我们如何使用您的信息
我们将收集的信息用于:
- 提供服务
- 改善用户体验
- 发送重要更新
## 数据安全
我们采取适当的安全措施来保护您的数据。
## 联系方式
如有隐私相关问题,请联系我们的团队。
## 政策更新
我们可能会不时更新本政策。请定期查看。

View File

@@ -0,0 +1,32 @@
---
title: Terms of Service
description: Terms and conditions for using our service
lastUpdated: "2025-02-19"
---
> Updated on 2025-02-19
# Terms of Service
Welcome to our service. By using our website, you agree to these terms.
## 1. Acceptance of Terms
By accessing and using this website, you accept and agree to be bound by the terms and conditions of this agreement.
## 2. User Responsibilities
You agree to:
- Provide accurate information
- Maintain the security of your account
- Comply with all applicable laws
## 3. Service Changes
We reserve the right to:
- Modify or discontinue services
- Update these terms at any time
## Contact Us
If you have any questions about these terms, please contact us.

View File

@@ -0,0 +1,32 @@
---
title: 利用規約
description: サービス利用に関する規約と条件
lastUpdated: "2025-02-19"
---
> 更新日2025年2月19日
# 利用規約
当社のサービスをご利用いただき、ありがとうございます。本ウェブサイトをご利用になる際は、以下の利用規約に同意いただいたものとみなします。
## 1. 規約の同意
本ウェブサイトにアクセスし利用することにより、お客様は本規約の条件に同意し、拘束されることを承諾するものとします。
## 2. ユーザーの責任
お客様には以下の事項に同意していただきます:
- 正確な情報の提供
- アカウントの安全管理
- 適用される法令の遵守
## 3. サービスの変更
当社は以下の権利を留保します:
- サービスの変更または終了
- 本規約の随時更新
## お問い合わせ
本規約に関するご質問がございましたら、お気軽にお問い合わせください。

View File

@@ -0,0 +1,32 @@
---
title: 服务条款
description: 使用我们服务的条款和条件
lastUpdated: "2025-02-19"
---
> 更新日期2025年2月19日
# 服务条款
欢迎使用我们的服务。使用本网站即表示您同意以下条款。
## 1. 条款接受
访问和使用本网站即表示您接受并同意受本协议条款和条件的约束。
## 2. 用户责任
您同意:
- 提供准确信息
- 维护账户安全
- 遵守所有适用法律
## 3. 服务变更
我们保留以下权利:
- 修改或终止服务
- 随时更新这些条款
## 联系我们
如果您对这些条款有任何疑问,请与我们联系。

15
gtag.js Normal file
View File

@@ -0,0 +1,15 @@
export const GA_TRACKING_ID = process.env.NEXT_PUBLIC_GOOGLE_ID || null;
export const pageview = (url) => {
window.gtag("config", GA_TRACKING_ID, {
page_path: url,
});
};
export const event = ({ action, category, label, value }) => {
window.gtag("event", action, {
event_category: category,
event_label: label,
value: value,
});
};

194
hooks/use-toast.ts Normal file
View File

@@ -0,0 +1,194 @@
"use client"
// Inspired by react-hot-toast library
import * as React from "react"
import type {
ToastActionElement,
ToastProps,
} from "@/components/ui/toast"
const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000
type ToasterToast = ToastProps & {
id: string
title?: React.ReactNode
description?: React.ReactNode
action?: ToastActionElement
}
const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
} as const
let count = 0
function genId() {
count = (count + 1) % Number.MAX_SAFE_INTEGER
return count.toString()
}
type ActionType = typeof actionTypes
type Action =
| {
type: ActionType["ADD_TOAST"]
toast: ToasterToast
}
| {
type: ActionType["UPDATE_TOAST"]
toast: Partial<ToasterToast>
}
| {
type: ActionType["DISMISS_TOAST"]
toastId?: ToasterToast["id"]
}
| {
type: ActionType["REMOVE_TOAST"]
toastId?: ToasterToast["id"]
}
interface State {
toasts: ToasterToast[]
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) {
return
}
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId)
dispatch({
type: "REMOVE_TOAST",
toastId: toastId,
})
}, TOAST_REMOVE_DELAY)
toastTimeouts.set(toastId, timeout)
}
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "ADD_TOAST":
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
}
case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t
),
}
case "DISMISS_TOAST": {
const { toastId } = action
// ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId)
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id)
})
}
return {
...state,
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
open: false,
}
: t
),
}
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
toasts: [],
}
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
}
}
}
const listeners: Array<(state: State) => void> = []
let memoryState: State = { toasts: [] }
function dispatch(action: Action) {
memoryState = reducer(memoryState, action)
listeners.forEach((listener) => {
listener(memoryState)
})
}
type Toast = Omit<ToasterToast, "id">
function toast({ ...props }: Toast) {
const id = genId()
const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
})
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
dispatch({
type: "ADD_TOAST",
toast: {
...props,
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss()
},
},
})
return {
id: id,
dismiss,
update,
}
}
function useToast() {
const [state, setState] = React.useState<State>(memoryState)
React.useEffect(() => {
listeners.push(setState)
return () => {
const index = listeners.indexOf(setState)
if (index > -1) {
listeners.splice(index, 1)
}
}
}, [state])
return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
}
}
export { useToast, toast }

139
i18n/messages/en.json Normal file
View File

@@ -0,0 +1,139 @@
{
"LanguageDetection": {
"title": "Language Suggestion",
"description": "We noticed your browser language differs from the current site language. You can switch languages anytime.",
"countdown": "Closing in {countdown} seconds",
"switchTo": "Switch to"
},
"Header": {
"links": [
{
"name": "Blog",
"href": "/blog"
},
{
"name": "About",
"href": "/about"
},
{
"name": "Source Code",
"href": "https://github.com/weijunext/nextjs-starter",
"target": "_blank",
"rel": "noopener noreferrer nofollow"
},
{
"name": "Pro Version",
"href": "https://nexty.dev",
"target": "_blank",
"rel": "noopener noreferrer"
}
]
},
"Footer": {
"Copyright": "Copyright © {year} {name} All rights reserved.",
"PrivacyPolicy": "Privacy Policy",
"TermsOfService": "Terms of Service",
"Links": {
"groups": [
{
"title": "Languages",
"links": [
{
"href": "/en",
"name": "English",
"useA": true
},
{
"href": "/zh",
"name": "中文",
"useA": true
},
{
"href": "/ja",
"name": "日本語",
"useA": true
}
]
},
{
"title": "Open Source",
"links": [
{
"href": "https://github.com/weijunext/nextjs-starter",
"name": "Next Forge",
"rel": "noopener noreferrer nofollow",
"target": "_blank"
},
{
"href": "https://github.com/weijunext/landing-page-boilerplate",
"name": "Landing Page Boilerplate",
"rel": "noopener noreferrer nofollow",
"target": "_blank"
},
{
"href": "https://github.com/weijunext/weekly-boilerplate",
"name": "Blog Boilerplate",
"rel": "noopener noreferrer nofollow",
"target": "_blank"
}
]
},
{
"title": "Other Products",
"links": [
{
"href": "https://nexty.dev/",
"name": "Nexty - SaaS Template",
"rel": "noopener noreferrer",
"target": "_blank"
},
{
"href": "https://ogimage.click/",
"name": "OG Image Generator",
"rel": "noopener noreferrer",
"target": "_blank"
},
{
"href": "https://dofollow.tools/",
"name": "Dofollow.Tools",
"rel": "noopener noreferrer",
"target": "_blank"
}
]
}
]
},
"Newsletter": {
"title": "Subscribe to our newsletter",
"description": "Get the latest news and updates from Next Forge",
"defaultErrorMessage": "Please enter a valid email address",
"successMessage": "Subscription successful",
"errorMessage": "Subscription failed",
"errorMessage2": "Subscription failed. Please try again later.",
"subscribe": "Subscribe",
"subscribing": "Subscribing...",
"subscribed": "Subscription successful! Thank you for your attention."
}
},
"Home": {
"title": "Next Forge",
"tagLine": "Multilingual Next.js 16 Starter",
"description": "A multilingual Next.js 16 starter with built-in i18n support. Launch your global-ready web application with a clean, efficient, and SEO-friendly foundation."
},
"Blog": {
"title": "Blog Posts",
"description": "A list of blog posts"
},
"About": {
"title": "About",
"description": "About the site"
},
"TermsOfService": {
"title": "Terms of Service",
"description": "Terms of Service"
},
"PrivacyPolicy": {
"title": "Privacy Policy",
"description": "Privacy Policy"
}
}

139
i18n/messages/ja.json Normal file
View File

@@ -0,0 +1,139 @@
{
"LanguageDetection": {
"title": "言語の提案",
"description": "ブラウザの言語設定が現在のサイト言語と異なっています。いつでも言語を切り替えることができます。",
"countdown": "閉じるまで {countdown} 秒",
"switchTo": "に切り替える"
},
"Header": {
"links": [
{
"name": "ブログ",
"href": "/blog"
},
{
"name": "サイトについて",
"href": "/about"
},
{
"name": "Source Code",
"href": "https://github.com/weijunext/nextjs-starter",
"target": "_blank",
"rel": "noopener noreferrer nofollow"
},
{
"name": "Pro Version",
"href": "https://nexty.dev",
"target": "_blank",
"rel": "noopener noreferrer"
}
]
},
"Footer": {
"Copyright": "Copyright © {year} {name} All rights reserved.",
"PrivacyPolicy": "プライバシーポリシー",
"TermsOfService": "利用規約",
"Links": {
"groups": [
{
"title": "言語",
"links": [
{
"href": "/en",
"name": "English",
"useA": true
},
{
"href": "/zh",
"name": "中文",
"useA": true
},
{
"href": "/ja",
"name": "日本語",
"useA": true
}
]
},
{
"title": "オープンソースプロジェクト",
"links": [
{
"href": "https://github.com/weijunext/nextjs-starter",
"name": "Next Forge",
"rel": "noopener noreferrer nofollow",
"target": "_blank"
},
{
"href": "https://github.com/weijunext/landing-page-boilerplate",
"name": "Landing Page Boilerplate",
"rel": "noopener noreferrer nofollow",
"target": "_blank"
},
{
"href": "https://github.com/weijunext/weekly-boilerplate",
"name": "Blog Boilerplate",
"rel": "noopener noreferrer nofollow",
"target": "_blank"
}
]
},
{
"title": "その他の製品",
"links": [
{
"href": "https://nexty.dev/",
"name": "Nexty - SaaS Template",
"rel": "noopener noreferrer",
"target": "_blank"
},
{
"href": "https://ogimage.click/",
"name": "OG Image Generator",
"rel": "noopener noreferrer",
"target": "_blank"
},
{
"href": "https://dofollow.tools/",
"name": "Dofollow.Tools",
"rel": "noopener noreferrer",
"target": "_blank"
}
]
}
]
},
"Newsletter": {
"title": "Next Forgeのニュースレターに登録する",
"description": "Next Forgeの最新のニュースと更新情報を入手します。",
"defaultErrorMessage": "有効なメールアドレスを入力してください。",
"successMessage": "登録が完了しました。",
"errorMessage": "登録に失敗しました。",
"errorMessage2": "登録に失敗しました。少し待ってから再試行してください。",
"subscribe": "登録する",
"subscribing": "登録中...",
"subscribed": "登録が完了しました。ありがとうございます。"
}
},
"Home": {
"title": "Next Forge",
"tagLine": "Next.js 多言語スターターテンプレート",
"description": "多言語対応機能を搭載したNext.js 15スターターテンプレート。SEO最適化済みで、グローバル展開可能なウェブアプリを効率的に構築できます。"
},
"Blog": {
"title": "ブログ一覧",
"description": "ブログ投稿の一覧"
},
"About": {
"title": "サイトについて",
"description": "サイトについて"
},
"TermsOfService": {
"title": "サービス利用規約",
"description": "サービス利用規約"
},
"PrivacyPolicy": {
"title": "プライバシーポリシー",
"description": "プライバシーポリシー"
}
}

144
i18n/messages/vi.json Normal file
View File

@@ -0,0 +1,144 @@
{
"LanguageDetection": {
"title": "Gợi ý ngôn ngữ",
"description": "Phát hiện ngôn ngữ trình duyệt của bạn khác với ngôn ngữ hiện tại, bạn có thể chuyển đổi mọi lúc.",
"countdown": "Sẽ đóng sau {countdown} giây",
"switchTo": "Chuyển sang"
},
"Header": {
"links": [
{
"name": "Blog",
"href": "/blog"
},
{
"name": "Về chúng tôi",
"href": "/about"
},
{
"name": "Mã nguồn",
"href": "https://github.com/weijunext/nextjs-starter",
"target": "_blank",
"rel": "noopener noreferrer nofollow"
},
{
"name": "Phiên bản cao cấp",
"href": "https://nexty.dev",
"target": "_blank",
"rel": "noopener noreferrer"
}
]
},
"Footer": {
"Copyright": "Bản quyền © {year} {name} mọi quyền được bảo lưu.",
"PrivacyPolicy": "Chính sách bảo mật",
"TermsOfService": "Điều khoản dịch vụ",
"Links": {
"groups": [
{
"title": "Ngôn ngữ",
"links": [
{
"href": "/en",
"name": "English",
"useA": true
},
{
"href": "/zh",
"name": "Tiếng Trung",
"useA": true
},
{
"href": "/ja",
"name": "Tiếng Nhật",
"useA": true
},
{
"href": "/vi",
"name": "Tiếng Việt",
"useA": true
}
]
},
{
"title": "Dự án mã nguồn mở",
"links": [
{
"href": "https://github.com/weijunext/nextjs-starter",
"name": "Next Forge",
"rel": "noopener noreferrer nofollow",
"target": "_blank"
},
{
"href": "https://github.com/weijunext/landing-page-boilerplate",
"name": "Landing Page Boilerplate",
"rel": "noopener noreferrer nofollow",
"target": "_blank"
},
{
"href": "https://github.com/weijunext/weekly-boilerplate",
"name": "Blog Boilerplate",
"rel": "noopener noreferrer nofollow",
"target": "_blank"
}
]
},
{
"title": "Sản phẩm khác",
"links": [
{
"href": "https://nexty.dev/",
"name": "Nexty - Mẫu SaaS",
"rel": "noopener noreferrer",
"target": "_blank"
},
{
"href": "https://ogimage.click/",
"name": "Trình tạo ảnh OG",
"rel": "noopener noreferrer",
"target": "_blank"
},
{
"href": "https://dofollow.tools/",
"name": "Dofollow.Tools",
"rel": "noopener noreferrer",
"target": "_blank"
}
]
}
]
},
"Newsletter": {
"title": "Đăng ký nhận email của chúng tôi",
"description": "Nhận tin tức và hướng dẫn Next.js mới nhất",
"defaultErrorMessage": "Vui lòng nhập địa chỉ email hợp lệ",
"successMessage": "Đăng ký thành công",
"errorMessage": "Đăng ký thất bại",
"errorMessage2": "Đăng ký thất bại, vui lòng thử lại sau",
"subscribe": "Đăng ký",
"subscribing": "Đang đăng ký...",
"subscribed": "Đăng ký thành công! Cảm ơn bạn đã quan tâm."
}
},
"Home": {
"title": "Next Forge",
"tagLine": "Mẫu khởi động đa ngôn ngữ Next.js",
"description": "Mẫu khởi động Next.js 16 tích hợp sẵn hỗ trợ đa ngôn ngữ, giúp bạn nhanh chóng xây dựng trang web ra thị trường quốc tế. Rõ ràng, hiệu quả, sẵn sàng sử dụng với cơ sở kiến trúc SEO hoàn toàn tối ưu."
},
"Blog": {
"title": "Danh sách blog",
"description": "Danh sách các bài viết blog"
},
"About": {
"title": "Về chúng tôi",
"description": "Thông tin về trang web"
},
"TermsOfService": {
"title": "Điều khoản dịch vụ",
"description": "Điều khoản sử dụng dịch vụ"
},
"PrivacyPolicy": {
"title": "Chính sách bảo mật",
"description": "Chính sách bảo mật thông tin cá nhân"
}
}

139
i18n/messages/zh.json Normal file
View File

@@ -0,0 +1,139 @@
{
"LanguageDetection": {
"title": "语言建议",
"description": "检测到你的浏览器语言和当前语言不一样,你随时都可切换语言。",
"countdown": "将在 {countdown} 秒后关闭",
"switchTo": "切换到"
},
"Header": {
"links": [
{
"name": "博客",
"href": "/blog"
},
{
"name": "关于",
"href": "/about"
},
{
"name": "源码",
"href": "https://github.com/weijunext/nextjs-starter",
"target": "_blank",
"rel": "noopener noreferrer nofollow"
},
{
"name": "高级版",
"href": "https://nexty.dev",
"target": "_blank",
"rel": "noopener noreferrer"
}
]
},
"Footer": {
"Copyright": "版权所有 © {year} {name} 保留所有权利。",
"PrivacyPolicy": "隐私政策",
"TermsOfService": "服务条款",
"Links": {
"groups": [
{
"title": "语言",
"links": [
{
"href": "/en",
"name": "English",
"useA": true
},
{
"href": "/zh",
"name": "中文",
"useA": true
},
{
"href": "/ja",
"name": "日本語",
"useA": true
}
]
},
{
"title": "开源项目",
"links": [
{
"href": "https://github.com/weijunext/nextjs-starter",
"name": "Next Forge",
"rel": "noopener noreferrer nofollow",
"target": "_blank"
},
{
"href": "https://github.com/weijunext/landing-page-boilerplate",
"name": "Landing Page Boilerplate",
"rel": "noopener noreferrer nofollow",
"target": "_blank"
},
{
"href": "https://github.com/weijunext/weekly-boilerplate",
"name": "Blog Boilerplate",
"rel": "noopener noreferrer nofollow",
"target": "_blank"
}
]
},
{
"title": "其他产品",
"links": [
{
"href": "https://nexty.dev/",
"name": "Nexty - SaaS Template",
"rel": "noopener noreferrer",
"target": "_blank"
},
{
"href": "https://ogimage.click/",
"name": "OG Image Generator",
"rel": "noopener noreferrer",
"target": "_blank"
},
{
"href": "https://dofollow.tools/",
"name": "Dofollow.Tools",
"rel": "noopener noreferrer",
"target": "_blank"
}
]
}
]
},
"Newsletter": {
"title": "订阅我们的邮件",
"description": "获取最新的 Next.js 资讯和教程",
"defaultErrorMessage": "请输入有效的邮箱地址",
"successMessage": "订阅成功",
"errorMessage": "订阅失败",
"errorMessage2": "订阅失败,请稍后再试",
"subscribe": "订阅",
"subscribing": "订阅中",
"subscribed": "订阅成功!感谢您的关注。"
}
},
"Home": {
"title": "Next Forge",
"tagLine": "Next.js 多语言启动模板",
"description": "内置多语言支持的 Next.js 16 启动模板助您快速构建面向全球的出海网站。简洁高效开箱即用完全优化的SEO基础架构。"
},
"Blog": {
"title": "博客列表",
"description": "博客列表"
},
"About": {
"title": "关于",
"description": "关于网站"
},
"TermsOfService": {
"title": "服务条款",
"description": "服务条款"
},
"PrivacyPolicy": {
"title": "隐私政策",
"description": "隐私政策"
}
}

32
i18n/request.ts Normal file
View File

@@ -0,0 +1,32 @@
import { getRequestConfig } from 'next-intl/server';
import { routing } from './routing';
export default getRequestConfig(async ({ requestLocale }) => {
// This typically corresponds to the `[locale]` segment
let locale = await requestLocale;
if (locale?.startsWith('zh')) {
locale = 'zh';
} else if (locale?.startsWith('ja')) {
locale = 'ja';
} else if (locale?.startsWith('en')) {
locale = 'en';
} else if (locale?.startsWith('vi')) {
locale = 'vi';
} else {
locale = 'zh';
}
// Ensure that a valid locale is used
if (!locale || !routing.locales.includes(locale as any)) {
return {
locale: routing.defaultLocale,
messages: (await import(`./messages/${routing.defaultLocale}.json`)).default
};
}
return {
locale,
messages: (await import(`./messages/${locale}.json`)).default
};
});

37
i18n/routing.ts Normal file
View File

@@ -0,0 +1,37 @@
import { createNavigation } from 'next-intl/navigation';
import { defineRouting } from 'next-intl/routing';
export const LOCALES = ['en', 'zh', 'ja', 'vi']
export const DEFAULT_LOCALE = 'zh'
export const LOCALE_NAMES: Record<string, string> = {
'en': "English",
'zh': "中文",
'ja': "日本語",
'vi': "Tiếng Việt",
};
export const routing = defineRouting({
// A list of all locales that are supported
locales: LOCALES,
// Used when no locale matches
defaultLocale: DEFAULT_LOCALE,
// auto detect locale
localeDetection: process.env.NEXT_PUBLIC_LOCALE_DETECTION === 'true',
localePrefix: 'always',
});
// Lightweight wrappers around Next.js' navigation APIs
// that will consider the routing configuration
export const {
Link,
redirect,
usePathname,
useRouter,
getPathname,
} = createNavigation(routing);
export type Locale = (typeof routing.locales)[number];

91
lib/email.ts Normal file
View File

@@ -0,0 +1,91 @@
const DISPOSABLE_EMAIL_DOMAINS = [
'tempmail.com',
'throwawaymail.com',
'tempmail100.com'
];
export type EmailValidationError =
| 'invalid_email_format'
| 'email_part_too_long'
| 'disposable_email_not_allowed'
| 'invalid_characters';
const EMAIL_REGEX = /^(?=[a-zA-Z0-9@._%+-]{6,254}$)[a-zA-Z0-9._%+-]{1,64}@(?:[a-zA-Z0-9-]{1,63}\.){1,8}[a-zA-Z]{2,63}$/;
export function validateEmail(email: string): {
isValid: boolean;
error?: string;
} {
// validate email format
if (!EMAIL_REGEX.test(email)) {
return {
isValid: false,
error: 'invalid_email_format'
};
}
// check domain length
const [localPart, domain] = email.split('@');
if (domain.length > 255 || localPart.length > 64) {
return {
isValid: false,
error: 'email_part_too_long'
};
}
// check if it's a disposable email
if (DISPOSABLE_EMAIL_DOMAINS.includes(domain.toLowerCase())) {
return {
isValid: false,
error: 'disposable_email_not_allowed'
};
}
// check for special characters
if (/[<>()[\]\\.,;:\s@"]+/.test(localPart)) {
return {
isValid: false,
error: 'invalid_characters'
};
}
return { isValid: true };
}
// email validation (including alias detection)
export function normalizeEmail(email: string): string {
if (!email) return '';
// convert to lowercase
let normalizedEmail = email.toLowerCase();
// separate email local part and domain part
const [localPart, domain] = normalizedEmail.split('@');
// handle different email service provider alias rules
switch (domain) {
case 'gmail.com':
// remove dot and + suffix
const gmailBase = localPart
.replace(/\./g, '')
.split('+')[0];
return `${gmailBase}@${domain}`;
case 'outlook.com':
case 'hotmail.com':
case 'live.com':
// remove + suffix
const microsoftBase = localPart.split('+')[0];
return `${microsoftBase}@${domain}`;
case 'yahoo.com':
// remove - suffix
const yahooBase = localPart.split('-')[0];
return `${yahooBase}@${domain}`;
default:
// for other emails, only remove + suffix
const baseLocalPart = localPart.split('+')[0];
return `${baseLocalPart}@${domain}`;
}
}

66
lib/getBlogs.ts Normal file
View File

@@ -0,0 +1,66 @@
import { DEFAULT_LOCALE } from '@/i18n/routing';
import { BlogPost } from '@/types/blog';
import fs from 'fs';
import matter from 'gray-matter';
import path from 'path';
const POSTS_BATCH_SIZE = 10;
export async function getPosts(locale: string = DEFAULT_LOCALE): Promise<{ posts: BlogPost[] }> {
const postsDirectory = path.join(process.cwd(), 'blogs', locale);
// is directory exist
if (!fs.existsSync(postsDirectory)) {
return { posts: [] };
}
let filenames = await fs.promises.readdir(postsDirectory);
filenames = filenames.reverse();
let allPosts: BlogPost[] = [];
// read file by batch
for (let i = 0; i < filenames.length; i += POSTS_BATCH_SIZE) {
const batchFilenames = filenames.slice(i, i + POSTS_BATCH_SIZE);
const batchPosts: BlogPost[] = await Promise.all(
batchFilenames.map(async (filename) => {
const fullPath = path.join(postsDirectory, filename);
const fileContents = await fs.promises.readFile(fullPath, 'utf8');
const { data, content } = matter(fileContents);
return {
locale, // use locale parameter
title: data.title,
description: data.description,
image: data.image || '',
slug: data.slug,
tags: data.tags,
date: data.date,
visible: data.visible || 'published',
pin: data.pin || false,
content,
metadata: data,
};
})
);
allPosts.push(...batchPosts);
}
// filter out non-published articles
allPosts = allPosts.filter(post => post.visible === 'published');
// sort posts by pin and date
allPosts = allPosts.sort((a, b) => {
if (a.pin !== b.pin) {
return (b.pin ? 1 : 0) - (a.pin ? 1 : 0);
}
return new Date(b.date).getTime() - new Date(a.date).getTime();
});
return {
posts: allPosts,
};
}

40
lib/logger.ts Normal file
View File

@@ -0,0 +1,40 @@
import * as fs from 'fs';
import * as winston from 'winston';
import 'winston-daily-rotate-file';
const logDir: string = process.env.LOG_DIR || 'log';
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true });
}
const fileTransport = new winston.transports.DailyRotateFile({
filename: `${logDir}/%DATE%-results.log`,
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
maxSize: '20m',
maxFiles: '3d',
level: 'info',
});
const logger: winston.Logger = winston.createLogger({
level: 'debug',
format: winston.format.combine(
winston.format.timestamp({
format: 'YYYY-MM-DD HH:mm:ss'
}),
winston.format.json()
),
transports: [
fileTransport,
new winston.transports.Console({
level: 'debug',
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
})
]
});
export default logger;

100
lib/metadata.ts Normal file
View File

@@ -0,0 +1,100 @@
import { siteConfig } from '@/config/site'
import { DEFAULT_LOCALE, LOCALE_NAMES, Locale } from '@/i18n/routing'
import { Metadata } from 'next'
import { getTranslations } from 'next-intl/server'
type MetadataProps = {
page?: string
title?: string
description?: string
images?: string[]
noIndex?: boolean
locale: Locale
path?: string
canonicalUrl?: string
}
export async function constructMetadata({
page = 'Home',
title,
description,
images = [],
noIndex = false,
locale,
path,
canonicalUrl,
}: MetadataProps): Promise<Metadata> {
// get translations
const t = await getTranslations({ locale, namespace: 'Home' })
// get page specific metadata translations
const pageTitle = title || t(`title`)
const pageDescription = description || t(`description`)
// build full title
const finalTitle = page === 'Home'
? `${pageTitle} - ${t('tagLine')}`
: `${pageTitle} | ${t('title')}`
// build image URLs
const imageUrls = images.length > 0
? images.map(img => ({
url: img.startsWith('http') ? img : `${siteConfig.url}/${img}`,
alt: pageTitle,
}))
: [{
url: `${siteConfig.url}/og.png`,
alt: pageTitle,
}]
// Open Graph Site
const pageURL = `${locale === DEFAULT_LOCALE ? '' : `/${locale}`}${path}` || siteConfig.url
// build alternate language links
const alternateLanguages = Object.keys(LOCALE_NAMES).reduce((acc, lang) => {
const path = canonicalUrl
? `${lang === DEFAULT_LOCALE ? '' : `/${lang}`}${canonicalUrl === '/' ? '' : canonicalUrl}`
: `${lang === DEFAULT_LOCALE ? '' : `/${lang}`}`
acc[lang] = `${siteConfig.url}${path}`
return acc
}, {} as Record<string, string>)
return {
title: finalTitle,
description: pageDescription,
keywords: [],
authors: siteConfig.authors,
creator: siteConfig.creator,
metadataBase: new URL(siteConfig.url),
alternates: {
canonical: canonicalUrl ? `${siteConfig.url}${locale === DEFAULT_LOCALE ? '' : `/${locale}`}${canonicalUrl === '/' ? '' : canonicalUrl}` : undefined,
languages: alternateLanguages,
},
openGraph: {
type: 'website',
title: finalTitle,
description: pageDescription,
url: pageURL,
siteName: t('title'),
locale: locale,
images: imageUrls,
},
twitter: {
card: 'summary_large_image',
title: finalTitle,
description: pageDescription,
site: `${siteConfig.url}${pageURL === '/' ? '' : pageURL}`,
images: imageUrls,
creator: siteConfig.creator,
},
robots: {
index: !noIndex,
follow: !noIndex,
googleBot: {
index: !noIndex,
follow: !noIndex,
},
},
}
}

19
lib/utils.ts Normal file
View File

@@ -0,0 +1,19 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
export const getDomain = (url: string) => {
try {
// Add https:// protocol if not present
const urlWithProtocol = url.startsWith('http') ? url : `https://${url}`;
const domain = new URL(urlWithProtocol).hostname;
// Remove 'www.' prefix if exists
return domain.replace(/^www\./, '');
} catch (error) {
// Return original input if URL parsing fails
return url;
}
};

View File

@@ -0,0 +1,15 @@
{
"keep": {
"days": true,
"amount": 3
},
"auditLog": "log/.db69a462fcd0bc6ae6d0cc5220e6aa21ec198c81-audit.json",
"files": [
{
"date": 1733723462799,
"name": "log/2024-12-09-results.log",
"hash": "05d583fcbceaa268a024a210f7aee5ba516a76a6faaaea634dd5caab904bd55c"
}
],
"hashType": "sha256"
}

6
next-env.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

29
next.config.mjs Normal file
View File

@@ -0,0 +1,29 @@
import createNextIntlPlugin from "next-intl/plugin";
const withNextIntl = createNextIntlPlugin();
/** @type {import('next').NextConfig} */
const nextConfig = {
// output: "export",
images: {
remotePatterns: [
...(process.env.R2_PUBLIC_URL
? [
{
hostname: process.env.R2_PUBLIC_URL.replace("https://", ""),
},
]
: []),
],
},
compiler: {
removeConsole:
process.env.NODE_ENV === "production"
? {
exclude: ["error"],
}
: false,
},
};
export default withNextIntl(nextConfig);

61
package.json Normal file
View File

@@ -0,0 +1,61 @@
{
"name": "next-forge",
"version": "2.1.0",
"packageManager": "pnpm@10.12.4",
"engines": {
"node": "20.x"
},
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@radix-ui/react-dropdown-menu": "^2.1.4",
"@radix-ui/react-select": "^2.1.6",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-toast": "^1.2.6",
"@types/js-cookie": "^3.0.6",
"@upstash/ratelimit": "^2.0.5",
"@upstash/redis": "^1.34.5",
"@vercel/analytics": "^1.4.1",
"axios": "^1.7.9",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"dayjs": "^1.11.13",
"gray-matter": "^4.0.3",
"js-cookie": "^3.0.5",
"lucide-react": "^0.468.0",
"next": "16.0.0",
"next-intl": "^4.4.0",
"next-mdx-remote-client": "2",
"next-themes": "^0.4.4",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-hot-toast": "^2.4.1",
"react-icons": "^5.4.0",
"remark-gfm": "^4.0.0",
"resend": "^4.1.2",
"tailwind-merge": "^2.5.5",
"tailwindcss-animate": "^1.0.7",
"winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0",
"zod": "^3.24.1",
"zustand": "^5.0.3"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"autoprefixer": "^10.4.19",
"eslint": "^9",
"eslint-config-next": "16.0.0",
"postcss": "^8.4.38",
"tailwindcss": "^3.4.3",
"typescript": "^5"
},
"volta": {
"node": "20.19.5"
}
}

7152
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

6
postcss.config.js Normal file
View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

20
proxy.ts Normal file
View File

@@ -0,0 +1,20 @@
import createMiddleware from 'next-intl/middleware';
import { routing } from './i18n/routing';
export default createMiddleware(routing);
export const config = {
matcher: [
// Enable a redirect to a matching locale at the root
'/',
// Set a cookie to remember the previous locale for
// all requests that have a locale prefix
'/(en|zh|ja)/:path*',
// Enable redirects that add missing locales
// (e.g. `/pathnames` -> `/en/pathnames`)
'/((?!api|_next|_vercel|.*\\.|favicon.ico).*)'
]
};

1
public/ads.txt Normal file
View File

@@ -0,0 +1 @@
google.com, pub-xxxxxxxxxxxxxx, DIRECT, yyyyyyyyyyyyyyyyyy

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

539
public/logo.svg Normal file
View File

@@ -0,0 +1,539 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="1200px" height="1200px" viewBox="0 0 1200 1200" enable-background="new 0 0 1200 1200" xml:space="preserve"> <image id="image0" width="1200" height="1200" x="0" y="0"
xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABLAAAASwCAMAAADc/0P9AAAAIGNIUk0AAHomAACAhAAA+gAAAIDo
AAB1MAAA6mAAADqYAAAXcJy6UTwAAAL6UExURQAAAIUGMJYCIJcEH5cDIJcDIJYDH5cDIJcDIJcD
IJcDIJcDH5YEIJcDIJQDIZcDH2MOPmMOPWIOPmIOPWIOPmIOPmMOPmIOPmIOPmIOPmINPmIOPmMN
PmINPn4JMJcDH2IOPmIPPZcEIJcDIJcEH2IQPZYDH5gDH5YEH5gDH5cDH2AOP48FJZEFJJEEI5AE
I5AFJI4FJI4FJY0FJYwFJYwFJosFJooGJ4kGJ4kGKIgGKIcGKIcGKYYGKYUHKoQHK4MHKoIHK4MI
LIIIK4IILIEHK4EILIAILX8ILX4JLn0ILn0JL3wILnwJL3sJL3sJMHoJMHkJMXgJMXcJMXgKMncK
MnYKMnYKM3UKM3QKM3QKNHMKNHILNXELNXALNnELNm8LNW8MN24LN20MN20MOGwMOGsMOWoMOWkM
OmoNOmgNOmgOO5IEIo0GJowGJ4YHKnoKMXoKMHULNGwMOWkNO2cNO5EEIpMEImcNPGYNO2YOPGUO
PZQEIZMEIWUNPJUEIWQOPWQNPGQOPJYDIGMOPpcDIGIOPmMPP5YDH5gEH2EPP2EOPpgDH2EOP18O
P5kDHmAPQN25wseLm7lpfalFXY8QMHETPZJJaqlvicKYqtq/yqA1UP///+rX3dKir71wg6Y+Vo9C
Y651jsyot4MvVOnT2dCbqIw7XsmisubS2fXs79aotJguS4c2WtOzwLRgdfDi5pwoRJdQcOPFzcyT
obJab5ggPaZog9GuvOXJ0ZQYNngdRp9deuLN1vfw8teuuX0mTf37/Na4xPz5+uDJ0qpLY+C/yMN9
jbd9k8SElbyLn/Tp7cCSpe7e46tXcHgTO6JZdMadrtqzvahSaptXdfnz9Pr2976GmrNleqRhfbeD
mLZvg69vh97Fz/Lm6p08Wa9QZ753io8eP4IdQs2gr6JFYJdEY+fN1L9/kpInR4YmSZlLaZ5Sb61p
gbF7k9q7xpM7W8mXp440VocTNbV2jKNMZ4otT+za4Kxfd38WO8SQoZc0UtGott/By8qRfv8AAAAs
dFJOUwAHHzdOYniMnLC9ydLj7O/17ePSyb2wmox+YU87JxCWcjI/b4MeWqEo95L6mZpkPQAAAAFi
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
YVnFKzjDqszRNar3R8osxWF5z2H1Vb2rUuXQ9Cdj0vzJDLorHJZIhxXXYQW6rCWq90iq9MNhec5h
afzs84LDfZuFIrrDwmFl5bAia1Uw/TXWpItwWJ5zWINUb6qkGTwoUKiMFTnDwmEpdlivxRatowNU
75SkmYDD8pzD0vWwhtlrm0Umvr/CYSlzWLGZcl31bkmST3FYnnNYet5KOHZP97juCoelj8OK/WQ4
VM/vmj/FYXnOYX2telMlyrjxUe2VVbpwWDo5rJgMH6Z6zyTKFByW5xyWjgXrcFSpehKHpafDiszr
HctU75oE+RKH5TmHpeH5fdOfaPZEs2axPRYOSz+HFf2xUPW+ic88HJbnHJZ2Bat80I+bmSvpDAuH
pZHDCndYr3c8rt0gax4Oy3MOa5LqTRWTjX3N/kp6h4XDEuWwIjJ3vurdE5OZOCzPOaw+qjdVdFYF
u6uocTsOS2eHFdFjvT5R9f6Jzkwclucc1meqN1VUpj9hJFGHhcPS2GFFZKXqHRSV/jgszzksrQrW
+B8baZZmhoXD0s9hWf2VmSGq91BkhuKwPOewdLo0J9RdJZhg4bD0dlgRhUv1LorISRyW5xzWF6o3
VTg/fiLcXeGw3OawKnssjSrWSRyW5xzWKtWbqjJPhOLEt4Q4LOEOqzKdVe+kyuzAYXnOYWnTYRn9
lTm/Sj/DwmFp6rBer4zqvRTKDhyW5xyWLgUrur/CYbnRYQUKV2dzqd5NwdiZYeGw3OWwNBm6m6Xq
x0+Eplg4LJc6rFCXpUnFOonDwmFJyRORwWG512EFZlja9FhDcViec1haSPdgd2UFh+Vmh6XVHKs/
DstzDkuHghXbXeGw3OywOr7eORDVu6rY3rOEOCx3OSwNHn62SlV4goXDcrfDMmdYesyxZuKwPOew
1Bespk/EBIflcodlLi16rO04LM85LOXnYQ0K91c4LI84LKvL6jRc9d7iPCzvOSzVRyTvSdhf4bDc
7bCC+VLx5voSh+U5h6W4YPWOKFU4LO84rODaoXZ3TcFhec5hLVe6oy4+0fQJWzMsHJbLHFYws9QW
LByW5xyW0nsJF8+I6q5wWB5yWK+/3qmzsUpU7i/uJfSew1J68/O2pnYnWDgs1zksK0oH75/isDzn
sFQWrKInEk6wcFjecFivmx1W5ykKN9gQHJbnHNYgddtpYdOmTe3OsHBYLnRYVk6q22GjcFiec1jq
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" />
</svg>

After

Width:  |  Height:  |  Size: 41 KiB

BIN
public/logo_nexty.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

BIN
public/og.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 297 KiB

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