Building Dynamic Web Apps with Node.js and EJS: A Practical Guide

Learn how to create server-rendered applications using Node.js with EJS templating. Step-by-step examples and best practices for building dynamic web interfaces.

S

StalkTechie

Author

January 7, 2026
54 views

Building Dynamic Web Applications with Node.js and EJS: A Complete Guide

Discover how to create powerful, server-rendered web applications using Node.js with the EJS templating engine. Learn why developers continue to choose EJS for its simplicity and effectiveness in building dynamic content.

Table of Contents

Why EJS Still Matters in 2026

In an era dominated by complex frontend frameworks, EJS (Embedded JavaScript templates) continues to thrive for specific use cases. Its enduring popularity stems from practical advantages that solve real development problems.

The EJS Advantage

  • Familiar Syntax: EJS uses plain JavaScript within templates, eliminating the learning curve of new template languages
  • Server-Side Rendering: Generates complete HTML on the server, improving initial page load performance
  • SEO Friendly: Search engines receive fully-rendered HTML content without requiring JavaScript execution
  • Simple Integration: Works seamlessly with Express.js, the most popular Node.js web framework
  • Minimal Overhead: Lightweight with no complex build processes required

When to Choose EJS

Use Case Why EJS Works
Content-heavy websites Server-side rendering improves SEO and initial load times
Admin dashboards Simple data binding without complex state management
Prototyping quickly Rapid development with minimal setup
Traditional web applications Familiar request-response cycle with server logic

Project Setup and Configuration

Let's start by setting up a Node.js project with EJS support. This foundation will support all our examples throughout the guide.

Initial Project Structure


# Create project directory
mkdir node-ejs-app
cd node-ejs-app

# Initialize npm project
npm init -y

# Install required dependencies
npm install express ejs

# Install development dependencies
npm install --save-dev nodemon
        

Basic Server Configuration


// app.js - Main application file
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;

// Configure EJS as the template engine
app.set('view engine', 'ejs');
app.set('views', './views');

// Serve static files from public directory
app.use(express.static('public'));

// Parse URL-encoded bodies (for forms)
app.use(express.urlencoded({ extended: true }));
app.use(express.json());

// Basic route to test setup
app.get('/', (req, res) => {
    res.render('index', {
        title: 'Home Page',
        message: 'Welcome to our EJS application!'
    });
});

// Start the server
app.listen(PORT, () => {
    console.log(`Server running on http://localhost:${PORT}`);
});
        

Project Structure Organization


node-ejs-app/
├── app.js
├── package.json
├── views/
│   ├── index.ejs
│   ├── layout.ejs
│   └── partials/
├── public/
│   ├── css/
│   ├── js/
│   └── images/
├── controllers/
├── models/
└── routes/
        

Package.json Scripts


{
    "scripts": {
        "start": "node app.js",
        "dev": "nodemon app.js",
        "test": "echo \"Error: no test specified\" && exit 1"
    }
}
        

EJS Template Fundamentals

EJS templates combine HTML with JavaScript logic using specific tags. Understanding these tags is crucial for effective template development.

Basic EJS Tags


<!-- views/index.ejs -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title><%= title %></title>
</head>
<body>
    <!-- Output escaped content -->
    <h1><%= title %></h1>
    
    <!-- Output raw HTML (use with caution) -->
    <div><%- rawHTML %></div>
    
    <!-- Execute JavaScript without output -->
    <% 
        const currentYear = new Date().getFullYear();
        const isCurrentYear = currentYear === 2026;
    %>
    
    <!-- Conditional rendering -->
    <% if (isCurrentYear) { %>
        <p>Welcome to 2026!</p>
    <% } else { %>
        <p>The year is <%= currentYear %></p>
    <% } %>
    
    <!-- Loops -->
    <ul>
        <% const items = ['Apple', 'Banana', 'Cherry']; %>
        <% items.forEach(item => { %>
            <li><%= item %></li>
        <% }); %>
    </ul>
</body>
</html>
        

Tag Reference Guide

