add SEO enhancements: OG images, meta robots, FAQ block, image sitemap
All checks were successful
deploy / deploy (push) Successful in 4m59s

- Per-page SEO controls: meta robots directives, focus keyword, OG image
- Site-wide default OG image in admin settings
- FAQ block type with FAQPage JSON-LD schema
- Enhanced Organization JSON-LD with business info, contact, address
- Image sitemap with product images
- SEO preview panel with Google/social card mockups
- SEO checklist with real-time scoring
- Business info section in site editor
- GSC integration scaffolding (OAuth, client, cache)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
jamey
2026-04-17 16:47:43 +01:00
parent 9facfd926e
commit 4aa7dece0c
42 changed files with 3881 additions and 41 deletions

View File

@@ -14,10 +14,21 @@ defmodule BerrypodWeb.Shop.Pages.Cart do
socket
|> assign(:page_title, "Cart")
|> assign(:page, page)
|> maybe_assign_meta_robots(page)
{:noreply, socket}
end
defp maybe_assign_meta_robots(socket, page) do
meta_robots = page && page[:meta_robots]
if meta_robots && meta_robots != "index, follow" do
assign(socket, :meta_robots, meta_robots)
else
socket
end
end
def handle_params(_params, _uri, socket) do
{:noreply, socket}
end

View File

@@ -50,6 +50,7 @@ defmodule BerrypodWeb.Shop.Pages.CheckoutSuccess do
|> assign(:page_title, "Order confirmed")
|> assign(:order, order)
|> assign(:page, page)
|> maybe_assign_meta_robots(page)
{:noreply, socket}
end
@@ -58,6 +59,16 @@ defmodule BerrypodWeb.Shop.Pages.CheckoutSuccess do
{:redirect, redirect(socket, to: R.home())}
end
defp maybe_assign_meta_robots(socket, page) do
meta_robots = page && page[:meta_robots]
if meta_robots && meta_robots != "index, follow" do
assign(socket, :meta_robots, meta_robots)
else
socket
end
end
def handle_params(_params, _uri, socket) do
{:noreply, socket}
end

View File

