Skip to content
Arsh edited this page Jul 19, 2022 · 9 revisions

Table of contents

Commands

norite registers the following commands:

  • norite init
    Initialize a new project in the current directory

  • norite build
    Delete the output directory and rebuild the website
    It has the following optional arguments available:

    • --drafts Include files/folders marked as drafts in output
  • norite serve
    Start a dev server, reloads automatically on file changes.
    It has the following optional arguments available:

    • -b --bind 0.0.0.0 Specify alternate host, default is localhost
    • -p --port 8000 Specify alternate port, default is 1234
    • --drafts Include files/folders marked as drafts in output
  • norite -f FILE
    Use a custom .toml file for config, default is norite.toml

  • norite -h
    Print the help message

  • norite -v
    Print the version number

Content Structure

Norite uses the following directories:

Website root
├── norite.toml
├── content/
├── dist/
└── source/
    ├── templates/
    └── sass/

content/ stores all website content and assets
source/templates contains all jinja2 templates used to build the website
source/sass contains all sass/scss files that would be compiled to css if enabled
dist/ is the output folder for all generated files

Here is an example showing how norite would generate urls for an imaginary website using the directory structure:

./content
├── index.md                    ---> example.com
├── about.md                    ---> example.com/about
├── blog 
│   ├── index.md                ---> example.com/blog
│   ├── article-1.md            ---> example.com/article-1
│   ├── article-2 
│   │   ├── index.md            ---> example.com/article-2
│   │   ├── some_image.png      ---> example.com/article-2/some_image.png
│   │   └── another_image.png   ---> example.com/article-2/another_image.png
│   └── article-3.md            ---> example.com/article-3
├── portfolio 
│   ├── index.md                ---> example.com/portfolio
│   ├── custom_script.js        ---> example.com/portfolio/custom_script.js
│   └── images 
│       ├── image1.png          ---> example.com/portfolio/images/image1.png
│       └── image2.png          ---> example.com/portfolio/images/image2.png
├── projects 
│   └── index.json              ---> example.com/projects
├── css 
│   └── main.css                ---> example.com/css/main.css
├── fonts 
│   └── sans-serif.ttf          ---> example.com/fonts/sans-serif.ttf
└── favicon.ico                 ---> example.com/favicon.ico

The content folder can contain all kinds of files in any kind of folder structure that you prefer. All content inside the content folder is exposed directly as a tree data-structure to the templates. This tree is also used to generate the permalinks for each page and the structure of the generated website will mimic the structure of the content folder with rendered HTML files in place of markdown and index files.

Drafts

Any folders, markdown files or assets who's name start with an underscore (_) will be marked as a draft and will not be rendered by default. To preview draft files on the dev server or include them in the build output --drafts CLI option can be used.

Pages

Any markdown files in the content folder at any nested level will be detected and rendered into HTML. Markdown files can have Jinja2 templates embedded in them, and can also optionally include a TOML frontmatter at the top.

There are no required variables for the frontmatter. However, the following optional variables, if present in the frontmatter, are recognized by norite -

---
# if present, use the specified template for this page
template = 'custom.html'

# if present, use the specified template for all child pages to this page
child_template = 'custom.html'

# if present, can be used by the `weight_sort` jinja filter
weight = 1

# if present, can be used by the `date_sort` jinja filter and for mentioning "last modified" in the sitemap
date = '2022-04-10'

# any other variables are directly passed through to templates
custom_var = 'custom variable'
---

In addition to these, any other variables that are defined in the frontmatter would also be available directly in the templates.

Index files

If you want a folder to also render its own HTML page, you can put a specially named index.md file in it, similar to how index.html named files are used in HTML. These behave exactly the same as any other markdown files with the only exception being their name, and can be placed at any nested level in the content folder.

