Packages

phoenix_kit

1.7.81
1.7.207 1.7.206 1.7.205 1.7.204 1.7.203 1.7.202 1.7.201 1.7.200 1.7.199 1.7.198 1.7.197 1.7.196 1.7.194 1.7.193 1.7.192 1.7.191 1.7.190 1.7.189 1.7.187 1.7.186 1.7.185 1.7.184 1.7.183 1.7.182 1.7.181 1.7.180 1.7.179 1.7.178 1.7.177 1.7.176 1.7.175 1.7.174 1.7.173 1.7.172 1.7.171 1.7.170 1.7.169 1.7.168 1.7.167 1.7.166 1.7.165 1.7.164 1.7.162 1.7.161 1.7.160 1.7.159 1.7.157 1.7.156 1.7.155 1.7.154 1.7.153 1.7.152 1.7.151 1.7.150 1.7.149 1.7.146 1.7.145 1.7.144 1.7.143 1.7.138 1.7.133 1.7.132 1.7.131 1.7.130 1.7.128 1.7.126 1.7.125 1.7.121 1.7.120 1.7.119 1.7.118 1.7.117 1.7.116 1.7.115 1.7.114 1.7.113 1.7.112 1.7.111 1.7.110 1.7.109 1.7.108 1.7.107 1.7.106 1.7.105 1.7.104 1.7.103 1.7.102 1.7.101 1.7.100 1.7.99 1.7.98 1.7.97 1.7.96 1.7.95 1.7.94 1.7.93 1.7.92 1.7.91 1.7.90 1.7.89 1.7.88 1.7.87 1.7.86 1.7.85 1.7.84 1.7.83 1.7.82 1.7.81 1.7.80 1.7.79 1.7.78 1.7.77 1.7.76 1.7.75 1.7.74 1.7.71 1.7.70 1.7.69 1.7.66 1.7.65 1.7.64 1.7.63 1.7.62 1.7.61 1.7.59 1.7.58 1.7.57 1.7.56 1.7.55 1.7.54 1.7.53 1.7.52 1.7.51 1.7.49 1.7.44 1.7.43 1.7.42 1.7.41 1.7.39 1.7.38 1.7.37 1.7.36 1.7.34 1.7.33 1.7.31 1.7.30 1.7.29 1.7.28 1.7.27 1.7.26 1.7.25 1.7.24 1.7.23 1.7.22 1.7.21 1.7.20 1.7.19 1.7.18 1.7.17 1.7.16 1.7.15 1.7.14 1.7.13 1.7.12 1.7.11 1.7.10 1.7.9 1.7.8 1.7.7 1.7.6 1.7.5 1.7.4 1.7.3 1.7.2 1.7.1 1.7.0 1.6.20 1.6.19 1.6.18 1.6.17 1.6.16 1.6.15 1.6.14 1.6.13 1.6.12 1.6.11 1.6.10 1.6.9 1.6.8 1.6.7 1.6.6 1.6.5 1.6.4 1.6.3 1.5.2 1.5.1 1.5.0 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.2 1.3.1 1.3.0 1.2.10 1.2.9 1.2.8 1.2.7 1.2.5 1.2.4 1.2.2 1.2.1 1.2.0 1.1.0 1.0.0

A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more

Current section

Files

Jump to
phoenix_kit lib modules publishing web controller.ex
Raw

lib/modules/publishing/web/controller.ex

