Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
425 changes: 221 additions & 204 deletions lib/modules/emails/templates.ex

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions lib/modules/publishing/posts.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ defmodule PhoenixKit.Modules.Publishing.Posts do
alias PhoenixKit.Modules.Publishing.DBStorage
alias PhoenixKit.Modules.Publishing.LanguageHelpers
alias PhoenixKit.Modules.Publishing.ListingCache
alias PhoenixKit.Modules.Publishing.Metadata
alias PhoenixKit.Modules.Publishing.PubSub, as: PublishingPubSub
alias PhoenixKit.Modules.Publishing.Shared
alias PhoenixKit.Modules.Publishing.SlugHelpers
Expand Down Expand Up @@ -650,12 +649,15 @@ defmodule PhoenixKit.Modules.Publishing.Posts do
true ->
{:error, :slug_already_exists}

{:error, %Ecto.Changeset{errors: errors}} ->
if Keyword.has_key?(errors, :slug),
{:error, %Ecto.Changeset{} = changeset} ->
Logger.warning("[Publishing] slug update changeset error: #{inspect(changeset.errors)}")

if Keyword.has_key?(changeset.errors, :slug),
do: {:error, :slug_already_exists},
else: {:error, :db_update_failed}

{:error, reason} ->
Logger.warning("[Publishing] slug update failed: #{inspect(reason)}")
{:error, reason}
end
end
Expand Down Expand Up @@ -709,12 +711,10 @@ defmodule PhoenixKit.Modules.Publishing.Posts do
end
end

defp resolve_post_title(params, post, content) do
extracted_title = Metadata.extract_title_from_content(content)
defp resolve_post_title(params, post, _content) do
post_metadata = post[:metadata] || %{}

Map.get(params, "title") ||
if(extracted_title != Constants.default_title(), do: extracted_title) ||
post_metadata[:title] ||
Constants.default_title()
end
Expand Down
15 changes: 15 additions & 0 deletions lib/modules/publishing/pubsub.ex
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,21 @@ defmodule PhoenixKit.Modules.Publishing.PubSub do
@topic_editor_forms "publishing:editor_forms"
@topic_groups "publishing:groups"

# ============================================================================
# Post Identifier Resolution
# ============================================================================

@doc """
Returns the broadcast identifier for a post.

Uses slug when available, falls back to uuid. This identifier is used
for PubSub topic construction and must be consistent between broadcasters
(e.g. translation worker) and subscribers (e.g. editor).
"""
def broadcast_id(post) do
post[:slug] || post[:uuid]
end

# ============================================================================
# Group-Level Updates (group creation/deletion)
# ============================================================================
Expand Down
10 changes: 5 additions & 5 deletions lib/modules/publishing/web/components/language_switcher.ex
Original file line number Diff line number Diff line change
Expand Up @@ -176,8 +176,8 @@ defmodule PhoenixKit.Modules.Publishing.Web.Components.LanguageSwitcher do
</button>
<% else %>
<%= if @lang[:url] do %>
<a
href={@lang[:url]}
<.link
navigate={@lang[:url]}
class={item_classes(@is_current, @exists, @show_add, @size, @enabled, @known)}
title={language_title(@lang, @exists, @status, @show_status, @enabled, @known)}
>
Expand All @@ -194,7 +194,7 @@ defmodule PhoenixKit.Modules.Publishing.Web.Components.LanguageSwitcher do
known={@known}
is_primary={@is_primary}
/>
</a>
</.link>
<% else %>
<span
class={item_classes(@is_current, @exists, @show_add, @size, @enabled, @known)}
Expand Down Expand Up @@ -326,13 +326,13 @@ defmodule PhoenixKit.Modules.Publishing.Web.Components.LanguageSwitcher do
cond do
# Disabled or unknown languages: grey styling but still clickable
is_current and (!enabled or !known) ->
"bg-base-content/10 text-base-content/50 font-semibold cursor-pointer"
"bg-base-content/30 text-base-content/50 font-semibold cursor-pointer"

