/** * 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