Show template used in this post
<article class="column post-section-title">
<h2><a href={"/post/#{@post.frontmatter["permalink"]}"}><%= @post.frontmatter["title"] %></a></h2>
<h3><%= @post.frontmatter["date"] %></h3>
<div class="post">
<details open>
<summary>Show template used in this post</summary>
<pre><code class="html language-html"><%= @post.frontmatter["template"]["heex"] %></code></pre>
</details>
<%= Phoenix.HTML.raw @post.content %>
</div>
</article>
After I wrote my first ever blog in Phoenix, I was thinking about what interesting things I can experiment with it. And here it is!
As you can see, this page has a <details>
element at the top, and that is the HEEx template for this post. So, yes, I wrote the HEEx template inside a markdown file, compile it on-the-fly, and output itself in the rendered page!
Let's see how I made this possible.
Write the HEEx Template in Markdown Frontmatter
In the last post, I mentioned how I parsed the frontmatter in a markdown file, and since the frontmatter is really just some YAML strings fenced by ---
, we can write anything we want inside it, that would of course include an HEEx template. For example,
---
title: My Next Post
date: 2023-01-04
excerpt: What should I write?
permalink: next-post
template:
using: "embed.html"
heex: |
<article class="column post-section-title">
<div class="post">
<h2>An HEEx template embbeded in a markdown file!</h2>
<%= Phoenix.HTML.raw @post.content %>
</div>
</article>
---
Conditional Rendering
After that, we need to check if the current post has a custom HEEx template, if so, we forward the rendering process to another module, say UwUBlogWeb.PageView
. Otherwise, we use the normal template defined in post.html.heex
. Therefore, I changed the code in post.html.heex
to
<%= if Map.has_key?(@post.frontmatter, "template") do %>
<%= render(UwUBlogWeb.PageView, @post.frontmatter["template"]["using"], post: @post) %>
<% else %>
<article class="column post-section-title">
<h2><a href={"/post/#{@post.frontmatter["permalink"]}"}><%= @post.frontmatter["title"] %></a></h2>
<h3><%= @post.frontmatter["date"] %></h3>
<div class="post">
<%= raw @post.content %>
</div>
</article>
<% end %>
The reason we can do this is because we can call Phoenix.View.render/3
and explicitly render the template. You can find a bit more information on Phoenix's hexdocs page, Manually rendering templates. I'll mention the code in UwUBlogWeb.PageView
later.
However, it wouldn't be that simple because the compilation of HEEx templates usually happens, well, at compile-time, via the Phoenix.LiveView.HTMLEngine
. It expects either a .heex
template file or a ~H
sigil, instead of the template we embedded in a markdown file.
Customise the HTMLEngine
Hence, I did some kind of reverse engineering and found that I need to customise the Phoenix.LiveView.HTMLEngine
and create my own UwUBlog.HTMLEngine
to fulfill this purpose. The reason is that Phoenix.LiveView.HTMLEngine.compile/2
calls to EEx.compile_file/2
, and of course we can't pass a markdown file and expect it to do what we want (unless we customise it, but that's basically the same thing here). Luckily, we have EEx.compile_string/2
available to us, and we can retrieve the HEEx template string from the frontmatter.
So I copied the code from Phoenix.LiveView.HTMLEngine
and modified the compile/2
function,
defmodule UwUBlog.HTMLEngine do
# ...
def compile(string, _name) do
trim = Application.get_env(:phoenix, :trim_on_html_eex_engine, true)
EEx.compile_string(string, engine: __MODULE__, line: 1, trim: trim)
end
# ...
end
Compile HEEx Template on-the-fly and Render the Page
Now I need to figure out a way to connect everything in the UwUBlogWeb.PageView
module. For the render/2
function in a Phoenix view, it expects two arguments: the template name and the assigns. Here I decided to use "embed.html"
as the name.
Note that the variable assigns
here is constructed in the true branch of the conditional rendering if
-statement,
<%= if Map.has_key?(@post.frontmatter, "template") do %>
<%= render(UwUBlogWeb.PageView, @post.frontmatter["template"]["using"], post: @post) %>
<% else %>
Therefore the assigns
variable will have a single key, post
, and that's really all I need: the value of the post
key contains the following information,
%{
content: "...",
file: "posts/next_post.md",
frontmatter: %{
"date" => "2023-01-04",
"excerpt" => "What should I write?",
"permalink" => "next-post",
"template" => %{
"heex" => "<article class=\"column post-section-title\">\n <div class=\"post\">\n <h2>An HEEx template embbeded in a markdown file!</h2>\n <%= Phoenix.HTML.raw @post.content %>\n </div>\n</article>\n",
"using" => "embed.html"
},
"title" => "My Next Post"
},
...
}
Now I can compile the embeded HEEx template via UwUBlog.HTMLEngine.compile/2
.
def render("embed.html", %{post: post}=assigns) do
body = UwUBlog.HTMLEngine.compile(post.frontmatter["template"]["heex"], [])
# ...
end
The return value is the compiled Elixir AST, and it looks like this,
{:__block__, [],
[
{:require, [context: UwUBlog.HTMLEngine],
[{:__aliases__, [alias: false], [:Phoenix, :LiveView, :Helpers]}]},
# ...
{:%, [],
[
{:__aliases__, [alias: false], [:Phoenix, :LiveView, :Rendered]},
{:%{}, [],
[
static: ["<article class=\"column post-section-title\">\n <div class=\"post\">\n <h2>An HEEx template embbeded in a markdown file!</h2>\n", "\n </div>\n</article>"],
dynamic: {:dynamic, [], Phoenix.LiveView.Engine},
fingerprint: 84211337665366584396207549738045929506,
root: true
]}
]}
]}
]}
]}
Since we have the AST, or we call it the quoted form in Elixir, the next step is to evaluate it with Code.eval_quoted/2
. Also, don't forget to pass in the binding list because we are doing it at runtime, so the variable assigns
in the AST is not bonded and that will cause an error.
The first element in the tuple returned by Code.eval_quoted
will be the %Phoenix.LiveView.Rendered{}
struct.
%Phoenix.LiveView.Rendered{
static: ["<article class=\"column post-section-title\">\n <div class=\"post\">\n <h2>An HEEx template embbeded in a markdown file!</h2>\n", "\n </div>\n</article>"],
dynamic: #Function<42.3316493/1 in :erl_eval.expr/6>,
fingerprint: 84211337665366584396207549738045929506,
root: true
}
Then we can simply use it as the return value of the UwUBlogWeb.PageView.render/2
function, and we are done! I don't even need to attach a screenshot here because what you are viewing is the result. :D
def render("embed.html", %{post: post}=assigns) do
body = UwUBlog.HTMLEngine.compile(post.frontmatter["template"]["heex"], [])
{rendered, _} = Code.eval_quoted(body, [assigns: assigns])
rendered
end
But why?
First of all, it's interesting!
The second point to do this is that if I ever need to create a special page for that one post, I can write the one-off template in that very markdown file instead of putting a single-use template somewhere in the project.