Now that we have an HTTP server sending plain text content, it's time to enhance the server so it can respond in a more appropriate content type for Web browsers.
Web standards
In the very beginning of Web, websites did not follow a standard, besides such constraints could take users to a bad navigation experience.
To mitigate this, the web standards model was created, which then became the foundation of Web, composed by the building blocks HTML, CSS and Javascript.
The idea behind these standards is to establish a well-defined set of elements, rules and behaviours for webpages hence providing a better experience for users navigating on Web.
Enhance the HTTP server to respond HTML content
Aiming to respond HTML, we should do no more than using HTML structured elements. Let's change our test to expect HTML content from the server response:
require 'socket'
require 'test/unit'
class ServerTest < Test::Unit::TestCase
def test_client_42
server = TCPSocket.open('localhost', 80)
request = "GET /users/42 HTTP/1.1\r\n\r\n"
server.puts(request)
response = ''
while line = server.gets
response += line
end
assert_equal "HTTP/1.1 200\r\nContent-Type: text/html\r\n\r\n<h1>Hey, 42!</h1>\n", response
server.close
end
end
Note the assertion:
HTTP/1.1 200\r\n
Content-Type: text/html\r\n
\r\n
<h1>Hey, 42!</h1> <---- HTMl content in the response body
That's enough to go changing the server:
...
loop do
client = socket.accept
first_line = client.gets
verb, path, _ = first_line.split
if verb == 'GET' && matched = path.match(/^\/customers\/(.*?)$/)
user_id = matched[1]
response = "HTTP/1.1 200\r\nContent-Type: text/html\r\n\r\n<h1>Hey, #{user_id}!</h1>"
client.puts(response)
end
client.close
end
...
Important to note that the header Content-Type: text/html
is very strict then necessary for some web browsers.
Now, run the test using make test
which should pass. Additionally, test the HTML using curl:
curl http://localhost/users/42
=> <h1>Hey, 42!</h1>
Open the web browser at http://localhost/users/42
too and see the content being rendered properly:
Unlike curl, a web browser is capable of using the header Content-Type
to render the correct type. Try removing the header from the server response and see that the text will be displayed in plain text:
<h1>Hey, 42!</h1>
CSS to "rule" them all 🥁
What if we wanted to add layout characteristics to our HTML elements? For instance, how to assign the color red to the h1
title?
We can use CSS to apply layout rules.
CSS inline
The most common, despite not encouraged way to write CSS is inline along with the HTMl element, by using the style
HTMl attribute:
body = "<h1 style='color: red'>Hey, #{user_id}!</h1>"
status = 200
response = "HTTP/1.1 #{status}\r\nContent-Type: text/html\r\n\r\n#{body}"
client.puts(response)
...
CSS in head
It works, but we can separate CSS from the HTML elements so it can be reused for other elements too!
<head>
<style>
h1 {
color: red;
}
</style>
</head>
<body>
<h1>Hey, 42!</h1>
</body>
Adding behaviour with Javascript
Javascript is a programming language used to add runtime behaviour to the HTML elements.
Runtime behaviour means when the HTMl content was already served by the server, as the server closed the connection with the client (web browser), so that the client can leverage on the solely power dynamycs of Javascript.
It can manipulate at various ways, since adding new elements to the page (DOM), removing existing ones, changing their layout rules (CSS), communicating to other websites and so on.
Every modern web browser comes with a runtime tool for Javascript, so the easiest way to start is opening the web browser developer tools and start using it to learn and experiment.
Changing the element color using Javascript
Let's give the user the ability to click on a button that will change the title's color to blue. Initially, our HTML will look like this:
<head>
<style>
h1 {
color: red;
}
</style>
</head>
<h1>Hey, 42!</h1>
<button onclick="changeTitleColor()">Change color to blue</button>
<script>
function changeTitleColor() {
let title = document.querySelector('h1');
title.style.color = 'blue';
}
</script>
- all the Javascript code will be placed inside the HTML tag
script
- the
button
element has an inline Javascript listener, theonclick
, which triggers the functionchangeTitleColor
when the button is clicked by the user.
Isolating HTML content from CSS and Javascript
As for the CSS inline, HTML content should not know about CSS rules nor Javascript listeners. Being isolated, it can be reused across multiple HTML files once the application starts growing more.
As such, the representation of our HTML content could be as follows:
<head>
<style>
h1 {
color: red;
}
</style>
</head>
<h1>Hey, 42!</h1> <---- isolated from CSS flavour
<button>Change color to blue</button> <---- isolated from Javascript flavour
<script>
function changeTitleColor() {
let title = document.querySelector('h1');
title.style.color = 'blue';
}
document.querySelector('button').addEventListener('click', changeTitleColor);
</script>
- CSS placed under
head
- HTML isolated from CSS and Javascript
- Javascript placed under
script
This approach will allow us in the future to even import CSS and Javascript from different files*, so that we would end up having a file for HTML, other for CSS and yet another for Javascript!
Let's see it in action. In the server.rb
, we define a "string template" for our structured HTMl, which now is much more rich and complex:
server.rb
require 'socket'
socket = TCPServer.new(80)
template = <<STR
<head>
<style>
h1 {
color: red;
}
</style>
</head>
<h1>Hey, {{user_id}}!</h1>
<button>Change color to blue</button>
<script>
function changeTitleColor() {
let title = document.querySelector('h1');
title.style.color = 'blue';
}
document.querySelector('button').addEventListener('click', changeTitleColor);
</script>
STR
Note the {{user_id}}
"tag". It's not a valid HTML tag, which would make the web browser to render it in plain text. But we want to replace it using the real userID, before the server sends the HTMl to the client.
In Ruby, we can do this by using gsub
:
body = template.gsub("{{user_id}}", user_id)
The final implementation
After all of those minor improvements, our server implementation looks like the following:
require 'socket'
socket = TCPServer.new(80)
template = <<STR
<head>
<style>
h1 {
color: red;
}
</style>
</head>
<h1>Hey, {{user_id}}!</h1>
<button>Change color to blue</button>
<script>
function changeTitleColor() {
let title = document.querySelector('h1');
title.style.color = 'blue';
}
document.querySelector('button').addEventListener('click', changeTitleColor);
</script>
STR
loop do
client = socket.accept
first_line = client.gets
verb, path, _ = first_line.split
if verb == 'GET' && matched = path.match(/^\/customers\/(.*?)$/)
user_id = matched[1]
body = template.gsub("{{user_id}}", user_id)
response = "HTTP/1.1 200\r\nContent-Type: text/html\r\n\r\n#{body}"
client.puts(response)
end
client.close
end
Then, after opening it in the web browser, we have the result:
Wrapping up
Understanding how web works is very important for web developers. In this post we learned the third part of the serie of "Web basics 101", which consists of sending HTML content through HTTP.
HTML is no more than a simple string content following a standard format for webpages. Along with CSS and Javascript being sent over HTTP, they are all the foundation of Web powering modern websites with rich content and usability.
Top comments (0)