108 lines
2.8 KiB
Elixir
108 lines
2.8 KiB
Elixir
|
|
defmodule BerrypodWeb.Helpers.SeoHelpers do
|
||
|
|
@moduledoc """
|
||
|
|
SEO-related helpers for generating structured data.
|
||
|
|
"""
|
||
|
|
|
||
|
|
import Phoenix.Component, only: [assign: 3]
|
||
|
|
alias Berrypod.Media
|
||
|
|
|
||
|
|
@doc """
|
||
|
|
Generates FAQPage JSON-LD schema from page blocks.
|
||
|
|
|
||
|
|
Extracts FAQ items from any FAQ blocks on the page and builds a valid
|
||
|
|
FAQPage schema. Returns nil if no FAQ blocks with valid content are found.
|
||
|
|
|
||
|
|
## Examples
|
||
|
|
|
||
|
|
iex> faq_json_ld([%{"type" => "faq", "settings" => %{"items" => [...]}}])
|
||
|
|
~s({"@context":"https://schema.org","@type":"FAQPage",...})
|
||
|
|
|
||
|
|
"""
|
||
|
|
def faq_json_ld(blocks) when is_list(blocks) do
|
||
|
|
questions =
|
||
|
|
blocks
|
||
|
|
|> Enum.filter(&(&1["type"] == "faq"))
|
||
|
|
|> Enum.flat_map(fn block ->
|
||
|
|
(block["settings"] || %{})["items"] || []
|
||
|
|
end)
|
||
|
|
|> Enum.filter(fn item ->
|
||
|
|
question = String.trim(item["question"] || "")
|
||
|
|
answer = String.trim(item["answer"] || "")
|
||
|
|
question != "" and answer != ""
|
||
|
|
end)
|
||
|
|
|> Enum.map(fn item ->
|
||
|
|
%{
|
||
|
|
"@type" => "Question",
|
||
|
|
"name" => item["question"],
|
||
|
|
"acceptedAnswer" => %{
|
||
|
|
"@type" => "Answer",
|
||
|
|
"text" => item["answer"]
|
||
|
|
}
|
||
|
|
}
|
||
|
|
end)
|
||
|
|
|
||
|
|
if questions != [] do
|
||
|
|
%{
|
||
|
|
"@context" => "https://schema.org",
|
||
|
|
"@type" => "FAQPage",
|
||
|
|
"mainEntity" => questions
|
||
|
|
}
|
||
|
|
|> Jason.encode!(escape: :html_safe)
|
||
|
|
else
|
||
|
|
nil
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
def faq_json_ld(_), do: nil
|
||
|
|
|
||
|
|
@doc """
|
||
|
|
Assigns an og_image URL to the socket based on page-specific or default image.
|
||
|
|
|
||
|
|
Priority: page-specific image > site-wide default > none
|
||
|
|
"""
|
||
|
|
def assign_og_image(socket, page, base) do
|
||
|
|
og_image_id = get_og_image_id(page)
|
||
|
|
|
||
|
|
og_image =
|
||
|
|
cond do
|
||
|
|
og_image_id ->
|
||
|
|
Media.get_image(og_image_id)
|
||
|
|
|
||
|
|
true ->
|
||
|
|
Media.get_default_og_image()
|
||
|
|
end
|
||
|
|
|
||
|
|
if og_image do
|
||
|
|
url = og_image_url(og_image, base)
|
||
|
|
assign(socket, :og_image, url)
|
||
|
|
else
|
||
|
|
socket
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
defp get_og_image_id(nil), do: nil
|
||
|
|
defp get_og_image_id(%{og_image_id: id}), do: id
|
||
|
|
defp get_og_image_id(page) when is_map(page), do: page[:og_image_id]
|
||
|
|
defp get_og_image_id(_), do: nil
|
||
|
|
|
||
|
|
@doc """
|
||
|
|
Generates a full URL for an OG image, preferring 1200px width for social sharing.
|
||
|
|
|
||
|
|
Falls back to the largest available width if the image is smaller than 1200px.
|
||
|
|
"""
|
||
|
|
def og_image_url(image, base) do
|
||
|
|
path =
|
||
|
|
if image.is_svg do
|
||
|
|
"/image_cache/#{image.id}.webp"
|
||
|
|
else
|
||
|
|
widths = Berrypod.Images.Optimizer.applicable_widths(image.source_width)
|
||
|
|
# Prefer 1200px or larger, otherwise use the largest available
|
||
|
|
width = Enum.find(widths, List.last(widths), &(&1 >= 1200))
|
||
|
|
|
||
|
|
"/image_cache/#{image.id}-#{width}.webp"
|
||
|
|
end
|
||
|
|
|
||
|
|
base <> path
|
||
|
|
end
|
||
|
|
end
|