Blog

Building a Blog with Phoenix

Posted on February 14, 2016 by Clive

We've previously built a chat server using this Phoenix, but today I thought I would build something simple, just to show how it can be done using Phoenix. For those that are familiar with the Ruby on Rails "build a blog in under 30 minutes" tutorials, this is similar although I can't promise that it will be that quick.

 

What we're building today

In short we're building a small blogging engine. We'll concentrate on adding posts first, the entry contents will be written in Markdown and not HTML. We'll then move on to displaying the entries, first as a list and then as a single item.

We'll not do anything complicated today like adding categories/tags, users or authentication - that is for another day, but we will allow posts to have two states: draft and published.

 

Dependencies

If you haven't already done so (and I can't imagine why you haven't already), you will need to install Erlang, Elixir, Phoenix, PostgreSQL and Node.js

Detailed instructions for this can be found on the Phoenix installation page

This treatment uses Phoenix v1.1.4.

With Phoenix et al installed we can now...

 

Basic info for following along

In the code inserts, I use '$' to demonstrate what to enter at a command line prompt. After the command, I will usually show you have the terminal might have reacted. If you are following using Linux or a Mac then that should cause you no concerns. Windows users will need to adapt the commands relevant to their operating system.

File editing will need to take place in whichever editor you are comfortable with. Personally I use Sublime Text 3, but then I'm on a Mac. If a file needs to be edited, the path to that file is given relative to the project directory.

I use '...' to show that there is content but it is not directly important to what is being shown in the example. Unless otherwise stated any file contents that are not shown should be left in place.

 

Start the build

Using a command line, navigate to a convenient directory (mine is ~/Projects), and issue the following command

$ mix phoenix.new blog
* creating blog/config/config.exs
* creating blog/config/dev.exs
* creating blog/config/prod.exs
...

Fetch and install dependencies? [Yn] y     <--- Enter y here

* running mix deps.get
* running npm install && node node_modules/brunch/bin/brunch build

We are all set! Run your Phoenix application:

    $ cd blog
    $ mix phoenix.server

You can also run your app inside IEx (Interactive Elixir) as:

    $ iex -S mix phoenix.server

Before moving on, configure your database in config/dev.exs and run:

    $ mix ecto.create

$

Once this is complete, follow the given instructions

$ cd blog
$ mix phoenix.server

==> connection
Compiled lib/connection.ex
Generated connection app
...

[info] Running Blog.Endpoint with Cowboy using http on port 4000
12 Feb 18:09:19 - error: Compiling of web/static/js/app.js failed. Couldn't find preset "es2015" relative to directory "web/static/js" ; Compiling of web/static/js/socket.js failed. Couldn't find preset "es2015" relative to directory "web/static/js"

If you receive this error you can either

$ npm install --save babel-preset-es2015

or

$ rm -rf blog/
$ npm cache clean
$ npm install npm -g
$ mix phoenix.new blog

Phoenix requires Node.js >= 5.0 and NPM >= 3

Once you've done this, restart the server with mix phoenix.server and all should be well

$ mix phoenix.server
[info] Running Blog.Endpoint with Cowboy using http on port 4000
12 Feb 18:16:25 - info: compiled 5 files into 2 files, copied 3 in 3.3 sec

If you point a browser at http://localhost:4000 you should now see the default Phoenix site.

 

Setting up the database

Phoenix uses PostgreSQL as the default datastore. In order to use it, you will need to give Phoenix some access credentials.