(!enabled or !known) and exists ->
"text-base-content/40 hover:bg-base-200/50 cursor-pointer"

is_current ->
"bg-primary/10 text-primary font-semibold"
"bg-primary/30 text-primary font-semibold"

!exists && show_add ->
"text-success hover:bg-success/10 cursor-pointer"
Expand Down
51 changes: 51 additions & 0 deletions lib/modules/publishing/web/controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ defmodule PhoenixKit.Modules.Publishing.Web.Controller do
alias PhoenixKit.Modules.Publishing.Web.Controller.Listing
alias PhoenixKit.Modules.Publishing.Web.Controller.PostRendering
alias PhoenixKit.Modules.Publishing.Web.Controller.Routing
alias PhoenixKit.Modules.Publishing.Web.HTML, as: PublishingHTML
alias PhoenixKit.Settings

# ============================================================================
Expand Down Expand Up @@ -160,6 +161,11 @@ defmodule PhoenixKit.Modules.Publishing.Web.Controller do
defp handle_group_listing(conn, group_slug, language) do
case Listing.render_group_listing(conn, group_slug, language, conn.params) do
{:ok, assigns} ->
listing_url = PublishingHTML.group_listing_path(assigns.current_language, group_slug)

base_url =
"#{conn.scheme}://#{conn.host}#{if conn.port in [80, 443], do: "", else: ":#{conn.port}"}"

conn
|> assign(:page_title, assigns.page_title)
|> assign(:group, assigns.group)
Expand All @@ -171,6 +177,12 @@ defmodule PhoenixKit.Modules.Publishing.Web.Controller do
|> assign(:total_count, assigns.total_count)
|> assign(:total_pages, assigns.total_pages)
|> assign(:breadcrumbs, assigns.breadcrumbs)
|> assign(:og, %{
title: assigns.group["name"],
url: base_url <> listing_url,
locale: assigns.current_language,
type: "website"
})
|> render(:index)

{:redirect, url} ->
Expand All @@ -188,6 +200,9 @@ defmodule PhoenixKit.Modules.Publishing.Web.Controller do
defp handle_post(conn, group_slug, identifier, language) do
case PostRendering.render_post(conn, group_slug, identifier, language) do
{:ok, assigns} ->
canonical_url =
PublishingHTML.build_post_url(group_slug, assigns.post, assigns.current_language)

conn
|> assign(:page_title, assigns.page_title)
|> assign(:group_slug, assigns.group_slug)
Expand All @@ -197,6 +212,7 @@ defmodule PhoenixKit.Modules.Publishing.Web.Controller do
|> assign(:translations, assigns.translations)
|> assign(:breadcrumbs, assigns.breadcrumbs)
|> assign(:version_dropdown, assigns.version_dropdown)
|> assign(:og, build_og_data(conn, assigns.post, canonical_url, assigns.current_language))
|> render(:show)

{:redirect, url} ->
Expand Down Expand Up @@ -228,6 +244,10 @@ defmodule PhoenixKit.Modules.Publishing.Web.Controller do
|> assign(:is_live_version, assigns.is_live_version)
|> assign(:version, assigns.version)
|> assign(:version_dropdown, assigns.version_dropdown)
|> assign(
:og,
build_og_data(conn, assigns.post, assigns.canonical_url, assigns.current_language)
)
|> render(:show)

{:error, reason} ->
Expand All @@ -238,6 +258,9 @@ defmodule PhoenixKit.Modules.Publishing.Web.Controller do
defp handle_date_only_url(conn, group_slug, date, language) do
case PostRendering.handle_date_only_url(conn, group_slug, date, language) do
{:ok, assigns} ->
canonical_url =
PublishingHTML.build_post_url(group_slug, assigns.post, assigns.current_language)

