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:
2026-09-16 23:08:53 -07:00
parent ac2c4c09ef
commit 8c0b04db0e
126 changed files with 8515 additions and 3984 deletions
+70
View File
@@ -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();
+23
View File
@@ -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
};
+106
View File
@@ -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;