Packages

phoenix_kit

1.7.78
1.7.208 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 slug_helpers.ex
Raw

lib/modules/publishing/slug_helpers.ex

defmodule PhoenixKit.Modules.Publishing.SlugHelpers do
@moduledoc """
Slug validation and generation for the Publishing module.
Handles slug format validation, uniqueness checking (DB-only),
URL slug validation for per-language slugs, and slug generation.
"""
alias PhoenixKit.Modules.Publishing.DBStorage
alias PhoenixKit.Modules.Publishing.LanguageHelpers
alias PhoenixKit.Modules.Publishing.ListingCache
alias PhoenixKit.Utils.Slug
require Logger
@slug_pattern ~r/^[a-z0-9]+(?:-[a-z0-9]+)*$/
# Reserved route words that cannot be used as URL slugs
@reserved_route_words ~w(admin api assets phoenix_kit auth login logout register settings)
@doc """
Validates whether the given string is a valid slug format and not a reserved language code.
"""
@spec validate_slug(String.t()) ::
{:ok, String.t()} | {:error, :invalid_format | :reserved_language_code}
def validate_slug(slug) when is_binary(slug) do
cond do
not Regex.match?(@slug_pattern, slug) ->
{:error, :invalid_format}
LanguageHelpers.reserved_language_code?(slug) ->
{:error, :reserved_language_code}
true ->
{:ok, slug}
end
end
@doc """
Validates whether the given string is a slug and not a reserved language code.
"""
@spec valid_slug?(String.t()) :: boolean()
def valid_slug?(slug) when is_binary(slug) do
case validate_slug(slug) do
{:ok, _} -> true
{:error, _} -> false
end
end
@doc """
Validates a per-language URL slug for uniqueness within a group+language combination.
"""
@spec validate_url_slug(String.t(), String.t(), String.t(), String.t() | nil) ::
{:ok, String.t()} | {:error, atom()}
def validate_url_slug(group_slug, url_slug, language, exclude_post_slug \\ nil) do
cond do
not Regex.match?(@slug_pattern, url_slug) ->
{:error, :invalid_format}
LanguageHelpers.reserved_language_code?(url_slug) ->
{:error, :reserved_language_code}
url_slug in @reserved_route_words ->
{:error, :reserved_route_word}
conflicts_with_post_slug?(group_slug, url_slug, exclude_post_slug) ->
{:error, :conflicts_with_post_slug}
url_slug_exists?(group_slug, url_slug, language, exclude_post_slug) ->
{:error, :slug_already_exists}
true ->
{:ok, url_slug}
end
end
@doc """
Checks if a slug already exists within the given publishing group (DB-only).
"""
@spec slug_exists?(String.t(), String.t()) :: boolean()
def slug_exists?(group_slug, post_slug) do
case DBStorage.get_post(group_slug, post_slug) do
nil -> false
_post -> true
end
rescue
_ -> false
end
@doc """
Clears custom url_slugs that conflict with a given post slug.
"""
@spec clear_conflicting_url_slugs(String.t(), String.t()) :: [{String.t(), String.t()}]
def clear_conflicting_url_slugs(group_slug, post_slug) do
case ListingCache.read(group_slug) do
{:ok, posts} ->
conflicts = find_conflicting_url_slugs(posts, post_slug)
clear_url_slugs_for_conflicts(group_slug, post_slug, conflicts)
log_cleared_conflicts(conflicts, post_slug)
conflicts
{:error, _} ->
[]
end
end
@doc """
Clears a specific url_slug from all translations of a single post (DB-only).
"""
@spec clear_url_slug_from_post(String.t(), String.t(), String.t()) :: [String.t()]
def clear_url_slug_from_post(group_slug, post_slug, url_slug_to_clear) do
DBStorage.clear_url_slug_from_post(group_slug, post_slug, url_slug_to_clear)
end
@doc """
Generates a unique slug based on title and optional preferred slug.
"""
@spec generate_unique_slug(String.t(), String.t(), String.t() | nil, keyword()) ::
{:ok, String.t()} | {:error, :invalid_format | :reserved_language_code}
def generate_unique_slug(group_slug, title, preferred_slug \\ nil, opts \\ []) do
current_slug = Keyword.get(opts, :current_slug)
base_slug_result =
case preferred_slug do
nil ->
{:ok, Slug.slugify(title)}
slug when is_binary(slug) ->
sanitized = Slug.slugify(slug)
if sanitized == "" do
{:ok, Slug.slugify(title)}
else
case validate_slug(sanitized) do
{:ok, valid_slug} -> {:ok, valid_slug}
{:error, reason} -> {:error, reason}
end
end
end
case base_slug_result do
{:ok, base_slug} when base_slug != "" ->
{:ok,
Slug.ensure_unique(base_slug, fn candidate ->
slug_exists_for_generation?(group_slug, candidate, current_slug)
end)}
{:ok, ""} ->
{:ok,
Slug.ensure_unique("untitled", fn candidate ->
slug_exists_for_generation?(group_slug, candidate, current_slug)
end)}
{:error, reason} ->
{:error, reason}
end
end
# ===========================================================================
# Private Helpers
# ===========================================================================
defp conflicts_with_post_slug?(group_slug, url_slug, exclude_post_slug) do
if url_slug == exclude_post_slug do
false
else
slug_exists?(group_slug, url_slug)
end
end
defp url_slug_exists?(group_slug, url_slug, language, exclude_post_slug) do
case ListingCache.read(group_slug) do
{:ok, posts} ->
Enum.any?(posts, fn post ->
post.slug != exclude_post_slug and
post.slug != url_slug and
Map.get(post.language_slugs || %{}, language) == url_slug
end)
{:error, _} ->
# Check via DBStorage
case DBStorage.find_by_url_slug(group_slug, language, url_slug) do
nil ->
false
content ->
post_slug = content.version.post.slug
post_slug != exclude_post_slug and post_slug != url_slug
end
end
rescue
_ -> false
end
defp slug_exists_for_generation?(_group_slug, candidate, current_slug)
when not is_nil(current_slug) and candidate == current_slug,
do: false
defp slug_exists_for_generation?(group_slug, candidate, _current_slug) do
slug_exists?(group_slug, candidate)
end
defp find_conflicting_url_slugs(posts, post_slug) do
Enum.flat_map(posts, fn post ->
if post.slug == post_slug do
[]
else
(post.language_slugs || %{})
|> Enum.filter(fn {_lang, url_slug} -> url_slug == post_slug end)
|> Enum.map(fn {lang, _} -> {post.slug, lang} end)
end
end)
end
defp clear_url_slugs_for_conflicts(group_slug, slug_to_clear, conflicts) do
Enum.each(conflicts, fn {conflicting_post_slug, _language} ->
DBStorage.clear_url_slug_from_post(group_slug, conflicting_post_slug, slug_to_clear)
end)
end
defp log_cleared_conflicts([], _post_slug), do: :ok
defp log_cleared_conflicts(conflicts, post_slug) do
Logger.warning(
"[Slugs] Cleared conflicting url_slugs for post slug '#{post_slug}': #{inspect(conflicts)}"
)
end
end