Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions lib/jola_dev/og_image.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,28 @@ defmodule JolaDev.OGImage do
{slug, Renderer.generate_bytes(title, description)}
end)

@default_image (
{title, description} = Catalog.content_for("home")
Renderer.generate_bytes(title, description)
)

@doc """
Returns the public asset path for a slug's OG image. Used so the
path scheme stays consistent.
"""
def path_for(slug) when is_binary(slug), do: "/images/og/#{slug}.png"

def image_for(slug) do
with :error <- image_for_slug(slug) do
{:ok, @default_image}
end
end

def default_image do
@default_image
end

defp image_for_slug(slug) do
if @images do
Map.fetch(@images, slug)
else
Expand Down
16 changes: 3 additions & 13 deletions lib/jola_dev_web/components/layouts/root.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,7 @@
<meta property="og:type" content={if(@conn.assigns[:post], do: "article", else: "website")} />
<meta
property="og:image"
content={
Phoenix.VerifiedRoutes.unverified_url(
JolaDevWeb.Endpoint,
@conn.assigns[:og_image] || "/images/og-image.png"
)
}
content={JolaDevWeb.Helpers.OGImage.url_for(@conn)}
/>
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content="1200" />
Expand Down Expand Up @@ -63,12 +58,7 @@
/>
<meta
name="twitter:image"
content={
Phoenix.VerifiedRoutes.unverified_url(
JolaDevWeb.Endpoint,
@conn.assigns[:og_image] || "/images/og-image.png"
)
}
content={JolaDevWeb.Helpers.OGImage.url_for(@conn)}
/>
<meta name="twitter:image:alt" content={@conn.assigns[:page_title] || "jola.dev"} />
<%= unless @conn.status && @conn.status >= 400 do %>
Expand Down Expand Up @@ -98,7 +88,7 @@
</script>
<link rel="icon" href="/favicon.ico" sizes="any" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<%= for schema <- JolaDevWeb.SEO.json_ld(@conn) do %>
<%= for schema <- JolaDevWeb.Helpers.SEO.json_ld(@conn) do %>
<script type="application/ld+json">
<%= raw(Jason.encode!(schema)) %>
</script>
Expand Down
9 changes: 3 additions & 6 deletions lib/jola_dev_web/controllers/blog_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ defmodule JolaDevWeb.BlogController do
posts: posts,
page_title: "Blog | jola.dev",
meta_description:
"Blog posts by Johanna Larsson on software engineering, Elixir, and engineering leadership.",
og_image: JolaDev.OGImage.path_for("posts")
"Blog posts by Johanna Larsson on software engineering, Elixir, and engineering leadership."
)
end

Expand All @@ -27,8 +26,7 @@ defmodule JolaDevWeb.BlogController do
tag: tag,
noindex: true,
page_title: "Posts tagged \"#{tag}\" | jola.dev",
meta_description: "Blog posts by Johanna Larsson tagged with #{tag}.",
og_image: JolaDev.OGImage.path_for("posts/tag/#{tag}")
meta_description: "Blog posts by Johanna Larsson tagged with #{tag}."
)
end
end
Expand All @@ -39,8 +37,7 @@ defmodule JolaDevWeb.BlogController do
post: post,
related_posts: JolaDev.Blog.recent_posts(post),
page_title: "#{post.title} | jola.dev",
meta_description: post.description,
og_image: JolaDev.OGImage.path_for("posts/#{post.id}")
meta_description: post.description
)
else
conn
Expand Down
8 changes: 2 additions & 6 deletions lib/jola_dev_web/controllers/page_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ defmodule JolaDevWeb.PageController do
page_title: "Johanna Larsson — Software Engineer & Speaker",
meta_description:
"Johanna Larsson is a software engineer, engineering leader, writer, and speaker with many years of experience building products and leading teams.",
og_image: JolaDev.OGImage.path_for("home"),
recent_posts: recent_posts
)
end
Expand All @@ -18,7 +17,6 @@ defmodule JolaDevWeb.PageController do
page_title: "About | jola.dev",
meta_description:
"About Johanna Larsson — software engineer, engineering leader, writer, and speaker with many years of experience.",
og_image: JolaDev.OGImage.path_for("about"),
page_type: :about
)
end
Expand All @@ -27,17 +25,15 @@ defmodule JolaDevWeb.PageController do
render(conn, :projects,
page_title: "Projects | jola.dev",
meta_description:
"Open source projects by Johanna Larsson, including HexDiff, ElixirEvents, and more.",
og_image: JolaDev.OGImage.path_for("projects")
"Open source projects by Johanna Larsson, including HexDiff, ElixirEvents, and more."
)
end

