When you visit a site like https://example.com, you see the content for that page. That content is rendered with HTML.
We know that HTML must be somewhere on this webpage, because we can see the "Example Domain" heading, along with other text content.
But how did our server render that HTML? If you right-click the page in your browser, and click "View Page Source", you'll be able to see what HTML was sent to our browser:
<!DOCTYPE html>
<html>
<head>
<title>Example Domain</title>
<meta charset="utf-8" />
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type="text/css">...</style>
</head>
<body>
<div>
<h1>Example Domain</h1>
<p>This domain is for use in illustrative examples in documents. You may use this
domain in literature without prior coordination or asking for permission.</p>
<p><a href="https://www.iana.org/domains/example">More information...</a></p>
</div>
</body>
</html>
When we view page source, we see what the server sent before any JavaScript runs.
From the HTML above, we see that <h1>Example Domain</h1>
was part of the initial HTML that came with our webpage.
This means our HTML content was definitely "server rendered", because JavaScript couldn't have run yet.
But that doesn't tell us the whole story!
One approach to rendering HTML from the server is to just load it directly from the filesystem.
In a static server setup, HTML is served straight from a folder. The web server translates the requested URL paths to filepaths.
The server sends the HTML content that was saved in those files as-is back to the browser.
If a user visits a URL like /people/scott
, and there is no scott.html
file in the people
folder, our static server will return a 404 status.
In many cases, the server will redirect to a static 404.html
file, so users don't receive a blank page.
Some applications don't show the same content for each user. If the server needs to render dynamic HTML for each page visit, it will need to run some code to create that HTML on the fly.
If we wanted to implement something like this with Node.js, it would look like this:
import express from 'express'
const app = express()
// Define a handler for the "/" route
app.get('/', (req, res) => {
console.log("Got homepage request!")
res.send(`<!DOCTYPE html> ... </html>`)
})
// Start server on port 3000
app.listen(3000)
In order to run this website, I would need to run a Node.js server in production. The server would run that JavaScript function each time someone visited the homepage.
It's more common to see dynamic servers working with databases before they respond with the rendered HTML. They can use that data to inform what HTML should be rendered for a given URL.
A "dynamic server" can access data that is being updated by another applicationβ and always show HTML content that is up-to-date with the latest information.
In our "static server" setup from before, that would involve editing HTML files and deploying those to production anytime the content needs to change.
For the rest of this article, when I refer to "SSR", I'll specifically be referring to servers that dynamicly render content with code.
Let's take a look at another website: https://elm-spa.dev. This was an older project of mine, and created as a "single page application".
Even though the user is still seeing HTML content, that HTML was rendered in the browser using JavaScript:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/style.css">
<link rel="shortcut icon" href="/favicon.png" type="image/x-png">
<title>elm-spa</title>
<meta name="description" content="single page apps made easy">
<meta name="image" content="https://elm-spa.dev/content/images/this-site.png">
</head>
<body>
<script src="/main.js"></script>
</body>
</html>
When the main.js
file runs, it injects the HTML in the page based on the current route. Because I'm using Elm, rather than JavaScript, the logic looks something like this:
import Pages.Home
import Pages.Guide
import Pages.Examples
import Pages.Example
import Pages.NotFound
view : Url -> Html Msg
view url of
case url.path of
"/" -> Pages.Home.view
"/guide" -> Pages.Guide.view
"/examples" -> Pages.Examples.view
"/examples/hello-world" -> Pages.Example.view { id = "hello-world" }
_ -> Pages.NotFound.view
// In "/main.js"
import Elm from './src/Main.elm'
// Start our app and injects HTML into the `<body>` tag
let app = Elm.init({
node: document.querySelector('body')
})
If this website has multiple webpages, why do we call this a "single page application"?
With an SPA, we use the same "static server" approach, but with from single HTML file.
The key difference from the "static server" is that every page opens the same ~/index.html
file. That means we only need to define one HTML file for the entire application.
A more accurate name would be a "Single HTML File Application"β but SHFA didn't catch on for some reason...
Our dynamic SSR server could render content from the database. When building web applications, you can use a static "single page application" that talks to any API you like. That API could be hosted by you, or something like GitHub's REST API.
Because our web client is able to send HTTP calls, it can communicate directly with the GitHub API. That API can only happen once the HTML is fetched, and the JavaScript is running.
I hope to write a follow-up article to this one soonβ there's a lot of debate in this area. (And there doesn't need to be!)
In that article, I'll cover a few things that you should consider for your next web project. See you then! π