Packages

phoenix_kit

1.7.49
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 url_entry.ex
Raw

lib/modules/sitemap/url_entry.ex

defmodule PhoenixKit.Modules.Sitemap.UrlEntry do
@moduledoc """
Struct representing a single URL entry in sitemap.
Used for both XML and HTML sitemap generation. Contains all necessary
metadata for proper sitemap formatting according to sitemaps.org protocol.
## Fields
- `loc` - Full URL (required)
- `lastmod` - Last modification date/time
- `changefreq` - Change frequency hint (weekly, daily, monthly, etc.)
- `priority` - Priority value 0.0-1.0
- `title` - Display title for HTML sitemap
- `category` - Category/group for organizing HTML sitemap
- `source` - Source module that generated this entry (:entities, :publishing, etc.)
- `alternates` - List of alternate language versions for hreflang (optional)
- `canonical_path` - Canonical path without language prefix (for grouping alternates)
## Alternates Format
Each alternate is a map with:
- `hreflang` - Language code (e.g., "en", "et", "x-default")
- `href` - Full URL for that language version
## Usage
entry = UrlEntry.new(%{
loc: "https://example.com/blog/my-post",
lastmod: ~U[2025-01-15 10:00:00Z],
changefreq: "weekly",
priority: 0.8,
title: "My Blog Post",
category: "Blog",
source: :publishing,
alternates: [
%{hreflang: "en", href: "https://example.com/blog/my-post"},
%{hreflang: "et", href: "https://example.com/et/blog/my-post"},
%{hreflang: "x-default", href: "https://example.com/blog/my-post"}
]
})
xml = UrlEntry.to_xml(entry)
"""
@type alternate :: %{hreflang: String.t(), href: String.t()}
@type t :: %__MODULE__{
loc: String.t(),
lastmod: DateTime.t() | Date.t() | NaiveDateTime.t() | nil,
changefreq: String.t() | nil,
priority: float() | String.t() | nil,
title: String.t() | nil,
category: String.t() | nil,
source: atom(),
alternates: [alternate()] | nil,
canonical_path: String.t() | nil
}
defstruct [
:loc,
:lastmod,
:changefreq,
:priority,
:title,
:category,
:source,
:alternates,
:canonical_path
]
@valid_changefreq ~w(always hourly daily weekly monthly yearly never)
@doc """
Creates a new UrlEntry struct from attributes.
## Examples
iex> UrlEntry.new(%{loc: "https://example.com/page"})
%UrlEntry{loc: "https://example.com/page"}
iex> UrlEntry.new(%{loc: "https://example.com", priority: 0.8, changefreq: "weekly"})
%UrlEntry{loc: "https://example.com", priority: 0.8, changefreq: "weekly"}
"""
@spec new(map()) :: t()
def new(attrs) when is_map(attrs) do
struct(__MODULE__, attrs)
end
@doc """
Converts a UrlEntry to XML format for sitemap.
Supports hreflang alternate links via xhtml:link elements when `alternates` is set.
## Examples
iex> entry = UrlEntry.new(%{loc: "https://example.com", lastmod: ~D[2025-01-15]})
iex> UrlEntry.to_xml(entry)
"<url>\\n <loc>https://example.com</loc>\\n <lastmod>2025-01-15</lastmod>\\n</url>"
iex> entry = UrlEntry.new(%{
...> loc: "https://example.com/page",
...> alternates: [
...> %{hreflang: "en", href: "https://example.com/page"},
...> %{hreflang: "et", href: "https://example.com/et/page"}
...> ]
...> })
iex> UrlEntry.to_xml(entry) |> String.contains?("xhtml:link")
true
"""
@spec to_xml(t()) :: String.t()
def to_xml(%__MODULE__{} = entry) do
parts = [
" <loc>#{escape_xml(entry.loc)}</loc>"
]
parts =
if entry.lastmod do
parts ++ [" <lastmod>#{format_date(entry.lastmod)}</lastmod>"]
else
parts
end
parts =
if entry.changefreq && entry.changefreq in @valid_changefreq do
parts ++ [" <changefreq>#{entry.changefreq}</changefreq>"]
else
parts
end
parts =
if entry.priority do
priority_value = normalize_priority(entry.priority)
parts ++ [" <priority>#{priority_value}</priority>"]
else
parts
end
# Add hreflang alternate links if present
parts =
if entry.alternates && not Enum.empty?(entry.alternates) do
alternate_links =
Enum.map(entry.alternates, fn alt ->
~s( <xhtml:link rel="alternate" hreflang="#{alt.hreflang}" href="#{escape_xml(alt.href)}"/>)
end)
parts ++ alternate_links
else
parts
end
"<url>\n#{Enum.join(parts, "\n")}\n</url>"
end
@doc """
Formats date/datetime to ISO8601 string for lastmod element.
"""
@spec format_date(DateTime.t() | Date.t() | NaiveDateTime.t() | nil) :: String.t() | nil
def format_date(nil), do: nil
def format_date(%DateTime{} = dt), do: DateTime.to_iso8601(dt)
def format_date(%NaiveDateTime{} = ndt), do: NaiveDateTime.to_iso8601(ndt)
def format_date(%Date{} = d), do: Date.to_iso8601(d)
def format_date(other) when is_binary(other) do
# Already a string, return as-is
other
end
def format_date(_), do: nil
@doc """
Normalizes priority value to a float between 0.0 and 1.0.
"""
@spec normalize_priority(float() | String.t() | nil) :: float()
def normalize_priority(nil), do: 0.5
def normalize_priority(priority) when is_float(priority) do
priority
|> max(0.0)
|> min(1.0)
|> Float.round(1)
end
def normalize_priority(priority) when is_binary(priority) do
case Float.parse(priority) do
{value, _} -> normalize_priority(value)
:error -> 0.5
end
end
def normalize_priority(priority) when is_integer(priority) do
normalize_priority(priority / 1.0)
end
def normalize_priority(_), do: 0.5
@doc """
Escapes XML special characters in a string.
"""
@spec escape_xml(String.t()) :: String.t()
def escape_xml(nil), do: ""
def escape_xml(str) when is_binary(str) do
str
|> String.replace("&", "&amp;")
|> String.replace("<", "&lt;")
|> String.replace(">", "&gt;")
|> String.replace("\"", "&quot;")
|> String.replace("'", "&apos;")
end
@doc """
Parses priority from various formats.
## Examples
iex> UrlEntry.parse_priority("0.8")
0.8
iex> UrlEntry.parse_priority(0.7)
0.7
iex> UrlEntry.parse_priority(nil)
nil
"""
@spec parse_priority(String.t() | float() | nil) :: float() | nil
def parse_priority(nil), do: nil
def parse_priority(priority) when is_float(priority), do: priority
def parse_priority(priority) when is_binary(priority) do
case Float.parse(priority) do
{value, _} -> value
:error -> nil
end
end
def parse_priority(_), do: nil
end