Building Websites with Mathematica, Part 2
So in this website’s very first post I ran through how one can use pelican and Mathematica in conjunction to make a website. Now we’re gonna cut pelican out all together.
The basic process will look almost exactly the same, though:
-
Write content in Mathematica notebooks
-
Customize the website via themes and templates
-
Export to build directory
-
(Optional) Deploy to cloud
The main trick here, though, is in that innocuous “customize the website via themes and templates”, as it’s that “templates” that will really trip us up.
XMLTemplates
Basic Overview
Mathematica’s had a templating system for a little while, now:
PacletFind["Templating"][[1]]["MathematicaVersion"]
(*Out:*)
"10.1+"
This system has a general templating language that runs through TemplateObject
. The templating language is too clunky for most simple cases, though. Because of this the team that made the templating system added a collection of specialized templates, including StringTemplate
, NotebookTemplate
, FileTemplate
, and most important for us, XMLTemplate
.
It’s this XMLTemplate
system that we’ll use to build our site. So the first order-of-business is to translate our old Jinja templates into XMLTemplate
-ready form.
Here’s a quick example. First the Jinja template:
<title> {% block title %} {{ SITENAME }} {% endblock %} </title>
<link rel="canonical" href="{{ SITEURL }}/{{ output_file }}">
And now the XMLTemplate
<title><wolfram:slot id="Title"><wolfram:slot id = "SiteName"/></wolfram:slot></title>
<link rel="canonical" href="<wolfram:slot id='SiteURL'/>/<wolfram:slot id='OutputFile'/>">
In general there’s a pretty simple translation between the two, although the Wolfram version is, unsurprisingly, more verbose.
XMLTemplate Complications
Unfortunately there are cases when things get more complex where this breaks down. The core case is when you are extending templates or using passed variables as slot names—or whenever you need variables to cascade up or down, essentially.
As an example, in Jinja we might have this:
{% extends "index.html" %}
{% block title %}
# {{ tag }} Articles | {{ SITENAME }}
{% endblock %}
{% block page_header %}
# {{ tag }} Articles
{% endblock %}
Our naive XMLTemplate
would look like this:
<wolfram:get path = "index.html">
<wolfram:slot id = "Title">
# <wolfram:slot id = "Tag"/> Articles | <wolfram:slot id = "SiteName"/>
</wolfram:slot>
<wolfram:slot id = "PageHeader">
# <wolfram:slot id = "Tag"/> Articles
</wolfram:slot>
</wolfram:get>
But we can run into issues here. The first might occur in passing "Title"
to our template.
Say we apply this template like so:
temp =
XMLTemplate@"
<wolfram:comment>
We're excluding the <wolfram:get>...</wolfram:get> for now
</wolfram:comment>
<wolfram:slot id='Title'>
#<wolfram:slot id='Tag'/> Articles | <wolfram:slot id='SiteName'/>
</wolfram:slot>
<wolfram:slot id='PageHeader'>
#<wolfram:slot id='Tag'/> Articles
</wolfram:slot>
";
TemplateApply[
temp, <|"Tag" -> "tag", "SiteName" -> "mycoolsite"|>] // StringTrim
-----------Out-----------
#tag Articles | mycoolsite
#tag Articles
All good here. But what happens if we also pass a "Title"
?
TemplateApply[
temp, <|"Tag" -> "tag", "SiteName" -> "mycoolsite",
"Title" -> "mytitle"|>] // StringTrim
-----------Out-----------
mytitle
#tag Articles
The "Title"
slot got wiped out. This shows how you can be burned if you’re expecting the "Title"
to cascade up. The "Title"
is a global replacement as are all passed variables. If you have a "Title"
slot in a parent template, the "Title"
will insert there. A somewhat better way to write this is like so:
temp =
XMLTemplate@"
<wolfram:comment>
We're excluding the <wolfram:get>...</wolfram:get> for now
</wolfram:comment>
<wolfram:slot id='PassedTitle'>
#<wolfram:slot id='Tag'/> Articles | <wolfram:slot id='SiteName'/> \
| <wolfram:slot id='Title'/>
</wolfram:slot>
<wolfram:slot id='PageHeader'>
#<wolfram:slot id='Tag'/> Articles
</wolfram:slot>
";
TemplateApply[
temp, <|"Tag" -> "tag", "SiteName" -> "mycoolsite",
"Title" -> "mytitle"|>] // StringTrim
-----------Out-----------
#tag Articles | mycoolsite | mytitle
#tag Articles
This way the "Title"
will cascade up as "PassedTitle"
.
This can also bite you when you are using "<wolfram:get>"
blocks the other way. Say you have a "var"
parameter that you’re generating and which you’d like to pass into a "<wolfram:get>"
block, like so:
varExists =
"<wolfram:which>
<wolfram:if test='KeyMemberQ[#,#var]'>
<wolfram:slot id='if'></wolfram:slot>
</wolfram:if>
<wolfram:else>
<wolfram:slot id='else' />
</wolfram:else>
</wolfram:which>";
This seems pretty good. We have a test which tests if a parameter "var"
is in our template variable list. Well, when we drop this in a file and test it like so:
dir = FileNameJoin@{$TemporaryDirectory, "xml_templates"};
CreateDirectory[dir];
Export[FileNameJoin@{dir, "varExists"},
varExists,
"Text"
];
temp = "
<title>
<wolfram:with longTitle='True'>
<wolfram:get path='varExists'>
<wolfram:slot id='var'>longTitle</wolfram:slot>
<wolfram:slot id='if'>A very long title</wolfram:slot>
<wolfram:slot id='else'>A more compact title</wolfram:slot>
</wolfram:get>
</wolfram:with>
</title>";
Block[{
$TemplatePath =
Append[$TemplatePath, dir]
},
TemplateApply[XMLTemplate@temp, <||>]
]
-----------Out-----------
<title>
A more compact title
</title>
We see here that the Slot
operator doesn’t work as one would expect. It is not an Association
of all current stack arguments, but rather just those passed. There’s some syntactic sugar to support "#var"
type names from the stack, but meta-programming of this nature is hard. Similarly there can be complex issues with variable passing and values which all derive from this core issue.
XMLTemplates Routine Libraries
An effective way to circumvent these issues is to fall back to the core Mathematica code. In particular there are parameters Templating`$TemplateArgumentStack
and Templating`$TemplateArguments
which provide access to stack values. We can define a new varExists
routine like this:
With[{
tempArgs =
$$templateLib["getTemplateArguments"][#]
},
! MatchQ[
$$templateLib["templateArgumentLookup"][tempArgs, "var"],
_Missing | False | None | _String?(StringMatchQ[Whitespace])
]
] &
Which takes advantage of two other routines, getTemplateArguments
:
(Join @@
Flatten@{
#,
Replace[Templating`$TemplateArgumentStack, {
{___, a_} :> a,
_ -> <||>
}]
}) &
And templateArgumentLookup
:
Replace[
#@
Replace[#[#2],
t_TemplateObject :>
TemplateApply[t, #]
],
TemplateObject[{
Templating`Evaluator`PackagePrivate`apply[_,
a_
]
}] :>
Block[
{
Templating`PackageScope`$TemplateEvaluate = True
},
a
]
] &
Then we place these in .m files and define a loading function like this:
With[{Templating`lib`Private`libdir = DirectoryName[$InputFileName]},
Templating`lib`$$templateLib[f_] :=
Templating`lib`$$templateLib[f] =
(
Begin["Templating`lib`Private`"];
(End[]; #) &@
Import@FileNameJoin[{Templating`lib`Private`libdir, f <> ".m"}]
)
]
Then we can use these routines in XMLTemplate
expressions, like this:
<!--varDefinedBlock.html-->
<wolfram:which>
<wolfram:if
test='$$templateLib["varDefined"][#]'>
<wolfram:slot id='if' />
</wolfram:if>
<wolfram:else>
<wolfram:slot id='else' />
</wolfram:else>
</wolfram:which>
And then in our generator we prep and clear $$templateLib
.
By doing this we can make the XMLTemplate
framework a lot more flexible and extensible and make our code a lot less verbose.
Building Content
Basic Idea
So with our XMLTemplate
considerations out of the way we move on to the real work of content generation. The basic process idea is that we’ll have an ever growing stack of parameters which encapsulates all of our site data. We’ll populate it from the .md and .html files in our content directory in the following order:
-
Meta-information for all files
-
Helper-info like URL, Summary, etc. for posts and pages
-
Raw HTML for posts and pages
-
Aggregation pages, like the index.html
We’ll have this stack be an Association
for convenient lookup, which allows us to provide helper functions like this:
"ContentData" ->
Function[
Fold[
Lookup[#, #2, <||>] &,
$ContentStack,
{#, "Attributes"}
]
]
Or aggregations like this:
"Pages" :>
Select[
Values@
$ContentStack[[All, "Attributes"]],
MemberQ[#["Templates"], "page.html"] &
]
Then by exposing this $ContentStack
to the templates, we can write even more powerful templates with even less work. For instance, here’s a snippet from my standard index.html template:
<wolfram:sequence
values="Replace[#IndexListing,Except[_List]:>#Articles]"
slot="IndexItem" delimiters="<hr>"
>
<div class="article-bubble bubble centered">
<article class="teaser">
<h4 class="article-title">
<a href='<wolfram:slot id="SiteURL"/>/<wolfram:expr>#IndexItem["URL"]</wolfram:expr>'>
<wolfram:expr>#IndexItem["Title"]</wolfram:expr>
</a>
</h4>
<div class="content article-summary">
<wolfram:expr>#IndexItem["Summary"]</wolfram:expr>
</div>
</article>
</div>
We pass the entire $ContentStack
to the TemplateApply
call, so the template can take advantage of the "Articles"
parameter it provides.
Markdown Parsing
The hard part of all this is that we have to have an effective way to go from Markdown to HTML. Since we want to work with pure Mathematica code we need a Markdown to HTML converter function—something which I was unable to find before starting work on the site builder.
I won’t go into all the details of how to write one of these now, but know that it’s a pretty tedious endeavor (and my version is not even entirely complete). Let it suffice to know that one now exists.
Copying Themes
The second-to-last (or final if you aren’t deploying in the cloud) step is to copy over the theme. We want to be efficient about how we do this—it’s wasteful to copy it over every time. To make this more efficient we use a system where we check the FileDate
of the previously copied version and the version in the core theme directory. If the previously copied one is older we copy over the new one.
Deployment
Finally, with all this in place, we can move to actually deploying our page. We will do it in exactly the same way as described in the original post.
Extensions
The power of having something like this in pure Mathematica code is that it significantly lowers the barriers to developing websites. Making a new site is as simple as creating a new collection of XMLTemplate
s (or more likely adapting an old one), maybe adding some new $$templateLib
functions.
This is very powerful, and I’ve already set up two systems that use this. My first is an extension of the built-in PacletManager
, which creates a static site that lists what is on a paclet server. The second is a general documentation host for providing a nicer interface on the documentation I generate and web-deploy.
The first one lives here and the impetus behind its creation is described in this post
And the latter is here and the impetus behind its creation is described in this post
Even better, both of these can also live as their own packages without any dependencies on anything outside of Mathematica. Having a native site builder just makes things vastly quicker to set up and so much more convenient.
And with that, I think, we’re done.