12 Commits

Author SHA1 Message Date
typist
d553c3e04c 0.0.17
All checks were successful
Build and Push Docker Image / build (push) Successful in 1m1s
2025-10-29 08:27:15 +08:00
typist
e2da2758cc chore: simplify build script in package.json
- Removed sitemap generation step from the build script for a more streamlined build process.
2025-10-29 08:27:00 +08:00
typist
3ab70498e6 feat: add ModeToggle component to layout and sidebar
- Integrated ModeToggle component into the layout for theme switching.
- Replaced the existing button in the sidebar footer with SidebarMenuButton for improved styling and functionality.
2025-10-29 08:26:26 +08:00
typist
a5ef1a1e70 0.0.16
All checks were successful
Build and Push Docker Image / build (push) Successful in 1m7s
2025-10-29 08:16:17 +08:00
typist
297000f208 feat: integrate SEO hook for dynamic metadata management
- Removed hardcoded canonical link from index.html.
- Implemented useSEO hook in layout.tsx to dynamically update SEO metadata including title, description, and canonical URL based on the current route.
- Added new use-seo.ts file containing the SEO hook logic for improved search engine optimization.
2025-10-29 08:15:53 +08:00
typist
04fbf12e07 0.0.15
All checks were successful
Build and Push Docker Image / build (push) Successful in 1m6s
2025-10-29 08:06:21 +08:00
a9a6354b2d Merge pull request 'feat: implement SEO enhancements and sitemap generation' (#10) from feat/seo into main
Reviewed-on: #10
2025-10-29 08:05:18 +08:00
typist
109139a42e feat: implement SEO enhancements and sitemap generation
- Added comprehensive SEO meta tags in index.html for improved search engine visibility, including Open Graph and Twitter Card tags.
- Created a sitemap.xml for better indexing of site tools, generated automatically via a new script (generate-sitemap.ts).
- Introduced robots.txt to manage crawler access and specified sitemap location.
- Updated package.json to include a build step for sitemap generation and added tsx as a dependency.
- Documented SEO optimizations and usage instructions in a new SEO-README.md file.
2025-10-29 08:05:37 +08:00
typist
b5a811e5ee 0.0.14
All checks were successful
Build and Push Docker Image / build (push) Successful in 1m0s
2025-10-29 07:28:46 +08:00
typist
b3adfe5c8f refactor: update IP query API and response handling
- Switched from ip-api.com to ipinfo.io for IP information retrieval, ensuring more reliable service.
- Simplified response handling by directly using the data returned from ipinfo.io, eliminating unnecessary data transformation.
- Adjusted risk level determination logic to utilize the 'org' field for identifying potential hosting or datacenter IPs.
2025-10-29 07:28:21 +08:00
typist
8eda2eae99 0.0.13
All checks were successful
Build and Push Docker Image / build (push) Successful in 1m1s
2025-10-29 07:18:09 +08:00
typist
99673913a6 chore: comment out cache settings in build workflow
- Temporarily disabled cache-from and cache-to settings in the build workflow to prevent potential issues with the build process.
- Retained platform specification for compatibility.
2025-10-29 07:18:02 +08:00
13 changed files with 860 additions and 68 deletions

View File

@@ -43,8 +43,8 @@ jobs:
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=registry,ref=${{ secrets.REGISTRY_ENDPOINT }}/${{ github.repository_owner }}/litek:buildcache
cache-to: type=registry,ref=${{ secrets.REGISTRY_ENDPOINT }}/${{ github.repository_owner }}/litek:buildcache,mode=max
# cache-from: type=registry,ref=${{ secrets.REGISTRY_ENDPOINT }}/${{ github.repository_owner }}/litek:buildcache
# cache-to: type=registry,ref=${{ secrets.REGISTRY_ENDPOINT }}/${{ github.repository_owner }}/litek:buildcache,mode=max
# platforms: linux/amd64,linux/arm64
platforms: linux/amd64

171
SEO-README.md Normal file
View File

@@ -0,0 +1,171 @@
# SEO 优化说明
本项目已完成基础 SEO 优化,以下是已实施的改动。
## 已完成的优化项
### 1. HTML Meta 标签优化 ✅
`index.html` 中添加了完整的 SEO 元数据:
- **基础 Meta 标签**
- title, description, keywords
- author, theme-color
- canonical URL
- **Open Graph 标签**(社交媒体分享优化)
- og:type, og:url, og:title
- og:description, og:image, og:site_name
- **Twitter Card 标签**
- twitter:card, twitter:title
- twitter:description, twitter:image
- **结构化数据**JSON-LD
- Schema.org WebSite 类型标记
- 提升搜索引擎理解能力
### 2. SEO 配置文件 ✅
- **`public/robots.txt`** - 搜索引擎爬虫规则
- 允许所有爬虫访问
- 指定 sitemap 位置
- **`public/sitemap.xml`** - 站点地图(自动生成)
- 包含所有工具页面 URL
- 设置更新频率和优先级
- 通过 `scripts/generate-sitemap.ts` 自动生成
- **`public/manifest.json`** - PWA 配置
- 支持渐进式 Web 应用
- 改善移动端体验
### 3. 构建流程优化 ✅
- **`package.json`**
- 构建时自动生成 sitemap
- 添加 tsx 依赖用于运行生成脚本
## 文件清单
### 新增文件
```
public/
├── robots.txt # 爬虫规则
├── sitemap.xml # 站点地图(自动生成)
└── manifest.json # PWA 配置
scripts/
└── generate-sitemap.ts # Sitemap 生成脚本
```
### 修改文件
```
index.html # 添加 SEO meta 标签和结构化数据
package.json # 添加 sitemap 生成命令
```
## 使用说明
### 开发
```bash
pnpm install # 安装依赖
pnpm dev # 启动开发服务器
```
### 构建
```bash
pnpm run build # 构建项目(自动生成 sitemap
pnpm run generate:sitemap # 单独生成 sitemap
```
### 部署前检查
⚠️ **重要:部署前必须更新配置文件中的域名!**
需要将以下文件中的 `https://litek.typist.cc` 替换为你的实际域名:
1. **`index.html`**
- canonical URL
- Open Graph URL 和 image
- Twitter Card image
- Structured Data URL
2. **`public/robots.txt`**
- Sitemap URL
3. **`scripts/generate-sitemap.ts`**
- BASE_URL 常量
更新后重新构建:
```bash
pnpm run build
```
## SEO 验证
### 部署后验证
1. **提交 sitemap 到 Google Search Console**
- 访问 https://search.google.com/search-console
- 添加网站资源
- 提交 sitemap: `https://你的域名/sitemap.xml`
2. **测试结构化数据**
- 访问 https://validator.schema.org/
- 输入网站 URL
- 检查是否有错误
3. **测试 Open Graph 预览**
- Facebook: https://developers.facebook.com/tools/debug/
- Twitter: https://cards-dev.twitter.com/validator
4. **性能测试**
- Google PageSpeed Insights: https://pagespeed.web.dev/
- 目标分数: SEO > 95
## 可访问的 URL
部署后,以下 URL 应该可以正常访问:
- `https://你的域名/` - 主页
- `https://你的域名/robots.txt` - 爬虫规则
- `https://你的域名/sitemap.xml` - 站点地图
- `https://你的域名/manifest.json` - PWA 配置
- `https://你的域名/tool/uuid` - UUID 工具
- `https://你的域名/tool/json` - JSON 工具
- `https://你的域名/tool/base64` - Base64 工具
- `https://你的域名/tool/network/dns` - DNS 工具
- ... 其他工具页面
## 添加新工具时更新 SEO
当添加新工具时,需要更新 `scripts/generate-sitemap.ts`
```typescript
const tools = [
// 现有工具...
{ path: '/tool/你的新工具', priority: '0.9' },
];
```
然后重新构建项目。
## SEO 效果预期
- **1-2 周**:搜索引擎开始索引页面
- **1-2 月**:主要关键词开始有排名
- **3-6 月**:稳定的搜索排名和自然流量增长
## 技术栈
- React 19 + TypeScript
- Vite (Rolldown)
- React Router v7
- Radix UI + Tailwind CSS 4
- Nginx
## 联系方式
需要更多工具或有建议联系litek@mail.typist.cc

View File

@@ -2,9 +2,55 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/lite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lite Kit</title>
<!-- Primary Meta Tags -->
<title>Lite Kit - Lightweight Online Tools</title>
<meta name="description" content="Free online tools including UUID generator, JSON formatter, Base64 encoder/decoder, network testing tools and more. Fast, secure, and easy to use." />
<meta name="keywords" content="online tools,UUID generator,JSON formatter,Base64 encoder,network tools,DNS lookup,Ping test,TCPing,speed test,IP query" />
<meta name="author" content="Lite Kit" />
<meta name="theme-color" content="#000000" />
<!-- Favicon -->
<link rel="icon" type="image/svg+xml" href="/lite.svg" />
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content="https://litek.typist.cc/" />
<meta property="og:title" content="Lite Kit - Lightweight Online Tools" />
<meta property="og:description" content="Free online tools including UUID generator, JSON formatter, Base64 encoder/decoder, network testing tools and more." />
<meta property="og:image" content="https://litek.typist.cc/lite.svg" />
<meta property="og:site_name" content="Lite Kit" />
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Lite Kit - Lightweight Online Tools" />
<meta name="twitter:description" content="Free online tools including UUID generator, JSON formatter, Base64 encoder/decoder, network testing tools and more." />
<meta name="twitter:image" content="https://litek.typist.cc/lite.svg" />
<!-- PWA Manifest -->
<link rel="manifest" href="/manifest.json" />
<!-- Structured Data (JSON-LD) -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebSite",
"name": "Lite Kit",
"description": "Free online tools including UUID generator, JSON formatter, Base64 encoder/decoder, network testing tools and more",
"url": "https://litek.typist.cc/",
"author": {
"@type": "Organization",
"name": "Lite Kit"
},
"applicationCategory": "UtilitiesApplication",
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "USD"
}
}
</script>
</head>
<body>
<div id="root"></div>

