Create dynamic sitemap.xml in Elixir and Phoenix
Tags: elixir, phoenix, sitemap
Published: 2019-07-18 12:37:50

I created a website for a construction company where they can sell their properties, I built it using Elixir and the Phoenix framework.

So I was searching on Google how to implement a sitemap.xml but all I can find was this lib called `sitemap`, so I tried it but...

On my website, I have a single page for each property that is created, and in this lib, I have to run a command to generate a new XML file every time I create a property. Also, I can't change the `<lastmod>` because it always uses the date where I generated the sitemap.

So I thought to create my own dynamic sitemap.xml and I will share with you how I did it, it's very simple :)

First of all, I created my new route on the file `router.ex`:

```ex
get "/sitemap.xml", SitemapController, :index
```

And then I created a new file `lib/myapp_web/controllers/sitemap_controller.ex` with this code:

```ex
defmodule MyAppWeb.SitemapController do
  use MyAppWeb, :controller

  plug :put_layout, false

  alias MyApp.Properties

  def index(conn, _params) do
    properties = Properties.list_properties()

    conn
    |> put_resp_content_type("text/xml")
    |> render("index.xml", properties: properties)
  end
end
```

Cool :) Now we have to create the file `lib/myapp_web/templates/sitemap/index.xml.eex` that will be our sitemap:

```xml
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc><%= Routes.home_url(@conn, :index) %></loc>
    <changefreq>weekly</changefreq>
    <priority>0.5</priority>
  </url>
  <url>
    <loc><%= Routes.about_url(@conn, :index) %></loc>
    <changefreq>never</changefreq>
    <priority>0.3</priority>
  </url>
  <url>
    <loc><%= Routes.apartment_url(@conn, :index) %></loc>
    <changefreq>weekly</changefreq>
    <priority>0.8</priority>
  </url>
  <url>
    <loc><%= Routes.house_url(@conn, :index) %></loc>
    <changefreq>weekly</changefreq>
    <priority>0.8</priority>
  </url>
  <url>
    <loc><%= Routes.lot_url(@conn, :index) %></loc>
    <changefreq>weekly</changefreq>
    <priority>0.8</priority>
  </url>
  <url>
    <loc><%= Routes.contact_url(@conn, :index) %></loc>
    <changefreq>never</changefreq>
    <priority>0.5</priority>
  </url>
  <%= for property <- @properties do %>
    <url>
      <loc><%= Routes.property_url(@conn, :show, property.slug) %></loc>
      <lastmod><%= format_date(property.updated_at) %></lastmod>
      <changefreq>weekly</changefreq>
      <priority>1</priority>
    </url>
  <% end %>
</urlset>
```

You can see that I added manually all the routes I have on my website: home, about, apartments, houses, lots and contact. And then I added all the dynamic pages for each property I have created.

And I have the `<lastmod>` with the correct date, using the `updated_at`. But we still have to create this function called format_date so we create the file `lib/myapp_web/views/sitemap_view.ex`:

```ex
defmodule MyAppWeb.SitemapView do
  use MyAppWeb, :view

  def format_date(date) do
    date
    |> DateTime.from_naive!("Etc/UTC")
    |> DateTime.to_date()
    |> to_string()
  end
end
```

And that's it! Now you can now add this URL: `yoursite.com/sitemap.xml` on the Google Console and it'll just work fine :)

But don't forget to add some tests for this! So we can do this on the file `test/myapp_web/controllers/sitemap_controller_test.exs`:

```ex
defmodule MyAppWeb.SitemapControllerTest do
  use MyAppWeb.ConnCase

  describe "GET /sitemap.xml" do
    test "accesses the sitemap in format xml", %{conn: conn} do
      property = property_fixture()

      conn = get(conn, "/sitemap.xml")

      assert response_content_type(conn, :xml)
      assert response(conn, 200) =~ ~r/<loc>.*#{property.slug}<\/loc>/
    end
  end
end
```

In this function `property_fixture()` you have to create a new property.

And then a test to our view in `test/myapp_web/views/sitemap_view_test.exs`:

```ex
defmodule MyAppWeb.SitemapViewTest do
  use MyAppWeb.ConnCase, async: true

  alias MyAppWeb.SitemapView

  test "format_date/1" do
    assert SitemapView.format_date(~N[2019-07-08 13:15:00]) == "2019-07-08"
  end
end
```

And that's all! :)