diff --git a/.github/workflows/build_and_publish.yml b/.github/workflows/build_and_publish.yml index 0b4055d..0fae30d 100644 --- a/.github/workflows/build_and_publish.yml +++ b/.github/workflows/build_and_publish.yml @@ -1,84 +1,87 @@ name: 发布新版本 on: - workflow_dispatch: + workflow_dispatch: + inputs: + enable_seo: + description: "启用SEO优化构建" + required: false + default: false + type: boolean permissions: write-all jobs: - publish: - name: 发布新版本 - runs-on: ubuntu-latest + publish: + name: 发布新版本 + runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 + steps: + - name: Checkout + uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + name: Install pnpm + with: + version: 10 + run_install: false + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: "pnpm" + - name: Install dependencies + run: pnpm install - - uses: pnpm/action-setup@v4 - name: Install pnpm - with: - version: 10 - run_install: false + - name: 安装 Playwright 浏览器 (仅 SEO 构建需要) + if: github.event.inputs.enable_seo == 'true' + run: npx playwright install chromium - - name: Install Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: 'pnpm' + - name: 更新制作人员列表 + run: node ${{ github.workspace }}/.github/workflows/scripts/update_authors.js - - name: Install dependencies - run: pnpm install + - name: 检查版本并同步版本号 + run: node ${{ github.workspace }}/.github/workflows/scripts/sync_version.js + - name: 检查更新日志并转markdown + run: | + node ${{ github.workspace }}/.github/workflows/scripts/update_changelog.js # 其他预处理以后要用的时候在上方添加 - - name: 安装依赖 - if: steps.pnpm-cache.outputs.cache-hit != 'true' - run: pnpm install + - name: 为网站SEO构建 + if: github.event.inputs.enable_seo == 'true' + run: pnpm run build:website:seo - - name: 更新制作人员列表 - run: node ${{ github.workspace }}/.github/workflows/scripts/update_authors.js + - name: 发布到 GitHub Pages + uses: crazy-max/ghaction-github-pages@v2.1.3 + with: + target_branch: gh-pages + build_dir: ./dist + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: 检查版本并同步版本号 - run: node ${{ github.workspace }}/.github/workflows/scripts/sync_version.js - - name: 检查更新日志并转markdown - run: | - node ${{ github.workspace }}/.github/workflows/scripts/update_changelog.js - # 其他预处理以后要用的时候在上方添加 - - - name: 为网站构建 - run: pnpm run build:website - - - name: 发布到 GitHub Pages - uses: crazy-max/ghaction-github-pages@v2.1.3 - with: - target_branch: gh-pages - build_dir: ./dist - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: 删除无用文件夹 - run: | - cd ${{ github.workspace }} - rm -rf ./dist - rm -rf ./nuitka-commander - ls - # 这可以做一些其他特殊的所需处理 - - name: 为本地运行构建 - run: | - pnpm run build:local_use - mv ${{ github.workspace }}/dist/index.html ${{ github.workspace }}/dist/Nuitka-Commander.html - - name: 读取版本号 - id: read_version #此处刚需存储在scripts中的version文件 - run: | - echo "VERSION=$(cat ${{ github.workspace }}/.github/workflows/scripts/version)" >> $GITHUB_ENV - - name: 为Github发布release - run: | # 此处刚需存储在scripts中的CHANGELOG.md文件 - gh release create $VERSION ${{ github.workspace }}/dist/Nuitka-Commander.html -F ${{ github.workspace }}/.github/workflows/scripts/CHANGELOG.md --latest -t $VERSION - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: 重置更新日志并更新语言支持列表 - run: | - cd ${{ github.workspace }}/.github/workflows/scripts - node ${{ github.workspace }}/.github/workflows/scripts/update_language_supports_list.js - node ${{ github.workspace }}/.github/workflows/scripts/reset_changelog.js - cd ${{ github.workspace }} - git config --global user.name "github action" - git config --global user.email "action_bot@github.com" - git add ./docs/CHANGELOG.md - git commit -m "Reset changelog & update language support list" - git push https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/Nuitka-Commander/nuitka-commander.github.io.git + - name: 删除无用文件夹 + run: | + cd ${{ github.workspace }} + rm -rf ./dist + rm -rf ./nuitka-commander + ls + # 这可以做一些其他特殊的所需处理 + - name: 为本地运行构建 + run: | + pnpm run build:local_use + mv ${{ github.workspace }}/dist/index.html ${{ github.workspace }}/dist/Nuitka-Commander.html + - name: 读取版本号 + id: read_version #此处刚需存储在scripts中的version文件 + run: | + echo "VERSION=$(cat ${{ github.workspace }}/.github/workflows/scripts/version)" >> $GITHUB_ENV + - name: 为Github发布release + run: | # 此处刚需存储在scripts中的CHANGELOG.md文件 + gh release create $VERSION ${{ github.workspace }}/dist/Nuitka-Commander.html -F ${{ github.workspace }}/.github/workflows/scripts/CHANGELOG.md --latest -t $VERSION + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: 重置更新日志并更新语言支持列表 + run: | + cd ${{ github.workspace }}/.github/workflows/scripts + node ${{ github.workspace }}/.github/workflows/scripts/update_language_supports_list.js + node ${{ github.workspace }}/.github/workflows/scripts/reset_changelog.js + cd ${{ github.workspace }} + git config --global user.name "github action" + git config --global user.email "action_bot@github.com" + git add ./docs/CHANGELOG.md + git commit -m "Reset changelog & update language support list" + git push https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/Nuitka-Commander/nuitka-commander.github.io.git diff --git a/.github/workflows/build_beta.yml b/.github/workflows/build_beta.yml index 1434d27..d4e7267 100644 --- a/.github/workflows/build_beta.yml +++ b/.github/workflows/build_beta.yml @@ -1,38 +1,61 @@ name: 构建测试版本 on: - workflow_dispatch: + workflow_dispatch: + inputs: + build_type: + description: "构建类型" + required: true + default: "local_use" + type: choice + options: + - local_use + - website + - website_seo permissions: write-all jobs: - publish: - name: 构建测试版本 - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - uses: pnpm/action-setup@v4 - name: Install pnpm - with: - version: 10 - run_install: false - - - name: Install Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: 'pnpm' - - - name: Install dependencies - run: pnpm install - - - name: 安装依赖 - if: steps.pnpm-cache.outputs.cache-hit != 'true' - run: pnpm install - - name: 为本地运行构建 - run: | - pnpm run build:local_use - - name: 上传 index.html - uses: actions/upload-artifact@v4 - with: - name: local_use index.html - path: ${{ github.workspace }}/dist/index.html + publish: + name: 构建测试版本 + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + + - uses: pnpm/action-setup@v4 + name: Install pnpm + with: + version: 10 + run_install: false + + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: "pnpm" + - name: Install dependencies + run: pnpm install + + - name: 安装 Playwright 浏览器 (仅 SEO 构建需要) + if: github.event.inputs.build_type == 'website_seo' + run: npx playwright install chromium + + - name: 为本地运行构建 + if: github.event.inputs.build_type == 'local_use' + run: | + pnpm run build:local_use + + - name: 为网站构建 + if: github.event.inputs.build_type == 'website' + run: | + pnpm run build:website + + - name: 为网站SEO构建 + if: github.event.inputs.build_type == 'website_seo' + run: | + pnpm run build:website:seo + + - name: 上传构建文件 + uses: actions/upload-artifact@v4 + with: + name: ${{ github.event.inputs.build_type }}-build + path: ${{ github.workspace }}/dist/ diff --git a/README.md b/README.md index c5aa868..bcb7505 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ ### 通过网页使用 最方便的方式是直接使用在线的网页,这样您同时可以获得最新的版本支持和语言支持。您可以从以下网址访问: -[在Github上使用Nuitka Commander](https://nuitka-commander.github.io) +[在网页上使用Nuitka Commander](https://nuitka-commander.erduotong.com) ### 下载到本地使用 diff --git a/docs/SEO_BUILD_GUIDE.md b/docs/SEO_BUILD_GUIDE.md new file mode 100644 index 0000000..a4f064f --- /dev/null +++ b/docs/SEO_BUILD_GUIDE.md @@ -0,0 +1,152 @@ +# SEO 构建指南 + +本项目现已支持 SEO 优化构建,专为**静态部署环境**(如 Cloudflare Pages)设计,能够预渲染静态页面以提高搜索引擎可见性。 + +## 功能特性 + +- **预渲染静态页面**: 使用 Playwright 预渲染所有语言和版本组合的页面(约 96 个页面) +- **客户端爬虫检测**: 在每个静态页面中注入 JavaScript 代码,智能检测访问者类型 +- **扁平化结构**: 静态页面使用扁平化文件名格式(如 `en_2_1___en.html`) +- **并发渲染**: 基于系统资源动态调整并发数,提高构建效率 +- **静态部署友好**: 无需服务器环境,完全适用于 Cloudflare Pages 等静态部署 + +## 工作原理 + +### 客户端智能检测 + +每个预渲染的静态页面都包含智能检测脚本: + +- **爬虫访问** → 继续显示静态页面内容(SEO 友好) +- **用户访问** → 自动重定向到 SPA 应用(用户体验友好) + +### 检测机制 + +1. **User-Agent 检测**: 识别 Google、Bing、百度等搜索引擎爬虫 +2. **无头浏览器检测**: 识别 Puppeteer、Playwright 等自动化工具 +3. **JavaScript 能力检测**: 检测现代 JavaScript 特性支持情况 +4. **综合判断**: 多重检测确保准确识别访问者类型 + +## 本地使用 + +### 1. 安装依赖 + +```bash +pnpm install +npx playwright install chromium +``` + +### 2. SEO 构建 + +```bash +# 启用 SEO 预渲染构建 +pnpm run build:website:seo + +# 或者只进行预渲染(不启动预览服务器) +pnpm run prerender +``` + +### 3. 本地测试 + +```bash +# 完整SEO功能测试 +pnpm run test:seo + +# 或者分步测试 +# 1. SEO构建 +pnpm run build:website:seo + +# 2. 启动测试服务器 +pnpm run serve + +# 3. 手动测试 +# 浏览器访问: http://localhost:3000 +# curl模拟爬虫: curl -H "User-Agent: Googlebot" http://localhost:3000/en/simple/en +``` + +### 4. 部署到 Cloudflare Pages + +1. 将 `dist` 目录的所有文件上传到 Cloudflare Pages +2. 静态页面会自动处理爬虫和用户访问 +3. 无需额外配置 + +## GitHub Actions 使用 + +### 测试版本构建 (build_beta.yml) + +在 GitHub Actions 中手动触发构建时,可以选择构建类型: + +- **local_use**: 本地使用版本 +- **website**: 标准网站版本 +- **website_seo**: SEO 优化版本(包含预渲染静态页面) + +### 发布版本构建 (build_and_publish.yml) + +在发布新版本时,可以选择是否启用 SEO 优化: + +- 勾选 "启用 SEO 优化构建" 复选框即可启用预渲染功能 +- 不勾选则使用标准构建流程 + +## 技术细节 + +### 预渲染配置 + +- 配置文件: `prerender.config.js` +- 插件文件: `plugins/vite-prerender.js` +- 页面组合: UI 语言 × Nuitka 版本 × 命令语言 + +### 爬虫检测 + +- 检测方式: **客户端 JavaScript 检测**(适用于静态部署) +- 支持的爬虫: Google、Bing、百度、搜狗等主流搜索引擎 +- 静态页面路径: `/static/` 目录下的扁平化 HTML 文件 +- 重定向逻辑: 普通用户自动重定向到根路径 SPA 应用 + +### 并发控制 + +- 自动计算最佳并发数(基于 CPU 核心数和内存大小) +- 默认最大并发数: 8 +- 内存充足时可达到更高并发数 + +## 注意事项 + +1. **构建时间**: SEO 构建比标准构建耗时更长(约需要额外 3-5 分钟) +2. **资源消耗**: Playwright 需要更多内存和 CPU 资源 +3. **浏览器依赖**: 需要安装 Chromium 浏览器(在 CI 中自动安装) +4. **文件大小**: 预渲染版本包含更多静态文件,整体体积更大 + +## 故障排除 + +### 常见问题 + +1. **Playwright 浏览器安装失败** + + ```bash + npx playwright install chromium --force + ``` + +2. **内存不足错误** + + - 减少并发数量(修改 `vite-prerender.js` 中的并发计算逻辑) + - 增加系统可用内存 + +3. **预渲染超时** + - 检查页面是否正确设置了 `data-prerender-ready="true"` + - 增加超时时间(在 `vite-prerender.js` 中修改) + +### 调试模式 + +启用调试模式查看详细日志: + +```bash +DEBUG=1 pnpm run build:website:seo +``` + +## 相关文件 + +- `prerender.config.js` - 预渲染页面配置 +- `plugins/vite-prerender.js` - Vite 预渲染插件 +- `server/crawler-detection.js` - 爬虫检测服务器 +- `src/App.vue` - 预渲染就绪标记 +- `vite.config.js` - Vite 配置集成 +- `.github/workflows/build_beta.yml` - 测试构建工作流 +- `.github/workflows/build_and_publish.yml` - 发布构建工作流 diff --git a/index.html b/index.html index 784cc04..c3ff45a 100644 --- a/index.html +++ b/index.html @@ -1,24 +1,29 @@ + - - - - - - Nuitka Commander + + + + + + - + -
- +
+ - + + \ No newline at end of file diff --git a/package.json b/package.json index 3227181..f2a47c2 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,9 @@ "scripts": { "dev": "vite --port 3002", "build:website": "vite build --mode website", + "build:website:seo": "cross-env ENABLE_PRERENDER=true vite build --mode website", "build:local_use": "vite build --mode local_use", - "preview": "vite preview" + "test:seo": "pwsh scripts/test-seo.ps1" }, "dependencies": { "@element-plus/icons-vue": "^2.3.1", @@ -15,7 +16,10 @@ "@voerkai18n/runtime": "^2.1.9", "@vueuse/core": "^10.9.0", "axios": "^1.8.2", + "cross-env": "^7.0.3", "element-plus": "^2.7.3", + "express": "^4.21.2", + "fs-extra": "^11.3.0", "fuse.js": "^7.0.0", "html-minifier-terser": "^7.2.0", "js-yaml": "^4.1.0", @@ -23,6 +27,7 @@ "localforage": "^1.10.0", "mousetrap": "^1.6.5", "node-fetch": "^3.3.2", + "playwright": "^1.52.0", "punycode": "^2.3.1", "sass": "^1.77.2", "scss-loader": "^0.0.1", @@ -39,4 +44,4 @@ "resolutions": { "bin-wrapper": "npm:bin-wrapper-china" } -} +} \ No newline at end of file diff --git a/plugins/vite-prerender.js b/plugins/vite-prerender.js new file mode 100644 index 0000000..c8062bc --- /dev/null +++ b/plugins/vite-prerender.js @@ -0,0 +1,368 @@ +/** + * @Description Vite Playwright 预渲染插件 + * @Author: GitHub Copilot + * @Date: 2024-12-19 + */ + +import { chromium } from 'playwright' +import fs from 'fs-extra' +import path from 'path' +import { fileURLToPath } from 'url' +import os from 'os' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) + +/** + * 计算最佳并发数量 + */ +function calculateOptimalConcurrency() { + const totalMemoryGB = os.totalmem() / (1024 * 1024 * 1024) + const cpuCores = os.cpus().length + + // 基于内存和CPU核心数计算并发数 + // 每个浏览器实例大约需要200-300MB内存 + const memoryBasedConcurrency = Math.floor(totalMemoryGB / 0.3) + const cpuBasedConcurrency = Math.max(2, Math.floor(cpuCores / 2)) + + // 取较小值,但至少为2,最多为8 + const concurrency = Math.min(Math.max(2, Math.min(memoryBasedConcurrency, cpuBasedConcurrency)), 8) + + console.log(`💻 系统信息: ${totalMemoryGB.toFixed(1)}GB 内存, ${cpuCores} CPU核心`) + console.log(`🚀 计算出的最佳并发数: ${concurrency}`) + + return concurrency +} + +/** + * Vite Playwright 预渲染插件 + */ +export default function vitePrerender(options = {}) { + let config + + const pluginInstance = { + name: 'vite-prerender', + configResolved(resolvedConfig) { + config = resolvedConfig + }, + + async closeBundle() { + // 只在生产构建时执行预渲染 + if (config.command !== 'build' || config.mode === 'development') { + return + } + + console.log('\n🎭 开始预渲染静态页面...') + + try { + await pluginInstance.prerender(options) + console.log('✅ 预渲染完成!') + } catch (error) { + console.error('❌ 预渲染失败:', error) + throw error + } + }, async prerender(options) { + const { + pages = [], + playwrightOptions = {}, + renderOptions = {}, + injectScript = '' + } = options + + if (pages.length === 0) { + console.log('⚠️ 没有配置预渲染页面') + return + } + + console.log(`📋 将渲染 ${pages.length} 个页面`) + + // 启动临时预览服务器 + let previewServer + try { + const { preview } = await import('vite') + previewServer = await preview({ + root: config.root, + build: { + outDir: config.build.outDir + }, + preview: { + port: 4173, + host: 'localhost' + } + }) + + // 等待服务器启动 + await new Promise(resolve => setTimeout(resolve, 2000)) + console.log('🚀 预览服务器已启动: http://localhost:4173/') + + // 计算最佳并发数量 + const optimalConcurrency = calculateOptimalConcurrency() + const concurrency = renderOptions.concurrency || optimalConcurrency + + console.log(`⚡ 使用并发数: ${concurrency}`) + + // 启动 Playwright 浏览器实例池 + const browsers = [] + for (let i = 0; i < concurrency; i++) { + const browser = await chromium.launch({ + headless: true, + ...playwrightOptions + }) + browsers.push(browser) + } + + try { + // 确保输出目录存在 + await fs.ensureDir(renderOptions.outputDir || 'dist/static') + // 分批并发渲染页面 + const chunks = [] + for (let i = 0; i < pages.length; i += concurrency) { + chunks.push(pages.slice(i, i + concurrency)) + } + + let processedCount = 0 + const startTime = Date.now() + + for (const chunk of chunks) { + await Promise.all( + chunk.map(async (pageConfig, index) => { + const browserIndex = index % browsers.length + await pluginInstance.renderPage(browsers[browserIndex], pageConfig, renderOptions, injectScript) + processedCount++ + const elapsed = ((Date.now() - startTime) / 1000).toFixed(1) + console.log(`📄 已渲染: ${processedCount}/${pages.length} - ${pageConfig.path} (${elapsed}s)`) + }) + ) + } + const totalTime = ((Date.now() - startTime) / 1000).toFixed(1) + console.log(`🎉 渲染完成! 总耗时: ${totalTime}s, 平均: ${(totalTime / pages.length).toFixed(1)}s/页`) + // 生成 sitemap 和 robots.txt - 生成在根目录而不是静态文件目录 + const outputDir = renderOptions.outputDir || 'dist/static' + const rootDir = path.dirname(outputDir); // 获取根目录 (dist/) + await pluginInstance.generateSitemap(pages, rootDir, renderOptions.baseUrl); + await pluginInstance.generateRobotsTxt(rootDir, renderOptions.baseUrl); + + } finally { + // 关闭所有浏览器实例 + await Promise.all(browsers.map(browser => browser.close())) + } + + } finally { + // 关闭预览服务器 + if (previewServer) { + await previewServer.close() + console.log('🛑 预览服务器已关闭') + } + } }, + async injectCrawlerDetectionScript(html) { + try { + // 读取客户端检测脚本 + const scriptPath = path.join(process.cwd(), 'scripts', 'client-crawler-detection.js') + const scriptContent = await fs.readFile(scriptPath, 'utf-8') + + // 将脚本包装为内联脚本标签 + const scriptTag = `` + + // 在 标签前插入脚本 + if (html.includes('')) { + return html.replace('', `${scriptTag}\n`) + } else { + // 如果没有 标签,在 后插入 + return html.replace('', `\n${scriptTag}`) + } + } catch (error) { + console.warn('⚠️ 注入客户端检测脚本失败:', error.message) + return html + } + }, + async generateSitemap(pages, outputDir, baseUrl = "https://your-domain.com") { + console.log('📋 生成sitemap.xml...') + + // 添加根目录作为主页,具有最高优先级 + const rootEntry = ` + ${baseUrl}/ + ${new Date().toISOString().split("T")[0]} + daily + 1.0 + `; + + const sitemap = ` + +${rootEntry} +${pages.map(page => { + const url = baseUrl + page.path + return ` + ${url} + ${new Date().toISOString().split('T')[0]} + weekly + 0.8 + ` +}).join('\n')} +` + + const sitemapPath = path.join(outputDir, 'sitemap.xml') + await fs.writeFile(sitemapPath, sitemap, 'utf-8') + console.log(`✅ Sitemap 已生成: ${sitemapPath}`) + }, + + async generateRobotsTxt(outputDir, baseUrl = 'https://your-domain.com') { + console.log('🤖 生成robots.txt...') + + const robots = `User-agent: * +Allow: / + +Sitemap: ${baseUrl}/sitemap.xml` + + const robotsPath = path.join(outputDir, 'robots.txt') + await fs.writeFile(robotsPath, robots, 'utf-8') + console.log(`✅ Robots.txt 已生成: ${robotsPath}`) + }, + + async renderPage(browser, pageConfig, renderOptions, injectScript) { + const page = await browser.newPage() + + try { + // 设置视窗大小 + if (renderOptions.viewport) { + await page.setViewportSize(renderOptions.viewport) + } + + // 注入脚本 + if (injectScript) { + await page.addInitScript(injectScript) + } + + // 设置 localStorage + if (pageConfig.localStorage) { + await page.addInitScript((storageData) => { + Object.entries(storageData).forEach(([key, value]) => { + localStorage.setItem(key, typeof value === 'string' ? value : JSON.stringify(value)) + }) + }, pageConfig.localStorage) } + + // 获取本地服务器地址 + const baseUrl = 'http://localhost:4173' + + // 导航到页面 + await page.goto(baseUrl, { + waitUntil: 'networkidle', + timeout: 30000 + }) // 等待页面渲染完成 + if (renderOptions.waitForSelector) { + try { + await page.waitForSelector(renderOptions.waitForSelector, { + timeout: renderOptions.waitTime || 10000 + }) + } catch (error) { + console.warn(`⚠️ 等待选择器超时: ${renderOptions.waitForSelector}`) + } + } else { + // 等待预渲染准备标记 + try { + console.log(`⏳ 等待页面预渲染准备... (${pageConfig.path})`) + + // 等待DOM加载 + await page.waitForLoadState('domcontentloaded') + + // 等待预渲染准备标记 + await page.waitForFunction(() => { + return document.body.getAttribute('data-prerender-ready') === 'true' + }, { + timeout: renderOptions.waitTime || 15000 + }) + + console.log(`✅ 页面预渲染准备就绪 (${pageConfig.path})`) + } catch (error) { + console.warn(`⚠️ 等待预渲染准备标记超时 (${pageConfig.path}),使用固定等待时间`) + + // 输出调试信息 + try { + const readyAttribute = await page.evaluate(() => { + return document.body.getAttribute('data-prerender-ready') + }) + console.log(`📝 当前预渲染标记值: "${readyAttribute}"`) + + // 检查控制台日志 + const logs = await page.evaluate(() => { + return Array.from(document.querySelectorAll('*')).some(el => + el.getAttribute('data-prerender-ready') === 'true' + ) + }) + console.log(`🔍 是否找到预渲染标记: ${logs}`) + } catch (debugError) { + console.log('调试信息获取失败:', debugError.message) + } + + await page.waitForTimeout(3000) // 减少回退等待时间 + } + } + + // 修改页面元数据 + if (pageConfig.meta) { + await page.evaluate((meta) => { + // 更新标题 + if (meta.title) { + document.title = meta.title + } + + // 更新或添加 meta 标签 + const updateMeta = (name, content, property = false) => { + if (!content) return + + const selector = property ? `meta[property="${name}"]` : `meta[name="${name}"]` + let element = document.querySelector(selector) + + if (!element) { + element = document.createElement('meta') + if (property) { + element.setAttribute('property', name) + } else { + element.setAttribute('name', name) + } + document.head.appendChild(element) + } + + element.setAttribute('content', content) + } + + updateMeta('description', meta.description) + updateMeta('keywords', meta.keywords) + updateMeta('language', meta.language) + + // Open Graph 标签 + updateMeta('og:title', meta.title, true) + updateMeta('og:description', meta.description, true) + updateMeta('og:type', 'website', true) + + // 添加 canonical URL + let canonical = document.querySelector('link[rel="canonical"]') + if (!canonical) { + canonical = document.createElement('link') + canonical.setAttribute('rel', 'canonical') + document.head.appendChild(canonical) + } + canonical.setAttribute('href', window.location.href) + + }, pageConfig.meta) + } // 获取渲染后的HTML + const html = await page.content() + + // 注入客户端爬虫检测脚本 + const injectedHtml = await this.injectCrawlerDetectionScript(html) + + // 保存到文件 + const outputDir = renderOptions.outputDir || 'dist/static' + // 扁平化结构:直接使用页面路径作为文件名 + const fileName = pageConfig.path.replace(/\//g, '_').replace(/^_+|_+$/g, '') || 'index' + const filePath = path.join(outputDir, `${fileName}.html`) + await fs.ensureDir(path.dirname(filePath)) + await fs.writeFile(filePath, injectedHtml, 'utf-8') + + } finally { + await page.close() + } + } + } + + return pluginInstance +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f5fa33e..425bcee 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,9 +26,18 @@ importers: axios: specifier: ^1.8.2 version: 1.8.4 + cross-env: + specifier: ^7.0.3 + version: 7.0.3 element-plus: specifier: ^2.7.3 version: 2.8.0(vue@3.4.38) + express: + specifier: ^4.21.2 + version: 4.21.2 + fs-extra: + specifier: ^11.3.0 + version: 11.3.0 fuse.js: specifier: ^7.0.0 version: 7.0.0 @@ -50,6 +59,9 @@ importers: node-fetch: specifier: ^3.3.2 version: 3.3.2 + playwright: + specifier: ^1.52.0 + version: 1.52.0 punycode: specifier: ^2.3.1 version: 2.3.1 @@ -104,8 +116,8 @@ packages: resolution: {integrity: sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@7.25.9': - resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} engines: {node: '>=6.9.0'} '@babel/parser@7.25.3': @@ -597,6 +609,10 @@ packages: '@vueuse/shared@9.13.0': resolution: {integrity: sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==} + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + acorn@5.7.4: resolution: {integrity: sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==} engines: {node: '>=0.4.0'} @@ -653,6 +669,9 @@ packages: resolution: {integrity: sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==} engines: {node: '>=0.10.0'} + array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + array-union@2.1.0: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} @@ -710,6 +729,10 @@ packages: bl@1.2.3: resolution: {integrity: sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==} + body-parser@1.20.3: + resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} @@ -741,9 +764,21 @@ packages: buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + cacheable-request@2.1.4: resolution: {integrity: sha512-vag0O2LKZ/najSoUwDbVlnlCFvhBE/7mGTY2B5FgCBDcRD+oVV1HYTOwM6JZfMg/hIcM6IwnTZ1uQQL5/X3xIQ==} + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + camel-case@3.0.0: resolution: {integrity: sha512-+MbKztAYHXPr1jNTSKQF52VpcFjwY5RkR7fxksV8Doo4KAYc5Fl4UJRgthBbTmEx8C54DqahhbLJkDwjI3PI/w==} @@ -843,9 +878,25 @@ packages: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + + cookie@0.7.1: + resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} + engines: {node: '>= 0.6'} + core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + cross-env@7.0.3: + resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} + engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} + hasBin: true + cross-spawn@5.1.0: resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==} @@ -853,10 +904,6 @@ packages: resolution: {integrity: sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==} engines: {node: '>=4.8'} - cross-spawn@7.0.3: - resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} - engines: {node: '>= 8'} - cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -903,6 +950,14 @@ packages: dayjs@1.11.12: resolution: {integrity: sha512-Rt2g+nTbLlDWZTwwrIXjy9MeiZmSDI375FvZs72ngxx8PDC6YXOeR3q5LAuPzjZQxhiWdRKac7RKV+YyQYfYIg==} + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + debug@4.3.6: resolution: {integrity: sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==} engines: {node: '>=6.0'} @@ -954,6 +1009,14 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -990,12 +1053,19 @@ packages: resolution: {integrity: sha512-xqnBTVd/E+GxJVrX5/eUJiLYjCGPwMpdL+jGhGU57BvtcA7wwhtHVbXBeUk51kOpW3S7Jn3BQbN9Q1R1Km2qDQ==} engines: {node: '>=6'} + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + duplexer3@0.1.5: resolution: {integrity: sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==} eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + ejs@3.1.10: resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} engines: {node: '>=0.10.0'} @@ -1012,6 +1082,14 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + end-of-stream@1.4.4: resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} @@ -1025,6 +1103,18 @@ packages: error-ex@1.3.2: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + esbuild-android-64@0.14.54: resolution: {integrity: sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ==} engines: {node: '>=12'} @@ -1190,6 +1280,10 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + exec-buffer@3.2.0: resolution: {integrity: sha512-wsiD+2Tp6BWHoVv3B+5Dcx6E7u5zky+hUwOHjuH2hKSLR3dvRmX8fk8UD8uqQixHs4Wk6eDmiegVrMPjKj7wpA==} engines: {node: '>=4'} @@ -1214,6 +1308,10 @@ packages: resolution: {integrity: sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==} engines: {node: '>=4'} + express@4.21.2: + resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} + engines: {node: '>= 0.10.0'} + ext-list@2.2.2: resolution: {integrity: sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==} engines: {node: '>=0.10.0'} @@ -1298,6 +1396,10 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + finalhandler@1.3.1: + resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} + engines: {node: '>= 0.8'} + find-up@1.1.2: resolution: {integrity: sha512-jvElSjyuo4EMQGoTwo1uJU5pQMwTW5lS1x05zzfJuTIyLR3zwO27LYrxNg+dlvKpGOuGy/MzBdXh80g0ve5+HA==} engines: {node: '>=0.10.0'} @@ -1330,6 +1432,14 @@ packages: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + from2@2.3.0: resolution: {integrity: sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==} @@ -1340,9 +1450,18 @@ packages: resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} engines: {node: '>=12'} + fs-extra@11.3.0: + resolution: {integrity: sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==} + engines: {node: '>=14.14'} + fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1355,6 +1474,14 @@ packages: resolution: {integrity: sha512-14F4hBIxqKvD4Zz/XjDc3y94mNZN6pRv3U13Udo0lNLCWRBUsrMv2xwcF/y/Z5sV6+FQW+/ow68cHpm4sunt8Q==} engines: {node: '>=10'} + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + get-proxy@2.1.0: resolution: {integrity: sha512-zmZIaQTWnNQb4R4fJUEp/FC51eZsc6EkErspy3xtIYStaq8EB/hDIWipxsal+E8rz0qD7f2sL/NA9Xee4RInJw==} engines: {node: '>=4'} @@ -1404,6 +1531,10 @@ packages: resolution: {integrity: sha512-7dUi7RvCoT/xast/o/dLN53oqND4yk0nsHkhRgn9w65C4PofCLOoJ39iSOg+qVDdWQPIEj+eszMHQ+aLVwwQSg==} engines: {node: '>=8'} + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + got@7.1.0: resolution: {integrity: sha512-Y5WMo7xKKq1muPsxD+KmrR8DH5auG7fBdDVueZwETwV6VytKyU9OX/ddpq2/1hp1vIPvVb4T81dKQz3BivkNLw==} engines: {node: '>=4'} @@ -1426,6 +1557,10 @@ packages: has-symbol-support-x@1.4.2: resolution: {integrity: sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw==} + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + has-to-string-tag-x@1.4.1: resolution: {integrity: sha512-vdbKfmw+3LoOYVr+mtxHaX5a96+0f3DljYd8JOqvOLsf5mw2Otda2qCDT9qRqLAhrjyQ0h7ual5nOiASpsGNFw==} @@ -1462,6 +1597,10 @@ packages: http-cache-semantics@3.8.1: resolution: {integrity: sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w==} + http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} @@ -1478,6 +1617,10 @@ packages: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + iconv-lite@0.6.3: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} @@ -1549,6 +1692,10 @@ packages: resolution: {integrity: sha512-TcdjPibTksa1NQximqep2r17ISRiNE9fwlfbg3F8ANdvP5/yrFTew86VcO//jk4QTaMlbjypPBq76HN2zaKfZQ==} engines: {node: '>=4'} + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} @@ -1778,9 +1925,17 @@ packages: resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==} engines: {node: '>=0.10.0'} + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + mdn-data@2.0.14: resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==} + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + memoize-one@6.0.0: resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} @@ -1788,6 +1943,9 @@ packages: resolution: {integrity: sha512-TNdwZs0skRlpPpCUK25StC4VH+tP5GgeY1HQOOGP+lQ2xtdkN2VtT/5tiX9k3IWpkBPV9b3LsAWXn4GGi/PrSA==} engines: {node: '>=0.10.0'} + merge-descriptors@1.0.3: + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + merge-source-map@1.1.0: resolution: {integrity: sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==} @@ -1798,6 +1956,10 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + micromatch@4.0.7: resolution: {integrity: sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==} engines: {node: '>=8.6'} @@ -1814,6 +1976,11 @@ packages: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} @@ -1851,9 +2018,15 @@ packages: engines: {node: '>=10'} hasBin: true + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -1864,6 +2037,10 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + nice-try@1.0.5: resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} @@ -1920,6 +2097,14 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -2004,6 +2189,10 @@ packages: parse5@7.1.2: resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + pascal-case@3.1.2: resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} @@ -2030,6 +2219,9 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} + path-to-regexp@0.1.12: + resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} + path-type@1.1.0: resolution: {integrity: sha512-S4eENJz1pkiQn9Znv33Q+deTOKmbl+jj1Fl+qiP/vYezj+S8x+J3Uo0ISrx/QoEvIlOaDWJhPaRd1flJ9HXZqg==} engines: {node: '>=0.10.0'} @@ -2081,6 +2273,16 @@ packages: pkg-types@1.1.3: resolution: {integrity: sha512-+JrgthZG6m3ckicaOB74TwQ+tBWsFl3qVQg7mN8ulwSOElJ7gBhKzj2VkCPnZ4NlF6kEquYU+RIYNVAvzd54UA==} + playwright-core@1.52.0: + resolution: {integrity: sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.52.0: + resolution: {integrity: sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==} + engines: {node: '>=18'} + hasBin: true + pngquant-bin@6.0.1: resolution: {integrity: sha512-Q3PUyolfktf+hYio6wsg3SanQzEU/v8aICg/WpzxXcuCMRb7H2Q81okfpcEztbMvw25ILjd3a87doj2N9kvbpQ==} engines: {node: '>=10'} @@ -2112,6 +2314,10 @@ packages: proto-list@1.2.4: resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} @@ -2128,6 +2334,10 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + qs@6.13.0: + resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} + engines: {node: '>=0.6'} + query-string@5.1.1: resolution: {integrity: sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==} engines: {node: '>=0.10.0'} @@ -2138,6 +2348,14 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@2.5.2: + resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} + engines: {node: '>= 0.8'} + read-pkg-up@1.0.1: resolution: {integrity: sha512-WD9MTlNtI55IwYUS27iHh9tK3YoIVhxis8yKhLpTqWtml739uXc9NWTpxoHkfZf3+DkCCsXox94/VWZniuZm6A==} engines: {node: '>=0.10.0'} @@ -2246,6 +2464,17 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true + send@0.19.0: + resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} + engines: {node: '>= 0.8.0'} + + serve-static@1.16.2: + resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} + engines: {node: '>= 0.8.0'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + shebang-command@1.2.0: resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==} engines: {node: '>=0.10.0'} @@ -2265,6 +2494,22 @@ packages: shell-quote@1.8.1: resolution: {integrity: sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==} + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -2327,6 +2572,10 @@ packages: resolution: {integrity: sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==} deprecated: 'Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility' + statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + strict-uri-encode@1.1.0: resolution: {integrity: sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ==} engines: {node: '>=0.10.0'} @@ -2443,6 +2692,10 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + tough-cookie@4.1.4: resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} engines: {node: '>=6'} @@ -2473,6 +2726,10 @@ packages: resolution: {integrity: sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==} engines: {node: '>=8'} + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + ufo@1.5.4: resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==} @@ -2501,6 +2758,10 @@ packages: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + unplugin-auto-import@0.17.8: resolution: {integrity: sha512-CHryj6HzJ+n4ASjzwHruD8arhbdl+UXvhuAIlHDs15Y/IMecG3wrf7FVg4pVH/DIysbq/n0phIjNHAjl7TG7Iw==} engines: {node: '>=14'} @@ -2551,6 +2812,10 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + uuid@3.4.0: resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details. @@ -2559,6 +2824,10 @@ packages: validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + vite-plugin-html@3.2.2: resolution: {integrity: sha512-vb9C9kcdzcIo/Oc3CLZVS03dL5pDlOFuhGlZYDCJ840BhWl/0nGeZWf3Qy7NlOayscY4Cm/QRgULCQkEZige5Q==} peerDependencies: @@ -2736,7 +3005,7 @@ snapshots: '@babel/helper-validator-identifier@7.24.7': {} - '@babel/helper-validator-identifier@7.25.9': + '@babel/helper-validator-identifier@7.27.1': optional: true '@babel/parser@7.25.3': @@ -2757,7 +3026,7 @@ snapshots: '@babel/types@7.26.7': dependencies: '@babel/helper-string-parser': 7.25.9 - '@babel/helper-validator-identifier': 7.25.9 + '@babel/helper-validator-identifier': 7.27.1 optional: true '@ctrl/tinycolor@3.6.1': {} @@ -3165,6 +3434,11 @@ snapshots: - '@vue/composition-api' - vue + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + acorn@5.7.4: {} acorn@8.12.1: {} @@ -3204,6 +3478,8 @@ snapshots: array-find-index@1.0.2: {} + array-flatten@1.1.1: {} + array-union@2.1.0: {} art-template@4.13.2: @@ -3278,6 +3554,23 @@ snapshots: readable-stream: 2.3.8 safe-buffer: 5.2.1 + body-parser@1.20.3: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.13.0 + raw-body: 2.5.2 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + boolbase@1.0.0: {} brace-expansion@1.1.11: @@ -3311,6 +3604,8 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 + bytes@3.1.2: {} + cacheable-request@2.1.4: dependencies: clone-response: 1.0.2 @@ -3321,6 +3616,16 @@ snapshots: normalize-url: 2.0.1 responselike: 1.0.2 + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + camel-case@3.0.0: dependencies: no-case: 2.3.2 @@ -3425,8 +3730,18 @@ snapshots: dependencies: safe-buffer: 5.2.1 + content-type@1.0.5: {} + + cookie-signature@1.0.6: {} + + cookie@0.7.1: {} + core-util-is@1.0.3: {} + cross-env@7.0.3: + dependencies: + cross-spawn: 7.0.6 + cross-spawn@5.1.0: dependencies: lru-cache: 4.1.5 @@ -3441,12 +3756,6 @@ snapshots: shebang-command: 1.2.0 which: 1.3.1 - cross-spawn@7.0.3: - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -3496,6 +3805,10 @@ snapshots: dayjs@1.11.12: {} + debug@2.6.9: + dependencies: + ms: 2.0.0 + debug@4.3.6: dependencies: ms: 2.1.2 @@ -3552,6 +3865,10 @@ snapshots: delayed-stream@1.0.0: {} + depd@2.0.0: {} + + destroy@1.2.0: {} + dir-glob@3.0.1: dependencies: path-type: 4.0.0 @@ -3612,10 +3929,18 @@ snapshots: p-event: 2.3.1 pify: 3.0.0 + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + duplexer3@0.1.5: {} eastasianwidth@0.2.0: {} + ee-first@1.1.1: {} + ejs@3.1.10: dependencies: jake: 10.9.2 @@ -3645,6 +3970,10 @@ snapshots: emoji-regex@9.2.2: {} + encodeurl@1.0.2: {} + + encodeurl@2.0.0: {} + end-of-stream@1.4.4: dependencies: once: 1.4.0 @@ -3657,6 +3986,14 @@ snapshots: dependencies: is-arrayish: 0.2.1 + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + esbuild-android-64@0.14.54: optional: true @@ -3796,6 +4133,8 @@ snapshots: esutils@2.0.3: {} + etag@1.8.1: {} + exec-buffer@3.2.0: dependencies: execa: 0.7.0 @@ -3838,7 +4177,7 @@ snapshots: execa@5.1.1: dependencies: - cross-spawn: 7.0.3 + cross-spawn: 7.0.6 get-stream: 6.0.1 human-signals: 2.1.0 is-stream: 2.0.1 @@ -3852,6 +4191,42 @@ snapshots: dependencies: pify: 2.3.0 + express@4.21.2: + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.3 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.7.1 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.3.1 + fresh: 0.5.2 + http-errors: 2.0.0 + merge-descriptors: 1.0.3 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.12 + proxy-addr: 2.0.7 + qs: 6.13.0 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.19.0 + serve-static: 1.16.2 + setprototypeof: 1.2.0 + statuses: 2.0.1 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + ext-list@2.2.2: dependencies: mime-db: 1.53.0 @@ -3927,6 +4302,18 @@ snapshots: dependencies: to-regex-range: 5.0.1 + finalhandler@1.3.1: + dependencies: + debug: 2.6.9 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + find-up@1.1.2: dependencies: path-exists: 2.1.0 @@ -3958,6 +4345,10 @@ snapshots: dependencies: fetch-blob: 3.2.0 + forwarded@0.2.0: {} + + fresh@0.5.2: {} + from2@2.3.0: dependencies: inherits: 2.0.4 @@ -3971,8 +4362,17 @@ snapshots: jsonfile: 6.1.0 universalify: 2.0.1 + fs-extra@11.3.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + fs.realpath@1.0.0: {} + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -3980,6 +4380,24 @@ snapshots: fuse.js@7.0.0: {} + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + get-proxy@2.1.0: dependencies: npm-conf: 1.1.3 @@ -4043,6 +4461,8 @@ snapshots: merge2: 1.4.1 slash: 3.0.0 + gopd@1.2.0: {} + got@7.1.0: dependencies: '@types/keyv': 3.1.4 @@ -4094,6 +4514,8 @@ snapshots: has-symbol-support-x@1.4.2: {} + has-symbols@1.1.0: {} + has-to-string-tag-x@1.4.1: dependencies: has-symbol-support-x: 1.4.2 @@ -4142,6 +4564,14 @@ snapshots: http-cache-semantics@3.8.1: {} + http-errors@2.0.0: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.1 @@ -4160,6 +4590,10 @@ snapshots: human-signals@2.1.0: {} + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + iconv-lite@0.6.3: dependencies: safer-buffer: 2.1.2 @@ -4245,6 +4679,8 @@ snapshots: from2: 2.3.0 p-is-promise: 1.1.0 + ipaddr.js@1.9.1: {} + is-arrayish@0.2.1: {} is-binary-path@2.1.0: @@ -4472,8 +4908,12 @@ snapshots: map-obj@1.0.1: {} + math-intrinsics@1.1.0: {} + mdn-data@2.0.14: {} + media-typer@0.3.0: {} + memoize-one@6.0.0: {} meow@3.7.0: @@ -4489,6 +4929,8 @@ snapshots: redent: 1.0.0 trim-newlines: 1.0.0 + merge-descriptors@1.0.3: {} + merge-source-map@1.1.0: dependencies: source-map: 0.6.1 @@ -4497,6 +4939,8 @@ snapshots: merge2@1.4.1: {} + methods@1.1.2: {} + micromatch@4.0.7: dependencies: braces: 3.0.3 @@ -4510,6 +4954,8 @@ snapshots: dependencies: mime-db: 1.52.0 + mime@1.6.0: {} + mimic-fn@2.1.0: {} mimic-response@1.0.1: {} @@ -4544,12 +4990,18 @@ snapshots: bin-build: 3.0.0 bin-wrapper: bin-wrapper-china@0.1.0 + ms@2.0.0: {} + ms@2.1.2: {} + ms@2.1.3: {} + nanoid@3.3.11: {} nanoid@3.3.7: {} + negotiator@0.6.3: {} + nice-try@1.0.5: {} no-case@2.3.2: @@ -4612,6 +5064,12 @@ snapshots: object-assign@4.1.1: {} + object-inspect@1.13.4: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + once@1.4.0: dependencies: wrappy: 1.0.2 @@ -4693,6 +5151,8 @@ snapshots: dependencies: entities: 4.5.0 + parseurl@1.3.3: {} + pascal-case@3.1.2: dependencies: no-case: 3.0.4 @@ -4715,6 +5175,8 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 + path-to-regexp@0.1.12: {} + path-type@1.1.0: dependencies: graceful-fs: 4.2.11 @@ -4753,6 +5215,14 @@ snapshots: mlly: 1.7.1 pathe: 1.1.2 + playwright-core@1.52.0: {} + + playwright@1.52.0: + dependencies: + playwright-core: 1.52.0 + optionalDependencies: + fsevents: 2.3.2 + pngquant-bin@6.0.1: dependencies: bin-build: 3.0.0 @@ -4781,6 +5251,11 @@ snapshots: proto-list@1.2.4: {} + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + proxy-from-env@1.1.0: {} pseudomap@1.0.2: {} @@ -4794,6 +5269,10 @@ snapshots: punycode@2.3.1: {} + qs@6.13.0: + dependencies: + side-channel: 1.1.0 + query-string@5.1.1: dependencies: decode-uri-component: 0.2.2 @@ -4804,6 +5283,15 @@ snapshots: queue-microtask@1.2.3: {} + range-parser@1.2.1: {} + + raw-body@2.5.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + read-pkg-up@1.0.1: dependencies: find-up: 1.1.2 @@ -4928,6 +5416,35 @@ snapshots: semver@6.3.1: {} + send@0.19.0: + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + + serve-static@1.16.2: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.0 + transitivePeerDependencies: + - supports-color + + setprototypeof@1.2.0: {} + shebang-command@1.2.0: dependencies: shebang-regex: 1.0.0 @@ -4942,6 +5459,34 @@ snapshots: shell-quote@1.8.1: {} + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + signal-exit@3.0.7: {} signal-exit@4.1.0: {} @@ -4995,6 +5540,8 @@ snapshots: stable@0.1.8: {} + statuses@2.0.1: {} + strict-uri-encode@1.1.0: {} string-width@4.2.3: @@ -5112,6 +5659,8 @@ snapshots: dependencies: is-number: 7.0.0 + toidentifier@1.0.1: {} + tough-cookie@4.1.4: dependencies: psl: 1.9.0 @@ -5141,6 +5690,11 @@ snapshots: type-fest@0.11.0: {} + type-is@1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + ufo@1.5.4: {} uglify-js@3.4.10: @@ -5179,6 +5733,8 @@ snapshots: universalify@2.0.1: {} + unpipe@1.0.0: {} + unplugin-auto-import@0.17.8(@vueuse/core@10.11.1(vue@3.4.38))(rollup@4.41.1): dependencies: '@antfu/utils': 0.7.10 @@ -5239,6 +5795,8 @@ snapshots: util-deprecate@1.0.2: {} + utils-merge@1.0.1: {} + uuid@3.4.0: {} validate-npm-package-license@3.0.4: @@ -5246,6 +5804,8 @@ snapshots: spdx-correct: 3.2.0 spdx-expression-parse: 3.0.1 + vary@1.1.2: {} + vite-plugin-html@3.2.2(vite@6.3.5(@types/node@22.13.0)(sass@1.77.8)(terser@5.31.6)): dependencies: '@rollup/pluginutils': 4.2.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index efc037a..da2aac1 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,2 +1,3 @@ onlyBuiltDependencies: - esbuild + - puppeteer diff --git a/prerender.config.js b/prerender.config.js new file mode 100644 index 0000000..e4da38b --- /dev/null +++ b/prerender.config.js @@ -0,0 +1,187 @@ +/** + * @Description 预渲染配置 - 定义需要预渲染的页面组合 + * @Author: GitHub Copilot + * @Date: 2024-12-19 + */ + +import {supported_i18n} from "./src/assets/languages/supported_i18n.js"; +import supported_nuitka_version from './src/nuitka_config_files/supported_nuitka_version.js' + +/** + * 生成预渲染页面配置 + * 每个配置包含 UI语言、Nuitka版本、以及对应的localStorage设置 + */ +function generatePageConfigurations() { + const configurations = [] + // 获取支持的UI语言 - 只保留中文和英文 + const allUiLanguages = Object.keys(supported_i18n); + const uiLanguages = allUiLanguages.filter(lang => lang === "zh-CN" || lang === "en"); + + // 获取支持的Nuitka版本 - 只保留最新的3个版本加上"simple"版本 + const allVersions = Object.keys(supported_nuitka_version.versions); + const numericVersions = allVersions.filter(v => v !== "simple").sort((a, b) => { + // 提取版本号进行比较 (例如: "2.7.*" -> 2.7) + const versionA = parseFloat(a.replace(/[.*]/g, "")); + const versionB = parseFloat(b.replace(/[.*]/g, "")); + return versionB - versionA; // 降序排列,最新版本在前 + }); + // 取最新的3个版本,加上"simple"版本 + const nuitkaVersions = [ + ...numericVersions.slice(0, 3), // 最新的3个版本 + "simple", // 始终包含simple版本 + ]; + + console.log("选择的UI语言:", uiLanguages); + console.log("选择的Nuitka版本:", nuitkaVersions); + + // 为每种组合生成配置 + uiLanguages.forEach(uiLang => { + nuitkaVersions.forEach(nuitkaVersion => { + // 获取该Nuitka版本支持的命令语言 + const commandLanguages = getCommandLanguagesForVersion(nuitkaVersion) + + commandLanguages.forEach(cmdLang => { + const config = { // 页面标识 + id: `${uiLang}-${nuitkaVersion}-${cmdLang}`, + // URL路径(扁平化结构)- 使用简化的文件名 + path: `/${uiLang}-${nuitkaVersion.replace(/[.*]/g, '_')}-${cmdLang}`, + + // localStorage设置 + localStorage: { + 'vueuse-locale': uiLang, + 'selected_nuitka_version': nuitkaVersion, + 'command_language': cmdLang + }, + + // IndexDB设置(如果需要) + indexDB: { + // 可以根据需要添加indexDB的初始化数据 + }, + + // 页面元数据(用于SEO) + meta: { + title: generatePageTitle(uiLang, nuitkaVersion, cmdLang), + description: generatePageDescription(uiLang, nuitkaVersion, cmdLang), + keywords: generatePageKeywords(uiLang, nuitkaVersion, cmdLang), + language: uiLang + } + } + + configurations.push(config) + }) + }) + }) + + return configurations +} + +/** + * 获取指定Nuitka版本支持的命令语言 + */ +function getCommandLanguagesForVersion(nuitkaVersion) { + // 根据实际翻译文件结构确定支持的命令语言 + // 从搜索结果可以看出,支持的命令语言包括: + const supportedLanguages = ['en', 'zh_cn', 'de'] // 基于实际翻译文件 + + // 根据版本目录结构映射语言代码 + const languageMapping = { + 'zh_cn': 'zh-CN', + 'en': 'en', + 'de': 'de' + } + + // 返回映射后的语言代码 + return supportedLanguages.map(lang => languageMapping[lang] || lang) +} + +/** + * 生成页面标题 + */ +function generatePageTitle(uiLang, nuitkaVersion, cmdLang) { + const titles = { + 'en': `Nuitka Commander - ${nuitkaVersion} (${cmdLang})`, + 'zh-CN': `Nuitka Commander - ${nuitkaVersion} (${cmdLang})`, + 'de': `Nuitka Commander - ${nuitkaVersion} (${cmdLang})`, + 'fr': `Nuitka Commander - ${nuitkaVersion} (${cmdLang})` + } + return titles[uiLang] || titles['en'] +} + +/** + * 生成页面描述 + */ +function generatePageDescription(uiLang, nuitkaVersion, cmdLang) { + const descriptions = { + 'en': `Nuitka Commander GUI tool for version ${nuitkaVersion} with ${cmdLang} command interface. Easy-to-use Python compilation tool.`, + 'zh-CN': `Nuitka Commander GUI工具,支持${nuitkaVersion}版本,${cmdLang}命令界面。简单易用的Python编译工具。`, + 'de': `Nuitka Commander GUI-Tool für Version ${nuitkaVersion} mit ${cmdLang} Befehlsschnittstelle. Benutzerfreundliches Python-Kompilierungstool.`, + 'fr': `Outil GUI Nuitka Commander pour la version ${nuitkaVersion} avec interface de commande ${cmdLang}. Outil de compilation Python facile à utiliser.` + } + return descriptions[uiLang] || descriptions['en'] +} + +/** + * 生成页面关键词 + */ +function generatePageKeywords(uiLang, nuitkaVersion, cmdLang) { + const baseKeywords = [ + 'Nuitka', 'Python', 'Compiler', 'GUI', 'Commander', + nuitkaVersion, cmdLang, 'compilation', 'executable' + ] + + const langSpecificKeywords = { + 'zh-CN': ['Python编译器', '图形界面', '编译工具', '可执行文件'], + 'de': ['Python-Compiler', 'Grafische Benutzeroberfläche', 'Kompilierung'], + 'fr': ['Compilateur Python', 'Interface graphique', 'compilation'] + } + + return [...baseKeywords, ...(langSpecificKeywords[uiLang] || [])].join(', ') +} + +// 导出配置 +export const prerenderConfig = { + // 预渲染页面配置 + pages: generatePageConfigurations(), + + // Puppeteer选项 + puppeteerOptions: { + headless: true, + args: ['--no-sandbox', '--disable-setuid-sandbox'], + defaultViewport: { + width: 1920, + height: 1080 + } + }, + + // 渲染选项 + renderOptions: { + // 等待时间(毫秒) + waitTime: 10000, + + // 等待条件(可选) + waitUntil: 'networkidle2', + + // 需要等待的选择器(表示页面加载完成) + waitForSelector: '[data-prerender-ready]', + // 输出目录 + outputDir: 'dist/static', + + // 网站基础URL(用于sitemap) + baseUrl: 'https://nuitka-commander.erduotong.com' + }, + + // 需要注入的脚本(在页面加载前执行) + injectScript: ` + // 标记页面准备好进行预渲染 + window.addEventListener('load', () => { + setTimeout(() => { + const readyElement = document.createElement('div'); + readyElement.setAttribute('data-prerender-ready', 'true'); + readyElement.style.display = 'none'; + document.body.appendChild(readyElement); + }, 1000); + }); + ` +} + +export default prerenderConfig diff --git a/public/404.html b/public/404.html new file mode 100644 index 0000000..77ffe75 --- /dev/null +++ b/public/404.html @@ -0,0 +1,131 @@ + + + + + + + + Page Not Found - Nuitka Commander + + + + +
+

404

+

页面未找到

+

抱歉,您访问的页面不存在或已被移动。

+ +
+

正在自动重定向到首页

+

如果没有自动跳转,请点击下方链接:

+ 返回主站 +
+
+ + + + + \ No newline at end of file diff --git a/public/sitemap.txt b/public/sitemap.txt deleted file mode 100644 index 80dcc4d..0000000 --- a/public/sitemap.txt +++ /dev/null @@ -1 +0,0 @@ -https://nuitka-commander.erduotong.com/ \ No newline at end of file diff --git a/scripts/client-crawler-detection.js b/scripts/client-crawler-detection.js new file mode 100644 index 0000000..15257d3 --- /dev/null +++ b/scripts/client-crawler-detection.js @@ -0,0 +1,179 @@ +/** + * @Description 客户端爬虫检测脚本 - 用于静态部署环境 + * @Author: GitHub Copilot + * @Date: 2024-12-19 + */ + +(function() { + 'use strict'; + + /** + * 爬虫用户代理检测列表 + */ + const crawlerUserAgents = [ + // Google + 'googlebot', + 'googlebotimage', + 'googlebotvideo', + 'googlebotnews', + 'googlebotmobile', + + // Bing + 'bingbot', + 'bingpreview', + + // Yahoo + 'slurp', + + // Yandex + 'yandexbot', + + // Baidu + 'baiduspider', + + // Social Media + 'facebookexternalhit', + 'twitterbot', + 'linkedinbot', + 'whatsapp', + 'telegrambot', + + // Other crawlers + 'seznambot', + 'duckduckbot', + 'applebot', + 'ia_archiver', + + // Tools + 'curl', + 'wget', + 'python-requests', + 'axios', + 'node-fetch' + ]; + + /** + * 检测是否为爬虫 + */ + function isCrawler() { + const userAgent = navigator.userAgent.toLowerCase(); + return crawlerUserAgents.some(bot => userAgent.includes(bot)); + } + + /** + * 检测是否为无头浏览器(通常是爬虫) + */ + function isHeadlessBrowser() { + // 检测常见的无头浏览器特征 + return ( + navigator.webdriver === true || + window.navigator.webdriver === true || + window.chrome && window.chrome.runtime && window.chrome.runtime.onConnect === undefined || + window.phantom !== undefined || + window.callPhantom !== undefined + ); + } + + /** + * 检测是否支持JavaScript(爬虫通常不会执行JS) + */ + function supportsModernJS() { + try { + // 检测ES6+特性支持 + return typeof Promise !== 'undefined' && + typeof Set !== 'undefined' && + typeof Map !== 'undefined'; + } catch (e) { + return false; + } + } + + /** + * 综合判断是否为爬虫 + */ + function shouldRedirectToSPA() { + // 如果是明确的爬虫,不重定向 + if (isCrawler()) { + console.log('🤖 检测到搜索引擎爬虫,显示静态页面'); + return false; + } + + // 如果是无头浏览器,可能是爬虫 + if (isHeadlessBrowser()) { + console.log('🤖 检测到无头浏览器,可能是爬虫,显示静态页面'); + return false; + } + + // 如果不支持现代JS,可能是爬虫 + if (!supportsModernJS()) { + console.log('🤖 检测到不支持现代JS的环境,可能是爬虫,显示静态页面'); + return false; + } + + console.log('👤 检测到普通用户,重定向到SPA应用'); + return true; + } + + /** + * 执行重定向逻辑 + */ + function executeRedirection() { + // 检查是否已经在根路径 + if (window.location.pathname === '/' || window.location.pathname === '/index.html') { + console.log('👤 已在SPA应用首页,无需重定向'); + return; + } + + // 检查是否应该重定向 + if (shouldRedirectToSPA()) { + console.log('👤 重定向用户到SPA应用...'); + + // 保存当前路径到sessionStorage,SPA可以读取并导航到对应页面 + try { + sessionStorage.setItem('seo_redirect_path', window.location.pathname + window.location.search + window.location.hash); + } catch (e) { + console.warn('无法保存重定向路径到sessionStorage:', e); + } + + // 重定向到根路径 + window.location.replace('/'); + } else { + console.log('🤖 爬虫访问,继续显示静态页面'); + + // 为爬虫优化:移除可能的重定向脚本 + try { + const scriptTags = document.querySelectorAll('script[data-seo-redirect]'); + scriptTags.forEach(script => script.remove()); + } catch (e) { + // 忽略错误 + } + } + } + + /** + * 添加调试信息(仅在开发环境) + */ + function addDebugInfo() { + if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') { + console.group('🔍 SEO重定向调试信息'); + console.log('User-Agent:', navigator.userAgent); + console.log('是否为爬虫:', isCrawler()); + console.log('是否为无头浏览器:', isHeadlessBrowser()); + console.log('支持现代JS:', supportsModernJS()); + console.log('当前路径:', window.location.pathname); + console.groupEnd(); + } + } + + // 页面加载完成后执行检测 + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', function() { + addDebugInfo(); + executeRedirection(); + }); + } else { + addDebugInfo(); + executeRedirection(); + } + +})(); diff --git a/scripts/test-seo.ps1 b/scripts/test-seo.ps1 new file mode 100644 index 0000000..2a0630e --- /dev/null +++ b/scripts/test-seo.ps1 @@ -0,0 +1,76 @@ +# SEO 功能测试脚本 +Write-Host "🧪 开始SEO功能测试..." -ForegroundColor Green + +# 1. 检查必要文件 +Write-Host "`n🔍 检查必要文件..." -ForegroundColor Yellow +$requiredFiles = @( + "scripts\client-crawler-detection.js", + "plugins\vite-prerender.js", + "prerender.config.js" +) + +foreach ($file in $requiredFiles) { + if (-not (Test-Path $file)) { + Write-Host "❌ 缺少文件: $file" -ForegroundColor Red + exit 1 + } +} +Write-Host "✅ 必要文件检查通过" -ForegroundColor Green + +# 2. 构建SEO版本 +Write-Host "`n📦 构建SEO版本..." -ForegroundColor Yellow +try { + pnpm run build:website:seo + if ($LASTEXITCODE -ne 0) { + throw "构建失败" + } + Write-Host "✅ SEO构建完成" -ForegroundColor Green +} +catch { + Write-Host "❌ SEO构建失败: $_" -ForegroundColor Red + exit 1 +} + +# 3. 检查生成的文件 +Write-Host "`n🔍 检查生成的文件..." -ForegroundColor Yellow + +# 检查静态文件 +$staticDir = "dist\static" +if (Test-Path $staticDir) { + $htmlFiles = Get-ChildItem -Path $staticDir -Filter "*.html" + Write-Host "📄 静态HTML文件: $($htmlFiles.Count)个" -ForegroundColor Cyan + + # 检查一个文件中是否包含爬虫检测脚本 + if ($htmlFiles.Count -gt 0) { + $sampleFile = $htmlFiles[0].FullName + $htmlContent = Get-Content -Path $sampleFile -Raw + if ($htmlContent -match "data-seo-redirect") { + Write-Host "✅ 爬虫检测脚本已注入" -ForegroundColor Green + } + else { + Write-Host "❌ 爬虫检测脚本未注入" -ForegroundColor Red + } + } +} +else { + Write-Host "❌ 静态目录不存在" -ForegroundColor Red +} + +# 检查 sitemap.xml +if (Test-Path "dist\static\sitemap.xml") { + Write-Host "✅ Sitemap.xml 已生成" -ForegroundColor Green +} +else { + Write-Host "⚠️ Sitemap.xml 未生成" -ForegroundColor Yellow +} + +# 检查 robots.txt +if (Test-Path "dist\static\robots.txt") { + Write-Host "✅ Robots.txt 已生成" -ForegroundColor Green +} +else { + Write-Host "⚠️ Robots.txt 未生成" -ForegroundColor Yellow +} + +Write-Host "`n🎉 SEO测试完成!" -ForegroundColor Green +Write-Host "💡 可以将 dist 目录的内容部署到 Cloudflare Pages 等静态托管服务" -ForegroundColor Cyan diff --git a/src/App.vue b/src/App.vue index 3fae59c..eb76451 100644 --- a/src/App.vue +++ b/src/App.vue @@ -8,6 +8,56 @@ import CommandEdit from "@/components/commandEdit.vue"; import {user_options} from "@/values/stores/user_options.js"; import OutputPage from "@/components/outputPage.vue"; import InputPage from "@/components/inputPage.vue"; +import { onMounted } from 'vue'; + +// SEO重定向处理 +function handleSEORedirect() { + try { + const redirectPath = sessionStorage.getItem('seo_redirect_path'); + if (redirectPath) { + console.log('🔄 处理SEO重定向路径:', redirectPath); + + // 清除重定向路径 + sessionStorage.removeItem('seo_redirect_path'); + + // 解析路径并设置相应的应用状态 + // 例如:/zh/2.5.*/zh → 设置语言为zh,版本为2.5.*等 + const pathSegments = redirectPath.split('/').filter(seg => seg); + + if (pathSegments.length >= 2) { + const [uiLang, version, ...rest] = pathSegments; + const cmdLang = rest[0] || 'en'; + + console.log(`📍 SEO重定向参数: UI语言=${uiLang}, 版本=${version}, 命令语言=${cmdLang}`); + + // 这里可以根据实际的应用状态管理方式来设置参数 + // 例如设置localStorage或者应用状态 + if (uiLang && window.localStorage) { + localStorage.setItem('seo_redirect_ui_lang', uiLang); + localStorage.setItem('seo_redirect_version', version); + localStorage.setItem('seo_redirect_cmd_lang', cmdLang); + } + } + } + } catch (error) { + console.warn('SEO重定向处理失败:', error); + } +} + +// 预渲染准备标记 +onMounted(() => { + // 处理SEO重定向 + handleSEORedirect(); + + // 等待一段时间确保所有组件都已加载 + setTimeout(() => { + // 直接在body上添加预渲染准备标记 + document.body.setAttribute('data-prerender-ready', 'true'); + + // 在控制台输出调试信息 + console.log('🎭 Page ready for prerendering'); + }, 2000); // 2秒延迟确保所有内容加载完成 +});