defmodule PhoenixKit.Modules.Publishing.Web.Controller do
@moduledoc """
Public post display controller.
Handles public-facing routes for viewing published posts with multi-language support.
URL patterns:
/:language/:group_slug/:post_slug - Slug mode post
/:language/:group_slug/:date/:time - Timestamp mode post
/:language/:group_slug - Group listing
## Architecture
This controller delegates to specialized submodules:
- `Routing` - URL path parsing and segment building
- `Language` - Language detection and resolution
- `SlugResolution` - URL slug resolution and redirects
- `PostFetching` - Post retrieval from cache/database
- `Listing` - Group listing rendering and pagination
- `PostRendering` - Post rendering and version handling
- `Translations` - Translation link building
- `Fallback` - 404 handling and smart fallback chain
"""
use PhoenixKitWeb, :controller
use Gettext, backend: PhoenixKitWeb.Gettext
alias PhoenixKit.Modules.Publishing
alias PhoenixKit.Modules.Publishing.Web.Controller.Fallback
alias PhoenixKit.Modules.Publishing.Web.Controller.Language
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
# ============================================================================
# Main Entry Points
# ============================================================================
@doc """
Displays a post, group listing, or all groups overview.
Path parsing determines which action to take:
- [] -> Invalid request (no group specified)
- [group_slug] -> Group listing
- [group_slug, post_slug] -> Slug mode post
- [group_slug, date] -> Date-only timestamp (resolves to single post or first post)
- [group_slug, date, time] -> Timestamp mode post
"""
def show(conn, %{"language" => language_param} = params) do
# Detect if 'language' param is actually a language code or a group slug
# This allows the same route to work for both single and multi-language setups
{language, adjusted_params} = Language.detect_language_or_group(language_param, params)
conn = assign(conn, :current_language, language)
set_gettext_locale(language)
if Publishing.enabled?() and public_enabled?() do
case Routing.build_segments(adjusted_params) do
[] ->
handle_not_found(conn, :invalid_path)
segments ->
handle_parsed_path(conn, Routing.parse_path(segments), language)
end
else
handle_not_found(conn, :module_disabled)
end
end
# Fallback for routes without language parameter
# This handles the non-localized route where :group might actually be a language code
def show(conn, params) do
if Publishing.enabled?() and public_enabled?() do
# Check if the first segment (group) is actually a language with content
case Language.detect_language_in_group_param(params) do
{:language_detected, language, adjusted_params} ->
# First segment was a language code with content - use localized logic
conn = assign(conn, :current_language, language)
set_gettext_locale(language)
handle_request(conn, language, adjusted_params)
:not_a_language ->
# First segment is a group slug - use default language
language = Language.get_default_language()
conn = assign(conn, :current_language, language)
set_gettext_locale(language)
handle_request(conn, language, params)
end
else
handle_not_found(conn, :module_disabled)
end
end
# ============================================================================
# Request Handlers
# ============================================================================
# Handles request after language has been resolved (localized or default)
defp handle_request(conn, language, params) do
case Routing.build_segments(params) do
[] ->
handle_not_found(conn, :invalid_path)
segments ->
handle_parsed_path(conn, Routing.parse_path(segments), language)
end
end
# Dispatches to appropriate handler based on parsed path
# Checks group exists and is active before serving content
defp handle_parsed_path(conn, parsed_path, language) do
group_slug = extract_group_slug(parsed_path)
if group_slug && group_trashed?(group_slug) do
handle_not_found(conn, :group_not_found)
else
case parsed_path do
{:listing, group_slug} ->
handle_group_listing(conn, group_slug, language)
{:slug_post, group_slug, post_slug} ->
handle_post(conn, group_slug, {:slug, post_slug}, language)
{:timestamp_post, group_slug, date, time} ->
handle_post(conn, group_slug, {:timestamp, date, time}, language)
{:date_only_post, group_slug, date} ->
handle_date_only_url(conn, group_slug, date, language)
{:versioned_post, group_slug, post_slug, version} ->
handle_versioned_post(conn, group_slug, post_slug, version, language)
{:error, reason} ->
handle_not_found(conn, reason)
end
end
end
# Suppress dialyzer warning — catch-all is defensive fallback for unexpected route formats
@dialyzer {:nowarn_function, extract_group_slug: 1}
defp extract_group_slug({_, group_slug}), do: group_slug
defp extract_group_slug({_, group_slug, _}), do: group_slug
defp extract_group_slug({_, group_slug, _, _}), do: group_slug
defp extract_group_slug(_), do: nil
defp group_trashed?(nil), do: false
defp group_trashed?(group_slug) do
case Publishing.get_group(group_slug) do
{:ok, group} -> group["status"] == "trashed"
{:error, _} -> false
end
end
# ============================================================================
# Group Listing Handler
# ============================================================================
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)
|> assign(:posts, assigns.posts)
|> assign(:current_language, assigns.current_language)
|> assign(:translations, assigns.translations)
|> assign(:page, assigns.page)
|> assign(:per_page, assigns.per_page)
|> 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} ->
redirect(conn, to: url)
{:error, reason} ->
handle_not_found(conn, reason)
end
end
# ============================================================================
# Post Handlers
# ============================================================================
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)
|> assign(:post, assigns.post)
|> assign(:html_content, assigns.html_content)
|> assign(:current_language, assigns.current_language)
|> 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} ->
redirect(conn, to: url)
{:redirect_301, url} ->
conn
|> put_status(301)
|> redirect(to: url)
{:error, reason} ->
handle_not_found(conn, reason)
end
end
defp handle_versioned_post(conn, group_slug, post_slug, version, language) do
case PostRendering.render_versioned_post(conn, group_slug, post_slug, version, language) do
{:ok, assigns} ->
conn
|> assign(:page_title, assigns.page_title)
|> assign(:group_slug, assigns.group_slug)
|> assign(:post, assigns.post)
|> assign(:html_content, assigns.html_content)
|> assign(:current_language, assigns.current_language)
|> assign(:translations, assigns.translations)
|> assign(:breadcrumbs, assigns.breadcrumbs)
|> assign(:canonical_url, assigns.canonical_url)
|> assign(:is_versioned_view, assigns.is_versioned_view)
|> 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} ->
handle_not_found(conn, reason)
end
end
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)
|> assign(:post, assigns.post)
|> assign(:html_content, assigns.html_content)
|> assign(:current_language, assigns.current_language)
|> 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} ->
redirect(conn, to: url)
{:redirect_301, url} ->
conn
|> put_status(301)
|> redirect(to: url)
{:error, reason} ->
handle_not_found(conn, reason)
end
end
# ============================================================================
# Error Handling
# ============================================================================
defp handle_not_found(conn, reason) do
case Fallback.handle_not_found(conn, reason) do
{:redirect_with_flash, path, message} ->
conn
|> put_flash(:info, message)
|> redirect(to: path)
{:render_404} ->
conn
|> put_status(:not_found)
|> put_view(html: PhoenixKitWeb.ErrorHTML)
|> render(:"404")
end
end
# ============================================================================
# Configuration Helpers
# ============================================================================
defp public_enabled? do
Settings.get_boolean_setting("publishing_public_enabled", true)
end
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://") or String.starts_with?(url, "https://"),
do: url,
else: base <> url
end
defp absolute_url(_base, _url), do: nil
end