- Implement ephemeral staging mode (APP_ENV=staging) to isolate test/dev DB writes in /tmp - Add read-only posts management dashboard under /guanli/posts with live links and comment counters - Add hot re-indexing endpoint (/guanli/reindex) to refresh Markdown without restarting server - Replace static recommended search config with dynamic frequency-based trending searches - Remove early scaffold mockup file (dropbox/zh.md) and clean legacy mock test queries - Add native Makefile with dev, dev-persistent, test, lint, and docker targets - Update test suite and documentation for development workflows
97 lines
2.4 KiB
JavaScript
97 lines
2.4 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 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 genuine trending searches by frequency from search_logs (result_count > 0)
|
|
const trendingDb = dbService.getTopSearchQueries(6);
|
|
const customPills = [];
|
|
for (const item of trendingDb) {
|
|
if (!dynamicPills.some(p => p.query.toLowerCase() === item.query.toLowerCase())) {
|
|
customPills.push({
|
|
label: item.label,
|
|
query: item.query,
|
|
frequency: item.frequency
|
|
});
|
|
}
|
|
}
|
|
|
|
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;
|