Skip to content

Commit

Permalink
feat: always generate site endpoints (#740)
Browse files Browse the repository at this point in the history
  • Loading branch information
leandrocp authored Feb 4, 2025
1 parent 9dde49d commit c243a49
Show file tree
Hide file tree
Showing 23 changed files with 396 additions and 263 deletions.
26 changes: 23 additions & 3 deletions dev.exs
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,19 @@ Demo.Repo.start_link()
Ecto.Migrator.run(Demo.Repo, path, :up, all: true, log_migrations_sql: true)
Demo.Repo.stop()

Application.put_env(:beacon, DemoWeb.Endpoint,
Application.put_env(:beacon, DemoWeb.ProxyEndpoint,
adapter: Bandit.PhoenixAdapter,
check_origin: {DemoWeb.ProxyEndpoint, :check_origin, []},
url: [port: 4001, scheme: "http"],
http: [ip: {0, 0, 0, 0}, port: 4001],
live_view: [signing_salt: "aaaaaaaa"],
secret_key_base: String.duplicate("a", 64),
server: true
)

Application.put_env(:beacon, DemoWeb.Endpoint,
adapter: Bandit.PhoenixAdapter,
http: [ip: {0, 0, 0, 0}, port: 4100],
server: true,
live_view: [signing_salt: "aaaaaaaa"],
secret_key_base: String.duplicate("a", 64),
Expand Down Expand Up @@ -87,12 +97,21 @@ defmodule DemoWeb.Router do
end
end

defmodule DemoWeb.ProxyEndpoint do
use Beacon.ProxyEndpoint,
otp_app: :beacon,
session_options: [store: :cookie, key: "_beacon_dev_key", signing_salt: "pMQYsz0UKEnwxJnQrVwovkBAKvU3MiuL"],
fallback: DemoWeb.Endpoint
end

defmodule DemoWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :beacon

@session_options [store: :cookie, key: "_beacon_dev_key", signing_salt: "pMQYsz0UKEnwxJnQrVwovkBAKvU3MiuL"]

socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]]
# TODO: add this function in the `gen.site` generator
def proxy_endpoint, do: DemoWeb.ProxyEndpoint

socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket

plug Phoenix.LiveReloader
Expand Down Expand Up @@ -1246,7 +1265,8 @@ Task.start(fn ->
Demo.Repo,
{Phoenix.PubSub, [name: Demo.PubSub]},
{Beacon, sites: [dev_site, dy_site]},
DemoWeb.Endpoint
DemoWeb.Endpoint,
DemoWeb.ProxyEndpoint
]

{:ok, _} = Supervisor.start_link(children, strategy: :one_for_one)
Expand Down
48 changes: 39 additions & 9 deletions lib/beacon/loader/routes.ex
Original file line number Diff line number Diff line change
Expand Up @@ -39,29 +39,59 @@ defmodule Beacon.Loader.Routes do

# TODO: secure cross site assets
# TODO: media_path sigil
@doc """
Media path relative to host
"""
def beacon_media_path(file_name) when is_binary(file_name) do
prefix = @router.__beacon_scoped_prefix_for_site__(@site)
sanitize_path("#{prefix}/__beacon_media__/#{file_name}")
end

# TODO: media_url sigil
def beacon_media_url(file_name) when is_binary(file_name) do
@endpoint.url() <> beacon_media_path(file_name)
public_site_host() <> beacon_media_path(file_name)
end

def beacon_page_url(conn, %{path: path} = page) do
def public_site_host do
uri = Beacon.ProxyEndpoint.public_uri(@site)
String.Chars.URI.to_string(%URI{scheme: uri.scheme, host: uri.host, port: uri.port})
end

@doc """
Returns the full public-facing site URL, including the prefix.
Scheme and port are fetched from the Proxy Endpoint, if available,
since that's the entry point for all requests.
Host is fetched from the site endpoint.
"""
def public_site_url do
uri =
case Beacon.ProxyEndpoint.public_uri(@site) do
# remove path: "/" to build URL without the / suffix
%{path: "/"} = uri -> %{uri | path: nil}
uri -> uri
end

String.Chars.URI.to_string(uri)
end

def public_page_url(%{site: site} = page) do
site == @site || raise Beacon.RuntimeError, message: "failed to generate public page url "
prefix = @router.__beacon_scoped_prefix_for_site__(@site)
path = Path.join([@endpoint.url(), prefix, path])
Phoenix.VerifiedRoutes.unverified_path(conn, conn.private.phoenix_router, path)
path = sanitize_path("#{prefix}#{page.path}")
String.Chars.URI.to_string(%{Beacon.ProxyEndpoint.public_uri(@site) | path: 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
def public_sitemap_url do
public_site_url() <> "/sitemap.xml"
end

def public_css_config_url do
public_site_url() <> "/__beacon_assets__/css_config"
end

# TODO: remove sanitize_path/1
defp sanitize_path(path) do
String.replace(path, "//", "/")
end
Expand Down
2 changes: 2 additions & 0 deletions lib/beacon/private.ex
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,6 @@ defmodule Beacon.Private do
# https://github.com/phoenixframework/phoenix/blob/4ebefb9d1f710c576f08c517f5852498dd9b935c/lib/phoenix/endpoint/supervisor.ex#L301-L302
defp host_to_binary({:system, env_var}), do: host_to_binary(System.get_env(env_var))
defp host_to_binary(host), do: host

def router(%{private: %{phoenix_router: router}}), do: router
end
58 changes: 58 additions & 0 deletions lib/beacon/proxy_endpoint.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
defmodule Beacon.ProxyEndpoint do
@moduledoc false

require Logger

defmacro __using__(opts) do
quote location: :keep, generated: true do
otp_app = Keyword.get(unquote(opts), :otp_app) || raise Beacon.RuntimeError, "missing required option :otp_app in Beacon.ProxyEndpoint"
Expand Down Expand Up @@ -78,4 +80,60 @@ defmodule Beacon.ProxyEndpoint do
defp check_origin(_, _), do: false
end
end

# https://github.com/phoenixframework/phoenix/blob/2614f2a0d95a3b4b745bdf88ccd9f3b7f6d5966a/lib/phoenix/endpoint/supervisor.ex#L386
@doc """
Similar to `public_url/1` but returns a `%URI{}` instead.
"""
@spec public_uri(Beacon.Types.Site.t()) :: URI.t()
def public_uri(site) do
site_endpoint = Beacon.Config.fetch!(site).endpoint
proxy_endpoint = site_endpoint.proxy_endpoint()
router = Beacon.Config.fetch!(site).router

proxy_url = proxy_endpoint.config(:url)
site_url = site_endpoint.config(:url)

https = proxy_endpoint.config(:https)
http = proxy_endpoint.config(:http)

{scheme, port} =
cond do
https -> {"https", https[:port] || 443}
http -> {"http", http[:port] || 80}
true -> {"http", 80}
end

scheme = proxy_url[:scheme] || scheme
host = host_to_binary(site_url[:host] || "localhost")
port = port_to_integer(proxy_url[:port] || port)
path = router.__beacon_scoped_prefix_for_site__(site)

if host =~ ~r"[^:]:\d" do
Logger.warning("url: [host: ...] configuration value #{inspect(host)} for #{inspect(site_endpoint)} is invalid")
end

%URI{scheme: scheme, host: host, port: port, path: path}
end

@doc """
Returns the public URL of a given `site`.
Scheme and port are fetched from the Proxy Endpoint to resolve the URL correctly
"""
@spec public_url(Beacon.Types.Site.t()) :: String.t()
def public_url(site) do
site
|> public_uri()
|> String.Chars.URI.to_string()
end

# TODO: Remove the first function clause once {:system, env_var} tuples are removed
defp host_to_binary({:system, env_var}), do: host_to_binary(System.get_env(env_var))
defp host_to_binary(host), do: host

# TODO: Remove the first function clause once {:system, env_var} tuples are removed
defp port_to_integer({:system, env_var}), do: port_to_integer(System.get_env(env_var))
defp port_to_integer(port) when is_binary(port), do: String.to_integer(port)
defp port_to_integer(port) when is_integer(port), do: port
end
3 changes: 3 additions & 0 deletions lib/beacon/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,9 @@ defmodule Beacon.Router do
Then Beacon will reference both of those sitemaps in the top-level index:
* `my_domain.com/sitemap_index.xml`
Note that `beacon_sitemap_index` will include the sitemap URL of all mounted sites
in the router, so that macro should be at the root and not duplicated.
## Requirements
Note that your sitemap index cannot have a path which is "deeper" in the directory structure than
Expand Down
9 changes: 4 additions & 5 deletions lib/beacon/runtime_css.ex
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,10 @@ defmodule Beacon.RuntimeCSS do
@doc """
Returns the URL to fetch the CSS config used to generate the site stylesheet.
"""
@spec asset_url(Site.t()) :: String.t()
def asset_url(site) do
%{endpoint: endpoint, router: router} = Beacon.Config.fetch!(site)
prefix = router.__beacon_scoped_prefix_for_site__(site)
endpoint.url() <> Beacon.Router.sanitize_path("#{prefix}/__beacon_assets__/css_config")
@spec css_config_url(Site.t()) :: String.t()
def css_config_url(site) do
routes_module = Beacon.Loader.fetch_routes_module(site)
Beacon.apply_mfa(site, routes_module, :public_css_config_url, [])
end

@doc false
Expand Down
6 changes: 3 additions & 3 deletions lib/beacon/web/controllers/robots_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ defmodule Beacon.Web.RobotsController do
|> put_view(Beacon.Web.RobotsTxt)
|> put_resp_content_type("text/txt")
|> put_resp_header("cache-control", "public max-age=300")
|> render(:robots, sitemap_url: get_sitemap_url(conn, site))
|> render(:robots, sitemap_url: get_sitemap_url(site))
end

defp get_sitemap_url(conn, site) do
defp get_sitemap_url(site) do
routes_module = Beacon.Loader.fetch_routes_module(site)
Beacon.apply_mfa(site, routes_module, :beacon_sitemap_url, [conn])
Beacon.apply_mfa(site, routes_module, :public_sitemap_url, [])
end
end
18 changes: 10 additions & 8 deletions lib/beacon/web/controllers/sitemap_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ defmodule Beacon.Web.SitemapController do
def init(action) when action in [:index, :show], do: action

def call(conn, :index) do
sites = Beacon.Private.router(conn).__beacon_sites__()

conn
|> accepts(["xml"])
|> 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))
|> render(:sitemap_index, urls: get_sitemap_urls(sites))
end

def call(%{assigns: %{site: site}} = conn, :show) do
Expand All @@ -19,27 +21,27 @@ defmodule Beacon.Web.SitemapController do
|> 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))
|> render(:sitemap, pages: get_pages(site))
end

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

defp get_pages(conn, site) do
defp get_pages(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]),
loc: Beacon.apply_mfa(site, routes_module, :public_page_url, [page]),
lastmod: DateTime.to_iso8601(page.updated_at)
}
end)
Expand Down
4 changes: 2 additions & 2 deletions lib/beacon/web/sitemap/sitemap_index.xml.eex
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<%= for url <- @urls do %><sitemap>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"><%= for url <- @urls do %>
<sitemap>
<loc><%= url %></loc>
</sitemap><% end %>
</sitemapindex>
2 changes: 1 addition & 1 deletion lib/mix/tasks/beacon.gen.proxy_endpoint.ex
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ defmodule Mix.Tasks.Beacon.Gen.ProxyEndpoint do

case Igniter.Project.Module.module_exists(igniter, proxy_endpoint_module_name) do
{true, igniter} ->
Igniter.add_notice(igniter, """
Igniter.add_warning(igniter, """
Module #{inspect(proxy_endpoint_module_name)} already exists. Skipping.
""")

Expand Down
Loading

0 comments on commit c243a49

Please sign in to comment.