Skip to content

Layout Patterns

Partials handle internal components, but they don’t solve the problem of repeated structural boilerplate (like <html>, <head>, and <body> tags) in every file. To achieve true maintainability, we use a “Layout” pattern—a master template that wraps around your individual views.

Standard EJS does not support layouts out of the box. We must install a middleware package to enable this functionality.

Terminal window
npm install express-ejs-layouts

We must register the middleware before our route definitions and configure the default layout file.

File: app.js

const expressLayouts = require("express-ejs-layouts");
// Middleware: Enable the layout engine
app.use(expressLayouts);
// Configuration: Set the default layout file
// This assumes a file exists at /views/layouts/full-width.ejs
app.set("layout", "./layouts/full-width");

A layout file is a standard EJS template with one special variable: body. When express-ejs-layouts processes a request, it renders your specific view (e.g., users.ejs) first, stores the result in a string, and then injects that string into the layout’s <%- body %> tag.

File: views/layouts/full-width.ejs

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title><%= title %></title>
<link rel="stylesheet" href="/styles/main.css" />
</head>
<body>
<!-- Persistent Navigation -->
<%- include('../partials/header.ejs') %>
<!-- Injection Point: The specific view content renders here -->
<main><%- body %></main>
<!-- Persistent Footer -->
<%- include('../partials/footer.ejs') %>
</body>
</html>

With the layout handling the frame, your individual view files become much cleaner. They only need to contain the content relevant to that specific route.

File: views/things.ejs

<!-- No <html>, <head>, or <body> tags needed here -->
<article class="card">
<header>
<h2>Inventory Log</h2>
<h3>[ All the Things ]</h3>
</header>
<section>
<p class="bio">
"This area is designated for Thing containment. Do not feed the objects."
</p>
<p>The dynamic list of Things is rendered here.</p>
<p>
Currently, the data is hardcoded in the router. In a real application, the
data would be retrieved from a database.
</p>
<h4>// Status_Report</h4>
<ul>
<% things.forEach(thing => { %>
<li>
<a
href="/things/<%= thing.id %>"
style="color: inherit; text-decoration: none; display: block; width: 100%;"
>
<%= thing.name %> <% if (!thing.active) { %> (Inactive) <% } %>
</a>
</li>
<% }) %>
</ul>
</section>
</article>

EJS Layouts - GitHub Repo


⏭ Route-Specific Layouts

Sometimes the default frame doesn’t fit. Let’s look at overrides.