From 1004865013ac6b2d28ad92224440df8ca79aa3d2 Mon Sep 17 00:00:00 2001 From: jamey Date: Wed, 1 Apr 2026 00:35:24 +0100 Subject: [PATCH] add page url slug management functions Context functions for custom page URLs: - list_pages_with_custom_urls/0 for admin overview - get_published_page_by_effective_url/1 for routing lookups - update_page_url_slug/2 with automatic redirect creation Co-Authored-By: Claude Opus 4.5 --- lib/berrypod/pages.ex | 108 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 107 insertions(+), 1 deletion(-) diff --git a/lib/berrypod/pages.ex b/lib/berrypod/pages.ex index 92f19ae..e27237f 100644 --- a/lib/berrypod/pages.ex +++ b/lib/berrypod/pages.ex @@ -90,6 +90,28 @@ defmodule Berrypod.Pages do list_system_pages() ++ list_custom_pages() end + @doc "Lists pages that have a custom URL slug set. Used by Routes cache." + def list_pages_with_custom_urls do + from(p in Page, where: not is_nil(p.url_slug)) + |> Repo.all() + end + + @doc "Gets a published page by its effective URL (url_slug or slug)." + def get_published_page_by_effective_url(url) do + # First check for custom url_slug match + case Repo.one(from p in Page, where: p.url_slug == ^url and p.published == true) do + nil -> + # Fall back to slug match for custom pages + Repo.one( + from p in Page, + where: p.slug == ^url and p.type == "custom" and p.published == true + ) + + page -> + page + end + end + # ── Write ───────────────────────────────────────────────────────── @doc """ @@ -272,7 +294,91 @@ defmodule Berrypod.Pages do meta_description: page.meta_description, show_in_nav: page.show_in_nav, nav_label: page.nav_label, - nav_position: page.nav_position + nav_position: page.nav_position, + url_slug: page.url_slug } end + + # ── URL customisation ──────────────────────────────────────────── + + @doc """ + Updates a page's custom URL slug. Creates a redirect from the old + effective URL to the new one, and invalidates the R module cache. + + Pass nil or empty string to clear the custom URL. + """ + def update_page_url_slug(%Page{} = page, url_slug) do + old_effective_url = Page.effective_url(page) + + # Normalise empty string to nil + url_slug = if url_slug == "", do: nil, else: url_slug + + changeset_fn = + if Page.system_slug?(page.slug), + do: &Page.system_changeset/2, + else: &Page.custom_changeset/2 + + result = + page + |> changeset_fn.(%{url_slug: url_slug}) + |> Repo.update() + + case result do + {:ok, updated} -> + new_effective_url = Page.effective_url(updated) + + # Create redirect if URL changed + if old_effective_url != new_effective_url do + # Delete any stale redirect FROM the new URL (it's now a live page) + delete_stale_redirect("/#{new_effective_url}") + + # Create redirect from old URL to new URL + Berrypod.Redirects.create_auto(%{ + from_path: "/#{old_effective_url}", + to_path: "/#{new_effective_url}", + source: "auto_slug_change" + }) + end + + # Invalidate caches synchronously to avoid race conditions + PageCache.invalidate(page.slug) + BerrypodWeb.R.invalidate_all_sync() + + {:ok, updated} + + error -> + error + end + end + + def update_page_url_slug(slug, url_slug) when is_binary(slug) do + case get_page_struct(slug) do + nil -> + # Page doesn't exist in DB - create it from defaults if it's a system page + if Page.system_slug?(slug) do + defaults = Defaults.for_slug(slug) + + case save_page(slug, defaults) do + {:ok, page} -> update_page_url_slug(page, url_slug) + error -> error + end + else + {:error, :not_found} + end + + page -> + update_page_url_slug(page, url_slug) + end + end + + # When a URL becomes a live page, delete any redirect from that URL + defp delete_stale_redirect(path) do + case Repo.one(from r in Berrypod.Redirects.Redirect, where: r.from_path == ^path) do + %Berrypod.Redirects.Redirect{} = redirect -> + Berrypod.Redirects.delete_redirect(redirect) + + nil -> + :ok + end + end end