How to build a (simple) blog using Rust

·

21 min read

So, you have been reading about rust for quite some time, learned that it’s been the most loved programming language multiple years in a row in Stack Overflow’s annual survey and you want in. But, you’re not a system-level programmer and Reddit keeps telling you to ‘use the right tool for the job’ and ‘rust is more aimed at low-level stuff like writing databases and programming drone’ (the first is actually good advice, I just need it to create a dramatic setting). So you get disheartened and choose to use what you’ve always been using. I think that’s a mistake as rust is an amazing language both in terms of speed of the execution but also developer experience. Not many languages give you a handy pair-programmer, i.e. the compiler, that only gives good advice, and shuts up in the meantime (rust’s compiler is amazing). I’ve been toying with Rust for some time now and overall it’s been a great experience (both learning and developer). So to get my hands a bit more dirty I decided to re-write my blog using Rust which was previously built using Next.js (This has also been a thing coming since I’m doing my best to move away from SPAs and Javascript, but that’s for another post).

The goal of the rewrite is to get something out which could serve as a learning example and be built upon to further my understanding of the language. I hope this post does the same for you.

Side note: I write Go for a living and have been coding professionally for the last ~5 years, so while I’m confident in my overall skills this tutorial might not always show idiomatic Rust and best practices. There’re a bunch of much more rust-abled developers than me who have written some great material, much of which lay the foundation for what I’m about to show you. At the end of the article, you’ll find a resource list that you can check out after reading (and hopefully coding along) this article. If you’re just interested in seeing the code you can find the repo here.

Kicking things off

So, to build your new blog using Rust, you're going to have Rust installed on your system and then run cargo new awesome-blog. Then run cd awesome-blog into the directory, open Cargo.toml and add the following under [dependencies]:

[dependencies]
tera = "1"
actix-web = "4"
env_logger = "0.9.0"
lazy_static = "1.4.0"
actix-files = "0.6.1"
pulldown-cmark = { version = "0.9.1", default-features = false }
ignore = "0.4"
toml = "0.5"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0

Feel free to use another version than the ones listed above; it's what I used at the time of writing this piece.

Let me briefly touch upon some of these libraries. The main one here is actix-web which is one of the big players in Rust web frameworks. There are multiple other choices (see for example warp, rocket, axum) but I thoroughly enjoy actix's API and have built multiple projects using it. Furthermore, it performs really well and can handle a lot of requests.

Next is serde which is a library you most likely are familiar with if you've done any Rust programming. If not, it's basically a set of methods for serializing and deserializing data structures efficiently and generically. If you can't find an implementation for your desired data structure, you simply implement the methods for your data structure and you're off to the races. For this project, serde will be used for serializing the metadata of a blog post (also named front matter) from a .toml file into a Rust struct.

Lastly, I want to touch upon tera which is the template engine I use for this blog. It's inspired by Jinja2 which you might be familiar with if you've ever done any web development with Django. Back in the day, I started out building web apps using Django and really enjoyed the way it does templating so wanted to replicate that experience in my personal projects. In a world where everything is components and the slightest duplication is a death sin, the experience of using "old school" templating has been a relief.

A simple server

Right out of the box, after running cargo new awesome-blog, you should be able to open up src/main.rs and be presented with something like this:

  fn main() {
    println!("Hello, world!");
  }

Executing the command: cargo run will (are pulling all the dependencies) output the classic Hello, world! in your terminal. We're going to treat main.rs as a thin entry point into our application, which basically just means it should do some high-level configs and call start-up functions. With that requirement, we end up with something like this:

use std::net::TcpListener;

use awesome_blog::start;

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    std::env::set_var("RUST_LOG", "actix_web=info");
    env_logger::init();

    let listener = TcpListener::bind("0.0.0.0:8080")?;
    start_blog(listener)?.await?;
    Ok(())
}

Not much going on here, except a bunch of errors, so let's break it down real quick:

  • We set an env variable RUST_LOG that determines what kind of log statements we're getting out
  • We initialize a simple logger that is configured using env variables
  • We create a listener that listens on port 8080
  • We pass the listener to the start_blog and we're off to the races

We will deal with start_blog later, which means that the only thing left that hasn’t been addressed is #[axtic_web::main]. This is macro, or more precisely, a proc_macro which generates some code for us that makes our awesome_blog run asynchronously. The rust team made a conscious decision not to provide an async runtime in the standard library but instead opted to provide essentials and let the community do the rest. The reason(s) why are manyfold, but one reason could be that there is no consensus on what such runtime should look like. That topic is also out of scope for this article; you just have to know that it generates code for us, which makes an async runtime possible.

