From b78e063a2425385034e63fe48b190ac4c5ee9d7a Mon Sep 17 00:00:00 2001 From: 337547038 <337547038@qq.com> Date: Fri, 30 Jun 2023 15:43:29 +0800 Subject: [PATCH] init --- .gitignore | 24 ++++++++++ README.md | 85 ++++++++++++++++++++++++++++++++++ index.html | 15 ++++++ package.json | 25 ++++++++++ packages/README.md | 87 +++++++++++++++++++++++++++++++++++ packages/package.json | 53 +++++++++++++++++++++ packages/src/index.ts | 65 ++++++++++++++++++++++++++ packages/src/render.js | 46 ++++++++++++++++++ packages/tsup.config.ts | 25 ++++++++++ src/App.vue | 8 ++++ src/components/HelloWorld.vue | 38 +++++++++++++++ src/main.ts | 5 ++ src/router.ts | 48 +++++++++++++++++++ src/views/about/index.vue | 7 +++ src/views/test.vue | 8 ++++ src/vite-env.d.ts | 12 +++++ tsconfig.json | 24 ++++++++++ tsconfig.node.json | 10 ++++ vite.config.ts | 12 +++++ 19 files changed, 597 insertions(+) create mode 100644 .gitignore create mode 100644 index.html create mode 100644 package.json create mode 100644 packages/README.md create mode 100644 packages/package.json create mode 100644 packages/src/index.ts create mode 100644 packages/src/render.js create mode 100644 packages/tsup.config.ts create mode 100644 src/App.vue create mode 100644 src/components/HelloWorld.vue create mode 100644 src/main.ts create mode 100644 src/router.ts create mode 100644 src/views/about/index.vue create mode 100644 src/views/test.vue create mode 100644 src/vite-env.d.ts create mode 100644 tsconfig.json create mode 100644 tsconfig.node.json create mode 100644 vite.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/README.md b/README.md index d69397f..01d5c93 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,87 @@ # vite-plugin-seo-prerender + `vite-plugin-seo-prerender` 插件是一个用于 `Vite` 构建工具的预渲染插件,它可以将你的单页面应用 (SPA) 在构建时静态预渲染为 HTML 文件,以提高首次加载速度和SEO友好性。适用于对站点少量页面生成静态HTML。支持 `Vue、React`等所有框架 + +*** + ++ 静态预渲染:将单页面应用在构建时预渲染为 HTML 文件。 ++ SSG (静态站点生成):支持根据路由配置生成静态 HTML 文件。 ++ 异步数据获取:支持在构建时获取异步数据并注入到预渲染的 HTML 文件中。 ++ SEO 友好:生成的静态 HTML 文件对搜索引擎友好,可以更好地被爬虫索引。 + + +## 安装使用 + +```shell +npm install vite-plugin-seo-prerender -D +# or +pnpm install vite-plugin-seo-prerender -D +# or +yarn install vite-plugin-seo-prerender -D +``` + +## 使用配置 + +```ts +// vite.config.ts +import { defineConfig } from 'vite' +import seoPrerender from 'vite-plugin-seo-prerender' + +export default defineConfig({ + plugins: [ + seoPrerender({ + puppeteer: {}, // puppeteer参数配置,可选 + routes: [], // 需要生成的路由,必填 + removeStyle:true, // 是否移除多余样式,默认true。在启动服务vite preview时会产生一些多余样式,如若丢失样式可设置为false + callback:(content,route)=>{ + // 可对当前页面html内容进行一些替换等处理 + // 一些处理逻辑... + return content + } + }) + ] +}) +``` + + +## 发布 + +运行 `vite build` 构建命令时即可生成HTML 文件 + +## 附seo关键词优化 + +```ts +// router.ts +const routes = [ + { + path: '/about', + name: '/about', + component: () => import('./views/about/index.vue'), + meta: { + title: '关于我们', + keywords: '关键词1, 关键词2', + description: '关于我们描述' + } + } +] + +router.afterEach((to, from, next) => { + const {title, keywords, description} = to.meta + if (title) { + document.title = title + } + if (keywords) { + const metaKeywords = document.querySelector('meta[name="keywords"]') + if (metaKeywords) { + metaKeywords.content = keywords + } + } + if (description) { + const metaDescription = document.querySelector('meta[name="description"]') + if (metaDescription) { + metaDescription.content = description + } + } + next() +}) +``` diff --git a/index.html b/index.html new file mode 100644 index 0000000..777ada7 --- /dev/null +++ b/index.html @@ -0,0 +1,15 @@ + + + + + + + Vite + Vue + TS + + + + +
+ + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..85f4e0d --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "vite-plugin-seo-prerender", + "private": true, + "version": "0.2.0", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "publish": "pnpm --filter ./packages tsup" + }, + "dependencies": { + "vue": "^3.3.4", + "vue-router": "^4.2.2" + }, + "devDependencies": { + "@types/node": "^20.3.1", + "@vitejs/plugin-vue": "^4.2.3", + "puppeteer": "^20.7.3", + "tsup": "^7.0.0", + "typescript": "^5.1.3", + "vite": "^4.3.9", + "vite-plugin-sprites": "^0.2.0", + "vue-tsc": "^1.8.0" + } +} diff --git a/packages/README.md b/packages/README.md new file mode 100644 index 0000000..01d5c93 --- /dev/null +++ b/packages/README.md @@ -0,0 +1,87 @@ +# vite-plugin-seo-prerender + +`vite-plugin-seo-prerender` 插件是一个用于 `Vite` 构建工具的预渲染插件,它可以将你的单页面应用 (SPA) 在构建时静态预渲染为 HTML 文件,以提高首次加载速度和SEO友好性。适用于对站点少量页面生成静态HTML。支持 `Vue、React`等所有框架 + +*** + ++ 静态预渲染:将单页面应用在构建时预渲染为 HTML 文件。 ++ SSG (静态站点生成):支持根据路由配置生成静态 HTML 文件。 ++ 异步数据获取:支持在构建时获取异步数据并注入到预渲染的 HTML 文件中。 ++ SEO 友好:生成的静态 HTML 文件对搜索引擎友好,可以更好地被爬虫索引。 + + +## 安装使用 + +```shell +npm install vite-plugin-seo-prerender -D +# or +pnpm install vite-plugin-seo-prerender -D +# or +yarn install vite-plugin-seo-prerender -D +``` + +## 使用配置 + +```ts +// vite.config.ts +import { defineConfig } from 'vite' +import seoPrerender from 'vite-plugin-seo-prerender' + +export default defineConfig({ + plugins: [ + seoPrerender({ + puppeteer: {}, // puppeteer参数配置,可选 + routes: [], // 需要生成的路由,必填 + removeStyle:true, // 是否移除多余样式,默认true。在启动服务vite preview时会产生一些多余样式,如若丢失样式可设置为false + callback:(content,route)=>{ + // 可对当前页面html内容进行一些替换等处理 + // 一些处理逻辑... + return content + } + }) + ] +}) +``` + + +## 发布 + +运行 `vite build` 构建命令时即可生成HTML 文件 + +## 附seo关键词优化 + +```ts +// router.ts +const routes = [ + { + path: '/about', + name: '/about', + component: () => import('./views/about/index.vue'), + meta: { + title: '关于我们', + keywords: '关键词1, 关键词2', + description: '关于我们描述' + } + } +] + +router.afterEach((to, from, next) => { + const {title, keywords, description} = to.meta + if (title) { + document.title = title + } + if (keywords) { + const metaKeywords = document.querySelector('meta[name="keywords"]') + if (metaKeywords) { + metaKeywords.content = keywords + } + } + if (description) { + const metaDescription = document.querySelector('meta[name="description"]') + if (metaDescription) { + metaDescription.content = description + } + } + next() +}) +``` diff --git a/packages/package.json b/packages/package.json new file mode 100644 index 0000000..95ed74a --- /dev/null +++ b/packages/package.json @@ -0,0 +1,53 @@ +{ + "name": "vite-plugin-seo-prerender", + "version": "0.0.1", + "description": "`vite-plugin-seo-prerender` 插件是一个用于 `Vite` 构建工具的预渲染插件,它可以将你的单页面应用 (SPA) 在构建时静态预渲染为 HTML 文件,以提高首次加载速度和SEO友好性。适用于对站点少量页面生成静态HTML。支持 `Vue、React`等所有框架", + "license": "MIT", + "author": "337547038", + "keywords": [ + "prerender", + "seo", + "puppeteer", + "vue3", + "react", + "vite" + ], + "repository": { + "type": "git", + "url": "https://github.com/337547038/vite-plugin-seo-prerender.git", + "directory": "packages" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org" + }, + "bugs": { + "url": "https://github.com/337547038/vite-plugin-seo-prerender/issues" + }, + "homepage": "https://github.com/337547038/vite-plugin-seo-prerender", + "files": [ + "dist" + ], + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + } + }, + "scripts": { + "build": "vite build", + "tsup": "tsup" + }, + "dependencies": { + "puppeteer": "^20.7.3" + }, + "devDependencies": { + "@types/node": "^20.2.5", + "typescript": "^5.0.2" + } +} diff --git a/packages/src/index.ts b/packages/src/index.ts new file mode 100644 index 0000000..325ae1f --- /dev/null +++ b/packages/src/index.ts @@ -0,0 +1,65 @@ +import seoPrerender from './render' +import childProcess from 'child_process' + +interface Config { + puppeteer?: any // puppeteer一些配置 + routes: string[] // 需要生成的路由地址 + removeStyle?: boolean // 启用vite preview会自带有些样式,默认下移除 + callback?: Function +} + +const prerender = (config: Config) => { + const cfgConfig = { + outDir: '', + mode: '', + root: '', + local: '' + } + return { + name: 'vitePluginSeoPrerender', + enforce: 'post', + configResolved(cfg: any) { + cfgConfig.outDir = cfg.build.outDir + cfgConfig.mode = cfg.mode + cfgConfig.root = cfg.root + }, + buildEnd() { + //console.log('buildEnd') + }, + closeBundle() { + if (!config?.routes?.length) { + console.log('路由地址为空,请配置需预渲染的routes') + return + } + // vite build 构建生产环境时才执行 + if (cfgConfig.mode !== 'production') { + return + } + console.log('[vite-plugin-seo-prerender] is start..') + const cProcess = childProcess.exec('vite preview', (err, stdout, stderr) => { + if (err) { + console.error('执行命令时发生错误:', err); + return; + } + }) + let localUrl + cProcess.stdout.on('data', async (data) => { + const local = data.match(/http:\/\/(.*?)\//g) + if (local && local.length && !localUrl) { + //转义并去掉最后一个/ + localUrl = local[0].replace(/\x1B\[\d+m/g, '').slice(0, -1) // 控制台输出的有些会经过转义 + console.log('Local: ' + localUrl) + cfgConfig.local = localUrl + await seoPrerender(Object.assign(config, cfgConfig)) + // 在某个条件满足时,关闭进程退出 + cProcess.kill('SIGTERM') + process.exit() // 关闭当前进程并退出 + localUrl = '' + } + }) + } + } +} +export default prerender + + diff --git a/packages/src/render.js b/packages/src/render.js new file mode 100644 index 0000000..b4f840e --- /dev/null +++ b/packages/src/render.js @@ -0,0 +1,46 @@ +import puppeteer from 'puppeteer' +import fs from 'fs' +import path from 'path' + +// 递归创建目录 +function recursiveMkdir(dirPath) { + const parentDir = path.dirname(dirPath); // 获取父级目录路径 + + if (!fs.existsSync(parentDir)) { + recursiveMkdir(parentDir); // 递归创建父级目录 + } + + if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath); // 创建当前目录 + } +} + +const seoPrerender = async (config) => { + const browser = await puppeteer.launch(Object.assign({headless: 'new'}, config.puppeteer || {})); + const page = await browser.newPage() + for (const item of config.routes) { + await page.goto(config.local + item) + await page.setViewport({width: 1024, height: 768}) + let content = await page.content() + if (content.removeStyle !== false) { + // 若出现导常,可设置参数removeStyle:false + content = content.replace(/)<[^<]*)*<\/style>/gi, ""); + } + if (config.callback) { + content = config.callback(content, item) + } + if (item.indexOf('?') !== -1) { + // 填写的路由地址带有意外参数时不处理 + console.log(`[vite-plugin-seo-prerender] ${item} is error,unexpected?`) + } else { + const fullPath = path.join(config.outDir, item) + recursiveMkdir(fullPath) + const filePath = path.join(fullPath, 'index.html') + fs.writeFileSync(filePath, content) + console.log(`[vite-plugin-seo-prerender] ${filePath} is success!`) + } + } + await browser.close(); + console.log('[vite-plugin-seo-prerender] is complete') +} +export default seoPrerender diff --git a/packages/tsup.config.ts b/packages/tsup.config.ts new file mode 100644 index 0000000..16606fa --- /dev/null +++ b/packages/tsup.config.ts @@ -0,0 +1,25 @@ +import { defineConfig } from 'tsup' +import pkg from './package.json' +import fs from 'fs' + +fs.createReadStream('../README.md').pipe(fs.createWriteStream('./README.md')) +export default defineConfig(() => { + return { + entryPoints: ['src/index.ts'], + format: ['esm', 'cjs'], + skipNodeModulesBundle: true, + platform: 'node', + splitting: false, + minify: true, + sourcemap: false, + clean: true, + dts: false, + define: { + 'process.env.NODE_ENV': '"production"', + __TEST__: 'false', + }, + banner: { + js: `/**\n * name: ${pkg.name}\n * version: ${pkg.version}\n */`, + }, + } +}) diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 0000000..6ceaa32 --- /dev/null +++ b/src/App.vue @@ -0,0 +1,8 @@ + + + diff --git a/src/components/HelloWorld.vue b/src/components/HelloWorld.vue new file mode 100644 index 0000000..7b25f3f --- /dev/null +++ b/src/components/HelloWorld.vue @@ -0,0 +1,38 @@ + + + + + diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..e455900 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,5 @@ +import {createApp} from 'vue' +import App from './App.vue' +import router from './router' + +createApp(App).use(router).mount('#app') diff --git a/src/router.ts b/src/router.ts new file mode 100644 index 0000000..7b56ddf --- /dev/null +++ b/src/router.ts @@ -0,0 +1,48 @@ +import {createRouter, createWebHistory} from 'vue-router' +// @ts-ignore + +const routes = [ + { + path: '/', + name: '/test', + component: () => import('./views/test.vue') + }, + { + path: '/about', + name: '/about', + component: () => import('./views/about/index.vue'), + meta: { + title: '关于我们', + keywords: '关键词3, 关键词4', + description: '关于我们描述' + } + } +] +// console.log(routes) +// 配置路由 +const router = createRouter({ + history: createWebHistory(), + //history: createWebHashHistory(), + routes: routes +}) + +router.afterEach((to, from, next) => { + const {title, keywords, description} = to.meta; + if (title) { + document.title = title; + } + if (keywords) { + const metaKeywords = document.querySelector('meta[name="keywords"]'); + if (metaKeywords) { + metaKeywords.content = keywords; + } + } + if (description) { + const metaDescription = document.querySelector('meta[name="description"]'); + if (metaDescription) { + metaDescription.content = description; + } + } + next() +}) +export default router diff --git a/src/views/about/index.vue b/src/views/about/index.vue new file mode 100644 index 0000000..446cb11 --- /dev/null +++ b/src/views/about/index.vue @@ -0,0 +1,7 @@ + + + diff --git a/src/views/test.vue b/src/views/test.vue new file mode 100644 index 0000000..3d9a067 --- /dev/null +++ b/src/views/test.vue @@ -0,0 +1,8 @@ + + diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..1e213da --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1,12 @@ +/// +declare module '*.vue' { + import type { ComponentOptions } from 'vue' + const Component: ComponentOptions + export default Component +} + +declare module '*.md' { + import type { ComponentOptions } from 'vue' + const Component: ComponentOptions + export default Component +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..b692355 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..42872c5 --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..2b2172a --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,12 @@ +import {defineConfig} from 'vite' +import vue from '@vitejs/plugin-vue' +//import seoPrerender from 'vite-plugin-seo-prerender' +import seoPrerender from './packages/src' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + vue(), + seoPrerender({routes: ['/about']}) + ] +}) -- GitLab