Files
haopengzhan 8c0b04db0e feat: initial release of pengzhan.dev personal editorial blog platform
- 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
2026-09-16 23:08:53 -07:00

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();