Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Beacon generates sitemap.xml per site and a sitemap_index.xml #687

Open
wants to merge 33 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
5a4fb2e
per-site sitemaps via beacon_site
APB9785 Dec 4, 2024
c0683fb
auto format code
APB9785 Dec 4, 2024
a9ff143
Merge branch 'main' into apb/sitemap
APB9785 Dec 5, 2024
7e2586d
sitemap index attempt 1
APB9785 Dec 5, 2024
447260b
fix sitemap index
APB9785 Dec 6, 2024
306ab5b
fix test
APB9785 Dec 6, 2024
5d7e485
Merge branch 'main' into apb/sitemap
leandrocp Dec 9, 2024
2d46ab3
WIP - Add `Beacon.ProxyEndpoint` to serve multiple domains
leandrocp Dec 10, 2024
273518d
add fallback
leandrocp Dec 10, 2024
384ab65
add TODO binary match
leandrocp Dec 10, 2024
49d248c
Merge branch 'main' into apb/sitemap
APB9785 Dec 10, 2024
7c275a0
add changelog entry
APB9785 Dec 10, 2024
0156cc9
use Phoenix.VerifiedRoutes.unverified_path/3
APB9785 Dec 10, 2024
4feae8d
router changes
APB9785 Dec 10, 2024
3116137
ensure iso8601 format for lastmod timestamp
APB9785 Dec 10, 2024
11c59af
test for :host option
APB9785 Dec 10, 2024
a67aa6a
Merge branch 'main' into apb/sitemap
APB9785 Dec 11, 2024
8b1f741
Merge branch 'main' into lp-endpoint
leandrocp Dec 11, 2024
781ee2f
WIP: docs
leandrocp Dec 11, 2024
bde1a82
separate compile_env
leandrocp Dec 11, 2024
29f43e0
Merge branch 'main' into lp-endpoint
leandrocp Dec 11, 2024
1943905
fetch endpoint from site config
leandrocp Dec 12, 2024
3f33ab8
debug
leandrocp Dec 12, 2024
75673d6
WIP: task to setup proxy endpoint
leandrocp Dec 12, 2024
2fe0bbd
Merge branch 'main' into lp-endpoint
leandrocp Dec 20, 2024
438f49f
clean up
leandrocp Dec 20, 2024
7b5d5a9
remove dup moduledoc
leandrocp Dec 20, 2024
b4b8742
update beacon_test
APB9785 Dec 20, 2024
07d9c5a
Merge branch 'main' into apb/sitemap
APB9785 Dec 20, 2024
2d4b2f7
changelog merge
APB9785 Dec 20, 2024
33d53cd
Merge branch 'lp-endpoint' into apb/sitemap
APB9785 Dec 20, 2024
c03ce1c
test with proxy endpoint
APB9785 Dec 20, 2024
78b4f0c
Merge branch 'main' into apb/sitemap
APB9785 Dec 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .formatter.exs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
locals_without_parens = [
beacon_site: 1,
beacon_site: 2
beacon_site: 2,
beacon_sitemap_index: 1
]

