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