DEV Community

Cover image for All the ways you can build a web based on content (II)
Daniel Vivar
Daniel Vivar

Posted on • Edited on

All the ways you can build a web based on content (II)

Photo by Joaquim Campa

Solution 1: Server-side Rendering

SSR was the first approach to CMSs, back in the 90s. With SSR, pages are created by a server, based on data which is detached from it. Either by creating the page dynamically when it is requested, or producing them beforehand and store it in a cache.

With the aim to keep simple examples and not deviate much from HTML, CSS & JavaScript, let's use Node.js technology for our server. Express, which is a Node.js web framework for servers, would do. Let's also use a template engine so that Express can focus on dealing with web requests only. Handlebars is a good simple one and works great with Express.

Let's get to it. First let's work on the main layout of all our pages. It would look like so:

<!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">
    <title>FruitLovers - {{ title }}</title>
    <link rel="stylesheet" href="/styles.css">
  </head>  
  <body>
    <header>
      <h1>πŸ‹ FruitLovers</h1>
      <p>
        If life gives you lemons... 
      </p>
    </header>
    {{{ body }}}
    <footer>
      FruitLovers Β© 2021
    </footer>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

This will be used as the default template for the Handlebars template engine in Express, where they're called views. In this template, the page content will will be replaced in {{{ body }}}. The title of the page will be replaced in {{ title }}.

In Handlebars syntax, three curly braces won't scape HTML characters, but two will. More info here about what that means:
https://handlebarsjs.com/guide/#html-escaping

The home page specific content will look like so:

<h2>
  Fruits you can buy from us
</h2>
<ul>
  {{#each fruits}}
    <li>
      <a href="/products/{{name}}.html">
        {{title}}
      </a>
    </li>
  {{/each}}
</ul>
Enter fullscreen mode Exit fullscreen mode

Notice how we've used a Handlebars built-in helper called #each. In our example, this helps when defining the list of fruits.

The product's template will be very similar:

<a href="/index.html">← Home</a>
<h2>
  {{fruit.title}}
</h2>
<p>
  Today's price is: {{fruit.price}}
</p>
<img width="300" src="{{fruit.image}}">
<h2>
  Other fruits you can buy from us:
</h2>
<ul>
  {{#each restOfFruits}}
    <li>
      <a href="/products/{{name}}.html">
        {{title}}
      </a>
    </li>
  {{/each}}
</ul>
Enter fullscreen mode Exit fullscreen mode

As you can see it's just about separating the data from the page and pulling only what you need. From the example, we can gather the data, in data.json:

[
  {
    "name": "lemon",
    "title": "Lemons, our classic",
    "price": "2€",
    "image": "images/lemon.jpg"
  },
  {
    "name": "strawberry",
    "title": "Strawberries, less sugar than lemons!",
    "price": "3€",
    "image": "images/strawberry.jpg"
  }
]
Enter fullscreen mode Exit fullscreen mode

Now let's work on our server: the process that will manage the web requests. I'm commenting every line to help understand the code:

// Import Express and create an app
const express = require("express");
const app = express();

// Import fruits data
const fruits = require('./data.json')

// Tell express that views are in the views folder
app.set('views', 'views');
// Tell express to use handlebars template views
app.set('view engine', 'hbs');

// Serve public folder as static
app.use(express.static("public"));

// Manage requests to the index
app.get(["/","/index.html"], (request , response) => {
  // Use the index view, passing the data it needs for fruits and title
  response.render('index', { fruits, title: 'Home' });
});

// Manage requests to product pages
app.get("/products/(:product).html", (request, response) => {
  // Get requested product from URL
  const requestedProduct = request.params.product
  // Get info about the fruit which corresponds to the requested product name
  const fruit = fruits.find(({name}) => name === requestedProduct)
  // Remove the requested product from the list of fruits
  const restOfFruits = fruits.filter(({name}) => name !== requestedProduct)
  // Use the product view, passing the data it needs
  response.render('product', { fruit, restOfFruits, title: fruit.title });
});

// Start listening for requests :)
const listener = app.listen(process.env.PORT, () => {
  console.log("Your app is listening on port " + listener.address().port);
});
Enter fullscreen mode Exit fullscreen mode

Please, take a look to the final version on Glitch, browse the /views folder and the package.json file to see how the server is set up. Click on View Source and View App to switch between panes without moving from here ;)

Please note all of the code in this series of articles is far from production-ready. Also, not written out for performance either. What if a visitor requests the non-existing orange product? What if the price is missing in the data? Is it performant to run through all the fruits twice (as seen in our code) for every server request? Etc.

SSR is great whenever you have tons of non-personal data which will produce thousands of pages, such as Amazon. It also requires a specific type of web server, which is more expensive than a static server.

Did you notice how every click required a full reload? That's how the web works by design!

A considerable group of people wanted to improve this experience, making it smoother: what if we only load the page once? What if JavaScript code inside that page manages the page routes from the client's browser and load only the parts of the page that change?

Head on to part III...

Top comments (0)