Blog20 Static Site Generator

This is my personal static site generator, not intended to be used by anyone who isn't me. Feel free to use it if you want, just don't expect much in the way of documentation or tech support.

It is built using waf, all content is 100% statically generated, and the site uses no javascript (although individual pages could have javascript).

It uses python-markdown to render markdown content, which includes support for code hilighting via pygments. The site's custom CSS also includes light and dark mode variants using media selectors.

It also uses the css-html-js-minify project to shrink generated content, which actually results in some pretty decent space savings for CSS. The HTML minifier isn't used because it was breaking my styles and the space savings were negligible anyways.

Usage

Some basic instructions on how to use it. Assumes basic knowledge of Waf.

Installing dependencies

This tool requires Python 3 (untested on Python 2.x), and the following dependencies from pip:

  • Markdown
  • Pygments
  • css-html-js-minify
  • Pillow
  • pygifsicle

Install them all as follows:

$ python3 -m pip install Markdown Pygments css-html-js-minify Pillow pygifsicle

Note however that pygifsicle also requires you to have gifsicle installed system-wide.

Creating Pages

Example from About page:

def build(bld):
    bld(
        target = "About",
        features = "page_template",
        source = "About.md",
        template = 'MyTemplate.html',
        navmenu = [
            "Home",
            "Blog",
            "Projects",
            "Contact",
            ("Gitlab", "https://gitlab.com")
        ],
    )

target is the output name of the page (translates to About.html in this example). The .md files in the source attribute will automatically be compiled into HTML.

The page_template feature will generate a page in the static output folder using the given template and navmenu.

template must point to an html file which is to be populated with the compiled .md content and mamvmenu. A valid template requires two elements:

  • A div with the id attribute "NavMenu"
  • A article with the id attribute "MainContent"

If template or navmenu are omitted, the environment variables tpl_main and NavMenu will be used.

NavMenu must be a list of strings, where each entry is a named generator. If no matching generator is found, the link will just point to '#'. You can also use a Tuple to add a custom link.

Copyfiles feature

The copyfiles feature string (used like page_template in the above example) allows you to specify a copyfiles attribute, where you can list source files that will be processed and copied to the static output directory. Processing depends on the file extension. The only processing done currently is that .css and .js files are passed through the minifier.

Automated image file conversions

You can also have waf automatically convert all of your images into a common format, as well as scale them down to a maximum size. In the example below, every image that passes through copyfiles will be converted to WebP and proportionally scaled down if any dimension exceeds 512 pixels. Additionally, gifs will be optimized using gifsicle:

def configure(conf):
    conf.env.MAX_IMG_DIMENSION = 512
    conf.env.IMAGE_FMT_OUT = 'webp'
    #...

def build(bld):
    bld(
        features = "copyfiles",
        copyfiles = bld.path.ant_glob(["*.png", "*.jpg", "*.gif", "*.webp", "*.psd"]),
        convert_images = True,
        shrink_images = True,
        gif_options = {
            "optimize": True,
            "colors": 128,
            "gifsicle_options": ['--verbose', '--lossy=80', '-O3']
        },
    )

When writing a Markdown file, you need to be sure to link to the original image file, and the generator will automatically update the links to the newly generated files. For example:

My dog:

![Picture of my dog](photos/napoleon.png)

That Markdown will be automatically converted to the following during compilation:

My dog:

![Picture of my dog](photos/napoleon.webp)

Creating Index Pages

An index page is like a folder which contains other pages. The About example above only had one source file, but adding multiple source files will automatically convert it into an index page. This means that instead of a single About.html file in the static output directory, there will be an About folder that contains all of the compiled files from the source attribute. Also note that the first file in the list will be named index.html, so visting the folder will show the contents of that file only.

The index feature string tells waf to generate a index.html page (instead of using the first source entry) which contains a list of links to all of the other files in the folder. See the Blog example below:

bld(
    target = "Blog",
    features = "page_template copyfiles index",
    copyfiles = bld.path.ant_glob("images/*"),
    source = bld.path.ant_glob("*.md", excl="index.md"),
    custom_index = "index.md",
    use = [n.name for n in bld.path.ant_glob("*", excl=["images"], src=False, dir=True)],
)