View File

@@ -1,11 +1,12 @@
{
"name": "litek",
"private": true,
"version": "0.0.12",
"version": "0.0.17",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"generate:sitemap": "tsx scripts/generate-sitemap.ts",
"lint": "eslint .",
"preview": "vite preview",
"release:patch": "npm version patch && git push --follow-tags",
@@ -43,6 +44,7 @@
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.22",
"globals": "^16.4.0",
"tsx": "^4.19.2",
"tw-animate-css": "^1.4.0",
"typescript": "~5.9.3",
"typescript-eslint": "^8.45.0",
@@ -53,7 +55,8 @@
"vite": "npm:rolldown-vite@7.1.14"
},
"onlyBuiltDependencies": [
"@swc/core"
"@swc/core",
"esbuild"
]
}
}

313
pnpm-lock.yaml generated
View File

@@ -31,7 +31,7 @@ importers:
version: 1.2.8(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@tailwindcss/vite':
specifier: ^4.1.16
version: 4.1.16(rolldown-vite@7.1.14(@types/node@24.9.1)(jiti@2.6.1))
version: 4.1.16(rolldown-vite@7.1.14(@types/node@24.9.1)(esbuild@0.25.11)(jiti@2.6.1)(tsx@4.20.6))
class-variance-authority:
specifier: ^0.7.1
version: 0.7.1
@@ -83,7 +83,7 @@ importers:
version: 19.2.2(@types/react@19.2.2)
'@vitejs/plugin-react':
specifier: ^5.1.0
version: 5.1.0(rolldown-vite@7.1.14(@types/node@24.9.1)(jiti@2.6.1))
version: 5.1.0(rolldown-vite@7.1.14(@types/node@24.9.1)(esbuild@0.25.11)(jiti@2.6.1)(tsx@4.20.6))
eslint:
specifier: ^9.36.0
version: 9.38.0(jiti@2.6.1)
@@ -96,6 +96,9 @@ importers:
globals:
specifier: ^16.4.0
version: 16.4.0
tsx:
specifier: ^4.19.2
version: 4.20.6
tw-animate-css:
specifier: ^1.4.0
version: 1.4.0
@@ -107,7 +110,7 @@ importers:
version: 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)
vite:
specifier: npm:rolldown-vite@7.1.14
version: rolldown-vite@7.1.14(@types/node@24.9.1)(jiti@2.6.1)
version: rolldown-vite@7.1.14(@types/node@24.9.1)(esbuild@0.25.11)(jiti@2.6.1)(tsx@4.20.6)
packages:
@@ -203,6 +206,162 @@ packages:
'@emnapi/wasi-threads@1.1.0':
resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==}
'@esbuild/aix-ppc64@0.25.11':
resolution: {integrity: sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [aix]
'@esbuild/android-arm64@0.25.11':
resolution: {integrity: sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==}
engines: {node: '>=18'}
cpu: [arm64]
os: [android]
'@esbuild/android-arm@0.25.11':
resolution: {integrity: sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==}
engines: {node: '>=18'}
cpu: [arm]
os: [android]
'@esbuild/android-x64@0.25.11':
resolution: {integrity: sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==}
engines: {node: '>=18'}
cpu: [x64]
os: [android]
'@esbuild/darwin-arm64@0.25.11':
resolution: {integrity: sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==}
engines: {node: '>=18'}
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-x64@0.25.11':
resolution: {integrity: sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [darwin]
'@esbuild/freebsd-arm64@0.25.11':
resolution: {integrity: sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==}
engines: {node: '>=18'}
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-x64@0.25.11':
resolution: {integrity: sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==}
engines: {node: '>=18'}
cpu: [x64]
os: [freebsd]
'@esbuild/linux-arm64@0.25.11':
resolution: {integrity: sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==}
engines: {node: '>=18'}
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm@0.25.11':
resolution: {integrity: sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==}
engines: {node: '>=18'}
cpu: [arm]
os: [linux]
'@esbuild/linux-ia32@0.25.11':
resolution: {integrity: sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==}
engines: {node: '>=18'}
cpu: [ia32]
os: [linux]
'@esbuild/linux-loong64@0.25.11':
resolution: {integrity: sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==}
engines: {node: '>=18'}
cpu: [loong64]
os: [linux]
'@esbuild/linux-mips64el@0.25.11':
resolution: {integrity: sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==}
engines: {node: '>=18'}
cpu: [mips64el]
os: [linux]
'@esbuild/linux-ppc64@0.25.11':
resolution: {integrity: sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [linux]
'@esbuild/linux-riscv64@0.25.11':
resolution: {integrity: sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==}
engines: {node: '>=18'}
cpu: [riscv64]
os: [linux]
'@esbuild/linux-s390x@0.25.11':
resolution: {integrity: sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==}
engines: {node: '>=18'}
cpu: [s390x]
os: [linux]
'@esbuild/linux-x64@0.25.11':
resolution: {integrity: sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [linux]
'@esbuild/netbsd-arm64@0.25.11':
resolution: {integrity: sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [netbsd]
'@esbuild/netbsd-x64@0.25.11':
resolution: {integrity: sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==}
engines: {node: '>=18'}
cpu: [x64]
os: [netbsd]
'@esbuild/openbsd-arm64@0.25.11':
resolution: {integrity: sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openbsd]
'@esbuild/openbsd-x64@0.25.11':
resolution: {integrity: sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==}
engines: {node: '>=18'}
cpu: [x64]
os: [openbsd]
'@esbuild/openharmony-arm64@0.25.11':
resolution: {integrity: sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openharmony]
'@esbuild/sunos-x64@0.25.11':
resolution: {integrity: sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==}
engines: {node: '>=18'}
cpu: [x64]
os: [sunos]
'@esbuild/win32-arm64@0.25.11':
resolution: {integrity: sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==}
engines: {node: '>=18'}
cpu: [arm64]
os: [win32]
'@esbuild/win32-ia32@0.25.11':
resolution: {integrity: sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==}
engines: {node: '>=18'}
cpu: [ia32]
os: [win32]
'@esbuild/win32-x64@0.25.11':
resolution: {integrity: sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==}
engines: {node: '>=18'}
cpu: [x64]
os: [win32]
'@eslint-community/eslint-utils@4.9.0':
resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -1035,6 +1194,11 @@ packages:
resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==}
engines: {node: '>=10.13.0'}
esbuild@0.25.11:
resolution: {integrity: sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==}
engines: {node: '>=18'}
hasBin: true
escalade@3.2.0:
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
engines: {node: '>=6'}
@@ -1153,6 +1317,9 @@ packages:
resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==}
engines: {node: '>=6'}
get-tsconfig@4.13.0:
resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==}
glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'}
@@ -1489,6 +1656,9 @@ packages:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'}
resolve-pkg-maps@1.0.0:
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
reusify@1.1.0:
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
@@ -1609,6 +1779,11 @@ packages:
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
tsx@4.20.6:
resolution: {integrity: sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==}
engines: {node: '>=18.0.0'}
hasBin: true
tw-animate-css@1.4.0:
resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==}
@@ -1810,6 +1985,84 @@ snapshots:
tslib: 2.8.1
optional: true
'@esbuild/aix-ppc64@0.25.11':
optional: true
'@esbuild/android-arm64@0.25.11':
optional: true
'@esbuild/android-arm@0.25.11':
optional: true
'@esbuild/android-x64@0.25.11':
optional: true
'@esbuild/darwin-arm64@0.25.11':
optional: true
'@esbuild/darwin-x64@0.25.11':
optional: true
'@esbuild/freebsd-arm64@0.25.11':
optional: true
'@esbuild/freebsd-x64@0.25.11':
optional: true
'@esbuild/linux-arm64@0.25.11':
optional: true
'@esbuild/linux-arm@0.25.11':
optional: true
'@esbuild/linux-ia32@0.25.11':
optional: true
'@esbuild/linux-loong64@0.25.11':
optional: true
'@esbuild/linux-mips64el@0.25.11':
optional: true
'@esbuild/linux-ppc64@0.25.11':
optional: true
'@esbuild/linux-riscv64@0.25.11':
optional: true
'@esbuild/linux-s390x@0.25.11':
optional: true
'@esbuild/linux-x64@0.25.11':
optional: true
'@esbuild/netbsd-arm64@0.25.11':
optional: true
'@esbuild/netbsd-x64@0.25.11':
optional: true
'@esbuild/openbsd-arm64@0.25.11':
optional: true
'@esbuild/openbsd-x64@0.25.11':
optional: true
'@esbuild/openharmony-arm64@0.25.11':
optional: true
'@esbuild/sunos-x64@0.25.11':
optional: true
'@esbuild/win32-arm64@0.25.11':
optional: true
'@esbuild/win32-ia32@0.25.11':
optional: true
'@esbuild/win32-x64@0.25.11':
optional: true
'@eslint-community/eslint-utils@4.9.0(eslint@9.38.0(jiti@2.6.1))':
dependencies:
eslint: 9.38.0(jiti@2.6.1)
@@ -2351,12 +2604,12 @@ snapshots:
'@tailwindcss/oxide-win32-arm64-msvc': 4.1.16
'@tailwindcss/oxide-win32-x64-msvc': 4.1.16
'@tailwindcss/vite@4.1.16(rolldown-vite@7.1.14(@types/node@24.9.1)(jiti@2.6.1))':
'@tailwindcss/vite@4.1.16(rolldown-vite@7.1.14(@types/node@24.9.1)(esbuild@0.25.11)(jiti@2.6.1)(tsx@4.20.6))':
dependencies:
'@tailwindcss/node': 4.1.16
'@tailwindcss/oxide': 4.1.16
tailwindcss: 4.1.16
vite: rolldown-vite@7.1.14(@types/node@24.9.1)(jiti@2.6.1)
vite: rolldown-vite@7.1.14(@types/node@24.9.1)(esbuild@0.25.11)(jiti@2.6.1)(tsx@4.20.6)
'@tybys/wasm-util@0.10.1':
dependencies:
@@ -2493,7 +2746,7 @@ snapshots:
'@typescript-eslint/types': 8.46.2
eslint-visitor-keys: 4.2.1
'@vitejs/plugin-react@5.1.0(rolldown-vite@7.1.14(@types/node@24.9.1)(jiti@2.6.1))':
'@vitejs/plugin-react@5.1.0(rolldown-vite@7.1.14(@types/node@24.9.1)(esbuild@0.25.11)(jiti@2.6.1)(tsx@4.20.6))':
dependencies:
'@babel/core': 7.28.5
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.5)
@@ -2501,7 +2754,7 @@ snapshots:
'@rolldown/pluginutils': 1.0.0-beta.43
'@types/babel__core': 7.20.5
react-refresh: 0.18.0
vite: rolldown-vite@7.1.14(@types/node@24.9.1)(jiti@2.6.1)
vite: rolldown-vite@7.1.14(@types/node@24.9.1)(esbuild@0.25.11)(jiti@2.6.1)(tsx@4.20.6)
transitivePeerDependencies:
- supports-color
@@ -2607,6 +2860,35 @@ snapshots:
graceful-fs: 4.2.11
tapable: 2.3.0
esbuild@0.25.11:
optionalDependencies:
'@esbuild/aix-ppc64': 0.25.11
'@esbuild/android-arm': 0.25.11
'@esbuild/android-arm64': 0.25.11
'@esbuild/android-x64': 0.25.11
'@esbuild/darwin-arm64': 0.25.11
'@esbuild/darwin-x64': 0.25.11
'@esbuild/freebsd-arm64': 0.25.11
'@esbuild/freebsd-x64': 0.25.11
'@esbuild/linux-arm': 0.25.11
'@esbuild/linux-arm64': 0.25.11
'@esbuild/linux-ia32': 0.25.11
'@esbuild/linux-loong64': 0.25.11
'@esbuild/linux-mips64el': 0.25.11
'@esbuild/linux-ppc64': 0.25.11
'@esbuild/linux-riscv64': 0.25.11
'@esbuild/linux-s390x': 0.25.11
'@esbuild/linux-x64': 0.25.11
'@esbuild/netbsd-arm64': 0.25.11
'@esbuild/netbsd-x64': 0.25.11
'@esbuild/openbsd-arm64': 0.25.11
'@esbuild/openbsd-x64': 0.25.11
'@esbuild/openharmony-arm64': 0.25.11
'@esbuild/sunos-x64': 0.25.11
'@esbuild/win32-arm64': 0.25.11
'@esbuild/win32-ia32': 0.25.11
'@esbuild/win32-x64': 0.25.11
escalade@3.2.0: {}
escape-string-regexp@4.0.0: {}
@@ -2736,6 +3018,10 @@ snapshots:
get-nonce@1.0.1: {}
get-tsconfig@4.13.0:
dependencies:
resolve-pkg-maps: 1.0.0
glob-parent@5.1.2:
dependencies:
is-glob: 4.0.3
@@ -2994,9 +3280,11 @@ snapshots:
resolve-from@4.0.0: {}
resolve-pkg-maps@1.0.0: {}
reusify@1.1.0: {}
rolldown-vite@7.1.14(@types/node@24.9.1)(jiti@2.6.1):
rolldown-vite@7.1.14(@types/node@24.9.1)(esbuild@0.25.11)(jiti@2.6.1)(tsx@4.20.6):
dependencies:
'@oxc-project/runtime': 0.92.0
fdir: 6.5.0(picomatch@4.0.3)
@@ -3007,8 +3295,10 @@ snapshots:
tinyglobby: 0.2.15
optionalDependencies:
'@types/node': 24.9.1
esbuild: 0.25.11
fsevents: 2.3.3
jiti: 2.6.1
tsx: 4.20.6
rolldown@1.0.0-beta.41:
dependencies:
@@ -3083,6 +3373,13 @@ snapshots:
tslib@2.8.1: {}
tsx@4.20.6:
dependencies:
esbuild: 0.25.11
get-tsconfig: 4.13.0
optionalDependencies:
fsevents: 2.3.3
tw-animate-css@1.4.0: {}
type-check@0.4.0:

