Building a Blog with Phoenix
Posted on February 14, 2016 by Clive in Elixir, Phoenix
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.
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. 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… 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. Using a command line, navigate to a convenient directory (mine is Once this is complete, follow the given instructions If you receive this error you can either or Phoenix requires Node.js >= 5.0 and NPM >= 3 Once you’ve done this, restart the server with If you point a browser at 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. then save the file. Create the database If this results in an error, most likely your database credentials have been entered into the dev.exs file incorrectly. 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](db_posts.png)
Using the generators for this Update the web/routes.ex file like it says then run the migration to create the database table Fire up the server with 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. 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 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 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 and replace the web/static/css/app.css file with this one This renders an unobtrusive layout that we can work with. 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 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). This shows how a post will be listed in the 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 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 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) Reloading the page now should give you the following 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 Remember to run Now the template just needs updating with This formats the dates in a more human readable form. The next thing we’ll look at is the Show action. 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 You’ll need to Then in web/templates/views/post/show.html.eex make the following changes 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. This section will be quite short web/templates/post/new.html.eex web/templates/post/edit.html.eex And thats it. 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 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 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. We’ll create the controller that we need for this If you check the default route of your site now on 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 Yes, Phoenix will still complain about that. Lets add the controller
web/controllers/article_controller.ex Adding makes Phoenix now complain about a missing view, so lets put that in place web/views/article_view.ex Ugh, now Phoenix says there’s a missing template, which there is. Create the article folder and the missing template files 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 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 This uses a partial, so you also need to add the following file web/templates/article/list_post.html.eex The leaves us to add mark up to web/templates/article/show.html.eex And there you have it, a simple blogging engine. There are several really important things that haven’t been addressed yet, namely Over the next post or three I will tackle these, but for now, ENJOYWhat we’re building today
Dependencies
Basic info for following along
Start the build
~/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
$
$ 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"
$ npm install --save babel-preset-es2015
$ rm -rf blog/
$ npm cache clean
$ npm install npm -g
$ mix phoenix.new blog
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
http://localhost:4000
you should now see the default Phoenix site.Setting up the database
$ 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
$ 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.
$
Generating the Post model
$ 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
scope "/", Blog do
pipe_through :browser # Use the default browser stack
resources "/posts", PostController <---- Add this line
get "/", PageController, :index
end
$ 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
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.Layout
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
...
$ touch web/templates/layout/admin_layout.html.eex
<!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>
Updating the post index template
<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" %>
$ 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)>
defmodule Blog.PostView do
use Blog.Web, :view
def publish_status(true), do: "Published"
def publish_status(_), do: "Draft"
end
<td><%= post.title %></td>
<td><%= publish_status post.published %></td>
<td><%= post.updated_at %></td>
<td><%= post.date_published %></td>
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)>
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
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
<td><%= date_format post.updated_at %></td>
<td><%= date_format post.date_published %></td>
Updating the post show template
...
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
...
mix deps.get
and restart Phoenix again.<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>
New/Edit layout changes
<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" %>
<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" %>
THE FORM
<%= 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 %>
@required_fields
and @optional_fields
to reflect the following code @required_fields ~w(title url content excerpt published)
@optional_fields ~w(date_published)
Serving to readers
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
http://localhost:4000
, Phoenix will complain about a missing controller.scope "/", Blog do
pipe_through :browser # Use the default browser stack
resources "/posts", PostController
get "/:url", ArticleController, :show
get "/", ArticleController, :index
end
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
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
$ mkdir web/templates/article
$ touch web/templates/article/index.html.eex
$ touch web/templates/article/show.html.eex
<%= render @view_module, @view_template, assigns %>
somewhere in it.<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>
<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>
<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>
Endgame