

Latest Update: 08/02/2016
As of the time of writing this, the current versions of our applications are:
If you are reading this and these are not the latest, let me know and Iโll update this tutorial accordingly.
Weโve now added roles, users, and posts to our blogging platform, fixed up some bugs, upgraded some dependencies, and fixed our seeds. Our platform is really nice and stable and has a good set of base functionality. If you havenโt been following along or missed a step along the way, you can continue with the โ11092015โ branch of https://github.com/Diamond/pxblog.
Given that weโre tech people building a blog, one that we may or may not be using, we should probably allow people to write posts in something other than plaintext support. Markdown happens to be a language that works great for blogging and is really simple to support in Elixir, so letโs implement Markdown support using a third party library called Earmark. We start off simply by adding a dependency to our application in the deps function in mix.exs:
{:earmark, "~> 1.0.1"}
And then weโll run mix do deps.get, compile to make sure we added it successfully to our application. If all goes well, we can move right on to adding support into our templates and views.
Weโll start by providing a convenient method to convert a post into Markdown. We donโt want to have to write Earmark.to_html(data) all over the place, and if we ever decide to support any of Earmarkโs other features and options, weโd have to rewrite that code over and over. Instead, weโll implement a function in web/views/post_view.ex that will give us a single place to modify if we ever want to change that. Open that file up and add the following function:
def markdown(body) do
body
|> Earmark.to_html
|> raw
end
We have the input text, body, that weโre piping into Earmarkโs โto_htmlโ function. Since weโre expecting html out that we can place into our post, so weโll use raw here. I know what youโre thinking: โyikes, isnโt raw terribly unsafe?โ The answer is yes, it absolutely is. Trouble is, we need to output in raw html from Earmark, so we need to get this data rendered. We also donโt want to end up having to strip the same tags every single time we render this out, so letโs go into web/models/post.ex and add some code that will strip it out:
def changeset(struct, params \\ %{}) do
struct
|> cast(params, [:title, :body])
|> validate_required([:title, :body])
|> strip_unsafe_body(params)
end
defp strip_unsafe_body(model, %{"body" => nil}) do
model
end
defp strip_unsafe_body(model, %{"body" => body}) do
{:safe, clean_body} = Phoenix.HTML.html_escape(body)
model |> put_change(:body, clean_body)
end
defp strip_unsafe_body(model, _) do
model
end
We added a decent amount of code here, so letโs discuss what weโre doing. The first change we made is at the top, in our changeset function. We added a new call, strip_unsafe_body(model,ย โฆ). This will pattern match into one of three possible functions. The first includes a body parameter that is nil; the second is the method that includes a body parameter which we have to strip it out, and the third is a final catch-all.
We use the put_change function to replace the body change with a stripped version, which calls out to Phoenix.HTMLโs โhtml_escapeโ function. This function takes in text, and then returns out a tuple of {:safe, cleaned_up_body} on success.
Writing this code with no tests makes me queasy, especially with what weโre doing here, so letโs test it!
The first thing weโre going to need to do, since weโre depending on these model actions to keep our code safe, is make sure theyโre tested appropriately!
defmodule Pxblog.PostTest do
use Pxblog.ModelCase
alias Pxblog.Post
@valid_attrs %{body: "some content", title: "some content"}
@invalid_attrs %{}
test "changeset with valid attributes" do
changeset = Post.changeset(%Post{}, @valid_attrs)
assert changeset.valid?
end
test "changeset with invalid attributes" do
changeset = Post.changeset(%Post{}, @invalid_attrs)
refute changeset.valid?
end
test "when the body includes a script tag" do
changeset = Post.changeset(%Post{}, %{@valid_attrs | body: "Hello <script type='javascript'>alert('foo');</script>"})
refute String.match? get_change(changeset, :body), ~r{<script>}
end
test "when the body includes an iframe tag" do
changeset = Post.changeset(%Post{}, %{@valid_attrs | body: "Hello <iframe src='http://google.com'></iframe>"})
refute String.match? get_change(changeset, :body), ~r{<iframe>}
end
test "body includes no stripped tags" do
changeset = Post.changeset(%Post{}, @valid_attrs)
assert get_change(changeset, :body) == @valid_attrs[:body]
end
end
The first thing we have to modify is our @valid_attrs declaration at the top. When we receive data from the controller itโs going to be in the form of string-based keys, not atoms, and it will cause our tests to fail unless we modify that.
Next, we import in Ecto.Changeset, but only the get_change/2 function, which we need to pull the translated value out of the changeset. Finally, we write four tests:
Re-run our test suite and we should be green. We have one more test to write and then some a few basic UI changes and then we are complete!
We should also write a quick test for our markdown helper function that we wrote in our Post View module. The good news is this should be a very simple test to write.
Create test/view/post_view_test.exs and fill it with the following:
defmodule Pxblog.PostViewTest do
use Pxblog.ConnCase, async: true
test "converts markdown to html" do
{:safe, result} = Pxblog.PostView.markdown("**bold me**")
assert String.contains? result, "<strong>bold me</strong>"
end
test "leaves text with no markdown alone" do
{:safe, result} = Pxblog.PostView.markdown("leave me alone")
assert String.contains? result, "leave me alone"
end
end
Run our tests, verify that all is green, and weโre done with updating our tests for this new functionality!
Right now, we have the ability to write up full posts but the UI doesnโt really support us editing anything larger than a single line. Weโll modify the standard form to include a text area instead of a textbox and weโll also give it a shiny, better editor.
Open up web/templates/post/form.html.eex and add an id parameter and give it an id of โbody-editorโ. The finished line should look like this:
<%= textarea f, :body, class: "form-control", id: "body-editor" %>
Finally, add the following code to the bottom of your formโs template (web/templates/post/form.html.eex):
<link rel="stylesheet" href="//cdn.jsdelivr.net/simplemde/latest/simplemde.min.css">
<script src="//cdn.jsdelivr.net/simplemde/latest/simplemde.min.js"></script>
<script>var simplemde = new SimpleMDE();</script>
Reload our UI and we should see a very clean interface for writing new posts! This is starting to look like a proper blogging platform and everything!
Finally, open up web/templates/post/show.html.eex and change the line that displays the postโs body and change it to use the markdown(body) function we defined in the view.
<%= markdown(@post.body) %>
We now have a better means of editing/formatting our posts and the editor looks much, much nicer! Weโll probably also want to fix up the layout at some point, and there still isnโt any way to add comments, but commenting actually would be a fun way to implement support for channels, so weโll hold off for now until weโre ready to build that support in!
If you want to see the completed version of this code, just grab the โadd_markdown_supportโ branch of our repo.
Writing A Blog Engine In Phoenix And Elixir: Part 7, Adding Comments Support
Hey everyone! If you liked what you read here and want to learn more with me, check out my new book on Elixir and Phoenix web development:
Iโm really excited to finally be bringing this project to the world! Itโs written in the same style as my other tutorials where we will be building the scaffold of a full project from start to finish, even covering some of the trickier topics like file uploads, Twitter/Google OAuth logins, and APIs!
Create your free account to unlock your custom reading experience.