Peeling off the layers

We need to tackle the start_blog function, so create a new file called lib.rs under src and open it up. We will be building it in steps, starting with adding the code needed to run an HttpServer:

use std::net::TcpListener;
use actix_web::{dev::Server, web, App, HttpResponse, HttpServer, middleware};

pub fn start_blog(listener: TcpListener) -> Result<Server, std::io::Error> {
    let srv = HttpServer::new(move || {
        App::new()
           .wrap(middleware::Logger::default()) // enable logger
           .route("/health", web::get().to(HttpResponse::Ok))
    })
    .listen(listener)?
    .run();

    Ok(srv)
}

So, we did a little more than just create an HttpServer. We also added in the logger defined in main.rs and created a route that pings back a 200 Ok response.

Most of the above code should make sense to you (assuming familiarity with rust) but let's quickly touch upon the move keyword. According to the docs: a move converts any variables captured by reference or mutable reference to variables captured by value. If you're a bit confused about the above statement, join the club. But, it can be broken down into a more (to me, at least) understandable sentence. What it boils down to is rust's ownership model and how it deals with memory (de)allocation and references. What we see in this line HttpServer::new( move || { App::new() }) is a closure and closures might escape. To (over)simply, actix_web will spin up multiple instances of your app, assuming a multi-thread environment, so variables/values passed to App::new() might outlive App, i.e. the closure might escape. That would leave dangling references laying around that are not accounted far and that's no bueno in rust. So, to get around that, we tell App to take ownership of the values passed to it so that each instance owns what is passed to it.

Even though we're going a bit "olds chool" here and using pure simple html we would still like to have some reusability. And this is exactly where tera comes in. However, since the templates will remain the same once we chuck them into prod, it will probably be a good idea only to load them one time and then pass a reference. To do this, we're going to be using lazy_static! so go ahead and add the following to your lib.rs:

use std::net::TcpListener;
use actix_web::{dev::Server, web, App, HttpResponse, HttpServer};
use tera::Tera;

#[macro_use]
extern crate lazy_static;

lazy_static! {
    pub static ref TEMPLATES: Tera = {
        let mut tera = match Tera::new("templates/**/*.html") {
            Ok(t) => t,
            Err(e) => {
                println!("Parsing error(s): {}", e);
                ::std::process::exit(1);
            }
        };
        tera.autoescape_on(vec![".html", ".sql"]);
        tera
    };
}

pub fn start_blog(listener: TcpListener) -> Result<Server, std::io::Error> {
    let srv = HttpServer::new(move || {
        App::new()
           .app_data(web::Data::new(TEMPLATES.clone()))
           .wrap(middleware::Logger::default()) // enable logger
           .route("/health", web::get().to(HttpResponse::Ok))
    })
    .listen(listener)?
    .run();

    Ok(srv)
}

All good, but one thing missing. We need to add a templates directory in the root directory so go ahead and mkdir templates. We will get back to main.rs, lib.rs and all that soon but for now, let's quickly touch upon the templates so we have something to show.

Dude, where are the components?

I started out with react when I got my first job as a front-end developer. Components were all the rage (well, they still are but this makes me sound a bit older and wiser) back then and it was how I started thinking about UI elements. I've since changed my mind quite a bit on this but that will be for another article. As you know, we're not going to be doing components here but partials and blocks so in your templates directory, create a file called base.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    {% block head %}
    <title>{% block title %}{% endblock title %}</title>
    {% endblock head %}
    <link rel="stylesheet" type="text/css" href="/static/css/index.css">
</head>

<body class="flex flex-col justify-between min-h-screen font-sans leading-normal tracking-normal">
    <div class="h-16">{% block header %}{% endblock header %}</div>

    <main class="container flex-1 w-full md:max-w-3xl mx-auto overflow-x-hidden">
        {% block content %}{% endblock content %}
    </main>
    {% block footer %}
    <div class="flex w-full h-20 justify-center items-center">
        &copy; 2022 by <a class="pl-2" href="https://awesomeblog.com/">Awesome Blog</a>.
    </div>
    {% endblock footer %}
</body>

<script src="/static/js/highlight.min.js"></script>
<script>hljs.highlightAll();</script>
</script>

</html>

The above is going to be the base for everything else we do.

