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

@@ -26,48 +26,66 @@ defmodule BerrypodWeb.SeoController do
def sitemap(conn, _params) do
base = BerrypodWeb.Endpoint.url()
products = Products.list_visible_products()
products = Products.list_products_for_sitemap()
categories = Products.list_categories()
static_pages = [
{R.home(), "daily", "1.0"},
{R.collection("all"), "daily", "0.9"},
{R.about(), "monthly", "0.5"},
{R.contact(), "monthly", "0.5"},
{R.delivery(), "monthly", "0.5"},
{R.privacy(), "monthly", "0.3"},
{R.terms(), "monthly", "0.3"}
{R.home(), "daily", "1.0", []},
{R.collection("all"), "daily", "0.9", []},
{R.about(), "monthly", "0.5", []},
{R.contact(), "monthly", "0.5", []},
{R.delivery(), "monthly", "0.5", []},
{R.privacy(), "monthly", "0.3", []},
{R.terms(), "monthly", "0.3", []}
]
category_pages =
Enum.map(categories, fn cat ->
{R.collection(cat.slug), "daily", "0.8"}
{R.collection(cat.slug), "daily", "0.8", []}
end)
product_pages =
Enum.map(products, fn product ->
{R.product(product.slug), "weekly", "0.9"}
images = product_image_entries(product, base)
{R.product(product.slug), "weekly", "0.9", images}
end)
custom_pages =
Pages.list_custom_pages()
|> Enum.filter(& &1.published)
|> Enum.map(fn page -> {"/#{page.slug}", "weekly", "0.6"} end)
|> Enum.map(fn page -> {"/#{page.slug}", "weekly", "0.6", []} end)
all_pages = static_pages ++ category_pages ++ product_pages ++ custom_pages
entries =
Enum.map_join(all_pages, "\n", fn {path, changefreq, priority} ->
" <url>\n" <>
" <loc>#{base}#{path}</loc>\n" <>
" <changefreq>#{changefreq}</changefreq>\n" <>
" <priority>#{priority}</priority>\n" <>
" </url>"
Enum.map_join(all_pages, "\n", fn {path, changefreq, priority, images} ->
image_tags =
Enum.map_join(images, "\n", fn img ->
"""
<image:image>
<image:loc>#{xml_escape(img.url)}</image:loc>
<image:title>#{xml_escape(img.title)}</image:title>
</image:image>
"""
|> String.trim_trailing()
end)
url_content =
" <loc>#{xml_escape(base <> path)}</loc>\n" <>
" <changefreq>#{changefreq}</changefreq>\n" <>
" <priority>#{priority}</priority>"
if image_tags == "" do
" <url>\n#{url_content}\n </url>"
else
" <url>\n#{url_content}\n#{image_tags}\n </url>"
end
end)
xml = """
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:image="http://www.google.com/schemas/sitemap-image/1.1">
#{entries}
</urlset>
"""
@@ -76,4 +94,27 @@ defmodule BerrypodWeb.SeoController do
|> put_resp_content_type("application/xml")
|> send_resp(200, xml)
end
defp product_image_entries(product, base_url) do
product.images
|> Enum.take(5)
|> Enum.map(fn product_image ->
image = product_image.image
alt_text = product_image.alt_text || product.title
url = "#{base_url}/image_cache/#{image.id}.webp"
%{url: url, title: alt_text}
end)
end
defp xml_escape(nil), do: ""
defp xml_escape(text) when is_binary(text) do
text
|> String.replace("&", "&amp;")
|> String.replace("<", "&lt;")
|> String.replace(">", "&gt;")
|> String.replace("\"", "&quot;")
|> String.replace("'", "&apos;")
end
end