@@ -7,6 +7,7 @@ defmodule BerrypodWeb.Shop.Pages.Collection do
import Phoenix.LiveView, only: [push_patch: 2, push_navigate: 2, put_flash: 3]
alias Berrypod.{Pages, Pagination, Products}
alias BerrypodWeb.Helpers.SeoHelpers
alias BerrypodWeb.R
@sort_options [
@@ -20,16 +21,29 @@ defmodule BerrypodWeb.Shop.Pages.Collection do
def init(socket, _params, _uri) do
page = Pages.get_page("collection")
base = BerrypodWeb.Endpoint.url()
socket =
socket
|> assign(:page, page)
|> assign(:sort_options, @sort_options)
|> assign(:current_sort, "featured")
|> maybe_assign_meta_robots(page)
|> SeoHelpers.assign_og_image(page, base)
{:noreply, socket}
end
defp maybe_assign_meta_robots(socket, page) do
meta_robots = page && page[:meta_robots]
if meta_robots && meta_robots != "index, follow" do
assign(socket, :meta_robots, meta_robots)
else
socket
end
end
# When accessed via custom URL (e.g. /shop) without a collection slug, show all products
def handle_params(params, uri, socket) when not is_map_key(params, "slug") do
handle_params(Map.put(params, "slug", "all"), uri, socket)

View File

@@ -10,10 +10,12 @@ defmodule BerrypodWeb.Shop.Pages.Contact do
alias BerrypodWeb.R
alias Berrypod.Orders.OrderNotifier
alias Berrypod.Pages
alias BerrypodWeb.Helpers.SeoHelpers
alias BerrypodWeb.OrderLookupController
def init(socket, _params, _uri) do
page = Pages.get_page("contact")
base = BerrypodWeb.Endpoint.url()
socket =
socket
@@ -25,10 +27,22 @@ defmodule BerrypodWeb.Shop.Pages.Contact do
|> assign(:og_url, R.url(R.contact()))
|> assign(:tracking_state, :idle)
|> assign(:page, page)
|> maybe_assign_meta_robots(page)
|> SeoHelpers.assign_og_image(page, base)
{:noreply, socket}
end
defp maybe_assign_meta_robots(socket, page) do
meta_robots = page && page[:meta_robots]
if meta_robots && meta_robots != "index, follow" do
assign(socket, :meta_robots, meta_robots)
else
socket
end
end
def handle_params(_params, _uri, socket) do
{:noreply, socket}
end

View File

@@ -9,6 +9,7 @@ defmodule BerrypodWeb.Shop.Pages.Content do
alias Berrypod.LegalPages
alias Berrypod.Pages
alias Berrypod.Theme.PreviewData
alias BerrypodWeb.Helpers.SeoHelpers
alias BerrypodWeb.R
def init(socket, _params, _uri) do
@@ -27,10 +28,22 @@ defmodule BerrypodWeb.Shop.Pages.Content do
|> assign(seo)
|> assign(:page, page)
|> assign(:content_blocks, content_blocks)
|> assign(:json_ld, SeoHelpers.faq_json_ld(page && page.blocks))
|> maybe_assign_meta_robots(page)
{:noreply, socket}
end
defp maybe_assign_meta_robots(socket, page) do
meta_robots = page && page[:meta_robots]
if meta_robots && meta_robots != "index, follow" do
assign(socket, :meta_robots, meta_robots)
else
socket
end
end
def handle_event(_event, _params, _socket), do: :cont
# Returns {seo_assigns, content_blocks} for each content page

View File

@@ -6,6 +6,7 @@ defmodule BerrypodWeb.Shop.Pages.CustomPage do
import Phoenix.Component, only: [assign: 2, assign: 3]
alias Berrypod.Pages
alias BerrypodWeb.Helpers.SeoHelpers
def init(socket, _params, _uri) do
# Custom pages load in handle_params based on slug
@@ -55,10 +56,13 @@ defmodule BerrypodWeb.Shop.Pages.CustomPage do
type: page.type,
published: page.published,
meta_description: page.meta_description,
meta_robots: page.meta_robots,
focus_keyword: page.focus_keyword,
url_slug: page.url_slug,
show_in_nav: page.show_in_nav,
nav_label: page.nav_label,
nav_position: page.nav_position
nav_position: page.nav_position,
og_image_id: page.og_image_id
}
end
@@ -91,6 +95,7 @@ defmodule BerrypodWeb.Shop.Pages.CustomPage do
defp maybe_assign_meta(socket, page, base) do
socket
|> assign(:og_url, base <> "/#{page.slug}")
|> assign(:json_ld, SeoHelpers.faq_json_ld(page.blocks))
|> then(fn s ->
if page.meta_description do
assign(s, :page_description, page.meta_description)
@@ -98,5 +103,15 @@ defmodule BerrypodWeb.Shop.Pages.CustomPage do
s
end
end)
|> then(fn s ->
meta_robots = page[:meta_robots]
if meta_robots && meta_robots != "index, follow" do
assign(s, :meta_robots, meta_robots)
else
s
end
end)
|> SeoHelpers.assign_og_image(page, base)
end
end

View File

