Packages

phoenix_kit

1.7.42
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 fallback.ex
Raw

lib/modules/publishing/web/controller/fallback.ex

defmodule PhoenixKit.Modules.Publishing.Web.Controller.Fallback do
@moduledoc """
404 fallback handling for the publishing controller.
Implements a smart fallback chain that attempts to redirect users
to related content when the requested resource is not found:
- Posts in other languages
- Other posts on the same date
- Blog listing page
"""
use Gettext, backend: PhoenixKitWeb.Gettext
alias PhoenixKit.Modules.Publishing
alias PhoenixKit.Modules.Publishing.Storage
alias PhoenixKit.Modules.Publishing.Web.Controller.Language
alias PhoenixKit.Modules.Publishing.Web.Controller.Listing
alias PhoenixKit.Modules.Publishing.Web.Controller.PostFetching
alias PhoenixKit.Modules.Publishing.Web.HTML, as: PublishingHTML
# ============================================================================
# Main Entry Point
# ============================================================================
@doc """
Handles 404 not found responses with smart fallback.
"""
def handle_not_found(conn, reason) do
# Try to fall back to nearest valid parent in the breadcrumb chain
case attempt_breadcrumb_fallback(conn, reason) do
{:ok, redirect_path} ->
{:redirect_with_flash, redirect_path,
gettext("The page you requested was not found. Showing closest match.")}
:no_fallback ->
{:render_404}
end
end
# ============================================================================
# Breadcrumb Fallback Logic
# ============================================================================
defp attempt_breadcrumb_fallback(conn, reason) do
language = conn.assigns[:current_language] || "en"
blog_slug = conn.params["blog"]
path = conn.params["path"] || []
# Build full path including blog slug for proper fallback handling
# Route params are: %{"blog" => "date", "path" => ["2025-12-09", "15:02"]}
# We need: ["date", "2025-12-09", "15:02"] for pattern matching
full_path = if blog_slug, do: [blog_slug | path], else: path
handle_fallback_case(reason, full_path, language)
end
# ============================================================================
# Fallback Case Handlers
# ============================================================================
# Slug mode posts (2-element path) - try other languages before blog listing
defp handle_fallback_case(reason, [blog_slug, post_slug], language)
when reason in [:post_not_found, :unpublished] do
fallback_to_default_language(blog_slug, post_slug, language)
end
# Timestamp mode posts (3-element path) - try other languages before blog listing
defp handle_fallback_case(reason, [blog_slug, date, time], language)
when reason in [:post_not_found, :unpublished] do
fallback_timestamp_to_other_language(blog_slug, date, time, language)
end
defp handle_fallback_case(:blog_not_found, [_blog_slug], language) do
fallback_to_default_blog(language)
end
defp handle_fallback_case(:blog_not_found, [], language) do
fallback_to_default_blog(language)
end
defp handle_fallback_case(_reason, _path, _language), do: :no_fallback
# ============================================================================
# Slug Mode Fallback
# ============================================================================
defp fallback_to_default_language(blog_slug, post_slug, requested_language) do
if blog_exists?(blog_slug) do
find_any_available_language_version(blog_slug, post_slug, requested_language)
else
:no_fallback
end
end
@doc """
Tries to find any available published language version of the post.
Priority:
1. Check for published versions in the SAME language first (across all versions)
2. Then try other languages
3. Falls back to blog listing if no published versions exist
Note: fetch_post now handles finding the latest published version automatically,
so we can just use base URLs here (no version-specific URLs needed)
"""
def find_any_available_language_version(blog_slug, post_slug, requested_language) do
default_lang = Language.get_default_language()
# Find the post in the blog to get available languages
case find_post_by_slug(blog_slug, post_slug) do
{:ok, post} ->
# The initial fetch failed, so we know no published version exists for the requested_language.
# Proceed directly to trying other available languages.
try_other_languages(blog_slug, post_slug, post, requested_language, default_lang)
:not_found ->
# Post doesn't exist at all - fall back to blog listing
{:ok, PublishingHTML.blog_listing_path(default_lang, blog_slug)}
end
end
# Finds the latest published version for a specific language
defp find_published_version_for_language(blog_slug, post_slug, language) do
versions = Storage.list_versions(blog_slug, post_slug)
published_version =
versions
|> Enum.sort(:desc)
|> Enum.find(fn version ->
Storage.get_version_status(blog_slug, post_slug, version, language) == "published"
end)
case published_version do
nil -> :not_found
version -> {:ok, version}
end
end
# Tries other languages when requested language has no published versions
defp try_other_languages(blog_slug, post_slug, post, requested_language, default_lang) do
available = post.available_languages || []
# Build priority list: default first, then others (excluding already-tried language)
languages_to_try =
([default_lang | available] -- [requested_language])
|> Enum.uniq()
find_first_published_version(blog_slug, post_slug, post, languages_to_try, default_lang)
end
# Finds a post by its slug from the blog's post list
defp find_post_by_slug(blog_slug, post_slug) do
posts = Publishing.list_posts(blog_slug, nil)
case Enum.find(posts, fn p -> p.slug == post_slug end) do
nil -> :not_found
post -> {:ok, post}
end
end
# Tries each language in order until finding a published version
# Uses find_published_version_for_language to check across all versions
# fetch_post will automatically find the right version when the URL is visited
defp find_first_published_version(blog_slug, post_slug, post, languages, fallback_lang) do
result =
Enum.find_value(languages, fn lang ->
# Check if any published version exists for this language
case find_published_version_for_language(blog_slug, post_slug, lang) do
{:ok, _version} ->
# Published version exists - use base URL
# fetch_post will find the right version
{:ok, PublishingHTML.build_post_url(blog_slug, post, lang)}
:not_found ->
nil
end
end)
# If no published version found, fall back to blog listing
result || {:ok, PublishingHTML.blog_listing_path(fallback_lang, blog_slug)}
end
# ============================================================================
# Timestamp Mode Fallback
# ============================================================================
@doc """
Fallback for timestamp mode posts - comprehensive fallback chain:
1. Try other languages for the exact date/time
2. If time doesn't exist, try other times on the same date
3. If date has no posts, fall back to blog listing
"""
def fallback_timestamp_to_other_language(blog_slug, date, time, requested_language) do
default_lang = Language.get_default_language()
if blog_exists?(blog_slug) do
# Step 1: Try other languages for this exact time
post_dir = Path.join([Storage.group_path(blog_slug), date, time])
# Use version-aware language detection (handles both versioned and legacy)
available = PostFetching.detect_available_languages_in_timestamp_dir(post_dir)
# Time exists with language files - try other languages
if available != [] do
languages_to_try =
([default_lang | available] -- [requested_language])
|> Enum.uniq()
case find_first_published_timestamp_version(blog_slug, date, time, languages_to_try) do
{:ok, url} ->
{:ok, url}
:not_found ->
# No published version at this time - try other times on this date
fallback_to_other_time_on_date(blog_slug, date, time, default_lang)
end
else
# Time doesn't exist - try other times on this date
fallback_to_other_time_on_date(blog_slug, date, time, default_lang)
end
else
:no_fallback
end
end
# Fallback to another time on the same date
defp fallback_to_other_time_on_date(blog_slug, date, exclude_time, default_lang) do
case Storage.list_times_on_date(blog_slug, date) do
[] ->
# No posts on this date at all - try other dates or fall back to blog listing
fallback_to_other_date(blog_slug, default_lang)
times ->
# Filter out the time we already tried
other_times = times -- [exclude_time]
case find_first_published_time(blog_slug, date, other_times, default_lang) do
{:ok, url} ->
{:ok, url}
:not_found ->
# No published posts on this date - try other dates
fallback_to_other_date(blog_slug, default_lang)
end
end
end
# No posts found on this date - fall back to blog listing
# The blog listing will show all available posts
defp fallback_to_other_date(blog_slug, default_lang) do
{:ok, PublishingHTML.blog_listing_path(default_lang, blog_slug)}
end
# Find the first published post at any of the given times
defp find_first_published_time(blog_slug, date, times, preferred_lang) do
Enum.find_value(times, fn time ->
post_dir = Path.join([Storage.group_path(blog_slug), date, time])
# Use version-aware language detection (handles both versioned and legacy)
available = PostFetching.detect_available_languages_in_timestamp_dir(post_dir)
if available != [] do
# Try preferred language first, then others
languages = [preferred_lang | available] |> Enum.uniq()
case find_first_published_timestamp_version(blog_slug, date, time, languages) do
{:ok, url} -> {:ok, url}
:not_found -> nil
end
else
nil
end
end) || :not_found
end
@doc """
Tries each language for timestamp mode until finding a published version.
Handles both versioned and legacy structures.
"""
def find_first_published_timestamp_version(blog_slug, date, time, languages) do
post_dir = Path.join([Storage.group_path(blog_slug), date, time])
case Storage.detect_post_structure(post_dir) do
:versioned ->
find_first_published_versioned_timestamp(blog_slug, date, time, languages, post_dir)
:legacy ->
find_first_published_legacy_timestamp(blog_slug, date, time, languages)
:empty ->
:not_found
end
end
# Find first published post in versioned timestamp structure
# Iterates versions from highest to lowest, then tries each language
defp find_first_published_versioned_timestamp(blog_slug, date, time, languages, post_dir) do
versions = PostFetching.list_timestamp_versions(post_dir) |> Enum.sort(:desc)
Enum.find_value(versions, fn version ->
version_dir = Path.join(post_dir, "v#{version}")
available_languages = PostFetching.detect_available_languages_in_dir(version_dir)
# Try preferred languages first, then fall back to what's available
languages_to_try =
(languages ++ available_languages)
|> Enum.uniq()
|> Enum.filter(&(&1 in available_languages))
Enum.find_value(languages_to_try, fn lang ->
path = "#{blog_slug}/#{date}/#{time}/v#{version}/#{lang}.phk"
case Publishing.read_post(blog_slug, path) do
{:ok, post} when post.metadata.status == "published" ->
{:ok, build_timestamp_url(blog_slug, date, time, lang)}
_ ->
nil
end
end)
end) || :not_found
end
# Find first published post in legacy timestamp structure
defp find_first_published_legacy_timestamp(blog_slug, date, time, languages) do
Enum.find_value(languages, fn lang ->
path = "#{blog_slug}/#{date}/#{time}/#{lang}.phk"
case Publishing.read_post(blog_slug, path) do
{:ok, post} when post.metadata.status == "published" ->
{:ok, build_timestamp_url(blog_slug, date, time, lang)}
_ ->
nil
end
end) || :not_found
end
# ============================================================================
# Blog Fallback
# ============================================================================
defp fallback_to_default_blog(language) do
case Listing.default_blog_listing(language) do
nil -> :no_fallback
path -> {:ok, path}
end
end
# ============================================================================
# Helper Functions
# ============================================================================
defp blog_exists?(blog_slug) do
case Listing.fetch_blog(blog_slug) do
{:ok, _} -> true
_ -> false
end
end
defp build_timestamp_url(blog_slug, date, time, language) do
PublishingHTML.build_public_path_with_time(language, blog_slug, date, time)
end
end