To do this edit the config.dev.exs file (dev.exs because this is the development environment, should you wish to promote to test/production, you will need to update the test.exs or prod.exs files respectively.

$ vim config/dev.exs

...
# Configure your database
config :blog, Blog.Repo,
  adapter: Ecto.Adapters.Postgres,
  username: "postgres",                    <--- Update this
  password: "postgres",                    <--- and this
  database: "blog_dev",
  hostname: "localhost",
  pool_size: 10

then save the file.

Create the database

$ mix ecto.create
Compiled lib/blog.ex
Compiled web/views/error_helpers.ex
Compiled web/web.ex
...

The database for Blog.Repo has been created.
$

If this results in an error, most likely your database credentials have been entered into the dev.exs file incorrectly.

 

Generating the Post model

To help us a long a bit, we're going to scaffold the central model and controller: Post.

As a visual aid to the design of the database table, we'll be creating something similar to this

![Posts table design](/blog/building-a-blog-with-phoenix/db_posts.png)

Using the generators for this

$ mix phoenix.gen.html Post posts title:string url:string content:text excerpt:text date_published:date published:boolean

* creating web/controllers/post_controller.ex
* creating web/templates/post/edit.html.eex
* creating web/templates/post/form.html.eex
* creating web/templates/post/index.html.eex
* creating web/templates/post/new.html.eex
* creating web/templates/post/show.html.eex
* creating web/views/post_view.ex
* creating test/controllers/post_controller_test.exs
* creating priv/repo/migrations/20160212225348_create_post.exs
* creating web/models/post.ex
* creating test/models/post_test.exs

Add the resource to your browser scope in web/router.ex:

    resources "/posts", PostController

Remember to update your repository by running migrations:

    $ mix ecto.migrate

Update the web/routes.ex file like it says

  scope "/", Blog do
    pipe_through :browser # Use the default browser stack

    resources "/posts", PostController                    <---- Add this line
    get "/", PageController, :index
  end

then run the migration to create the database table

$ mix ecto.migrate

Compiled web/models/post.ex
Compiled web/views/error_view.ex
Compiled web/views/page_view.ex
Compiled web/controllers/page_controller.ex
Compiled web/views/layout_view.ex
Compiled web/controllers/post_controller.ex
Compiled web/router.ex
Compiled lib/blog/endpoint.ex
Compiled web/views/post_view.ex
Generated blog app

22:55:03.527 [info]  == Running Blog.Repo.Migrations.CreatePost.change/0 forward

22:55:03.527 [info]  create table posts

22:55:03.574 [info]  == Migrated in 0.4s

Fire up the server with mix phoenix.server and navigate to http://localhost:4000/posts (the resources route added to the routes file) and you should see a 'Listing Posts' page. The generator has created a basic site for us to use.

You can click around this and explore but as you can see it is very basic looking and still has the Phoenix branding.

You effectively have a blog, but it’s not very usable.

We'll address the layout first and then the various templates before building out any functionality.

 

Layout

From a design decision, there will be two layouts - one for the Admin section and one for general display.

To do this, you'll use a technique discussed in Phoenix - Applying alternative layouts to all a controllers views.

In the newly generated PostController (web/controllers/post_controller.ex), you'll make the following changes

defmodule Blog.PostController do
  use Blog.Web, :controller

  alias Blog.Post

  plug :scrub_params, "post" when action in [:create, :update]

  ### Add this function to the controller ###
  def action(conn, _) do
    conn = conn |> put_layout("admin_layout.html")
    apply(__MODULE__, action_name(conn), [conn, conn.params])
  end

  def index(conn, _params) do
    posts = Repo.all(Post)
    render(conn, "index.html", posts: posts)
  end

  ### Leave the rest of the module in place
  ...

If you try and run this now, you will receive a Phoenix.Template.UndefinedError error page. That's because the web/templates/layout/admin_layout.html.eex file doesn't exist yet.

So lets add this file

$ touch web/templates/layout/admin_layout.html.eex

Reloading the page should now give you a blank page - the error has gone away, but its not displaying anything, obvious really if you think about it, there's nothing in the file yet.

Let's sort that out, by adding the following content to it

<!DOCTYPE html>
<html lang="en">
    <head>
        <title>MyBlog Admin</title>
        <meta charset='utf-8' />
        <link rel="stylesheet" href="<%= static_path(@conn, "/css/app.css") %>">
    </head>
    <body>
        <header class=container-fluid>
            <div id="nav-header">
                <div class=row">
                    <div class="header-bar container">
                        <h2>MyBlog Admin</h2>
                    </div>
                    <hr />
                </div>
            </div>
        </header>
        <div class=container>
            <div class="col-sm-3">
                Nav content
            </div>
            <div class="col-sm-9">
                <%= render @view_module, @view_template, assigns %>
            </div>
        </div>
        <script src="<%= static_path(@conn, "/js/app.js") %>"></script>
    </body>
</html>

and replace the web/static/css/app.css file with this one

This renders an unobtrusive layout that we can work with.

 

Updating the post index template

Looking at the index page (http://localhost:4000/posts) template, we don't need to show all of the information - what we really need to show is the post title, the last updated and published date and its current status (draft/published).

To facilitate, you'll need to update the /web/templates/post/index.html.eex in the following way

<h2>Listing posts</h2>

<table class="table">
  <thead>
    <tr>
      <th>Title</th>
      <th>Status</th>
      <th>Updated Date</th>
      <th>Published Date</th>
      <th></th>
    </tr>
  </thead>
  <tbody>
<%= for post <- @posts do %>
    <tr>
      <td><%= post.title %></td>
      <td><%= post.published %></td>
      <td><%= post.updated_at %></td>
      <td><%= post.date_published %></td>

      <td class="text-right">
        <%= link "Show", to: post_path(@conn, :show, post), class: "btn btn-default btn-xs" %>
        <%= link "Edit", to: post_path(@conn, :edit, post), class: "btn btn-default btn-xs" %>
        <%= link "Delete", to: post_path(@conn, :delete, post), method: :delete, data: [confirm: "Are you sure?"], class: "btn btn-danger btn-xs" %>
      </td>
    </tr>
<% end %>
  </tbody>
</table>
<hr />
<%= link "New post", to: post_path(@conn, :new), class: "btn btn-primary" %>

You'll be making more changes to this file.

Before we do, let's just quickly add a Post item into the database. Instead of messing about with the form for this, we'll do this using the REPL (I've not included IEx output for brevity).

$ iex -S mix
Erlang/OTP 18 [erts-7.2.1] [source] [64-bit] [smp:8:8] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]

Interactive Elixir (1.2.2) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> alias Blog.Post
iex(2)> alias Blog.Repo
iex(3)> post = %Post{title: "Post 1", content: "This is test content", url: "post-1"}
iex(4)> Repo.insert post
iex(5)>

This shows how a post will be listed in the table.

Screenshot image of posts table

As you can tell from the image, the data in the Status column looks wrong and the date needs formatting so that it is more readable.

To do this, let’s add some functions to the View.

Addressing the Status column first.

Update web/views/post_view.ex to reflect the following

defmodule Blog.PostView do
  use Blog.Web, :view

  def publish_status(true), do: "Published"
  def publish_status(_), do: "Draft"
end

These two function clauses pattern-match against the input - if the post is published, then it will return "Published", everything else will be false.

To use this, update the web/templates/post/index.html.eex to use it

      <td><%= post.title %></td>
      <td><%= publish_status post.published %></td>
      <td><%= post.updated_at %></td>
      <td><%= post.date_published %></td>

To make sure that both clauses are working, add another Post (were adding the published_date as well which is an Ecto.Date to pretend that the second post is fully published)

iex(6)> {:ok, date} = Ecto.Date.cast("2016-01-31T00:00:00z")
iex(7)> date
iex(8)> post = %Post{title: "Post 2", content: "This is a second test post", published: true, date_published: date, url: "post-2"}
iex(9)> Repo.insert post
[debug] INSERT INTO "posts" ("inserted_at", "updated_at", "content", "date_published", "excerpt", "published", "title", "url") VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING "id" [{{2016, 2, 13}, {12, 57, 38, 0}}, {{2016, 2, 13}, {12, 57, 38, 0}}, "This is a second test post", {2016, 1, 31}, nil, true, "Post 2", nil] OK query=78.7ms queue=13.4ms
{:ok,
 %Blog.Post{__meta__: #Ecto.Schema.Metadata<:loaded>,
  content: "This is a second test post", date_published: #Ecto.Date<2016-01-31>,
  excerpt: nil, id: 3, inserted_at: #Ecto.DateTime<2016-02-13T12:57:38Z>,
  published: true, title: "Post 2",
  updated_at: #Ecto.DateTime<2016-02-13T12:57:38Z>, url: nil}}
iex(10)>

Reloading the page now should give you the following

Screenshot image of posts table

Now we can clearly see whats "Published" and whats "Draft"

Next we need to format the date, to do that we will use the Timex module, which needs to be added to the project dependencies in mix.exs.

defp deps do
  [{:phoenix, "~> 1.1.4"},
   {:postgrex, ">= 0.0.0"},
   {:phoenix_ecto, "~> 2.0"},
   {:phoenix_html, "~> 2.4"},
   {:phoenix_live_reload, "~> 1.0", only: :dev},
   {:gettext, "~> 0.9"},
   {:cowboy, "~> 1.0"},
   {:timex, "~> 0.19"}]
end

Remember to run mix deps.get and restart Phoenix once you've made this change.

defmodule Blog.PostView do
  use Blog.Web, :view
  use Timex

  def publish_status(true), do: "Published"
  def publish_status(_), do: "Draft"

  def date_format(date), do: date_format date, "%d %b %Y"

  def date_format(date = %Ecto.DateTime{}, format_string) do
    Ecto.DateTime.to_iso8601(date)
    |> date_formatter(format_string)
  end
  def date_format(date = %Ecto.Date{}, format_string) do
    << Ecto.Date.to_iso8601(date) <> "T00:00:00Z" >>
    |> date_formatter(format_string)
  end
  def date_format(_, _format), do: ""

  defp date_formatter(date, format_string) do
    date
    |> DateFormat.parse!("{ISOz}")
    |> DateFormat.format!(format_string, :strftime)
  end
end

Now the template just needs updating with

<td><%= date_format post.updated_at %></td>
<td><%= date_format post.date_published %></td>

This formats the dates in a more human readable form.

The next thing we'll look at is the Show action.

 

Updating the post show template

The Show template is really not pretty at all. We would like it to act as a "preview" for the article. We will put in a metadata section and a display section.

The metadata section will display the various dates, the URL and the publish status, and the display section will render out the article in a manner similar to how it will be displayed to the site reader.

In order to render out the post content, we will need to add a Markdown parser/formatter. I have chosen to use Earmark (others are available).

So add Earmark.

Update the mix.exs file as follows

...
defp deps do
  [{:phoenix, "~> 1.1.4"},
   {:phoenix_ecto, "~> 2.0"},
   {:postgrex, ">= 0.0.0"},
   {:phoenix_html, "~> 2.3"},
   {:phoenix_live_reload, "~> 1.0", only: :dev},
   {:gettext, "~> 0.9"},
   {:cowboy, "~> 1.0"},
   {:earmark, "~> 0.1"},
   {:timex, "~> 0.19"}]
end
...

You'll need to mix deps.get and restart Phoenix again.

Then in web/templates/views/post/show.html.eex make the following changes

<h2>Post Preview</h2>
<section class="meta">
  <h5>Meta Data</h5>
  <div class="row">
    <div class="col-sm-2">
      <p class="small">URL:</p>
    </div>
    <div class="col-sm-10">
      <%= @post.url %>
    </div>
  </div>
  <div class="row">
    <div class="col-sm-2">
      <p class="small">Published Status:</p>
    </div>
    <div class="col-sm-2">
      <%= publish_status @post.published %>
    </div>
    <div class="col-sm-2">
      <p class="small">Date Published:</p>
    </div>
    <div class="col-sm-2">
      <%= if @post.published do %>
        <%= date_format @post.date_published %>
      <% else %>
        N/A
      <% end %>
    </div>
    <div class="col-sm-2">
      <p class="small">Last Updated:</p>
    </div>
    <div class="col-sm-2">
      <%= date_format @post.updated_at %>
    </div>
  </div>
</section>
<hr/>
<section class="preview">
  <h5>Post Content</h5>
  <div class="row">
    <div class="col-sm-2">
      <p class="small">Header:</p>
    </div>
    <div class="col-sm-10">
      <h3><%= @post.title %></h3>
      <%= if @post.published do %>
        <p class="small"><%= date_format @post.date_published %></p>
      <% end %>
    </div>
  </div>
  <hr/>
  <%= if @post.excerpt do %>
  <div class="row">
    <div class="col-sm-2">
      <p class="small">Excerpt:</p>
    </div>
    <div class="col-sm-10">
      <%=  raw Earmark.to_html(@post.excerpt) %>
    </div>
  </div>
  <hr/>
  <% end %>

  <div class="row">
    <div class="col-sm-2">
      <p class="small">Article Content:</p>
    </div>
    <div class="col-sm-10">
      <%= raw Earmark.to_html(@post.content) %>
    </div>
  </div>
</section>
<hr/>
<section class="control">
  <%= link "Edit", to: post_path(@conn, :edit, @post), class: "btn btn-primary" %>
  <%= link "Back", to: post_path(@conn, :index), class: "btn btn-default" %>
</section>

We'll come back to this a little later to add in some functionality to mark the post as published, but for now these changes will suffice.

If you want to check the web/templates/post folder, there are three templates left to alter: new.html.eex, edit.html.eex and form.html.eex.

The two that we will address now are quite similar in layout, so we'll do these together and then sort the form.html.eex out last.

 

New/Edit layout changes

This section will be quite short

web/templates/post/new.html.eex

<h2>New post</h2>

<%= render "form.html", changeset: @changeset,
                        action: post_path(@conn, :create) %>

<hr/>
<%= link "Back", to: post_path(@conn, :index), class: "btn btn-default" %>

web/templates/post/edit.html.eex

<h2>Edit post</h2>

<%= render "form.html", changeset: @changeset,
                        action: post_path(@conn, :update, @post) %>

<hr/>
<%= link "Back", to: post_path(@conn, :index), class: "btn btn-default" %>

And thats it.

 

THE FORM

The generator created us a form, one that provides a data input for each item that we specified, but there are some things in this form that we want to control differently, so change the form.

web/templates/post/form.html.eex

<%= form_for @changeset, @action, fn f -> %>
  <%= if @changeset.action do %>
    <div class="alert alert-danger">
      <p>Oops, something went wrong! Please check the errors below.</p>
    </div>
  <% end %>

  <div class="form-group">
    <%= label f, :title, class: "control-label" %>
    <%= text_input f, :title, class: "form-control" %>
    <%= error_tag f, :title %>
  </div>

  <div class="form-group">
    <%= label f, :url, class: "control-label" %>
    <%= text_input f, :url, class: "form-control" %>
    <%= error_tag f, :url %>
  </div>

  <div class="form-group">
    <%= label f, :excerpt, class: "control-label" %> <small>(remember to use markdown)</small>
    <%= textarea f, :excerpt, class: "form-control" %>
    <%= error_tag f, :excerpt %>
  </div>

  <div class="form-group">
    <%= label f, :content, class: "control-label" %> <small>(remember to use markdown)</small>
    <%= textarea f, :content, class: "form-control", rows: 35 %>
    <%= error_tag f, :content %>
  </div>

  <div class="form-group">
    <%= submit "Submit", class: "btn btn-primary" %>
  </div>
<% end %>

If we try and use this form now, it will throw an error. This is because the model is checking for data in the date_published field (which we've removed) and deciding that it "can't be blank". Let's go and sort that out.

In web/models/post.ex change the @required_fields and @optional_fields to reflect the following code

  @required_fields ~w(title url content excerpt published)
  @optional_fields ~w(date_published)

This tells the model to enforce content in the title, url, excerpt, content and published fields, but to not care too deeply if the date_published field is missing.

Try the form again, it should now submit and pass you on to the preview page where you can survey your handiwork.

That's all the post creation/editing sorted.

Let's move on to making relevant changes for displaying your posts to readers. For this we will need to create a new controller with an index and a show method - for listing all the published posts and for displaying the detail of a selected post.

 

Serving to readers

We'll create the controller that we need for this by hand, but before we do, we should remove the default generated controller from the project.

$ rm web/controllers/page_controller.ex
$ rm web/views/page_view.ex
$ rm -rf web/templates/page

If you check the default route of your site now on http://localhost:4000, Phoenix will complain about a missing controller.

Update web/routes.ex, change the reference to the PageController to ArticleController - the new controller that we're adding. Also add in a route to the show action.

The "/" scope should now reflect the following

scope "/", Blog do
  pipe_through :browser # Use the default browser stack

  resources "/posts", PostController
  get "/:url", ArticleController, :show
  get "/", ArticleController, :index
end

Yes, Phoenix will still complain about that.

Lets add the controller web/controllers/article_controller.ex

defmodule Blog.ArticleController do
  use Blog.Web, :controller

  alias Blog.Post

  def index(conn, _params) do
    posts = Repo.all(from p in Post, where: p.published == true, order_by: [desc: p.date_published])
    render conn, "index.html", posts: posts
  end

  def show(conn, %{"url" => url}) do
    post = Repo.get_by!(Post, url: url)
    render conn, "show.html", post: post
  end
end

Adding makes Phoenix now complain about a missing view, so lets put that in place

web/views/article_view.ex

defmodule Blog.ArticleView do
  use Blog.Web, :view
  use Timex

  def list_date_format(date, format_string \\ "%B %d, %Y") do
    << Ecto.Date.to_iso8601(date) <> "T00:00:00Z" >>
    |> DateFormat.parse!("{ISOz}")
    |> DateFormat.format!(format_string, :strftime)
  end
end

Ugh, now Phoenix says there's a missing template, which there is. Create the article folder and the missing template files

$ mkdir web/templates/article
$ touch web/templates/article/index.html.eex
$ touch web/templates/article/show.html.eex

So we end up with a page looking like this, which is ok, because there's nothing in the index.html.eex file to display and we haven't addressed the default layout yet.

The default layout is in web/templates/layout/app.html.eex, update this however you see fit, but in order to display template contents you need to have <%= render @view_module, @view_template, assigns %> somewhere in it.

In order to list the posts that we have fetched from the database in the action (if you added one previously as above, then this will result in one post being available to you).

web/templates/article/index.html.eex

<div class="col-sm-8 col-sm-offset-2">
    <%= if Enum.empty? @posts do %>
    <div class="text-center">
        <h4>There's nothing here yet, but there will be soon</h4>
    </div>
    <% end %>

    <%= for post <- @posts do %>
        <%= render "list_post.html", conn: @conn, post: post %>
    <% end %>
</div>

This uses a partial, so you also need to add the following file

web/templates/article/list_post.html.eex

<div class="row article_list_item">
  <div class="col-sm-12">
    <h3><%= link @post.title, to: article_path(@conn, :show, @post.url) %></h3>
    <p class="small">Posted on <%= list_date_format @post.date_published %></p>
    <hr/>
    <div>
      <%=  raw Earmark.to_html(@post.excerpt) %>
    </div>
    <%= link "Read more ...", to: article_path(@conn, :show, @post.url) %>
  </div>
</div>

The leaves us to add mark up to web/templates/article/show.html.eex

<div class="col-sm-8 col-sm-offset-2">
  <h3><%= @post.title %></h3>
  <div class="row small">
  <div class="col-sm-6">
    Posted on <%= list_date_format @post.date_published %>
  </div>
  <div class="col-sm-6 text-right">
    <%= link "<< Full Listing", to: article_path(@conn, :index) %>
  </div>
  </div>
  <hr/>
  <div class="content">
    <%= raw Earmark.to_html(@post.content) %>
  </div>
  <div class="row">
    <div class="col-sm-6 small">
      <%= link "<< Full Listing", to: article_path(@conn, :index) %>
    </div>
  </div>
</div>

And there you have it, a simple blogging engine.

 

Endgame

There are several really important things that haven't been addressed yet, namely

  • Form data validation
  • Data Safety
  • Users and authentication
  • Post publishing
  • Post categorisation
  • Deployment to a production system

Over the next post or three I will tackle these, but for now, ENJOY

DistortedThinking.Agency
Phone: +44 (0)7815 166434Email: clive@distortedthinking.agency
www.distortedthinking.agency