Your First Site
If you followed the Quick Start guide, you should have a stub of your new page. It is really good place to start your journey with Stag!
Content
Websites don’t exist without content. Static site generators use some markup files to create HTML documents. These days Markdown is hands down the most commonly used markup language. Besides Markdown, Stag also supports Asciidoc, through Asciidoctor. You can integrate other markup languages by creating a custom plugin.
Stag reads input files from content directory and writes output documents to _output directory.
For now, let’s suppose that we’ll stick with Markdown. Inside the content directory there’s index.md file will generate the front page. We can put other files and each one will generate another page. Subdirectories inside content also have a meaning: they structure our site.
Metadata
Each page should provide at least some minimal metadata. For Markdown metadata is encoded in a front-matter, which is a special section of TOML at the beginning of Markdown file. It looks like this:
+++
title = "My First Post"
date = 2023-09-16
tags = ["foo", "bar"]
+++
For Asciidoc we can may its metadata fields specified by the format itself:
= My First Post
:date: 2023-09-16
:tags: [foo, bar]
We may put anything in metadata blocks, but some fields are more important than the others (sorry, there’s no democracy here):
-
title
- page title; -
date
- page publication date, or its first publication date in ISO 8601 format; -
lastmod
- date of last modification; if not set, it’ll be automatically copied fromdate
; -
type
- page type, used to select a different template than the default one.
Metadata fields may be accessed in templates by either page.metadata
or
shortcut: page.md
. For example, we can set page’s title like that:
<title>Your First Site</title>
Template
When you run stag build
or stag serve
, Stag renders the content through
Jinja templating engine.
Rendered output will be put in _output directory.
Stag passes 3 variables to each template:
-
content
- rendered output of page; in fact, it is the same aspage.output.content
; -
page
- all informations about page gathered by Stag and plugins before rendering -
site
- collection of pages and data about them.
Besides these variables, templates may also use template functions and macros.
Default theme is rather barebones so we probably need to create a new one. We
can place it in a configurable themes/default
directory. We may also want to run a built-in development web server, which
automatically rebuilds _output when it detects any changes. To do this, run
stag serve
.
Tip
|
When you’re running stag serve and you don’t see your changes updated,
check the console output to see if it had regenerated correctly.
|
Template Structure
In Stag each page has its own type, configured by a type
metadata field.
Default type is page
for ordinary pages and list
for listing pages, like
taxonomies.
Stag chooses templates based on page.metadata.type
and desired output
format (HTML in our case). For ordinary pages it’ll use
themes/default.page.html and for listing pages it’ll use
themes/default/list.html.
Besides templates, themes need static files, like CSS or JavaScript files. Stag copies them from static subdirectory.
The overview of basic theme structure looks like this:
themes/ default/ static/ favicon.png style.css base.html index.html list.html page.html rss.xml
Base Template
If we use several page types, we’ll inevitably of repeating the same HTML skeleton over and over again. To prevent it, we may use template inheritance. In our example we’re going to define a base template, base.html, which defines certain blocks filled by specific templates. There will be 3 blocks in our skeleton:
-
header
- optional header, which will use the default greeting if child template doesn’t provide it -
sidebar
- optional sidebar, which will be empty if child template doesn’t provide it -
main
- main section for page content
{% from "stag.html" import absurl with context %} (1)
<!DOCTYPE html>
<html lang="{{ site.config.language }}"> (2)
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{% if page.md.author -%}<meta name="author" content="{{ page.md.author }}">{% endif %} (3)
<title>{%- if page.metadata.title -%}{{ page.metadata.title }} · {% endif %}{{ site.config.title }}</title> (4)
<link rel="stylesheet" href="{{ absurl("style.css") }}"> (5)
<link rel="icon" type="image/png" sizes="16x16" href="absurl("favicon.png")">
</head>
<body>
<div id="layout" class="layout-center">
{% block header %} (6)
<header>
<h1>Hello!</h1>
</header>
{% endblock %}
{% block sidebar %}{% endblock %} (7)
{% block main required %}{% endblock %} (8)
</div>
</body>
</html>
-
First, we import
absurl
macro from built-in macros. Most macros require importing themwith context
in templates, which enables them to usesite
andpage
variables. -
Set page language to the value of
language
, which is configured in config.toml -
Check whether page has an author and add it to page’s
<meta>
. -
Check whether page has a title and use it to create the HTML
<title>
tag. -
Use
absurl
macro to create absolute URL to style.css. You should prefer to create all inter-site links with either ref, absref or absurl macros which allows to correctly create URLs relatively to configured base URL, and also when usingstag serve
. -
Start of header block. If not provided in child templates, header block will render its contents from base.html template. Blocks start with
{% block name %}
and end with{% endblock %}
. -
Start and end of sidebar block.
-
Start and end of main block. Main block is required: it is an error of child template to not provide it.
Each template which wants to reuse this skeleton must extend it by
putting {% extends "base.html" %}
inside.
Ordinary Pages
Ordinary pages render from page.html template.
Here, page.html template extends all of the blocks which base.html skeleton defines:
-
inside
header
we’ll put page’s title; -
inside
sidebar
we’ll put page’s table of contents, if it is available and when it isn’t disabled; -
inside
main
we’ll put page’s contents.
{% extends "base.html" %} (1)
(2)
{% block header %}
<div id="header"><h1>{{ page.md.title }}</h1></div>
{% endblock %}
(2)
{% block sidebar %}
{%- if page.toc and page.md.get("toc", True) -%} (3)
<aside id="sidebar">
{{ page.toc.content }}
</aside>
{%- endif -%}
{% endblock %}
(2)
{% block main %}
<main>
{% if page.md.get("tags") -%}
<div class="taglist">
Tags:
{%- for tag in page.md["tags"] -%}
<a class="tag" href="{ ref("tags/" + slugify(tag)) }">{{ tag }}</a>{%- if loop.nextitem -%}, {%- endif -%} (4)
{%- endfor %}
</div>
{%- endif %}
<article id="post" class="content">
{{ content }} (5)
</article>
</main>
{% endblock %}
-
Mark that this template extends base.html skeleton.
-
Beginnings of overridden blocks.
-
Generate table of contents in the sidebar. Table of contents is stored separately from page contents inside
page.toc.content
. Additionally, our template checks if table of contents is disabled for a particular page through page’s metadata. -
Use tags assigned to the page to generate links to taxonomy terms. We’re using
slugify()
function to create a string passed toref()
macro. It isn’t perfect, but works and is rather straightforward. -
Enter page’s content
Front Page
Many web pages have a front page which looks different from the other pages. We can achieve this in Stag by assigning a specific page type for content/index.md file. For example, something like this will force Stag to use index.html template to render the page:
+++
type = "startpage"
+++
Hello and welcome to my front page!
Then we can follow the same techniques which we have used previously to create a separate theme for the front page.
Taxonomies
Pages can be grouped in taxonomies which we define in config.toml. This improves navigation for users, who can view similar pages. For example, we may create two taxonomies: category and tags.
[[taxonomies]]
key = "tags"
singular = "tag"
plural = "tags"
[[taxonomies]]
key = "category"
singular = "category"
plural = "categories"
We add page to taxonomy by assigning it in metadata:
+++
tags = ["personal", "cooking"]
category = "Recipes"
+++
Taxonomies are "special" kinds of pages, because they are virtual. It means that they do not have a physical file which represents each taxonomy in content directory. Instead, stag groups them under the path constructed from their plural form (so for example under /tags/personal, /tags/cooking and /categories/recipes).
Note
|
If Stag encounters a file which is named after the name of taxonomy, it incorporates it. See description of Taxonomies Plugin for details. |
There are three ways we use taxonomies in templates. First, we can get list all taxonomy terms assigned to a particular page. For example, if we’d like to list all tags assigned to a page, we could do it like that:
{% from "stag.html" import ref with context %}
{% if "tags" in page.metadata -%}
<ul>
{% for tag in page.metadata["tags"] -%}
<li><a href="{ ref("tags/" + slugify(tag)) }">{{ tag }}</a></li>
{%- endfor %}
</ul>
{% endif %}
We may also list all pages assigned to taxonomy term. Such list is by
default rendered by a list
template:
{% if page.term %}
<ul>
{% for termpage in page.term.pages -%}
<li><a href="{{ termpage.relurl }}">{{ termpage.md.title }}</a></li>
{%- endfor %}
</ul>
{% endif %}
Finally, we can list all of taxonomy terms. Pages which group taxonomy
terms (so for example /tags and /categories) have a separate type:
taxonomy
. This way we can have a separate template for them, instead of
placing a complex logic in e.g list.html template:
<h1>List of {{ page.taxonomy.plural }}</h1>
<ul>
{% for term in page.taxonomy.terms -%}
{%- set no = term.term.pages | length -%}
<li>
<a href="{{ term.relurl }}">{{ term.metadata.title }}</a>
<small>({{ no }})</small>
</li>
{%- endfor %}
</ul>
RSS Feed
RSS feeds are XML files which are used to notify about changes on your website. To create one, we should add 2 files:
-
content/rss.xml - source for our RSS feed, which will change the type of this page to rss; this file can be otherwise empty;
-
themes/default/rss.xml - template used for generating pages of type rss.
+++
title = "Recent changes"
type = "rss"
+++
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>{{ page.md.title }}</title>
<link>{{ site.config.url }}</link>
<description>{{ site.config.title }} - recent content</description>
<generator>stag - git.goral.net.pl/stag.git</generator>
{%- if site.config.language -%}<language>{{ site.config.language }}</language>{%- endif -%}
<lastBuildDate>{{ page.md.date | rfc822format }}</lastBuildDate>
<atom:link href="{{ page.url }}" rel="self" type="application/rss+xml" />
{%- set subdir = page.source.reldirname -%}
{% for item in (site.subpages_of(subdir, recursive=True) |
pagetype("html") |
selectattr("md.lastmod") |
sort(attribute="md.lastmod_timestamp", reverse=True))[site.config.get("maxfeeds", 20)] %}
<item>
<title>{{ item.md.title }}</title>
<link>{{ item.url }}</link>
<guid>{{ item.url }}</guid>
<pubDate>{{ item.md.lastmod | rfc822format }}</pubDate>
<description>{{ item.output.content | escape }}</description>
</item>
{% endfor %}
</channel>
</rss>