A “theme” refers to the templates, styles, and other assets used to render content.


myWebLog uses DotLiquid, a .NET implementation of the Liquid template language, to render pages and posts. A complete theme contains four templates:

Themes can also support different templates for pages and posts. A practical example is the theme for the site you're reading now; project-page.liquid is the template for pages associated with an open source project; solution-page.liquid is used to generate the pages on the homepage sidebar's “About” links; and, speaking of the home page, home-page.liquid is used render it.

htmx Support

htmx brings the interactivity of the SPA to basic HTML content, allowing us to swap out portions of the page instead of the entire page. This has several benefits, the biggest one being speed; none of the page-wide styles, scripts, and images need to be changed when some other content is swapped out. The admin area of myWebLog uses htmx extensively; the inline editing of tag mappings and categories, and every page navigation, do not reload the entire page.

For a publicly-facing weblog, though, using htmx will be relatively easy. Placing hx-boost="true" on an element makes all the non-external links contained in that element be handled by htmx, and by default, the content it receives will replace the inner HTML of that element. When rendering content for a link going through htmx, myWebLog uses the layout-partial.liquid template. If the hx-boost attribute is applied to the <body> tag, the partial layout should render the entire page, minus the <head> content (except for the <title>); if it is placed on a <main> tag, the layout should render whatever content belongs there. (See the include_template custom Liquid filter below for a strategy to reduce duplication between layout and layout-partial.)

The actual decision of where to place the hx-boost attribute is dependent on what you want to preserve between navigations. Placing it on the <body> tag lets you update things like highlighting the active page, while placing it on a content <div> (or other tag) will only replace that content.

Theme Files

Any theme files (*-theme.zip) that are in the same directory as myWebLog are loaded automatically when it starts. The basic installation comes with a default theme (as well as an admin theme, which should not be modified, and is not able to be modified via the application). Within a theme file, any files in its wwwroot directory are loaded as theme assets; these are described below.

Themes can be loaded in three ways:

Both the first and last options above result in the theme's .zip file being in the application's directory, so they will be loaded each time the application starts. While this is not required – the theme information in the database should persist between restarts – this enables upgrades to provided themes to occur transparently, and can be utilized for custom themes as well. The admin page will show “NOT ON DISK” for themes where the .zip file is not in the directory.

A few other notes:

Theme Assets

In a theme .zip file, items within the wwwroot folder are considered theme assets. They are stored in the database, and as such, are included in database backups. Assets are served with a Last-Modified header and a “cache this for 30 days” header, and support requests with an If-Modified-Since header (in short, clients can check to see if their cached version is still good, and not download them again, saving you bandwidth). For this reason, though, if you change an asset, you may need to do a hard refresh before you see your changes.

There are a few assets with special handling:

For others, you can use the theme_asset custom Liquid filter to generate a link for the asset; see its description below.

Displaying Content in Templates

The rendering context for pages can vary based on what is being rendered; however, the following values are always available. (The true/false values will be false if they are not set.)

Custom Filters and Tags

myWebLog provides several custom filters and tags used to assist with common processes a theme may need to perform. These are detailed below.

{% page_head %} should go within the <head> tag of any theme's layout, as it does much of the heavy lifting for your page.

{% page_foot %} can be placed anywhere in your page (including the <head>, if you really wanted to), but will generally work best just inside the closing <body> tag. It adds script references for htmx (if enabled) and script.js, if it exists in your theme.

absolute_link generates an absolute URL for the given link. It can take a page, a post, or a string, and will use the web log's URL base and the permalink (for pages or posts) to create a complete link that will always work. relative_link does the same thing, but excludes the authority (scheme, host, and port) portion of the URL base for the web log. Within the context of a link that someone would click in a web page, these are equivalent; some links may need to be absolute, though, so both filters are provided.

ex. {{ "2020/my-page.html" | absolute_link }} would result in https://yadda.example.com/2020/my-page.html, while {{ "2020/my-page.html" | relative_link }} would generate /2020/my-page.html.

category_link generates a relative URL for the given category. Using the categories context item is the best way to get a category. Within a post, {% for cat_id in post.category_ids %} will loop through the category IDs for the post, and the category can be obtained via {% assign cat = categories | where: "id", cat_id | first %}. In a sidebar, you may simply iterate over the categories themselves {% for cat in categories %}; the categories are sorted/grouped alphabetically. However you obtain the category, though, {{ cat | category_link }} will generate a link to the first page of posts for that category (include its subcategories).

tag_link generates a relative URL for the given tag. It uses any tag mappings that have been defined. Within a {% for tag in post.tags %} loop, <a href="{{ tag | tag_link }}">{{ tag }}</a> generates a link to the first page of posts tagged with the same tag.

edit_page_link and edit_post_link generate links to edit a particular page or post. Note that, if a user is not logged on, they will need to do so before these links will work. You can also wrap them in {% if logged_on %}...{% endif %} to only render them if a user is already logged on. Each filter will take either its type (post or page) or a string with the ID of a post or page.

theme_asset generates a link to an asset within a given theme. The path given there should begin with the root of the wwwroot folder in your theme definition. For example, if there was a file wwwroot/img/profile.png, it could be rendered as <img src="{{ "img/profile.png" | theme_asset }}">. Note that this does not validate that the asset actually exists; if it does not, it will render as a broken image.


value takes an array of metadata items and returns the first value where the name matches. Both pages and posts have metadata items. If the metadata item does not exist, the string -- [name] not found -- is returned.

A quick example will give you an idea of where this might be useful. Let's say you are a musician, and you have albums for sale. If you create a post about each album, you can use a purchase_url metadata item to generate a “Buy Now” link (maybe via an “album-page” template, or logic in the “single-page” template); once the album is no longer being sold, you can remove that metadata item. Here is one way this could be implemented:

  {% assign purchase_url = post.metadata | value: "purchase_url" %}
  {% if purchase_url == "-- purchase_url not found --" %}
    <em>This title is not currently available</em>
  {% else %}
    <a href="{{ purchase_url }}" target="_blank" rel="noopener">Buy Now!</a>
  {% endif %}

As mentioned above, the theme for this site uses some custom templates; these use metadata extensively to customize the layout of the pages, while keeping the same overall site theming.

Author names are also exposed as metadata collections; in an index template, model.authors indexes the authors' preferred name by their ID. {{ model.authors | value: post.author_id }} will display the author's name.