const express = require('express'); const path = require('path'); const fs = require('fs'); const { CONTENT_DIR, loadSiteConfig } = require('./config'); const contentService = require('./services/content'); const commentService = require('./services/comments'); const pluginManager = require('./services/plugin-manager'); const watcherService = require('./services/watcher'); const seoMiddleware = require('./middleware/seo'); const pageCache = require('./middleware/cache'); const { notFoundHandler, serverErrorHandler } = require('./middleware/error-handler'); // Route modules const homeRouter = require('./routes/home'); const blogsRouter = require('./routes/blogs'); const searchRouter = require('./routes/search'); const feedRouter = require('./routes/feed'); const sitemapRouter = require('./routes/sitemap'); const commentsRouter = require('./routes/comments'); const postRouter = require('./routes/post'); const pagesRouter = require('./routes/pages'); const adminRouter = require('./routes/admin'); const app = express(); const PORT = process.env.PORT || 3000; // View engine setup app.set('view engine', 'ejs'); app.set('views', path.join(__dirname, '../views')); // Parse JSON & URL-encoded request bodies app.use(express.urlencoded({ extended: true })); app.use(express.json()); // Load site configuration const siteConfig = loadSiteConfig(); // Serve built-in skeleton static assets (CSS, JS, fonts) app.use(express.static(path.join(__dirname, '../public'))); // Serve Persistent Volume static directories const pvStaticDir = path.join(CONTENT_DIR, 'static'); if (fs.existsSync(pvStaticDir)) { app.use(express.static(pvStaticDir)); } // Serve PV custom CSS if present const pvCssDir = path.join(CONTENT_DIR, 'css'); if (fs.existsSync(pvCssDir)) { app.use('/css/custom', express.static(pvCssDir)); } // Serve post assets (e.g. cover images) at /posts-assets//... const pvPostsDir = path.join(CONTENT_DIR, 'posts'); if (fs.existsSync(pvPostsDir)) { app.use('/posts-assets', express.static(pvPostsDir)); } // Initialize services commentService.init(CONTENT_DIR); pluginManager.loadPlugins(CONTENT_DIR, app, siteConfig); contentService.init(CONTENT_DIR); watcherService.start(CONTENT_DIR, app); // Inject SEO locals & plugin hook helper app.use(seoMiddleware(siteConfig)); app.use((req, res, next) => { res.locals.pluginHook = (hookName, extraContext = {}) => { return pluginManager.renderHook(hookName, { req, res, site: res.locals.site, ...extraContext }); }; next(); }); // Super Page Cache micro-caching app.use(pageCache.middleware()); // Mount routes in order of specificity app.use('/', adminRouter); app.use('/', homeRouter); app.use('/', blogsRouter); app.use('/', searchRouter); app.use('/', feedRouter); app.use('/', sitemapRouter); app.use('/', commentsRouter); // Specific blog post routes (/:slug, /:slug/:lang) app.use('/', postRouter); // Fallback standalone pages (/:pageSlug from pages/) app.use('/', pagesRouter); // Error handlers app.use(notFoundHandler); app.use(serverErrorHandler); if (require.main === module) { const server = app.listen(PORT, () => { console.log(`=========================================`); console.log(` Pengzhan.dev Web Server running at:`); console.log(` http://localhost:${PORT}`); console.log(` Content directory: ${CONTENT_DIR}`); console.log(`=========================================`); }); process.on('SIGTERM', () => { console.log('SIGTERM signal received: closing HTTP server'); watcherService.close(); server.close(() => { console.log('HTTP server closed'); }); }); } module.exports = app;