Anyone who's been following my blog may have noticed it recently moved home, and got a complete redesign.
This all happened because of my decisions to dogfood my own static site generator.
A what, now?
In case you've been out of the loop, a static site generator is a program which allows you to have a CMS, without running a CMS.
You write all your content, keep it stashed safely in a source control, and then run a program that converts it all to static HTML pages.
It's really the natural progression from heavily caching dynamically generated sites.
When all your content are static, why put in the work to render it each time?
Why your own?
OK, so besides the fact I an a chronic wheel reinventer... no, actually, that was it.
I'd seen these tools around, and tried to imagine what I thought one would need to provide to be useful.
My first attempt, graaf, was functional, but revealed I didn't understand the problem space all that well.
It had no way to build navigation, for instance. Every page was rendered in isolation.
After a friend discussed an idea for a service based on SSGs, I decided to take another pass at the problem.
Pages and Content would be kept separate.
Sometimes you have content in your site that is shared amongst other parts of the site, and not rendered directly as a page on its own. Navigation is an obvious example here.
Object types and file types are separate.
Just because you read a
.ymlfile, doesn't mean all YAML files should be rendered the same way.
Besides the absolute minimum, all file loaders, content types, and else will be supplied by plugins.
Introducing gilbert !
It uses my own stencil template engine - and in using this, has prompted some significant improvements to the stencil template syntax!
A site has
templates/, and produces
It can have a
config.yml, which currently can provide defaults for
ContentTypes. Nothing else uses it yet. This was the cleanest way I could
see to solve problems like providing search paths for SCSS.
Any packages in the
gilbert.plugins namespace are imported, allowing them to:
- define subclasses of
gilbert.content.Content, automatically registered as new content
- register new file loaders on the
- register a
context providerfor adding globally accessible data to the template context.
Your site can also have a
plugins.py which will be loaded after all the other
By default plugins are provided for:
- YAML loader
- MarkdownPage content type
- SCSS loader and content type
- a Collection content type for defining filtered collections of content objects.
At its core, a Site is composed of
Content objects. These extend a validated
schema class created for the project. It uses python3 type annotations to allow
you to declare attributes and their acceptable types, which will be validated
from datetime import datetime from gilbert.content import Content class MyContent(Content): name : str when : datetime
Now when a loaded files specifies it's a "MyContent" type, it MUST contain a
name attribute which is a
str, and a
when that is a
datetime. If not,
an error will be raised, and rendering will stop.
This extends to python
typing types. You can declare Container, Mapping,
Union, and their variants.
Other constructs, such as Option, Any, or AnyStr are not yet supported, but should be easy to add.
Containers and Queries
Site object, there are two "containers", which hold the
Each of these provide a query interface called
.matches(), allowing you to
select content using a basic query language.
For instance, on this site I use a sub-class of Collection to build a list of blog posts.
all: - eq: - attr: ["content_type"] - "MarkdownPage" - startswith: - attr: ["name"] - "blog/" - attr: ['date']
This declares a top-level
all() across 3 terms.
The first says the
content_type attribute must equal "MarkdownPage".
The second says the
name (original filename) must start with
And finally, it must have a
Right now I'm not happy with the render speed, though a lot of that is actually the load speed, since MarkdownPage renders the markdown on load.
I would like to make the Content interface cleaner for separating intial content from rendered content, so it's easier to wrap in a one-shot property.
But most of all, I need other people to use it and tell me what's missing!