gus

gus

Nerves Core Team

HEEx inside of standalone markdown templates - a guide

I wanted to share a method for embedding HEEx components inside of markdown files as templates, in the form of a mini-guide here on the forum. It is heavily based on the implementation of sigil_M by @leandrocp and his excellent MDEx library - huge thank you for developing and maintaining this library!!

The reason I wanted to do this was so that I could write my markdown in separate files located in the priv/blog/posts directory, but render the markdown into HEEx at compile time for good performance.

Add Support for Compiling Markdown to HEEx:

For regular HTML HEEx templates, you can place your templates anywhere and use the embed_templates/2 macro to import the templates as function components. The default supported templates are :eex, :exs, :leex, and :heex

Fortunately, Phoenix makes it super easy to add a new template engine, it just needs to implement the Phoenix.Template.Engine behavior. We can take the implementation of sigil_M in the MDEx examples and tweak it to load a file, read the contents and then convert the markdown string to HEEx.

Note: this requires that you add {:mdex, "~> 0.3.2"} and {:html_entities, "~> 0.5.2"} to your mix.exs

Engine implementation:

# lib/my_app_web/md_engine.ex
defmodule MyAppWeb.MdEngine do
  @behaviour Phoenix.Template.Engine

  # based on https://github.com/phoenixframework/phoenix_live_view/blob/main/lib/phoenix_live_view/html_engine.ex#L12

  def compile(path, _name) do
    quote do
      require MyAppWeb.MdEngine
      MyAppWeb.MdEngine.compile(unquote(path))
    end
  end

  # based on https://github.com/leandrocp/mdex/blob/main/examples/live_view.exs

  @doc false
  defmacro compile(path) do
    trim = Application.get_env(:phoenix, :trim_on_html_eex_engine, true)

    source = File.read!(path)

    mdex_opts = [
      extension: [
        strikethrough: true,
        tagfilter: true,
        table: true,
        tasklist: true,
        footnotes: true,
        shortcodes: true
      ],
      parse: [
        relaxed_tasklist_matching: true
      ],
      render: [
        unsafe_: true
      ]
    ]

    md =
      source
      |> MDEx.to_html!(mdex_opts)
      |> MyAppWeb.MdEngine.unescape()
      |> IO.iodata_to_binary()

    eex_opts = [
      engine: Phoenix.LiveView.TagEngine,
      file: path,
      line: 1,
      trim: trim,
      caller: __CALLER__,
      source: md,
      tag_handler: Phoenix.LiveView.HTMLEngine
    ]

    EEx.compile_string(md, eex_opts)
  end

  def unescape(html) do
    ~r/(<pre.*?<\/pre>)/s
    |> Regex.split(html, include_captures: true)
    |> Enum.map(fn part ->
      if String.starts_with?(part, "<pre") do
        part
      else
        HtmlEntities.decode(part)
      end
    end)
    |> Enum.join()
  end
end

To tell Phoenix how to use the new template engine, just add the following in your config/config.exs:

config :phoenix, :template_engines, md: MyAppWeb.MdEngine

And that’s it - you can now use embed_templates/2 in your controllers or LiveViews to compile markdown files to HEEx components

Example:

Markdown file located at priv/blog/posts/example.md

# You can use regular markdown

_Some_ **markdown** [here](https://phoenixframework.org)

## You can also embed components!
<.link navigate={~p"/home"}>Go home</.link>

<.button phx-click="clicked">Hello there!</.button>

## You can even use assigns:
<.form for={@form} :let={f} phx-submit="submit">
  <.input field={f[:email]} label="Email address" />
</.form>

LiveView:

# lib/my_app_web/live/test_live.ex
defmodule MyAppWeb.TestLive do
  use MyAppWeb, :live_view

  # note, do not add the `.md` extension for the template!
  embed_templates "example", root: Application.app_dir(:my_app, "priv/blog/posts")

  def render(assigns) do
    ~H"""
    <div class="prose">
      <p>This is outside of the component</p>
      <.example form={@form} />
    </div>
    """
  end

  def mount(_params, _session, socket) do
    {:ok, assign(socket, form: to_form(%{}))}
  end
end

Conclusion

  • I use this method with Nimble Publisher to render blog posts. You can add support for the Elixir map attributes at to the top of the file by modifying md_engine.ex with the following:
    source =
      File.read!(path)
      |> String.split(["\n---\n", "\r\n---\r\n"])
      |> List.last()
  • I like external markdown files because you can integrate additional tooling to help you write. For example, I use dprint for auto-format and harper for spell check.

Let me know if this is helpful!

Where Next?

Popular in Guides/Tuts Top

marcelo
I wrote a small article on how to use current_user with coherence on Phoenix. There are already a couple of blogposts about it but I thin...
New
fuelen
Hi all! Just want to share a small code snippet which allows writing CASE expressions using macro which is similar to cond. Here is an ...
New
dennisreimann
I wrote a guide for implementing Passwordless Authentication a.k.a. “Magic Login Links”: https://dennisreimann.de/articles/phoenix-passw...
New
OvermindDL1
Ran across this recently, it’s a set of cheatsheet inforgraphic things of the OTP behaviours on the BEAM: https://github.com/Telichkin/o...
New
WolfDan
So my main OS is Windows, I do must of my work with it, Elixir and vscode elixirls works just fine when you’re working only with elixir, ...
New
dkuhlman
For those of you who might be interested in using ZeroMQ in Elixir, I converted the Erlang examples in the ZeroMQ “zguide” to Elixir. I ...
New
lukertty
Install web-mode and mmm-mode first and put this in your config file: (require 'mmm-mode) (require 'web-mode) (setq mmm-global-mode 'may...
New
magnetic
Hey :waving_hand:t3: Elixir community, I’ve been learning Elixir, and working on some side projects. My editor of choice is VSCode, and ...
New
Morzaram
Hey guys I’ve made a guide on how to connect Quill to Phoenix. For the sake of formatting it might be easier to view it on my Notion Doc...
New
njwest
In the process of developing a Phx-based multiplayer experience, I found myself with so many browser tabs open with Elixir gaming resourc...
New

Other popular topics Top

malloryerik
Hi, this is for people who, like me, have had some friction using .html.heex templates in VSCode. The solution seems to be, in a hyphena...
New
TunkShif
This post is an instruction guide to help you setup your Neovim for Elixir development from scratch. It includes general information on h...
274 42158 114
New
shahryarjb
Hello, I have map which I want to convert it to string like this: the map: %{last_name: "tavakkoli", name: "shahryar"} the string I ne...
New
Fl4m3Ph03n1x
About me? ( if you have nothing better to do than reading about some random guy in the internet :stuck_out_tongue: ) Hello all, this is ...
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New
rms.mrcs
Hi, I need to transform a list of numbers into a map where the keys are the indexes and the values are the original values of the list. ...
New
hariharasudhan94
I would like to know what is the best IDE for elixir development?
New
openscript
Hello! Sorry for this astonishing simple question, but I’m really stuck. I try to set up the intellij-elixir plugin, but I don’t know ho...
New
hariharasudhan94
Lets say I have map like this fetching from my database %{"_id" =&gt; #BSON.ObjectId&lt;58eb1a7a9ad169198c3dXXXX&gt;, "email" =&gt; ...
New
jononomo
For some reason my phoenix channels are working for me in my local dev environment, but as soon as I deploy via Docker, I get a 403 error...
New

We're in Beta

About us Mission Statement