@@ -5,7 +5,8 @@ defmodule BerrypodWeb.Shop.Pages.Home do
import Phoenix.Component, only: [assign: 2, assign: 3]
alias Berrypod.Pages
alias Berrypod.{Pages, Settings}
alias BerrypodWeb.Helpers.SeoHelpers
def init(socket, _params, _uri) do
page = Pages.get_page("home")
@@ -14,28 +15,145 @@ defmodule BerrypodWeb.Shop.Pages.Home do
base = BerrypodWeb.Endpoint.url()
site_name = socket.assigns.site_name
org_ld =
Jason.encode!(
%{
"@context" => "https://schema.org",
"@type" => "Organization",
"name" => site_name,
"url" => base <> "/"
},
escape: :html_safe
)
org_ld = build_organization_json_ld(socket.assigns, base, site_name)
json_ld = combine_json_ld([org_ld, SeoHelpers.faq_json_ld(page.blocks)])
socket =
socket
|> assign(:page_title, "Home")
|> assign(:og_url, base <> "/")
|> assign(:json_ld, org_ld)
|> assign(:json_ld, json_ld)
|> assign(:page, page)
|> maybe_assign_meta_robots(page)
|> SeoHelpers.assign_og_image(page, base)
|> assign(extra)
{:noreply, socket}
end
# Combine multiple JSON-LD scripts into a single output (newline-separated)
defp combine_json_ld(ld_list) do
ld_list
|> Enum.reject(&is_nil/1)
|> case do
[] -> nil
[single] -> single
many -> Enum.join(many, "\n</script>\n<script type=\"application/ld+json\">\n")
end
end
defp build_organization_json_ld(assigns, base_url, site_name) do
business_info = Settings.get_business_info()
org_type =
if business_info["business_type"] == "LocalBusiness",
do: "LocalBusiness",
else: "Organization"
org = %{
"@context" => "https://schema.org",
"@type" => org_type,
"name" => site_name,
"url" => base_url <> "/"
}
org
|> maybe_add_logo(assigns[:logo_image], base_url)
|> maybe_add_contact_point(business_info)
|> maybe_add_address(business_info)
|> maybe_add_same_as(assigns[:social_links])
|> Jason.encode!(escape: :html_safe)
end
defp maybe_add_logo(org, nil, _base_url), do: org
defp maybe_add_logo(org, logo_image, base_url) do
logo_url = base_url <> "/image_cache/#{logo_image.id}.webp"
Map.put(org, "logo", logo_url)
end
defp maybe_add_contact_point(org, business_info) do
phone = business_info["business_phone"]
email = business_info["business_email"]
cond do
present?(phone) and present?(email) ->
Map.put(org, "contactPoint", [
%{"@type" => "ContactPoint", "telephone" => phone, "contactType" => "customer service"},
%{"@type" => "ContactPoint", "email" => email, "contactType" => "customer service"}
])
present?(phone) ->
Map.put(org, "contactPoint", %{
"@type" => "ContactPoint",
"telephone" => phone,
"contactType" => "customer service"
})
present?(email) ->
Map.put(org, "contactPoint", %{
"@type" => "ContactPoint",
"email" => email,
"contactType" => "customer service"
})
true ->
org
end
end
defp maybe_add_address(org, business_info) do
street = business_info["address_street"]
city = business_info["address_city"]
country = business_info["address_country"]
if present?(street) or present?(city) or present?(country) do
address = %{"@type" => "PostalAddress"}
address =
address
|> maybe_put("streetAddress", street)
|> maybe_put("addressLocality", city)
|> maybe_put("addressRegion", business_info["address_region"])
|> maybe_put("postalCode", business_info["address_postal_code"])
|> maybe_put("addressCountry", country)
Map.put(org, "address", address)
else
org
end
end
defp maybe_add_same_as(org, nil), do: org
defp maybe_add_same_as(org, []), do: org
defp maybe_add_same_as(org, social_links) do
urls =
social_links
|> Enum.map(& &1.url)
|> Enum.filter(&present?/1)
if urls != [], do: Map.put(org, "sameAs", urls), else: org
end
defp maybe_put(map, _key, nil), do: map
defp maybe_put(map, _key, ""), do: map
defp maybe_put(map, key, value), do: Map.put(map, key, value)
defp present?(nil), do: false
defp present?(""), do: false
defp present?(_), do: true
defp maybe_assign_meta_robots(socket, page) do
meta_robots = page && page[:meta_robots]
if meta_robots && meta_robots != "index, follow" do
assign(socket, :meta_robots, meta_robots)
else
socket
end
end
def handle_params(_params, _uri, socket) do
{:noreply, socket}
end

View File

@@ -18,10 +18,21 @@ defmodule BerrypodWeb.Shop.Pages.OrderDetail do
socket
|> assign(:lookup_email, session["email_session"])
|> assign(:page, page)
|> maybe_assign_meta_robots(page)
{:noreply, socket}
end
defp maybe_assign_meta_robots(socket, page) do
meta_robots = page && page[:meta_robots]
if meta_robots && meta_robots != "index, follow" do
assign(socket, :meta_robots, meta_robots)
else
socket
end
end
def handle_params(%{"order_number" => order_number}, _uri, socket) do
email = socket.assigns.lookup_email

