- 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
105 lines
2.7 KiB
JavaScript
105 lines
2.7 KiB
JavaScript
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;
|