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>
121 lines
3.2 KiB
Elixir
121 lines
3.2 KiB
Elixir
defmodule BerrypodWeb.SeoController do
|
|
use BerrypodWeb, :controller
|
|
|
|
alias Berrypod.{Pages, Products}
|
|
alias BerrypodWeb.R
|
|
|
|
def robots(conn, _params) do
|
|
base = BerrypodWeb.Endpoint.url()
|
|
|
|
content = """
|
|
User-agent: *
|
|
Allow: /
|
|
Disallow: /admin/
|
|
Disallow: /api/
|
|
Disallow: /users/
|
|
Disallow: /webhooks/
|
|
Disallow: /checkout/
|
|
|
|
Sitemap: #{base}/sitemap.xml
|
|
"""
|
|
|
|
conn
|
|
|> put_resp_content_type("text/plain")
|
|
|> send_resp(200, content)
|
|
end
|
|
|
|
def sitemap(conn, _params) do
|
|
base = BerrypodWeb.Endpoint.url()
|
|
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", []}
|
|
]
|
|
|
|
category_pages =
|
|
Enum.map(categories, fn cat ->
|
|
{R.collection(cat.slug), "daily", "0.8", []}
|
|
end)
|
|
|
|
product_pages =
|
|
Enum.map(products, fn product ->
|
|
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)
|
|
|
|
all_pages = static_pages ++ category_pages ++ product_pages ++ custom_pages
|
|
|
|
entries =
|
|
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"
|
|
xmlns:image="http://www.google.com/schemas/sitemap-image/1.1">
|
|
#{entries}
|
|
</urlset>
|
|
"""
|
|
|
|
conn
|
|
|> 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("&", "&")
|
|
|> String.replace("<", "<")
|
|
|> String.replace(">", ">")
|
|
|> String.replace("\"", """)
|
|
|> String.replace("'", "'")
|
|
end
|
|
end
|