21
public/manifest.json Normal file
View File

@@ -0,0 +1,21 @@
{
"name": "Lite Kit - Lightweight Online Tools",
"short_name": "Lite Kit",
"description": "Free online tools including UUID generator, JSON formatter, Base64 encoder/decoder, network testing tools and more",
"start_url": "/",
"display": "standalone",
"background_color": "#000000",
"theme_color": "#000000",
"orientation": "portrait-primary",
"icons": [
{
"src": "/lite.svg",
"type": "image/svg+xml",
"sizes": "any"
}
],
"categories": ["utilities", "productivity"],
"lang": "en",
"dir": "ltr"
}

21
public/robots.txt Normal file
View File

@@ -0,0 +1,21 @@
# Allow all crawlers
User-agent: *
Allow: /
# Sitemaps
Sitemap: https://litek.typist.cc/sitemap.xml
# Common bots
User-agent: Googlebot
Allow: /
User-agent: Bingbot
Allow: /
User-agent: Baiduspider
Allow: /
# Crawl-delay for less aggressive bots
User-agent: *
Crawl-delay: 1

63
public/sitemap.xml Normal file
View File

@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://litek.typist.cc</loc>
<lastmod>2025-10-29</lastmod>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://litek.typist.cc/tool</loc>
<lastmod>2025-10-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://litek.typist.cc/tool/uuid</loc>
<lastmod>2025-10-29</lastmod>
<changefreq>monthly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://litek.typist.cc/tool/json</loc>
<lastmod>2025-10-29</lastmod>
<changefreq>monthly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://litek.typist.cc/tool/base64</loc>
<lastmod>2025-10-29</lastmod>
<changefreq>monthly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://litek.typist.cc/tool/network/dns</loc>
<lastmod>2025-10-29</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://litek.typist.cc/tool/network/ping</loc>
<lastmod>2025-10-29</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://litek.typist.cc/tool/network/tcping</loc>
<lastmod>2025-10-29</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://litek.typist.cc/tool/network/speedtest</loc>
<lastmod>2025-10-29</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://litek.typist.cc/tool/network/ipquery</loc>
<lastmod>2025-10-29</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
</urlset>

