Shodo is a framework for rapidly building a static site from markdown files, json, and Jinja templates. Simply make changes to your site in the src
directory, run the build command, and access the build in the dist
directory. Easily deploy to Netlify in just a few clicks.
There is no shortage of options out there for building websites and apps, but they can quickly feel overcomplicated when all you need is a simple website with a few reusable components. The goal of Shodo is to make publishing content to the web as simple and elegant as possible for developers, whether it's a personal blog, a portfolio, documentation, or a professional marketing site.
-
Create a new project directory and start a virtual environment using your preferred method
-
Install the
shodo_ssg
package by running one of the following commands:
Via pip:
pip install git+https://github.com/ryanmphill/shodo-static-gen.git@main#egg=shodo_ssg
Via pipenv:
pipenv install git+https://github.com/ryanmphill/shodo-static-gen.git@main#egg=shodo_ssg
Via Poetry:
poetry add git+https://github.com/ryanmphill/shodo-static-gen.git@main#egg=shodo_ssg
Via uv:
uv pip install "shodo_ssg @ git+https://github.com/ryanmphill/shodo-static-gen.git@main"
- Once the package is installed, you can scaffold a new project using the command
start-shodo-project <name of project directory>
To create the project in the current directory, run
start-shodo-project .
- Build the starter site and serve it to localhost by running the following command from the root directory of the project:
python3 serve.py
You should now be able to view the site on localhost and can start by making changes to home.jinja
. When you simply want to build the static site, run the following command from the root directory:
python3 site_builder.py
and you can find your static site located in the dist/
directory
First, there is the main home page template located at src/theme/views/home.jinja
that can render partial sub-views, which can either be other Jinja2 templates located in src/theme/views/partials
, or markdown files located in src/theme/markdown
.
This project uses Jinja2 as its templating engine, so it would be beneficial to visit the Jinja docs. This project leverages Jinja to integrate with Python and build HTML from templates that have access to functions and variables.
Any template added to the pages/
directory will be written as an index.html file in its own subfolder within the dist
directory. When linking between pages, simply write a backslash followed by the page name, exluding any file extensions. So if you wanted to link to pages/linked-page.jinja
from home.jinja
, the anchor tag would be
<a href="/linked-page">Click Here!</a>
You can create nested pages by adding a subdirectory within pages/
with the name of the route. For routes with multiple pages, the index page of that route will need to be on the same level as the route subdirectory with the same name followed by the .jinja
extension. For example:
__pages/
____about.jinja (template for '/about')
____nested.jinja (index template for '/nested')
____nested/
______nested-page.jinja (template for '/nested/nested-page')
Any markdown files added to the /markdown/partials
directory will be exposed to Jinja templates with a variable name identical to the markdown file name, minus the extension. So, the contents of summary.md
can be passed to the Jinja template as {{ summary }}
, where it will be converted to HTML upon running the build script.
In order to avoid any naming conflicts, The articles further nested in directories within "articles/partials/" will have a variable prefix that is the accumulated names of the preceding directories in dot notation (excluding '/partials' and higher).
For example, a markdown file located in markdown/partials/collections/quotes/my_quote.md
, will be exposed to all templates with the following variable using the jinja variable syntax:
{{ collections.quotes.my_quote }}
In addition to partial variables that can be included in templates, entire new pages can also be automatically be generated from markdown files added to the
markdown/articles
directory. The url path to the page will match what is defined in the markdown/articles
directory.
Articles from the markdown/articles
directory are rendered with a reusable template defined in views/articles/
Just add a layout.jinja
file under a subdirectory that matches the subdirectory tree used in markdown/articles
, or simply define a layout.jinja
at the root of views/articles
if you want to use a single layout template for all articles. In the layout.jinja
file, control where you would like your content to be dynamically inserted by passing in the reserved {{ article }}
variable. More below.
The site builder will always match up the layout template that is closest in the tree, so markdown/articles/blog/updates/new-post.md
would be matched with views/articles/blog/layout.jinja
if no layout is defined for the updates
directory.
__markdown/
____articles/
______blog/
________updates/
__________new-post.md
__views/
____articles/
______layout.jinja (default root layout for all markdown 'articles')
______blog/
________layout.jinja (Maps to markdown/articles/blog, overwrites root layout)
________updates/
__________layout.jinja (Maps to markdown/articles/blog/updates, overwrites other previous layouts in tree)
The layout.jinja
is just a normal jinja template, but the {{ article }}
variable has been reserved for passing in the content from each file in markdown/articles
. Simply define whatever repeated layout you would like to wrap the {{ article }}
content, such as a header and footer.
Here is an example layout template:
<div class="container">
<header class="page-header">
<h1 class="page-header__title">This is the root article layout</h1>
</header>
<main>
{{ article }}
</main>
<footer>Thanks for reading</footer>
</div>
For dynamically rendering lists of content, Jinja Macros can be used to build components for looping through a passed argument. Simply use the macro
keyword in the partial template, like the following example:
{% macro loop_template(items) -%}
<ul>
{%- for item in items %}
<li>{{ item }}</li>
{%- endfor %}
</ul>
{%- endmacro %}
Then, it can be included in the main template:
{% from 'loop_template.jinja' import loop_template as loop_template %}
{{ loop_template(['Content', 'to', 'loop', 'through']) }}
Then after running the build, the HTML should look like the following:
<ul>
<li>Content</li>
<li>to</li>
<li>loop</li>
<li>through</li>
</ul>
For easy configuration and keeping repeated values in one place, any property defined in a .json
file within the /store
directory will be passed to Jinja templates with an identical variable to the property name. Each nested object can be accessed using dot notation in the templates.
For example, to access the title
value from /store/config.json
:
{
"metadata": {
"title": "Shodo - A Static Site Generator",
"description": "Shodo is a static site generator that uses Markdown and JSON files to generate a static site.",
"author": "Shodo"
}
}
in the template, you would use the following syntax:
{{ metadata.title }}
This is where all source paths and project settings are defined.
NOTE: Any path included in root_template_paths
will have all of its children directories recursively added to the search path for Jinja2, so only top level paths should be included in the settings. In most cases, "root_template_paths": [ "src/theme/views/" ]
should suffice, but it would be possible to add another path to src/theme/assets/images
for example if you wanted to use the templates for working with an SVG but still wanted to maintain separation of concerns.
- Allow Netlify to install the project dependencies
If you are using pipenv, Netflify will install dependencies directly from the pipfile
. Otherwise, you will need to generate a requirements.txt
file via pip freeze > requirements.txt
, poetry export --format=requirements.txt > requirements.txt
, uv pip compile pyproject.toml -o requirements.txt
, or similar to allow the dependencies to be installed via pip.
If using pipenv, your pipfile
dependency should look something like this:
[packages]
shodo_ssg = {ref = "<specific-commit-hash-or-branch-goes-here>", git = "https://github.com/ryanmphill/shodo-static-gen.git"}
If you haven't already, generate the lock file via pipenv lock, then go ahead and verify the package is installable with the command pipenv sync.
If you installed the project via pipenv install
, this was already done and you can move on to the next step
If using pip, after generating your requirements.txt
, the file should look similar to this:
Jinja2==3.1.5
markdown2==2.5.2
MarkupSafe==3.0.2
shodo_ssg @ git+https://github.com/ryanmphill/shodo-static-gen.git@<commit-hash>
- Create a new repository on GitHub and push the Shodo project up to it
- Now, we have everything we will need to build and deploy the static site on Netlify. We will have to make a few specifications since Netlify won't be able to autodetect everything about the build configuration
- Choose "Add new site" on Netlify, and select the repository with your site
- For the
build command
, specifypython site_builder.py
- Luckily, Netlify supports Python and will be able to automatically install dependencies from either the pipfile or requirements.txt. The only extra step we need to take is to change the default python version from 3.8 to 3.9. To do this, go to the environment variables section and add
PYTHON_VERSION
for the variable name, and3.9
for the value. - Now click to deploy the site. After around a minute, verify that the build was successful, and you should be able to view the deployed site!
Reference: Netlify Python Documentation
For all jinja templates, use the .jinja
file extension. Other extensions such as .j2
or .jinja2
are not fully supported at this time.
If you're using VSCode, the Better Jinja extension is recommended for full syntax highlighting out of the box using the .jinja
extension. Other extensions will work, although you might need to configure the settings to look for the .jinja
extension.
This project uses the Black Formatter and follows the current style guide
-
Start up a virtual environment and install the dependencies using your preferred method after pulling down the repository
-
Once your virtual environment is activated, in the root of the project directory run
pip install -e .
(Don't forget the.
) -
Upon successful install, navigate to an entirely separate directory and run
start-shodo-project <name of new project directory>
Upon success, a new starter project template should have been set up in the specified directory
Start editing by making changes to src/theme/views/home.jinja
- Run
Python site_builder.py
from the main project directory when your ready to generate the site
Find your static site located in the dist/
directory
For development, run Python serve.py
from the root project directory. This will build the site in the dist
directory with the latest changes from src
and serve it on localhost.