It is helpful in many cases to just specify some raw data that can be passed to a template and rendered, without needing to pass through an markdown renderer. For these cases, an index.toml or index.json can be used instead of index.md. Any of the special variables mentioned in the frontmatter section above will also be detected if present in these files. Any .toml or .json files not named "index" would be treated as normal assets and wont render as a page.

Config file

An empty norite.toml file is enough for norite to recognize and build the website, but it is recommended to define the baseurl before deploying to the internet. The config file can be used to override some of the defaults, enable optional features or define custom global variables:

# website's base url, used to generate sitemap.xml and in the default robots.txt
# define without any slash at the end
baseurl = 'https://example.com'

# override content folder
content = 'custom_content_folder'

# override output folder
output = 'custom_output_folder'

# any custom variables can be added here
custom_var = 'custom variable'

[rss]

# enable rss support
enable = false

# can be anything, usually its `rss.xml` or `atom.xml`
filename = 'rss.xml'

[sass]

# enable sass compilation support
enable = false

# compiler to use, valid options are - "libass" or "dartsass"
compiler = 'libsass'

In addition to these, any other variables defined here would also be available directly in the templates.

It is possible to have different configurations for different scenarios by using the -f CLI option. For example, config files for development and production environments can be saved in different files and then used like so:

# use dev config with dev server
norite -f dev.norite.toml serve

# build for prod with different config
norite -f prod.norite.toml build

Templates

Templates are Jinja2 files present in the source/templates folder. Files inside this folder can be placed in any preferable directory structure. The only required template file is index.html placed at the root of the templates folder, this template is used as the default for all pages if no other template is specified.

All jinja2 templates rendered as pages and all templates embedded inside markdown receive the exact same context. This context contains the following 2 variables:

  • page: A Page() object representing the current page,
  • config: A python dict() containing all variables from norite.toml

Special templates, such as templates for sitemap.xml, rss/atom templates and robots.txt receive a variable called root instead of page containing a Page() object representing the root, as there is no "current page" for these templates.

To print the entire context received by any template you can add {% debug %} anywhere in the template.

In norite, all files that will render to a HTML page or folders that contain files that render to a HTML page are called "page" and are represented by a Page() object. All other files are called "asset" and are represented by a Asset() object, with is just an alias for Page() named differently to make it easy to differentiate and reduce confusion.

Each Page() (or Asset()) object has the following properties available:

page.content

A string containing this page's markdown content rendered as HTML. This will be empty for index.toml and index.json style pages.

Example:

{{ page.content | safe }}

page.permalink

A string containing this page's relative URL. Always starts with a /. If the page object represents a folder that has no index file associated with it, then the permalink will be empty.

Example:

{{ page.permalink }}

page.path

A python Path() object representing the file behind this page.

Example:

<p>filename: {{ page.path.name }} + {{ page.path.suffix }}</p>

page.pages

A list of Page() objects containing the immediate child pages for this page. Pages can be both child markdown files or folders containing markdown files.

Example:

<nav>
    {% for inner_page in page.pages %}
        <a href='{{ inner_page.permalink }}'> {{ inner_page.custom_title }} </a>
    {% endblock %}
</nav>

Note: here custom_title is an assumed custom variable in frontmatter.

page.assets

A list of Asset() objects containing all assets present alongside this page. Assets can be any non-markdown files or folders containing all kinds of files other than markdown.

Example:

{% for images in page.assets %}
    <img src='{{ image.permalink }}' />
{% endblock %}

page.parent

A Page() object containing this page's parent page. This would be None if the page is the root of the tree.

Example:

<a href='{{ page.parent.permalink }}'> Go Back! </a>

<!-- sibling pages -->
{% for sibling in page.parent.pages %}
    {{ sibling }}
{% endblock %}

page.root

A Page() object containing the root of the tree, usually the website's homepage.

Example:

<a href='{{ page.root.permalink }}'> home </a>

<!-- extract one section of the website from multiple -->
{% set blogs_section = page.root.pages | selectattr('section_name', 'blogs') %}
{% for article in blogs_section.pages %}
    {{ article.permalink }}
{% endblock %}