View File

@@ -0,0 +1,83 @@
import * as fs from 'fs';
import * as path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
interface SitemapUrl {
loc: string;
lastmod: string;
changefreq: string;
priority: string;
}
const BASE_URL = 'https://litek.typist.cc';
const currentDate = new Date().toISOString().split('T')[0];
const tools = [
{ path: '/tool/uuid', priority: '0.9' },
{ path: '/tool/json', priority: '0.9' },
{ path: '/tool/base64', priority: '0.9' },
{ path: '/tool/network/dns', priority: '0.8' },
{ path: '/tool/network/ping', priority: '0.8' },
{ path: '/tool/network/tcping', priority: '0.8' },
{ path: '/tool/network/speedtest', priority: '0.8' },
{ path: '/tool/network/ipquery', priority: '0.8' },
];
const urls: SitemapUrl[] = [
{
loc: BASE_URL,
lastmod: currentDate,
changefreq: 'weekly',
priority: '1.0',
},
{
loc: `${BASE_URL}/tool`,
lastmod: currentDate,
changefreq: 'weekly',
priority: '0.9',
},
];
// Add all tool pages
tools.forEach((tool) => {
urls.push({
loc: `${BASE_URL}${tool.path}`,
lastmod: currentDate,
changefreq: 'monthly',
priority: tool.priority,
});
});
function generateSitemap(): string {
let xml = '<?xml version="1.0" encoding="UTF-8"?>\n';
xml += '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n';
urls.forEach((url) => {
xml += ' <url>\n';
xml += ` <loc>${url.loc}</loc>\n`;
xml += ` <lastmod>${url.lastmod}</lastmod>\n`;
xml += ` <changefreq>${url.changefreq}</changefreq>\n`;
xml += ` <priority>${url.priority}</priority>\n`;
xml += ' </url>\n';
});
xml += '</urlset>\n';
return xml;
}
// Generate and write sitemap
const sitemap = generateSitemap();
const publicDir = path.resolve(__dirname, '../public');
const sitemapPath = path.join(publicDir, 'sitemap.xml');
// Ensure public directory exists
if (!fs.existsSync(publicDir)) {
fs.mkdirSync(publicDir, { recursive: true });
}
fs.writeFileSync(sitemapPath, sitemap, 'utf-8');
console.log(`✅ Sitemap generated successfully at ${sitemapPath}`);

