175 lines
5.4 KiB
JavaScript
175 lines
5.4 KiB
JavaScript
/**
|
||
* SEO中间件
|
||
* 用于在Node.js服务器中检测爬虫并返回静态HTML
|
||
*/
|
||
|
||
const fs = require('fs')
|
||
const path = require('path')
|
||
const CrawlerDetector = require('./crawler-detector')
|
||
|
||
class SEOMiddleware {
|
||
constructor(options = {}) {
|
||
this.detector = new CrawlerDetector()
|
||
this.templateDir = options.templateDir || path.join(__dirname, '../public/seo-templates')
|
||
this.defaultTemplate = options.defaultTemplate || 'index.html'
|
||
this.fallbackToSPA = options.fallbackToSPA !== false
|
||
this.debug = options.debug || false
|
||
|
||
// 路由到模板的映射(与useSEO.js保持一致)
|
||
this.routeTemplateMap = {
|
||
'/': 'index.html',
|
||
'/agent': 'agent.html',
|
||
'/help': 'help.html',
|
||
'/help/guide': 'help-guide.html',
|
||
'/example': 'example.html',
|
||
'/service': 'service.html',
|
||
'/inquire/riskassessment': 'inquire-riskassessment.html',
|
||
'/inquire/companyinfo': 'inquire-companyinfo.html',
|
||
'/inquire/marriage': 'inquire-marriage.html',
|
||
'/promote': 'promote.html'
|
||
}
|
||
|
||
// 初始化模板缓存
|
||
this.templateCache = new Map()
|
||
this.cacheTemplates()
|
||
}
|
||
|
||
/**
|
||
* 缓存所有模板文件
|
||
*/
|
||
cacheTemplates() {
|
||
try {
|
||
if (!fs.existsSync(this.templateDir)) {
|
||
console.warn(`[SEOMiddleware] 模板目录不存在: ${this.templateDir}`)
|
||
return
|
||
}
|
||
|
||
const files = fs.readdirSync(this.templateDir)
|
||
files.forEach(file => {
|
||
const filePath = path.join(this.templateDir, file)
|
||
if (fs.statSync(filePath).isFile()) {
|
||
this.templateCache.set(file, fs.readFileSync(filePath, 'utf-8'))
|
||
if (this.debug) {
|
||
console.log(`[SEOMiddleware] 已缓存模板: ${file}`)
|
||
}
|
||
}
|
||
})
|
||
|
||
console.log(`[SEOMiddleware] 已缓存 ${this.templateCache.size} 个模板文件`)
|
||
} catch (error) {
|
||
console.error('[SEOMiddleware] 缓存模板失败:', error)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取对应的模板文件名
|
||
* @param {String} path - 请求路径
|
||
* @returns {String} 模板文件名
|
||
*/
|
||
getTemplatePath(requestPath) {
|
||
// 完全匹配
|
||
if (this.routeTemplateMap[requestPath]) {
|
||
return this.routeTemplateMap[requestPath]
|
||
}
|
||
|
||
// 模糊匹配(处理动态路由)
|
||
const matchedKey = Object.keys(this.routeTemplateMap).find(route => {
|
||
return requestPath.startsWith(route)
|
||
})
|
||
|
||
return matchedKey ? this.routeTemplateMap[matchedKey] : this.defaultTemplate
|
||
}
|
||
|
||
/**
|
||
* 获取模板内容
|
||
* @param {String} templateName - 模板文件名
|
||
* @returns {String|null} 模板内容
|
||
*/
|
||
getTemplate(templateName) {
|
||
// 首先尝试缓存
|
||
let content = this.templateCache.get(templateName)
|
||
|
||
// 如果缓存中没有,尝试从磁盘读取
|
||
if (!content) {
|
||
try {
|
||
const filePath = path.join(this.templateDir, templateName)
|
||
if (fs.existsSync(filePath)) {
|
||
content = fs.readFileSync(filePath, 'utf-8')
|
||
this.templateCache.set(templateName, content)
|
||
}
|
||
} catch (error) {
|
||
console.error(`[SEOMiddleware] 读取模板失败: ${templateName}`, error)
|
||
}
|
||
}
|
||
|
||
return content || null
|
||
}
|
||
|
||
/**
|
||
* Express中间件
|
||
*/
|
||
express() {
|
||
return (req, res, next) => {
|
||
// 检测是否为爬虫
|
||
if (this.detector.isCrawler(req)) {
|
||
const templateName = this.getTemplatePath(req.path)
|
||
const template = this.getTemplate(templateName)
|
||
|
||
if (template) {
|
||
// 设置响应头
|
||
res.setHeader('Content-Type', 'text/html; charset=utf-8')
|
||
res.setHeader('X-SEOMiddleware', 'prerendered')
|
||
|
||
// 返回静态HTML
|
||
if (this.debug) {
|
||
console.log(`[SEOMiddleware] 返回SEO模板: ${templateName} for ${req.path}`)
|
||
}
|
||
|
||
return res.send(template)
|
||
}
|
||
}
|
||
|
||
// 不是爬虫或模板不存在,继续处理SPA
|
||
next()
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Koa中间件
|
||
*/
|
||
koa() {
|
||
return async (ctx, next) => {
|
||
// 检测是否为爬虫
|
||
if (this.detector.isCrawler(ctx.req)) {
|
||
const templateName = this.getTemplatePath(ctx.path)
|
||
const template = this.getTemplate(templateName)
|
||
|
||
if (template) {
|
||
ctx.type = 'text/html; charset=utf-8'
|
||
ctx.set('X-SEOMiddleware', 'prerendered')
|
||
|
||
if (this.debug) {
|
||
console.log(`[SEOMiddleware] 返回SEO模板: ${templateName} for ${ctx.path}`)
|
||
}
|
||
|
||
ctx.body = template
|
||
return
|
||
}
|
||
}
|
||
|
||
await next()
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 重新加载模板缓存
|
||
*/
|
||
reloadCache() {
|
||
this.templateCache.clear()
|
||
this.cacheTemplates()
|
||
console.log('[SEOMiddleware] 模板缓存已重新加载')
|
||
}
|
||
}
|
||
|
||
module.exports = SEOMiddleware
|