Tag Purpose Example
<%= %> Output escaped value <%= user.name %>
<%- %> Output raw HTML <%- htmlContent %>
<% %> Execute JavaScript <% const x = 10; %>
<%# %> Comments <%# This is a comment %>
<%_ _%> Trim whitespace before <%_ code %>

Dynamic Data Rendering

The real power of EJS emerges when rendering dynamic data from your Node.js application. Let's explore common data rendering patterns.

Passing Data from Controller to View


// In your route handler
app.get('/dashboard', (req, res) => {
    const userData = {
        name: 'Alex Johnson',
        email: 'alex@example.com',
        joined: '2025-06-15',
        isAdmin: true,
        notifications: 5
    };
    
    const siteStats = {
        totalUsers: 1245,
        activeToday: 342,
        revenue: 12500.50
    };
    
    res.render('dashboard', {
        title: 'User Dashboard',
        user: userData,
        stats: siteStats,
        currentPage: 'dashboard',
        successMessage: req.flash('success') || null,
        errorMessage: req.flash('error') || null
    });
});
        

Complex Data Rendering in EJS


<!-- views/dashboard.ejs -->
<div class="dashboard">
    <h1>Welcome back, <%= user.name %>!</h1>
    
    <!-- Display flash messages -->
    <% if (successMessage) { %>
        <div class="alert alert-success">
            <%= successMessage %>
        </div>
    <% } %>
    
    <!-- User profile section -->
    <div class="profile-card">
        <h2>Your Profile</h2>
        <p><strong>Email:</strong> <%= user.email %></p>
        <p><strong>Member since:</strong> <%= user.joined %></p>
        
        <% if (user.isAdmin) { %>
            <span class="badge badge-admin">Administrator</span>
        <% } %>
    </div>
    
    <!-- Statistics display -->
    <div class="stats-grid">
        <% 
            const formatCurrency = (amount) => {
                return '$' + amount.toLocaleString('en-US', {
                    minimumFractionDigits: 2,
                    maximumFractionDigits: 2
                });
            };
        %>
        
        <div class="stat-card">
            <h3>Total Users</h3>
            <p class="stat-number"><%= stats.totalUsers.toLocaleString() %></p>
        </div>
        
        <div class="stat-card">
            <h3>Active Today</h3>
            <p class="stat-number"><%= stats.activeToday %></p>
        </div>
        
        <div class="stat-card">
            <h3>Revenue</h3>
            <p class="stat-number"><%= formatCurrency(stats.revenue) %></p>
        </div>
    </div>
    
    <!-- Notification badge -->
    <% if (user.notifications > 0) { %>
        <div class="notification-badge">
            <%= user.notifications %> new notification<%= user.notifications !== 1 ? 's' : '' %>
        </div>
    <% } %>
</div>
        

Rendering Arrays and Objects


// Route handler
app.get('/products', async (req, res) => {
    try {
        const products = await Product.find().limit(10);
        const categories = await Category.find();
        
        res.render('products/list', {
            title: 'Our Products',
            products: products,
            categories: categories,
            currentCategory: req.query.category || 'all',
            user: req.session.user
        });
    } catch (error) {
        console.error('Error fetching products:', error);
        res.status(500).render('error', {
            title: 'Server Error',
            message: 'Unable to load products at this time'
        });
    }
});
        

<!-- views/products/list.ejs -->
<div class="products-container">
    <h1><%= title %></h1>
    
    <!-- Category filter -->
    <div class="category-filter">
        <a href="/products" class="<%= currentCategory === 'all' ? 'active' : '' %>">
            All Products
        </a>
        <% categories.forEach(category => { %>
            <a href="/products?category=<%= category.slug %>" 
               class="<%= currentCategory === category.slug ? 'active' : '' %>">
                <%= category.name %>
            </a>
        <% }); %>
    </div>
    
    <!-- Products grid -->
    <% if (products.length === 0) { %>
        <div class="no-products">
            <p>No products found in this category.</p>
        </div>
    <% } else { %>
        <div class="products-grid">
            <% products.forEach(product => { %>
                <div class="product-card">
                    <img src="<%= product.imageUrl %>" alt="<%= product.name %>">
                    <h3><%= product.name %></h3>
                    <p class="price">$<%= product.price.toFixed(2) %></p>
                    <p class="description"><%= product.description.slice(0, 100) %>...</p>
                    
                    <% if (product.stock <= 0) { %>
                        <span class="out-of-stock">Out of Stock</span>
                    <% } else if (product.stock < 10) { %>
                        <span class="low-stock">Only <%= product.stock %> left!</span>
                    <% } %>
                    
                    <a href="/products/<%= product._id %>" class="view-btn">
                        View Details
                    </a>
                </div>
            <% }); %>
        </div>
    <% } %>