[
Expand Down
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
# Changelog

## Unreleased

### Enhancements
- Beacon will now automatically generate a `sitemap.xml` for each `beacon_site` defined in the Router
- Add macro `beacon_sitemap_index` for use in the Router to serve a sitemap index

## 0.3.1 (2024-12-10)

### Fixes

- Avoid unloading imported dynamic Components modules without a replacement

## 0.3.0 (2024-12-05)
Expand Down
13 changes: 13 additions & 0 deletions lib/beacon/loader/routes.ex
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,19 @@ defmodule Beacon.Loader.Routes do
@endpoint.url() <> beacon_media_path(file_name)
end

def beacon_page_url(conn, %{path: path} = page) do
prefix = @router.__beacon_scoped_prefix_for_site__(@site)
APB9785 marked this conversation as resolved.
Show resolved Hide resolved
path = Path.join([@endpoint.url(), prefix, path])
Phoenix.VerifiedRoutes.unverified_path(conn, conn.private.phoenix_router, path)
end

def beacon_sitemap_url(conn) do
if prefix = @router.__beacon_scoped_prefix_for_site__(@site) do
path = Path.join([@endpoint.url(), prefix, "sitemap.xml"])
Phoenix.VerifiedRoutes.unverified_path(conn, conn.private.phoenix_router, path)
end
end

defp sanitize_path(path) do
String.replace(path, "//", "/")
end
Expand Down
71 changes: 67 additions & 4 deletions lib/beacon/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ defmodule Beacon.Router do
defp prelude do
quote do
Module.register_attribute(__MODULE__, :beacon_sites, accumulate: true)
import Beacon.Router, only: [beacon_site: 2]
import Beacon.Router, only: [beacon_site: 2, beacon_sitemap_index: 1]
@before_compile unquote(__MODULE__)
end
end
Expand All @@ -111,25 +111,28 @@ defmodule Beacon.Router do
@doc false
def __beacon_sites__, do: unquote(Macro.escape(sites))
unquote(prefixes)
def __beacon_scoped_prefix_for_site__(_), do: nil
end
end

@doc """
Mounts a site in the `prefix` in your host application router.

This will automatically serve a `sitemap.xml` file from the `prefix` path defined for this site.

## Options

* `:site` (required) `t:Beacon.Types.Site.t/0` - register your site with a unique name.
Note that the name has to match the one used in your site configuration.
See the module doc and `Beacon.Config` for more info.
* `:root_layout` - override the default root layout for the site. Defaults to `{Beacon.Web.Layouts, :runtime}`.
See `Beacon.Web.Layouts` and `Phoenix.LiveView.Router.live_session/3` for more info.
Use with caution.
See `Beacon.Web.Layouts` and `Phoenix.LiveView.Router.live_session/3` for more info.
Use with caution.

"""
defmacro beacon_site(prefix, opts) do
# TODO: raise on duplicated sites defined on the same prefix
quote bind_quoted: binding(), location: :keep do
quote bind_quoted: binding(), location: :keep, generated: true do
import Phoenix.Router, only: [scope: 3, get: 3, get: 4]
import Phoenix.LiveView.Router, only: [live: 3, live_session: 3]

Expand All @@ -145,6 +148,8 @@ defmodule Beacon.Router do
get "/__beacon_assets__/css-:md5", Beacon.Web.AssetsController, :css, assigns: %{site: opts[:site]}
get "/__beacon_assets__/js-:md5", Beacon.Web.AssetsController, :js, assigns: %{site: opts[:site]}

get "/sitemap.xml", Beacon.Web.SitemapController, :show, as: :beacon_sitemap, assigns: %{site: opts[:site]}

live "/*path", Beacon.Web.PageLive, :path
end
end
Expand All @@ -153,6 +158,64 @@ defmodule Beacon.Router do
end
end

@doc """
Creates a sitemap index at the given path (including the filename and extension).

## Example

defmodule MyApp.Router do
...
scope "/" do
pipe_through :browser

beacon_sitemap_index "/sitemap_index.xml"

beacon_site "/other", site: :other
beacon_site "/", site: :home
end
end

In the above example, there are two Beacon sites, so Beacon will serve two sitemaps:
* `my_domain.com/sitemap.xml` for site `:home`
* `my_domain.com/other/sitemap.xml` for site `:other`

Then Beacon will reference both of those sitemaps in the top-level index:
* `my_domain.com/sitemap_index.xml`

## Requirements

Note that your sitemap index cannot have a path which is "deeper" in the directory structure than
your Beacon sites (which will be contained in the index).

For example, the following is NOT allowed:

scope "/" do
...
beacon_sitemap_index "/root/nested/sitemap_index.xml"

beacon_site "/root", site: :root
end

However, the opposite case (nesting the sites deeper than the index) is perfectly fine:

scope "/" do
...
beacon_sitemap_index "/sitemap_index.xml"

beacon_site "/nested/path/to/site", site: :nested
end

"""
defmacro beacon_sitemap_index(path_with_filename) do
quote bind_quoted: binding(), location: :keep, generated: true do
import Phoenix.Router, only: [scope: 3, get: 4]

scope "/", alias: false, as: false do
get path_with_filename, Beacon.Web.SitemapController, :index, as: :beacon_sitemap
end
end
end

@doc false
@spec __options__(keyword()) :: {atom(), atom(), keyword()}
def __options__(opts) do
Expand Down
45 changes: 45 additions & 0 deletions lib/beacon/web/controllers/sitemap_controller.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
defmodule Beacon.Web.SitemapController do
@moduledoc false
use Beacon.Web, :controller

def init(action) when action in [:index, :show], do: action

def call(conn, :index) do
conn
|> put_view(Beacon.Web.SitemapXML)
|> put_resp_content_type("text/xml")
|> put_resp_header("cache-control", "public max-age=300")
|> render(:sitemap_index, urls: get_sitemap_urls(conn))
end

def call(%{assigns: %{site: site}} = conn, :show) do
conn
|> put_view(Beacon.Web.SitemapXML)
|> put_resp_content_type("text/xml")
|> put_resp_header("cache-control", "public max-age=300")
|> render(:sitemap, pages: get_pages(conn, site))
end

defp get_sitemap_urls(conn) do
Beacon.Registry.running_sites()
|> Enum.map(fn site ->
routes_module = Beacon.Loader.fetch_routes_module(site)
Beacon.apply_mfa(site, routes_module, :beacon_sitemap_url, [conn])
end)
|> Enum.reject(&is_nil/1)
|> Enum.sort()
end

defp get_pages(conn, site) do
routes_module = Beacon.Loader.fetch_routes_module(site)

site
|> Beacon.Content.list_published_pages()
|> Enum.map(fn page ->
%{
loc: Beacon.apply_mfa(site, routes_module, :beacon_page_url, [conn, page]),
lastmod: DateTime.to_iso8601(page.updated_at)
}
end)
end
end
7 changes: 7 additions & 0 deletions lib/beacon/web/sitemap/sitemap.xml.eex
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<%= for page <- @pages do %><url>
<loc><%= page.loc %></loc>
<lastmod><%= page.lastmod %></lastmod>
</url><% end %>
</urlset>
6 changes: 6 additions & 0 deletions lib/beacon/web/sitemap/sitemap_index.xml.eex
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<%= for url <- @urls do %><sitemap>
<loc><%= url %></loc>
</sitemap><% end %>
</sitemapindex>
4 changes: 4 additions & 0 deletions lib/beacon/web/sitemap_xml.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
defmodule Beacon.Web.SitemapXML do
import Phoenix.Template, only: [embed_templates: 1]
embed_templates "sitemap/*.xml"
end
6 changes: 0 additions & 6 deletions test/beacon_web/controllers/media_library_controller_test.exs
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
defmodule Beacon.Web.Controllers.MediaLibraryControllerTest do
use Beacon.Web.ConnCase, async: true

setup do
Process.flag(:error_handler, Beacon.ErrorHandler)
Process.put(:__beacon_site__, :my_site)
:ok
end

test "show", %{conn: conn} do
%{file_name: file_name} = Beacon.Test.Fixtures.beacon_media_library_asset_fixture(site: :my_site)
routes = Beacon.Loader.fetch_routes_module(:my_site)
Expand Down
60 changes: 60 additions & 0 deletions test/beacon_web/controllers/sitemap_controller_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
defmodule Beacon.Web.SitemapControllerTest do
use Beacon.Web.ConnCase, async: false

setup do
site = :my_site

layout =
beacon_published_layout_fixture(
site: site,
template: """
<header>Page header</header>
<%= @inner_content %>
<footer>Page footer</footer>
"""
)

page = beacon_published_page_fixture(site: site, path: "/foo", layout_id: layout.id)

routes = Beacon.Loader.fetch_routes_module(site)

[site: site, layout: layout, page: page, routes: routes]
end

test "index", %{conn: conn} do
conn = get(conn, "/sitemap_index.xml")

assert response(conn, 200) == """
&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
&lt;sitemapindex xmlns=&quot;http://www.sitemaps.org/schemas/sitemap/0.9&quot;&gt;
&lt;sitemap&gt;
&lt;loc&gt;http://localhost:4000/nested/media/sitemap.xml&lt;/loc&gt;
&lt;/sitemap&gt;&lt;sitemap&gt;
&lt;loc&gt;http://localhost:4000/nested/site/sitemap.xml&lt;/loc&gt;
&lt;/sitemap&gt;&lt;sitemap&gt;
&lt;loc&gt;http://localhost:4000/other/sitemap.xml&lt;/loc&gt;
&lt;/sitemap&gt;&lt;sitemap&gt;
&lt;loc&gt;http://localhost:4000/sitemap.xml&lt;/loc&gt;
&lt;/sitemap&gt;&lt;sitemap&gt;
&lt;loc&gt;http://localhost:4000/sitemap.xml&lt;/loc&gt;
&lt;/sitemap&gt;
&lt;/sitemapindex&gt;
"""
end

test "show", %{conn: conn, page: page, routes: routes} do
conn = get(conn, "/sitemap.xml")

assert response(conn, 200) == """
&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
&lt;urlset xmlns=&quot;http://www.sitemaps.org/schemas/sitemap/0.9&quot;&gt;
&lt;url&gt;
&lt;loc&gt;#{routes.beacon_page_url(conn, page)}&lt;/loc&gt;
&lt;lastmod&gt;#{DateTime.to_iso8601(page.updated_at)}&lt;/lastmod&gt;
&lt;/url&gt;
&lt;/urlset&gt;
"""

assert response_content_type(conn, :xml) =~ "charset=utf-8"
end
end
4 changes: 2 additions & 2 deletions test/mix/tasks/install_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ defmodule Mix.Tasks.Beacon.InstallTest do
project
|> Igniter.compose_task("beacon.install")
|> assert_has_patch(".formatter.exs", """
7 - | import_deps: [:ecto, :ecto_sql, :phoenix],
7 + | import_deps: [:beacon, :ecto, :ecto_sql, :phoenix],
8 - | import_deps: [:ecto, :ecto_sql, :phoenix],
8 + | import_deps: [:beacon, :ecto, :ecto_sql, :phoenix],
""")
end

Expand Down
6 changes: 6 additions & 0 deletions test/support/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ defmodule Beacon.BeaconTest.Router do
plug :put_secure_browser_headers
end

scope "/" do
pipe_through :browser
beacon_sitemap_index "/sitemap_index.xml"
end

scope "/nested" do
pipe_through :browser
beacon_site "/site", site: :booted
Expand All @@ -24,6 +29,7 @@ defmodule Beacon.BeaconTest.Router do
# `alias` is not really used but is present here to verify that `beacon_site` has no conflicts with custom aliases
scope path: "/", alias: AnyAlias do
pipe_through :browser

beacon_site "/other", site: :not_booted
beacon_site "/", site: :my_site
end
Expand Down
Loading