const db = require('./db'); const marked = require('marked'); const createDOMPurify = require('dompurify'); const { JSDOM } = require('jsdom'); const DOMPurify = createDOMPurify(new JSDOM('').window); const locks = require('./lib/locks'); function escapeHtml(s) { return String(s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, '''); } function ssrPage(title, contentHtml, metaDesc, extra = {}) { const siteName = db.getSetting('site_name') || 'RainWeb'; const siteDesc = db.getSetting('site_description') || '个人云管理平台'; const siteFavicon = db.getSetting('site_favicon') || ''; const color = db.getSetting('primary_color') || '#6750a4'; const baseUrl = extra.url || ''; // og:title 允许详情页覆盖(去「- 站点名」后缀,防社交卡片标题被截断) const ogTitle = escapeHtml(extra.ogTitle || (title + ' - ' + siteName)); const ogDesc = escapeHtml(metaDesc || siteDesc); const canonical = baseUrl ? `` : ''; // RSS 自发现:extra.rssUrl 传入对应内容源(博客 /feed.xml、论坛 /feed/forum.xml、版块 /feed/forum/c/:id.xml) const rssUrl = extra.rssUrl ? `` : ''; const jsonld = extra.jsonld || ''; // 详情页用 summary_large_image(配 og:image),其余页面 summary const twitterCard = extra.twitterCard || 'summary'; return `
论坛已设为私密,请登录后访问。
'); } const post = db.get( `SELECT fp.*, u.username as author_name, u.nickname, fc.name as category_name FROM forum_posts fp LEFT JOIN users u ON fp.author_id = u.id LEFT JOIN forum_categories fc ON fp.category_id = fc.id WHERE fp.id = ?`, [req.params.id]); if (!post) return res.status(404).send('帖子不存在'); const replies = db.all( `SELECT fr.*, u.username as author_name, u.nickname FROM forum_replies fr LEFT JOIN users u ON fr.author_id = u.id WHERE fr.post_id = ? ORDER BY fr.created_at ASC`, [req.params.id]); const body = renderLocksAware(post.content, post.use_markdown); const siteName = db.getSetting('site_name') || 'RainWeb'; const siteUrl = db.getSetting('site_url') || (req.protocol + '://' + req.get('host')); const baseDomain = siteUrl.replace(/\/$/, ''); const baseUrl = baseDomain + '/forum/' + post.id; const ogImage = baseDomain + '/og-default.png'; // 面包屑 schema(与页面可见面包屑「首页 / 论坛 / 标题」对应) const breadcrumbJsonld = JSON.stringify({ '@context': 'https://schema.org', '@type': 'BreadcrumbList', itemListElement: [ { '@type': 'ListItem', position: 1, name: '首页', item: baseDomain + '/' }, { '@type': 'ListItem', position: 2, name: '论坛', item: baseDomain + '/forum.html' }, { '@type': 'ListItem', position: 3, name: post.title, item: baseUrl } ] }).replace(/${breadcrumbJsonld} `; const repliesHtml = replies.map(r => `暂无回复
'}`, post.title, { url: baseUrl, ogType: 'article', ogImage, ogTitle: post.title, twitterCard: 'summary_large_image', jsonld: jsonldHtml, breadcrumb: '', rssUrl: baseDomain + '/feed/forum.xml' }); res.send(html); } // 论坛版块页 SSR(P5):/forum/c/:id——版块名/描述/公告 + 首屏帖子 + 面包屑 JSON-LD。 // 私密模式(forum_guest_visible!=='1')返回 404 + noindex(不对外索引)。 function categorySSR(req, res) { const privateNotice = () => { res.status(404); return res.send('论坛已设为私密,请登录后访问。
'); }; if (db.getSetting('forum_guest_visible') !== '1') return privateNotice(); const cat = db.get('SELECT * FROM forum_categories WHERE id = ?', [req.params.id]); if (!cat) return res.status(404).send('版块不存在'); const posts = db.all( `SELECT fp.*, u.username as author_name, u.nickname, (SELECT COUNT(*) FROM forum_replies WHERE post_id = fp.id) as reply_count FROM forum_posts fp LEFT JOIN users u ON fp.author_id = u.id WHERE fp.category_id = ? ORDER BY fp.is_pinned DESC, fp.created_at DESC LIMIT 20`, [cat.id]); const moderators = db.all( 'SELECT u.username FROM forum_moderators fm LEFT JOIN users u ON fm.user_id = u.id WHERE fm.category_id = ?', [cat.id]); const siteName = db.getSetting('site_name') || 'RainWeb'; const siteUrl = db.getSetting('site_url') || (req.protocol + '://' + req.get('host')); const baseDomain = siteUrl.replace(/\/$/, ''); const baseUrl = baseDomain + '/forum/c/' + cat.id; const ogImage = baseDomain + '/og-default.png'; // 面包屑 schema(首页 / 论坛 / 版块)+ 版块 CollectionPage/ItemList const breadcrumbJsonld = JSON.stringify({ '@context': 'https://schema.org', '@type': 'BreadcrumbList', itemListElement: [ { '@type': 'ListItem', position: 1, name: '首页', item: baseDomain + '/' }, { '@type': 'ListItem', position: 2, name: '论坛', item: baseDomain + '/forum.html' }, { '@type': 'ListItem', position: 3, name: cat.name, item: baseUrl } ] }).replace(/ ({ '@type': 'ListItem', position: i + 1, url: baseDomain + '/forum/' + p.id, name: p.title })) } }).replace(/${breadcrumbJsonld} `; const moderatorsHtml = moderators.length ? `版主:${moderators.map(m => escapeHtml(m.username)).join('、')}` : ''; const announcementHtml = cat.announcement ? `暂无帖子
'}`, cat.description || cat.name, { url: baseUrl, ogType: 'website', ogImage, ogTitle: cat.name, jsonld: jsonldHtml, breadcrumb: '', rssUrl: baseDomain + '/feed/forum/c/' + cat.id + '.xml' }); res.send(html); } // lastmod 规范化:'YYYY-MM-DD HH:MM:SS'(UTC)→ 'YYYY-MM-DD'(ISO 8601 日期,sitemap 协议要求) function toSitemapDate(ts) { const s = String(ts || ''); return s.slice(0, 10); } function sitemapXml(req, res) { const siteUrl = db.getSetting('site_url') || (req.protocol + '://' + req.get('host')); const baseUrl = siteUrl.replace(/\/$/, ''); // lastmod:updated_at 优先,无则 created_at(均格式化为 YYYY-MM-DD) const blogPosts = db.all('SELECT id, created_at, updated_at, title FROM blog_posts WHERE published = 1 ORDER BY created_at DESC'); // 论坛私密模式下不从 sitemap 输出论坛帖子/版块 URL(避免收录后 404) const forumVisible = db.getSetting('forum_guest_visible') === '1'; const forumPosts = forumVisible ? db.all('SELECT id, created_at, updated_at FROM forum_posts ORDER BY created_at DESC') : []; // forum_categories 无 created_at 列;版块 URL 无 lastmod(最近有帖子时帖子的 lastmod 已足够) const forumCats = forumVisible ? db.all('SELECT id FROM forum_categories ORDER BY sort_order ASC') : []; // 核心页:首页 + 博客列表 + 论坛列表 + 归档(无分页 URL);Google 忽略