Simply specifying "index" will achieve the desired result, but you can also customize the generated index page with the custom_index attribute. This should be a .md file which includes the string "$items" somewhere. This will be substituted for the list of links to items.

For example:

Welcome to my blog! Below are all of the things I've written:

$items

That's it!

Creating a Series of Pages

A series is like a linked list of individual pages. It's similar to an Index page, but additionally includes navigation links towards the bottom to go to the next/prev page. Example:

def build(bld):
    bld(
        features = "series index page_template copyfiles", 
        target = "Summoner-Brown", 
        copyfiles = bld.path.ant_glob("*.png"),
        static_root = "Blog",
        custom_index = "index.md",
        pages = [
            "Prologue.md",
            "Part1.md",
            "Part2.md",
            "Part3.md",
            "Part4.md",
            "Part5.md",
        ]
    )

This example defines an index page with a custom index (page_template and index feature strings). It also uses the series feature string, and has all of the source files listed in the pages attribute rather than the source attribute.

This is because the series feature will process the pages list to build a navigation and other info, and then add those to the source attribute for regular processing into an index page. You could use ant_glob to populate the pages, but doing it manually like in the example above allows you to control the ordering.

The example above uses the static_root attribute to specify where in the static output folder this series should be placed. In this example, it's set to "Blog", so it will go into the Blog subfolder. Without the static_root attribute, the series will be placed at the root of the static output folder. This is a problem if the source files for the series are located in a subfolder of another index page. When this occurs, the series will render as an item in the that index page, but the links to it won't work.

Nested Series

You can also have nested pages in your series by adding tabs to your ordered list of pages. See the example below:

def build(bld):
    bld(
        features = "series index page_template copyfiles", 
        target = "Summoner-Brown", 
        copyfiles = bld.path.ant_glob("*.png"),
        static_root = "Blog",
        custom_index = "index.md",
        pages = [
            "Prologue.md",
            "   Part1.md",
            "       Part2.md",
            "   Part3.md",
            "       Part4.md",
            "           Part5.md",
        ]
    )

The only thing this does is affect the way index pages are rendered. Nested items will be indented using CSS.

Markdown Metadata

You can add some metadata to your pages by prepending your markdown with a json string followed by exactly five hypens as shown below:

{
    "title": "My Cool Page",
    "date": "2020-12-03/13:30"
}
-----
Hey everyone, this is my cool page with a date and title!

The title attribute will be automatically added as a header to the top of the article, and to the title property of the window. You can omit the title, and a title will automatically be generated based on the filename. hyphens (-) in the filename will be converted to spaces when rendering titles. If you want to add a hyphen to your title via the json metadata, you need to escape it with a \.

Example:

{
    "title": "That's one big\-ass boat!",
    "date": "2020-12-03/13:30"
}
-----
![Big-ass boat](images/big-ass-boat.png)

The date should be in the following custom format: YYYY-MM-DD/hh:mm:ss. It will be automatically added as a subtitle under the title header, and shown on index pages.

date can be omitted completely, or you can choose to omit the time only. Examples of valid date strings:

  • 2020-12-03/13:30:54
  • 2020-12-03/13:30
  • 2020-12-03

OpenGraph Tags

The following keys will be used to generate opengraph tags if present in the metadata json:

Json Key OpenGraph Tag
title og:title *
description og:description
image og:description
type og:type
url og:url *
locale og:locale

* Automatically generated if omitted

You can also manually override the tag values by using the OpenGraph tag as the key name. For example:

{
    "title": "Blog",
    "og:title": "Alex's Blog"
}

That will use the string Blog when rendering the page/title bar, and Alex's Blog as the OpenGraph title tag.

Build Options

You can use the following options during configuration to customize the build:

  • --no-gif-optimizer disables all gif optimization tasks. This is useful when you need quick rebuilds, as optimizing gifs is slow
  • --img-shrink-maximum sets the maximum dimension when shrinking images. An image with a dimension larger than this will be proportionally shrunk
  • --img-convert-format sets the output format used when converting images. The default is "webp"
  • --snd-convert-format sets the output format used when converting sounds. The default is "mp3" (audio file conversions not currently implemented)