The part that's going to let us extend base.html (or, inherit..) is this {% block content %}{% endblock content %}. Let's see how that is done, so go ahead and create a new file under templates called home.html:

{% extends "base.html" %}
{% block title %}Awesome blog | by DeveloperMan{% endblock title %}

{% block content %}
<div class="w-full h-full px-4 md:px-6 text-xl text-gray-800 leading-normal">
    <h1 class="text-center text-3xl">All about tech</h1>
    <h3 class="text-center text-lg mt-2 text-gray-600">More words describing the blog here</h3>
    {% for fm in posts %}
    <a class="mb-3" href="/posts/{{fm.file_name}}">
        <div class="flex flex-col mb-5 px-4 py-6 cursor-pointer">
            <h2 class="text-2xl font-semibold hover:underline">{{fm.title}}</h2>
            <div class="flex items-center mt-2">
                <p class="hidden md:block text-base ml-2 mr-2">{{fm.posted}}</p>
                <p class="hidden md:block text-base mx-2">{{fm.author}}</p>
                <p class="text-base ml-0 md:ml-2">Reading time:
                {{fm.estimated_reading_time}} min</p>
            </div>
        </div>
    </a>
    {% endfor %}
</div>
{% endblock content %}

First off, notice the {% extends "base.html" %} at the top. This lets us interact with all of the blocks we created in the base.html template. Furthermore, we have a for loop in the markup: {% for frontmatter in posts %}. We can pass data as variables when serving the html as part of the response to, say, http://awesomeblog.com/ which in the above case would return a variable named posts that is an array of posts.

If you try and spin up the application now, not much would be different, so let's implement some handlers/controllers/etc to actually serve some html!

The controller/handler layer

Now we're getting into the actual meat of this thing and starting to write some actual rust code!

We need a few things to get started, so go ahead and create a new folder under src called handlers. In that holder, create a file called mod.rs and home_handler.rs. Let's start simple with some test data to show something when we load the home page. Open home_handler.rs and add the following:

use actix_web::{get, web, HttpResponse, Responder};
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug)]
pub struct Frontmatter {
    title: String,
    file_name: String,
    description: String,
    posted: String,
    tags: Vec<String>,
    author: String,
    estimated_reading_time: u32,
    order: u32,
}

#[get("/")]
pub async fn index(templates: web::Data<tera::Tera>) -> impl Responder {
   let mut context = tera::Context::new();

   let frontmatters = vec![Frontmatter{
    tags: vec!["Rusty".to_string(), "Test".to_string()],
    title: "Test posts".to_string(),
    file_name: "test_posts.md".to_string(),
    description: "Just testing out the system".to_string(),
    posted: "2022-08-09".to_string(),
    author: "MBvisti".to_string(),
    estimated_reading_time: 12,
    order: 1,
   }];

   context.insert("posts", &frontmatters);

   match templates.render("home.html", &context) {
    Ok(s) => HttpResponse::Ok().content_type("text/html").body(s),
    Err(e) => {
        println!("{:?}", e);
        HttpResponse::InternalServerError()
            .content_type("text/html")
            .body("<p>Something went wrong!</p>")
    }
   }
}

If you've been through the rust book most of this should make sense to you.

If not, let's do a quick rundown. We create a struct and derive (the #[derive()] on top of Frontmatter) some methods from serde and the standard library. Next, we create a context through tera. Remember how we talked about providing data to the templates through our handlers? that's how we do it. We then simply call .insert() on the context, provide the variable name and the data and we're good to go. In the return part of the function, we put impl Responder so we just have to return something that implements Responder. It just so happens that HttpResponse does implement Responder, so all there is left to do is to pull out the correct template from tera. And since template.render() can fail we provide a backup that, admittedly, is a bit lazy for now but we can always come back and improve on this. Lastly, you might be wondering how we actually access the templates we reference in the handler as templates: web::Data<tera::TerA>.

Actix has a neat way of sharing data though App::app_data and the struct Data<T: ?Sized>(_) type. By passing something wrapped in web::Data::new to App.app_data (side note: we could improve upon this by passing web::Data::clone to app_data instead of web::Data::new and just wrap our data once in web::Data::new and simply pass a reference to that in web::Data::copy. This is because Data uses Arc internally which makes it very cheap to clone) we can now extract it in your handlers!

Lastly, before we can access our new shiny handler, we need to make it accessible so open up mod.rs under src/handlers:

mod home_handler;

