defmodule BerrypodWeb.Admin.GSC do @moduledoc """ Google Search Console dashboard. Shows connection status, top queries, and top pages from GSC data. """ use BerrypodWeb, :live_view alias Berrypod.GSC.{Cache, Client, OAuth} alias Berrypod.Settings @impl true def mount(_params, _session, socket) do # Demo mode: set GSC_DEMO=1 to see the dashboard with sample data demo_mode = System.get_env("GSC_DEMO") == "1" connected = demo_mode or OAuth.connected?() site_url = if demo_mode, do: "https://example.com", else: Settings.get_setting("gsc_site_url") socket = socket |> assign(:page_title, "Search Console") |> assign(:connected, connected) |> assign(:site_url, site_url) |> assign(:sites, []) |> assign(:loading, false) |> assign(:error, nil) |> assign(:data, nil) |> assign(:demo_mode, demo_mode) socket = cond do demo_mode -> assign(socket, :data, demo_data()) connected && site_url -> load_data(socket) true -> socket end {:ok, socket} end defp demo_data do %{ top_queries: [ %{ keys: %{"query" => "wildflower tote bag"}, clicks: 145, impressions: 2340, ctr: 6.2, position: 3.2 }, %{ keys: %{"query" => "custom art prints"}, clicks: 98, impressions: 1890, ctr: 5.2, position: 4.1 }, %{ keys: %{"query" => "botanical poster"}, clicks: 76, impressions: 1456, ctr: 5.2, position: 5.8 }, %{ keys: %{"query" => "nature wall art"}, clicks: 54, impressions: 980, ctr: 5.5, position: 7.2 }, %{ keys: %{"query" => "meadow illustration"}, clicks: 32, impressions: 654, ctr: 4.9, position: 8.4 } ], top_pages: [ %{ keys: %{"page" => "https://example.com/products/wildflower-tote"}, clicks: 234, impressions: 4500, ctr: 5.2, position: 4.1 }, %{ keys: %{"page" => "https://example.com/collections/art-prints"}, clicks: 187, impressions: 3200, ctr: 5.8, position: 3.8 }, %{ keys: %{"page" => "https://example.com/"}, clicks: 156, impressions: 2800, ctr: 5.6, position: 2.1 }, %{ keys: %{"page" => "https://example.com/products/botanical-poster"}, clicks: 98, impressions: 1900, ctr: 5.2, position: 5.4 }, %{ keys: %{"page" => "https://example.com/about"}, clicks: 45, impressions: 890, ctr: 5.1, position: 6.2 } ], updated_at: DateTime.utc_now() } end @impl true def handle_event("select_site", %{"site_url" => site_url}, socket) do Settings.put_setting("gsc_site_url", site_url, "string") Cache.invalidate() socket = socket |> assign(:site_url, site_url) |> load_data() {:noreply, socket} end def handle_event("refresh_data", _params, socket) do Cache.invalidate() {:noreply, load_data(socket)} end def handle_event("load_sites", _params, socket) do case Client.list_sites() do {:ok, sites} -> {:noreply, assign(socket, :sites, sites)} {:error, reason} -> {:noreply, assign(socket, :error, "Failed to load sites: #{inspect(reason)}")} end end defp load_data(socket) do site_url = socket.assigns.site_url case Cache.get_all() do {:ok, data} -> assign(socket, :data, data) :miss -> fetch_fresh_data(socket, site_url) end end defp fetch_fresh_data(socket, site_url) do with {:ok, queries} <- Client.top_queries(site_url, row_limit: 25), {:ok, pages} <- Client.top_pages(site_url, row_limit: 25) do Cache.put_top_queries(queries) Cache.put_top_pages(pages) data = %{ top_queries: queries, top_pages: pages, updated_at: DateTime.utc_now() } assign(socket, :data, data) else {:error, :not_connected} -> assign(socket, :connected, false) {:error, reason} -> assign(socket, :error, "Failed to fetch data: #{inspect(reason)}") end end @impl true def render(assigns) do ~H""" <.header>Search Console

See how your site performs in Google search results

<%= if not @connected do %> <.connection_card configured={gsc_configured?()} /> <% else %> <.site_selector sites={@sites} site_url={@site_url} loading={@loading} /> <%= if @site_url do %> <%= if @data do %> <.data_header updated_at={@data.updated_at} /> <.metrics_grid data={@data} /> <% else %> <.loading_state error={@error} /> <% end %> <% else %> <.no_site_selected /> <% end %> <% end %>
""" end # Connection card for when not connected defp connection_card(assigns) do ~H"""
<.icon name="hero-magnifying-glass" class="size-8" />

Connect Google Search Console

See your search performance data, top queries, and page rankings directly in your admin dashboard.

<%= if @configured do %> Connect with Google <% else %>

Set GSC_CLIENT_ID and GSC_CLIENT_SECRET environment variables to enable this feature.

<% end %>
""" end # Site selector dropdown defp site_selector(assigns) do ~H"""
<%= if @sites == [] do %> <% else %>
<% end %> Disconnect
""" end # Data header with refresh button and last updated defp data_header(assigns) do ~H"""
Last updated: {format_datetime(@updated_at)}
""" end # Metrics grid with queries and pages tables defp metrics_grid(assigns) do ~H"""

Top queries

What people search for to find your site

<.queries_table queries={@data.top_queries} />

Top pages

Your best performing pages in search

<.pages_table pages={@data.top_pages} />
""" end # Top queries table defp queries_table(assigns) do ~H""" <%= if @queries == [] do %> <% else %> <%= for row <- @queries do %> <% end %> <% end %>
Query Clicks Impr. CTR Pos.
No data yet
{row.keys["query"]} {row.clicks} {format_number(row.impressions)} {row.ctr}% {row.position}
""" end # Top pages table defp pages_table(assigns) do ~H""" <%= if @pages == [] do %> <% else %> <%= for row <- @pages do %> <% end %> <% end %>
Page Clicks Impr. CTR Pos.
No data yet
{format_page_url(row.keys["page"])} {row.clicks} {format_number(row.impressions)} {row.ctr}% {row.position}
""" end defp loading_state(assigns) do ~H"""
<%= if @error do %>

{@error}

<% else %>

Loading data...

<% end %>
""" end defp no_site_selected(assigns) do ~H"""

Select a site to view its search performance data.

""" end # Helper functions defp gsc_configured? do System.get_env("GSC_CLIENT_ID") != nil end defp format_datetime(nil), do: "Never" defp format_datetime(dt) do Calendar.strftime(dt, "%-d %b %Y, %H:%M") end defp format_number(n) when n >= 1000 do "#{Float.round(n / 1000, 1)}k" end defp format_number(n), do: to_string(n) defp format_page_url(url) when is_binary(url) do case URI.parse(url) do %{path: path} when is_binary(path) and path != "" -> path _ -> url end end defp format_page_url(url), do: url end