Soju is an extension mechanism that lets you call custom Python code from substition parameters.
To run user-defined Python code and have its output appear on a page, you need to do two things:
This page will show you how to do both.
To define a Soju function, you need to add your own user-defined Python code into the lib/soju.py file within your project.
This is what the lib/soju.py file looks like when Uriel first creates a new project:
##############################################################################
# soju.py #
##############################################################################
# The following symbols are imported using magic:
#
# import uriel
# from uriel import SojuError
# from uriel import log
# from uriel import escape
# The following variables are available to pass to functions:
#
# page
# node
# project_root
# use_canonical_url
# {{soju:node_title(node)}}
def node_title(node):
return escape(node.get_title())
You can add your own functions to this file, and reference them in nodes and templates, so the output of these functions shows up in pages on your web site.
The uriel program itself is imported into lib/soju.py as a module, along with the log() and escape() functions and the SojuError exception. See the Uriel API reference for details.
Four variables are present when your functions are called. You can write your functions to accept and make use of these variables:
| Variable Name | Type | Comments |
|---|---|---|
| page | uriel.Page | uriel.Page instance used to render the current page |
| node | uriel.Node | The uriel.Node instance for the current page |
| project_root | str | Filesystem path to your Uriel project root directory |
| use_canonical_url | bool | Use canonical URL when generating links? |
We won't discuss page further here. See the Uriel source code if you're curious.
The node variable is extremely useful. This gives you access to dozens of methods that can provide information about the current node, find other nodes on the site and get their Node instances, etc.
The project_root variable contains the filesystem path to your project directory.
The use_canonical_url variable is a boolean that indicates the mode that is being used to render the page when your function is called. Normal HTML pages are rendered with use_canonical_url set to False. But if you are generating an RSS feed, then your function can be called with use_canonical_url set to True during that rendering step. If you are generating your own links to other nodes inside of your Soju function, you'll want to accept this as a function argument, and conditionalize your code to work in both conditions.
To reference your custom Soju function, you will need to call it from a substition parameter in a node or a template.
For example, the default lib/soju.py that Uriel generates when it creates a new project contains the following function:
# {{soju:node_title(node)}}
def node_title(node):
return escape(node.get_title())
We can reference that function by including the {{soju:node_title(node)}} substitution parameter on the node that creates this page. When we do that, here is the result of that Soju function call to the node_title(node) function:
Soju
The function evaluated to the HTML escaped title of the current node, and the string was included in the rendered page.
Let's take a look at a non-trivial example function. Here is the source code for the example() function in the lib/soju.py file for this documentation site:
# {{soju:example(page, node, project_root, use_canonical_url)}}
def example(page, node, project_root, use_canonical_url):
"""
Example Soju function, demonstrating several things in one place.
Accepts a page, node, project_root, and use_canonical_url.
Returns an example string to use in the Soju documentation.
"""
# log a message when the site builds
log("the example() function is running now")
# create a list of strings that we will combine and return
lines = []
# show the uriel version (which is referenced from the uriel module,
# along with the string values of each argument passed to this function
lines.append("Hello from the <b>example()</b> function!")
lines.append("")
lines.append("Generated by uriel version " + escape(uriel.VERSION))
lines.append("")
lines.append("page: " + escape(str(page)))
lines.append("node: " + escape(str(node)))
lines.append("project_root: " + escape(project_root))
lines.append("use_canonical_url: " + escape(str(use_canonical_url)))
lines.append("")
# find the node for the Uriel API documentation index
# (the nodes/uriel/index file)
uriel_api_doc_index_node = node.find_node_by_path("uriel/index")
# show a link to the node
lines.append(uriel_api_doc_index_node.get_link())
# go through each child node under the Uriel API documentation index,
# sorted by (title + node path). Title is not always guaranteed to
# be unique, so the unique node path will act as a tie breaker in.
# case any nodes have the same title.
for child_node in sorted(uriel_api_doc_index_node.get_children(),
key=lambda n: n.get_title() + n.get_path()):
lines.append(" " + child_node.get_link())
# if you uncomment this line, it will cause an error when the site builds
#raise SojuError("oops")
# combine and return the lines as a string
return "\n".join(lines)
When we call this function, by including it a node or template, using the
{{soju:example(page, node, project_root, use_canonical_url)}}
substitution parameter, this is the output of the function that ends up
on the rendered page:
Hello from the example() function! Generated by uriel version 1.4.1 page: soju [default.html] node: soju project_root: . use_canonical_url: False Uriel API Constants Exceptions Functions Node Classes
You can see the output of the log() function when the site builds:
copying 'static' to 'public', overwriting previous contents initializing soju initializing handlers reading node files rendering node content soju: the example() function is running now creating pages in 'public' from nodes and templates ... lots of output omitted for brevity ... created 110 pages (95 file, 15 virtual) creating 'public/rss.xml' soju: the example() function is running now creating 'public/sitemap.xml' creating 'public/robots.txt' copying 'static' to 'public'
Notice that the log() message shows up twice? This is because the example() function is called twice. Once when the HTML page is generated, and again when the RSS feed is generated.
The example() function basically collects a variety of values from various parts of the Uriel API, and then returns a string that can be displayed in place of the substitution parameter on the generated page.
The escape() function performs HTML escaping. As a general rule, it should be wrapped around any dynamic values that are returned.
However, any Node methods that return HTML fragments should not be escaped (e.g. get_link()).
You should always be aware of HTML escaping when writing custom Soju functions.
All Soju functions must return a Python str value.
If your function returns a value with a type other than str, has a syntax error, or if it raises an exception when it is called, Uriel will show an error when the site builds. This error includes detailed information about which node and templates were involved, which Soju function was called, and what the error was, including a Python traceback. This makes it easy to find what caused the build to fail, and where to start your debugging efforts.
If you want to avoid the Python traceback portion of the error, you can raise a SojuError when you need to raise an expected error. This will still result in detailed information about where the error originated, but it will skip the Python traceback portion.
If you uncomment the SojuError line from the example() function, it will cause the web site build to fail, with a brief error message.
Example SojuError build error message:
copying 'static' to 'public', overwriting previous contents
initializing soju
initializing handlers
reading node files
rendering node content
soju: the example() function is running now
parameter error:
nodes/soju
templates/default.html
nodes/soju
'{{soju:example(page, node, project_root, use_canonical_url)}}'
'oops'
soju: error in function call to 'soju.example(page, node, project_root, use_canonical_url)': 'oops'
Tags: user-defined-python-code
This page was generated by Uriel with the following settings:
Page Details
| Resource | Path | Project File |
|---|---|---|
| Node | soju | nodes/soju |
| Template | default.html | templates/default.html |
| URL | /soju/ | public/soju/index.html |
Node Headers
| Header (Lowercase) | Value |
|---|---|
| rss-include | true |
| tags | user-defined-python-code |
| title | Soju |
| breadcrumb-separator | » |
| canonical-url | https://documentation.uriel.foo |
| rss-description | Uriel Documentation |
| rss-image-height | 32 |
| rss-image-url | /favicon-32x32.png |
| rss-image-width | 32 |
| rss-max-entries | 50 |
| rss-title | Uriel Documentation |
| rss-url | /rss.xml |
| sitemap-max-entries | 10000 |
| sitemap-url | /sitemap.xml |
| tag-node | tag |
| template | default.html |
Node Timestamps
| Type | Value |
|---|---|
| Created | |
| Modified | 2026-06-10T23:23:30-04:00 |
Node Methods
| Method | Value |
|---|---|
| get_parent_node() | index |
| get_path() | soju |
| get_node_type() | file |
| get_url() | /soju/ |
| get_canonical_url() | https://documentation.uriel.foo/soju/ |
| get_name() | soju |
| get_display_name() | Soju |
| get_title() | Soju |
| get_escaped_title() | Soju |
| get_link() | <a href="/soju/">Soju</a> |
| get_canonical_link() | <a href="https://documentation.uriel.foo/soju/">Soju</a> |
| get_link_prefix() | <p> |
| get_link_suffix() | </p> |
| get_tags() | ['user-defined-python-code'] |
| get_dest_dir() | ./public/soju |
| get_dest_file() | ./public/soju/index.html |
| get_breadcrumb_separator() | » |