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:
parent
a2d027c07a
commit
861f38d666
3 changed files with 93 additions and 31 deletions
43
public/404.html
Normal file
43
public/404.html
Normal 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>
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue