Packages

phoenix_kit

1.7.113
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 sitemap sources static.ex
Raw

lib/modules/sitemap/sources/static.ex

defmodule PhoenixKit.Modules.Sitemap.Sources.Static do
@moduledoc """
Static routes source for sitemap generation.
Collects configurable static routes for the sitemap. Routes are configured
through Settings and resolved via RouteResolver - NO hardcoded fallbacks.
## Settings
- `sitemap_static_routes` - JSON array of route configurations
- `sitemap_custom_urls` - JSON array of custom URL entries
## Route Configuration Format
Each route in `sitemap_static_routes` can have:
%{
"plug" => "PhoenixKitWeb.Users.Registration", # Module to resolve via RouteResolver
"path" => "/custom/path", # OR explicit path (overrides plug)
"priority" => 0.7, # Sitemap priority (0.0-1.0)
"changefreq" => "monthly", # Change frequency
"title" => "Register", # Display title
"category" => "Authentication", # Category for grouping
"prefixed" => true # Use PhoenixKit URL prefix
}
## Custom URL Format
Each entry in `sitemap_custom_urls`:
%{
"path" => "/about-us",
"priority" => 0.8,
"changefreq" => "monthly",
"title" => "About Us",
"category" => "Company"
}
## Default Configuration
By default, includes:
- Homepage (/) - Priority: 0.9, daily
- Registration page - Priority: 0.7, monthly (if route exists)
- Login page - Priority: 0.7, monthly (if route exists)
## No Hardcoded Fallbacks
If RouteResolver cannot find a route and no explicit path is configured,
the route is skipped. This ensures sitemap only contains valid URLs.
"""
@behaviour PhoenixKit.Modules.Sitemap.Sources.Source
alias PhoenixKit.Modules.Languages
alias PhoenixKit.Modules.Sitemap.RouteResolver
alias PhoenixKit.Modules.Sitemap.UrlEntry
alias PhoenixKit.Settings
alias PhoenixKit.Utils.Routes
# Login is ALWAYS excluded from sitemap (auth pages shouldn't be indexed).
# Registration is conditional via sitemap_include_registration setting.
@default_static_routes [
%{
"path" => "/",
"priority" => 0.9,
"changefreq" => "daily",
"title" => "Home",
"category" => "Main",
"prefixed" => false,
# Don't add language prefix to homepage - it usually doesn't have localized route
"skip_language_prefix" => true
}
]
@registration_route %{
"path" => "/users/register",
"priority" => 0.7,
"changefreq" => "monthly",
"title" => "Register",
"category" => "Authentication",
"prefixed" => true
}
@impl true
def source_name, do: :static
@impl true
def enabled?, do: true
@impl true
def sitemap_filename, do: "sitemap-static"
@impl true
def collect(opts \\ []) do
is_default = Keyword.get(opts, :is_default_language, true)
# Static pages only generate URLs for the default language
# Non-default language URLs would lead to 404 errors
if is_default do
base_url = Keyword.get(opts, :base_url)
language = Keyword.get(opts, :language)
static_entries = collect_static_routes(base_url, language, is_default)
custom_entries = collect_custom_urls(base_url, language, is_default)
(static_entries ++ custom_entries)
|> Enum.reject(&is_nil/1)
else
[]
end
rescue
error ->
require Logger
Logger.warning("Static routes sitemap source failed to collect: #{inspect(error)}")
[]
end
defp collect_static_routes(base_url, language, is_default) do
get_static_routes_config()
|> Enum.map(&build_static_entry(&1, base_url, language, is_default))
end
defp collect_custom_urls(base_url, language, is_default) do
get_custom_urls_config()
|> Enum.map(&build_custom_entry(&1, base_url, language, is_default))
end
defp get_static_routes_config do
base_routes =
case Settings.get_setting("sitemap_static_routes") do
nil ->
@default_static_routes
json_string when is_binary(json_string) ->
case Jason.decode(json_string) do
{:ok, routes} when is_list(routes) -> routes
_ -> @default_static_routes
end
routes when is_list(routes) ->
routes
_ ->
@default_static_routes
end
# Conditionally include registration page
if include_registration?() do
base_routes ++ [@registration_route]
else
base_routes
end
end
defp include_registration? do
Settings.get_boolean_setting("sitemap_include_registration", false)
rescue
_ -> false
end
defp get_custom_urls_config do
case Settings.get_setting("sitemap_custom_urls") do
nil ->
[]
json_string when is_binary(json_string) ->
case Jason.decode(json_string) do
{:ok, urls} when is_list(urls) -> urls
_ -> []
end
urls when is_list(urls) ->
urls
_ ->
[]
end
end
defp build_static_entry(config, base_url, language, is_default) do
path = resolve_path(config)
if path do
prefixed = Map.get(config, "prefixed", false)
skip_language = Map.get(config, "skip_language_prefix", false)
# Canonical path without language prefix (for hreflang grouping)
canonical_path = if prefixed, do: Routes.path(path), else: path
# Build localized path (skip language prefix if configured)
localized_path =
if skip_language do
canonical_path
else
build_path_with_language(canonical_path, language, is_default)
end
url = build_url_from_localized_path(localized_path, base_url, prefixed)
UrlEntry.new(%{
loc: url,
lastmod: static_lastmod(path),
changefreq: Map.get(config, "changefreq", "weekly"),
priority: Map.get(config, "priority", 0.5),
title: Map.get(config, "title", path),
category: Map.get(config, "category", "Static"),
source: :static,
canonical_path: canonical_path
})
else
# Route not found and no explicit path - skip
nil
end
end
defp build_custom_entry(config, base_url, language, is_default) do
path = Map.get(config, "path")
if path do
# Canonical path without language prefix (for hreflang grouping)
canonical_path = path
localized_path = build_path_with_language(path, language, is_default)
url = build_url_from_localized_path(localized_path, base_url, false)
UrlEntry.new(%{
loc: url,
lastmod: static_lastmod(path),
changefreq: Map.get(config, "changefreq", "weekly"),
priority: Map.get(config, "priority", 0.5),
title: Map.get(config, "title", path),
category: Map.get(config, "category", "Custom"),
source: :static,
canonical_path: canonical_path
})
else
nil
end
end
# For the homepage, use the newest published-content date across all
# publishing groups so crawlers see an honest "site was updated when its
# newest post landed" signal. Other static pages have no associated
# content date, so today's date is the best approximation.
defp static_lastmod("/") do
alias PhoenixKit.Modules.Sitemap.Sources.Publishing
if Code.ensure_loaded?(Publishing) and
function_exported?(Publishing, :latest_post_date_global, 0) do
Publishing.latest_post_date_global() || Date.utc_today()
else
Date.utc_today()
end
rescue
_ -> Date.utc_today()
end
defp static_lastmod(_path), do: Date.utc_today()
# Resolve path from config: explicit path OR via RouteResolver
defp resolve_path(%{"path" => path}) when is_binary(path) and path != "" do
path
end
defp resolve_path(%{"plug" => plug_string}) when is_binary(plug_string) do
# Try to resolve module via RouteResolver
module = String.to_existing_atom("Elixir." <> plug_string)
RouteResolver.find_route(module)
rescue
# Module doesn't exist - return nil (no hardcoded fallback!)
_ -> nil
end
defp resolve_path(_), do: nil
# Build URL from already localized path
defp build_url_from_localized_path(path, nil, _prefixed) do
base = Settings.get_setting("site_url", "")
normalized_base = String.trim_trailing(base, "/")
"#{normalized_base}#{path}"
end
defp build_url_from_localized_path(path, base_url, _prefixed) when is_binary(base_url) do
normalized_base = String.trim_trailing(base_url, "/")
"#{normalized_base}#{path}"
end
# Add language prefix to path when in multi-language mode
# Single language: no prefix for anyone
# Multiple languages: ALL languages get prefix (including default)
defp build_path_with_language(path, language, _is_default) do
if language && !single_language_mode?() do
"/#{Languages.DialectMapper.extract_base(language)}#{path}"
else
path
end
end
# Check if we're in single language mode (no locale prefix needed)
# Returns true when languages module is off OR only one language is enabled
# Mirrors PublishingHTML.single_language_mode?/0 logic
defp single_language_mode? do
not Languages.enabled?() or length(Languages.get_enabled_languages()) <= 1
rescue
_ -> true
end
end