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
end

OG 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
end

Social 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
end

Wire 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 URLsog:image must be an absolute HTTPS URL. Use url(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.