</div>
        

Layouts and Partials for Reusable Components

EJS doesn't have built-in layout systems like some other templating engines, but we can create our own using includes and partials for maximum reusability.

Creating a Layout System


<!-- views/layout.ejs -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title><%= title || 'My Application' %></title>
    
    <!-- Stylesheets -->
    <link rel="stylesheet" href="/css/style.css">
    <% if (stylesheets) { %>
        <% stylesheets.forEach(stylesheet => { %>
            <link rel="stylesheet" href="<%= stylesheet %>">
        <% }); %>
    <% } %>
    
    <!-- Meta tags -->
    <% if (metaDescription) { %>
        <meta name="description" content="<%= metaDescription %>">
    <% } %>
</head>
<body>
    <!-- Header -->
    <%- include('partials/header', {currentPage}) %>
    
    <!-- Main content -->
    <main class="container">
        <%- body %>
    </main>
    
    <!-- Footer -->
    <%- include('partials/footer') %>
    
    <!-- Scripts -->
    <script src="/js/main.js"></script>
    <% if (scripts) { %>
        <% scripts.forEach(script => { %>
            <script src="<%= script %>"></script>
        <% }); %>
    <% } %>
</body>
</html>
        

Partial Components


<!-- views/partials/header.ejs -->
<header class="site-header">
    <nav class="navbar">
        <a href="/" class="logo">
            <img src="/images/logo.svg" alt="Company Logo">
        </a>
        
        <ul class="nav-links">
            <li>
                <a href="/" class="<%= currentPage === 'home' ? 'active' : '' %>">
                    Home
                </a>
            </li>
            <li>
                <a href="/products" class="<%= currentPage === 'products' ? 'active' : '' %>">
                    Products
                </a>
            </li>
            <li>
                <a href="/about" class="<%= currentPage === 'about' ? 'active' : '' %>">
                    About
                </a>
            </li>
            <li>
                <a href="/contact" class="<%= currentPage === 'contact' ? 'active' : '' %>">
                    Contact
                </a>
            </li>
        </ul>
        
        <div class="user-actions">
            <% if (user) { %>
                <div class="user-dropdown">
                    <span>Welcome, <%= user.name %></span>
                    <div class="dropdown-content">
                        <a href="/dashboard">Dashboard</a>
                        <a href="/profile">Profile</a>
                        <a href="/logout">Logout</a>
                    </div>
                </div>
            <% } else { %>
                <a href="/login" class="btn-login">Login</a>
                <a href="/register" class="btn-register">Sign Up</a>
            <% } %>
        </div>
    </nav>
</header>
        

Custom Layout Render Function


// Custom render function for layouts
app.use((req, res, next) => {
    // Store original render function
    const originalRender = res.render;
    
    // Override render function
    res.render = function(view, options = {}, callback) {
        // Default layout options
        const defaults = {
            layout: 'layout',
            title: 'My App',
            user: req.session.user || null,
            currentYear: new Date().getFullYear()
        };
        
        // Merge options
        const mergedOptions = { ...defaults, ...options };
        
        // If layout is false, render without layout
        if (mergedOptions.layout === false) {
            return originalRender.call(this, view, mergedOptions, callback);
        }
        
        // Read the view file
        const fs = require('fs').promises;
        const path = require('path');
        const viewPath = path.join(__dirname, 'views', `${view}.ejs`);
        
        fs.readFile(viewPath, 'utf8')
            .then(viewContent => {
                // Read layout file
                const layoutPath = path.join(__dirname, 'views', `${mergedOptions.layout}.ejs`);
                return fs.readFile(layoutPath, 'utf8')
                    .then(layoutContent => {
                        // Replace <%- body %> with view content
                        const finalContent = layoutContent.replace(
                            /<%- body %>/g, 
                            viewContent
                        );
                        
                        // Render the combined content
                        res.send(finalContent);
                    });
            })
            .catch(err => {
                console.error('Render error:', err);
                res.status(500).send('Error rendering page');
            });
    };
    
    next();
});

