Simple, lightweight, and magic-free static site/blog generator for Python coders

Related tags


Take full control of your static website/blog generation by writing your own simple, lightweight, and magic-free static site generator in Python. That's right! Reinvent the wheel!

View Source View Demo MIT License



This repository contains the source code of an example website containing two static blogs and a few static pages. The website can be generated by running The output looks like this. That's it!

So go ahead, fork this repository, replace the content with your own, and generate your static website. It's that simple!

You are free to copy, use, and modify this project for your blog or website, so go ahead and fork this repository and make it your own project. Change the layout if you wish to, improve the stylesheet to suit your taste, enhance if you need to, and develop your website/blog just the way you want it.

But Why?

For fun and profit! Okay, maybe not for profit, but hopefully for fun.

Have you used a popular static site generator like Jekyll to generate your blog? I have too. It is simple and great. But then did you yearn to use something even simpler to generate your blog? Do you like Python? Perhaps the thought of writing your own static site generator crossed your mind but you thought it would be too much work? If you answered "yes" to these questions, then this project is for you.

With, you are in full control. There is no hidden magic! There is no need to read any documentation to understand how it works. There is no need to learn how to write configuration files to produce some desired effect.


  • The code is the documentation.
  • The code is the configuration.

Everything is laid out as plain and simple Python code for you to read and enhance. It is less than 130 lines of code (excluding comments, docstrings, and blank lines). It gets you off the ground pretty quickly. You only need to execute

You can develop a decent website/blog within a few minutes and then you can begin tinkering with the source code, the layout, and the stylesheet to customize the look and feel of your website to your satisfaction.

Get Started

This section provides some quick steps to get you off the ground as quickly as possible.

  1. For a quick demo on your local system, just enter this command:

    make serve

    If you don't have make but have Python 3.x, enter this command:

    cd _site
    python3 -m http.server

    Note: In some environments, you may need to use python instead of python3 to invoke Python 3.x.

    If you only have Python 2.7, enter this command:

    cd _site
    python -m SimpleHTTPServer

    Then visit http://localhost:8000/. It should look like this.

    Note: You can run with Python 2.7 or Python 3.x.

  2. You may see a few Cannot render Markdown warning messages in the output of the previous command. This is due to the fact that an example blog in this project has a few posts written in Markdown. To render them correctly, install the commonmark package with this command:

    pip install commonmark

    Then try the previous step again.

  3. For an Internet-facing website, you would be hosting the static website/blog on a hosting service and/or with a web server such as Apache HTTP Server, Nginx, etc. You probably only need to generate the static files and know where the static files are and move them to your hosting location.

    If you have the make command, enter this command to generate your website:

    make site

    If you don't have make but have python3, enter this command:


    Note: In some environments, you may need to use python instead of python3 to invoke Python 3.x.

    If you only have python, enter this command:


    The _site directory contains the entire generated website. The content of this directory may be copied to your website hosting location.

The Code

