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,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;
|
||||
Reference in New Issue
Block a user