Rendering Dynamic Content · learncode.live

What is Dynamic Content?

Static content is the same for every user. Dynamic content changes based on data, user state, or request parameters.

Static:  <h1>Welcome to My App</h1>
Dynamic: <h1>Welcome back, Alice!</h1>
         <h1>Welcome back, Bob!</h1>

Template engines let you inject data, run logic, and generate HTML dynamically.

Conditionals

Show or hide content based on conditions.

EJS

<% if (user) { %>
  <h1>Welcome back, <%= user.name %>!</h1>
  <a href="/logout">Logout</a>
<% } else { %>
  <h1>Welcome, Guest!</h1>
  <a href="/login">Login</a>
<% } %>

<%# else if %>
<% if (user.role === 'admin') { %>
  <a href="/admin">Admin Panel</a>
<% } else if (user.role === 'moderator') { %>
  <a href="/moderate">Moderate</a>
<% } else { %>
  <a href="/dashboard">Dashboard</a>
<% } %>

<%# unless (inverse if) %>
<% if (!user.verified) { %>
  <p class="warning">Please verify your email.</p>
<% } %>

Pug

if user
  h1 Welcome back, #{user.name}!
  a(href="/logout") Logout
else
  h1 Welcome, Guest!
  a(href="/login") Login

//- else if
if user.role === 'admin'
  a(href="/admin") Admin Panel
else if user.role === 'moderator'
  a(href="/moderate") Moderate
else
  a(href="/dashboard") Dashboard

//- unless
unless user.verified
  p.warning Please verify your email.

Handlebars