Now that you know how to generate the static website that comes with this project, it is time to see what does. You probably don't really need to read the entire section. The source code is pretty self-explanatory but just in case, you need a detailed overview of what it does, here are the details:

  1. The main() function is the starting point of website generation. It calls the other functions necessary to get the website generation done.

  2. First it creates a fresh new _site directory from scratch. All files in the static directory are copied to this directory. Later the static website is generated and written to this directory.

  3. Then it creates a params dictionary with some default parameters. This dictionary is passed around to other functions. These other functions would pick values from this dictionary to populate placeholders in the layout template files.

    Let us take the subtitle parameter for example. It is set to our example website's fictitious brand name: "Lorem Ipsum". We want each page to include this brand name as a suffix in the title. For example, the about page has "About - Lorem Ipsum" in its title. Now take a look at the page layout template that is used as the layout for all pages in the static website. This layout file uses the {{ subtitle }} syntax to denote that it is a placeholder that should be populated while rendering the template.

    Another interesting thing to note is that a content file can override these parameters by defining its own parameters in the content header. For example, take a look at the content file for the home page. In its content header, i.e., the HTML comments at the top with key-value pairs, it defines a new parameter named title and overrides the subtitle parameter.

    We will discuss the syntax for placeholders and content headers later. It is quite simple.

  4. It then loads all the layout templates. There are 6 of them in this project.

    • layout/page.html: It contains the base template that applies to all pages. It begins with <!DOCTYPE html> and <html>, and ends with </html>. The {{ content }} placeholder in this template is replaced with the actual content of the page. For example, for the about page, the {{ content }} placeholder is replaced with the the entire content from content/about.html. This is done with the make_pages() calls further down in the code.

    • layout/post.html: It contains the template for the blog posts. Note that it does not begin with <!DOCTYPE html> and does not contain the <html> and </html> tags. This is not a complete standalone template. This template defines only a small portion of the blog post pages that are specific to blog posts. It contains the HTML code and the placeholders to display the title, publication date, and author of blog posts.

      This template must be combined with the page layout template to create the final standalone template. To do so, we replace the {{ content }} placeholder in the page layout template with the HTML code in the post layout template to get a final standalone template. This is done with the render() calls further down in the code.

      The resulting standalone template still has a {{ content }} placeholder from the post layout template template. This {{ content }} placeholder is then replaced with the actual content from the blog posts.

    • layout/list.html: It contains the template for the blog listing page, the page that lists all the posts in a blog in reverse chronological order. This template does not do much except provide a title at the top and an RSS link at the bottom. The {{ content }} placeholder is populated with the list of blog posts in reverse chronological order.

      Just like the post layout template , this template must be combined with the page layout template to arrive at the final standalone template.

    • layout/item.html: It contains the template for each blog post item in the blog listing page. The make_list() function renders each blog post item with this template and inserts them into the list layout template to create the blog listing page.

    • layout/feed.xml: It contains the XML template for RSS feeds. The {{ content }} placeholder is populated with the list of feed items.

    • layout/item.xml: It contains the XML template for each blog post item to be included in the RSS feed. The make_list() function renders each blog post item with this template and inserts them into the layout/feed.xml template to create the complete RSS feed.

  5. After loading all the layout templates, it makes a render() call to combine the post layout template with the page layout template to form the final standalone post template.

    Similarly, it combines the list layout template template with the page layout template to form the final list template.

  6. Then it makes two make_pages() calls to render the home page and a couple of other site pages: the contact page and the about page.

  7. Then it makes two more make_pages() calls to render two blogs: one that is named simply blog and another that is named news.

    Note that the make_pages() call accepts three positional arguments:

    • Path to content source files provided as a glob pattern.
    • Output path template as a string.
    • Layout template code as a string.

    These three positional arguments are then followed by keyword arguments. These keyword arguments are used as template parameters in the output path template and the layout template to replace the placeholders with their corresponding values.

    As described in point 2 above, a content file can override these parameters in its content header.

  8. Then it makes two make_list() calls to render the blog listing pages for the two blogs. These calls are very similar to the make_pages() calls. There are only two things that are different about the make_list() calls:

    • There is no point in reading the same blog posts again that were read by make_pages(), so instead of passing the path to content source files, we feed a chronologically reverse-sorted index of blog posts returned by make_pages() to make_list().
    • There is an additional argument to pass the item layout template as a string.
  9. Finally it makes two more make_list() calls to generate the RSS feeds for the two blogs. There is nothing different about these calls than the previous ones except that we use the feed XML templates here to generate RSS feeds.

To recap quickly, we create a _site directory to write the static site generated, define some default parameters, load all the layout templates, and then call make_pages() to render pages and blog posts with these templates, call make_list() to render blog listing pages and RSS feeds. That's all!

Take a look at how the make_pages() and make_list() functions are implemented. They are very simple with less than 20 lines of code each. Once you are comfortable with this code, you can begin modifying it to add more blogs or reduce them. For example, you probably don't need a news blog, so you may delete the make_pages() and make_list() calls for 'news' along with its content at content/news.


In this project, the layout template files are located in the layout directory. But they don't necessarily have to be there. You can place the layout files wherever you want and update accordingly.