View File

@@ -19,6 +19,7 @@ defmodule BerrypodWeb.Shop.Pages.Orders do
|> assign(:page_title, "Your orders")
|> assign(:lookup_email, email)
|> assign(:page, page)
|> maybe_assign_meta_robots(page)
socket =
if email do
@@ -30,6 +31,16 @@ defmodule BerrypodWeb.Shop.Pages.Orders do
{:noreply, socket}
end
defp maybe_assign_meta_robots(socket, page) do
meta_robots = page && page[:meta_robots]
if meta_robots && meta_robots != "index, follow" do
assign(socket, :meta_robots, meta_robots)
else
socket
end
end
def handle_params(_params, _uri, socket) do
{:noreply, socket}
end

View File

@@ -7,6 +7,7 @@ defmodule BerrypodWeb.Shop.Pages.Product do
import Phoenix.LiveView, only: [connected?: 1, push_navigate: 2]
alias Berrypod.{Analytics, Cart, Pages, Reviews}
alias BerrypodWeb.Helpers.SeoHelpers
alias BerrypodWeb.R
alias Berrypod.Images.Optimizer
alias Berrypod.Products
@@ -57,6 +58,12 @@ defmodule BerrypodWeb.Shop.Pages.Product do
page = Pages.get_page("pdp")
is_discontinued = product.status == "discontinued"
product_ld =
product_json_ld(product, og_url, og_image, avg_rating, review_count, seo_reviews)
faq_ld = SeoHelpers.faq_json_ld(page.blocks)
combined_ld = combine_json_ld([product_ld, faq_ld])
socket =
socket
|> assign(:page_title, product.title)
@@ -64,10 +71,7 @@ defmodule BerrypodWeb.Shop.Pages.Product do
|> assign(:og_type, "product")
|> assign(:og_url, og_url)
|> assign(:og_image, og_image)
|> assign(
:json_ld,
product_json_ld(product, og_url, og_image, avg_rating, review_count, seo_reviews)
)
|> assign(:json_ld, combined_ld)
|> assign(:product, product)
|> assign(:all_images, all_images)
|> assign(:quantity, 1)
@@ -78,6 +82,7 @@ defmodule BerrypodWeb.Shop.Pages.Product do
|> assign(:review_form, nil)
|> assign(:review_status, nil)
|> assign(:existing_review, nil)
|> maybe_assign_meta_robots(page)
# Check if user has an existing review for this product
socket = load_existing_review(socket)
@@ -488,6 +493,27 @@ defmodule BerrypodWeb.Shop.Pages.Product do
end
end
defp maybe_assign_meta_robots(socket, page) do
meta_robots = page && page[:meta_robots]
if meta_robots && meta_robots != "index, follow" do
assign(socket, :meta_robots, meta_robots)
else
socket
end
end
# Combine multiple JSON-LD scripts into a single output (newline-separated)
defp combine_json_ld(ld_list) do
ld_list
|> Enum.reject(&is_nil/1)
|> case do
[] -> nil
[single] -> single
many -> Enum.join(many, "\n</script>\n<script type=\"application/ld+json\">\n")
end
end
defp format_review_for_display(review) do
%{
id: review.id,

View File

@@ -16,10 +16,21 @@ defmodule BerrypodWeb.Shop.Pages.Search do
socket
|> assign(:page_title, "Search")
|> assign(:page, page)
|> maybe_assign_meta_robots(page)
{:noreply, socket}
end
defp maybe_assign_meta_robots(socket, page) do
meta_robots = page && page[:meta_robots]
if meta_robots && meta_robots != "index, follow" do
assign(socket, :meta_robots, meta_robots)
else
socket
end
end
def handle_params(params, _uri, socket) do
query = params["q"] || ""
results = if query != "", do: Search.search(query), else: []