From 4e36b654d396b8fceb8f3bee5e48676f181210bd Mon Sep 17 00:00:00 2001 From: jamey Date: Mon, 23 Feb 2026 22:37:34 +0000 Subject: [PATCH] add JSON-LD structured data Product pages: Product schema (name, description, image, price/currency, availability) + BreadcrumbList (Home > Category > Product). Home page: Organization schema (name, url). Uses Jason with html_safe escaping so the JSON is safe to embed in + <% end %> <%= for preload <- Berrypod.Theme.Fonts.preload_links( @theme_settings.typography, diff --git a/lib/berrypod_web/live/shop/home.ex b/lib/berrypod_web/live/shop/home.ex index 44ccb8f..81a4e6d 100644 --- a/lib/berrypod_web/live/shop/home.ex +++ b/lib/berrypod_web/live/shop/home.ex @@ -7,10 +7,25 @@ defmodule BerrypodWeb.Shop.Home do def mount(_params, _session, socket) do products = Products.list_visible_products(limit: 8) + base = BerrypodWeb.Endpoint.url() + site_name = socket.assigns.theme_settings.site_name + + org_ld = + Jason.encode!( + %{ + "@context" => "https://schema.org", + "@type" => "Organization", + "name" => site_name, + "url" => base <> "/" + }, + escape: :html_safe + ) + socket = socket |> assign(:page_title, "Home") - |> assign(:og_url, BerrypodWeb.Endpoint.url() <> "/") + |> assign(:og_url, base <> "/") + |> assign(:json_ld, org_ld) |> assign(:products, products) {:ok, socket} diff --git a/lib/berrypod_web/live/shop/product_show.ex b/lib/berrypod_web/live/shop/product_show.ex index dbe3170..8b9cdca 100644 --- a/lib/berrypod_web/live/shop/product_show.ex +++ b/lib/berrypod_web/live/shop/product_show.ex @@ -48,13 +48,18 @@ defmodule BerrypodWeb.Shop.ProductShow do ) end + base = BerrypodWeb.Endpoint.url() + og_url = base <> "/products/#{slug}" + og_image = og_image_url(all_images) + socket = socket |> assign(:page_title, product.title) |> assign(:page_description, meta_description(product.description)) |> assign(:og_type, "product") - |> assign(:og_url, BerrypodWeb.Endpoint.url() <> "/products/#{slug}") - |> assign(:og_image, og_image_url(all_images)) + |> assign(:og_url, og_url) + |> assign(:og_image, og_image) + |> assign(:json_ld, product_json_ld(product, og_url, og_image, base)) |> assign(:product, product) |> assign(:all_images, all_images) |> assign(:gallery_images, gallery_images) @@ -219,6 +224,71 @@ defmodule BerrypodWeb.Shop.ProductShow do """ end + defp product_json_ld(product, url, image, base) do + category_slug = + if product.category, + do: product.category |> String.downcase() |> String.replace(" ", "-"), + else: "all" + + breadcrumbs = + [ + %{"@type" => "ListItem", "position" => 1, "name" => "Home", "item" => base <> "/"}, + product.category && + %{ + "@type" => "ListItem", + "position" => 2, + "name" => product.category, + "item" => base <> "/collections/#{category_slug}" + }, + %{ + "@type" => "ListItem", + "position" => if(product.category, do: 3, else: 2), + "name" => product.title, + "item" => url + } + ] + |> Enum.reject(&is_nil/1) + + data = %{ + "@context" => "https://schema.org", + "@graph" => [ + %{ + "@type" => "Product", + "name" => product.title, + "description" => plain_text(product.description), + "image" => Enum.reject([image], &is_nil/1), + "url" => url, + "offers" => %{ + "@type" => "Offer", + "price" => format_price(product.cheapest_price), + "priceCurrency" => "GBP", + "availability" => + if(product.in_stock, + do: "https://schema.org/InStock", + else: "https://schema.org/OutOfStock" + ), + "url" => url + } + }, + %{ + "@type" => "BreadcrumbList", + "itemListElement" => breadcrumbs + } + ] + } + + Jason.encode!(data, escape: :html_safe) + end + + defp format_price(pence) when is_integer(pence) do + "#{div(pence, 100)}.#{String.pad_leading(to_string(rem(pence, 100)), 2, "0")}" + end + + defp format_price(_), do: "0.00" + + defp plain_text(nil), do: nil + defp plain_text(text), do: String.replace(text, ~r/<[^>]+>/, "") + defp og_image_url([%{url: "/" <> _ = path} | _]), do: BerrypodWeb.Endpoint.url() <> path defp og_image_url(_), do: nil