View Source SEO (SEO v0.1.5)
WORK IN PROGRESS, DO NOT USE
/ˈ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
Installation
def deps do
[
{:phoenix_seo, "~> 0.1.5"}
]
end
usage
Usage
- Define an SEO module for your web application and defaults
defmodule MyAppWeb.SEO do
alias MyAppWeb.Router.Helpers, as: Routes
use SEO, [
json_library: Jason,
# a function reference will be called with a conn during render
# note: you must pass conn to the SEO.juice component for this to work
site: &__MODULE__.site_config/1,
open_graph: SEO.OpenGraph.build(
description: "A blog about development",
site_name: "My Blog",
type: :website,
locale: "en_US"
),
twitter: SEO.Twitter.build(
site: "@example",
site_id: "27704724",
creator: "@example",
creator_id: "27704724",
card: :summary
)
]
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: Routes.static_path(conn, "/images/safari-pinned-tab.svg"),
manifest_url: Routes.robot_path(conn, :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
alias MyAppWeb.Router.Helpers, as: Routes
def build(article, conn) do
SEO.OpenGraph.build(
type: :article,
type_detail:
SEO.OpenGraph.Article.build(
published_time: article.published_at |> DateTime.to_date() |> Date.to_iso8601(),
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? =
[Application.app_dir(:my_app), "/priv/static", file]
|> Path.join()
|> File.exists?()
if exists? do
SEO.OpenGraph.Image.build(
url: Routes.static_url(conn, file),
secure_url: Routes.static_url(conn, file),
alt: article.title
)
end
end
end
defimpl SEO.Site.Build, for: MyApp.Article do
alias MyAppWeb.Router.Helpers, as: Routes
def build(article, conn) do
SEO.Site.build(
url: Routes.article_url(conn, :show, article.id),
title: article.title,
description: article.description
)
end
end
defimpl SEO.Facebook.Build, for: MyApp.Article do
def build(_article, _conn) do
SEO.Facebook.build(app_id: "123")
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
alias MyAppWeb.Router.Helpers, as: Routes
def build(article, conn) do
SEO.Breadcrumb.List.build([
%{name: "Articles", item: Routes.article_url(conn, :index),
%{name: article.title, item: Routes.article_url(conn, :show, article.id)
])
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
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()}
item={SEO.item(@conn)}
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>
Link to this section Summary
Functions
Setup your defaults. Domains are mapped this way
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
Link to this section Types
Attributes describing an item
Fallback attributes describing an item and configuration
Link to this section Functions
Setup your defaults. Domains are mapped this way:
: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()}
item={SEO.item(assigns)}
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(assigns))}
/>
<%# Or with runtime configuration %>
<SEO.Twitter.meta
config={%{site_name: "Foo Fighters"}}
item={SEO.Twitter.Build.build(SEO.item(assigns))}
/>
<%# Or without configuration is fine too %>
<SEO.Unfurl.meta item={SEO.Unfurl.Build.build(SEO.item(assigns))} />
</head>
attributes
Attributes
item
(:any
) (required) - Item to render that implements SEO protocols.conn
(Plug.Conn
) (required) - Plug.Conn for the request. Used for domain configs that are functions.page_title
(:string
) - Page Title. Overrides item's title if supplied. Defaults tonil
.config
(:any
) - Configuration for your SEO module or another module that implements config/0and config/1.
Defaults to
nil
.json_library
(:atom
) - JSON library to use when rendering JSON.config[:json_library]
willbe used if not supplied.
Defaults to
nil
.