Monochrome Static Site Generator¶

i. About¶

A custom-built Python Static Site-Generator(SSG) used to generate pages for my personal website and blog. It's used to fulfil the "back-end" needs of my website hosted here on GitHub Pages.

Called "Monochrome" because it was for being built for a website that was rendered using mostly black and white, and a hint of grey. However, before I could finish this script, I added a dark-mode that's got a lovely shade of pink as the highlight colour.

The name stuck due to the argument that the pink is the only "chrome" in the surrounding greyscale. Though it was mostly because I could not think of anything else.

The code was inspired by Jahongir Rahmonov's tutorial.

ii. Python Libraries Used¶

  • os
  • datetime
  • markdown2
  • jinja2
  • tkinter

iii. Markdown File Set-Up¶

The markdown files need to begin with certain YAML style metadata that will provide the SSG the necessary information to style the page.

Each file will begin with the following block of information:

---
page-title: {{Title of the page}}
page-description: {{Description of the page}}
main-class: {{CSS class to be used}}
title: {{Title of the blog post}}
date: {{Date of the blog post}}
tags: {{Tags relating to the blog post}}
thumbnail: {{Thumbnail image}}
summary: {{Summary of the page}}
slug: {{Filepath to the post}}
---
{{Blank line}}

*The block must be followed by an empty line.*

iv. HTML Template Set-Up¶

Blog Post HTML file:¶

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
<h1>{{post.title}}</h1>

<small>{{post.date}}</small>

{{post.content}}
</body>
</html>

Index / Recents HTML file:¶

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
{% for post in posts %}
  <p>
      <h2>{{loop.index}}: <a href="/blog/posts/{{ post.slug }}.html">{{post.title}}</a> <small>{{post.date}}</small></h2>
      {{post.summary}}
  </p>
{% endfor %}
</body>
</html>

v. Directory Structure¶

.
├── drafts
│   ├── crazy-ideas.md
│   └── writers-block.md
├── content
│   ├── 2022-09-10-arson.md
│   └── 2022-19-11-boiled-egg.md
├── posts
│   ├── arson-crime-or-hobby.html
│   └── my-life-as-a-boiled-egg.html
├── template
│   ├── all-posts-template.html
│   |── blog-template.html
|   └── recent-posts-template.html
├── all-posts.html # list of all posts of the blog
└── index.html # contains only 5 recent posts in case of my blog

1. Importing Libraries¶

In [ ]:
import os
from datetime import datetime
from jinja2 import Environment, PackageLoader
from markdown2 import markdown
from tkinter import *
from tkinter import filedialog

label_file_explorer = object

2. Setting-up Funtions¶

a. Select the Input Markdown File¶

In [ ]:
def inputFile():
    # Asks user to select the source file to be converted.
    # Supports .md and .txt written in Markdown format
    global input_filename
    input_filename = filedialog.askopenfilename(
        initialdir= ".",
        filetypes= (
            ("All files", "*.*"),
            ("Markdown files","*.md"),
            ("Text files", "*.txt")
        )
    )
    label_file_explorer.configure(text="File Opened: "+input_filename)

b. Render a Blog with the Input File¶

In [ ]:
def newBlog():
    with open(input_filename, 'r') as file:
        parsed_md = markdown(file.read(), extras=['metadata'])

        env = Environment(loader=PackageLoader('package', 'templates'))
        # seems to work only with a blank package.py file in the project root.
        post_detail_template = env.get_template('post-detail.html')

        data = {
            'content': parsed_md,
            'title': parsed_md.metadata['title'],
            'date': parsed_md.metadata['date']
        }

    blog_html_content = post_detail_template.render(post=parsed_md.metadata)
    blog_filepath = 'output/posts/{slug}.html'.format(slug=parsed_md.metadata['slug'])

    with open('blog_filepath', 'w') as file:
        file.write(blog_html_content)
    
    label_file_explorer.configure(text="Blog Rendered")

c. Updating the "All Posts" and "Recents" Pages¶

In [ ]:
def indexUpdate():
    BLOG = {}

    label_file_explorer.configure(text="Updating index...")

    for blog_md in os.listdir('content'):
        file_path = os.path.join('content', blog_md)

        with open(file_path, 'r') as file:
            BLOG[blog_md] = markdown(file.read(),extras=['metadata'])


    BLOG = {
        post: BLOG[post] for post in sorted(
            BLOG, key=lambda post: datetime.strptime(
                BLOG[post].metadata['date'], '%Y-%m-%d'),
                reverse=True
        )
    }

    env = Environment(loader=PackageLoader('monochrome', 'templates'))
    all_posts_template = env.get_template('all-posts-template.html')
    recent_posts_template = env.get_template('recent-posts-template.html')

    index_blog_metadata = [
        BLOG[post].metadata for post in BLOG
    ]

    all_posts_html = all_posts_template.render(posts=index_blog_metadata)

    recents = []
    blogsize = len(index_blog_metadata) 

    if blogsize < 5:
        r = blogsize
    else:
        r = 5
    
    i=0
    while i < r:
        recents.append(index_blog_metadata[i])
        i = i + 1
    
    recent_posts_html = all_posts_template.render(posts=recents)


    with open('output/all.html', 'w') as file:
        file.write(all_posts_html)
    with open('output/index.html', 'w') as file:
        file.write(recent_posts_html)

    label_file_explorer.configure(text="Index Updated")

3. Ugly GUI Set-Up¶

Using TKinter

In [ ]:
window = Tk()
window.title('Monochrome SSG v-01')
window.geometry("500x200")
window.config(background = "white")

label_file_explorer = Label(window,
							text = "Please Select the Input file",
                            width= 60,
							fg = "black",
                            bg="white")

button_inputFile = Button(window,
                            text = "Input File",
                            command = inputFile)

button_newBlog = Button(window,
                            text = "Render Blog",
                            command = newBlog)
                        
button_indexUpdate = Button(window,
                            text = "Update Index",
                            command = indexUpdate)

button_exit = Button(window,
                            text = "Exit",
                            command = exit)

label_file_explorer.grid(column = 1, row = 1)
button_inputFile.grid(column = 1, row = 2)
button_newBlog.grid(column = 1, row = 3)
button_indexUpdate.grid(column = 1, row = 4)
button_exit.grid(column = 1, row = 5)

window.mainloop()

The GUI currently looks like this:

"Monochrome SSG v01 - Ugly GUI"

A. Future Work¶

  • Add templates for non-blog pages.
  • Add functionality to update templates.
  • Add functionality to regenerate pages with new templates.
  • Add functionality to import .ipynb HTML export.