Let's split this weird one page blog into multiple pages!
Currently all articles are written into a single page (index.html
) in alphabetical order based on the name of the markdown files. Makes sense right? From a technical viewpoint maybe, but not really. So what I want to do is "simple", create an index page with an overview of articles, and for every article create a page (so.. more than one page, you see). Ofcourse I also want the articles to be sorted in order of being published, which will require us to add a bit of metadata to the markdown source of the articles. Let's get started.
To add metadata to a markdown file, you can add a frontmatter block (usually at the front..). The frontmatter block can then be used to set variables such as the date an article was published, the state of an article (is it a draft?), etc. Frontmatter is often written as YAML, but we will use toml instead, just because. The metadata we need at the moment includes publish date but preferably also the slug
for the article, which is part of the URL that the reader will see when reading an article, so we can set it to something different than the markdown filename, the date or a combination of the two.
+++
date = "2022-10-27"
slug = "more-than-one-page"
+++
We will use the serde
and the toml
crates to deserialize frontmatter to our Metadata type:
let mut frontmatter: Option<FrontMatter> = None;
// our parser loop
loop {
// ...
if line.starts_with("+++") {
let mut inner = String::new();
loop {
// read all lines
}
frontmatter =
Some(toml::from_str(&inner).expect("Error deserializing frontmatter block"));
}
// ...
}
if let Some(frontmatter) = frontmatter {
Page {
uri: format!("articles/{}.html", &frontmatter.slug), // page now has a uri
elements,
frontmatter // page now has a FrontMatter struct
}
} else {
// do something dirty
}
In our writer we use the new uri
to write each post to their own html file.
for page in blog.pages {
fs::write(
format!("../www/{}", page.uri),
page.render(),
)?;
}
We now have all our blog posts in their own separate file! At the time of writing:
blog/
├─ www/
│ ├─ articles/
│ | ├─ build-your-own-blog.html
│ | ├─ css-for-mobile.html
│ | ├─ more-than-one-page.html
│ | ├─ run-ronnie-runner.html
│ |
| ├─ (index.html) <- What about ME???
It turns out splitting the blog posts to multiple files was actually the easy part, but now we need an index page!
Currently we parse markdown to our model, then render the model to files, according to this flow:
┌──────────┐ ┌──────────────────┐ ┌───────────┐
│ Markdown │ parse() │ Internal │ write() │ HTML │
│ ├─────────►│ Representation ├─────────►│ │
└──────────┘ └──────────────────┘ └───────────┘
But neither the parser nor the writer makes sense to actually add our index, so I decided to add a process()
step, as below
┌──────────┐ ┌───────────────────┐
│ Markdown │ parse() │ Internal │
│ ├─────────►│ Representation I |
└──────────┘ └───────┬───────────┘
│
process()
│
┌───────▼───────────┐ ┌───────────┐
│ Internal │ write() │ HTML │
│ Representation II ├─────────►│ │
└───────────────────┘ └───────────┘
Our main loop then becomes:
fn main() -> Result<(), std::io::Error> {
let mut blog = parse_blog()?;
process_blog(&mut blog); // added
write_blog(blog)?;
Ok(())
}
In process_blog(...)
we create an IndexEntry for each page:
pub struct IndexEntry {
pub title: String,
pub date: NaiveDate,
pub summary: String,
pub uri: String,
}
impl Element for IndexEntry {
// ...
}
Then we add an extra page with the index entries as elements:
pub fn process_blog(mut blog: &mut Blog) {
blog.pages.sort_by(|a, b| b.frontmatter.date.cmp(&a.frontmatter.date));
let elements = blog
.pages
.iter()
.map(|page| -> Box<dyn Element> {
Box::new(IndexEntry {
date: page.frontmatter.date,
title: page.elements[0].render(),
summary: page.elements[1].render(),
uri: page.uri.clone()
})
})
.collect::<Vec<_>>();
blog.pages.push(Page {
elements,
frontmatter: FrontMatter {
slug: String::from("index"),
date: NaiveDate::from_ymd(1900, 1, 1),
},
uri: String::from("index.html"),
});
}
We fill the index element with the first 2 elements of a page, which should be the title and the first paragraph. Should be good enough for now :-)
Personally I like it when there are as few distractions on the page of a blog post as possible, but we do need one thing now that we split our articles to multiple pages, being, a means to return to our index page. To do this, I have added this simple snippet to the page model:
<div class="nav">
<a href="https://ron.srht.site"><h1 class="nav">~ronblog</h1></a>
</div>
My fingers are itching to do more improvements to this static site generator, for instance the change to support inline code, bold and italic text, etcetera. But let us resist the urge to scope creep and save these changes for later. Check out the source code here