def talks(conn, _params) do
render(conn, :talks,
page_title: "Talks | jola.dev",
meta_description:
"Conference talks and presentations by Johanna Larsson on Elixir, distributed systems, and engineering leadership.",
og_image: JolaDev.OGImage.path_for("talks")
"Conference talks and presentations by Johanna Larsson on Elixir, distributed systems, and engineering leadership."
)
end
end
14 changes: 14 additions & 0 deletions lib/jola_dev_web/helpers/og_image.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
defmodule JolaDevWeb.Helpers.OGImage do
@moduledoc """
Builds URLs for OG images from assigns and falls back to request_path.
"""
use JolaDevWeb, :verified_routes

def url_for(%{assigns: %{post: post}}), do: build("posts/#{post.id}")
def url_for(%{assigns: %{tag: tag}}), do: build("posts/tag/#{tag}")
def url_for(%{request_path: "/"}), do: build("home")
def url_for(%{request_path: "/" <> rest}), do: build(rest)

defp build(slug),
do: unverified_url(JolaDevWeb.Endpoint, JolaDev.OGImage.path_for(slug))
end
2 changes: 1 addition & 1 deletion lib/jola_dev_web/helpers/seo.ex
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
defmodule JolaDevWeb.SEO do
defmodule JolaDevWeb.Helpers.SEO do
@moduledoc """
Generates JSON-LD structured data for search engines and AI systems.
"""
Expand Down
22 changes: 8 additions & 14 deletions lib/jola_dev_web/plugs/og_image.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ defmodule JolaDevWeb.Plugs.OGImage do
@moduledoc """
Serves Open Graph preview images from `JolaDev.OGImage`. Intercepts any
`/images/og/<slug>.png` request, looks up the baked PNG bytes, and sends
them with appropriate cache headers. Falls through for unknown slugs so
Phoenix's router can render a 404.
them with appropriate cache headers.
"""

@behaviour Plug
Expand All @@ -20,18 +19,13 @@ defmodule JolaDevWeb.Plugs.OGImage do
def call(%Plug.Conn{request_path: "/images/og/" <> rest} = conn, _) do
slug = String.replace_suffix(rest, ".png", "")

case JolaDev.OGImage.image_for(slug) do
{:ok, bytes} ->
conn
|> put_resp_content_type("image/png")
|> put_resp_header("cache-control", @cache_control)
|> send_resp(200, bytes)
|> halt()

:error ->
# Let the request fall through so Phoenix handles the 404
conn
end
{:ok, bytes} = JolaDev.OGImage.image_for(slug)

conn
|> put_resp_content_type("image/png")
|> put_resp_header("cache-control", @cache_control)
|> send_resp(200, bytes)
|> halt()
end

def call(conn, _), do: conn
Expand Down
2 changes: 1 addition & 1 deletion test/jola_dev/og_image_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ defmodule JolaDev.OGImageTest do
end

test "returns :error for an unknown slug" do
assert OGImage.image_for("no-such-page") == :error
assert OGImage.image_for("no-such-page") == {:ok, OGImage.default_image()}
end
end
end
20 changes: 11 additions & 9 deletions test/jola_dev_web/helpers/seo_test.exs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
defmodule JolaDevWeb.SEOTest do
defmodule JolaDevWeb.Helpers.SEOTest do
use JolaDevWeb.ConnCase, async: true

alias JolaDevWeb.Helpers.SEO

