feat: add /blog index route + friendly HTML 404 page

Previously /blog returned JSON 404 because only /blog/:slug was wired;
now app.get('/blog') serves the existing public/blog.html.

notFound middleware now detects browser GETs (Accept: text/html,
path not /api/*) and serves public/404.html; API clients keep JSON.
Added public/404.html styled to match the site theme.

Also cleaned up 11 pre-existing lint errors in src/server.js (unused
rate-limit/csrf imports, brace-style on the cache-control chain) while
editing the file.
This commit is contained in:
TheFlow 2026-04-17 14:46:49 +12:00
parent a2d027c07a
commit 861f38d666
3 changed files with 93 additions and 31 deletions

43
public/404.html Normal file
View file

@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Page Not Found | Tractatus AI Safety Framework</title>
<meta name="description" content="The page you were looking for does not exist.">
<meta name="robots" content="noindex,nofollow">
<link rel="icon" type="image/svg+xml" href="/favicon-new.svg">
<link rel="stylesheet" href="/css/tailwind.css?v=0.1.2.1776366945602">
<link rel="stylesheet" href="/css/tractatus-theme.min.css?v=0.1.2.1776366945602">
<style>
a:focus, button:focus { outline: 3px solid #3b82f6; outline-offset: 2px; }
a:focus:not(:focus-visible) { outline: none; }
a:focus-visible { outline: 3px solid #3b82f6; outline-offset: 2px; }
</style>
</head>
<body class="bg-gray-50 min-h-screen flex flex-col">
<!-- Navigation (injected by navbar.js) -->
<script src="/js/components/navbar.js?v=0.1.2.1776366945602"></script>
<main class="flex-1 flex items-center justify-center px-4 py-16">
<div class="max-w-xl text-center">
<p class="text-sm font-semibold text-indigo-700 uppercase tracking-wider mb-3">404</p>
<h1 class="text-4xl sm:text-5xl font-bold text-gray-900 mb-4">Page not found</h1>
<p class="text-lg text-gray-600 mb-8">
The page you were looking for does not exist, may have moved, or may never have been published.
</p>
<div class="flex items-center justify-center gap-4 flex-wrap">
<a href="/" class="inline-flex items-center bg-blue-700 text-white px-6 py-3 rounded-lg font-semibold hover:bg-blue-800 transition shadow-md border-2 border-blue-900">
Go to home
</a>
<a href="/blog" class="inline-flex items-center text-gray-700 hover:text-indigo-700 px-4 py-3 rounded-lg font-medium border border-gray-300 hover:border-indigo-300 transition">
Browse the blog
</a>
</div>
</div>
</main>
<script src="/js/components/footer.js?v=0.1.2.1776366945602"></script>
</body>
</html>

View file

@ -6,8 +6,31 @@ const logger = require('../utils/logger.util');
/**
* 404 Not Found handler
*
* Browser requests (Accept: text/html, GET) get the friendly 404.html page.
* API clients (Accept: application/json, non-GET, /api/* paths) get JSON.
*/
function notFound(req, res, _next) {
const path = require('path');
const isApiPath = req.originalUrl.startsWith('/api/');
const wantsHtml = req.method === 'GET'
&& !isApiPath
&& req.accepts(['html', 'json']) === 'html';
if (wantsHtml) {
return res.status(404).sendFile(
path.resolve(__dirname, '..', '..', 'public', '404.html'),
err => {
if (err) {
res.status(404).json({
error: 'Not Found',
message: `Route ${req.method} ${req.originalUrl} not found`
});
}
}
);
}
res.status(404).json({
error: 'Not Found',
message: `Route ${req.method} ${req.originalUrl} not found`
@ -17,7 +40,7 @@ function notFound(req, res, _next) {
/**
* Global error handler
*/
function errorHandler(err, req, res, next) {
function errorHandler(err, req, res, _next) {
// Log error
logger.error('Error:', {
message: err.message,

View file

@ -8,7 +8,6 @@ require('dotenv').config();
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const rateLimit = require('express-rate-limit');
const cookieParser = require('cookie-parser');
// const csrf = require('csurf'); // Disabled - deprecated package, will implement modern solution in Phase 3
@ -20,9 +19,9 @@ const { notFound, errorHandler } = require('./middleware/error.middleware');
// Security middleware (Quick Wins)
const { securityHeadersMiddleware } = require('./middleware/security-headers.middleware');
const { publicRateLimiter, formRateLimiter, authRateLimiter } = require('./middleware/rate-limit.middleware');
const { publicRateLimiter } = require('./middleware/rate-limit.middleware');
const { sanitizeErrorResponse, sanitizeResponseData } = require('./middleware/response-sanitization.middleware');
const { setCsrfToken, csrfProtection, getCsrfToken } = require('./middleware/csrf-protection.middleware');
const { setCsrfToken, getCsrfToken } = require('./middleware/csrf-protection.middleware');
// Create Express app
const app = express();
@ -84,50 +83,47 @@ app.use(publicRateLimiter);
app.use((req, res, next) => {
const path = req.path;
// Version manifest and service worker: No cache (always fetch fresh)
if (path === '/version.json' || path === '/service-worker.js') {
// Version manifest and service worker: never cache (always fetch fresh)
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
res.setHeader('Pragma', 'no-cache');
res.setHeader('Expires', '0');
}
// HTML files: No cache (always fetch fresh - users must see updates immediately)
else if (path.endsWith('.html') || path === '/') {
} else if (path.endsWith('.html') || path === '/') {
// HTML: never cache — users must see updates immediately
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0');
res.setHeader('Pragma', 'no-cache');
}
// Admin JS/HTML files: NEVER cache (always fetch fresh)
else if (path.startsWith('/js/admin/') || path.startsWith('/admin/')) {
} else if (path.startsWith('/js/admin/') || path.startsWith('/admin/')) {
// Admin JS/HTML: never cache (always fetch fresh)
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
res.setHeader('Pragma', 'no-cache');
res.setHeader('Expires', '0');
}
// CSS and JS files: Short cache for active development
// With versioned URLs (?v=timestamp), browsers will fetch new versions when HTML updates
else if (path.endsWith('.css') || path.endsWith('.js')) {
res.setHeader('Cache-Control', 'public, max-age=3600'); // 1 hour - reasonable for active development
}
// Images and fonts: Long cache
else if (path.match(/\.(jpg|jpeg|png|gif|svg|ico|woff|woff2|ttf|eot)$/)) {
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable'); // 1 year
}
// PWA manifest: Medium cache
else if (path === '/manifest.json') {
res.setHeader('Cache-Control', 'public, max-age=86400'); // 1 day
}
// API responses: NEVER cache (security middleware sets this too, but catch-all below would override)
else if (path.startsWith('/api/')) {
} else if (path.endsWith('.css') || path.endsWith('.js')) {
// CSS/JS: 1 hour — versioned URLs force fresh fetch when HTML updates
res.setHeader('Cache-Control', 'public, max-age=3600');
} else if (path.match(/\.(jpg|jpeg|png|gif|svg|ico|woff|woff2|ttf|eot)$/)) {
// Images/fonts: 1 year, immutable
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
} else if (path === '/manifest.json') {
// PWA manifest: 1 day
res.setHeader('Cache-Control', 'public, max-age=86400');
} else if (path.startsWith('/api/')) {
// API: never cache (security middleware sets this too; catch-all below would otherwise override)
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
res.setHeader('Pragma', 'no-cache');
res.setHeader('Expires', '0');
}
// Everything else (static assets without extensions): Short cache
else {
res.setHeader('Cache-Control', 'public, max-age=3600'); // 1 hour
} else {
// Static assets without extension: 1 hour
res.setHeader('Cache-Control', 'public, max-age=3600');
}
next();
});
// Blog index: /blog → blog.html (before /blog/:slug so "/blog" doesn't get treated as a slug)
app.get('/blog', (req, res) => {
res.sendFile('blog.html', { root: 'public' });
});
// Blog post URL rewriting: /blog/:slug → /blog-post.html?slug=:slug
// This provides cleaner URLs for blog posts (SEO-friendly)
app.get('/blog/:slug', (req, res) => {