// Usage in routes
app.get('/custom-page', (req, res) => {
    res.render('custom-view', {
        title: 'Custom Page',
        layout: 'custom-layout', // Use different layout
        specialData: 'Some special content'
    });
});

app.get('/no-layout', (req, res) => {
    res.render('plain-view', {
        layout: false, // Render without layout
        title: 'Plain View'
    });
});
        

Forms and Data Processing

Handling forms is a fundamental part of web applications. EJS makes form creation and processing straightforward with server-side rendering.

Creating Forms with EJS


<!-- views/users/register.ejs -->
<div class="registration-form">
    <h1>Create Your Account</h1>
    
    <% if (errors) { %>
        <div class="error-messages">
            <ul>
                <% errors.forEach(error => { %>
                    <li><%= error %></li>
                <% }); %>
            </ul>
        </div>
    <% } %>
    
    <form action="/register" method="POST" novalidate>
        <div class="form-group">
            <label for="name">Full Name</label>
            <input type="text" 
                   id="name" 
                   name="name" 
                   value="<%= oldValues.name || '' %>"
                   required
                   class="<%= errors && errors.some(e => e.includes('name')) ? 'error' : '' %>">
        </div>
        
        <div class="form-group">
            <label for="email">Email Address</label>
            <input type="email" 
                   id="email" 
                   name="email" 
                   value="<%= oldValues.email || '' %>"
                   required
                   class="<%= errors && errors.some(e => e.includes('email')) ? 'error' : '' %>">
        </div>
        
        <div class="form-group">
            <label for="password">Password</label>
            <input type="password" 
                   id="password" 
                   name="password" 
                   required
                   class="<%= errors && errors.some(e => e.includes('password')) ? 'error' : '' %>">
            <small>Must be at least 8 characters long</small>
        </div>
        
        <div class="form-group">
            <label for="confirmPassword">Confirm Password</label>
            <input type="password" 
                   id="confirmPassword" 
                   name="confirmPassword" 
                   required>
        </div>
        
        <div class="form-group">
            <label>
                <input type="checkbox" name="agreeToTerms" required>
                I agree to the Terms and Conditions
            </label>
        </div>
        
        <button type="submit" class="btn-submit">Create Account</button>
        
        <p class="login-link">
            Already have an account? <a href="/login">Login here</a>
        </p>
    </form>
</div>
        

Processing Form Data


// Route handlers for form processing
app.get('/register', (req, res) => {
    res.render('users/register', {
        title: 'Register',
        errors: null,
        oldValues: {}
    });
});

app.post('/register', async (req, res) => {
    const { name, email, password, confirmPassword, agreeToTerms } = req.body;
    const errors = [];
    
    // Validation
    if (!name || name.trim().length < 2) {
        errors.push('Name must be at least 2 characters long');
    }
    
    if (!email || !email.includes('@')) {
        errors.push('Please enter a valid email address');
    }
    
    if (!password || password.length < 8) {
        errors.push('Password must be at least 8 characters long');
    }
    
    if (password !== confirmPassword) {
        errors.push('Passwords do not match');
    }
    
    if (!agreeToTerms) {
        errors.push('You must agree to the terms and conditions');
    }
    
    // Check if email already exists
    const existingUser = await User.findOne({ email });
    if (existingUser) {
        errors.push('This email is already registered');
    }
    
    // If there are errors, re-render form with errors and old values
    if (errors.length > 0) {
        return res.status(400).render('users/register', {
            title: 'Register',
            errors: errors,
            oldValues: req.body // Preserve user input
        });
    }
    
    // Create new user
    try {
        const newUser = new User({
            name,
            email,
            password: await bcrypt.hash(password, 10)
        });
        
        await newUser.save();
        
        // Set session
        req.session.userId = newUser._id;
        req.session.userName = newUser.name;
        
        // Redirect to dashboard with success message
        req.flash('success', 'Registration successful! Welcome to our platform.');
        res.redirect('/dashboard');
        
    } catch (error) {
        console.error('Registration error:', error);
        res.status(500).render('users/register', {
            title: 'Register',
            errors: ['An error occurred during registration. Please try again.'],
            oldValues: req.body
        });
    }
});