describe "json_ld/1" do
test "includes WebSite schema on all pages", %{conn: conn} do
conn = get(conn, ~p"/")
schemas = JolaDevWeb.SEO.json_ld(conn)
schemas = SEO.json_ld(conn)

website = Enum.find(schemas, &(&1["@type"] == "WebSite"))
assert website["name"] == "jola.dev"
Expand All @@ -14,7 +16,7 @@ defmodule JolaDevWeb.SEOTest do
test "includes BlogPosting schema on blog post pages", %{conn: conn} do
post = List.first(JolaDev.Blog.all_posts())
conn = get(conn, ~p"/posts/#{post.id}")
schemas = JolaDevWeb.SEO.json_ld(conn)
schemas = SEO.json_ld(conn)

blog_posting = Enum.find(schemas, &(&1["@type"] == "BlogPosting"))
assert blog_posting["headline"] == post.title
Expand All @@ -30,7 +32,7 @@ defmodule JolaDevWeb.SEOTest do

test "includes ProfilePage schema on about page", %{conn: conn} do
conn = get(conn, ~p"/about")
schemas = JolaDevWeb.SEO.json_ld(conn)
schemas = SEO.json_ld(conn)

profile = Enum.find(schemas, &(&1["@type"] == "ProfilePage"))
assert profile["mainEntity"]["@type"] == "Person"
Expand All @@ -40,21 +42,21 @@ defmodule JolaDevWeb.SEOTest do

test "does not include BlogPosting on non-post pages", %{conn: conn} do
conn = get(conn, ~p"/projects")
schemas = JolaDevWeb.SEO.json_ld(conn)
schemas = SEO.json_ld(conn)

refute Enum.any?(schemas, &(&1["@type"] == "BlogPosting"))
end

test "omits BreadcrumbList on the home page", %{conn: conn} do
conn = get(conn, ~p"/")
schemas = JolaDevWeb.SEO.json_ld(conn)
schemas = SEO.json_ld(conn)

refute Enum.any?(schemas, &(&1["@type"] == "BreadcrumbList"))
end

test "includes BreadcrumbList on /about", %{conn: conn} do
conn = get(conn, ~p"/about")
schemas = JolaDevWeb.SEO.json_ld(conn)
schemas = SEO.json_ld(conn)

breadcrumb = Enum.find(schemas, &(&1["@type"] == "BreadcrumbList"))

Expand All @@ -77,7 +79,7 @@ defmodule JolaDevWeb.SEOTest do
test "includes BreadcrumbList with post title on post pages", %{conn: conn} do
post = List.first(JolaDev.Blog.all_posts())
conn = get(conn, ~p"/posts/#{post.id}")
schemas = JolaDevWeb.SEO.json_ld(conn)
schemas = SEO.json_ld(conn)

breadcrumb = Enum.find(schemas, &(&1["@type"] == "BreadcrumbList"))
items = breadcrumb["itemListElement"]
Expand All @@ -90,7 +92,7 @@ defmodule JolaDevWeb.SEOTest do
test "includes BreadcrumbList with tag on tag pages", %{conn: conn} do
tag = "elixir"
conn = get(conn, ~p"/posts/tag/#{tag}")
schemas = JolaDevWeb.SEO.json_ld(conn)
schemas = SEO.json_ld(conn)

breadcrumb = Enum.find(schemas, &(&1["@type"] == "BreadcrumbList"))
items = breadcrumb["itemListElement"]
Expand Down
6 changes: 3 additions & 3 deletions test/jola_dev_web/plugs/og_image_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,13 @@ defmodule JolaDevWeb.Plugs.OGImageTest do
assert <<137, "PNG\r\n", 26, "\n", _rest::binary>> = conn.resp_body
end

test "falls through for an unknown OG slug", %{conn: conn} do
test "returns default image for an unknown OG slug", %{conn: conn} do
original = Map.put(conn, :request_path, "/images/og/no-such-page.png")

result = OGImage.call(original, [])

assert result == original
refute result.halted
assert result.status == 200
assert <<137, "PNG\r\n", 26, "\n", _rest::binary>> = result.resp_body
end

test "passes through for paths outside /images/og/", %{conn: conn} do
Expand Down
Loading