View File

@@ -1,11 +1,11 @@
import type { ReactNode } from "react";
import { Link } from "react-router-dom";
import { ChevronRight } from "lucide-react";
import { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarMenuButton, SidebarMenuItem, SidebarMenu, SidebarMenuSub, SidebarMenuSubItem, SidebarMenuSubButton } from "@/components/ui/sidebar";
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "@/components/ui/collapsible";
import { tools, type Tool } from "@/components/tool";
import { Link } from "react-router-dom";
import { ModeToggle } from "@/components/theme/toggle";
import { Button } from "../ui/button";
import { ChevronRight } from "lucide-react";
export const AppSidebar = () => {
// 递归构建完整路径
@@ -88,10 +88,9 @@ export const AppSidebar = () => {
</SidebarGroup>
</SidebarContent>
<SidebarFooter className="flex flex-row justify-between items-center gap-2">
<Button variant="link">
<SidebarMenuButton variant="outline" asChild>
<a href="mailto:litek@mail.typist.cc">need more tools?</a>
</Button>
<ModeToggle />
</SidebarMenuButton>
</SidebarFooter>
</Sidebar>
);

View File

@@ -91,8 +91,8 @@ const Tool: FC = () => {
const startTime = performance.now();
try {
// 使用 ip-api.com (免费,功能较全)
const response = await fetch(`http://ip-api.com/json/${encodeURIComponent(ip.trim())}?fields=status,message,country,countryCode,region,city,lat,lon,timezone,isp,org,as,proxy,hosting,query`);
// 使用 ipinfo.io (免费,稳定可靠)
const response = await fetch(`https://ipinfo.io/${encodeURIComponent(ip.trim())}/json`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
@@ -103,28 +103,8 @@ const Tool: FC = () => {
setQueryTime(endTime - startTime);
if (data.status === "fail") {
toast.error(data.message || "Query failed");
return;
}
// 转换为统一格式
const ipData: IPInfo = {
ip: data.query,
city: data.city,
region: data.region,
country: data.country,
countryCode: data.countryCode,
loc: data.lat && data.lon ? `${data.lat},${data.lon}` : undefined,
timezone: data.timezone,
isp: data.isp,
org: data.org,
as: data.as,
proxy: data.proxy,
hosting: data.hosting,
};
setIpInfo(ipData);
// ipinfo.io 返回格式已经符合 IPInfo 接口
setIpInfo(data);
toast.success("Query successful");
} catch (error) {
if (error instanceof Error) {
@@ -146,14 +126,18 @@ const Tool: FC = () => {
const getRiskLevel = () => {
if (!ipInfo) return null;
if (ipInfo.proxy || ipInfo.hosting) {
// ipinfo.io 通过 org 字段可以简单判断是否为托管IP
const orgLower = ipInfo.org?.toLowerCase() || "";
const isHosting = orgLower.includes("hosting") ||
orgLower.includes("datacenter") ||
orgLower.includes("cloud") ||
orgLower.includes("server");
if (isHosting) {
return {
level: "High",
color: "text-red-500",
reasons: [
ipInfo.proxy && "Proxy/VPN detected",
ipInfo.hosting && "Hosting/Datacenter IP",
].filter(Boolean),
level: "Medium",
color: "text-yellow-500",
reasons: ["Possible Hosting/Datacenter IP"],
};
}

96
src/hooks/use-seo.ts Normal file
View File

@@ -0,0 +1,96 @@
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
interface UseSEOOptions {
title?: string;
description?: string;
baseUrl?: string;
}
/**
* SEO Hook - 动态更新页面 SEO 元数据
*
* @param options - SEO 配置选项
* @param options.title - 页面标题(可选)
* @param options.description - 页面描述(可选)
* @param options.baseUrl - 网站基础 URL默认为 https://litek.typist.cc
*
* @example
* ```tsx
* // 在组件中使用
* useSEO({
* title: 'UUID Generator',
* description: 'Free online UUID generator tool'
* });
* ```
*/
export const useSEO = (options: UseSEOOptions = {}) => {
const location = useLocation();
const {
title,
description,
baseUrl = 'https://litek.typist.cc'
} = options;
useEffect(() => {
// 构建当前页面的完整 URL
const canonicalUrl = `${baseUrl}${location.pathname}`;
// 更新或创建 canonical 链接
let canonical = document.querySelector('link[rel="canonical"]') as HTMLLinkElement;
if (!canonical) {
canonical = document.createElement('link');
canonical.setAttribute('rel', 'canonical');
document.head.appendChild(canonical);
}
canonical.setAttribute('href', canonicalUrl);
// 更新页面标题
if (title) {
document.title = `${title} - Lite Kit`;
}
// 更新 meta description
if (description) {
let metaDescription = document.querySelector('meta[name="description"]') as HTMLMetaElement;
if (metaDescription) {
metaDescription.setAttribute('content', description);
}
}
// 更新 Open Graph URL
let ogUrl = document.querySelector('meta[property="og:url"]') as HTMLMetaElement;
if (ogUrl) {
ogUrl.setAttribute('content', canonicalUrl);
}
// 更新 Open Graph Title
if (title) {
let ogTitle = document.querySelector('meta[property="og:title"]') as HTMLMetaElement;
if (ogTitle) {
ogTitle.setAttribute('content', `${title} - Lite Kit`);
}
// 更新 Twitter Card Title
let twitterTitle = document.querySelector('meta[name="twitter:title"]') as HTMLMetaElement;
if (twitterTitle) {
twitterTitle.setAttribute('content', `${title} - Lite Kit`);
}
}
// 更新 Open Graph Description
if (description) {
let ogDescription = document.querySelector('meta[property="og:description"]') as HTMLMetaElement;
if (ogDescription) {
ogDescription.setAttribute('content', description);
}
// 更新 Twitter Card Description
let twitterDescription = document.querySelector('meta[name="twitter:description"]') as HTMLMetaElement;
if (twitterDescription) {
twitterDescription.setAttribute('content', description);
}
}
}, [location.pathname, title, description, baseUrl]);
};

View File

@@ -4,20 +4,28 @@ import { Outlet } from "react-router-dom";
import { ThemeProvider } from "@/components/theme/provider"
import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"
import { AppSidebar } from "@/components/sidebar";
import { ModeToggle } from "@/components/theme/toggle";
export const Layout: FC = () => (
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
<SidebarProvider>
<AppSidebar />
<div className="p-4 flex flex-col w-full h-[100vh] overflow-hidden">
<nav className="flex items-center justify-between">
<SidebarTrigger className="size-10" />
<div role="actions" />
</nav>
<main className="flex-1 overflow-auto p-4 overflow-hidden">
<Outlet />
</main>
</div>
</SidebarProvider>
</ThemeProvider>
);
import { useSEO } from "@/hooks/use-seo";
export const Layout: FC = () => {
// 使用 SEO hook 自动更新 canonical URL 和其他 SEO 元数据
useSEO();
return (
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
<SidebarProvider>
<AppSidebar />
<div className="p-4 flex flex-col w-full h-[100vh] overflow-hidden">
<nav className="flex items-center justify-between">
<SidebarTrigger className="size-10" />
<ModeToggle />
</nav>
<main className="flex-1 overflow-auto p-4 overflow-hidden">
<Outlet />
</main>
</div>
</SidebarProvider>
</ThemeProvider>
);
};