padraic.xyz

What Runs this Site

One of the problems I had when I first learned how to programme was that I hated using other peoples' code. I guess I was falling for the myth of the genius programmer. I truly felt that I had to understand everything, from the ground up, and obviously that made even the simplest tasks totally daunting. It wasn't until I joined voXup that I got comfortable with building on existing platforms.

All that said, I do sort of believe that reinventing the wheel a little bit is an essential part of learning to code. And as such, this site is basically just that; a static-file blogging site kind of like Jekyll, built using Python and Markdown.

I really like Python. It was the language we used to build the backend of voXup, and since then it's been my go-to language for any code I've written outside of an academic setting. But, again, wanting to understand everything, I didn't feel like hosting all my personal projects on Google App Engine. (For one thing, it'd be complete overkill resource-wise.) So, last year, I started messing around with Flask, first using it to build a dumb little site that would Yo you walking directions to the nearest tube-stop.

What I like about Flask is that it keeps everything simple. Similarly to how App Engine works, you can define everything in one file if you feel like it; far cry from the strict regimenting of a Django project. There's also very little required to get started. A 'Hello, world' in flask looks like this:

from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello():
    return 'Hello, world!'

if __name__ == '__main__':
    app.run()

And running it gives you a webapp on port 5000 that'll shout Hello World at you!

It's still only version 0.10, but Flask already has a lot of extensions available that add a tonne of functionality. Thanks to two of these addons, this core of this site is less than a 100 lines of code!

I decided early on that I wanted to be able to write posts in Markdown, and so I use Flask-FlatPages to render the files as HTML, and then pass these objects into my templates. That's as simple as adding

pages = Pages(app)

give or take some configuration to point to where the markdown files are on disk. The pages object is then a generator, with all your pages in it as objects ready to be passed in to Jinja and rendered just by adding a {{page}} tag!

I also decided (again, as I wanted to do absolutely everything in this project) that I was going to self-host, using my old Raspberry Pi Model-B (with 256MB of RAM; it's an old Pi). So, to keep things light, I figured this would be a static-file site, pre-rendering all the pages so they can be served up with minimal effort on the Pi's part.

Dusty shelves...

Frozen-Flask came to the rescue with exactly the functionality I needed. This took a little bit more configuration than FlatPages to get going, mostly to set up pagination on the main index (which is sort of putting the cart before the horse as this blog has almost no content as it stands.) Once the url generators were in place, it's as simple as calling freezer.freeze() to export everything to /var/www/Static.

The pages themselves are kept in a private Git repo. Eventually I'd like to use webhooks to push changes, but as it stands my Pi has a cron-task that runs hourly to check for changes, pull them down and re-freeze the site.

As for server configuration, I put much more effort in to securing it (but not too much, thanks to these guides ) than anything else. Actually serving the files was as easy as editing the default server config to point to the folder mentioned above, and installing a Dynamic DNS client to update my host as this is being run from my living room.

This whole project is hosted over on Github, but I've stuffed the core of the site down below because it's nice and simple, and because there's something meta about a server hosting the code that runs it. I hope, in the long run, that I could package this up into a little extensible tool for people wanting to make small static sites for themselves.

import markdown

from flask import Flask, render_template, render_template_string
from flask_flatpages import FlatPages, pygmented_markdown, pygments_style_defs
from flask_frozen import Freezer
from os import listdir, path

class Testing():
    DEBUG = True
    FLATPAGES_MARKDOWN_EXTENSIONS = ['codehilite', 'mdx_math', 'sane_lists', 'mdx_gfm']
    FREEZER_DESTINATION = '../../Files'
    FREEZER_BASE_URL = 'http://localhost'
    FREEZER_REMOVE_EXTRA_FILES = True
    FLATPAGES_EXTENSION = '.md'
    TWITTER = 'https://twitter.com/padraic_padraic'
    GITHUB = 'https://github.com/padraic-padraic'
    APP_DIR = path.dirname(path.abspath(__file__))
    SIDEBAR_TITLE = "Padraic Calpin?"
    PAGE_TITLE = 'My blog is a blog'

class Config(Testing):
    DEBUG = False
    FREEZER_BASE_URL = 'http://calpin.me'
    # FREEZER_DESTINATION = '/var/www/Static'

pages = FlatPages()
app = Flask(__name__)
app.config.from_object(Config())
pages.init_app(app)
freezer = Freezer(app)

@freezer.register_generator
def archive():
    posts = [p for p in pages if p.path != 'about']
    _pages= int(len(posts)/10)
    if _pages == 0:
        yield {'_page': 1}
    else:
        for n in range(1,_pages):
            yield {'_page': n}

@freezer.register_generator
def page():
    for filename in listdir(app.config['APP_DIR']+'/pages'):
        if filename != 'about' or filename.split('.')[1] != 'md':
            yield{'path':filename.split('.')[0]}


@app.route('/')
@app.route('/<int:_page>/')
def archive(_page=1):
    _pages = sorted((p for p in pages if 'published' in p.meta),
                    reverse=True, key=lambda p: p.meta['published'])
    posts = _pages[(_page-1)*10:_page*10]
    for post in posts:
        bits = post.body.split('\n')[:7]
        for index, bit in enumerate(bits):
            if not bit:
                bits[index] = "\n"
        post.meta['extract'] = pygmented_markdown("\n".join(bits))
    if post.meta['extract'] == post.body:
        post.meta['read_more'] = False
    else:
        post.meta['read_more'] = True
    _next = len(_pages[_page*10:(_page+1)*10])>0
    return render_template('index.html', posts=posts, next=_next, nextpage=_page+1,
                           prevpage=_page-1)

@app.route('/<path:path>/')
def page(path):
    _page = pages.get_or_404(path)
    return render_template('page.html', page=_page)

@app.route('/about/')
def about():
    _page = pages.get_or_404('about')
    return render_template('page.html', page=_page)

@app.route('/pygments.css')
def pygments_css():
    return pygments_style_defs('tango'), 200, {'Content-Type': 'text/css'}

if __name__ == '__main__':
    #app.config.from_object(Testing())
    #app.run(port=5003)
    freezer.freeze()