webspresso 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -184,11 +184,134 @@ Creates and configures the Express app.
184
184
  - `true` or `undefined`: Use default secure configuration
185
185
  - `false`: Disable Helmet
186
186
  - `Object`: Custom Helmet configuration (merged with defaults)
187
+ - `middlewares` (optional): Named middleware registry for routes
187
188
 
188
- **Default Helmet Configuration:**
189
- - CSP disabled in development, enabled in production
190
- - HSTS, XSS protection, frame guard, and other security headers enabled
191
- - Tailwind CSS inline styles allowed in CSP
189
+ **Example with middlewares:**
190
+
191
+ ```javascript
192
+ const { createApp } = require('webspresso');
193
+
194
+ const { app } = createApp({
195
+ pagesDir: './pages',
196
+ viewsDir: './views',
197
+ middlewares: {
198
+ auth: (req, res, next) => {
199
+ if (!req.session?.user) {
200
+ return res.redirect('/login');
201
+ }
202
+ next();
203
+ },
204
+ admin: (req, res, next) => {
205
+ if (req.session?.user?.role !== 'admin') {
206
+ return res.status(403).send('Forbidden');
207
+ }
208
+ next();
209
+ },
210
+ rateLimit: require('express-rate-limit')({ windowMs: 60000, max: 100 })
211
+ }
212
+ });
213
+ ```
214
+
215
+ Then use in route configs by name:
216
+
217
+ ```javascript
218
+ // pages/admin/index.js
219
+ module.exports = {
220
+ middleware: ['auth', 'admin'], // Use named middlewares
221
+ load(req, ctx) { ... }
222
+ };
223
+
224
+ // pages/api/data.get.js
225
+ module.exports = {
226
+ middleware: ['auth', 'rateLimit'],
227
+ handler: (req, res) => res.json({ data: 'protected' })
228
+ };
229
+ ```
230
+
231
+ **Custom Error Pages:**
232
+
233
+ ```javascript
234
+ const { createApp } = require('webspresso');
235
+
236
+ const { app } = createApp({
237
+ pagesDir: './pages',
238
+ viewsDir: './views',
239
+ errorPages: {
240
+ // Option 1: Custom handler function
241
+ notFound: (req, res) => {
242
+ res.render('errors/404.njk', { url: req.url });
243
+ },
244
+
245
+ // Option 2: Template path (rendered with Nunjucks)
246
+ serverError: 'errors/500.njk'
247
+ }
248
+ });
249
+ ```
250
+
251
+ Error templates receive these variables:
252
+ - `404.njk`: `{ url, method }`
253
+ - `500.njk`: `{ error, status, isDev }`
254
+
255
+ **Asset Management:**
256
+
257
+ Configure asset handling with versioning and manifest support:
258
+
259
+ ```javascript
260
+ const { createApp } = require('webspresso');
261
+ const path = require('path');
262
+
263
+ const { app } = createApp({
264
+ pagesDir: './pages',
265
+ viewsDir: './views',
266
+ publicDir: './public',
267
+ assets: {
268
+ // Option 1: Simple versioning (cache busting)
269
+ version: '1.2.3', // or process.env.APP_VERSION
270
+
271
+ // Option 2: Manifest file (Vite, Webpack, etc.)
272
+ manifestPath: path.join(__dirname, 'public/.vite/manifest.json'),
273
+
274
+ // URL prefix for assets
275
+ prefix: '/static'
276
+ }
277
+ });
278
+ ```
279
+
280
+ Use asset helpers in templates:
281
+
282
+ ```njk
283
+ {# Using fsy helpers (auto-resolved) #}
284
+ <link rel="stylesheet" href="{{ fsy.asset('/css/style.css') }}">
285
+
286
+ {# Or generate full HTML tags #}
287
+ {{ fsy.css('/css/style.css') | safe }}
288
+ {{ fsy.js('/js/app.js', { defer: true, type: 'module' }) | safe }}
289
+ {{ fsy.img('/images/logo.png', 'Site Logo', { class: 'logo', loading: 'lazy' }) | safe }}
290
+ ```
291
+
292
+ Asset helpers available in `fsy`:
293
+ - `asset(path)` - Returns versioned/manifest-resolved URL
294
+ - `css(href, attrs)` - Generates `<link>` tag
295
+ - `js(src, attrs)` - Generates `<script>` tag
296
+ - `img(src, alt, attrs)` - Generates `<img>` tag
297
+
298
+ **Manifest Support:**
299
+
300
+ Works with Vite and Webpack manifest formats:
301
+
302
+ ```json
303
+ // Vite manifest format (.vite/manifest.json)
304
+ {
305
+ "css/style.css": { "file": "assets/style-abc123.css" },
306
+ "js/app.js": { "file": "assets/app-xyz789.js" }
307
+ }
308
+
309
+ // Webpack manifest format
310
+ {
311
+ "/css/style.css": "/dist/style.abc123.css",
312
+ "/js/app.js": "/dist/app.xyz789.js"
313
+ }
314
+ ```
192
315
 
