Packages
phoenix_kit
1.7.52
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
Current section
Files
lib/modules/publishing/web/controller/listing.ex
defmodule PhoenixKit.Modules.Publishing.Web.Controller.Listing do
@moduledoc """
Group listing functionality for the publishing controller.
Handles rendering post listings with:
- Language filtering and fallback
- Pagination
- Translation link building for listings
"""
alias PhoenixKit.Modules.Languages.DialectMapper
alias PhoenixKit.Modules.Publishing
alias PhoenixKit.Modules.Publishing.Web.Controller.Language
alias PhoenixKit.Modules.Publishing.Web.Controller.PostFetching
alias PhoenixKit.Modules.Publishing.Web.Controller.Translations
alias PhoenixKit.Modules.Publishing.Web.HTML, as: PublishingHTML
alias PhoenixKit.Settings
# ============================================================================
# Group Listing Rendering
# ============================================================================
@doc """
Renders a group listing page.
"""
def render_group_listing(conn, group_slug, language, params) do
case fetch_group(group_slug) do
{:ok, group} ->
# Only preserve pagination params for redirects
pagination_params = Map.take(params, ["page"])
# Check if we need to redirect to canonical URL
canonical_language = Language.get_canonical_url_language(language)
if canonical_language != language do
# Redirect to canonical URL
canonical_url =
PublishingHTML.group_listing_path(canonical_language, group_slug, pagination_params)
{:redirect, canonical_url}
else
page = get_page_param(params)
per_page = get_per_page_setting()
# Try cache first, fall back to filesystem scan
all_posts_unfiltered = PostFetching.fetch_posts_with_cache(group_slug)
published_posts = filter_published(all_posts_unfiltered)
# Resolve posts for the requested language, with fallback handling
listing_context = %{
group: group,
group_slug: group_slug,
language: language,
canonical_language: canonical_language,
published_posts: published_posts,
all_posts_unfiltered: all_posts_unfiltered,
page: page,
per_page: per_page,
pagination_params: pagination_params
}
resolve_listing_posts_for_language(conn, listing_context)
end
{:error, reason} ->
{:error, reason}
end
end
# ============================================================================
# Language Resolution for Listings
# ============================================================================
@doc """
Resolves posts for the requested language, handling exact match vs fallback.
"""
def resolve_listing_posts_for_language(conn, ctx) do
exact_language_posts = filter_by_exact_language_strict(ctx.published_posts, ctx.language)
case resolve_language_posts(
exact_language_posts,
ctx.published_posts,
ctx.group_slug,
ctx.language
) do
{:exact, posts} ->
render_group_index(conn, ctx, posts)
{:fallback, fallback_language} ->
fallback_url =
PublishingHTML.group_listing_path(
fallback_language,
ctx.group_slug,
ctx.pagination_params
)
{:redirect, fallback_url}
:not_found ->
{:error, :no_content_for_language}
end
end
# Returns {:exact, posts}, {:fallback, language}, or :not_found
defp resolve_language_posts(exact_posts, _published_posts, _group_slug, _language)
when exact_posts != [] do
{:exact, exact_posts}
end
defp resolve_language_posts([], published_posts, group_slug, language) do
fallback_posts = filter_by_exact_language(published_posts, group_slug, language)
if fallback_posts != [] do
{:fallback, get_fallback_language(language, fallback_posts)}
else
:not_found
end
end
# ============================================================================
# Group Index Rendering
# ============================================================================
@doc """
Renders the group index page with resolved posts.
"""
def render_group_index(_conn, ctx, all_posts) do
total_count = length(all_posts)
posts =
all_posts
|> paginate(ctx.page, ctx.per_page)
|> resolve_posts_for_language(ctx.canonical_language)
breadcrumbs = [%{label: ctx.group["name"], url: nil}]
translations =
Translations.build_listing_translations(
ctx.group_slug,
ctx.canonical_language,
ctx.all_posts_unfiltered
)
{:ok,
%{
page_title: ctx.group["name"],
group: ctx.group,
posts: posts,
current_language: ctx.canonical_language,
translations: translations,
page: ctx.page,
per_page: ctx.per_page,
total_count: total_count,
total_pages: ceil(total_count / ctx.per_page),
breadcrumbs: breadcrumbs
}}
end
# ============================================================================
# Per-Language Resolution
# ============================================================================
# Resolves listing post metadata (title, excerpt) for the requested language.
# DB-mode listing maps carry `language_titles` and `language_excerpts` maps
# so the template shows the correct translation, not just the primary language.
defp resolve_posts_for_language(posts, language) do
Enum.map(posts, fn post ->
resolve_post_for_language(post, language)
end)
end
defp resolve_post_for_language(post, language) do
lang_titles = post[:language_titles] || %{}
lang_excerpts = post[:language_excerpts] || %{}
title = Map.get(lang_titles, language, post.metadata.title)
excerpt = Map.get(lang_excerpts, language)
post
|> put_in([:metadata, :title], title)
|> then(fn p ->
if excerpt, do: Map.put(p, :content, excerpt), else: p
end)
end
# ============================================================================
# Filtering Functions
# ============================================================================
@doc """
Filters posts to only include published ones.
"""
def filter_published(posts) do
Enum.filter(posts, fn post ->
post.metadata.status == "published"
end)
end
@doc """
Filter posts to only include those that have a matching language file.
Handles both exact matches and base code matches (e.g., "en" matches "en-US").
Uses preloaded language_statuses to avoid redundant file reads.
"""
def filter_by_exact_language(posts, _group_slug, language) do
Enum.filter(posts, fn post ->
# Find the matching language file (exact or base code match)
matching_language = find_matching_language(language, post.available_languages)
# Use preloaded status from language_statuses map
matching_language != nil and
Map.get(post.language_statuses, matching_language) == "published"
end)
end
@doc """
Strict version - only matches exact language, no fallback to base code.
"""
def filter_by_exact_language_strict(posts, language) do
Enum.filter(posts, fn post ->
language in post.available_languages and
Map.get(post.language_statuses, language) == "published"
end)
end
@doc """
Find a matching language in available languages.
Handles exact matches and base code matching.
"""
def find_matching_language(language, available_languages) do
cond do
# Direct match
language in available_languages ->
language
# Base code - find a dialect that matches
Language.base_code?(language) ->
Language.find_dialect_for_base_in_files(language, available_languages)
# Full dialect not found - try base code match
true ->
base = DialectMapper.extract_base(language)
Language.find_dialect_for_base_in_files(base, available_languages)
end
end
@doc """
Get the actual language that the fallback matched.
Used to redirect to the correct URL when requested language has no content.
"""
def get_fallback_language(requested_language, posts) do
# Look at the first post to find what language actually matched
case posts do
[first_post | _] ->
find_matching_language(requested_language, first_post.available_languages) ||
requested_language
[] ->
requested_language
end
end
# ============================================================================
# Pagination
# ============================================================================
@doc """
Paginates a list of posts.
"""
def paginate(posts, page, per_page) do
posts
|> Enum.drop((page - 1) * per_page)
|> Enum.take(per_page)
end
@doc """
Gets the page number from params.
"""
def get_page_param(params) do
case Map.get(params, "page", "1") do
page when is_binary(page) ->
case Integer.parse(page) do
{num, _} when num > 0 -> num
_ -> 1
end
page when is_integer(page) and page > 0 ->
page
_ ->
1
end
end
@doc """
Gets the posts per page setting.
"""
def get_per_page_setting do
# Check new key first, fallback to legacy
value =
case Settings.get_setting_cached("publishing_posts_per_page") do
nil -> Settings.get_setting_cached("blogging_posts_per_page")
v -> v
end
case value do
nil ->
20
v when is_binary(v) ->
case Integer.parse(v) do
{num, _} when num > 0 -> num
_ -> 20
end
v when is_integer(v) and v > 0 ->
v
_ ->
20
end
end
# ============================================================================
# Helper Functions
# ============================================================================
@doc """
Fetches group configuration by slug.
"""
def fetch_group(group_slug) do
group_slug = group_slug |> to_string() |> String.trim()
case Enum.find(Publishing.list_groups(), fn group ->
case group["slug"] do
slug when is_binary(slug) ->
String.downcase(slug) == String.downcase(group_slug)
_ ->
false
end
end) do
nil -> {:error, :group_not_found}
group -> {:ok, group}
end
end
@doc """
Gets the default group listing path for a language.
"""
def default_group_listing(language) do
case Publishing.list_groups() do
[%{"slug" => slug} | _] -> PublishingHTML.group_listing_path(language, slug)
_ -> nil
end
end
end