Feature: OG Social media cards
Not Original Gangsta, but ObjectGraph Reason: images were showing on LinkedIn, but small.
This commit is contained in:
parent
764585c642
commit
061787e897
@ -12,6 +12,27 @@ defmodule FirehoseWeb.Layouts do
|
|||||||
|
|
||||||
embed_templates "layouts/*"
|
embed_templates "layouts/*"
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Stores content in the connection for later rendering in layouts.
|
||||||
|
|
||||||
|
Used to inject dynamic meta tags into the `<head>` section.
|
||||||
|
"""
|
||||||
|
def put_content_for(conn, key, content) do
|
||||||
|
content_for = Map.get(conn.assigns, :content_for, %{})
|
||||||
|
new_content_for = Map.update(content_for, key, [content], &(&1 ++ [content]))
|
||||||
|
Plug.Conn.assign(conn, :content_for, new_content_for)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Retrieves content stored via `put_content_for/3`.
|
||||||
|
|
||||||
|
Can be called from HEEx templates with `get_content_for(assigns, :meta_tags)`.
|
||||||
|
"""
|
||||||
|
def get_content_for(assigns, key) do
|
||||||
|
content_for = assigns[:content_for]
|
||||||
|
if is_map(content_for), do: Map.get(content_for, key, []), else: []
|
||||||
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Shows the flash group with standard titles and content.
|
Shows the flash group with standard titles and content.
|
||||||
|
|
||||||
|
|||||||
@ -31,6 +31,16 @@
|
|||||||
window.addEventListener("phx:set-theme", (e) => setTheme(e.target.dataset.phxTheme));
|
window.addEventListener("phx:set-theme", (e) => setTheme(e.target.dataset.phxTheme));
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
<%= for meta <- get_content_for(assigns, :meta_tags) do %>
|
||||||
|
<meta property="og:title" content={meta.og_title} />
|
||||||
|
<meta property="og:description" content={meta.og_description} />
|
||||||
|
<meta property="og:type" content={meta.og_type} />
|
||||||
|
<meta property="og:url" content={meta.og_url} />
|
||||||
|
<%= if meta.og_image do %>
|
||||||
|
<meta property="og:image" content={meta.og_image} />
|
||||||
|
<% end %>
|
||||||
|
<meta name="twitter:card" content={meta.twitter_card} />
|
||||||
|
<% end %>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
{@inner_content}
|
{@inner_content}
|
||||||
|
|||||||
@ -8,7 +8,11 @@ defmodule FirehoseWeb.BlogController do
|
|||||||
page = parse_page(params["page"])
|
page = parse_page(params["page"])
|
||||||
result = blog.paginate(page)
|
result = blog.paginate(page)
|
||||||
|
|
||||||
render(conn, :index,
|
meta = Blogex.SEO.meta_tags_for_blog(blog, FirehoseWeb.Endpoint.url())
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> FirehoseWeb.Layouts.put_content_for(:meta_tags, meta)
|
||||||
|
|> render(:index,
|
||||||
page_title: blog.title(),
|
page_title: blog.title(),
|
||||||
blog_title: blog.title(),
|
blog_title: blog.title(),
|
||||||
blog_description: blog.description(),
|
blog_description: blog.description(),
|
||||||
@ -24,9 +28,14 @@ defmodule FirehoseWeb.BlogController do
|
|||||||
post = blog.get_post!(slug)
|
post = blog.get_post!(slug)
|
||||||
visibility = Blogex.Post.visibility(post)
|
visibility = Blogex.Post.visibility(post)
|
||||||
|
|
||||||
render(conn, :show,
|
meta = Blogex.SEO.meta_tags(post, FirehoseWeb.Endpoint.url(), blog)
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> FirehoseWeb.Layouts.put_content_for(:meta_tags, meta)
|
||||||
|
|> render(:show,
|
||||||
page_title: post.title,
|
page_title: post.title,
|
||||||
post: post,
|
post: post,
|
||||||
|
meta: meta,
|
||||||
base_path: blog.base_path(),
|
base_path: blog.base_path(),
|
||||||
visibility: visibility,
|
visibility: visibility,
|
||||||
authenticated: !!(conn.assigns[:current_scope] && conn.assigns.current_scope.user)
|
authenticated: !!(conn.assigns[:current_scope] && conn.assigns.current_scope.user)
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
author: "Willem van den Ende",
|
author: "Willem van den Ende",
|
||||||
tags: ~w(meta ai),
|
tags: ~w(meta ai),
|
||||||
description: "Why I built a separate personal blog, and what it has to do with drinking from the firehose.",
|
description: "Why I built a separate personal blog, and what it has to do with drinking from the firehose.",
|
||||||
|
image: "/images/firehose-logo.png",
|
||||||
published: true
|
published: true
|
||||||
}
|
}
|
||||||
---
|
---
|
||||||
|
|||||||
@ -66,4 +66,64 @@ defmodule FirehoseWeb.BlogControllerTest do
|
|||||||
refute response =~ "post-status-banner"
|
refute response =~ "post-status-banner"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "GET /blog/:blog_id/:slug - Open Graph meta tags" do
|
||||||
|
test "meta tags include og_title", %{conn: conn} do
|
||||||
|
response = conn |> get(~p"/blog/engineering/hello-world") |> html_response(200)
|
||||||
|
|
||||||
|
assert response =~ ~s(<meta property="og:title")
|
||||||
|
assert response =~ "Hello World"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "meta tags include og_description", %{conn: conn} do
|
||||||
|
response = conn |> get(~p"/blog/engineering/hello-world") |> html_response(200)
|
||||||
|
|
||||||
|
assert response =~ ~s(<meta property="og:description")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "meta tags include og_type article", %{conn: conn} do
|
||||||
|
response = conn |> get(~p"/blog/engineering/hello-world") |> html_response(200)
|
||||||
|
|
||||||
|
assert response =~ ~s(<meta property="og:type" content="article")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "meta tags include twitter:card", %{conn: conn} do
|
||||||
|
response = conn |> get(~p"/blog/engineering/hello-world") |> html_response(200)
|
||||||
|
|
||||||
|
assert response =~ ~s(<meta name="twitter:card" content="summary_large_image")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "meta tags include og_image when post has image", %{conn: conn} do
|
||||||
|
response = conn |> get(~p"/blog/engineering/why-firehose") |> html_response(200)
|
||||||
|
|
||||||
|
assert response =~ ~s(<meta property="og:image")
|
||||||
|
assert response =~ "firehose-logo.png"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "meta tags have correct og_url", %{conn: conn} do
|
||||||
|
response = conn |> get(~p"/blog/engineering/hello-world") |> html_response(200)
|
||||||
|
|
||||||
|
assert response =~ ~s(<meta property="og:url" content="http://localhost:4002/blog/engineering/hello-world")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "GET /blog/:blog_id - Open Graph meta tags" do
|
||||||
|
test "meta tags include og_title for blog listing", %{conn: conn} do
|
||||||
|
response = conn |> get(~p"/blog/engineering") |> html_response(200)
|
||||||
|
|
||||||
|
assert response =~ ~s(<meta property="og:title")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "meta tags have og_type website for blog listing", %{conn: conn} do
|
||||||
|
response = conn |> get(~p"/blog/engineering") |> html_response(200)
|
||||||
|
|
||||||
|
assert response =~ ~s(<meta property="og:type" content="website")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "meta tags use summary twitter card for blog listing", %{conn: conn} do
|
||||||
|
response = conn |> get(~p"/blog/engineering") |> html_response(200)
|
||||||
|
|
||||||
|
assert response =~ ~s(<meta name="twitter:card" content="summary")
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -1,89 +1,44 @@
|
|||||||
defmodule FirehoseWeb.BlogTagsTest do
|
defmodule FirehoseWeb.BlogTagsTest do
|
||||||
use FirehoseWeb.ConnCase
|
use FirehoseWeb.ConnCase
|
||||||
|
|
||||||
defp goto_engineering_tag_page(conn, tag) do
|
|
||||||
path = "/blog/engineering/tag/#{tag}"
|
|
||||||
conn_res = get(conn, path)
|
|
||||||
body = html_response(conn_res, 200)
|
|
||||||
assert body =~ ~s(tagged "#{tag}")
|
|
||||||
assert body =~ "Engineering Blog"
|
|
||||||
body
|
|
||||||
end
|
|
||||||
|
|
||||||
defp goto_releases_tag_page(conn, tag) do
|
|
||||||
path = "/blog/releases/tag/#{tag}"
|
|
||||||
conn_res = get(conn, path)
|
|
||||||
body = html_response(conn_res, 200)
|
|
||||||
assert body =~ ~s(tagged "#{tag}")
|
|
||||||
assert body =~ "Release Notes"
|
|
||||||
body
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "engineering blog tags" do
|
describe "engineering blog tags" do
|
||||||
test "GET /blog/engineering/tag/:tag shows tag page with all posts", %{conn: conn} do
|
test "GET /blog/engineering/tag/:tag shows tag page with filtered posts", %{conn: conn} do
|
||||||
body = goto_engineering_tag_page(conn, "elixir")
|
body = conn |> get("/blog/engineering/tag/elixir") |> html_response(200)
|
||||||
|
assert body =~ ~s(tagged "elixir")
|
||||||
|
assert body =~ "Engineering Blog"
|
||||||
assert body =~ "Hello World"
|
assert body =~ "Hello World"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "GET /blog/engineering/tag/:tag page shows filtered posts", %{conn: conn} do
|
test "GET /blog/engineering/tag/:tag shows empty list for nonexistent tag", %{conn: conn} do
|
||||||
body = goto_engineering_tag_page(conn, "phoenix")
|
body = conn |> get("/blog/engineering/tag/nonexistent-tag") |> html_response(200)
|
||||||
assert body =~ "Hello World"
|
assert body =~ ~s(tagged "nonexistent-tag")
|
||||||
end
|
|
||||||
|
|
||||||
test "GET /blog/engineering/tag/:tag page shows empty list for nonexistent tag", %{
|
|
||||||
conn: conn
|
|
||||||
} do
|
|
||||||
conn_res = get(conn, "/blog/engineering/tag/nonexistent-tag")
|
|
||||||
assert html_response(conn_res, 200) =~ ~s(tagged "nonexistent-tag")
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "release notes blog tags" do
|
describe "release notes blog tags" do
|
||||||
test "GET /blog/releases/tag/:tag shows tag page with all posts", %{conn: conn} do
|
test "GET /blog/releases/tag/:tag shows tag page with filtered posts", %{conn: conn} do
|
||||||
body = goto_releases_tag_page(conn, "release")
|
body = conn |> get("/blog/releases/tag/release") |> html_response(200)
|
||||||
|
assert body =~ ~s(tagged "release")
|
||||||
|
assert body =~ "Release Notes"
|
||||||
assert body =~ "v0.1.0 Released"
|
assert body =~ "v0.1.0 Released"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "GET /blog/releases/tag/:tag page shows filtered posts", %{conn: conn} do
|
test "GET /blog/releases/tag/:tag shows empty list for nonexistent tag", %{conn: conn} do
|
||||||
conn_res = get(conn, "/blog/releases/tag/nonexistent-tag")
|
body = conn |> get("/blog/releases/tag/nonexistent-tag") |> html_response(200)
|
||||||
assert html_response(conn_res, 200) =~ ~s(tagged "nonexistent-tag")
|
assert body =~ ~s(tagged "nonexistent-tag")
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "tag URL pattern" do
|
|
||||||
test "tag URLs follow pattern /blog/:blog_id/tag/:tag for engineering blog", %{conn: conn} do
|
|
||||||
# Test that the tag route exists and works correctly
|
|
||||||
conn_res1 = get(conn, "/blog/engineering/tag/elixir")
|
|
||||||
assert html_response(conn_res1, 200) =~ ~s(tagged "elixir")
|
|
||||||
|
|
||||||
conn_res2 = get(conn, "/blog/engineering/tag/phoenix")
|
|
||||||
assert html_response(conn_res2, 200) =~ ~s(tagged "phoenix")
|
|
||||||
end
|
|
||||||
|
|
||||||
test "tag URLs follow pattern /blog/:blog_id/tag/:tag for releases blog", %{conn: conn} do
|
|
||||||
# Test that the tag route exists and works correctly
|
|
||||||
conn_res = get(conn, "/blog/releases/tag/release")
|
|
||||||
assert html_response(conn_res, 200) =~ ~s(tagged "release")
|
|
||||||
end
|
|
||||||
|
|
||||||
test "nonexistent tags return 200 with empty post list", %{conn: conn} do
|
|
||||||
conn_res = get(conn, "/blog/engineering/tag/nonexistent-tag")
|
|
||||||
assert html_response(conn_res, 200)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "tag page structure" do
|
describe "tag page structure" do
|
||||||
test "tag page has proper layout and back link", %{conn: conn} do
|
test "engineering tag page has proper layout and back link", %{conn: conn} do
|
||||||
body = goto_engineering_tag_page(conn, "elixir")
|
body = conn |> get("/blog/engineering/tag/elixir") |> html_response(200)
|
||||||
|
|
||||||
assert body =~ "Engineering Blog"
|
assert body =~ "Engineering Blog"
|
||||||
assert body =~ ~s(tagged "elixir")
|
assert body =~ ~s(tagged "elixir")
|
||||||
assert body =~ "All posts"
|
assert body =~ "All posts"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "release tag page has proper layout and back link", %{conn: conn} do
|
test "release tag page has proper layout and back link", %{conn: conn} do
|
||||||
body = goto_releases_tag_page(conn, "release")
|
body = conn |> get("/blog/releases/tag/release") |> html_response(200)
|
||||||
|
|
||||||
assert body =~ "Release Notes"
|
assert body =~ "Release Notes"
|
||||||
assert body =~ ~s(tagged "release")
|
assert body =~ ~s(tagged "release")
|
||||||
assert body =~ "All posts"
|
assert body =~ "All posts"
|
||||||
|
|||||||
@ -3,34 +3,35 @@
|
|||||||
defmodule FirehoseWeb.BlogTest do
|
defmodule FirehoseWeb.BlogTest do
|
||||||
use FirehoseWeb.ConnCase
|
use FirehoseWeb.ConnCase
|
||||||
|
|
||||||
defp visit_engineering_page(conn, suffix \\ "") do
|
defp visit_blog_page(conn, blog_id, suffix \\ "") do
|
||||||
path = "/blog/engineering" <> suffix
|
path = "/blog/#{blog_id}" <> suffix
|
||||||
body = conn |> get(path) |> html_response(200)
|
body = conn |> get(path) |> html_response(200)
|
||||||
assert body =~ "Engineering Blog"
|
|
||||||
assert body =~ "firehose"
|
|
||||||
body
|
|
||||||
end
|
|
||||||
|
|
||||||
defp visit_engineering_path(conn, suffix) do
|
|
||||||
path = "/blog/engineering" <> suffix
|
|
||||||
body = conn |> get(path) |> html_response(200)
|
|
||||||
assert body =~ "firehose"
|
|
||||||
body
|
body
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "engineering blog (HTML)" do
|
describe "engineering blog (HTML)" do
|
||||||
test "GET /blog/engineering returns HTML index with layout", %{conn: conn} do
|
test "GET /blog/engineering returns HTML index with layout", %{conn: conn} do
|
||||||
visit_engineering_page(conn)
|
body = visit_blog_page(conn, "engineering")
|
||||||
|
assert body =~ "Engineering Blog"
|
||||||
|
assert body =~ "firehose"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "GET /blog/engineering/:slug returns HTML post with layout", %{conn: conn} do
|
test "GET /blog/engineering/:slug returns HTML post with layout", %{conn: conn} do
|
||||||
body = visit_engineering_path(conn, "/hello-world")
|
body = visit_blog_page(conn, "engineering", "/hello-world")
|
||||||
assert body =~ "Hello World"
|
assert body =~ "Hello World"
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
test "GET /blog/engineering/tag/:tag returns HTML tag page", %{conn: conn} do
|
describe "release notes blog (HTML)" do
|
||||||
body = visit_engineering_path(conn, "/tag/elixir")
|
test "GET /blog/releases returns HTML index", %{conn: conn} do
|
||||||
assert body =~ ~s(tagged "elixir")
|
body = visit_blog_page(conn, "releases")
|
||||||
|
assert body =~ "Release Notes"
|
||||||
|
assert body =~ "v0.1.0 Released"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "GET /blog/releases/:slug returns HTML post", %{conn: conn} do
|
||||||
|
body = visit_blog_page(conn, "releases", "/v0-1-0")
|
||||||
|
assert body =~ "v0.1.0 Released"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -58,24 +59,6 @@ defmodule FirehoseWeb.BlogTest do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "release notes blog (HTML)" do
|
|
||||||
test "GET /blog/releases returns HTML index", %{conn: conn} do
|
|
||||||
body = conn |> get("/blog/releases") |> html_response(200)
|
|
||||||
assert body =~ "Release Notes"
|
|
||||||
assert body =~ "v0.1.0 Released"
|
|
||||||
end
|
|
||||||
|
|
||||||
test "GET /blog/releases/:slug returns HTML post", %{conn: conn} do
|
|
||||||
body = conn |> get("/blog/releases/v0-1-0") |> html_response(200)
|
|
||||||
assert body =~ "v0.1.0 Released"
|
|
||||||
end
|
|
||||||
|
|
||||||
test "GET /blog/releases/tag/:tag returns HTML tag page", %{conn: conn} do
|
|
||||||
body = conn |> get("/blog/releases/tag/elixir") |> html_response(200)
|
|
||||||
assert body =~ ~s(tagged "elixir")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "engineering blog (JSON API)" do
|
describe "engineering blog (JSON API)" do
|
||||||
test "GET /api/blog/engineering returns post index", %{conn: conn} do
|
test "GET /api/blog/engineering returns post index", %{conn: conn} do
|
||||||
assert %{"blog" => "engineering", "posts" => posts} =
|
assert %{"blog" => "engineering", "posts" => posts} =
|
||||||
|
|||||||
@ -16,6 +16,13 @@ defmodule FirehoseWeb.UserSessionControllerTest do
|
|||||||
assert response =~ "Log in with email"
|
assert response =~ "Log in with email"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "renders login page with password mode", %{conn: conn} do
|
||||||
|
response = conn |> get(~p"/users/log-in?mode=password") |> html_response(200)
|
||||||
|
assert response =~ "Log in"
|
||||||
|
assert response =~ ~p"/users/register"
|
||||||
|
assert response =~ "Log in with email"
|
||||||
|
end
|
||||||
|
|
||||||
test "renders login page with email filled in (sudo mode)", %{conn: conn, user: user} do
|
test "renders login page with email filled in (sudo mode)", %{conn: conn, user: user} do
|
||||||
html =
|
html =
|
||||||
conn
|
conn
|
||||||
@ -30,13 +37,6 @@ defmodule FirehoseWeb.UserSessionControllerTest do
|
|||||||
assert html =~
|
assert html =~
|
||||||
~s(<input type="email" name="user[email]" id="login_form_magic_email" value="#{user.email}")
|
~s(<input type="email" name="user[email]" id="login_form_magic_email" value="#{user.email}")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "renders login page (email + password)", %{conn: conn} do
|
|
||||||
response = conn |> get(~p"/users/log-in?mode=password") |> html_response(200)
|
|
||||||
assert response =~ "Log in"
|
|
||||||
assert response =~ ~p"/users/register"
|
|
||||||
assert response =~ "Log in with email"
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "GET /users/log-in/:token" do
|
describe "GET /users/log-in/:token" do
|
||||||
|
|||||||
58
app/test/firehose_web/layouts_test.exs
Normal file
58
app/test/firehose_web/layouts_test.exs
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
defmodule FirehoseWeb.LayoutsTest do
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
|
||||||
|
alias FirehoseWeb.Layouts
|
||||||
|
|
||||||
|
@conn %Plug.Conn{
|
||||||
|
assigns: %{}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe "put_content_for/3" do
|
||||||
|
test "stores content under the given key" do
|
||||||
|
conn = put_content_for(@conn, :meta_tags, %{og_title: "Test"})
|
||||||
|
|
||||||
|
assert conn.assigns[:content_for][:meta_tags] == [%{og_title: "Test"}]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "appends multiple calls to the same key" do
|
||||||
|
conn =
|
||||||
|
@conn
|
||||||
|
|> put_content_for(:meta_tags, %{og_title: "First"})
|
||||||
|
|> put_content_for(:meta_tags, %{og_title: "Second"})
|
||||||
|
|
||||||
|
assert length(conn.assigns[:content_for][:meta_tags]) == 2
|
||||||
|
assert Enum.at(conn.assigns[:content_for][:meta_tags], 0).og_title == "First"
|
||||||
|
assert Enum.at(conn.assigns[:content_for][:meta_tags], 1).og_title == "Second"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "stores different keys independently" do
|
||||||
|
conn =
|
||||||
|
@conn
|
||||||
|
|> put_content_for(:meta_tags, %{og_title: "Meta"})
|
||||||
|
|> put_content_for(:scripts, "%(script)")
|
||||||
|
|
||||||
|
assert Map.has_key?(conn.assigns[:content_for], :meta_tags)
|
||||||
|
assert Map.has_key?(conn.assigns[:content_for], :scripts)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "get_content_for/2" do
|
||||||
|
test "returns stored content for a key" do
|
||||||
|
content_for = %{meta_tags: [%{og_title: "Test"}]}
|
||||||
|
|
||||||
|
assert Layouts.get_content_for(%{content_for: content_for}, :meta_tags) ==
|
||||||
|
[%{og_title: "Test"}]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns empty list when key does not exist" do
|
||||||
|
assert Layouts.get_content_for(%{content_for: %{}}, :nonexistent) == []
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns empty list when content_for is nil" do
|
||||||
|
assert Layouts.get_content_for(%{content_for: nil}, :nonexistent) == []
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Re-export for use in tests
|
||||||
|
defp put_content_for(conn, key, content), do: Layouts.put_content_for(conn, key, content)
|
||||||
|
end
|
||||||
@ -28,6 +28,7 @@ defmodule Blogex.Post do
|
|||||||
:description,
|
:description,
|
||||||
:date,
|
:date,
|
||||||
:blog,
|
:blog,
|
||||||
|
:image,
|
||||||
tags: [],
|
tags: [],
|
||||||
published: true
|
published: true
|
||||||
]
|
]
|
||||||
@ -39,6 +40,7 @@ defmodule Blogex.Post do
|
|||||||
body: String.t(),
|
body: String.t(),
|
||||||
description: String.t(),
|
description: String.t(),
|
||||||
date: Date.t(),
|
date: Date.t(),
|
||||||
|
image: String.t() | nil,
|
||||||
tags: [String.t()],
|
tags: [String.t()],
|
||||||
blog: atom(),
|
blog: atom(),
|
||||||
published: boolean()
|
published: boolean()
|
||||||
|
|||||||
@ -3,11 +3,33 @@ defmodule Blogex.SEO do
|
|||||||
SEO helpers for generating meta tags and sitemaps.
|
SEO helpers for generating meta tags and sitemaps.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns a map of meta tag attributes for a blog listing page.
|
||||||
|
Useful for setting OpenGraph and Twitter card tags on blog index pages.
|
||||||
|
|
||||||
|
<meta property="og:title" content={@meta.og_title} />
|
||||||
|
"""
|
||||||
|
def meta_tags_for_blog(blog_module, base_url) do
|
||||||
|
url = "#{base_url}#{blog_module.base_path()}"
|
||||||
|
|
||||||
|
%{
|
||||||
|
title: blog_module.title(),
|
||||||
|
description: blog_module.description(),
|
||||||
|
og_title: blog_module.title(),
|
||||||
|
og_description: blog_module.description(),
|
||||||
|
og_type: "website",
|
||||||
|
og_url: url,
|
||||||
|
og_image: nil,
|
||||||
|
twitter_card: "summary"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns a map of meta tag attributes for a post.
|
Returns a map of meta tag attributes for a post.
|
||||||
Useful for setting OpenGraph and Twitter card tags in your layout.
|
Useful for setting OpenGraph and Twitter card tags in your layout.
|
||||||
|
|
||||||
<meta property="og:title" content={@meta.og_title} />
|
<meta property="og:title" content={@meta.og_title} />
|
||||||
|
<meta property="og:image" content={@meta.og_image} />
|
||||||
"""
|
"""
|
||||||
def meta_tags(post, base_url, blog_module) do
|
def meta_tags(post, base_url, blog_module) do
|
||||||
url = "#{base_url}#{blog_module.base_path()}/#{post.id}"
|
url = "#{base_url}#{blog_module.base_path()}/#{post.id}"
|
||||||
@ -19,6 +41,7 @@ defmodule Blogex.SEO do
|
|||||||
og_description: post.description,
|
og_description: post.description,
|
||||||
og_type: "article",
|
og_type: "article",
|
||||||
og_url: url,
|
og_url: url,
|
||||||
|
og_image: if(post.image, do: "#{base_url}#{post.image}", else: nil),
|
||||||
article_published_time: Date.to_iso8601(post.date),
|
article_published_time: Date.to_iso8601(post.date),
|
||||||
article_author: post.author,
|
article_author: post.author,
|
||||||
article_tags: post.tags,
|
article_tags: post.tags,
|
||||||
|
|||||||
@ -58,6 +58,20 @@ defmodule Blogex.PostTest do
|
|||||||
|
|
||||||
assert post.published == false
|
assert post.published == false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "preserves image from frontmatter" do
|
||||||
|
attrs = Map.put(valid_attrs(), :image, "/images/cover.png")
|
||||||
|
|
||||||
|
post = Post.build("x/2026/01-01-x.md", attrs, "<p>x</p>")
|
||||||
|
|
||||||
|
assert post.image == "/images/cover.png"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "defaults image to nil when not in frontmatter" do
|
||||||
|
post = Post.build("x/2026/01-01-x.md", valid_attrs(), "<p>x</p>")
|
||||||
|
|
||||||
|
assert post.image == nil
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp valid_attrs do
|
defp valid_attrs do
|
||||||
|
|||||||
@ -6,6 +6,8 @@ defmodule Blogex.SEOTest do
|
|||||||
|
|
||||||
defmodule StubBlog do
|
defmodule StubBlog do
|
||||||
def base_path, do: "/blog/eng"
|
def base_path, do: "/blog/eng"
|
||||||
|
def title, do: "Engineering Blog"
|
||||||
|
def description, do: "Our engineering insights"
|
||||||
end
|
end
|
||||||
|
|
||||||
@base_url "https://example.com"
|
@base_url "https://example.com"
|
||||||
@ -38,6 +40,63 @@ defmodule Blogex.SEOTest do
|
|||||||
assert meta.article_published_time == "2026-06-15"
|
assert meta.article_published_time == "2026-06-15"
|
||||||
assert meta.article_tags == ["a", "b"]
|
assert meta.article_tags == ["a", "b"]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "includes og_image when post has an image" do
|
||||||
|
post = build(image: "/images/cover.png")
|
||||||
|
|
||||||
|
meta = SEO.meta_tags(post, @base_url, StubBlog)
|
||||||
|
|
||||||
|
assert meta.og_image == "https://example.com/images/cover.png"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "og_image is nil when post has no image" do
|
||||||
|
post = build()
|
||||||
|
|
||||||
|
meta = SEO.meta_tags(post, @base_url, StubBlog)
|
||||||
|
|
||||||
|
assert meta.og_image == nil
|
||||||
|
end
|
||||||
|
|
||||||
|
test "og_image uses absolute URL with base_url prefix" do
|
||||||
|
post = build(image: "/assets/post-hero.jpg")
|
||||||
|
|
||||||
|
meta = SEO.meta_tags(post, "https://myapp.dev", StubBlog)
|
||||||
|
|
||||||
|
assert meta.og_image == "https://myapp.dev/assets/post-hero.jpg"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "meta_tags_for_blog/2" do
|
||||||
|
test "returns website type" do
|
||||||
|
meta = SEO.meta_tags_for_blog(StubBlog, @base_url)
|
||||||
|
|
||||||
|
assert meta.og_type == "website"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "includes blog title and description" do
|
||||||
|
meta = SEO.meta_tags_for_blog(StubBlog, @base_url)
|
||||||
|
|
||||||
|
assert meta.og_title == "Engineering Blog"
|
||||||
|
assert meta.og_description == "Our engineering insights"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "has no og_image for blog listing" do
|
||||||
|
meta = SEO.meta_tags_for_blog(StubBlog, @base_url)
|
||||||
|
|
||||||
|
assert meta.og_image == nil
|
||||||
|
end
|
||||||
|
|
||||||
|
test "builds correct blog URL" do
|
||||||
|
meta = SEO.meta_tags_for_blog(StubBlog, @base_url)
|
||||||
|
|
||||||
|
assert meta.og_url == "https://example.com/blog/eng"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "uses summary twitter card for blog listing" do
|
||||||
|
meta = SEO.meta_tags_for_blog(StubBlog, @base_url)
|
||||||
|
|
||||||
|
assert meta.twitter_card == "summary"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "sitemap/2" do
|
describe "sitemap/2" do
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user