193
316
  **Returns:** `{ app, nunjucksEnv }`
194
317
 
package/index.js CHANGED
@@ -12,7 +12,13 @@ const {
12
12
  createTranslator,
13
13
  detectLocale
14
14
  } = require('./src/file-router');
15
- const { createHelpers, utils } = require('./src/helpers');
15
+ const {
16
+ createHelpers,
17
+ utils,
18
+ AssetManager,
19
+ configureAssets,
20
+ getAssetManager
21
+ } = require('./src/helpers');
16
22
 
17
23
  module.exports = {
18
24
  // Main API
@@ -29,6 +35,11 @@ module.exports = {
29
35
 
30
36
  // Template helpers
31
37
  createHelpers,
32
- utils
38
+ utils,
39
+
40
+ // Asset management
41
+ AssetManager,
42
+ configureAssets,
43
+ getAssetManager
33
44
  };
34
45
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webspresso",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "Minimal, production-ready SSR framework for Node.js with file-based routing, Nunjucks templating, built-in i18n, and CLI tooling",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -272,16 +272,48 @@ function detectLocale(req) {
272
272
  return process.env.DEFAULT_LOCALE || 'en';
273
273
  }
274
274
 
275
+ /**
276
+ * Resolve middleware from config - supports both functions and named strings
277
+ * @param {Array} middlewareConfig - Array of middleware functions or names
278
+ * @param {Object} middlewareRegistry - Named middleware registry
279
+ * @returns {Array} Array of resolved middleware functions
280
+ */
281
+ function resolveMiddlewares(middlewareConfig, middlewareRegistry = {}) {
282
+ if (!middlewareConfig || !Array.isArray(middlewareConfig)) {
283
+ return [];
284
+ }
285
+
286
+ return middlewareConfig.map((mw, index) => {
287
+ if (typeof mw === 'function') {
288
+ return mw;
289
+ }
290
+
291
+ if (typeof mw === 'string') {
292
+ const resolved = middlewareRegistry[mw];
293
+ if (!resolved) {
294
+ throw new Error(`Middleware "${mw}" not found in registry. Available: ${Object.keys(middlewareRegistry).join(', ') || 'none'}`);
295
+ }
296
+ if (typeof resolved !== 'function') {
297
+ throw new Error(`Middleware "${mw}" must be a function`);
298
+ }
299
+ return resolved;
300
+ }
301
+
302
+ throw new Error(`Invalid middleware at index ${index}: must be a function or string name`);
303
+ });
304
+ }
305
+
275
306
  /**
276
307
  * Mount all pages as routes on the Express app
277
308
  * @param {Object} app - Express app
278
309
  * @param {Object} options - Options
279
310
  * @param {string} options.pagesDir - Pages directory path
280
311
  * @param {Object} options.nunjucks - Nunjucks environment
312
+ * @param {Object} options.middlewares - Named middleware registry
281
313
  * @param {boolean} options.silent - Suppress console output
282
314
  */
