webspresso 0.0.1 → 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
@@ -180,6 +180,138 @@ Creates and configures the Express app.
180
180
  - `viewsDir` (optional): Path to views/layouts directory
181
181
  - `publicDir` (optional): Path to public/static directory
182
182
  - `logging` (optional): Enable request logging (default: true in development)
183
+ - `helmet` (optional): Helmet security configuration
184
+ - `true` or `undefined`: Use default secure configuration
185
+ - `false`: Disable Helmet
186
+ - `Object`: Custom Helmet configuration (merged with defaults)
187
+ - `middlewares` (optional): Named middleware registry for routes
188
+
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
+ ```
183
315
 
184
316
  **Returns:** `{ app, nunjucksEnv }`
185
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.1",
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": {
@@ -34,6 +34,7 @@
34
34
  "dependencies": {
35
35
  "commander": "^11.1.0",
36
36
  "express": "^4.18.2",
37
+ "helmet": "^7.2.0",
37
38
  "inquirer": "^8.2.6",
38
39
  "nunjucks": "^3.2.4"
39
40
  },
@@ -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
@@ -4,9 +4,109 @@
4
4
  */
5
5
 
6
6
  const express = require('express');
7
+ const helmet = require('helmet');
7
8
  const nunjucks = require('nunjucks');
8
- const path = require('path');
9
+
9
10
  const { mountPages } = require('./file-router');
11
+ const { configureAssets } = require('./helpers');
12
+
13
+ /**
14
+ * Get default Helmet configuration
15
+ * @param {boolean} isDev - Whether in development mode
16
+ * @returns {Object} Helmet configuration
17
+ */
18
+ function getDefaultHelmetConfig(isDev) {
19
+ return {
20
+ // Disable CSP in development for easier development (Nunjucks hot reload, etc.)
21
+ contentSecurityPolicy: isDev ? false : {
22
+ directives: {
23
+ defaultSrc: ["'self'"],
24
+ styleSrc: ["'self'", "'unsafe-inline'"], // Allow inline styles for Tailwind
25
+ scriptSrc: ["'self'"],
26
+ imgSrc: ["'self'", "data:", "https:"],
27
+ fontSrc: ["'self'", "data:"],
28
+ connectSrc: ["'self'"],
29
+ frameSrc: ["'none'"],
30
+ objectSrc: ["'none'"],
31
+ upgradeInsecureRequests: []
32
+ }
33
+ },
34
+ // Other security headers
35
+ crossOriginEmbedderPolicy: false, // Disable for better compatibility
36
+ crossOriginOpenerPolicy: { policy: 'same-origin' },
37
+ crossOriginResourcePolicy: { policy: 'cross-origin' },
38
+ dnsPrefetchControl: true,
39
+ frameguard: { action: 'deny' },
40
+ hidePoweredBy: true,
41
+ hsts: {
42
+ maxAge: 31536000,
43
+ includeSubDomains: true,
44
+ preload: true
45
+ },
46
+ ieNoOpen: true,
47
+ noSniff: true,
48
+ originAgentCluster: true,
49
+ permittedCrossDomainPolicies: false,
50
+ referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
51
+ xssFilter: true
52
+ };
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
+ }
10
110
 
11
111
  /**
12
112
  * Create and configure the Express app
@@ -15,6 +115,15 @@ const { mountPages } = require('./file-router');
15
115
  * @param {string} options.viewsDir - Path to views directory
16
116
  * @param {string} options.publicDir - Path to public/static directory
17
117
  * @param {boolean} options.logging - Enable request logging (default: isDev)
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
18
127
  * @returns {Object} { app, nunjucksEnv }
19
128
  */
20
129
  function createApp(options = {}) {
@@ -26,15 +135,35 @@ function createApp(options = {}) {
26
135
  pagesDir,
27
136
  viewsDir,
28
137
  publicDir,
29
- logging = isDev && !isTest
138
+ logging = isDev && !isTest,
139
+ helmet: helmetConfig,
140
+ middlewares = {},
141
+ assets: assetsConfig = {},
142
+ errorPages = {}
30
143
  } = options;
31
144
 
145
+ // Configure asset manager
146
+ configureAssets({
147
+ publicDir: publicDir || 'public',
148
+ ...assetsConfig
149
+ });
150
+
32
151
  if (!pagesDir) {
33
152
  throw new Error('pagesDir is required');
34
153
  }
35
154
 
36
155
  const app = express();
37
156
 
157
+ // Security headers with Helmet
158
+ if (helmetConfig !== false) {
159
+ const defaultConfig = getDefaultHelmetConfig(isDev);
160
+ const finalConfig = helmetConfig === undefined || helmetConfig === true
161
+ ? defaultConfig
162
+ : { ...defaultConfig, ...helmetConfig };
163
+
164
+ app.use(helmet(finalConfig));
165
+ }
166
+
38
167
  // Trust proxy (for correct req.ip, req.protocol behind reverse proxy)
39
168
  app.set('trust proxy', 1);
40
169
 
@@ -103,73 +232,73 @@ function createApp(options = {}) {
103
232
  mountPages(app, {
104
233
  pagesDir,
105
234
  nunjucks: nunjucksEnv,
235
+ middlewares,
106
236
  silent: isTest
107
237
  });
108
238
 
109
239
  // 404 handler
110
240
  app.use((req, res) => {
111
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
112
262
  if (req.accepts('html')) {
113
- res.send(`
114
- <!DOCTYPE html>
115
- <html>
116
- <head>
117
- <title>404 - Not Found</title>
118
- <style>
119
- body { font-family: system-ui, sans-serif; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; background: #f5f5f5; }
120
- .container { text-align: center; }
121
- h1 { font-size: 4rem; margin: 0; color: #333; }
122
- p { color: #666; margin: 1rem 0; }
123
- a { color: #0066cc; text-decoration: none; }
124
- a:hover { text-decoration: underline; }
125
- </style>
126
- </head>
127
- <body>
128
- <div class="container">
129
- <h1>404</h1>
130
- <p>Page not found</p>
131
- <a href="/">← Back to Home</a>
132
- </div>
133
- </body>
134
- </html>
135
- `);
263
+ res.send(default404Html());
136
264
  } else {
137
- res.json({ error: 'Not Found' });
265
+ res.json({ error: 'Not Found', status: 404 });
138
266
  }
139
267
  });
140
268
 
141
269
  // Error handler
142
270
  app.use((err, req, res, next) => {
143
271
  console.error('Server error:', err);
144
- 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
145
294
  if (req.accepts('html')) {
146
- res.send(`
147
- <!DOCTYPE html>
148
- <html>
149
- <head>
150
- <title>500 - Server Error</title>
151
- <style>
152
- body { font-family: system-ui, sans-serif; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; background: #f5f5f5; }
153
- .container { text-align: center; }
154
- h1 { font-size: 4rem; margin: 0; color: #333; }
155
- p { color: #666; margin: 1rem 0; }
156
- pre { background: #fff; padding: 1rem; border-radius: 4px; text-align: left; overflow: auto; max-width: 600px; }
157
- a { color: #0066cc; text-decoration: none; }
158
- a:hover { text-decoration: underline; }
159
- </style>
160
- </head>
161
- <body>
162
- <div class="container">
163
- <h1>500</h1>
164
- <p>Internal Server Error</p>
165
- ${isDev ? `<pre>${err.stack || err.message}</pre>` : ''}
166
- <a href="/">← Back to Home</a>
167
- </div>
168
- </body>
169
- </html>
170
- `);
295
+ res.send(default500Html(err, isDev));
171
296
  } else {
172
- 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
+ });
173
302
  }
174
303
  });
175
304