Building a website "from scratch" - Part 2/?

This is a continuation to the first part of this (maybe) ongoing series.


Motivation

I visited the web page of a fellow student, and apart from noticing that it looks really nice, I noticed that it had these nice "Previous post" and "Next post" buttons, where you could navigate through published posts in what I assume is a chronological ordering.

Because that looked like a neat feature to me, I decided to implement it on this page as well.

Rough Implementation Idea

I did something similar previously, linking together pages depending on some factors. At that time I did not use a database, because I didn't know how they worked. ¯\_(ツ)_/¯

Because that was a pain to do, I decided to do it in a more reasonable way this time: Using a database and running queries to decide which page should link to which other page.

I decided to use diesel, as it had a decent "Getting Started" page and looked nice. Diesel supports a range of database systems. I decided to use PostgreSQL, as that is what I need to know for this semesters database course.

Then, I need to save all posts in the database with some metadata: For now, I just save the url, page title, version, published status, date of creation, date of last update, and the content.

Editing interface

Now that I plan to store everything in the database, opening markdown files and editing them is not that straightforward anymore. In my first iteration, I annotated the markdown files on the top with metadata in yaml, and then only read the metadata from these files and store it in the database. This turned out to be annoying to work with (in the code), as now the metadata is stored in two places.

Due to that, I decided to store everything in the database, and not have any persistent files storing blog content. This introduced a new problem: now I need to create an interface to the database to write & edit the posts. As I have never really done UI before, and I prefer a text editor, I opted to read posts from the database, serialize the post to a file, open a text editor to edit that file, and upon closing of the editor store everything in the database again.

This turned out to work quite nicely.

Putting everything together

First I needed to download and set up PostgreSQL and Diesel. It was fairly monotonous, which is why I will skip that in this post.

Then I need to create a type in Rust corresponding to the database table:

use super::schema::posts;
use chrono::NaiveDateTime;
use serde::{Deserialize, Serialize};

#[derive(Queryable, Insertable, Serialize, Deserialize, Debug, PartialEq, Eq)]
#[table_name = "posts"]
pub struct Post {
    #[serde(skip, default)]
    pub url: String,
    pub title: String,
    #[serde(default)]
    pub version: String,
    pub published: bool,
    #[serde(skip, default = "now")]
    pub created: NaiveDateTime,
    #[serde(skip, default = "now")]
    pub updated: NaiveDateTime,
    #[serde(skip, default)]
    pub content: String,
}

The Queryable and Insertable derives in combination with the macro #[table_name = "posts"] tell Diesel that this struct can be queried from and inserted into the table named posts.

The Serialize and Deserialize derives implement the serialization and deserialization of the struct from and to yaml. The #[serde(skip)] annotations tell the serialization library that these fields should not be serialized and deserialized. The creation date and update date will be edited from code, not by editing the post, and the content will be read differently (not in the yaml metadata).


With the magic of automatic code generation of Diesel, I can now write queries like this:

let prev = posts
    .filter(created.lt(&this.created).and(published))
    .order_by(created.desc())
    .limit(1)
    .load::<Post>(db)?;

This code example is how the previous page is found in the database, in order to link it in the footer.

There are a few queries in the same style.


Adding new functionality based on the stored posts is also fairly easy, for example the code needed to generate the overview page is only the following short snippet:

pub fn overview(db: &PgConnection) -> AResult<String> {
    let mut body = String::from("<h1>Blog Posts</h1>");
    body += "<hr>";

    // query all published posts
    use crate::schema::posts::dsl::*;
    let sites = posts
        .filter(published)
        .order_by(created.desc())
        .load::<Post>(db)?;

    // build the table with all posts
    body += r#"<table class="post-list">"#;
    body += "<th>Post</th><th>Date</th>";
    for site in sites.iter() {
        body += &format!(
            r#"<tr><td><a href="{}">{}</a></td><td>{}</td></tr>"#,
            site.url,
            site.title,
            site.created.date().format("%d-%m-%Y")
        );
    }
    body += "</table>";

    // put the generated html body into the existing code skeleton
    let page = format!(
        include_str!("skeleton.html"),
        title = "Overview",
        body = body,
        bottom_navigation = "",
        copyright = copyright_years(
            &sites.last().unwrap().created,
            &sites.first().unwrap().created
        )
    );

    // return the generated page
    Ok(page)
}

Future plans

As of now, the website is still quite bare bones. I don't even have an "about me" page!

So I definitely plan on expanding the page, and when the code is in a suitable shape, I will maybe release it on my GitHub profile.


Tags: CODING WEBSITE