pub use home_handler::index;

We just need to add a route so we can serve the content to the user. Open up main.rs and add the following:

use std::net::TcpListener;
use actix_web::{dev::Server, web, App, HttpResponse, HttpServer};
use tera::Tera;

pub mod handlers; // new line

#[macro_use]
extern crate lazy_static;

lazy_static! {
    pub static ref TEMPLATES: Tera = {
        let mut tera = match Tera::new("templates/**/*.html") {
            Ok(t) => t,
            Err(e) => {
                println!("Parsing error(s): {}", e);
                ::std::process::exit(1);
            }
        };
        tera.autoescape_on(vec![".html", ".sql"]);
        tera
    };
}

pub fn start_blog(listener: TcpListener) -> Result<Server, std::io::Error> {
    let srv = HttpServer::new(move || {
        App::new()
           .app_data(web::Data::new(TEMPLATES.clone()))
           .wrap(middleware::Logger::default()) // enable logger
           .route("/health", web::get().to(HttpResponse::Ok))
            .service(handlers::index) // new line
    })
    .listen(listener)?
    .run();

    Ok(srv)
}

Give it a spin and you should see (a rather ugly) web page with your blog post!

Static assets and the path of least resistance

While we were setting up our html templates you might have noticed what looks like tailwindcss classes and then promptly wondered what the hell kind of end result it had on the home page. First of all, good on you for choosing great frontend tooling. Secondly, we haven't yet included the css needed for our styles to actually do something.

If you check out base.html, you will see this line: <link rel="stylesheet" type="text/css" href="/static/css/index.css"> which hints at we need a css folder within a static. Now, we can go about this in a number of ways depending on what kind of traffic we expect to get and how (over)engineered we are planning to make this.