{{#if user}}
  <h1>Welcome back, {{user.name}}!</h1>
  <a href="/logout">Logout</a>
{{else}}
  <h1>Welcome, Guest!</h1>
  <a href="/login">Login</a>
{{/if}}

{{#if user.admin}}
  <a href="/admin">Admin Panel</a>
{{else if user.moderator}}
  <a href="/moderate">Moderate</a>
{{else}}
  <a href="/dashboard">Dashboard</a>
{{/if}}

{{#unless user.verified}}
  <p class="warning">Please verify your email.</p>
{{/unless}}

Loops

Iterate over arrays and objects.

EJS

<h2>Posts (<%= posts.length %>)</h2>
<ul>
  <% posts.forEach((post, index) => { %>
    <li class="<%= index % 2 === 0 ? 'even' : 'odd' %>">
      <h3><%= post.title %></h3>
      <p><%= post.body.substring(0, 100) %>...</p>
      <small><%= post.date.toLocaleDateString() %></small>
    </li>
  <% }) %>
</ul>

<%# Iterating over object keys %>
<dl>
  <% Object.entries(metadata).forEach(([key, value]) => { %>
    <dt><%= key %></dt>
    <dd><%= value %></dd>
  <% }) %>
</dl>

Pug

h2 Posts (#{posts.length})
ul
  each post, index in posts
    li(class= index % 2 === 0 ? 'even' : 'odd')
      h3= post.title
      p= post.body.substring(0, 100) + '...'
      small= post.date.toLocaleDateString()

//- else when array is empty
ul
  each post in posts
    li= post.title
  else
    li No posts yet.

//- Iterating object
each value, key in metadata
  dt= key
  dd= value

Handlebars

<h2>Posts ({{posts.length}})</h2>
<ul>
  {{#each posts}}
    <li>
      <h3>{{this.title}}</h3>
      <p>{{this.body}}</p>
      <small>{{this.date}}</small>
    </li>
  {{else}}
    <li>No posts yet.</li>
  {{/each}}
</ul>

{{#each metadata}}
  <dt>{{@key}}</dt>
  <dd>{{this}}</dd>
{{/each}}

Working with Variables

Accessing Properties

// EJS
<%= user.name %>
<%= user['full name'] %>
<%= users[0].email %>

// Pug
= user.name
= user['full name']
= users[0].email

// Handlebars
{{user.name}}
{{user.[full name]}}
{{users.[0].email}}

Filters and Transformations

EJS - Inline JavaScript

<p><%= post.body.toUpperCase().substring(0, 50) %>...</p>
<p><%= new Date(post.date).toISOString().split('T')[0] %></p>
<p><%= Number(price).toFixed(2) %></p>

Pug - JavaScript in templates

- const truncate = (str, len) => str.length > len ? str.slice(0, len) + '...' : str;
- const formatDate = (d) => new Date(d).toLocaleDateString();
- const formatPrice = (p) => '$' + Number(p).toFixed(2);

p= truncate(post.body, 100)
p= formatDate(post.date)
p= formatPrice(product.price)

Handlebars - Custom Helpers

const hbs = require('hbs');

hbs.registerHelper('truncate', (str, len) => {
  return str.length > len ? str.substring(0, len) + '...' : str;
});

hbs.registerHelper('formatDate', (date) => {
  return new Date(date).toLocaleDateString('en-US', {
    year: 'numeric', month: 'long', day: 'numeric'
  });
});

hbs.registerHelper('formatPrice', (price) => {
  return `$${Number(price).toFixed(2)}`;
});

hbs.registerHelper('json', (context) => {
  return JSON.stringify(context);
});
<p>{{truncate post.body 100}}</p>
<p>{{formatDate post.date}}</p>
<p>{{formatPrice product.price}}</p>
<script>const data = {{{json user}}};</script>

Dynamic Attributes and Classes

EJS

<div class="card <%= post.featured ? 'featured' : '' %>"
     data-id="<%= post.id %>">
</div>

<a href="/posts/<%= post.slug %>">Read More</a>

<button <%= user ? '' : 'disabled' %>>Submit</button>

Pug

div.card(class= post.featured ? 'featured' : null, data-id= post.id)
a(href='/posts/' + post.slug) Read More
button(disabled= !user) Submit

Handlebars

<div class="card {{#if post.featured}}featured{{/if}}" data-id="{{post.id}}">
</div>

<a href="/posts/{{post.slug}}">Read More</a>

<button {{#unless user}}disabled{{/unless}}>Submit</button>

Rendering with Different Status Codes

// Error page with 404
app.get('/404', (req, res) => {
  res.status(404).render('errors/not-found', {
    title: 'Page Not Found',
    url: req.originalUrl
  });
});

// Server error page
app.use((err, req, res, next) => {
  console.error(err);
  res.status(500).render('errors/server-error', {
    title: 'Server Error',
    message: process.env.NODE_ENV === 'development' ? err.message : 'Something went wrong'
  });
});

Rendering JSON vs HTML from the Same Route

app.get('/api/users/:id', (req, res) => {
  const user = getUser(req.params.id);
  if (!user) return res.status(404).render('errors/not-found');

  // Respond with HTML or JSON depending on the request
  if (req.accepts('html')) {
    res.render('users/show', { user });
  } else {
    res.json({ user });
  }
});

Caching Rendered Output

For performance, cache rendered templates:

const mcache = require('memory-cache');

function cache(duration) {
  return (req, res, next) => {
    const key = `__express__${req.originalUrl}`;
    const cached = mcache.get(key);

    if (cached) {
      return res.send(cached);
    }

    const originalSend = res.send.bind(res);
    res.send = (body) => {
      mcache.put(key, body, duration * 1000);
      originalSend(body);
    };
    next();
  };
}

app.get('/posts', cache(300), (req, res) => {
  res.render('posts', { posts: fetchPosts() });
});

Dynamic Layout Selection

Switch layouts based on context:

// Different layouts for different sections
app.get('/admin/*', (req, res, next) => {
  res.locals.layout = 'layouts/admin';
  next();
});

app.get('/*', (req, res, next) => {
  res.locals.layout = 'layouts/main';
  next();
});

Key Takeaways

  • Use conditionals (if/else) to show/hide content based on data
  • Use loops (forEach, each) to iterate arrays and objects
  • Handlebars helpers extend template functionality without inline logic
  • Inject variables into attributes, classes, and URLs
  • Render error pages with appropriate HTTP status codes
  • Cache rendered templates for production performance
  • Switch layouts dynamically based on route or user role
Courses