Template Engines and SSR: EJS, Pug, and Server-Side Rendering in Practice
1. ストーリー:チャーリーの管理ツール・インターフェース
チャーリーはスタートアップ企業のバックエンド開発者です。チームでは、注文内容、ユーザー統計、システムのステータスを確認するための社内管理ツールが必要でした。同僚たちはReactやVueの使用を提案しましたが、チャーリーは、ページ数が少なく操作も単純なこのような社内ツールには、それらのフレームワークは重すぎると感じました。テンプレートエンジンを用いたサーバーサイドレンダリングで十分だと考えたのです。彼はEJSを選択しました。EJSは基本的にHTMLにいくつかのタグを追加しただけのものなので、非常に簡単に始められるからです。わずか1日で、彼はExpressとEJSを用いて、再利用可能なレイアウトを備えた完全な管理インターフェースを構築した。
学習内容:
- EJSにおける4つの主要な構文タグとその用途
- ExpressでEJSとPugを連携させる設定方法
- Pugのインデント構文、変数、ループ、および条件分岐
- テンプレートのレンダリングの全プロセス
- 部分ビューとレイアウトを再利用するための戦略
- データがルーターからテンプレートへどのように渡されるか
- SSRとCSRの主な相違点および選定基準
2. テンプレートのレンダリングプロセス
テンプレートエンジンの主な機能は、テンプレートファイルとデータを結合して最終的なHTML文字列を生成し、それをブラウザに返すことです。
flowchart LR
A[Browser Request] --> B[Express Routing]
B --> C[Retrieve Data]
C --> D["res.render()"]
D --> E[EJS / Pug Compilation]
E --> F[Generate HTML]
F --> G[HTTP Response]
この一連のプロセスは、次のように要約できます:
- ブラウザがHTTPリクエストを送信する
- エクスプレスルートは、対応するハンドラ関数と紐付けられます
- ハンドラーは、データベースまたはAPIからデータを取得します。
res.render()を呼び出し、テンプレート名とデータを渡す- テンプレートエンジンは、テンプレートとデータをHTMLにコンパイルします
- HTMLはHTTPレスポンスとしてブラウザに返されます
3. EJSの基本構文
EJS(Embedded JavaScript)は、最も直感的なテンプレートエンジンです。テンプレートは単なるプレーンなHTMLであり、<% %>タグを使ってロジックが埋め込まれています。
▶ 例:EJSの4つの主要なタグ
// <%= %> Output the escaped value (Safe, prevents XSS)
<p>Hello, <%= userName %>!</p>
// <% %> Execution Logic,No output
<% if (isAdmin) { %>
<span>Admin Panel</span>
<% } %>
// <%- %> Output Raw HTML (Not escaped, use with caution)
<%- articleHtml %>
// <%- include %> Importing Subtemplates
<%- include('partials/header') %>
(1) EJS 構文クイックリファレンス
| タグ | 機能 | 出力 | エスケープ | 例 |
|---|---|---|---|---|
<%= %> |
出力変数の値 | はい | はい | <%= name %> |
<%- %> |
オリジナルコンテンツを出力 | はい | いいえ | <%- html %> |
<% %> |
実行ロジック | なし | — | <% if (x) { %> |
<%# %> |
コメント | なし | — | <%# comment %> |
<%- include('x') %> |
サブテンプレートを含める | はい | いいえ | <%- include('nav') %> |
▶ 例:EJSのループと条件分岐
<ul>
<% items.forEach(function(item) { %>
<li>
<strong><%= item.name %></strong>
<% if (item.onSale) { %>
<span class="badge">Sale</span>
<% } %>
</li>
<% }); %>
</ul>
4. Express + EJS の設定
▶ 例:Express + EJS の完全なプロジェクトの設定
プロジェクトの構成:
views/
partials/
header.ejs
footer.ejs
layout.ejs
index.ejs
app.js
npm init -y
npm install express ejs
const express = require('express');
const app = express();
app.set('view engine', 'ejs');
app.set('views', './views');
app.get('/', (req, res) => {
res.render('index', {
title: 'Dashboard',
user: { name: 'Charlie', role: 'admin' },
stats: { orders: 128, users: 56 }
});
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});
(1) 主要なAPIのドキュメント
| API | 目的 | 例 |
|---|---|---|
app.set('view engine', 'ejs') |
デフォルトのテンプレートエンジンを設定する | レンダリング時にファイル拡張子を指定する必要はありません |
app.set('views', path) |
テンプレートディレクトリを設定 | デフォルトは ./views |
res.render(view, data) |
テンプレートをレンダリングして応答する | res.render('index', {title: 'Hi'}) |
res.render(view, data, callback) |
レンダリング後のHTML文字列を取得 | メール送信などの場面で利用可能 |
▶ 例:res.render の 2 番目の引数としてデータを渡す
app.get('/profile/:id', (req, res) => {
const user = { id: req.params.id, name: 'Alice', bio: 'Full-stack dev' };
res.render('profile', {
user: user,
pageTitle: user.name + "'s Profile",
isLoggedIn: true
});
});
テンプレート内で変数を名前で直接参照するには:
<h1><%= pageTitle %></h1>
<p>Name: <%= user.name %></p>
<p>Bio: <%= user.bio %></p>
5. Pugの基本構文
Pug(旧称 Jade)は、終了タグの代わりにインデントを使用するため、構文が非常に簡潔ですが、EJSに比べて習得の難易度が高くなっています。
▶ 例:Pugの基本構文
//- Pug Template
doctype html
html
head
title= pageTitle
body
h1= message
p Welcome to #{siteName}
コンパイル済みHTML:
<!DOCTYPE html>
<html>
<head><title>My Page</title></head>
<body>
<h1>Hello</h1>
<p>Welcome to MySite</p>
</body>
</html>
(1) Pug構文クイックリファレンス
| 構文 | 機能 | 例 |
|---|---|---|
tag= value |
プロパティのバインディング | h1= title |
#{expr} |
補間 | p Hello #{name} |
- code |
JSを実行 | - const x = 1 |
if/else if/else |
状態 | if (admin) ... |
each val in arr |
ループ | each item in list |
extends layout |
レイアウトを継承 | extends layout |
block name |
定義/埋め込みブロック | block content |
include path |
サブテンプレートのインポート | include header |
▶ 例:Pugのループと条件分岐
ul
each item in items
li
strong= item.name
if item.onSale
span.badge Sale
6. 部分ビューとレイアウトの再利用
テンプレートエンジンの最大の利点は、すべてのページでヘッダー、フッター、HTMLの骨格を繰り返し記述する必要がなくなることです。
▶ 例:EJSレイアウト + パーシャルビュー
views/partials/header.ejs
<header>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
</nav>
</header>
views/partials/footer.ejs
<footer>© 2026 My App</footer>
views/layout.ejs
<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
<link rel="stylesheet" href="/style.css">
</head>
<body>
<%- include('partials/header') %>
<main>
<%- body %>
</main>
<%- include('partials/footer') %>
</body>
</html>
EJSにはネイティブなレイアウト機能がありません。レイアウトは、
express-ejs-layoutsミドルウェアを使用するか、手動でファイルを読み込むことで実装する必要があります。
npm install express-ejs-layouts
const expressLayouts = require('express-ejs-layouts');
app.use(expressLayouts);
app.set('layout', 'layout');
▶ 例:Pugのレイアウト継承
views/layout.pug
doctype html
html
head
title= title
link(rel="stylesheet" href="/style.css")
body
include partials/header
block content
include partials/footer
views/index.pug
extends layout
block content
h1= pageTitle
p Welcome!
(1) レイアウト戦略の比較
| プロパティ | EJS | Pug |
|---|---|---|
| レイアウトの継承 | ミドルウェア express-ejs-layouts が必要 |
ネイティブ extends + block |
| 現地輸入 | <%- include('partial') %> |
include partial |
| 子テンプレートへのパラメータの渡し方 | <%- include('x', {data}) %> |
親テンプレートを介したデータの渡し方 |
| マルチブロックのサポート | 限定的 | 複数のブロックに対するネイティブサポート |
7. テンプレートエンジンの比較
(1) EJS 対 Pug 対 Handlebars
| 特集 | EJS | Pug | Handlebars |
|---|---|---|---|
| 構文スタイル | HTML + JS タグ | インデントあり、終了タグなし | Mustache {{}} |
| 学習曲線 | 最低 | 中 | 低 |
| 論理的推論 | 完全なJS | 完全なJS | 制限あり(任意のJSは不可) |
| アーキテクチャ/継承 | ミドルウェアが必要 | ネイティブ対応 | 追加の設定が必要 |
| 出力エスケープ | <%= %> |
= または #{} |
{{}} |
| 元の出力 | <%- %> |
!{} |
{{{}}} |
| ユースケース | クイックスタート / 社内ツール | シンプルさを最優先 / フルスタックプロジェクト | セキュリティを最優先 / マルチデバイス対応レンダリング |
| コミュニティ活動 | 高 | 高 | 中 |
- 8 SSRとCSRの比較
(1) 主な相違点の比較表
| ディメンション | SSR(サーバーサイドレンダリング) | CSR(クライアントサイドレンダリング) |
|---|---|---|
| レンダリング場所 | サーバー | ブラウザ |
| 初回画面表示速度 | 速い(HTMLが即座に読み込まれる) | 遅い(JSの読み込みと実行を待たなければならない) |
| SEOへの対応度 | 高 | 低(コンテンツがクローラーに認識されない可能性があります) |
| インタラクションの複雑さ | 低(リクエストごとにページが更新される) | 高(SPA、ページ更新なし) |
| サーバー負荷 | 高 | 低 |
| 主な技術 | EJS / Pug / PHP | React / Vue / Angular |
| ユースケース | 社内ツール / ブログ / SEOページ | 複雑なインタラクション / SPAアプリケーション |
▶ 例:SSRとCSRにおける回答の違い
SSRの動作—ブラウザは完全なHTMLを直接受け取ります:
GET /about
→ Server-Side Rendering HTML Return in Full
→ Display directly in the browser,No need to wait JS
CSRへの対応—ブラウザは空のシェルとJSバンドルを受信します:
GET /about
→ Server Response <div id="app"></div> + bundle.js
→ Browser Download JS,Execute Rendering
→ Users view the content(Duration of the white screen)
8. 総合的な例:EJSのブログページ
再利用可能なレイアウト、投稿一覧、投稿詳細、ローカルナビゲーションなどを含む、Express + EJS による完全なブログアプリケーションを構築します。
(1) プロジェクトの構成
blog/
views/
partials/
header.ejs
footer.ejs
nav.ejs
index.ejs
post.ejs
app.js
▶ 例:アプリケーションのエントリポイント app.js
const express = require('express');
const expressLayouts = require('express-ejs-layouts');
const app = express();
app.set('view engine', 'ejs');
app.set('views', './views');
app.use(expressLayouts);
app.set('layout', 'layout');
const posts = [
{ id: 1, title: 'Getting Started with Node.js', author: 'Charlie', date: '2026-06-20', excerpt: 'Learn the basics of Node.js runtime.' },
{ id: 2, title: 'Understanding Express Middleware', author: 'Alice', date: '2026-06-25', excerpt: 'Deep dive into middleware patterns.' },
{ id: 3, title: 'Template Engines Compared', author: 'Bob', date: '2026-07-01', excerpt: 'EJS vs Pug vs Handlebars showdown.' }
];
app.get('/', (req, res) => {
res.render('index', { title: 'Blog Home', posts: posts });
});
app.get('/post/:id', (req, res) => {
const post = posts.find(p => p.id === parseInt(req.params.id));
if (!post) return res.status(404).send('Post not found');
res.render('post', { title: post.title, post: post });
});
app.listen(3000, () => {
console.log('Blog running on http://localhost:3000');
});
▶ 例:ローカルナビゲーションのパーシャルファイル partials/nav.ejs
<nav class="blog-nav">
<a href="/">All Posts</a>
<% posts.forEach(function(p) { %>
<a href="/post/<%= p.id %>"><%= p.title %></a>
<% }); %>
</nav>
▶ 例:ローカルヘッダーのパーシャルファイル partials/header.ejs
<header class="site-header">
<h1><%= title %></h1>
</header>
▶ 例:ローカルフッターのパーシャルファイル partials/footer.ejs
<footer class="site-footer">
<p>© 2026 My Blog. Powered by Express + EJS.</p>
</footer>
▶ 例:記事一覧ページのビュー / index.ejs
<h2>Latest Posts</h2>
<div class="post-list">
<% posts.forEach(function(post) { %>
<article class="post-card">
<h3><a href="/post/<%= post.id %>"><%= post.title %></a></h3>
<p class="meta">By <%= post.author %> on <%= post.date %></p>
<p><%= post.excerpt %></p>
</article>
<% }); %>
</div>
▶ 例:投稿詳細ページの views/post.ejs
<h2><%= post.title %></h2>
<p class="meta">By <%= post.author %> on <%= post.date %></p>
<div class="post-body">
<p><%= post.excerpt %></p>
<p>Full article content goes here...</p>
</div>
<a href="/">← Back to all posts</a>
アプリを起動した後の画面は次のようになります:
http://localhost:3000 → Article List Page
http://localhost:3000/post/1 → Article Details Page
http://localhost:3000/post/2 → Second Article
❓ よくある質問
express-ejs-layoutsミドルウェアを使用してレイアウトの継承を実装しています。また、<%- include() %>を使用して部分ビューを手動でインポートすることも可能です。Pugでは、extendsとblockを組み合わせてレイアウトの継承を実装する機能がネイティブにサポートされています。res.render の 2 番目のパラメータとは何ですか?res.render('index', {title: 'Hi'}) のようにテンプレート内で直接変数として使用できます。テンプレート内では <%= title %> と記述します。livereload ミドルウェアを使用することで、ブラウザの自動リフレッシュを有効にでき、手動でページを更新する必要がなくなります。<%= XSS攻撃を防ぐために、HTMLエスケープされた値を出力します。これは安全なオプションです。<%- エスケープされていない生のコンテンツを出力します。信頼できるコンテンツの場合、またはサブテンプレートを埋め込む場合にのみ使用してください。async/await を使用できますか?await はサポートされていません。ルート内で非同期のデータ取得をすべて完了させた後、res.render() を使用して結果をテンプレートに渡す必要があります。📖 まとめ
- 第1章:主要な概念と「チャーリー」の管理ツール・インターフェースの使い方
- 2 テンプレートレンダリングプロセスの基本概念と使用方法
- 3 基本概念とEJSの基本構文の使い方
- Express + EJS の 4 つの基本概念と設定方法
- 5つの基本概念とPugの基本構文の使い方
- 6 部分ビューとレイアウトの再利用に関する基本概念と使用方法
- テンプレートエンジンを比較するための7つの重要な概念と活用方法
- SSRとCSRの比較における8つの主要な概念と活用方法
📝 練習問題
- このレッスンにあるすべてのコード例を完成させ、それぞれが正しく動作することを確認してください。
- 包括的な例を修正し、独自の拡張機能を追加する
- 公式ドキュメントを確認し、このレッスンで扱われていないAPIを1~2つ見つけ、それらのテストコードを作成してください。
- 振り返り:このレッスンで学んだことを、実際のプロジェクトにどのように活かしますか?
- このレッスンで学んだことと、これまでのレッスンの内容を組み合わせて、小さなプロジェクトを作成してみてください。



