Elixir & Phoenix Open Graph Meta Tags: Complete Setup Guide
How to add og:title, og:image, og:description, and Twitter card meta tags to Phoenix Framework apps — covering layouts, LiveView, and dynamic OG image generation.
OG tags in Phoenix HTML layout
Phoenix renders pages through a root layout template. Add OG meta tags there using assigns passed from each controller or LiveView:
<%# lib/my_app_web/components/layouts/root.html.heex %>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title><%= assigns[:page_title] || "My App" %></title>
<meta name="description" content="<%= assigns[:page_description] || "Default description" %>" />
<!-- Open Graph -->
<meta property="og:title" content="<%= assigns[:og_title] || assigns[:page_title] || "My App" %>" />
<meta property="og:description" content="<%= assigns[:og_description] || assigns[:page_description] || "Default description" %>" />
<meta property="og:image" content="<%= assigns[:og_image] || "https://myapp.com/images/default-og.png" %>" />
<meta property="og:url" content="<%= assigns[:og_url] || url(~p"/") %>" />
<meta property="og:type" content="<%= assigns[:og_type] || "website" %>" />
<!-- Twitter / X -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="<%= assigns[:og_title] || assigns[:page_title] || "My App" %>" />
<meta name="twitter:description" content="<%= assigns[:og_description] || assigns[:page_description] || "Default description" %>" />
<meta name="twitter:image" content="<%= assigns[:og_image] || "https://myapp.com/images/default-og.png" %>" />
<link rel="canonical" href="<%= assigns[:og_url] || url(~p"/") %>" />
<%= @inner_content %>
</head>
</html>Set OG assigns in controllers
In each controller action, assign the page-specific OG values before rendering:
# lib/my_app_web/controllers/post_controller.ex
defmodule MyAppWeb.PostController do
use MyAppWeb, :controller
def show(conn, %{"slug" => slug}) do
post = Blog.get_post_by_slug!(slug)
conn
|> assign(:page_title, post.title)
|> assign(:og_title, post.title)
|> assign(:og_description, String.slice(post.excerpt, 0, 160))
|> assign(:og_image, "https://myapp.com/og/#{slug}.png")
|> assign(:og_url, url(conn, ~p"/blog/#{slug}"))
|> assign(:og_type, "article")
|> render(:show, post: post)
end
endOG tags with Phoenix LiveView
LiveView pages can set meta tags using assign in mount/3. The root layout receives these on initial HTTP render (SSR), which is what social crawlers read:
# lib/my_app_web/live/post_live/show.ex
defmodule MyAppWeb.PostLive.Show do
use MyAppWeb, :live_view
def mount(%{"slug" => slug}, _session, socket) do
post = Blog.get_post_by_slug!(slug)
socket =
socket
|> assign(:post, post)
|> assign(:page_title, post.title)
|> assign(:og_title, post.title)
|> assign(:og_description, String.slice(post.excerpt, 0, 160))
|> assign(:og_image, "https://myapp.com/og/#{slug}.png")
|> assign(:og_url, "https://myapp.com/blog/#{slug}")
|> assign(:og_type, "article")
{:ok, socket}
end
def render(assigns) do
~H"""
<article>
<h1><%= @post.title %></h1>
<%= @post.body %>
</article>
"""
end
endSocial scrapers hit the initial HTML response (before WebSocket upgrade), so assigns set in mount/3 are visible to crawlers.
Dynamic OG image endpoint in Phoenix
Add a dedicated route that generates PNG OG images on demand:
# lib/my_app_web/controllers/og_image_controller.ex
defmodule MyAppWeb.OgImageController do
use MyAppWeb, :controller
def show(conn, %{"slug" => slug}) do
post = Blog.get_post_by_slug!(slug)
# Use an SVG template + convert to PNG (via :mogrify or a JS edge worker)
svg = og_svg(post.title, post.excerpt)
conn
|> put_resp_content_type("image/svg+xml")
|> put_resp_header("cache-control", "public, max-age=86400, s-maxage=86400")
|> send_resp(200, svg)
end
defp og_svg(title, description) do
"""
<svg width="1200" height="630" xmlns="http://www.w3.org/2000/svg">
<rect width="1200" height="630" fill="#0f0f0f"/>
<text x="60" y="200" font-size="56" font-family="sans-serif" fill="white">#{title}</text>
<text x="60" y="300" font-size="28" font-family="sans-serif" fill="#aaa">#{description}</text>
</svg>
"""
end
endWire it in your router:
# lib/my_app_web/router.ex scope "/og", MyAppWeb do get "/:slug", OgImageController, :show end
Common Elixir / Phoenix OG mistakes
- Missing SSR for LiveView — always set OG assigns in
mount/3, not just in event handlers. Scrapers read the initial HTML only. - Relative image URLs —
og:imagemust be an absolute HTTPS URL. Useurl(conn, ...)or hard-code the domain. - Wrong image size — use 1200×630px. Smaller images render as inline thumbnails on Twitter/LinkedIn.
Verify your Phoenix OG tags
After deploying, paste your URL into OGFixer to see exactly how your Phoenix app's link previews appear on Twitter, LinkedIn, Slack, and Discord — and catch missing or broken tags before sharing.