Skip to content
40 changes: 35 additions & 5 deletions lib/modules/legal/legal.ex
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ defmodule PhoenixKit.Modules.Legal do
alias PhoenixKit.Modules.Legal.PageType
alias PhoenixKit.Modules.Legal.TemplateGenerator
alias PhoenixKit.Settings
alias PhoenixKit.Utils.Routes

@enabled_key "legal_enabled"
@module_name "legal"
Expand Down Expand Up @@ -551,12 +552,26 @@ defmodule PhoenixKit.Modules.Legal do
- google_consent_mode: boolean
- hide_for_authenticated: boolean
- frameworks: list of framework IDs
- cookie_policy_url: string
- privacy_policy_url: string
- cookie_policy_url: string (backward compat, derived from published pages)
- privacy_policy_url: string (backward compat, derived from published pages)
- legal_links: list of %{title: string, url: string} for all published legal pages
- legal_index_url: string
"""
@spec get_consent_widget_config() :: map()
def get_consent_widget_config do
prefix = PhoenixKit.Config.get_url_prefix()
legal_links = get_published_legal_links()

cookie_policy_url =
case Enum.find(legal_links, &String.ends_with?(&1.url, "/cookie-policy")) do
%{url: url} -> url
nil -> Routes.path("/legal/cookie-policy")
end

privacy_policy_url =
case Enum.find(legal_links, &String.ends_with?(&1.url, "/privacy-policy")) do
%{url: url} -> url
nil -> Routes.path("/legal/privacy-policy")
end

%{
enabled: consent_widget_enabled?(),
Expand All @@ -567,11 +582,26 @@ defmodule PhoenixKit.Modules.Legal do
policy_version: get_auto_policy_version(),
google_consent_mode: google_consent_mode_enabled?(),
frameworks: get_selected_frameworks(),
cookie_policy_url: "#{prefix}/legal/cookie-policy",
privacy_policy_url: "#{prefix}/legal/privacy-policy"
cookie_policy_url: cookie_policy_url,
privacy_policy_url: privacy_policy_url,
legal_links: legal_links,
legal_index_url: Routes.path("/legal")
}
end

@doc """
Returns a list of all published legal pages as link maps.

Each map has `:title` and `:url` keys. Used by the cookie consent widget
to render dynamic links to all published legal pages.
"""
@spec get_published_legal_links() :: list(%{title: String.t(), url: String.t()})
def get_published_legal_links do
list_generated_pages()
|> Enum.filter(&(&1.status == "published"))
|> Enum.map(&%{title: &1.title, url: Routes.path("/legal/#{&1.slug}")})
end

@doc """
Check if there are unpublished legal pages that are required.

Expand Down
16 changes: 15 additions & 1 deletion lib/modules/sitemap/sources/publishing.ex
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ defmodule PhoenixKit.Modules.Sitemap.Sources.Publishing do

