Skip to content

Express: Pug Ingredient Application

Build a recipe page using the Pug templating engine.

  • Pass dynamic recipe data (e.g., name, ingredients, steps) to the Pug view.
  • Apply external CSS for the main layout and inline CSS for highlighting specific ingredients.
  • Include a virtual path (/assets) for serving images used in the recipe view.

Project Structure

/recipe-project
├── /assets
│   ├── external.css
│   └── dish.png
├── /views
│   └── recipe.pug
├── package.json
└── server.js

Step 1: The Express Server

This file sets up the server, defines the virtual path /assets, and passes the dynamic recipe data to the Pug template.

server.js

js
import express from 'express';

const app = express();
const port = 3001;

// Set Pug as the view engine. 
//Express will look for files in the 'views' folder by default.
app.set('view engine', 'pug');

// Virtual path '/assets' that maps to the physical 'assets' directory.
// This is where images and CSS will be served from.
app.use('/assets', express.static('assets'));

// --- Dynamic Recipe Data ---
const pizza = {
    name: "Pasta Alfredo",
    image: "/assets/dish.png", // virtual path for the image
    ingredients: ["Pasta", "Cream", "Garlic", "Parmesan Cheese", "Butter"],
    steps: [
        "Boil the pasta until al dente.",
        "Prepare Alfredo sauce with butter, garlic, and cream.",
        "Add cheese and stir until melted.",
        "Mix pasta with sauce and serve hot."
    ]
};

app.get('/', (req, res) => {
    res.render('recipe', { pizza });
	// Render the 'recipe.pug' view and pass the recipe object to it
});

app.listen(port, () => {
    console.log(`Server running at http://localhost:${port}`);
});

Step 2: The Pug Template

views/recipe.pug

pug
doctype html
html
  head
    title= pizza.name
    // Link to the external CSS file
    link(rel="stylesheet", href="/assets/external.css")
  body
    header
      h1= pizza.name
      img(src=pizza.image, alt=recipe.name, class="dish")
      
    h2 Ingredients
    ul
      // Applies the inline style to every ingredient.
      each item in pizza.ingredients
        li(style="color: darkgreen; font-weight: bold")= item
        
    h2 Steps
    ol
      each step in pizza.steps
        li= step
        
    footer
      p © 2025 Recipe Book

Step 3: The External Stylesheet

assets/external.css

css
body {
    font-family: Arial, sans-serif;
    margin: 0;
    padding: 2rem;
    background-color: #f9f9f9;
}

header {
    background-color: #e67e22;
    color: white;
    padding: 1rem;
    text-align: center;
}

h2 {
    color: #333;
    margin-top: 2rem;
}

img.dish {
    display: block;
    width: 20%;
    height: auto;
    margin: 1rem auto;
    border-radius: 10px;
}

ul, ol {
    margin-left: 2rem;
}

footer {
    text-align: center;
    margin-top: 3rem;
    color: #888;
}

Run and Test

bash
npm init -y
npm install express pug

Then, ensure your package.json includes "type": "module".

Place an image named recipe-image.jpg inside the public/images directory.

bash
node app.js

Navigate to http://localhost:3000/recipe. You will see the styled recipe page, with the image loaded from the /assets virtual path and the specified ingredients highlighted in red and bold.


Advanced Version

Directory Structure

/pug-recipe-app
├── public/
│   ├── css/
│   │   └── style.css
│   └── images/
│       └── recipe-image.jpg
├── views/
│   └── recipe.pug
├── app.js
└── package.json

app.js

This file sets up the Express server, defines the recipe data, creates a virtual path for assets, and renders the recipe view.

js
import express from 'express';
import path from 'path';
import { fileURLToPath } from 'url';

const app = express();
const PORT = 3000;

// Needed for __dirname in ES Modules
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

// Set Pug as the view engine
app.set('view engine', 'pug');
app.set('views', path.join(__dirname, 'views'));

// Serve static files from the 'public' directory under the '/assets' virtual path
app.use('/assets', express.static(path.join(__dirname, 'public')));

// In-memory recipe data
const recipe = {
    name: 'Classic Spaghetti Carbonara',
    image: 'recipe-image.jpg',
    ingredients: [
        { name: '200g Spaghetti', highlight: false },
        { name: '100g Pancetta or Guanciale', highlight: true },
        { name: '2 large eggs', highlight: false },
        { name: '50g Pecorino Romano cheese, grated', highlight: true },
        { name: 'Freshly ground black pepper', highlight: false },
        { name: 'Salt', highlight: false }
    ],
    steps: [
        'Boil water in a large pot and cook spaghetti until al dente.',
        'While the pasta cooks, fry the pancetta in a pan until crisp.',
        'In a separate bowl, whisk the eggs and grated Pecorino cheese.',
        'Drain the pasta, reserving some pasta water.',
        'Quickly mix the hot pasta with the pancetta, then add the egg and cheese mixture, stirring rapidly to create a creamy sauce. Add pasta water if needed.',
        'Serve immediately with a generous amount of black pepper.'
    ]
};

// --- Route ---
app.get('/recipe', (req, res) => {
    res.render('recipe', { recipe });
});

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

Pug and CSS Files

views/recipe.pug

This Pug template structures the HTML, iterates through the recipe data, and applies conditional inline styling.

pug
doctype html
html(lang="en")
    head
        meta(charset="UTF-8")
        meta(name="viewport", content="width=device-width, initial-scale=1.0")
        title= recipe.name
        link(rel="stylesheet", href="/assets/css/style.css")
    body
        div.container
            h1= recipe.name
            
            img(src=`/assets/images/${recipe.image}`, alt=recipe.name).recipe-image

            h2 Ingredients
            ul
                each ingredient in recipe.ingredients
                    if ingredient.highlight
                        li(style={color: '#d9534f', 'font-weight': 'bold'})= ingredient.name
                    else
                        li= ingredient.name

            h2 Instructions
            ol
                each step in recipe.steps
                    li= step

public/css/style.css

css
body {
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    line-height: 1.6;
    background-color: #f8f9fa;
    color: #333;
    margin: 0;
    padding: 20px;
}

.container {
    max-width: 800px;
    margin: auto;
    background: #fff;
    padding: 30px;
    border-radius: 8px;
    box-shadow: 0 0 15px rgba(0, 0, 0, 0.1);
}

h1 {
    color: #0275d8;
    text-align: center;
    border-bottom: 2px solid #f0f0f0;
    padding-bottom: 15px;
}

h2 {
    color: #5cb85c;
    margin-top: 30px;
}

.recipe-image {
    width: 100%;
    height: auto;
    border-radius: 8px;
    margin-bottom: 20px;
}

ul, ol {
    padding-left: 20px;
}

li {
    margin-bottom: 8px;
}

Made with ❤️ for students, by a fellow learner.