One option could be to serve the content of index.css through an S3 bucket and then point an URL directly at it which then becomes referenced here. It would certainly keep the resulting binary smaller but it does add some overhead that is not really warranted right now (more complex CI/CD flow, handling various paths in dev and prod etc). Another option (the one we're going with), would be to just add it as part of the overall binary.

It keeps things simple for now and should our blog become a raving success we can most likely worry about this at that point. So, create two new folders at the root, namely static and under that, css. Next up we need to do some tailwind setup that you can find on their own site, so for your convenience, here are the cmds you can run in your terminal:

mkdir tailwind && cd tailwind

npm init -y

npm install -D tailwindcss

npx tailwindcss init

touch base.css

Almost there, just a few more steps so open up tailwind.config.js and copypaste the following:

module.exports = {
  content: ["../templates/**/*.{html,js}"],
  theme: {
    extend: {},
  },
  plugins: [],
}

Two more things, open base.css and copypaste the following:

@tailwind base;
@tailwind components;
@tailwind utilities;

Lastly, open up package.json and add the following under scripts:

{
    "scripts": {
        "watch-css": "npx tailwindcss -i ./base.css -o ../static/css/index.css --watch",
        "build-css-prod": "npx tailwindcss -i ./base.css -o ../static/css/index.css --minify"
    },
}

Those two scripts give you a cmd to watch for any changes in development and then build for production when we merge. That also implies that you would have to remember to run build-css-prod before merging to master. This can definitely be improved upon but since you're the lead (and only) developer on this, it will be fine for our needs.

Last step, we need to make actix_web aware of our static files. So open up lib.rs and update it as follows:

use actix_files::Files; // new line
use actix_web::{dev::Server, middleware, web, App, HttpResponse, HttpServer};
use std::net::TcpListener;
use tera::Tera;

pub mod handlers;

#[macro_use]
extern crate lazy_static;

lazy_static! {
    pub static ref TEMPLATES: Tera = {
        let mut tera = match Tera::new("templates/**/*.html") {
            Ok(t) => t,
            Err(e) => {
                println!("Parsing error(s): {}", e);
                ::std::process::exit(1);
            }
        };
        tera.autoescape_on(vec![".html", ".sql"]);
        tera
    };
}

pub fn start_blog(listener: TcpListener) -> Result<Server, std::io::Error> {
    let srv = HttpServer::new(move || {
        App::new()
            .app_data(web::Data::new(TEMPLATES.clone()))
            .wrap(middleware::Logger::default())
            .service(Files::new("/static", "static/").use_last_modified(true)) // new line
            .route("/health", web::get().to(HttpResponse::Ok))
            .service(handlers::index) 
    })
    .listen(listener)?
    .run();

    Ok(srv)
}

We just need to build the css so from the root directory, run: cd tailwind && npm run build-css-prod && cd ...

Now, give cargo run a spin, and you should see a much better-looking site.

A quick interlude

Two things are currently missing: 1. a place to store the actual blog posts and 2. a page to show a post in all its glory.

Let's start with the 2nd item on the list since we pretty much already did that. To not bore you with the same thing twice, go ahead and open this link (link to the repo with the complete code), copy the content and create a new file under templates called post.html.

Next, go to here copy the content and paste it into your base.css. Most of this should look familiar to you if you've done any css and just applies some rather simple styles to the post page.

Steal like an artist (or, copy smarter people than yourself)

Everything is mostly done, we just need a way to store our awesome blog posts and a way to retrieve them so we can remove the hard coding we did earlier. And the way to do this is heavily inspired by another blog post written by a much smarter guy than myself: fasterthanlime whom, if you haven't already, should check out. This guy does some seriously deep dives and really knows his craft.

Go ahead and make a new folder called posts at the same level as the static folder. Inside this one, create another folder, give it a name like my-first-article and cd into it. I write all my stuff using markdown so that is what we're going to do here as well.

Create a file called post.md and copy-paste some lorem ipsum text (or, an article you've prepared). Next, create a file called post_frontmatter.toml, open it and add the following:

title = 'This is my first article'
file_name = 'my-first-article'
description = 'The first article authored by me'
tags = []
posted = '22/08/2022'
estimated_reading_time = 13
author = 'Morten Vistisen' # feel free to swap with your own name
order = 1

If this looks suspiciously like our struct Frontmatter {....}, then its because it's.

Next order business, add some logic to extract all the frontmatters we might have and display it on our home page. Let's start with the logic for getting all the frontmatters of our awesome blogs, so open home_handler.rs and add the following:

use std::{fs, io::Error}; // add these imports
use ignore::WalkBuilder; // add these imports

fn find_all_frontmatters() -> Result<Vec<Frontmatter>, std::io::Error> {
    let mut t = ignore::types::TypesBuilder::new();
    t.add_defaults();
    let toml = match t.select("toml").build() {
      Ok(t)=> t,
      Err(e)=> {
        println!("{:}", e);
        return Err(Error::new(std::io::ErrorKind::Other,
        "could not build toml file type matcher"))
      }
    };

    let file_walker = WalkBuilder::new("./posts").types(toml).build();

    let mut frontmatters = Vec::new();
    for frontmatter in file_walker {
        match frontmatter {
          Ok(fm) => {
            if fm.path().is_file() {
                let fm_content = fs::read_to_string(fm.path())?;
              let frontmatter: Frontmatter = toml::from_str(&fm_content)?;

              frontmatters.push(frontmatter);
            }
        }
        Err(e) => {
            println!("{:}", e); // we're just going to print the error for now
            return Err(Error::new(std::io::ErrorKind::NotFound, "could not locate frontmatter"))
        }
      }
    }

    Ok(frontmatters)
}

Since we're storing our posts as part of the binary we need a way to locate all the frontmatter of our posts and for this, we're going to use ignore::WalkBuilder. This gives us a recursive directory iterator with a large number of configs that we can set, depending on the type of action we want to do. For this case, we want it to look in the posts directory and look for all files with the .toml extension, and it does it, BLAZINGLY fast.

One thing to note here is that find_all_frontmatters returns a Result since there are actions that can fail, which means we also have to handle some errors. As you might be able to tell, I haven't spent the most time on errors in this function and basically, just log whatever error comes from ignore and then return the most similar one from std::io::ErrorKind. This can be done better using libraries like anyerror or thiserror. I encourage you to play around with this yourself or open a pull request if you've improvements here.

To actually show some dynamic data on the home page, we need to use the above function in our index handler so open up home_handler.rs and make the following adjustments:

#[get("/")]
pub async fn index(templates: web::Data<tera::Tera>) -> impl Responder {
    let mut context = tera::Context::new();

    let mut frontmatters = match find_all_frontmatters() {
        Ok(fm) => fm,
        Err(e) => {
            println!("{:?}", e);
            return HttpResponse::InternalServerError()
                .content_type("text/html")
                .body("<p>Something went wrong!</p>");
        }
    };
    frontmatters.sort_by(|a, b| b.order.cmp(&a.order));

    context.insert("posts", &frontmatters);

    match templates.render("home.html", &context) {
        Ok(s) => HttpResponse::Ok().content_type("text/html").body(s),
        Err(e) => {
            println!("{:?}", e);
            HttpResponse::InternalServerError()
                .content_type("text/html")
                .body("<p>Something went wrong!</p>")
        }
    }
}

Give cargo run a spin and see the results!

The last thing that's needed is to add the handler for the post.html page and extract the markdown of the article, convert it to html and serve it. Since we already have post.html ready, go ahead and create post_handler.rs under src/handlers and open it up. We need to add two functions: 1. to extract the frontmatter from a specific post and 2. to extract the markdown of the post.

use std::{io::Error, fs};

use super::home_handler::Frontmatter;

fn extract_markdown(post_name: &str) -> Result<String, Error> {
    let markdown = match fs::read_to_string(format!("./posts/{}/post.md", post_name)) {
        Ok(markdown) => markdown,
        Err(e) => {
            println!("{:?}", e);
            return Err(e)
        }
    };

    Ok(markdown)
}

fn extract_frontmatter(post_name: &str) -> Result<Frontmatter, Error> {
    let frontmatter_input =    match fs::read_to_string(format!("./posts/{}/post_frontmatter.toml", post_name)) {
        Ok(s) => s,
        Err(e) => {
            println!("{:?}", e);
            return Err(e)
        }
    };

    let frontmatter = match toml::from_str(&frontmatter_input) {
        Ok(fm) => fm,
        Err(e) => {
            println!("{:?}", e);
            return Err(Error::new(std::io::ErrorKind::Other, "could not find post frontmatter"))
        }
    };

    Ok(frontmatter)
}

Not much new here, only the use of toml::from_str to deserialize a string into a specific type, in this case: Frontmatter.

Last thing we need is to create the handler and add it as a service to our App in lib.rs, so add the following to post_handler.rs:

use actix_web::{web, get, Responder, HttpResponse};
use pulldown_cmark::{Options, Parser, html};

#[get("/posts/{post_name}")]
pub async fn post(
    tmpl: web::Data<tera::Tera>,
    post_name: web::Path<String>,
) -> impl Responder {
    let mut context = tera::Context::new();
    let options = Options::empty(); // used as part of pulldown_cmark for setting flags to enable extra features - we're not going to use any of those, hence the `empty();`

    let markdown_input = match extract_markdown(&post_name) {
        Ok(s) => s,
        Err(e) => {
            println!("{:?}", e);
            return HttpResponse::NotFound()
                .content_type("text/html")
                .body("<p>Could not find post - sorry!</p>")
        }
    };

    let frontmatter = match extract_frontmatter(&post_name) {
        Ok(s) => s,
        Err(e) => {
            println!("{:?}", e);
            return HttpResponse::NotFound()
                .content_type("text/html")
                .body("<p>Could not find post - sorry!</p>")
        }
    };

    let parser = Parser::new_ext(&markdown_input, options);

    let mut html_output = String::new();
    html::push_html(&mut html_output, parser);

    context.insert("post", &html_output);
    context.insert("meta_data", &frontmatter);

    match tmpl.render("post.html", &context) {
        Ok(s) => HttpResponse::Ok().content_type("text/html").body(s),    
        Err(e) => {
            println!("{:?}", e);
            return HttpResponse::NotFound()
                .content_type("text/html")
                .body("<p>Could not find post - sorry!</p>")
        }
    }
}

Much of this is familiar, the difference here is that we make two variables available to our template: post and meta_data.

Last thing, we need to include the new handler in lib.rs and our handlers/mod.rs. I will leave that up to you!

After adding that, give cargo run a spin again and you should be able to click on the article from the home page, be re-directed to the post page and see your article!

Closing thoughts

I hope you enjoyed the walk-through and are interested in doing more web development with rust, it's in a really good state right now, with an exciting future ahead. Yes, the beginning might be a little bit frustrating and getting your head wrapped around ownership, borrowing and referencing can take some time. But I promise you, it's well worth it!

The compiler errors you get can be annoying, but will also guide you towards the correct path, and compared to something like Typescript it's a godsend. The more you write the easier it becomes.

It has quickly become my go-to language for writing small services as it plays very nice with aws lambdas and due to how fast it is, cold starts are not even an issue anymore. So, please take what you have here and extend it as much as you want.

Resources

Here is a collection of all the materials I've used to get started with rust. Some of it is free and some of it is paid, but all of them are highly recommendable so I hope you find some further learning!