add SEO enhancements: OG images, meta robots, FAQ block, image sitemap
All checks were successful
deploy / deploy (push) Successful in 4m59s
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: []
|
||||
|
||||
Reference in New Issue
Block a user