The source code of that comes with this project understands the notion of placeholders in the layout templates. The template placeholders have the following syntax:

{{ <key> }}

Any whitespace before {{, around <key>, and after }} is ignored. The <key> should be a valid Python identifier. Here is an example of template placeholder:

{{ title }}

This is a very simple template mechanism that is implemented already in the For a simple website or blog, this should be sufficient. If you need a more sophisticated template engine such as Jinja2 or Cheetah, you need to modify to add support for it.


In this project, the content files are located in the content directory. Most of the content files are written in HTML. However, the content files for the blog named blog are written in Markdown.

The notion of headers in the content files is supported by Each content file may begin with one or more consecutive HTML comments that contain headers. Each header has the following syntax:

<!-- <key>: <value> -->

Any whitespace before, after, and around the <!--, <key>, :, <value>, and --> tokens are ignored. Here are some example headers:

<!-- title: About -->
<!-- subtitle: Lorem Ipsum -->
<!-- author: Admin -->

It looks for the headers at the top of every content file. As soon as some non-header text is encountered, the rest of the content from that point is not checked for headers.

By default, placeholders in content files are not populated during rendering. This behaviour is chosen so that you can write content freely without having to worry about makesite interfering with the content, i.e., you can write something like {{ title }} in the content and makesite would leave it intact by default.

