- Editorial web platform built with Node.js, Express, EJS, and Vanilla CSS - Markdown content engine with YAML frontmatter, bilingual variants, and hot-reload watcher - Unified SQLite with FTS5 search indexing, query logging, and schema filters - Centered editorial post layout with margin-docked left TOC and mobile bottom sheet - WordPress-grade comment system with moderation dashboard and bulk operations under /guanli - Complete SEO suite (JSON-LD structured data, RSS/Atom feeds, XML sitemap, robots.txt) - CI/CD workflows for linting, security audits (Gitleaks), automated tests, and Docker releases
71 lines
2.2 KiB
JavaScript
71 lines
2.2 KiB
JavaScript
const crypto = require('crypto');
|
|
|
|
class SuperPageCache {
|
|
constructor() {
|
|
this.cache = new Map(); // path -> { body, contentType, etag, createdAt }
|
|
this.ttlMs = 10 * 60 * 1000; // 10 minutes default memory cache
|
|
}
|
|
|
|
middleware() {
|
|
return (req, res, next) => {
|
|
// Only cache GET requests that are not admin, guanli, or API routes
|
|
if (req.method !== 'GET' || req.path.startsWith('/guanli') || req.path.startsWith('/admin') || req.path.startsWith('/api')) {
|
|
return next();
|
|
}
|
|
|
|
const cacheKey = req.originalUrl || req.url;
|
|
const cached = this.cache.get(cacheKey);
|
|
|
|
if (cached && (Date.now() - cached.createdAt < this.ttlMs)) {
|
|
// Check conditional ETag
|
|
if (req.headers['if-none-match'] === cached.etag) {
|
|
res.setHeader('X-Cache', 'HIT-304');
|
|
return res.status(304).end();
|
|
}
|
|
|
|
res.setHeader('X-Cache', 'HIT');
|
|
res.setHeader('ETag', cached.etag);
|
|
res.setHeader('Content-Type', cached.contentType || 'text/html; charset=utf-8');
|
|
res.setHeader('Cache-Control', 'no-cache, must-revalidate');
|
|
return res.send(cached.body);
|
|
}
|
|
|
|
// Intercept res.send to populate cache
|
|
const originalSend = res.send.bind(res);
|
|
res.send = (body) => {
|
|
if (res.statusCode === 200 && typeof body === 'string') {
|
|
const etag = `W/"${crypto.createHash('md5').update(body).digest('hex').slice(0, 16)}"`;
|
|
this.cache.set(cacheKey, {
|
|
body,
|
|
contentType: res.getHeader('Content-Type') || 'text/html; charset=utf-8',
|
|
etag,
|
|
createdAt: Date.now()
|
|
});
|
|
res.setHeader('X-Cache', 'MISS');
|
|
res.setHeader('ETag', etag);
|
|
res.setHeader('Cache-Control', 'no-cache, must-revalidate');
|
|
}
|
|
return originalSend(body);
|
|
};
|
|
|
|
next();
|
|
};
|
|
}
|
|
|
|
clear() {
|
|
const size = this.cache.size;
|
|
this.cache.clear();
|
|
console.log(`[SuperPageCache] Flushed ${size} cached pages.`);
|
|
}
|
|
|
|
invalidate(urlPattern) {
|
|
for (const key of this.cache.keys()) {
|
|
if (key.includes(urlPattern)) {
|
|
this.cache.delete(key);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = new SuperPageCache();
|