// Login form processing
app.post('/login', async (req, res) => {
    const { email, password } = req.body;
    
    try {
        // Find user
        const user = await User.findOne({ email });
        
        if (!user) {
            return res.render('users/login', {
                title: 'Login',
                error: 'Invalid email or password',
                email: email // Preserve email
            });
        }
        
        // Check password
        const validPassword = await bcrypt.compare(password, user.password);
        
        if (!validPassword) {
            return res.render('users/login', {
                title: 'Login',
                error: 'Invalid email or password',
                email: email
            });
        }
        
        // Set session
        req.session.userId = user._id;
        req.session.userName = user.name;
        req.session.userRole = user.role;
        
        // Redirect based on role
        if (user.role === 'admin') {
            res.redirect('/admin/dashboard');
        } else {
            res.redirect('/dashboard');
        }
        
    } catch (error) {
        console.error('Login error:', error);
        res.status(500).render('users/login', {
            title: 'Login',
            error: 'An error occurred. Please try again.',
            email: email
        });
    }
});
        

Best Practices and Patterns

Following established patterns ensures your EJS applications remain maintainable, scalable, and performant as they grow.

Project Structure Best Practices


# Recommended project structure
src/
├── app.js                  # Application entry point
├── config/                # Configuration files
│   ├── database.js
│   └── session.js
├── controllers/           # Route controllers
│   ├── userController.js
│   ├── productController.js
│   └── authController.js
├── models/               # Database models
│   ├── User.js
│   └── Product.js
├── routes/               # Route definitions
│   ├── userRoutes.js
│   ├── productRoutes.js
│   └── authRoutes.js
├── middleware/           # Custom middleware
│   ├── auth.js
│   └── validation.js
├── utils/               # Utility functions
│   ├── validators.js
│   └── helpers.js
├── views/               # EJS templates
│   ├── layout.ejs
│   ├── partials/
│   ├── users/
│   ├── products/
│   └── admin/
├── public/              # Static assets
│   ├── css/
│   ├── js/
│   └── images/
└── .env                 # Environment variables
        

Controller Pattern Example


// controllers/userController.js
const User = require('../models/User');

const userController = {
    // Get user profile
    getProfile: async (req, res) => {
        try {
            const user = await User.findById(req.session.userId)
                .select('-password')
                .lean();
            
            if (!user) {
                return res.redirect('/login');
            }
            
            res.render('users/profile', {
                title: 'Your Profile',
                user: user,
                successMessage: req.flash('success'),
                errorMessage: req.flash('error')
            });
            
        } catch (error) {
            console.error('Profile fetch error:', error);
            res.status(500).render('error', {
                title: 'Server Error',
                message: 'Unable to load profile'
            });
        }
    },
    
    // Update profile
    updateProfile: async (req, res) => {
        try {
            const { name, email } = req.body;
            const userId = req.session.userId;
            
            // Validation
            if (!name || !email) {
                req.flash('error', 'Name and email are required');
                return res.redirect('/profile');
            }
            
            // Check if email is taken by another user
            const existingUser = await User.findOne({ 
                email, 
                _id: { $ne: userId } 
            });
            
            if (existingUser) {
                req.flash('error', 'Email is already in use');
                return res.redirect('/profile');
            }
            
            // Update user
            await User.findByIdAndUpdate(userId, {
                name,
                email,
                updatedAt: new Date()
            });
            
            // Update session
            req.session.userName = name;
            
            req.flash('success', 'Profile updated successfully');
            res.redirect('/profile');
            
        } catch (error) {
            console.error('Profile update error:', error);
            req.flash('error', 'Failed to update profile');
            res.redirect('/profile');
        }
    },
    
    // List all users (admin only)
    listUsers: async (req, res) => {
        try {
            const page = parseInt(req.query.page) || 1;
            const limit = 20;
            const skip = (page - 1) * limit;
            
            const [users, total] = await Promise.all([
                User.find()
                    .select('-password')
                    .skip(skip)
                    .limit(limit)
                    .sort({ createdAt: -1 })
                    .lean(),
                User.countDocuments()
            ]);
            
            const totalPages = Math.ceil(total / limit);
            
            res.render('admin/users', {
                title: 'User Management',
                users: users,
                currentPage: page,
                totalPages: totalPages,
                totalUsers: total,
                searchQuery: req.query.search || ''
            });
            
        } catch (error) {
            console.error('User list error:', error);
            res.status(500).render('error', {
                title: 'Server Error',
                message: 'Unable to load user list'
            });
        }
    }
};

