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,137 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const express = require('express');
|
||||
|
||||
class PluginManager {
|
||||
constructor() {
|
||||
this.plugins = new Map();
|
||||
this.pluginStaticRouters = [];
|
||||
}
|
||||
|
||||
loadPlugins(contentDir, app, siteConfig) {
|
||||
const pluginsDir = path.join(contentDir, 'plugins');
|
||||
this.plugins.clear();
|
||||
|
||||
if (!fs.existsSync(pluginsDir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entries = fs.readdirSync(pluginsDir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
const pluginName = entry.name;
|
||||
const pluginPath = path.join(pluginsDir, pluginName);
|
||||
const manifestPath = path.join(pluginPath, 'plugin.json');
|
||||
const entryScriptPath = path.join(pluginPath, 'index.js');
|
||||
|
||||
if (!fs.existsSync(entryScriptPath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
let manifest = { name: pluginName, enabled: true };
|
||||
if (fs.existsSync(manifestPath)) {
|
||||
manifest = { ...manifest, ...JSON.parse(fs.readFileSync(manifestPath, 'utf8')) };
|
||||
}
|
||||
|
||||
// Check if enabled in site config (default true if not explicitly false)
|
||||
const configEntry = siteConfig.plugins && siteConfig.plugins[pluginName];
|
||||
if (configEntry === false || (configEntry && configEntry.enabled === false)) {
|
||||
console.log(`[PluginManager] Plugin ${pluginName} is disabled in config`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Clear module cache for hot reload
|
||||
delete require.cache[require.resolve(entryScriptPath)];
|
||||
const pluginModule = require(entryScriptPath);
|
||||
|
||||
const pluginInstance = {
|
||||
name: pluginName,
|
||||
manifest,
|
||||
module: pluginModule,
|
||||
config: (configEntry && typeof configEntry === 'object') ? configEntry : {}
|
||||
};
|
||||
|
||||
// Mount public assets if directory exists
|
||||
const publicDir = path.join(pluginPath, 'public');
|
||||
if (fs.existsSync(publicDir) && app) {
|
||||
app.use(`/plugins/${pluginName}`, express.static(publicDir));
|
||||
}
|
||||
|
||||
// Run onInit hook
|
||||
if (typeof pluginModule.onInit === 'function' && app) {
|
||||
try {
|
||||
pluginModule.onInit({
|
||||
app,
|
||||
config: siteConfig,
|
||||
pluginConfig: pluginInstance.config
|
||||
});
|
||||
} catch (initErr) {
|
||||
console.error(`[PluginManager] Error in onInit for plugin ${pluginName}:`, initErr);
|
||||
}
|
||||
}
|
||||
|
||||
this.plugins.set(pluginName, pluginInstance);
|
||||
console.log(`[PluginManager] Loaded plugin: ${pluginName}`);
|
||||
} catch (err) {
|
||||
console.error(`[PluginManager] Failed to load plugin ${pluginName}:`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
applyMarkdownHooks(markdownIt) {
|
||||
for (const [name, plugin] of this.plugins) {
|
||||
if (typeof plugin.module.onMarkdownInit === 'function') {
|
||||
try {
|
||||
plugin.module.onMarkdownInit({ markdownIt, pluginConfig: plugin.config });
|
||||
} catch (err) {
|
||||
console.error(`[PluginManager] Error in onMarkdownInit for ${name}:`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
applyPostHooks(post) {
|
||||
for (const [name, plugin] of this.plugins) {
|
||||
if (typeof plugin.module.onPostLoad === 'function') {
|
||||
try {
|
||||
plugin.module.onPostLoad({ post, pluginConfig: plugin.config });
|
||||
} catch (err) {
|
||||
console.error(`[PluginManager] Error in onPostLoad for ${name}:`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async triggerCommentCreated(comment, post) {
|
||||
for (const [name, plugin] of this.plugins) {
|
||||
if (typeof plugin.module.onCommentCreated === 'function') {
|
||||
try {
|
||||
await plugin.module.onCommentCreated({ comment, post, pluginConfig: plugin.config });
|
||||
} catch (err) {
|
||||
console.error(`[PluginManager] Error in onCommentCreated for ${name}:`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
renderHook(hookName, context) {
|
||||
let output = '';
|
||||
for (const [name, plugin] of this.plugins) {
|
||||
if (typeof plugin.module[hookName] === 'function') {
|
||||
try {
|
||||
const res = plugin.module[hookName]({ ...context, pluginConfig: plugin.config });
|
||||
if (res && typeof res === 'string') {
|
||||
output += '\n' + res;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`[PluginManager] Error in ${hookName} for ${name}:`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new PluginManager();
|
||||
Reference in New Issue
Block a user