diff --git a/lib/jola_dev/og_image.ex b/lib/jola_dev/og_image.ex
index d719767..4c58307 100644
--- a/lib/jola_dev/og_image.ex
+++ b/lib/jola_dev/og_image.ex
@@ -17,6 +17,11 @@ 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.
@@ -24,6 +29,16 @@ defmodule JolaDev.OGImage do
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
diff --git a/lib/jola_dev_web/components/layouts/root.html.heex b/lib/jola_dev_web/components/layouts/root.html.heex
index b66b566..bcb1d78 100644
--- a/lib/jola_dev_web/components/layouts/root.html.heex
+++ b/lib/jola_dev_web/components/layouts/root.html.heex
@@ -27,12 +27,7 @@
@@ -63,12 +58,7 @@
/>
<%= unless @conn.status && @conn.status >= 400 do %>
@@ -98,7 +88,7 @@
- <%= for schema <- JolaDevWeb.SEO.json_ld(@conn) do %>
+ <%= for schema <- JolaDevWeb.Helpers.SEO.json_ld(@conn) do %>
diff --git a/lib/jola_dev_web/controllers/blog_controller.ex b/lib/jola_dev_web/controllers/blog_controller.ex
index e350410..8c45707 100644
--- a/lib/jola_dev_web/controllers/blog_controller.ex
+++ b/lib/jola_dev_web/controllers/blog_controller.ex
@@ -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
@@ -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
@@ -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
diff --git a/lib/jola_dev_web/controllers/page_controller.ex b/lib/jola_dev_web/controllers/page_controller.ex
index bb9c122..2c23d04 100644
--- a/lib/jola_dev_web/controllers/page_controller.ex
+++ b/lib/jola_dev_web/controllers/page_controller.ex
@@ -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
@@ -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
@@ -27,8 +25,7 @@ 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
@@ -36,8 +33,7 @@ defmodule JolaDevWeb.PageController 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
diff --git a/lib/jola_dev_web/helpers/og_image.ex b/lib/jola_dev_web/helpers/og_image.ex
new file mode 100644
index 0000000..27d8df4
--- /dev/null
+++ b/lib/jola_dev_web/helpers/og_image.ex
@@ -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
diff --git a/lib/jola_dev_web/helpers/seo.ex b/lib/jola_dev_web/helpers/seo.ex
index 533451c..0e21c81 100644
--- a/lib/jola_dev_web/helpers/seo.ex
+++ b/lib/jola_dev_web/helpers/seo.ex
@@ -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.
"""
diff --git a/lib/jola_dev_web/plugs/og_image.ex b/lib/jola_dev_web/plugs/og_image.ex
index d8b4f80..0e69038 100644
--- a/lib/jola_dev_web/plugs/og_image.ex
+++ b/lib/jola_dev_web/plugs/og_image.ex
@@ -2,8 +2,7 @@ defmodule JolaDevWeb.Plugs.OGImage do
@moduledoc """
Serves Open Graph preview images from `JolaDev.OGImage`. Intercepts any
`/images/og/.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
@@ -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
diff --git a/test/jola_dev/og_image_test.exs b/test/jola_dev/og_image_test.exs
index c2c985a..6bcead4 100644
--- a/test/jola_dev/og_image_test.exs
+++ b/test/jola_dev/og_image_test.exs
@@ -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
diff --git a/test/jola_dev_web/helpers/seo_test.exs b/test/jola_dev_web/helpers/seo_test.exs
index 0424796..62203e3 100644
--- a/test/jola_dev_web/helpers/seo_test.exs
+++ b/test/jola_dev_web/helpers/seo_test.exs
@@ -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"
@@ -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
@@ -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"
@@ -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"))
@@ -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"]
@@ -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"]
diff --git a/test/jola_dev_web/plugs/og_image_test.exs b/test/jola_dev_web/plugs/og_image_test.exs
index b5978f6..a16808f 100644
--- a/test/jola_dev_web/plugs/og_image_test.exs
+++ b/test/jola_dev_web/plugs/og_image_test.exs
@@ -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