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
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
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();
|
||||
@@ -0,0 +1,23 @@
|
||||
function notFoundHandler(req, res, next) {
|
||||
res.status(404);
|
||||
res.render('pages/error', {
|
||||
title: '404 - Page Not Found',
|
||||
errorCode: 404,
|
||||
errorMessage: "The page you are looking for doesn't exist or has moved."
|
||||
});
|
||||
}
|
||||
|
||||
function serverErrorHandler(err, req, res, next) {
|
||||
console.error('[ServerError]', err);
|
||||
res.status(500);
|
||||
res.render('pages/error', {
|
||||
title: '500 - Server Error',
|
||||
errorCode: 500,
|
||||
errorMessage: "An unexpected error occurred."
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
notFoundHandler,
|
||||
serverErrorHandler
|
||||
};
|
||||
@@ -0,0 +1,106 @@
|
||||
function generateJsonLd(site, seo, req) {
|
||||
const siteUrl = site.url || 'https://pengzhan.dev';
|
||||
const person = {
|
||||
'@type': 'Person',
|
||||
'@id': `${siteUrl}/#person`,
|
||||
name: (site.author && site.author.name) || site.title,
|
||||
url: siteUrl,
|
||||
jobTitle: (site.author && site.author.subtitle) || 'Software Engineer',
|
||||
sameAs: (site.social || []).map(s => s.url).filter(Boolean)
|
||||
};
|
||||
|
||||
const website = {
|
||||
'@type': 'WebSite',
|
||||
'@id': `${siteUrl}/#website`,
|
||||
url: siteUrl,
|
||||
name: site.title,
|
||||
description: site.tagline,
|
||||
publisher: { '@id': `${siteUrl}/#person` },
|
||||
potentialAction: [
|
||||
{
|
||||
'@type': 'SearchAction',
|
||||
target: {
|
||||
'@type': 'EntryPoint',
|
||||
urlTemplate: `${siteUrl}/search?q={search_term_string}`
|
||||
},
|
||||
'query-input': 'required name=search_term_string'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const graph = [person, website];
|
||||
|
||||
if (seo.type === 'article') {
|
||||
const blogPosting = {
|
||||
'@type': 'BlogPosting',
|
||||
'@id': `${seo.canonicalUrl}#article`,
|
||||
isPartOf: { '@id': `${siteUrl}/#website` },
|
||||
author: { '@id': `${siteUrl}/#person` },
|
||||
headline: seo.title,
|
||||
datePublished: seo.publishedTime || new Date().toISOString(),
|
||||
dateModified: seo.modifiedTime || seo.publishedTime || new Date().toISOString(),
|
||||
mainEntityOfPage: seo.canonicalUrl,
|
||||
description: seo.description,
|
||||
image: seo.ogImage,
|
||||
articleSection: seo.category || 'Tech',
|
||||
keywords: Array.isArray(seo.tags) ? seo.tags.join(', ') : seo.tags
|
||||
};
|
||||
graph.push(blogPosting);
|
||||
}
|
||||
|
||||
// Breadcrumbs
|
||||
const breadcrumbItems = [
|
||||
{
|
||||
'@type': 'ListItem',
|
||||
position: 1,
|
||||
name: 'Home',
|
||||
item: siteUrl
|
||||
}
|
||||
];
|
||||
|
||||
if (req.path !== '/') {
|
||||
breadcrumbItems.push({
|
||||
'@type': 'ListItem',
|
||||
position: 2,
|
||||
name: seo.title,
|
||||
item: seo.canonicalUrl
|
||||
});
|
||||
}
|
||||
|
||||
graph.push({
|
||||
'@type': 'BreadcrumbList',
|
||||
'@id': `${seo.canonicalUrl}#breadcrumb`,
|
||||
itemListElement: breadcrumbItems
|
||||
});
|
||||
|
||||
return JSON.stringify({
|
||||
'@context': 'https://schema.org',
|
||||
'@graph': graph
|
||||
});
|
||||
}
|
||||
|
||||
function seoMiddleware(siteConfig) {
|
||||
return (req, res, next) => {
|
||||
res.locals.site = siteConfig.site;
|
||||
res.locals.navigation = siteConfig.navigation;
|
||||
res.locals.social = siteConfig.social;
|
||||
res.locals.currentPath = req.path;
|
||||
|
||||
// Helper to generate SEO metadata
|
||||
res.locals.seo = {
|
||||
title: siteConfig.site.title,
|
||||
description: siteConfig.site.tagline,
|
||||
canonicalUrl: `${siteConfig.site.url}${req.path}`,
|
||||
ogImage: `${siteConfig.site.url}/images/og-default.png`,
|
||||
type: 'website',
|
||||
tags: [],
|
||||
category: ''
|
||||
};
|
||||
|
||||
res.locals.getJsonLd = () => generateJsonLd(siteConfig.site, res.locals.seo, req);
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = seoMiddleware;
|
||||
Reference in New Issue
Block a user