Files
haopengzhan 95a7dcb93b feat(admin): add ephemeral staging DB, read-only posts overview, and dynamic search
- 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
2026-09-16 23:09:14 -07:00

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;