Skip to content

Commit c559c9c

Browse files
committed
Merge branch 'apb/sitemap' into apb/robots-txt
2 parents 90adf5f + c03ce1c commit c559c9c

35 files changed

+611
-307
lines changed

CHANGELOG.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,24 @@
66
- Beacon will now automatically generate a `sitemap.xml` for each `beacon_site` defined in the Router
77
- Add macro `beacon_sitemap_index` for use in the Router to serve a sitemap index
88

9+
## 0.3.3 (2024-12-13)
10+
11+
### Fixes
12+
- Support LiveView v1.0.1
13+
- Fix variant roll changing when fetching assets
14+
15+
## 0.3.2 (2024-12-11)
16+
17+
### Fixes
18+
- Make the logic to find reachable sites less strict
19+
- Prevent components module reloading in manual mode
20+
921
### Doc
1022
- Add missing instructions on Upgrade Guide v0.3.0 where to place the Beacon tuple
1123

1224
### Chore
1325
- Exclude Dialyzer files from package
1426

15-
1627
## 0.3.1 (2024-12-10)
1728

1829
### Fixes

assets/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

assets/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "beacon",
3-
"version": "0.3.1",
3+
"version": "0.3.3",
44
"license": "MIT",
55
"repository": {},
66
"scripts": {

config/test.exs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,34 @@ config :beacon, Beacon.BeaconTest.Repo,
1616
stacktrace: true,
1717
show_sensitive_data_on_connection_error: true
1818

19+
config :beacon,
20+
session_options: [
21+
store: :cookie,
22+
key: "_beacon_test_key",
23+
signing_salt: "LKBurgGF",
24+
same_site: "Lax"
25+
]
26+
27+
config :beacon, Beacon.BeaconTest.ProxyEndpoint, live_view: [signing_salt: "LKBurgGF"]
28+
29+
config :beacon, Beacon.BeaconTest.Endpoint,
30+
url: [host: "localhost", port: 4000],
31+
secret_key_base: "dVxFbSNspBVvkHPN5m6FE6iqNtMnhrmPNw7mO57CJ6beUADllH0ux3nhAI1ic65X",
32+
live_view: [signing_salt: "LKBurgGF"],
33+
render_errors: [view: Beacon.BeaconTest.ErrorView],
34+
pubsub_server: Beacon.BeaconTest.PubSub,
35+
check_origin: false,
36+
debug_errors: true
37+
38+
config :beacon, Beacon.BeaconTest.EndpointB,
39+
url: [host: "site_b.com", port: 4000],
40+
secret_key_base: "dVxFbSNspBVvkHPN5m6FE6iqNtMnhrmPNw7mO57CJ6beUADllH0ux3nhAI1ic65X",
41+
live_view: [signing_salt: "LKBurgGF"],
42+
render_errors: [view: Beacon.BeaconTest.ErrorView],
43+
pubsub_server: Beacon.BeaconTest.PubSub,
44+
check_origin: false,
45+
debug_errors: true
46+
1947
# Fake Key Values. We're not putting real creds here.
2048
config :ex_aws,
2149
access_key_id: "AKIAIOSFODNN7EXAMPLE",

guides/deployment/deployment-topologies.md

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -77,16 +77,6 @@ which is not a big problem when you have a couple of small sites, but that becom
7777

7878
To avoid this problem, Beacon will selectively boot only the sites that are reachable in the current host, so in the example above,
7979
only `:site_a` will be booted in the node hosting `sitea.com` and only `:site_b` in the node hosting `siteb.com`.
80-
This behavior also applies to route conflicts as well, for example:
81-
82-
```elixir
83-
scope do
84-
get "/", PageController, :hello
85-
beacon_site "/", site: :site_a
86-
end
87-
```
88-
89-
Two routes in the same prefix will cause a conflict.
9080

9181
Or this other example:
9282

@@ -105,7 +95,7 @@ So Beacon won't try to boot sites that can't be reached, but a warning will be d
10595

10696
### Admin Sites Discovery
10797

108-
BeaconLiveAdmin is designed to scan the all apps connected in the same cluster to find running sites
98+
[BeaconLiveAdmin](https://hex.pm/packages/beacon_live_admin) is designed to scan the all apps connected in the same cluster to find running sites
10999
and make them available in the admin interface as displayed below:
110100

111101
```mermaid
@@ -160,8 +150,11 @@ With these constraints in mind, let's check some deployment strategies.
160150

161151
## Strategies
162152

163-
### Single application
164-
The most simple strategy is a single project with one or more sites and the admin interface in the same project.
153+
Below we'll describe some common deployment strategies but Beacon is not limited to the strategies below,
154+
you can adapt to your needs.
155+
156+
### 1. Single application on same host
157+
The most simple strategy is a single project with one or more sites and the admin interface in the same host.
165158

166159
```elixir
167160
# endpoint
@@ -197,7 +190,7 @@ flowchart TD
197190
r3["mysite.com/admin"] --> Admin
198191
```
199192

200-
### Clustered single applications
193+
### 2. Clustered single applications
201194
Same project as the previous strategy but with multiple nodes deployed in the same cluster.
202195

203196
```mermaid
@@ -235,7 +228,7 @@ flowchart TD
235228
r3["mysite.com/admin"] --> Node2Admin
236229
```
237230

238-
### Clustered applications with separated admin
231+
### 3. Clustered applications with separated admin
239232
You can observe the previous strategy duplicates the admin interface in each node, which works fairly well when you have
240233
no more than a couple of sites, but that setup tends to become harder to manage and also become a waste of resources
241234
if you start booting more site and more nodes.
@@ -288,16 +281,26 @@ flowchart TD
288281

289282
A huge benefit of this topology is the flexibility to protect the Admin interface behind a VPN or scale it independently from the main applications.
290283

291-
### Multiple hosts in single project, separated hosting apps
284+
### 4. Multiple hosts in single project, separated hosting apps
292285
Still a single project but now serving multiple sites at the root path for different dynamic hosts.
293286

294-
In this example let's assume the most simple scenario where you'd create a new app in the hosting platform
295-
to serve each domain:
287+
In this case we're still deploying just one application but serving multiple domains for each site:
288+
289+
- :campaigns -> campaigns.mysite.com
290+
- :root -> mysite.com
291+
292+
TODO: diagram
293+
294+
TODO: gen task and constraints
295+
296+
### 5. Multiple hosts in single project, separated hosting apps
297+
Similar to the previous strategy but this time we're splitting each domain into its own app:
296298

297299
- App1 -> mysite.com
298300
- App2 -> campaigns.mysite.com
299301

300-
That means isolated apps not connected to each other, just sharing the same codebase.
302+
That means deploying isolated apps for each domain/site, not connected to each other,
303+
but still sharing the same codebase.
301304

302305
```elixir
303306
# endpoint
@@ -349,7 +352,7 @@ flowchart TD
349352
r4["campaigns.mysite.com/admin"] --> Node2Admin
350353
```
351354

352-
### Multiple hosts in single project, connected hosting apps
355+
### 6. Multiple hosts in single project, connected hosting apps
353356
Similar setup as the previous strategy but now connecting the apps in the same cluster with a separated admin interface.
354357

355358
```elixir
@@ -400,4 +403,4 @@ flowchart TD
400403
r1["mysite.com/campaigns/christmas"] --> n2_site
401404
r2["mysite.com/contact"] --> n1_site
402405
r3["admin.mysite.com"] --> Admin
403-
```
406+
```

guides/general/troubleshooting.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,14 @@ a site prefix can never match and it will never receive requests.
2929

3030
That's is not necessarily an error if you have multiple sites in the same project
3131
and each scope is filtering requests on the `:host` option.
32+
But it may indicate:
3233

33-
But it may indicate an invalid configuration, as a preceding route matching the prefix
34+
1. An invalid configuration, as a preceding route matching the prefix
3435
that was supposed to be handled by this site, or an invalid `:host` value.
3536

37+
2. Missing `use Beacon.Router` and/or missing `beacon_site` in your
38+
app's router file.
39+
3640
Note that if you're using `:host` on the scope and running in `localhost`,
3741
consider adding `"localhost"` to the list of allowed hosts.
3842

@@ -41,4 +45,4 @@ Also check the [Beacon.Router](https://hexdocs.pm/beacon/Beacon.Router.html) for
4145
## RuntimeError - could not find persistent term for endpoint
4246

4347
`Beacon` should be started after your host's `Endpoint`, please review the application children
44-
and make sure is declared after the endpoint.
48+
and make sure is declared after the endpoint.

lib/beacon/igniter.ex

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
defmodule Beacon.Igniter do
2+
@moduledoc false
3+
4+
def select_router!(igniter, opts \\ []) do
5+
label = Keyword.get(opts, :label)
6+
raise_msg = Keyword.get(opts, :raise_msg, "No router found.")
7+
8+
case Igniter.Libs.Phoenix.select_router(igniter, label) do
9+
{_igniter, nil} -> Mix.raise(raise_msg)
10+
found -> found
11+
end
12+
end
13+
14+
def select_endpoint(igniter, router, label \\ "Which endpoint should be modified?") do
15+
case Igniter.Libs.Phoenix.endpoints_for_router(igniter, router) do
16+
{igniter, []} ->
17+
{igniter, nil}
18+
19+
{igniter, [endpoint]} ->
20+
{igniter, endpoint}
21+
22+
{igniter, endpoints} ->
23+
{igniter, Igniter.Util.IO.select(label, endpoints, display: &inspect/1)}
24+
end
25+
end
26+
27+
def select_endpoint!(igniter, router, opts \\ []) do
28+
label = Keyword.get(opts, :label)
29+
raise_msg = Keyword.get(opts, :raise_msg, "No endpoint found.")
30+
31+
case select_endpoint(igniter, router, label) do
32+
{_igniter, nil} -> Mix.raise(raise_msg)
33+
found -> found
34+
end
35+
end
36+
end

lib/beacon/plug.ex

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,32 @@ defmodule Beacon.Plug do
1717
"""
1818
@behaviour Plug
1919

20+
@private_routes [
21+
"__beacon_check__",
22+
"__beacon_assets__",
23+
"__beacon_media__"
24+
]
25+
2026
@impl Plug
2127
def init(_opts), do: []
2228

2329
@impl Plug
24-
def call(conn, _opts), do: Plug.Conn.put_session(conn, :beacon_variant_roll, Enum.random(1..100))
30+
def call(conn, _opts) do
31+
if Enum.any?(@private_routes, &(&1 in conn.path_info)) do
32+
conn
33+
else
34+
put_roll(conn)
35+
end
36+
end
37+
38+
defp put_roll(conn) do
39+
path_list = conn.path_params["path"]
40+
41+
with %{private: %{phoenix_live_view: {_, _, %{extra: %{session: %{"beacon_site" => site}}}}}} <- conn,
42+
{_, _} <- Beacon.RouterServer.lookup_path(site, path_list, 1) do
43+
Plug.Conn.put_session(conn, "beacon_variant_roll", Enum.random(1..100))
44+
else
45+
_ -> conn
46+
end
47+
end
2548
end

lib/beacon/private.ex

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ defmodule Beacon.Private do
66

77
# Should be avoided as much as possible.
88

9+
@phoenix_live_view_version to_string(Application.spec(:phoenix_live_view)[:vsn])
10+
911
@doc """
1012
On page navigation, the request might actually hit a different site than the one defined by the current session.
1113
@@ -20,7 +22,7 @@ defmodule Beacon.Private do
2022
We use the private function `Phoenix.LiveView.Route.live_link_info/3` in order to keep the same behavior as LV.
2123
"""
2224
def site_from_session(endpoint, router, url, view) do
23-
case Phoenix.LiveView.Route.live_link_info(endpoint, router, url) do
25+
case live_link_info(endpoint, router, url) do
2426
{_,
2527
%{
2628
view: ^view,
@@ -36,4 +38,14 @@ defmodule Beacon.Private do
3638
nil
3739
end
3840
end
41+
42+
if Version.compare(@phoenix_live_view_version, "1.0.0") == :gt do
43+
defp live_link_info(endpoint, router, url) do
44+
Phoenix.LiveView.Route.live_link_info_without_checks(endpoint, router, url)
45+
end
46+
else
47+
defp live_link_info(endpoint, router, url) do
48+
Phoenix.LiveView.Route.live_link_info(endpoint, router, url)
49+
end
50+
end
3951
end

lib/beacon/proxy_endpoint.ex

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
defmodule Beacon.ProxyEndpoint do
2+
@moduledoc false
3+
4+
# Proxy Endpoint to redirect requests to each site endpoint in a multiple domains setup.
5+
#
6+
# TODO: beacon.deploy.add_domain fobar
7+
#
8+
# TODO: use Beacon.ProxyEndpoint, otp_app: :my_app, endpoints: [MyAppWeb.EndpointSiteA, MyAppWeb.EndpointSiteB]
9+
10+
defmacro __using__(opts) do
11+
quote location: :keep, generated: true do
12+
otp_app = Keyword.get(unquote(opts), :otp_app) || raise Beacon.RuntimeError, "missing required option :otp_app in Beacon.ProxyEndpoint"
13+
14+
session_options =
15+
Keyword.get(unquote(opts), :session_options) || raise Beacon.RuntimeError, "missing required option :session_options in Beacon.ProxyEndpoint"
16+
17+
Module.put_attribute(__MODULE__, :session_options, session_options)
18+
19+
fallback = Keyword.get(unquote(opts), :fallback) || raise Beacon.RuntimeError, "missing required option :fallback in Beacon.ProxyEndpoint"
20+
Module.put_attribute(__MODULE__, :__beacon_proxy_fallback__, fallback)
21+
22+
use Phoenix.Endpoint, otp_app: otp_app
23+
24+
socket "/live", Phoenix.LiveView.Socket,
25+
websocket: [connect_info: [session: @session_options]],
26+
longpoll: [connect_info: [session: @session_options]]
27+
28+
plug :proxy
29+
30+
def proxy(conn, opts) do
31+
%{host: host} = conn
32+
33+
# TODO: cache endpoint resolver
34+
endpoint =
35+
Enum.reduce_while(Beacon.Registry.running_sites(), @__beacon_proxy_fallback__, fn site, default ->
36+
%{endpoint: endpoint} = Beacon.Config.fetch!(site)
37+
38+
if endpoint.host() == host do
39+
{:halt, endpoint}
40+
else
41+
{:cont, default}
42+
end
43+
end)
44+
45+
endpoint.call(conn, endpoint.init(opts))
46+
end
47+
end
48+
end
49+
end

0 commit comments

Comments
 (0)