diff --git a/lib/towerops_web/controllers/sitemap_controller.ex b/lib/towerops_web/controllers/sitemap_controller.ex new file mode 100644 index 00000000..95226d1f --- /dev/null +++ b/lib/towerops_web/controllers/sitemap_controller.ex @@ -0,0 +1,28 @@ +defmodule ToweropsWeb.SitemapController do + @moduledoc false + + use ToweropsWeb, :controller + + @public_urls ["/", "/privacy", "/terms", "/docs/api", "/docs/graphql", "/help"] + + def index(conn, _params) do + base_url = ToweropsWeb.Endpoint.url() + xml = build_sitemap(base_url) + + conn + |> put_resp_content_type("text/xml") + |> send_resp(200, xml) + end + + defp build_sitemap(base_url) do + urls = + Enum.map_join(@public_urls, "\n", fn path -> " #{base_url}#{path}" end) + + String.trim(""" + + + #{urls} + + """) + end +end diff --git a/lib/towerops_web/router.ex b/lib/towerops_web/router.ex index 65135ea2..63f6213e 100644 --- a/lib/towerops_web/router.ex +++ b/lib/towerops_web/router.ex @@ -80,6 +80,11 @@ defmodule ToweropsWeb.Router do get "/health", HealthController, :index, log: false end + # Public crawlable endpoints (no pipeline, no CSRF/session required) + scope "/", ToweropsWeb do + get "/sitemap.xml", SitemapController, :index + end + scope "/", ToweropsWeb do pipe_through :browser diff --git a/priv/static/robots.txt b/priv/static/robots.txt index 26e06b5f..45bd446c 100644 --- a/priv/static/robots.txt +++ b/priv/static/robots.txt @@ -1,5 +1,5 @@ # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file -# -# To ban all spiders from the entire site uncomment the next two lines: -# User-agent: * -# Disallow: / +User-agent: * +Allow: / + +Sitemap: https://towerops.net/sitemap.xml diff --git a/test/towerops_web/controllers/sitemap_controller_test.exs b/test/towerops_web/controllers/sitemap_controller_test.exs new file mode 100644 index 00000000..1472cd05 --- /dev/null +++ b/test/towerops_web/controllers/sitemap_controller_test.exs @@ -0,0 +1,18 @@ +defmodule ToweropsWeb.SitemapControllerTest do + use ToweropsWeb.ConnCase, async: true + + describe "GET /sitemap.xml" do + test "returns valid XML with public pages", %{conn: conn} do + conn = get(conn, "/sitemap.xml") + + assert response(conn, 200) + assert get_resp_header(conn, "content-type") == ["text/xml; charset=utf-8"] + body = response(conn, 200) + assert body =~ ~r/<\?xml/ + assert body =~ "" + assert body =~ "/privacy" + assert body =~ "/terms" + end + end +end