However if you do want to populate the placeholders in a content file, you need to specify a parameter named render with value of yes. This can be done in two ways:

  • Specify the parameter in a header in the content file in the following manner:

    <!-- render: yes -->
  • Specify the parameter as a keyword argument in make_pages call. For example:

    blog_posts = make_pages('content/blog/*.md',
                            '_site/blog/{{ slug }}/index.html',
                            post_layout, blog='blog', render='yes',


Here are some frequently asked questions along with answers to them:

  1. Can you add feature X to this project?

    I do not have any plans to add new features to this project. It is intended to be as minimal and as simple as reasonably possible. This project is meant to be a quick-starter-kit for developers who want to develop their own static site generators. Someone who needs more features is free to fork this project repository and customize the project as per their needs in their own fork.

  2. Can you add support for Jinja templates, YAML front matter, etc.?

    I will not add or accept support for Jinja templates, YAML front matter, etc. in this project. However, you can do so in your fork. The reasons are explained in the first point.

  3. Do you accept any new features from the contributors?

    I do not accept any new features in this project. The reasons are explained in the first point.

  4. Do you accept bug fixes and improvements?

    Yes, I accept bug fixes and minor improvements that do not increase the scope and complexity of this project.

  5. Are there any contribution guidelines?

    Yes, please see

  6. How do I add my own copyright notice to the source code without violating the terms of license while customizing this project in my own fork?

    This project is released under the terms of the MIT license. One of the terms of the license is that the original copyright notice and the license text must be preserved. However, at the same time, when you edit and customize this project in your own fork, you hold the copyright to your changes. To fulfill both conditions, please add your own copyright notice above the original copyright notice and clarify that your software is a derivative of the original.

    Here is an example of such a notice where a person named J. Doe wants to reserve all rights to their changes:

    # Copyright (c) 2018-2019 J. Doe
    # All rights reserved
    # This software is a derivative of the original
    # The license text of the original is included below.

    Anything similar to the above notice or something to this effect is sufficient.


Thanks to:

  • Susam Pal for the initial documentation and the initial unit tests.
  • Keith Gaughan for an improved single-pass rendering of templates.


This is free and open source software. You can use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of it, under the terms of the MIT License.

This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND, express or implied. See the MIT License for details.


To report bugs, suggest improvements, or ask questions, please visit

  • Vars


    Create a file to store website vars in order to allow an easier website customization.

    Also adds the following features:

    • Support for multiple optional environments (with different document root, base path and URL).
    • Add support for template variables ({{ some_var }}) in page content
    • Add support to add new sections (beyond blog and news)
    • Add support to switch file extension (.md, .html...) in each section
    opened by gabfl 7
  • Make it work in modern versions.

    Make it work in modern versions.

    1. Solves DeprecationWarning in regexes using raw strings.
    2. Solves issue #13 - renames module CommonMark to commonmark - CommonMark was an alias, but it was removed in commit 4ee43fbfbc0e3c6f42b8ddb31f2d12cdc3d34c04
    opened by paper42 5
  • Proposal: Good template engine

    Proposal: Good template engine

    opened by orsinium 4
  • Improved, single-pass templating.

    Improved, single-pass templating.

    Relates to #5. All tests pass.

    opened by kgaughan 4
  • Fix Makefile

    Fix Makefile

    make serve shows directory content and not the webpage. This fix cd into '_site' folder on make serve

    opened by PlaidDroid 4
  • Additional parser

    Additional parser

    Allows the user to create a module to add additional parsing when rendering pages.

    For example in markdown, if a user writes: # My title

    It will be converted to: <h1>My title</h1>

    The user could use the parser to add attributes to <h1> like: <h1 id="my_id">My title</h1>

    opened by gabfl 3
  • Allow any renderable page to live in content.

    Allow any renderable page to live in content.

    As things are only .html files can live in the content directory.

    This simple change should allow any pages renderable by read_content() to live here.

    This is useful if you want to have .md pages.

    opened by mholiv 3
  • Add cleaner python2/3 make serve command

    Add cleaner python2/3 make serve command

    Stops the need to Ctrl-C twice in the event where you have both python versions on your system

    opened by mholiv 3
  • Update to be more gender inclusive

    Update to be more gender inclusive

    Nice project and generally nice attitude in the README! And I realize the main author is a woman, and it's easy to make mistakes with these things (I have). But it's good to avoid little micro expressions that encourage only 'fellas'.

    opened by p10q 3
  • Add autoscaling of images to mobile browsers

    Add autoscaling of images to mobile browsers

    Currently, on mobile devices that are not wide enough for the images in the blog, the image sets the max width of the blog and downscales all the text relative to it, leading to crushed text and a large empty space on the right side.

    I've reproduced this using Google Chrome developer tools, setting the device mode to emulate an iPhone X, but it appears to occur any time the mobile device has a screen width smaller than the image to be displayed.

    The code change here hints that the max width of the image should be 100% of the screen width, not an absolute number of pixels, and the height will be automatically adjusted to maintain the image aspect ratio.

    Example images below, I created a test image that was >800 pixels across, added it to the index page, and displayed it (emulated) on a screen that was ~350 pixels across in portrait mode.

    Pros: Makes text reflow look a lot cleaner top navigation bar subjectively "looks less broken on first glance" Text remains full size and dominates the website on mobile devices

    Cons: Squishes images in whatever way the phone decides to squish them, resulting in a loss of legibility and fidelity. Still results in full size images being sent to a phone for the phone to scale down (more bandwidth + compute required by phone)



    And as an example in how I am using it on my own site:


    P.S. Thanks for a wonderful, readable tool. It does exactly what it says on the tin, it is easy to read what the code is doing, and your tool was just the right balance of power and simplicity I wanted when dipping my toes into blogging. If you are interested in what I created with your excellent tool, my newly created blog is at:

    opened by NortySpock 2
  • Tags


    I am not seeing any support for tags. Is that the case?

    opened by ghost 0
  • Configuration file and other extensions

    Configuration file and other extensions

    Hello - great work, thanks for sharing.

    I was wondering whether you are interested in expanding your site generator, or leaving is as it?

    I am asking as I will fork it and make several changes:

    • make the content delimiters {{ and }} configurable as they clash with Vue.js
    • add the capacity to generate some indexes from the HTML / MD code to be fed to a search component.

    I would be happy to make PRs for these but I am not sure about your plans for long-term maintenance.

    opened by wsw70 0
  • socket.error: [Errno 98] Address already in use

    socket.error: [Errno 98] Address already in use

    i run your code on debian with python 2.7. And it throw an error : socket.error: [Errno 98] Address already in use i change the ip:port to my server ip+new port. but the error still exist. i search the question online . someonte say to add one line code below , but i can't find where to add , self.recSocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    self.recSocket.settimeout(CHECK_TIMEOUT) self.recSocket.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
    self.recSocket.bind(('', UDP_PORT))
    the url: hope you can help me. Thanks.

    opened by DamonDBT 5
  • Proposal: use Classless as the default layout

    Proposal: use Classless as the default layout

    Classless is a default HTML template that supports basically article pages and list pages, plus a global header, a navigation menu, a global footer, everything you need to build a simple blog or website.

    The idea is that if you write HTML conforming to the rules specified by it you'll get access to multiple themes written with the same structure in mind, so you don't have to reinvent theming engine and themes every time you change or write your own CMS or static site generator, you just bring you CSS file over and it's done. The same minimalism ideal that powers

    My idea is that with a few tweaks we can make's default layouts conform to the Classless structure, making it easy for newcomers to start their sites by already using one of the prebuilt themes. Of course, none of the flexibility is lost and people can still tweak everything and either create a custom Classless theme or totally move away from the Classless template if they want.

    What do you think?

    opened by fiatjaf 0
  • Turn into a docker command

    Turn into a docker command

    I think this would be great to have as a docker "run" with two volumes - the input and the output. No dorking around with python packages and the like. Just a simple shell script that you call with the two dirs and it will build then run the code.

    opened by jensenbox 1
Sunaina Pai
Sunaina Pai
A simple static site generator with deployment to S3/Cloudfront.

Stasis A simple static site generator with deployment to S3/Cloudfront. Features Stasis is a static website generator written in Python, using Pandoc

Scott Czepiel 56 Aug 30, 2021
A static website and blog generator

Nikola, a Static Site and Blog Generator In goes content, out comes a website, ready to deploy. Why Static Websites? Static websites are safer, use fe

Nikola, a static site generator 2.2k Oct 25, 2021
dirmaker is a simple, opinionated static site generator for quickly publishing directory websites.

dirmaker is a simple, opinionated static site generator for publishing directory websites (eg:, It takes entries from a YAML file and generates a categorised, paginated directory website.

Kailash Nadh 27 Oct 19, 2021
A Python Static Website Generator

Version 0.8.9 Overview Hyde starter kit by merlinrebrovic is a really nice way to get started with hyde. Hyde layout for bootstrap by auzigog is also

Hyde - Static Website Generator 1.6k Oct 25, 2021
Static site generator that supports Markdown and reST syntax. Powered by Python.

Pelican Pelican is a static site generator, written in Python. Write content in reStructuredText or Markdown using your editor of choice Includes a si

Pelican dev team 10.6k Oct 26, 2021
a static website generator to make beautiful customizable pictures galleries that tell a story

Prosopopee Prosopopee. Static site generator for your story. Make beautiful customizable pictures galleries that tell a story using a static website g

Bram 224 Oct 22, 2021
AutoLoader is a plugin for Pelican, a static site generator written in Python.

AutoLoader AutoLoader is a plugin for Pelican, a static site generator written in Python. AutoLoader is designed to autoload the other Pelican plugins

null 1 Oct 25, 2021
The lektor static file content management system

Lektor Lektor is a static website generator. It builds out an entire project from static files into many individual HTML pages and has a built-in admi

Lektor CMS 3.5k Oct 25, 2021
A declarative website generator designed for high-quality websites, with a focus on easy maintenance and localization.

Grow Grow is a declarative tool for rapidly building, launching, and maintaining high-quality static HTML. Easy installation Jinja template engine Con

Grow 367 Oct 15, 2021
A Python media index

pyvideo is simply an index of Python-related media records. The raw data being used here comes out of the pyvideo/data repo. Befor

pyvideo 227 Oct 5, 2021
Project documentation with Markdown.

MkDocs Project documentation with Markdown. View the MkDocs documentation. Project release notes. Visit the MkDocs wiki for community resources, inclu

MkDocs 13k Oct 20, 2021
Makes dynamic linked shit "static". Amazing What does it do? You give it a dynamically linked binary and it will make a directory that has all the dependencies (recursively). It also f

Stephen Tong 20 Sep 28, 2021