283
315
  function mountPages(app, options) {
284
- const { pagesDir, nunjucks, silent = false } = options;
316
+ const { pagesDir, nunjucks, middlewares = {}, silent = false } = options;
285
317
  const isDev = process.env.NODE_ENV !== 'production';
286
318
  const log = silent ? () => {} : console.log.bind(console);
287
319
 
@@ -353,6 +385,7 @@ function mountPages(app, options) {
353
385
  for (const route of sortRoutes(apiRoutes)) {
354
386
  const handler = require(route.fullPath);
355
387
  const handlerFn = typeof handler === 'function' ? handler : handler.default || handler.handler;
388
+ const routeMiddleware = handler.middleware;
356
389
 
357
390
  if (typeof handlerFn !== 'function') {
358
391
  console.warn(`API route ${route.file} does not export a function`);
@@ -372,6 +405,20 @@ function mountPages(app, options) {
372
405
  ? currentHandler
373
406
  : currentHandler.default || currentHandler.handler;
374
407
 
408
+ // Run middleware if defined
409
+ const mwConfig = isDev ? currentHandler.middleware : routeMiddleware;
410
+ if (mwConfig) {
411
+ const resolvedMw = resolveMiddlewares(mwConfig, middlewares);
412
+ for (const mw of resolvedMw) {
413
+ await new Promise((resolve, reject) => {
414
+ mw(req, res, (err) => {
415
+ if (err) reject(err);
416
+ else resolve();
417
+ });
418
+ });
419
+ }
420
+ }
421
+
375
422
  await fn(req, res, next);
376
423
  } catch (err) {
377
424
  console.error(`API error ${route.routePath}:`, err);
@@ -429,8 +476,9 @@ function mountPages(app, options) {
429
476
  await executeHook(routeHooks, 'beforeMiddleware', ctx);
430
477
 
431
478
  // Run route middleware
432
- if (config?.middleware && Array.isArray(config.middleware)) {
433
- for (const mw of config.middleware) {
479
+ if (config?.middleware) {
480
+ const resolvedMiddlewares = resolveMiddlewares(config.middleware, middlewares);
481
+ for (const mw of resolvedMiddlewares) {
434
482
  await new Promise((resolve, reject) => {
435
483
  mw(req, res, (err) => {
436
484
  if (err) reject(err);
@@ -515,6 +563,7 @@ module.exports = {
515
563
  scanDirectory,
516
564
  loadI18n,
517
565
  createTranslator,
518
- detectLocale
566
+ detectLocale,
567
+ resolveMiddlewares
519
568
  };
520
569
 
package/src/helpers.js CHANGED
@@ -4,6 +4,167 @@
4
4
  */
5
5
 
6
6
  const querystring = require('querystring');
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
10
+ /**
11
+ * Asset Manager - handles asset paths, versioning, and manifest
12
+ */
13
+ class AssetManager {
14
+ constructor(options = {}) {
15
+ this.publicDir = options.publicDir || 'public';
16
+ this.manifestPath = options.manifestPath || null;
17
+ this.manifest = null;
18
+ this.version = options.version || null;
19
+ this.prefix = options.prefix || '';
20
+
21
+ // Load manifest if path provided
22
+ if (this.manifestPath) {
23
+ this.loadManifest();
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Load asset manifest file (Vite, Webpack, etc.)
29
+ */
30
+ loadManifest() {
31
+ try {
32
+ const fullPath = path.resolve(this.manifestPath);
33
+ if (fs.existsSync(fullPath)) {
34
+ const content = fs.readFileSync(fullPath, 'utf-8');
35
+ this.manifest = JSON.parse(content);
36
+ }
37
+ } catch (err) {
38
+ console.warn('Failed to load asset manifest:', err.message);
39
+ this.manifest = null;
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Resolve asset path from manifest or add version
45
+ * @param {string} assetPath - Asset path
46
+ * @returns {string}
47
+ */
48
+ resolve(assetPath) {
49
+ // Remove leading slash for manifest lookup
50
+ const lookupPath = assetPath.replace(/^\//, '');
51
+
52
+ // Check manifest first
53
+ if (this.manifest) {
54
+ // Vite manifest format
55
+ if (this.manifest[lookupPath]) {
56
+ const entry = this.manifest[lookupPath];
57
+ const resolved = typeof entry === 'string' ? entry : entry.file;
58
+ return this.prefix + '/' + resolved;
59
+ }
60
+
61
+ // Webpack manifest format (direct mapping)
62
+ if (this.manifest[assetPath]) {
63
+ return this.prefix + this.manifest[assetPath];
64
+ }
65
+ }
66
+
67
+ // Add version query string if provided
68
+ let finalPath = assetPath.startsWith('/') ? assetPath : '/' + assetPath;
69
+ finalPath = this.prefix + finalPath;
70
+
71
+ if (this.version) {
72
+ const separator = finalPath.includes('?') ? '&' : '?';
73
+ finalPath += `${separator}v=${this.version}`;
74
+ }
75
+
76
+ return finalPath;
77
+ }
78
+
79
+ /**
80
+ * Get asset URL
81
+ */
82
+ asset(assetPath) {
83
+ return this.resolve(assetPath);
84
+ }
85
+
86
+ /**
87
+ * Generate CSS link tag
88
+ */
89
+ css(href, attributes = {}) {
90
+ const resolvedHref = this.resolve(href);
91
+ const attrs = this.buildAttributes({
92
+ rel: 'stylesheet',
93
+ href: resolvedHref,
94
+ ...attributes
95
+ });
96
+ return `<link ${attrs}>`;
97
+ }
98
+
99
+ /**
100
+ * Generate JS script tag
101
+ */
102
+ js(src, attributes = {}) {
103
+ const resolvedSrc = this.resolve(src);
104
+ const attrs = this.buildAttributes({
105
+ src: resolvedSrc,
106
+ ...attributes
107
+ });
108
+ return `<script ${attrs}></script>`;
109
+ }
110
+
111
+ /**
112
+ * Generate image tag
113
+ */
114
+ img(src, alt = '', attributes = {}) {
115
+ const resolvedSrc = this.resolve(src);
116
+ const attrs = this.buildAttributes({
117
+ src: resolvedSrc,
118
+ alt,
119
+ ...attributes
120
+ });
121
+ return `<img ${attrs}>`;
122
+ }
123
+
124
+ /**
125
+ * Build HTML attributes string
126
+ */
127
+ buildAttributes(attrs) {
128
+ return Object.entries(attrs)
129
+ .filter(([_, v]) => v !== undefined && v !== null && v !== false)
130
+ .map(([k, v]) => v === true ? k : `${k}="${this.escapeHtml(String(v))}"`)
131
+ .join(' ');
132
+ }
133
+
134
+ /**
135
+ * Escape HTML special characters
136
+ */
137
+ escapeHtml(str) {
138
+ return str
139
+ .replace(/&/g, '&amp;')
140
+ .replace(/"/g, '&quot;')
141
+ .replace(/'/g, '&#39;')
142
+ .replace(/</g, '&lt;')
143
+ .replace(/>/g, '&gt;');
144
+ }
145
+ }
146
+
147
+ // Global asset manager instance
148
+ let globalAssetManager = null;
149
+
150
+ /**
151
+ * Configure global asset manager
152
+ * @param {Object} options - Asset manager options
153
+ */
154
+ function configureAssets(options = {}) {
155
+ globalAssetManager = new AssetManager(options);
156
+ return globalAssetManager;
157
+ }
158
+
159
+ /**
160
+ * Get or create asset manager
161
+ */
162
+ function getAssetManager() {
163
+ if (!globalAssetManager) {
164
+ globalAssetManager = new AssetManager();
165
+ }
166
+ return globalAssetManager;
167
+ }
7
168
 
8
169
  /**
9
170
  * Create the fsy helper object bound to the current request context
@@ -217,6 +378,47 @@ function createHelpers(ctx) {
217
378
  return regex.test(currentPath);
218
379
  }
219
380
  return false;
381
+ },
382
+
383
+ // Asset helpers
384
+ /**
385
+ * Get asset URL with versioning/manifest support
386
+ * @param {string} assetPath - Asset path
387
+ * @returns {string}
388
+ */
389
+ asset(assetPath) {
390
+ return getAssetManager().asset(assetPath);
391
+ },
392
+
393
+ /**
394
+ * Generate CSS link tag
395
+ * @param {string} href - CSS file path
396
+ * @param {Object} attrs - Additional attributes
397
+ * @returns {string}
398
+ */
399
+ css(href, attrs = {}) {
400
+ return getAssetManager().css(href, attrs);
401
+ },
402
+
403
+ /**
404
+ * Generate JS script tag
405
+ * @param {string} src - JS file path
406
+ * @param {Object} attrs - Additional attributes
407
+ * @returns {string}
408
+ */
409
+ js(src, attrs = {}) {
410
+ return getAssetManager().js(src, attrs);
411
+ },
412
+
413
+ /**
414
+ * Generate image tag
415
+ * @param {string} src - Image file path
416
+ * @param {string} alt - Alt text
417
+ * @param {Object} attrs - Additional attributes
418
+ * @returns {string}
419
+ */
420
+ img(src, alt = '', attrs = {}) {
421
+ return getAssetManager().img(src, alt, attrs);
220
422
  }
221
423
  };
222
424
  }
@@ -270,6 +472,9 @@ const utils = {
270
472
 
271
473
  module.exports = {
272
474
  createHelpers,
273
- utils
475
+ utils,
476
+ AssetManager,
477
+ configureAssets,
478
+ getAssetManager
274
479
  };
275
480
 
package/src/server.js CHANGED
@@ -8,6 +8,7 @@ const helmet = require('helmet');
8
8
  const nunjucks = require('nunjucks');
9
9
 
10
10
  const { mountPages } = require('./file-router');
11
+ const { configureAssets } = require('./helpers');
11
12
 
12
13
  /**
13
14
  * Get default Helmet configuration
@@ -51,6 +52,62 @@ function getDefaultHelmetConfig(isDev) {
51
52
  };
52
53
  }
53
54
 
55
+ /**
56
+ * Default 404 page HTML
57
+ */
58
+ function default404Html() {
59
+ return `<!DOCTYPE html>
60
+ <html>
61
+ <head>
62
+ <title>404 - Not Found</title>
63
+ <style>
64
+ body { font-family: system-ui, sans-serif; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; background: #f5f5f5; }
65
+ .container { text-align: center; }
66
+ h1 { font-size: 4rem; margin: 0; color: #333; }
67
+ p { color: #666; margin: 1rem 0; }
68
+ a { color: #0066cc; text-decoration: none; }
69
+ a:hover { text-decoration: underline; }
70
+ </style>
71
+ </head>
72
+ <body>
73
+ <div class="container">
74
+ <h1>404</h1>
75
+ <p>Page not found</p>
76
+ <a href="/">← Back to Home</a>
77
+ </div>
78
+ </body>
79
+ </html>`;
80
+ }
81
+
82
+ /**
83
+ * Default 500 page HTML
84
+ */
85
+ function default500Html(err, isDev) {
86
+ return `<!DOCTYPE html>
87
+ <html>
88
+ <head>
89
+ <title>500 - Server Error</title>
90
+ <style>
91
+ body { font-family: system-ui, sans-serif; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; background: #f5f5f5; }
92
+ .container { text-align: center; }
93
+ h1 { font-size: 4rem; margin: 0; color: #333; }
94
+ p { color: #666; margin: 1rem 0; }
95
+ pre { background: #fff; padding: 1rem; border-radius: 4px; text-align: left; overflow: auto; max-width: 600px; }
96
+ a { color: #0066cc; text-decoration: none; }
97
+ a:hover { text-decoration: underline; }
98
+ </style>
99
+ </head>
100
+ <body>
101
+ <div class="container">
102
+ <h1>500</h1>
103
+ <p>Internal Server Error</p>
104
+ ${isDev && err ? `<pre>${err.stack || err.message}</pre>` : ''}
105
+ <a href="/">← Back to Home</a>
106
+ </div>
107
+ </body>
108
+ </html>`;
109
+ }
110
+
54
111
  /**
55
112
  * Create and configure the Express app
56
113
  * @param {Object} options - Configuration options
@@ -59,6 +116,14 @@ function getDefaultHelmetConfig(isDev) {
59
116
  * @param {string} options.publicDir - Path to public/static directory
60
117
  * @param {boolean} options.logging - Enable request logging (default: isDev)
61
118
  * @param {Object|boolean} options.helmet - Helmet configuration (default: auto-configured, false to disable)
119
+ * @param {Object} options.middlewares - Named middleware registry for route configs
120
+ * @param {Object} options.assets - Asset manager configuration
121
+ * @param {string} options.assets.manifestPath - Path to asset manifest file (Vite, Webpack)
122
+ * @param {string} options.assets.version - Asset version for cache busting
123
+ * @param {string} options.assets.prefix - URL prefix for assets
124
+ * @param {Object} options.errorPages - Custom error page handlers
125
+ * @param {Function|string} options.errorPages.notFound - Custom 404 handler or template path
126
+ * @param {Function|string} options.errorPages.serverError - Custom 500 handler or template path
62
127
  * @returns {Object} { app, nunjucksEnv }
63
128
  */
64
129
  function createApp(options = {}) {
@@ -71,9 +136,18 @@ function createApp(options = {}) {
71
136
  viewsDir,
72
137
  publicDir,
73
138
  logging = isDev && !isTest,
74
- helmet: helmetConfig
139
+ helmet: helmetConfig,
140
+ middlewares = {},
141
+ assets: assetsConfig = {},
142
+ errorPages = {}
75
143
  } = options;
76
144
 
145
+ // Configure asset manager
146
+ configureAssets({
147
+ publicDir: publicDir || 'public',
148
+ ...assetsConfig
149
+ });
150
+
77
151
  if (!pagesDir) {
78
152
  throw new Error('pagesDir is required');
79
153
  }
@@ -158,73 +232,73 @@ function createApp(options = {}) {
158
232
  mountPages(app, {
159
233
  pagesDir,
160
234
  nunjucks: nunjucksEnv,
235
+ middlewares,
161
236
  silent: isTest
162
237
  });
163
238
 
164
239
  // 404 handler
165
240
  app.use((req, res) => {
166
241
  res.status(404);
242
+
243
+ // Custom handler function
244
+ if (typeof errorPages.notFound === 'function') {
245
+ return errorPages.notFound(req, res);
246
+ }
247
+
248
+ // Custom template
249
+ if (typeof errorPages.notFound === 'string') {
250
+ try {
251
+ const html = nunjucksEnv.render(errorPages.notFound, {
252
+ url: req.url,
253
+ method: req.method
254
+ });
255
+ return res.send(html);
256
+ } catch (e) {
257
+ console.error('Error rendering 404 template:', e);
258
+ }
259
+ }
260
+
261
+ // Default response
167
262
  if (req.accepts('html')) {
168
- res.send(`
169
- <!DOCTYPE html>
170
- <html>
171
- <head>
172
- <title>404 - Not Found</title>
173
- <style>
174
- body { font-family: system-ui, sans-serif; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; background: #f5f5f5; }
175
- .container { text-align: center; }
176
- h1 { font-size: 4rem; margin: 0; color: #333; }
177
- p { color: #666; margin: 1rem 0; }
178
- a { color: #0066cc; text-decoration: none; }
179
- a:hover { text-decoration: underline; }
180
- </style>
181
- </head>
182
- <body>
183
- <div class="container">
184
- <h1>404</h1>
185
- <p>Page not found</p>
186
- <a href="/">← Back to Home</a>
187
- </div>
188
- </body>
189
- </html>
190
- `);
263
+ res.send(default404Html());
191
264
  } else {
192
- res.json({ error: 'Not Found' });
265
+ res.json({ error: 'Not Found', status: 404 });
193
266
  }
194
267
  });
195
268
 
196
269
  // Error handler
197
270
  app.use((err, req, res, next) => {
198
271
  console.error('Server error:', err);
199
- res.status(500);
272
+ res.status(err.status || 500);
273
+
274
+ // Custom handler function
275
+ if (typeof errorPages.serverError === 'function') {
276
+ return errorPages.serverError(err, req, res);
277
+ }
278
+
279
+ // Custom template
280
+ if (typeof errorPages.serverError === 'string') {
281
+ try {
282
+ const html = nunjucksEnv.render(errorPages.serverError, {
283
+ error: isDev ? err : { message: 'Internal Server Error' },
284
+ status: err.status || 500,
285
+ isDev
286
+ });
287
+ return res.send(html);
288
+ } catch (e) {
289
+ console.error('Error rendering 500 template:', e);
290
+ }
291
+ }
292
+
293
+ // Default response
200
294
  if (req.accepts('html')) {
201
- res.send(`
202
- <!DOCTYPE html>
203
- <html>
204
- <head>
205
- <title>500 - Server Error</title>
206
- <style>
207
- body { font-family: system-ui, sans-serif; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; background: #f5f5f5; }
208
- .container { text-align: center; }
209
- h1 { font-size: 4rem; margin: 0; color: #333; }
210
- p { color: #666; margin: 1rem 0; }
211
- pre { background: #fff; padding: 1rem; border-radius: 4px; text-align: left; overflow: auto; max-width: 600px; }
212
- a { color: #0066cc; text-decoration: none; }
213
- a:hover { text-decoration: underline; }
214
- </style>
215
- </head>
216
- <body>
217
- <div class="container">
218
- <h1>500</h1>
219
- <p>Internal Server Error</p>
220
- ${isDev ? `<pre>${err.stack || err.message}</pre>` : ''}
221
- <a href="/">← Back to Home</a>
222
- </div>
223
- </body>
224
- </html>
225
- `);
295
+ res.send(default500Html(err, isDev));
226
296
  } else {
227
- res.json({ error: 'Internal Server Error' });
297
+ res.json({
298
+ error: 'Internal Server Error',
299
+ status: err.status || 500,
300
+ ...(isDev && { message: err.message, stack: err.stack })
301
+ });
228
302
  }
229
303
  });
230
304