module.exports = userController;
        

Performance Optimization Tips

Optimization Implementation Impact
Template Caching app.set('view cache', true); Reduces file system reads
Compression Use compression middleware Reduces bandwidth usage
CDN for Static Files Serve assets from CDN Faster asset delivery
Database Indexing Index frequently queried fields Faster database queries

Real-World Blog Application Example

Let's build a complete blog application to see EJS in action for a real-world use case.

Blog Application Structure


// Blog application - app.js
const express = require('express');
const mongoose = require('mongoose');
const session = require('express-session');
const flash = require('connect-flash');
const app = express();

// Configuration
app.set('view engine', 'ejs');
app.set('views', './views');

// Middleware
app.use(express.static('public'));
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use(session({
    secret: process.env.SESSION_SECRET,
    resave: false,
    saveUninitialized: false,
    cookie: { secure: process.env.NODE_ENV === 'production' }
}));
app.use(flash());

// Database connection
mongoose.connect(process.env.MONGODB_URI, {
    useNewUrlParser: true,
    useUnifiedTopology: true
});

// Global template variables
app.use((req, res, next) => {
    res.locals.currentUser = req.session.userId ? {
        id: req.session.userId,
        name: req.session.userName,
        role: req.session.userRole
    } : null;
    
    res.locals.currentYear = new Date().getFullYear();
    res.locals.successMessage = req.flash('success');
    res.locals.errorMessage = req.flash('error');
    
    next();
});

// Routes
app.use('/', require('./routes/homeRoutes'));
app.use('/auth', require('./routes/authRoutes'));
app.use('/blog', require('./routes/blogRoutes'));
app.use('/admin', require('./routes/adminRoutes'));

// Error handling middleware
app.use((err, req, res, next) => {
    console.error(err.stack);
    res.status(500).render('error/500', {
        title: 'Server Error',
        message: 'Something went wrong!'
    });
});

// 404 handler
app.use((req, res) => {
    res.status(404).render('error/404', {
        title: 'Page Not Found'
    });
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
    console.log(`Blog application running on port ${PORT}`);
});
        

Blog Post Display Template