Note: here section_name is an assumed custom variable defined in frontmatter of all subsections of the homepage.

page.is_leaf

Boolean that indicated whether the page is a leaf node and has no further child pages.

Example:

{{ page.is_leaf }}

page.is_page and page.is_asset

Booleans that can be used to check if page represents a page or an asset.

Example:

{{ page.is_page }}
{{ page.is_asset }}

page.raw_content

A string containing this page's raw markdown content after all jinja2 templates embedded in it have been rendered but before it has been rendered to HTML. This will be empty for index.toml and index.json style pages.

Example:

{{ page.raw_content }}

Templates - Custom global functions

In addition to the builtin global functions provided by jinja2 norite provides the following additional functions:

now()

Returns the current time in UTC as a python Datetime() object.

Example:

{{ now() }}

datetime()

Alias for python's Datetime() class.

Example:

{{ datetime(2022, 6, 12, hour=4) }}
{{ datetime.fromisoformat('2022-06-12') }}
{{ datetime.today() }}

Templates - Custom filters

In addition to the builtin filters provided by jinja2 norite provides the following additional filters:

all_pages

Get a list of Page() objects for all child pages at all nested levels below this page.

Example:

{{ page | all_pages }}

<!-- all pages for the entire website -->
{{ page.root | all_pages }}

strftime

Format the input date or time with a strftime based format.

Example:

{{ now() | strftime('%Y-%m-%d') }}
{{ datetime(2022, 6, 12) | strftime(format='%b %e, %Y') }}

markdown and md

Interpret the input string as markdown and return rendered HTML. This filter is also useful for turning markdown strings stored inside index.toml or index.json to HTML.

Example:

{{ 'this *is* [markdown](https://commonmark.org/)!' | markdown | safe }}
{{ page.description | markdown | safe }}

<!-- shorter version -->
{{ 'this *is* [markdown](https://commonmark.org/)!' | md | safe }}
{{ page.description | md | safe }}

Note: here description is an assumed custom variable in frontmatter.

highlight and hl

Interpret the input string as code and return rendered HTML. It takes the language name and optional keyword arguments as input, which are then passed directly into pygments' HtmlFormatter. Some useful keyword arguments are hl_lines, linenostart and linespans, refer to pygment's documentation for a full list of supported arguments.

Example:

{{ 'evens = [x for x in range(100) if x % 2 == 0]' | highlight('python') | safe }}
{{ page.code_snippet | highlight('js', linenostart=4) | safe }}

<!-- shorter version -->
{{ 'evens = [x for x in range(100) if x % 2 == 0]' | hl('python') | safe }}
{{ page.code_snippet | hl('js', linenostart=4) | safe }}

Note: here code_snippet is an assumed custom variable in frontmatter.

date_sort

If a list of Page() objects contains pages that specified a date in their frontmatter using the date variable, it can be sorted using this filter. This is basically an alternate filter to a more verbose version of the same that can be built using the builtin sort().

Example:

{{ page.pages | date_sort() }}
{{ page.pages | date_sort(reverse=True) }}

weight_sort

If a list of Page() objects contains pages that specified a weight in their frontmatter using the weight variable, it can be sorted using this filter. This is basically an alternate filter to a more verbose version of the same that can be built using the builtin sort().

Example:

{{ page.pages | weight_sort() }}
{{ page.pages | weight_sort(reverse=True) }}

Syntax Highlighting

Any code inside markdown present inside fenced code blocks (inside `` or ``` ```) is automatically syntax highlighted via pygments. If you want to manually highlight any strings containing code you can use the highlight or hl filters that norite provides.

