From c115f08cb8a4b46efa1c1b1e4f0ccac128e60b89 Mon Sep 17 00:00:00 2001 From: jamey Date: Wed, 1 Apr 2026 00:36:05 +0100 Subject: [PATCH] add url slug resolution to shop routing Shop.Page resolves custom URLs to correct page types: - resolve_custom_slug/2 detects system pages with custom URLs - Dynamic prefix resolution for /:prefix/:id routes - Updates live_action when slug resolves to different type Co-Authored-By: Claude Opus 4.5 --- lib/berrypod_web/live/shop/page.ex | 64 +++++++++++++++++- .../live/shop/pages/custom_page.ex | 66 ++++++++++++++----- 2 files changed, 112 insertions(+), 18 deletions(-) diff --git a/lib/berrypod_web/live/shop/page.ex b/lib/berrypod_web/live/shop/page.ex index 36edd07..2041efe 100644 --- a/lib/berrypod_web/live/shop/page.ex +++ b/lib/berrypod_web/live/shop/page.ex @@ -9,10 +9,11 @@ defmodule BerrypodWeb.Shop.Page do use BerrypodWeb, :live_view alias BerrypodWeb.Shop.Pages + alias BerrypodWeb.R alias Berrypod.{Media, Settings} alias Berrypod.Workers.FaviconGeneratorWorker - # Map live_action atoms to page handler modules + # Map page type atoms to handler modules @page_modules %{ home: Pages.Home, product: Pages.Product, @@ -187,10 +188,22 @@ defmodule BerrypodWeb.Shop.Page do action = socket.assigns.live_action prev_action = socket.assigns[:_current_page_action] prev_path = socket.assigns[:_current_path] - module = @page_modules[action] parsed_uri = URI.parse(uri) current_path = parsed_uri.path + # For custom_page action, check if slug is a custom URL for a system page + {action, params} = resolve_custom_slug(action, params) + + # Update live_action if we resolved to a different page type + socket = + if action != socket.assigns.live_action do + assign(socket, :live_action, action) + else + socket + end + + module = @page_modules[action] + # Clean up previous page if needed (e.g., unsubscribe from PubSub) socket = maybe_cleanup_previous_page(socket, prev_action) @@ -375,4 +388,51 @@ defmodule BerrypodWeb.Shop.Page do end defp clear_page_specific_assigns(socket, _), do: socket + + # Resolve custom URL slugs to system pages + # E.g., if cart page has url_slug: "basket", visiting /basket should load the cart page + defp resolve_custom_slug(:custom_page, %{"slug" => slug} = params) do + case R.page_type_from_slug(slug) do + {:page, page_type} -> + # Custom URL for a system page - dispatch to that page type + # Preserve other params (like query params) but remove slug + {page_type, Map.delete(params, "slug")} + + {:custom, _page} -> + # Regular custom CMS page - let CustomPage handle it + {:custom_page, params} + + nil -> + # Unknown slug - let CustomPage handle the 404 + {:custom_page, params} + end + end + + # Handle dynamic prefix routes for products, collections, and orders. + # Works with both default prefixes (/products/123) and custom prefixes (/p/123). + # R.prefix_type_from_segment checks both the ETS cache (custom) and defaults. + defp resolve_custom_slug(:dynamic_prefix, %{"prefix" => prefix} = params) do + # The second segment param name is "id_or_slug" from the route definition + id_or_slug = params["id_or_slug"] + # Preserve query params by keeping all params except route-specific ones + other_params = Map.drop(params, ["prefix", "id_or_slug"]) + + case R.prefix_type_from_segment(prefix) do + :collections -> + {:collection, Map.put(other_params, "slug", id_or_slug)} + + :products -> + {:product, Map.put(other_params, "id", id_or_slug)} + + :orders -> + {:order_detail, Map.put(other_params, "order_number", id_or_slug)} + + nil -> + # Not a known prefix - check if combined path is a system page (e.g. checkout/success) + combined_slug = prefix <> "/" <> id_or_slug + resolve_custom_slug(:custom_page, Map.put(other_params, "slug", combined_slug)) + end + end + + defp resolve_custom_slug(action, params), do: {action, params} end diff --git a/lib/berrypod_web/live/shop/pages/custom_page.ex b/lib/berrypod_web/live/shop/pages/custom_page.ex index 9dc40c0..e43ca64 100644 --- a/lib/berrypod_web/live/shop/pages/custom_page.ex +++ b/lib/berrypod_web/live/shop/pages/custom_page.ex @@ -12,7 +12,7 @@ defmodule BerrypodWeb.Shop.Pages.CustomPage do {:noreply, socket} end - def handle_params(%{"slug" => slug}, _uri, socket) do + def handle_params(%{"slug" => slug} = params, uri, socket) do page = Pages.get_page(slug) cond do @@ -27,33 +27,67 @@ defmodule BerrypodWeb.Shop.Pages.CustomPage do raise BerrypodWeb.NotFoundError true -> - extra = Pages.load_block_data(page.blocks, socket.assigns) - base = BerrypodWeb.Endpoint.url() - - socket = - socket - |> assign(:page_title, page.title) - |> assign(:page, page) - |> maybe_assign_meta(page, base) - |> assign(extra) - - {:noreply, socket} + handle_page(page_to_map(page), params, uri, socket) end end + defp handle_page(page, _params, _uri, socket) do + extra = Pages.load_block_data(page.blocks, socket.assigns) + base = BerrypodWeb.Endpoint.url() + + socket = + socket + |> assign(:page_title, page.title) + |> assign(:page, page) + |> maybe_assign_meta(page, base) + |> assign(extra) + + {:noreply, socket} + end + + # Convert Page struct to map for consistent handling + defp page_to_map(%Berrypod.Pages.Page{} = page) do + %{ + id: page.id, + slug: page.slug, + title: page.title, + blocks: page.blocks, + type: page.type, + published: page.published, + meta_description: page.meta_description, + url_slug: page.url_slug, + show_in_nav: page.show_in_nav, + nav_label: page.nav_label, + nav_position: page.nav_position + } + end + + defp page_to_map(page) when is_map(page), do: page + def handle_event(_event, _params, _socket), do: :cont defp record_broken_url(path) do - prior_hits = Berrypod.Analytics.count_pageviews_for_path(path) - Berrypod.Redirects.record_broken_url(path, prior_hits) + # Skip static asset paths - these are expected 404s + unless static_path?(path) do + prior_hits = Berrypod.Analytics.count_pageviews_for_path(path) + Berrypod.Redirects.record_broken_url(path, prior_hits) - if prior_hits > 0 do - Berrypod.Redirects.attempt_auto_resolve(path) + if prior_hits > 0 do + Berrypod.Redirects.attempt_auto_resolve(path) + end end rescue _ -> :ok end + defp static_path?(path) do + String.starts_with?(path, "/assets/") or + String.starts_with?(path, "/images/") or + String.starts_with?(path, "/image_cache/") or + String.starts_with?(path, "/mockups/") or + String.starts_with?(path, "/favicon") + end + defp maybe_assign_meta(socket, page, base) do socket |> assign(:og_url, base <> "/#{page.slug}")