Packages
phoenix_kit
1.7.144
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
Current section
Files
lib/modules/sitemap/sources/posts.ex
defmodule PhoenixKit.Modules.Sitemap.Sources.Posts do
@moduledoc """
Posts source for sitemap generation.
Collects public posts from the PhoenixKit Posts system.
Automatically detects if public routes exist for posts.
## URL Structure
- Posts index: `/posts` (or with language prefix for non-default: `/et/posts`)
- Individual posts: `/posts/:slug` (or `/et/posts/:slug`)
## Enabling
This source is enabled when:
1. Posts module is enabled (`posts_enabled` setting)
2. Public routes exist for posts in the parent router
3. Routes do NOT require authentication (checked via on_mount hooks)
Routes with the following on_mount hooks are considered private and excluded:
- `:phoenix_kit_ensure_authenticated_scope` - requires logged-in user
- `:phoenix_kit_ensure_admin` - requires admin access
- `:ensure_authenticated` - generic authentication requirement
- `:require_authenticated_user` - requires authenticated user
## Exclusion
Posts can be excluded by setting `post.metadata["sitemap_exclude"] = true`.
## Sitemap Properties
- Posts index:
- Priority: 0.7
- Change frequency: daily
- Category: "Posts"
- Individual posts:
- Priority: 0.8
- Change frequency: weekly
- Category: "Posts"
- Last modified: Post's updated_at or published_at
"""
@behaviour PhoenixKit.Modules.Sitemap.Sources.Source
require Logger
alias PhoenixKit.Modules.Languages
alias PhoenixKit.Modules.Sitemap.LocalePath
# PhoenixKitPosts is an optional external package
# All calls are guarded with Code.ensure_loaded?/1
alias PhoenixKit.Modules.Sitemap.RouteResolver
alias PhoenixKit.Modules.Sitemap.UrlEntry
alias PhoenixKit.Settings
@impl true
def source_name, do: :posts
@impl true
def sitemap_filename, do: "sitemap-posts"
@impl true
def enabled? do
# Only enabled if PhoenixKitPosts package is installed and posts module is enabled
Code.ensure_loaded?(PhoenixKitPosts) and posts_module_enabled?()
rescue
_ -> false
end
@impl true
def collect(opts \\ []) do
is_default = Keyword.get(opts, :is_default_language, true)
# Posts only generate URLs for the default language
# Non-default language URLs would lead to 404 errors
if enabled?() and is_default do
do_collect(opts)
else
[]
end
rescue
error ->
Logger.warning("Posts sitemap source failed to collect: #{inspect(error)}")
[]
end
defp posts_module_enabled? do
Settings.get_setting_cached("posts_enabled", "true") == "true"
rescue
_ -> false
end
defp do_collect(opts) do
# Optimization: Get routes ONCE for all checks
routes = RouteResolver.get_routes()
cond do
# Early exit if no public posts routes exist
not has_public_posts_route?(routes) ->
Logger.debug("Sitemap: No public posts routes found, skipping posts source")
[]
# Early exit if routes require authentication
posts_route_requires_auth?(routes) ->
Logger.debug("Sitemap: Posts routes require authentication, skipping")
[]
# Proceed with collection
true ->
do_collect_posts(opts)
end
end
defp do_collect_posts(opts) do
base_url = Keyword.get(opts, :base_url)
language = Keyword.get(opts, :language)
is_default = Keyword.get(opts, :is_default_language, true)
index_entry = build_index_entry(base_url, language, is_default)
post_entries = collect_posts(base_url, language, is_default)
[index_entry | post_entries]
|> Enum.reject(&is_nil/1)
end
# Check if posts route exists in cached routes
defp has_public_posts_route?(routes) do
find_posts_content_route(routes) != nil
end
# Check if posts route requires auth using cached routes
defp posts_route_requires_auth?(routes) do
case find_posts_content_route(routes) do
nil -> false
route -> RouteResolver.route_requires_auth?(route)
end
end
# Find posts content route in cached routes (reusable)
defp find_posts_content_route(routes) do
Enum.find(routes, fn route ->
route.verb == :get and
(String.contains?(route.path, ":slug") or String.contains?(route.path, ":id")) and
posts_route_match?(route)
end)
end
defp posts_route_match?(route) do
path_lower = String.downcase(route.path)
plug_name = route.plug |> to_string() |> String.downcase()
String.contains?(path_lower, "/posts/") or
String.starts_with?(path_lower, "/posts/") or
(String.contains?(plug_name, "post") and not String.contains?(plug_name, "page"))
end
defp build_index_entry(base_url, language, is_default) do
# Canonical path without language prefix (for hreflang grouping)
canonical_path = "/posts"
path = build_path_with_language(canonical_path, language, is_default)
url = build_url(path, base_url)
UrlEntry.new(%{
loc: url,
lastmod: nil,
changefreq: "daily",
priority: 0.7,
title: "Posts",
category: "Posts",
source: :posts,
canonical_path: canonical_path
})
end
defp collect_posts(base_url, language, is_default) do
if Code.ensure_loaded?(PhoenixKitPosts) do
mod = Module.concat([PhoenixKitPosts])
posts = mod.list_public_posts(preload: [])
posts
|> Enum.reject(&excluded?/1)
|> Enum.map(fn post ->
build_post_entry(post, base_url, language, is_default)
end)
else
[]
end
rescue
error ->
Logger.warning("Failed to collect posts: #{inspect(error)}")
[]
end
defp build_post_entry(post, base_url, language, is_default) do
slug = post.slug || to_string(post.uuid)
# Canonical path without language prefix (for hreflang grouping)
canonical_path = "/posts/#{slug}"
path = build_path_with_language(canonical_path, language, is_default)
url = build_url(path, base_url)
lastmod = post.updated_at || post.published_at
UrlEntry.new(%{
loc: url,
lastmod: lastmod,
changefreq: "weekly",
priority: 0.8,
title: post.title,
category: "Posts",
source: :posts,
canonical_path: canonical_path
})
end
defp excluded?(post) do
case post.metadata do
%{"sitemap_exclude" => true} -> true
%{"sitemap_exclude" => "true"} -> true
_ -> false
end
rescue
_ -> false
end
# Adds the locale segment to `path` when applicable. Decision rules
# live in `LocalePath.emit_prefix?/2` — see that module for the
# canonical policy shared across all sitemap sources. Post sitemap
# entries use the base code (no dialect suffix) for the segment.
defp build_path_with_language(path, language, is_default) do
if LocalePath.emit_prefix?(language, is_default) do
"/#{Languages.DialectMapper.extract_base(language)}#{path}"
else
path
end
end
defp build_url(path, nil) do
# Fallback to site_url from settings
base = Settings.get_setting("site_url", "")
normalized_base = String.trim_trailing(base, "/")
"#{normalized_base}#{path}"
end
defp build_url(path, base_url) when is_binary(base_url) do
normalized_base = String.trim_trailing(base_url, "/")
"#{normalized_base}#{path}"
end
end