The HTML representing the code is annotated using CSS classes, and no colors will show up until the definitions for those CSS classes (aka a pygments theme) is provided. There are a number of ways to provide the theme:

  1. To use one of pygment's builtin themes, norite provides an helper function - get_syntax_css():
    <head>
        <style> {{ get_syntax_css('material') }} </style>
    </head>
  2. Provide the theme as an external CSS file. You can build your own, use one from this repo, or look for pygments themes on the internet.
  3. Use the shell command pygmentize provided by pygments to generate an CSS file for one of the builtin themes:
    pygmentize -S material -f html -a .highlight > syntax.css

Rss / Atom Feeds

Norite has the option to parse custom rss / atom templates if enabled in the config. As norite's goal is to provide as much flexibility as possible for the website's structure, it cannot automatically generate a rss/atom feed for it and you would have to provide your own template.

To provide the template for feeds, place a rss.xml or atom.xml named file in source/templates. In this file you can describe your custom feed using jinja2 templates. Refer to the documentation for rss and atom to get started.

Here's an example RSS template:

<rss version="2.0">
    <channel>
        <title>{{ config['feed_name'] }}</title>
        <link>{{ config['baseurl'] ~ root.permalink }}</link>
        <description>{{ root.rss_description }}</description>
        <language>en-us</language>
        <lastBuildDate>
            {{- now() | strftime('%a, %d %b %Y %H:%M:%S GMT') -}}
        </lastBuildDate>
        <generator>Norite</generator>
        <managingEditor>Add email here with name</managingEditor>
        <docs>http://www.rssboard.org/rss-specification</docs>
        <copyright>Copyright {{ now() | strftime('%Y') }}</copyright>

        {%- for page in root.pages | date_sort(reverse=True) %}
        <item>
            <title>{{ page.title }}</title>
            <link>{{ config['baseurl'] ~ page.permalink }}</link>
            <description>{{ page.description }}</description>
            <guid>{{ config['baseurl'] ~ page.permalink }}</guid>
            <pubDate>
                {{- page.date | strftime('%a, %d %b %Y %H:%M:%S GMT') -}}
            </pubDate>
        </item>
        {%- endfor %}
    </channel>
</rss>

And an example Atom template:

<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <id>{{ config['baseurl']}}</id>
    <title>{{ config['feed_name'] }}</title>
    <link href="{{ config['baseurl'] ~ root.permalink }}"/>
    <link rel="self" href="{{ '/' ~ config['rss']['filename'] }}"/>
    <updated>{{- now().astimezone().isoformat() -}}</updated>
    <author>
        <name> add name here </name>
        <email> add email here </email>
    </author>
    <rights>Copyright {{ now() | strftime('%Y') }}</rights>
    <icon>/icon.jpg</icon>
    <logo>/logo.jpg</logo>
    <generator uri="https://github.com/prdx23/norite">Norite</generator>

    {%- for page in root.pages | date_sort(reverse=True) %}
    <entry>
        <title>{{ page.title }}</title>
        <id>{{ config['baseurl'] ~ page.permalink }}</id>
        <updated>{{- page.date.isoformat() -}}</updated>
        <link rel="alternate" href="{{ config['baseurl'] ~ page.permalink }}"/>
        <summary>{{ page.description }}</summary>
        <published>{{- page.date.isoformat() -}}</published>
    </entry>
    {%- endfor %}
</feed>

Modify the for loops in the above examples to match the structure of your website.

Sass

Norite has the option to also compile sass stylesheets when building the website. Enable sass compilation in the config, place .sass or .scss files in sources/sass and when the website is built, compiled CSS files will be placed in dist/css folder. Norite can use two different sass compilers, both with their pros and cons:

libsass

To use libsass, install it by running pip install libsass.
pros: very fast compile times, loaded directly as a python module.
cons: libsass C implementation, on which the python module is based, has been deprecated and no longer receives new updates, but is still maintained.

dartsass

To use dartsass, install the compiler by following the instructions given in their documentation.
pros: Is considered the reference implementation for sass, contains all the new features.
cons: norite has to call into dart-sass as an external subprocess, resulting in much slower compile time.