From 012aed1aaf054347130809313d4aee929a79e0c7 Mon Sep 17 00:00:00 2001 From: erduotong <3135351980@qq.com> Date: Sun, 1 Jun 2025 16:54:31 +0800 Subject: [PATCH 1/6] =?UTF-8?q?feat(index,=20robots,=20sitemap):=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20robots.txt=20=E5=92=8C=20sitemap.xml=20?= =?UTF-8?q?=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 robots.txt 文件以控制搜索引擎爬虫的访问权限,并添加 sitemap.xml 文件以便于搜索引擎索引网站内容。 - robots.txt 允许所有用户代理访问,并禁止访问不必要的文件。 - sitemap.xml 提供网站的结构信息,帮助搜索引擎更好地抓取页面。 --- index.html | 31 ++++++++++++++++++------------- public/robots.txt | 11 +++++++++++ public/sitemap.xml | 9 +++++++++ 3 files changed, 38 insertions(+), 13 deletions(-) create mode 100644 public/robots.txt create mode 100644 public/sitemap.xml 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/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..0d9520f --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,11 @@ +User-agent: * +Allow: / + +# Sitemap +Sitemap: https://nuitka-commander.erduotong.com/sitemap.xml + +# Disallow unnecessary files +Disallow: /src/ +Disallow: /*.js$ +Disallow: /*.css$ +Disallow: /*.json$ \ No newline at end of file diff --git a/public/sitemap.xml b/public/sitemap.xml new file mode 100644 index 0000000..f703e33 --- /dev/null +++ b/public/sitemap.xml @@ -0,0 +1,9 @@ + + + + https://nuitka-commander.erduotong.com/ + 2025-06-01 + weekly + 1.0 + + \ No newline at end of file From 5987e531975732566d7e21376350ba6ea83b9c2b Mon Sep 17 00:00:00 2001 From: erduotong <3135351980@qq.com> Date: Sun, 1 Jun 2025 20:14:32 +0800 Subject: [PATCH 2/6] feat: add SEO build support with prerendering and crawler detection - Updated pnpm workspace to include puppeteer as a built dependency. - Implemented prerendering in App.vue to mark the page as ready for SEO. - Enhanced vite.config.js to integrate the vite-prerender plugin for static page generation. - Created comprehensive SEO_BUILD_GUIDE.md for instructions on SEO build process. - Developed vite-prerender.js plugin to handle page rendering with Playwright. - Configured prerender.config.js to define page combinations for prerendering. - Added PowerShell script for building SEO optimized versions with prerendering. - Implemented crawler detection in server/crawler-detection.js to serve static pages for bots. --- .github/workflows/build_and_publish.yml | 149 +++--- .github/workflows/build_beta.yml | 81 ++-- docs/SEO_BUILD_GUIDE.md | 120 +++++ package.json | 13 +- plugins/vite-prerender.js | 321 +++++++++++++ pnpm-lock.yaml | 590 +++++++++++++++++++++++- pnpm-workspace.yaml | 1 + prerender.config.js | 177 +++++++ scripts/build-seo.ps1 | 164 +++++++ server/crawler-detection.js | 233 ++++++++++ src/App.vue | 13 + vite.config.js | 16 +- 12 files changed, 1756 insertions(+), 122 deletions(-) create mode 100644 docs/SEO_BUILD_GUIDE.md create mode 100644 plugins/vite-prerender.js create mode 100644 prerender.config.js create mode 100644 scripts/build-seo.ps1 create mode 100644 server/crawler-detection.js diff --git a/.github/workflows/build_and_publish.yml b/.github/workflows/build_and_publish.yml index eda78f2..69e18e6 100644 --- a/.github/workflows/build_and_publish.yml +++ b/.github/workflows/build_and_publish.yml @@ -1,84 +1,91 @@ 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: 8 - run_install: false + - uses: pnpm/action-setup@v4 + name: Install pnpm + with: + version: 8 + run_install: false - - name: Install Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: 'pnpm' + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: "pnpm" - name: Install dependencies + run: pnpm install - - name: Install dependencies - run: pnpm install + - name: 安装 Playwright 浏览器 (仅 SEO 构建需要) + if: github.event.inputs.enable_seo == 'true' + run: npx playwright install chromium - - name: 安装依赖 - if: steps.pnpm-cache.outputs.cache-hit != 'true' - run: pnpm install + - name: 更新制作人员列表 + run: node ${{ github.workspace }}/.github/workflows/scripts/update_authors.js - - name: 更新制作人员列表 - run: node ${{ github.workspace }}/.github/workflows/scripts/update_authors.js + - 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: github.event.inputs.enable_seo != 'true' + run: pnpm run build:website - - name: 检查版本并同步版本号 - run: node ${{ github.workspace }}/.github/workflows/scripts/sync_version.js - - name: 检查更新日志并转markdown - run: | - node ${{ github.workspace }}/.github/workflows/scripts/update_changelog.js - # 其他预处理以后要用的时候在上方添加 + - name: 为网站SEO构建 + if: github.event.inputs.enable_seo == 'true' + run: pnpm run build:website:seo - - 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: 发布到 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 914f106..b4e5456 100644 --- a/.github/workflows/build_beta.yml +++ b/.github/workflows/build_beta.yml @@ -1,38 +1,59 @@ 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 + publish: + name: 构建测试版本 + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 - name: Install pnpm - with: - version: 8 - run_install: false + - uses: pnpm/action-setup@v4 + name: Install pnpm + with: + version: 8 + run_install: false - - name: Install Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: 'pnpm' + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: "pnpm" - name: Install dependencies + run: pnpm install - - name: Install dependencies - run: pnpm install + - name: 安装 Playwright 浏览器 (仅 SEO 构建需要) + if: github.event.inputs.build_type == 'website_seo' + run: npx playwright install chromium - - 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 + - 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/docs/SEO_BUILD_GUIDE.md b/docs/SEO_BUILD_GUIDE.md new file mode 100644 index 0000000..f6fc25e --- /dev/null +++ b/docs/SEO_BUILD_GUIDE.md @@ -0,0 +1,120 @@ +# SEO 构建指南 + +本项目现已支持 SEO 优化构建,能够预渲染静态页面以提高搜索引擎可见性。 + +## 功能特性 + +- **预渲染静态页面**: 使用 Playwright 预渲染所有语言和版本组合的页面(约 96 个页面) +- **爬虫检测**: 自动检测搜索引擎爬虫并提供静态页面 +- **扁平化结构**: 静态页面使用扁平化文件名格式(如 `en_2_1___en.html`) +- **并发渲染**: 基于系统资源动态调整并发数,提高构建效率 + +## 本地使用 + +### 1. 安装依赖 + +```bash +pnpm install +npx playwright install chromium +``` + +### 2. SEO 构建 + +```bash +# 启用 SEO 预渲染构建 +pnpm run build:website:seo + +# 或者只进行预渲染(不启动预览服务器) +pnpm run prerender +``` + +### 3. 本地测试 + +```bash +# 启动爬虫检测服务器 +pnpm run serve + +# 开发环境测试 +pnpm run serve:dev +``` + +## 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 版本 × 命令语言 + +### 爬虫检测 + +- 服务器文件: `server/crawler-detection.js` +- 支持的爬虫: Google、Bing、百度、搜狗等主流搜索引擎 +- 静态页面路径: `/dist/static/` + +### 并发控制 + +- 自动计算最佳并发数(基于 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/package.json b/package.json index 3227181..1f32240 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,13 @@ "scripts": { "dev": "vite --port 3002", "build:website": "vite build --mode website", + "build:website:seo": "cross-env ENABLE_PRERENDER=true vite build --mode website && pnpm run preview:temp", "build:local_use": "vite build --mode local_use", - "preview": "vite preview" + "preview": "vite preview", + "preview:temp": "vite preview --port 4173 --host", + "serve": "node server/crawler-detection.js", + "serve:dev": "cross-env NODE_ENV=development node server/crawler-detection.js", + "prerender": "cross-env ENABLE_PRERENDER=true vite build --mode website" }, "dependencies": { "@element-plus/icons-vue": "^2.3.1", @@ -15,7 +20,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 +31,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 +48,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..19c2cf8 --- /dev/null +++ b/plugins/vite-prerender.js @@ -0,0 +1,321 @@ +/** + * @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 + if (renderOptions.generateSitemap) { + await pluginInstance.generateSitemap(pages, renderOptions) + console.log('🗺️ Sitemap 已生成') + } + + } finally { + // 关闭所有浏览器实例 + await Promise.all(browsers.map(browser => browser.close())) + } + + } finally { + // 关闭预览服务器 + if (previewServer) { + await previewServer.close() + console.log('🛑 预览服务器已关闭') + } + } + }, + + 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 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, html, 'utf-8') + + } finally { + await page.close() + } }, + + async generateSitemap(pages, renderOptions) { + const baseUrl = renderOptions.baseUrl || 'https://your-domain.com' + + const sitemap = ` + +${pages.map(page => ` + ${baseUrl}${page.path} + ${new Date().toISOString().split('T')[0]} + weekly + 0.8 + `).join('\n')} +` + const sitemapPath = path.join(renderOptions.outputDir || 'dist/static', 'sitemap.xml') + await fs.writeFile(sitemapPath, sitemap, 'utf-8') + } + } + + 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..c77117f --- /dev/null +++ b/prerender.config.js @@ -0,0 +1,177 @@ +/** + * @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 uiLanguages = Object.keys(supported_i18n) + + // 获取支持的Nuitka版本 + const nuitkaVersions = Object.keys(supported_nuitka_version.versions) + + // 为每种组合生成配置 + 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', + + // 是否生成sitemap + generateSitemap: true, + + // 网站基础URL(用于sitemap) + baseUrl: 'https://your-domain.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/scripts/build-seo.ps1 b/scripts/build-seo.ps1 new file mode 100644 index 0000000..d42a22a --- /dev/null +++ b/scripts/build-seo.ps1 @@ -0,0 +1,164 @@ +# SEO构建脚本 - PowerShell版本 +# 用于构建带有预渲染的SEO优化版本 + +param( + [string]$Mode = "production", + [switch]$SkipInstall, + [switch]$SkipPreview, + [string]$Port = "4173" +) + +Write-Host "🚀 开始SEO预渲染构建..." -ForegroundColor Green + +# 检查Node.js和pnpm +try { + $nodeVersion = node --version + Write-Host "✅ Node.js版本: $nodeVersion" -ForegroundColor Green + + # 检查pnpm + try { + $pnpmVersion = pnpm --version + Write-Host "✅ pnpm版本: $pnpmVersion" -ForegroundColor Green + } + catch { + Write-Host "❌ 未找到pnpm,正在检查npm..." -ForegroundColor Yellow + $npmVersion = npm --version + Write-Host "✅ npm版本: $npmVersion" -ForegroundColor Green + } +} +catch { + Write-Host "❌ 请确保已安装Node.js和pnpm(或npm)" -ForegroundColor Red + exit 1 +} + +# 安装依赖 +if (-not $SkipInstall) { + Write-Host "📦 安装依赖..." -ForegroundColor Yellow + try { + # 优先使用pnpm + if (Get-Command pnpm -ErrorAction SilentlyContinue) { + pnpm install + } + else { + npm install + } + Write-Host "✅ 依赖安装完成" -ForegroundColor Green + } + catch { + Write-Host "❌ 依赖安装失败: $_" -ForegroundColor Red + exit 1 + } +} + +# 清理之前的构建 +Write-Host "🧹 清理构建目录..." -ForegroundColor Yellow +if (Test-Path "dist") { + Remove-Item -Path "dist" -Recurse -Force +} + +# 构建主应用 +Write-Host "🔨 构建主应用..." -ForegroundColor Yellow +try { + # 优先使用pnpm + if (Get-Command pnpm -ErrorAction SilentlyContinue) { + pnpm run build:website + } + else { + npm run build:website + } + Write-Host "✅ 主应用构建完成" -ForegroundColor Green +} +catch { + Write-Host "❌ 主应用构建失败: $_" -ForegroundColor Red + exit 1 +} + +# 启动预览服务器进行预渲染 +if (-not $SkipPreview) { + Write-Host "🌐 启动预览服务器..." -ForegroundColor Yellow + # 启动后台预览服务器 + $previewJob = Start-Job -ScriptBlock { + param($Port) + Set-Location $using:PWD + # 优先使用pnpm + if (Get-Command pnpm -ErrorAction SilentlyContinue) { + pnpm vite preview --port $Port --host + } + else { + npx vite preview --port $Port --host + } + } -ArgumentList $Port + + # 等待服务器启动 + Write-Host "⏳ 等待服务器启动..." -ForegroundColor Yellow + Start-Sleep -Seconds 5 + + # 检查服务器是否启动成功 + try { + $response = Invoke-WebRequest -Uri "http://localhost:$Port" -TimeoutSec 10 + Write-Host "✅ 预览服务器启动成功" -ForegroundColor Green + } + catch { + Write-Host "❌ 预览服务器启动失败" -ForegroundColor Red + Stop-Job $previewJob + Remove-Job $previewJob + exit 1 + } +} + +# 运行预渲染 +Write-Host "🎭 开始预渲染..." -ForegroundColor Yellow +try { + $env:ENABLE_PRERENDER = "true" + $env:PREVIEW_PORT = $Port + # 优先使用pnpm + if (Get-Command pnpm -ErrorAction SilentlyContinue) { + pnpm run build:website:seo + } + else { + npm run build:website:seo + } + + Write-Host "✅ 预渲染完成" -ForegroundColor Green +} +catch { + Write-Host "❌ 预渲染失败: $_" -ForegroundColor Red + if ($previewJob) { + Stop-Job $previewJob + Remove-Job $previewJob + } + exit 1 +} +finally { + # 清理环境变量 + Remove-Item Env:ENABLE_PRERENDER -ErrorAction SilentlyContinue + Remove-Item Env:PREVIEW_PORT -ErrorAction SilentlyContinue +} + +# 停止预览服务器 +if ($previewJob) { + Write-Host "🛑 停止预览服务器..." -ForegroundColor Yellow + Stop-Job $previewJob + Remove-Job $previewJob +} + +# 显示构建结果 +Write-Host "`n📊 构建结果:" -ForegroundColor Cyan +Write-Host "主应用: dist/" -ForegroundColor Gray +Write-Host "静态页面: dist/static/" -ForegroundColor Gray + +if (Test-Path "dist/static") { + $staticPages = Get-ChildItem -Path "dist/static" -Recurse -Filter "*.html" | Measure-Object + Write-Host "生成的静态页面数量: $($staticPages.Count)" -ForegroundColor Gray +} + +if (Test-Path "dist/static/sitemap.xml") { + Write-Host "✅ Sitemap已生成: dist/static/sitemap.xml" -ForegroundColor Green +} +else { + Write-Host "⚠️ Sitemap未生成" -ForegroundColor Yellow +} + +Write-Host "`n🎉 SEO构建完成!" -ForegroundColor Green +Write-Host "运行服务器: pnpm run serve" -ForegroundColor Cyan +Write-Host "开发服务器: pnpm run serve:dev" -ForegroundColor Cyan diff --git a/server/crawler-detection.js b/server/crawler-detection.js new file mode 100644 index 0000000..dce73f3 --- /dev/null +++ b/server/crawler-detection.js @@ -0,0 +1,233 @@ +/** + * @Description 服务端路由 - 检测爬虫并提供静态页面 + * @Author: GitHub Copilot + * @Date: 2024-12-19 + */ + +import express from 'express' +import path from 'path' +import fs from 'fs-extra' +import { fileURLToPath } from 'url' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) + +/** + * 爬虫用户代理检测 + */ +const crawlerUserAgents = [ + // Google + 'Googlebot', + 'GooglebotVideo', + 'GooglebotImage', + 'GooglebotNews', + 'GooglebotMobile', + + // Bing + 'Bingbot', + 'BingPreview', + + // Yahoo + 'Slurp', + + // Yandex + 'YandexBot', + + // Baidu + 'Baiduspider', + + // Facebook + 'facebookexternalhit', + + // Twitter + 'Twitterbot', + + // LinkedIn + 'LinkedInBot', + + // WhatsApp + 'WhatsApp', + + // Telegram + 'TelegramBot', + + // 其他常见爬虫 + 'SeznamBot', + 'DuckDuckBot', + 'AppleBot', + 'ia_archiver', // Internet Archive + 'curl', + 'wget', + 'python-requests', + 'axios', + 'node-fetch' +] + +/** + * 检测是否为爬虫 + */ +function isCrawler(userAgent) { + if (!userAgent) return false + + const ua = userAgent.toLowerCase() + return crawlerUserAgents.some(bot => ua.includes(bot.toLowerCase())) +} + +/** + * 查找对应的静态页面(扁平化结构) + */ +async function findStaticPage(requestPath, staticDir) { + // 移除开头和结尾的斜杠 + const cleanPath = requestPath.replace(/^\//, '').replace(/\/$/, '') + + // 如果是根路径,使用默认页面 + if (!cleanPath) { + const defaultPage = path.join(staticDir, 'en_simple_en.html') + if (await fs.pathExists(defaultPage)) { + return defaultPage + } + } + + // 将路径转换为扁平化的文件名格式 + const flattenedName = cleanPath.replace(/\//g, '_') + '.html' + const directMatch = path.join(staticDir, flattenedName) + if (await fs.pathExists(directMatch)) { + return directMatch + } + + // 尝试解析路径并构建可能的文件名 + const pathSegments = cleanPath.split('/').filter(seg => seg) + + if (pathSegments.length >= 2) { + const [lang, version, ...rest] = pathSegments + const cmdLang = rest[0] || 'en' + + // 构建扁平化的文件名 (lang_version_cmdLang.html) + const versionFormatted = version.replace(/[.*]/g, '_') + const flattenedName = `${lang}_${versionFormatted}_${cmdLang}.html` + const staticFilePath = path.join(staticDir, flattenedName) + + if (await fs.pathExists(staticFilePath)) { + return staticFilePath + } + } + + // 尝试基于语言的匹配 + if (pathSegments.length >= 1) { + const lang = pathSegments[0] + + // 查找以该语言开头的任意文件 + try { + const files = await fs.readdir(staticDir) + const matchingFile = files.find(file => + file.startsWith(`${lang}_`) && file.endsWith('.html') + ) + + if (matchingFile) { + return path.join(staticDir, matchingFile) + } + } catch (error) { + console.log('读取静态目录失败:', error.message) + } + } + + return null +} + +/** + * 创建路由中间件 + */ +export function createCrawlerDetectionMiddleware(options = {}) { + const { + staticDir = 'dist/static', + fallbackPath = '/index.html' + } = options + + return async (req, res, next) => { + const userAgent = req.get('User-Agent') || '' + const requestPath = req.path + + // 记录访问日志 + console.log(`${new Date().toISOString()} - ${isCrawler(userAgent) ? '🤖 CRAWLER' : '👤 USER'} - ${userAgent} - ${requestPath}`) + + // 如果是爬虫访问 + if (isCrawler(userAgent)) { + // 尝试提供对应的静态页面 + const staticFilePath = await findStaticPage(requestPath, staticDir) + + if (staticFilePath) { + console.log(`📄 提供静态页面: ${staticFilePath}`) + return res.sendFile(path.resolve(staticFilePath)) + } else { + // 如果没有找到对应的静态页面,提供默认的静态页面 + const defaultStaticPath = path.join(staticDir, 'en_simple_en.html') + if (await fs.pathExists(defaultStaticPath)) { + console.log(`📄 提供默认静态页面: ${defaultStaticPath}`) + return res.sendFile(path.resolve(defaultStaticPath)) + } else { + // 如果连默认页面都没有,返回404 + console.log(`❌ 未找到静态页面: ${requestPath}`) + return res.status(404).send('Page not found') + } + } + } + + // 普通用户访问,继续正常流程 + next() + } +} + +/** + * Express 服务器设置 + */ +export function createServer(options = {}) { + const app = express() + const { + port = 3000, + staticDir = 'dist', + prerenderStaticDir = 'dist/static', + fallbackPath = '/index.html' + } = options + + // 添加爬虫检测中间件 + app.use(createCrawlerDetectionMiddleware({ + staticDir: prerenderStaticDir, + fallbackPath + })) + + // 提供静态文件 + app.use(express.static(staticDir)) + + // SPA 回退路由(对于普通用户) + app.get('*', (req, res) => { + res.sendFile(path.resolve(staticDir, 'index.html')) + }) + + return app +} + +/** + * 启动服务器 + */ +export function startServer(options = {}) { + const app = createServer(options) + const port = options.port || 3000 + + app.listen(port, () => { + console.log(`🚀 服务器启动在端口 ${port}`) + console.log(`🌐 访问地址: http://localhost:${port}`) + console.log(`🤖 爬虫将被重定向到预渲染页面`) + console.log(`👤 用户将访问SPA应用`) + }) + + return app +} + +// 如果直接运行此文件,启动服务器 +if (import.meta.url === `file://${process.argv[1]}`) { + startServer({ + port: process.env.PORT || 3000, + staticDir: 'dist', + prerenderStaticDir: 'dist/static' + }) +} diff --git a/src/App.vue b/src/App.vue index 3fae59c..ae0b8ca 100644 --- a/src/App.vue +++ b/src/App.vue @@ -8,6 +8,19 @@ 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'; + +// 预渲染准备标记 +onMounted(() => { + // 等待一段时间确保所有组件都已加载 + setTimeout(() => { + // 直接在body上添加预渲染准备标记 + document.body.setAttribute('data-prerender-ready', 'true'); + + // 在控制台输出调试信息 + console.log('🎭 Page ready for prerendering'); + }, 2000); // 2秒延迟确保所有内容加载完成 +});