import express from 'express' import { readFile } from 'node:fs/promises' import { escapeHtml, escapeXml, injectSeoIntoHtml, normalizeSitemapPath } from '../helpers.js' import { DIST_DIR, DIST_IMAGES_DIR, PUBLIC_IMAGES_DIR, UPLOADS_DIR, INDEX_FILE, DEFAULT_SEO, } from '../config.js' import { state } from '../state.js' import { loadSiteContentFile } from '../data.js' import { DATA_FILE } from '../config.js' import { sanitizeRedirectRules } from '../study-helpers.js' export function register(app) { app.get('/robots.txt', async (_req, res) => { let content = state.cachedSiteContent if (!content) { try { const parsed = await loadSiteContentFile(DATA_FILE) content = parsed.siteContent } catch { content = {} } } const seo = content?.seo ?? DEFAULT_SEO const canonical = seo.canonicalUrl || DEFAULT_SEO.canonicalUrl const root = canonical.endsWith('/') ? canonical.slice(0, -1) : canonical res.type('text/plain').send( ['User-agent: *', 'Allow: /', `Sitemap: ${root}/sitemap.xml`].join('\n'), ) }) app.get('/sitemap.xml', async (_req, res) => { let content = state.cachedSiteContent if (!content) { try { const parsed = await loadSiteContentFile(DATA_FILE) content = parsed.siteContent } catch { content = {} } } const seo = content?.seo ?? DEFAULT_SEO const canonical = seo.canonicalUrl || DEFAULT_SEO.canonicalUrl const root = canonical.endsWith('/') ? canonical.slice(0, -1) : canonical const paths = Array.isArray(seo.sitemapPaths) && seo.sitemapPaths.length > 0 ? seo.sitemapPaths : DEFAULT_SEO.sitemapPaths const urls = paths .map(item => normalizeSitemapPath(item)) .filter(Boolean) .map(item => `${root}${item}`) const xml = [ '', '', ...urls.map(url => ` ${escapeXml(url)}`), '', ].join('\n') res.type('application/xml').send(xml) }) // Redirect rules middleware app.use((req, res, next) => { const rules = sanitizeRedirectRules(state.cachedSiteContent?.redirects) const match = rules.find(rule => rule.path === req.path) if (!match) { next(); return } res.redirect(match.statusCode === 302 ? 302 : 301, match.target) }) // Static files app.use('/images', express.static(DIST_IMAGES_DIR)) app.use('/images', express.static(PUBLIC_IMAGES_DIR)) app.use('/uploads', express.static(UPLOADS_DIR)) // Social share stub for questions app.get('/questions/share/:id', (req, res) => { const id = req.params.id if (!id || !/^[\w-]{1,120}$/.test(id)) { res.redirect(302, '/questions'); return } const sourceQuestions = state.draftQuestions ?? state.questions const question = sourceQuestions.find(q => q.id === id && q.isApproved === true && q.answer) if (!question) { res.redirect(302, '/questions'); return } const BASE = 'https://versebyversewithnate.us' const canonicalUrl = `${BASE}/questions#qa-${encodeURIComponent(id)}` const shareUrl = `${BASE}/questions/share/${encodeURIComponent(id)}` const ogTitle = escapeHtml(question.question.length > 100 ? `${question.question.slice(0, 97)}…` : question.question) const answerSnippet = question.answer.replace(/\n+/g, ' ').trim() const ogDescription = escapeHtml(answerSnippet.length > 200 ? `${answerSnippet.slice(0, 197)}…` : answerSnippet) const ogImage = `${BASE}/images/banner.png` res.type('html').send(` ${ogTitle} — Verse by Verse with Nate `) }) // SPA static files and fallback app.use(express.static(DIST_DIR)) app.use(async (_req, res) => { try { const html = await readFile(INDEX_FILE, 'utf8') res.type('html').send(injectSeoIntoHtml(html, state.cachedSiteContent)) } catch { res.status(503).send('Frontend build not found. Run "npm run build" first.') } }) }