defmodule BerrypodWeb.Components.SeoPreview do @moduledoc """ SEO preview component showing how pages appear in search results and social cards. Provides live-updating previews as users edit page titles and descriptions, with character count indicators showing optimal lengths. """ use Phoenix.Component @doc """ Renders the full SEO preview panel with Google and social previews. """ attr :title, :string, required: true attr :description, :string, default: "" attr :url, :string, required: true attr :og_image, :string, default: nil attr :site_name, :string, required: true def seo_preview(assigns) do ~H"""
<.google_preview title={@title} description={@description} url={@url} site_name={@site_name} /> <.social_preview title={@title} description={@description} url={@url} image={@og_image} site_name={@site_name} /> <.character_counts title={@title} description={@description} />
""" end @doc """ Google search result preview mockup. """ attr :title, :string, required: true attr :description, :string, default: "" attr :url, :string, required: true attr :site_name, :string, required: true def google_preview(assigns) do # Truncate title at ~60 chars, description at ~160 chars truncated_title = truncate(assigns.title, 60) truncated_desc = truncate(assigns.description || "", 160) # Build breadcrumb-style URL breadcrumb = build_breadcrumb(assigns.url, assigns.site_name) assigns = assigns |> assign(:truncated_title, truncated_title) |> assign(:truncated_desc, truncated_desc) |> assign(:breadcrumb, breadcrumb) ~H"""
Google search preview
{@breadcrumb}
{@truncated_title}
{@truncated_desc}
""" end @doc """ Social media card preview (Facebook/Twitter style). """ attr :title, :string, required: true attr :description, :string, default: "" attr :url, :string, required: true attr :image, :string, default: nil attr :site_name, :string, required: true def social_preview(assigns) do truncated_title = truncate(assigns.title, 70) truncated_desc = truncate(assigns.description || "", 100) domain = extract_domain(assigns.url) assigns = assigns |> assign(:truncated_title, truncated_title) |> assign(:truncated_desc, truncated_desc) |> assign(:domain, domain) ~H"""
Social card preview
<%= if @image do %> <% else %>
No image set
<% end %>
{@domain}
{@truncated_title}
{@truncated_desc}
""" end @doc """ Character count indicators for title and description. """ attr :title, :string, required: true attr :description, :string, default: "" def character_counts(assigns) do title_len = String.length(assigns.title || "") desc_len = String.length(assigns.description || "") title_status = title_status(title_len) desc_status = desc_status(desc_len) assigns = assigns |> assign(:title_len, title_len) |> assign(:desc_len, desc_len) |> assign(:title_status, title_status) |> assign(:desc_status, desc_status) ~H"""
Title {@title_len}/60
Description {@desc_len}/160
""" end # Title: green ≤60, yellow 61-70, red >70 defp title_status(len) when len <= 60, do: "good" defp title_status(len) when len <= 70, do: "warning" defp title_status(_), do: "error" # Description: green 120-155, yellow 100-119 or 156-160, red <100 or >160 defp desc_status(len) when len >= 120 and len <= 155, do: "good" defp desc_status(len) when len >= 100 and len <= 160, do: "warning" defp desc_status(_), do: "error" defp truncate(nil, _max), do: "" defp truncate("", _max), do: "" defp truncate(text, max) when byte_size(text) > max do String.slice(text, 0, max - 3) <> "..." end defp truncate(text, _max), do: text defp build_breadcrumb(url, site_name) do case URI.parse(url) do %URI{path: path} when is_binary(path) -> parts = path |> String.split("/", trim: true) |> Enum.take(2) if parts == [] do site_name else site_name <> " › " <> Enum.join(parts, " › ") end _ -> site_name end end defp extract_domain(url) do case URI.parse(url) do %URI{host: host} when is_binary(host) -> host _ -> "example.com" end end end