conn
|> assign(:page_title, assigns.page_title)
|> assign(:group_slug, assigns.group_slug)
Expand All @@ -247,6 +270,7 @@ defmodule PhoenixKit.Modules.Publishing.Web.Controller do
|> assign(:translations, assigns.translations)
|> assign(:breadcrumbs, assigns.breadcrumbs)
|> assign(:version_dropdown, assigns.version_dropdown)
|> assign(:og, build_og_data(conn, assigns.post, canonical_url, assigns.current_language))
|> render(:show)

{:redirect, url} ->
Expand Down Expand Up @@ -292,4 +316,31 @@ defmodule PhoenixKit.Modules.Publishing.Web.Controller do
defp set_gettext_locale(language) do
Gettext.put_locale(PhoenixKitWeb.Gettext, language)
end

defp build_og_data(conn, post, canonical_url, language) do
seo = Map.get(post.metadata, :seo) || Map.get(post, :seo) || %{}
description = seo["og_description"] || Map.get(post.metadata, :description)
image = seo["og_image"] || PublishingHTML.featured_image_url(post, "large")

base_url =
"#{conn.scheme}://#{conn.host}#{if conn.port in [80, 443], do: "", else: ":#{conn.port}"}"

%{
title: seo["og_title"] || post.metadata.title,
description: description,
image: absolute_url(base_url, image),
url: absolute_url(base_url, canonical_url),
locale: language,
type: "article"
}
end

defp absolute_url(_base, nil), do: nil
defp absolute_url(_base, ""), do: nil

defp absolute_url(base, url) when is_binary(url) do
if String.starts_with?(url, "http"), do: url, else: base <> url
end

defp absolute_url(_base, _url), do: nil
end
20 changes: 18 additions & 2 deletions lib/modules/publishing/web/controller/listing.ex
Original file line number Diff line number Diff line change
Expand Up @@ -182,8 +182,13 @@ defmodule PhoenixKit.Modules.Publishing.Web.Controller.Listing do
lang_excerpts = post[:language_excerpts] || %{}
metadata = post[:metadata] || %{}

title = Map.get(lang_titles, language, metadata[:title] || Constants.default_title())
excerpt = Map.get(lang_excerpts, language)
# Language map keys are full dialect codes (e.g., "en-US") but the
# canonical_language may be a base code (e.g., "en") when only one
# dialect is enabled. Try exact match first, then match by base code.
resolved_key = resolve_language_key(language, Map.keys(lang_titles))

title = Map.get(lang_titles, resolved_key, metadata[:title] || Constants.default_title())
excerpt = Map.get(lang_excerpts, resolved_key)

post
|> Map.update(:metadata, %{title: title}, &Map.put(&1, :title, title))
Expand All @@ -192,6 +197,17 @@ defmodule PhoenixKit.Modules.Publishing.Web.Controller.Listing do
end)
end

# Resolves a display language code to a key in the language map.
# Handles base code ("en") → dialect code ("en-US") matching.
defp resolve_language_key(language, available_keys) do
if language in available_keys do
language
else
base = DialectMapper.extract_base(language)
Enum.find(available_keys, language, fn key -> DialectMapper.extract_base(key) == base end)
end
end

# ============================================================================
# Filtering Functions
# ============================================================================
Expand Down
12 changes: 3 additions & 9 deletions lib/modules/publishing/web/edit.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -50,18 +50,12 @@
)}
</p>
<p class="text-xs text-success">
✓ {gettext("Valid examples")}: <code class="font-mono">blog</code>, <code class="font-mono">
product-updates
</code>,
✓ {gettext("Valid examples")}: <code class="font-mono">blog</code>, <code class="font-mono">product-updates</code>,
<code class="font-mono">news-2025</code>
</p>
<p class="text-xs text-error">
✗ {gettext("Invalid examples")}: <code class="font-mono">Blog</code>, <code class="font-mono">
product_updates
</code>, <code class="font-mono">-news</code>,
<code class="font-mono">
my blog
</code>
✗ {gettext("Invalid examples")}: <code class="font-mono">Blog</code>, <code class="font-mono">product_updates</code>, <code class="font-mono">-news</code>,
<code class="font-mono">my blog</code>
</p>
</div>
</div>
Expand Down
Loading
Loading