UrlEntry.new(%{
loc: url,
lastmod: nil,
lastmod: latest_post_date(slug, language),
changefreq: "daily",
priority: 0.7,
title: name,
Expand Down Expand Up @@ -384,6 +384,20 @@ defmodule PhoenixKit.Modules.Sitemap.Sources.Publishing do
end
end

# Latest lastmod among published posts in a group (for group listing pages)
defp latest_post_date(group_slug, language) do
post_language = language || get_default_language()

Publishing.list_posts(group_slug, post_language)
|> Enum.filter(&published?/1)
|> Enum.reject(&excluded?/1)
|> Enum.map(&get_post_lastmod/1)
|> Enum.reject(&is_nil/1)
|> Enum.max(Date, fn -> nil end)
rescue
_ -> nil
end

defp get_post_lastmod(post) do
case post do
# Check metadata fields first (PhoenixKit Publishing uses published_at)
Expand Down
23 changes: 21 additions & 2 deletions lib/modules/sitemap/sources/static.ex
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ defmodule PhoenixKit.Modules.Sitemap.Sources.Static do

UrlEntry.new(%{
loc: url,
lastmod: Date.utc_today(),
lastmod: static_lastmod(path),
changefreq: Map.get(config, "changefreq", "weekly"),
priority: Map.get(config, "priority", 0.5),
title: Map.get(config, "title", path),
Expand All @@ -224,7 +224,7 @@ defmodule PhoenixKit.Modules.Sitemap.Sources.Static do

UrlEntry.new(%{
loc: url,
lastmod: Date.utc_today(),
lastmod: static_lastmod(path),
changefreq: Map.get(config, "changefreq", "weekly"),
priority: Map.get(config, "priority", 0.5),
title: Map.get(config, "title", path),
Expand All @@ -237,6 +237,25 @@ defmodule PhoenixKit.Modules.Sitemap.Sources.Static do
end
end

# For homepage, use the latest published content date across all publishing groups.
# For other static pages, use today's date as a reasonable approximation.
defp static_lastmod("/") do
alias PhoenixKit.Modules.Sitemap.Sources.Publishing

if Code.ensure_loaded?(Publishing) and function_exported?(Publishing, :collect, 1) do
Publishing.collect([])
|> Enum.map(& &1.lastmod)
|> Enum.reject(&is_nil/1)
|> Enum.max(Date, fn -> Date.utc_today() end)
else
Date.utc_today()
end
rescue
_ -> Date.utc_today()
end

defp static_lastmod(_path), do: Date.utc_today()

# Resolve path from config: explicit path OR via RouteResolver
defp resolve_path(%{"path" => path}) when is_binary(path) and path != "" do
path
Expand Down
89 changes: 27 additions & 62 deletions lib/phoenix_kit_web/components/core/cookie_consent.ex
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,13 @@ defmodule PhoenixKitWeb.Components.Core.CookieConsent do
attr :policy_version, :string, default: "1.0", doc: "Policy version for consent tracking"
attr :cookie_policy_url, :string, default: "/legal/cookie-policy"
attr :privacy_policy_url, :string, default: "/legal/privacy-policy"

attr :legal_links, :list,
default: [],
doc: "Dynamic list of %{title, url} for published legal pages"

attr :legal_index_url, :string, default: "/legal", doc: "URL to legal pages index"

attr :google_consent_mode, :boolean, default: false, doc: "Enable Google Consent Mode v2"
attr :class, :string, default: ""

Expand Down Expand Up @@ -173,7 +180,7 @@ defmodule PhoenixKitWeb.Components.Core.CookieConsent do
}

.pk-glass {
background: oklch(var(--b1) / 0.95);
background: oklch(var(--b1) / 0.98);
backdrop-filter: blur(20px) saturate(180%);
-webkit-backdrop-filter: blur(20px) saturate(180%);
border: 1px solid var(--pk-border);
Expand All @@ -190,25 +197,6 @@ defmodule PhoenixKitWeb.Components.Core.CookieConsent do
transform: translateY(-2px);
box-shadow: 0 4px 12px oklch(var(--bc) / 0.1);
}

.pk-toggle-track {
background: var(--pk-border);
transition: background-color 0.2s ease;
}

.pk-toggle-track.active {
background: var(--pk-primary);
}

.pk-toggle-thumb {
background: var(--pk-bg);
box-shadow: 0 1px 3px oklch(var(--bc) / 0.2);
transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
}

input:checked + .pk-toggle-track .pk-toggle-thumb {
transform: translateX(20px);
}
</style>

<%!-- Floating Icon (only for opt-in frameworks) --%>
Expand Down Expand Up @@ -254,17 +242,16 @@ defmodule PhoenixKitWeb.Components.Core.CookieConsent do
<h3 class="font-semibold text-base-content text-sm sm:text-base">
{gettext("We value your privacy")}
</h3>
<p class="text-base-content/70 text-xs sm:text-sm mt-0.5 leading-relaxed max-w-xl">
<p class="text-base-content/80 text-xs sm:text-sm mt-0.5 leading-relaxed max-w-xl">
{gettext(
"We use cookies to enhance your browsing experience and analyze our traffic."
)}
{" "}
<a
href={@cookie_policy_url}
href={@legal_index_url}
class="link link-primary text-xs sm:text-sm"
target="_blank"
>
{gettext("Cookie Policy")}
{gettext("Legal")}
</a>
</p>
</div>
Expand Down Expand Up @@ -308,7 +295,7 @@ defmodule PhoenixKitWeb.Components.Core.CookieConsent do
>
<%!-- Backdrop --%>
<div
class="pk-modal-backdrop absolute inset-0 bg-black/40 backdrop-blur-sm"
class="pk-modal-backdrop absolute inset-0 bg-base-100/70 backdrop-blur-sm"
onclick="window.PhoenixKitConsent?.closePreferences()"
>
</div>
Expand All @@ -328,7 +315,7 @@ defmodule PhoenixKitWeb.Components.Core.CookieConsent do
<h2 class="font-semibold text-lg text-base-content">
{gettext("Privacy Preferences")}
</h2>
<p class="text-xs text-base-content/60">
<p class="text-xs text-base-content/70">
{gettext("Manage your cookie settings")}
</p>
</div>
Expand Down Expand Up @@ -356,7 +343,7 @@ defmodule PhoenixKitWeb.Components.Core.CookieConsent do
<%= for category <- @categories do %>
<div class={[
"pk-category-card rounded-xl p-4",
"bg-base-200/50 border border-base-300/30"
"bg-base-200/80 border border-base-300/30"
]}>
<div class="flex items-start justify-between gap-3">
<div class="flex items-start gap-3 flex-1 min-w-0">
Expand All @@ -374,34 +361,21 @@ defmodule PhoenixKitWeb.Components.Core.CookieConsent do
</span>
<% end %>
</div>
<p class="text-xs text-base-content/60 mt-1 leading-relaxed">
<p class="text-xs text-base-content/70 mt-1 leading-relaxed">
{category.description}
</p>
</div>
</div>

<%!-- Custom Toggle --%>
<label class="relative inline-flex items-center cursor-pointer flex-shrink-0">
<input
type="checkbox"
id={"pk-consent-#{category.id}"}
class="sr-only peer"
checked={Map.get(category, :always_enabled, false)}
disabled={Map.get(category, :always_enabled, false)}
data-category={category.id}
/>
<div class={[
"pk-toggle-track relative w-11 h-6 rounded-full",
"bg-base-300 peer-checked:bg-primary",
"peer-disabled:opacity-60 peer-disabled:cursor-not-allowed",
"after:content-[''] after:absolute after:top-0.5 after:left-0.5",
"after:bg-white after:rounded-full after:h-5 after:w-5",
"after:shadow-sm after:transition-transform after:duration-200",
"peer-checked:after:translate-x-5",
"peer-focus:ring-2 peer-focus:ring-primary/30"
]}>
</div>
</label>
<%!-- Toggle --%>
<input
type="checkbox"
id={"pk-consent-#{category.id}"}
class="toggle toggle-primary"
checked={Map.get(category, :always_enabled, false)}
disabled={Map.get(category, :always_enabled, false)}
data-category={category.id}
/>
</div>
</div>
<% end %>
Expand All @@ -411,21 +385,12 @@ defmodule PhoenixKitWeb.Components.Core.CookieConsent do
<div class="px-6 py-4 border-t border-base-300/50 bg-base-200/30">
<div class="flex flex-col sm:flex-row items-stretch sm:items-center gap-3">
<%!-- Policy Links --%>
<div class="flex items-center gap-3 text-xs text-base-content/60">
<a
href={@privacy_policy_url}
class="link hover:text-primary transition-colors"
target="_blank"
>
{gettext("Privacy Policy")}
</a>
<span class="text-base-300">•</span>
<div class="flex items-center gap-3 text-xs text-base-content/70">
<a
href={@cookie_policy_url}
href={@legal_index_url}
class="link hover:text-primary transition-colors"
target="_blank"
>
{gettext("Cookie Policy")}
{gettext("Legal")}
</a>
</div>

Expand Down
2 changes: 2 additions & 0 deletions lib/phoenix_kit_web/components/layout_wrapper.ex
Original file line number Diff line number Diff line change
Expand Up @@ -700,6 +700,8 @@ defmodule PhoenixKitWeb.Components.LayoutWrapper do
policy_version={config.policy_version}
cookie_policy_url={config.cookie_policy_url}
privacy_policy_url={config.privacy_policy_url}
legal_links={config.legal_links}
legal_index_url={config.legal_index_url}
google_consent_mode={config.google_consent_mode}
/>
<% end %>
Expand Down
Loading