Template-based routing system that routes to view files for server-side rendering, supporting various template engines and rendering strategies.
import ViewRouter from '@stackpress/ingest/plugin/ViewRouter';
const router = new ViewRouter(actionRouter, listen);
// Configure template engine
router.engine = async (filePath, req, res, ctx) => {
const html = await renderTemplate(filePath, req.data.get());
res.setHTML(html);
};
// Route to template files
router.get('/profile', './views/profile.hbs');
router.get('/dashboard', './views/dashboard.ejs');- Properties
- HTTP Method Routing
- Event-Based Routing
- Template Engine Configuration
- Render Function Configuration
- Creating Actions from Views
- Using Other ViewRouters
- Template Engine Integration
- Data Flow and Context
- Error Handling and Fallbacks
- Integration with ActionRouter
- Best Practices
The following properties are available when instantiating a ViewRouter.
| Property | Type | Description |
|---|---|---|
views |
Map<string, Set<ViewRouterTaskItem>> |
Map of event names to view file configurations (readonly) |
engine |
ViewEngine<R, S, X> |
Template engine function for rendering views |
render |
ViewRender |
Render function for processing templates |
The following examples show how to define view-based routes for different HTTP methods.
// GET routes with template rendering
router.get('/home', './views/home.hbs');
router.get('/users/:id', './views/user-profile.ejs');
// POST routes (typically for form submissions)
router.post('/contact', './views/contact-success.hbs');
// Handle any method
router.all('/error', './views/error.hbs');Parameters
| Parameter | Type | Description |
|---|---|---|
path |
string |
Route path with optional parameters (:id) |
action |
string |
File path to the template file |
priority |
number |
Priority level (default: 0) |
Returns
The ViewRouter instance to allow method chaining.
The following example shows how to route events to template files.
// Route custom events to templates
router.on('user-welcome', './views/welcome.hbs');
router.on('order-confirmation', './views/order-confirm.ejs');
// Route with regex patterns
router.on(/^admin-.+$/, './views/admin-layout.hbs');Parameters
| Parameter | Type | Description |
|---|---|---|
event |
string|RegExp |
Event name or pattern |
entry |
string |
File path to the template file |
priority |
number |
Priority level (default: 0) |
Returns
The ViewRouter instance to allow method chaining.
The following example shows how to configure the template engine.
// Handlebars engine
router.engine = async (filePath, req, res, ctx) => {
const template = await fs.readFile(filePath, 'utf8');
const compiled = Handlebars.compile(template);
const html = compiled(req.data.get());
res.setHTML(html);
};
// EJS engine
router.engine = async (filePath, req, res, ctx) => {
const html = await ejs.renderFile(filePath, req.data.get());
res.setHTML(html);
};
// Custom engine with layout support
router.engine = async (filePath, req, res, ctx) => {
const data = req.data.get();
const html = await renderWithLayout(filePath, data);
res.setHTML(html);
};Parameters
| Parameter | Type | Description |
|---|---|---|
filePath |
string |
Path to the template file |
req |
Request<R> |
Request object with data |
res |
Response<S> |
Response object for output |
ctx |
X |
Context object |
Returns
TaskResult - void, boolean, or Promise of these types.
The following example shows how to configure the render function for standalone template processing.
// Basic render function
router.render = async (filePath, props, options) => {
const template = await fs.readFile(filePath, 'utf8');
return processTemplate(template, props, options);
};
// Render with caching
const templateCache = new Map();
router.render = async (filePath, props, options) => {
if (!templateCache.has(filePath)) {
const template = await fs.readFile(filePath, 'utf8');
templateCache.set(filePath, template);
}
const template = templateCache.get(filePath);
return processTemplate(template, props, options);
};Parameters
| Parameter | Type | Description |
|---|---|---|
filePath |
string |
Path to the template file |
props |
UnknownNest |
Template data/props (optional) |
options |
UnknownNest |
Rendering options (optional) |
Returns
string|null|Promise<string|null> - Rendered template or null if failed.
The following example shows how view files are converted to executable actions.
// Internal method - creates action from template path
const action = router.action('GET /profile', './views/profile.hbs', 0);
// The action calls the configured engine with the template path
await action(request, response, context);Parameters
| Parameter | Type | Description |
|---|---|---|
event |
string |
Event name for tracking |
action |
string |
File path to the template |
priority |
number |
Priority level (default: 0) |
Returns
An async function that calls the template engine with the file path.
The following example shows how to merge views from another router.
const adminRouter = new ViewRouter(actionRouter, listen);
adminRouter.get('/admin/dashboard', './admin/dashboard.hbs');
const mainRouter = new ViewRouter(actionRouter, listen);
mainRouter.use(adminRouter); // Merges view configurationsParameters
| Parameter | Type | Description |
|---|---|---|
router |
ViewRouter<R, S, X> |
Another ViewRouter to merge views from |
Returns
The ViewRouter instance to allow method chaining.
ViewRouter supports various template engines through the engine configuration.
The following example shows how to integrate Handlebars with ViewRouter.
import Handlebars from 'handlebars';
import fs from 'node:fs/promises';
// Configure Handlebars engine
router.engine = async (filePath, req, res, ctx) => {
try {
const template = await fs.readFile(filePath, 'utf8');
const compiled = Handlebars.compile(template);
const data = {
...req.data.get(),
user: req.data.get('user'),
url: req.url,
method: req.method
};
const html = compiled(data);
res.setHTML(html);
} catch (error) {
res.setError('Template rendering failed', {}, [], 500);
}
};
// Register Handlebars helpers
Handlebars.registerHelper('formatDate', (date) => {
return new Date(date).toLocaleDateString();
});
// Use the router
router.get('/profile', './views/profile.hbs');The following example shows how to integrate EJS with ViewRouter.
import ejs from 'ejs';
// Configure EJS engine
router.engine = async (filePath, req, res, ctx) => {
try {
const data = {
...req.data.get(),
user: req.data.get('user'),
helpers: {
formatDate: (date) => new Date(date).toLocaleDateString()
}
};
const html = await ejs.renderFile(filePath, data);
res.setHTML(html);
} catch (error) {
res.setError('Template rendering failed', {}, [], 500);
}
};
router.get('/dashboard', './views/dashboard.ejs');The following example shows how to integrate Mustache with ViewRouter.
import Mustache from 'mustache';
import fs from 'node:fs/promises';
// Configure Mustache engine
router.engine = async (filePath, req, res, ctx) => {
try {
const template = await fs.readFile(filePath, 'utf8');
const data = req.data.get();
const html = Mustache.render(template, data);
res.setHTML(html);
} catch (error) {
res.setError('Template rendering failed', {}, [], 500);
}
};
router.get('/simple', './views/simple.mustache');The following example shows how to create a custom template engine with layout support.
// Custom template engine with layout support
router.engine = async (filePath, req, res, ctx) => {
try {
const data = req.data.get();
const layout = data.layout || 'default';
// Load template and layout
const template = await fs.readFile(filePath, 'utf8');
const layoutTemplate = await fs.readFile(`./layouts/${layout}.hbs`, 'utf8');
// Render template
const content = Handlebars.compile(template)(data);
// Render with layout
const html = Handlebars.compile(layoutTemplate)({
...data,
content
});
res.setHTML(html);
} catch (error) {
res.setError('Template rendering failed', {}, [], 500);
}
};ViewRouter provides rich context to templates through request data.
The following example shows how templates receive request data.
router.get('/user/:id', './views/user.hbs');
// Template receives all request data
// ./views/user.hbs
/*
<h1>User Profile</h1>
<p>User ID: {{id}}</p>
<p>Name: {{name}}</p>
<p>Email: {{email}}</p>
*/
// Route handler can prepare data
router.on('GET /user/:id', async (req, res, ctx) => {
const userId = req.data.get('id');
const user = await getUser(userId);
// Add user data to request
req.data.set('name', user.name);
req.data.set('email', user.email);
return true; // Continue to view rendering
}, 10); // Higher priority than view routerThe following example shows how to integrate server context into templates.
router.engine = async (filePath, req, res, ctx) => {
const data = {
// Request data
...req.data.get(),
// Request metadata
url: req.url.pathname,
method: req.method,
headers: Object.fromEntries(req.headers.entries()),
// Context data (server instance)
config: ctx.config.get(),
// Helper functions
helpers: {
asset: (path) => `/assets/${path}`,
route: (name, params) => ctx.generateRoute(name, params),
csrf: () => req.session.get('csrf_token')
}
};
const html = await renderTemplate(filePath, data);
res.setHTML(html);
};ViewRouter provides robust error handling for template rendering.
The following example shows how to handle template rendering errors.
router.engine = async (filePath, req, res, ctx) => {
try {
const html = await renderTemplate(filePath, req.data.get());
res.setHTML(html);
} catch (error) {
console.error('Template error:', error);
// Try fallback template
try {
const fallbackHtml = await renderTemplate('./views/error.hbs', {
error: error.message,
path: filePath
});
res.setHTML(fallbackHtml, 500);
} catch (fallbackError) {
// Ultimate fallback
res.setHTML('<h1>Template Error</h1><p>Unable to render page</p>', 500);
}
}
};The following example shows different error handling strategies for different environments.
const isDev = process.env.NODE_ENV === 'development';
router.engine = async (filePath, req, res, ctx) => {
try {
const html = await renderTemplate(filePath, req.data.get());
res.setHTML(html);
} catch (error) {
if (isDev) {
// Detailed error in development
res.setHTML(`
<h1>Template Error</h1>
<p><strong>File:</strong> ${filePath}</p>
<p><strong>Error:</strong> ${error.message}</p>
<pre>${error.stack}</pre>
`, 500);
} else {
// Generic error in production
const errorHtml = await renderTemplate('./views/500.hbs', {});
res.setHTML(errorHtml, 500);
}
}
};ViewRouter works as an extension of ActionRouter, sharing the same event system and routing capabilities.
The following example shows how ViewRouter integrates with ActionRouter.
import ActionRouter from '@stackpress/ingest/plugin/ActionRouter';
const actionRouter = new ActionRouter(context);
// ViewRouter is automatically available
actionRouter.view.get('/home', './views/home.hbs');
// Or create standalone
const viewRouter = new ViewRouter(actionRouter, listen);The following example shows how to combine different routing approaches.
// API routes return JSON
actionRouter.get('/api/users', async (req, res, ctx) => {
const users = await getUsers();
res.setResults(users);
});
// View routes return HTML
actionRouter.view.get('/users', './views/users.hbs');
// Combined approach - API + View
actionRouter.get('/users/:id', async (req, res, ctx) => {
const user = await getUser(req.data.get('id'));
req.data.set('user', user);
return true; // Continue to view
}, 10);
actionRouter.view.get('/users/:id', './views/user.hbs', 0);The following guidelines help ensure effective use of ViewRouter in production applications.
The following examples show recommended template organization patterns.
// Organize templates by feature
router.get('/auth/login', './views/auth/login.hbs');
router.get('/auth/register', './views/auth/register.hbs');
router.get('/users/profile', './views/users/profile.hbs');
router.get('/users/settings', './views/users/settings.hbs');
// Use layouts for consistency
router.engine = async (filePath, req, res, ctx) => {
const data = req.data.get();
const layout = data.layout || 'main';
const content = await renderTemplate(filePath, data);
const html = await renderTemplate(`./layouts/${layout}.hbs`, {
...data,
content
});
res.setHTML(html);
};The following example shows how to implement template caching for better performance.
// Template caching
const templateCache = new Map();
const layoutCache = new Map();
router.engine = async (filePath, req, res, ctx) => {
// Cache templates in production
const useCache = process.env.NODE_ENV === 'production';
let template;
if (useCache && templateCache.has(filePath)) {
template = templateCache.get(filePath);
} else {
template = await fs.readFile(filePath, 'utf8');
if (useCache) {
templateCache.set(filePath, template);
}
}
const html = Handlebars.compile(template)(req.data.get());
res.setHTML(html);
};The following example shows important security considerations for ViewRouter.
router.engine = async (filePath, req, res, ctx) => {
// Sanitize file path to prevent directory traversal
const safePath = path.resolve('./views', path.relative('./views', filePath));
if (!safePath.startsWith(path.resolve('./views'))) {
res.setError('Invalid template path', {}, [], 400);
return;
}
const data = {
...req.data.get(),
// Add CSRF token
csrfToken: req.session.get('csrf_token'),
// Escape user input
helpers: {
escape: (str) => Handlebars.escapeExpression(str)
}
};
const html = await renderTemplate(safePath, data);
res.setHTML(html);
};The following example shows how to handle SEO and meta tags in templates.
router.engine = async (filePath, req, res, ctx) => {
const data = {
...req.data.get(),
meta: {
title: req.data.get('title') || 'Default Title',
description: req.data.get('description') || 'Default Description',
keywords: req.data.get('keywords') || 'default, keywords',
canonical: `${req.url.origin}${req.url.pathname}`
}
};
const html = await renderTemplate(filePath, data);
res.setHTML(html);
};