View Source SEO (SEO v0.1.11)
/ˈin(t)ərˌnet jo͞os/
noun: internet juice
SEO (Search Engine Optimization) provides a framework for Phoenix applications to more-easily optimize your site for search engines and displaying rich results when your URLs are shared across the internet. The better visibility your pages have in search results, the more likely you are to have visitors.
Installation
def deps do
[
{:phoenix_seo, "~> 0.1.11"}
]
end
Usage
- Define an SEO module for your web application and defaults
defmodule MyAppWeb.SEO do
use MyAppWeb, :verified_routes
use SEO, [
json_library: Jason,
# a function reference will be called with a conn during render
# arity 1 will be passed the conn, arity 0 is also supported.
site: &__MODULE__.site_config/1,
open_graph: SEO.OpenGraph.build(
description: "A blog about development",
site_name: "My Blog",
locale: "en_US"
),
facebook: SEO.Facebook.build(app_id: "123"),
twitter: SEO.Twitter.build(
site: "@example",
site_id: "27704724",
creator: "@example",
creator_id: "27704724",
card: :summary
)
]
# Or arity 0 is also supported, which can be great if you're using
# Phoenix verified routes and don't need the conn to generate paths.
def site_config(conn) do
SEO.Site.build(
default_title: "Default Title",
description: "A blog about development",
title_suffix: " · My App",
theme_color: "#663399",
windows_tile_color: "#663399",
mask_icon_color: "#663399",
mask_icon_url: static_url(conn, "/images/safari-pinned-tab.svg"),
manifest_url: url(conn, ~p"/site.webmanifest")
)
end
end
- Implement functions to build SEO information about your entities
defmodule MyApp.Article do
# This might be an Ecto schema or a plain struct
defstruct [:id, :title, :description, :author, :reading, :published_at]
end
defimpl SEO.OpenGraph.Build, for: MyApp.Article do
use MyAppWeb, :verified_routes
def build(article, conn) do
SEO.OpenGraph.build(
detail:
SEO.OpenGraph.Article.build(
published_time: article.published_at,
author: article.author,
section: "Tech"
),
image: image(article, conn),
title: article.title,
description: article.description
)
end
defp image(article, conn) do
file = "/images/article/#{article.id}.png"
exists? =
[:code.priv_dir(:my_app), "static", file]
|> Path.join()
|> File.exists?()
if exists? do
SEO.OpenGraph.Image.build(
url: static_url(conn, file),
alt: article.title
)
end
end
end
defimpl SEO.Site.Build, for: MyApp.Article do
use MyAppWeb, :verified_routes
def build(article, conn) do
# Because of `Phoenix.Param`, structs will assume the key of `:id` when
# interpolating the struct into the verified route.
SEO.Site.build(
url: url(conn, ~p"/articles/#{article}"),
title: article.title,
description: article.description
)
end
end
defimpl SEO.Twitter.Build, for: MyApp.Article do
def build(article, _conn) do
SEO.Twitter.build(description: article.description, title: article.title)
end
end
defimpl SEO.Unfurl.Build, for: MyApp.Article do
def build(article, _conn) do
SEO.Unfurl.build(
label1: "Reading Time",
data1: "5 minutes",
label2: "Published",
data2: DateTime.to_iso8601(article.published_at)
)
end
end
defimpl SEO.Breadcrumb.Build, for: MyApp.Article do
use MyAppWeb, :verified_routes
def build(article, conn) do
# Because of `Phoenix.Param`, structs will assume the key of `:id` when
# interpolating the struct into the verified route.
SEO.Breadcrumb.List.build([
%{name: "Articles", item: url(conn, ~p"/articles")},
%{name: article.title, item: url(conn, ~p"/articles/#{article}")}
])
end
end
- Assign the item to your conns and/or sockets
# In a plain Phoenix Controller
def show(conn, params) do
article = load_article(params)
conn
|> SEO.assign(article)
|> render("show.html")
end
def index(conn, params) do
# Note: it's better to implement a struct that represent a route like this,
# so you can customize it per implementation. In this example below, the
# `:title` attribute will be passed to all domains.
conn
|> SEO.assign(%{title: "Listing Best Hugs"})
|> render("show.html")
end
# In a Phoenix LiveView, make sure you handle with
# mount/3 or handle_params/3 so it's present on
# first static render.
def mount(_params, _session, socket) do
# You may mark it as temporary since it's only needed on the first render.
{:ok, socket, temporary_assigns: [{SEO.key(), nil}]}
end
def handle_params(params, _uri, socket) do
{:noreply, SEO.assign(socket, load_article(params))}
end
- Juice up your root layout:
<head>
<%# remove the Phoenix-generated <.live_title> component %>
<%# and replace with SEO.juice component %>
<SEO.juice
conn={@conn}
config={MyAppWeb.SEO.config()}
page_title={assigns[:page_title]}
/>
</head>
Alternatively, you may selectively render components. For example:
<head>
<%# With your SEO module's configuration %>
<SEO.OpenGraph.meta
config={MyAppWeb.SEO.config(:open_graph)}
item={SEO.OpenGraph.Build.build(SEO.item(@conn))}
/>
<%# Or with some other default configuration %>
<SEO.OpenGraph.meta
config={[default_title: "Foo Fighters"]}
item={SEO.OpenGraph.Build.build(SEO.item(@conn))}
/>
<%# Or without defaults %>
<SEO.OpenGraph.meta item={SEO.OpenGraph.Build.build(SEO.item(@conn))} />
</head>
FAQ
Question: What do I do for non-show routes, like for index routes?
You can pass maps or keyword lists for non-specific routes like index routes; however, since it's not an implementation of a struct, it's generic and will be passed to all SEO domains. In the case where an attribute is shared between domains, such as a Twitter title and an Site title and an OpenGraph title, then you won't be able to implement them differently. This is probably ok in most cases.
Even better, you can define a struct on your controller or LiveView and pass that struct as the SEO item, then implement the struct per domain.
For example:
defmodule MyAppWeb.PokemonController do
use MyAppWeb, :controller
defstruct [title: "Listing Pokemon"]
def index(conn, _params) do
# ... your usual index logic
SEO.assign(conn, %__MODULE__{})
end
end
defimpl SEO.OpenGraph.Build, for: MyAppWeb.PokemonController do
def build(index, conn) do
SEO.OpenGraph.build(title: index.title, ...)
end
end
Question: Can I globally configure a JSON library?
Sure. Without configuration, SEO will choose the JSON library configured for Phoenix. If that's not configured and Jason is available, SEO will use Jason. If Jason is not available, but Poison is, then Poison will be used. In any case, you can specify the JSON library for SEO in your mix config:
import Config
config :phoenix_seo, json_library: Jason
This will be picked up when you use SEO
so the config will have json_library
available for the components to use later.
Question: What's the difference between SEO.OpenGraph.Build.build
and SEO.OpenGraph.build
?
Elixir protocols are core to how this library works. Using OpenGraph as
an example, protocols are defined in SEO domains such as SEO.OpenGraph.Build
(big B) which are dispatched by Elixir to your implementation for the given struct. This
is how polymorphism can work for Elixir! Whereas the function SEO.OpenGraph.build
(little b) is building the SEO.OpenGraph
struct based on the defaults for your
domain and the result of your implementation. Again, shorter, Build
(big b) is
the protocol, and build
(little b) is merging your implementation's result with
defaults. Technically, your implementation doesn't have to return an
SEO.OpenGraph
struct, but it's very handy since documentation is present on the
build function so your editor can quickly show you what is available. Knowing is
half the battle!
Summary
Functions
Setup your defaults. Domains are mapped
Assign the SEO item from the Plug.Conn or LiveView Socket
Fetch the SEO item from the Plug.Conn or LiveView Socket
Provide SEO juice. Requires an item and passes the item through all available domains
The key used in the conn or socket to find the item
Types
Functions
Setup your defaults. Domains are mapped:
:site
->SEO.Site
:open_graph
->SEO.OpenGraph
:unfurl
->SEO.Unfurl
:facebook
->SEO.Facebook
:twitter
->SEO.Twitter
:breadcrumb
->SEO.Breadcrumb
For example:
use SEO, [
site: SEO.Site.build(description: "My Blog of many words and infrequent posts", default_title: "Fanastic Site")
facebook: SEO.Facebook.build(app_id: "123")
]
@spec assign(Plug.Conn.t() | Phoenix.LiveView.Socket.t(), any()) :: Plug.Conn.t() | Phoenix.LiveView.Socket.t()
Assign the SEO item from the Plug.Conn or LiveView Socket
@spec item(Plug.Conn.t() | Phoenix.LiveView.Socket.t()) :: any()
Fetch the SEO item from the Plug.Conn or LiveView Socket
Provide SEO juice. Requires an item and passes the item through all available domains
<head>
<%# remove the Phoenix-generated <.live_title> component %>
<%# and replace with SEO.juice component %>
<SEO.juice
conn={@conn}
config={MyAppWeb.SEO.config()}
page_title={assigns[:page_title]}
/>
</head>
Alternatively, you may selectively render components:
<head>
<%# With your SEO module's configuration %>
<SEO.OpenGraph.meta
config={MyAppWeb.SEO.config(:open_graph)}
item={SEO.OpenGraph.Build.build(SEO.item(@conn))}
/>
<%# Or with runtime configuration %>
<SEO.Twitter.meta
config={%{site_name: "Foo Fighters"}}
item={SEO.Twitter.Build.build(SEO.item(@conn))}
/>
<%# Or without configuration is fine too %>
<SEO.Unfurl.meta item={SEO.Unfurl.Build.build(SEO.item(@conn))} />
</head>
Attributes
conn
(Plug.Conn
) (required) -Plug.Conn
for the request. Used for domain configs that are functions and to fetch the item.item
(:any
) - Item to render that implements SEO protocols.SEO.item(@conn)
will be used if not supplied.page_title
(:string
) - Page Title. Overrides item's title if supplied. Defaults tonil
.config
(:any
) - Configuration for your SEO module or another module that implementsSEO.Config
. Defaults tonil
.json_library
(:atom
) - JSON library to use when rendering JSON.config[:json_library]
will be used if not supplied. Defaults tonil
.
The key used in the conn or socket to find the item