<!-- views/blog/post.ejs -->
<article class="blog-post">
    <header class="post-header">
        <h1 class="post-title"><%= post.title %></h1>
        
        <div class="post-meta">
            <img src="<%= post.author.avatar %>" 
                 alt="<%= post.author.name %>" 
                 class="author-avatar">
            
            <div class="meta-info">
                <span class="author-name">
                    By <%= post.author.name %>
                </span>
                <span class="post-date">
                    <%= new Date(post.createdAt).toLocaleDateString('en-US', {
                        year: 'numeric',
                        month: 'long',
                        day: 'numeric'
                    }) %>
                </span>
                
                <% if (post.readTime) { %>
                    <span class="read-time">
                        <%= post.readTime %> min read
                    </span>
                <% } %>
            </div>
        </div>
        
        <% if (post.featuredImage) { %>
            <img src="<%= post.featuredImage %>" 
                 alt="<%= post.title %>" 
                 class="featured-image">
        <% } %>
    </header>
    
    <div class="post-content">
        <%- post.content %>
    </div>
    
    <footer class="post-footer">
        <div class="post-tags">
            <% post.tags.forEach(tag => { %>
                <a href="/blog/tag/<%= tag.slug %>" class="tag">
                    #<%= tag.name %>
                </a>
            <% }); %>
        </div>
        
        <div class="post-actions">
            <% 
                const shareUrl = `https://${req.get('host')}${req.originalUrl}`;
                const shareText = encodeURIComponent(post.title);
            %>
            
            <button onclick="shareToTwitter('<%= shareUrl %>', '<%= shareText %>')" 
                    class="share-btn twitter">
                Share on Twitter
            </button>
            
            <button onclick="shareToFacebook('<%= shareUrl %>')" 
                    class="share-btn facebook">
                Share on Facebook
            </button>
            
            <% if (currentUser && currentUser.role === 'admin') { %>
                <a href="/admin/posts/<%= post._id %>/edit" class="btn-edit">
                    Edit Post
                </a>
            <% } %>
        </div>
        
        <div class="author-bio">
            <h3>About the Author</h3>
            <div class="bio-content">
                <img src="<%= post.author.avatar %>" 
                     alt="<%= post.author.name %>">
                <div>
                    <h4><%= post.author.name %></h4>
                    <p><%= post.author.bio %></p>
                    <a href="/author/<%= post.author.username %>">
                        View all posts by this author
                    </a>
                </div>
            </div>
        </div>
    </footer>
    
    <!-- Comments section -->
    <section class="comments-section">
        <h3>Comments (<%= post.comments.length %>)</h3>
        
        <% if (currentUser) { %>
            <form action="/blog/<%= post._id %>/comments" method="POST" class="comment-form">
                <textarea name="content" 
                          placeholder="Add a comment..." 
                          required></textarea>
                <button type="submit">Post Comment</button>
            </form>
        <% } else { %>
            <p class="login-prompt">
                <a href="/auth/login">Login</a> to post a comment
            </p>
        <% } %>
        
        <div class="comments-list">
            <% post.comments.forEach(comment => { %>
                <div class="comment">
                    <div class="comment-header">
                        <img src="<%= comment.user.avatar %>" 
                             alt="<%= comment.user.name %>">
                        <div>
                            <strong><%= comment.user.name %></strong>
                            <span class="comment-date">
                                <%= new Date(comment.createdAt).toLocaleDateString() %>
                            </span>
                        </div>
                    </div>
                    <div class="comment-content">
                        <%= comment.content %>
                    </div>
                </div>
            <% }); %>
        </div>
    </section>
</article>

<script>
function shareToTwitter(url, text) {
    const twitterUrl = `https://twitter.com/intent/tweet?url=${encodeURIComponent(url)}&text=${text}`;
    window.open(twitterUrl, '_blank', 'width=550,height=420');
}

function shareToFacebook(url) {
    const facebookUrl = `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(url)}`;
    window.open(facebookUrl, '_blank', 'width=550,height=420');
}
</script>
        

Conclusion

EJS continues to be a reliable choice for server-side rendering with Node.js, especially for projects where simplicity, performance, and SEO matter. Its familiar JavaScript syntax lowers the learning curve, while its integration with Express.js makes it a practical choice for many web applications.

As we've seen throughout this guide, EJS handles everything from simple variable interpolation to complex layouts and partials. The combination of Node.js for backend logic and EJS for templating creates a powerful stack for building dynamic web applications.

Remember that the right tool depends on your project's specific needs. For content-heavy websites, admin panels, or applications where server-side rendering provides tangible benefits, EJS remains an excellent choice in 2026 and beyond.

Key Takeaways:

  • EJS shines in server-rendered applications where SEO and initial load performance are priorities
  • Its JavaScript-based syntax makes it accessible to developers already familiar with JavaScript
  • Proper project structure and organization are crucial for maintainable EJS applications
  • Combine EJS with modern Node.js patterns for scalable, maintainable applications
  • Consider EJS for content-focused websites, dashboards, and traditional web applications
Share this post:

Related Articles

Discussion

0 comments

Please log in to join the discussion.

Login to Comment