When your Phoenix application grows beyond a few hundred pages, maintaining a single monolithic sitemap.xml becomes unwieldy. Search engines like Google recommend splitting large sitemaps into smaller, organized chunks. Let's build a WordPress-style sitemap structure in Phoenix!
π― What We're Building
Instead of one giant sitemap, we'll create:
/sitemap.xml- Main index pointing to sub-sitemaps/items-sitemap.xml- All rental items/categories-sitemap.xml- Category pages/pages-sitemap.xml- Static pages and locations
Why Split Sitemaps?
Benefits:
Better organization and maintainability
Faster generation (regenerate only what changed)
Better caching strategies
Search engine friendly (Google's recommended approach)
Easier debugging (isolate issues to specific content types)
Step 1: Setup Routes
First, add the sitemap routes in your router.ex:
elixir
pipeline :xml do
plug :accepts, ["xml"]
end
scope "/", RentgaraWeb do
pipe_through :xml
get "/sitemap.xml", SitemapController, :index
get "/items-sitemap.xml", SitemapController, :items
get "/categories-sitemap.xml", SitemapController, :categories
get "/pages-sitemap.xml", SitemapController, :pages
endImportant: Place this BEFORE your /:locale scoped routes to avoid route conflicts.
Step 2: Create the Sitemap Controller
Create lib/rentgara_web/controllers/sitemap_controller.ex:
elixir
defmodule RentgaraWeb.SitemapController do
use RentgaraWeb, :controller
alias Rentgara.Items
alias Rentgara.Locations
# Supported locales for multi-language support
@locales ["en", "ne", "ne_RO"]
@static_pages [
"about",
"how-it-works",
"contact",
"safety",
"terms",
"privacy"
]
# Main sitemap index (points to sub-sitemaps)
def index(conn, _params) do
xml = """
<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<sitemap>
<loc>#{url(~p"/items-sitemap.xml")}</loc>
<lastmod>#{Date.utc_today()}</lastmod>
</sitemap>
<sitemap>
<loc>#{url(~p"/categories-sitemap.xml")}</loc>
<lastmod>#{Date.utc_today()}</lastmod>
</sitemap>
<sitemap>
<loc>#{url(~p"/pages-sitemap.xml")}</loc>
<lastmod>#{Date.utc_today()}</lastmod>
</sitemap>
</sitemapindex>
"""
conn
|> put_resp_content_type("text/xml")
|> send_resp(200, xml)
end
# Items sitemap
def items(conn, _params) do
items = Items.list_items()
xml = """
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
#{generate_item_urls(items)}
</urlset>
"""
conn
|> put_resp_content_type("text/xml")
|> send_resp(200, xml)
end
# Categories sitemap
def categories(conn, _params) do
categories = Items.list_categories_raw()
xml = """
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
#{generate_category_urls(categories)}
</urlset>
"""
conn
|> put_resp_content_type("text/xml")
|> send_resp(200, xml)
end
# Pages sitemap (static pages + locations + home)
def pages(conn, _params) do
locations = Locations.list_active_locations()
xml = """
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
#{generate_home_urls()}
#{generate_static_urls()}
#{generate_location_urls(locations)}
</urlset>
"""
conn
|> put_resp_content_type("text/xml")
|> send_resp(200, xml)
end
# Helper functions for generating URLs
defp generate_home_urls do
for locale <- @locales do
"""
<url>
<loc>#{url(~p"/#{locale}")}</loc>
<changefreq>daily</changefreq>
<priority>1.0</priority>
</url>
"""
end
|> Enum.join("\n")
end
defp generate_static_urls do
for locale <- @locales, page <- @static_pages do
"""
<url>
<loc>#{url(~p"/#{locale}/#{page}")}</loc>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
"""
end
|> Enum.join("\n")
end
defp generate_location_urls(locations) do
for locale <- @locales, location <- locations do
"""
<url>
<loc>#{url(~p"/#{locale}/location/#{location.slug}")}</loc>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
"""
end
|> Enum.join("\n")
end
defp generate_category_urls(categories) do
for locale <- @locales, category <- categories do
"""
<url>
<loc>#{url(~p"/#{locale}/items?category=#{category.slug}")}</loc>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
"""
end
|> Enum.join("\n")
end
defp generate_item_urls(items) do
for locale <- @locales, item <- items do
if item.location do
lastmod =
if item.updated_at,
do: Calendar.strftime(item.updated_at, "%Y-%m-%d"),
else: nil
lastmod_tag = if lastmod, do: "<lastmod>#{lastmod}</lastmod>", else: ""
"""
<url>
<loc>#{url(~p"/#{locale}/items/#{item.location.slug}/#{item.slug}")}</loc>
<changefreq>daily</changefreq>
<priority>1.0</priority>
#{lastmod_tag}
</url>
"""
else
""
end
end
|> Enum.join("\n")
end
endπ¨ Key Features Explained
1. Multi-Language Support
The @locales list ensures every page is generated for each supported language:
elixir
@locales ["en", "ne", "ne_RO"]2. Verified URLs with ~p Sigil
Phoenix's verified routes (~p) ensure type-safe URL generation:
elixir
url(~p"/#{locale}/items/#{location.slug}/#{item.slug}")3. Dynamic Priority & Change Frequency
Different content types get different priorities:
Items:
priority: 1.0,changefreq: daily(most important, changes often)Locations:
priority: 0.9,changefreq: weekly(important landing pages)Static Pages:
priority: 0.8,changefreq: weekly(stable content)
4. Last Modified Dates
For dynamic content like items, include lastmod tags:
elixir
lastmod = Calendar.strftime(item.updated_at, "%Y-%m-%d")π Testing Your Sitemaps
Start your Phoenix server and visit:
bash
http://localhost:4000/sitemap.xml
http://localhost:4000/items-sitemap.xml
http://localhost:4000/categories-sitemap.xml
http://localhost:4000/pages-sitemap.xmlYour main sitemap should look like:
xml
<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<sitemap>
<loc>http://localhost:4000/items-sitemap.xml</loc>
<lastmod>2025-12-21</lastmod>
</sitemap>
<sitemap>
<loc>http://localhost:4000/categories-sitemap.xml</loc>
<lastmod>2025-12-21</lastmod>
</sitemap>
<sitemap>
<loc>http://localhost:4000/pages-sitemap.xml</loc>
<lastmod>2025-12-21</lastmod>
</sitemap>
</sitemapindex>π― Production Optimizations
1. Add Caching
elixir
def items(conn, _params) do
items =
Cachex.fetch(:sitemap_cache, "items", fn ->
{:commit, Items.list_items()}
end)
|> elem(1)
# ... generate XML
end2. Invalidate Cache on Updates
elixir
# In your Items context
def create_item(attrs) do
with {:ok, item} <- create_item_changeset(attrs) do
Cachex.del(:sitemap_cache, "items")
{:ok, item}
end
end3. Submit to Search Engines
Add to your robots.txt:
Sitemap: https://yourdomain.com/sitemap.xmlSubmit manually to:
Google Search Console
Bing Webmaster Tools
π Performance Benefits
Before (single sitemap):
10,000 items Γ 3 locales = 30,000 URLs in one file
~3-5 second generation time
Cache invalidated on ANY content change
After (split sitemaps):
Items changed? Regenerate only
/items-sitemap.xml~0.5-1 second per sitemap
Better cache hit rates
π Takeaways
Split large sitemaps by content type for better maintainability
Use Phoenix verified routes (
~p) for type-safe URLsCache aggressively and invalidate strategically
Follow SEO best practices with proper priorities and change frequencies
Support multiple languages from day one if planning i18n