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
+104
View File
@@ -0,0 +1,104 @@
const express = require('express');
const router = express.Router();
const searchEngine = require('../services/search-engine');
const contentService = require('../services/content');
const { loadSiteConfig } = require('../config');
function getRecommendedSearches() {
const config = loadSiteConfig();
const taxonomy = contentService.getTaxonomy();
const dbService = require('../services/db');
const dynamicPills = [];
// 1. Add categories as discovery pills
for (const cat of taxonomy.categories) {
dynamicPills.push({
label: `category:${cat.name.toLowerCase()}`,
query: `category:${cat.name.toLowerCase()}`,
count: cat.count
});
}
// 2. Add top tags as discovery pills
for (const tag of taxonomy.tags.slice(0, 6)) {
dynamicPills.push({
label: `tag:${tag.name.toLowerCase()}`,
query: `tag:${tag.name.toLowerCase()}`,
count: tag.count
});
}
// 3. Add top trending searches from SQLite database
const trendingDb = dbService.getTopSearchRecommendations(4);
const customPills = [];
for (const item of trendingDb) {
if (!dynamicPills.some(p => p.query === item.query)) {
customPills.push({
label: item.label,
query: item.query
});
}
}
// 4. Add any static curated pills from config
const staticConfig = (config.search && config.search.recommended) || [];
for (const item of staticConfig) {
if (!dynamicPills.some(p => p.query === item.query) && !customPills.some(p => p.query === item.query)) {
customPills.push(item);
}
}
return {
dynamicPills,
customPills
};
}
router.get('/search', (req, res) => {
const query = (req.query.q || '').trim();
const recommended = getRecommendedSearches();
let searchData = {
query: { filters: {}, phrases: [], keywords: [], raw: '' },
results: [],
total: 0
};
if (query) {
searchData = searchEngine.search(query);
}
res.render('pages/search', {
title: query ? `Search: ${query}` : 'Search',
query,
searchData,
recommended
});
});
// JSON API endpoint for client-side search autocomplete / live search
router.get('/api/search', (req, res) => {
const query = (req.query.q || '').trim();
if (!query) {
return res.json({ query: null, results: [], total: 0 });
}
const searchData = searchEngine.search(query);
res.json({
query: searchData.query,
total: searchData.total,
results: searchData.results.map(r => ({
slug: r.post.slug,
lang: r.post.lang,
title: r.post.title,
date: r.post.date,
category: r.post.category,
tags: r.post.tags,
score: r.score,
snippet: r.snippet
}))
});
});
module.exports = router;