In December of 2021, Brian Cardarella introduced DockYard Beacon CMS in this series of tweets:
Over the course of the past year, I've created a sample project a total of 3 times to get a better understanding for how it operates. I haven't seen a ton of content on Beacon beyond announcement tweets, the mention in the ElixirConf 2022 keynote, and https://beaconcms.org/. This post covers the complete instructions in the readme with some notes on where to go from here. I had run into a few snags at first but a lot of those initial pain points have been hammered out so far. While a basic "Hello World" sample project is great, I plan on expanding on the sample with deeper dives into how Beacon serves up content. It takes a few novel approaches I haven't seen before to create either a CMS that runs along your application or it can be centralized with multi-tenancy. One CMS can service all of your ancillary marketing sites, blogs, or wherever you need the content.
The following instructions are also listed on the sample application readme so you're welcome to skip them if you want to look at the code.
Create a top-level directory to keep our application pair. This is temporary as the project matures.
mkdir beacon_sample
Clone GitHub - BeaconCMS/beacon: Beacon CMS to ./beacon
.
git clone git@github.com:BeaconCMS/beacon.git
Start with our first step from the Readme
mix phx.new --umbrella --install beacon_sample
Go to the umbrella project directory
cd beacon_sample/
Initialize git
git init
Commit the freshly initialized project
Initial commit of Phoenix v1.6.15
as of the time of this writing.Add :beacon as a dependency to both apps in your umbrella project
# Local:
{:beacon, path: "../../../beacon"},
# Or from GitHub:
{:beacon, github: "beaconCMS/beacon"},
apps/beacon_sample/mix.exs
and apps/beacon_sample_web/mix.exs
under the section defp deps do
.mix deps.get
to install the dependencies.Commit the changes.
Add :beacon as a dependency to both apps in your umbrella project
seems like a good enough commit message.Configure Beacon Repo
Beacon.Repo
under the ecto_repos:
section in config/config.exs
.Configure the database in dev.exs
. We'll do production later.
# Configure beacon database
config :beacon, Beacon.Repo,
username: "postgres",
password: "postgres",
database: "beacon_sample_beacon",
hostname: "localhost",
show_sensitive_data_on_connection_error: true,
pool_size: 10
Commit the changes.
Configure Beacon Repo
subject with Configure the beacon repository in our dev only environment for now.
body.Create a BeaconDataSource module that implements Beacon.DataSource.Behaviour
Create apps/beacon_sample/lib/beacon_sample/datasource.ex
defmodule BeaconSample.BeaconDataSource do
@behaviour Beacon.DataSource.Behaviour
def live_data("my_site", ["home"], _params), do: %{vals: ["first", "second", "third"]}
def live_data("my_site", ["blog", blog_slug], _params), do: %{blog_slug_uppercase: String.upcase(blog_slug)}
def live_data(_, _, _), do: %{}
end
Add that DataSource to your config/config.exs
config :beacon,
data_source: BeaconSample.BeaconDataSource
Commit the changes.
Configure BeaconDataSource
Make router (apps/beacon_sample_web/lib/beacon_sample_web/router.ex
) changes to cover Beacon pages.
Add a :beacon
pipeline. I typically do this towards the pipeline sections at the top, starting at line 17.
pipeline :beacon do
plug BeaconWeb.Plug
end
Add a BeaconWeb
scope.
scope "/", BeaconWeb do
pipe_through :browser
pipe_through :beacon
live_session :beacon, session: %{"beacon_site" => "my_site"} do
live "/beacon/*path", PageLive, :path
end
end
Comment out existing scope.
# scope "/", BeaconSampleWeb do
# pipe_through :browser
# get "/", PageController, :index
# end
Commit the changes.
Add routing changes
Add some components to your apps/beacon_sample/priv/repo/seeds.exs
.
alias Beacon.Components
alias Beacon.Pages
alias Beacon.Layouts
alias Beacon.Stylesheets
Stylesheets.create_stylesheet!(%{
site: "my_site",
name: "sample_stylesheet",
content: "body {cursor: zoom-in;}"
})
Components.create_component!(%{
site: "my_site",
name: "sample_component",
body: """
<li>
<%= @val %>
</li>
"""
})
%{id: layout_id} =
Layouts.create_layout!(%{
site: "my_site",
title: "Sample Home Page",
meta_tags: %{"foo" => "bar"},
stylesheet_urls: [],
body: """
<header>
Header
</header>
<%= @inner_content %>
<footer>
Page Footer
</footer>
"""
})
%{id: page_id} =
Pages.create_page!(%{
path: "home",
site: "my_site",
layout_id: layout_id,
template: """
<main>
<h2>Some Values:</h2>
<ul>
<%= for val <- @beacon_live_data[:vals] do %>
<%= my_component("sample_component", val: val) %>
<% end %>
</ul>
<.form let={f} for={:greeting} phx-submit="hello">
Name: <%= text_input f, :name %> <%= submit "Hello" %>
</.form>
<%= if assigns[:message], do: assigns.message %>
</main>
"""
})
Pages.create_page!(%{
path: "blog/:blog_slug",
site: "my_site",
layout_id: layout_id,
template: """
<main>
<h2>A blog</h2>
<ul>
<li>Path Params Blog Slug: <%= @beacon_path_params.blog_slug %></li>
<li>Live Data blog_slug_uppercase: <%= @beacon_live_data.blog_slug_uppercase %></li>
</ul>
</main>
"""
})
Pages.create_page_event!(%{
page_id: page_id,
event_name: "hello",
code: """
{:noreply, Phoenix.LiveView.assign(socket, :message, "Hello \#{event_params["greeting"]["name"]}!")}
"""
})
Run ecto.reset
to create and seed our database(s).
cd apps/beacon_sample
.mix ecto.setup
(as our repos haven't been created yet).mix ecto.reset
thereafter.SafeCode
package works as expected.This is typically where we run into issues with safe_code
on the inner content of the layout seed, specifically:
** (RuntimeError) invalid_node:
assigns . :inner_content
<%= @inner_content %>
, seeding seems to complete.Running mix phx.server
throws another error:
** (RuntimeError) invalid_node:
assigns . :val
safe_code
is problematic and needs to be surgically removed from Beacon for now.In Beacon's repository, remove SafeCode.Validator.validate_heex!
function calls from the loaders
lib/beacon/loader/layout_module_loader.ex
lib/beacon/loader/page_module_loader.ex
lib/beacon/loader/component_module_loader.ex
Fix the seeder to work without SafeCode.
apps/beacon_sample/priv/repo/seeds.exs
under Pages.create_page!
from <%= for val <- live_data[:vals] do %>
to <%= for val <- live_data.vals do %>
.Commit the seeder changes.
Add component seeds
Enable Page Management and the Page Management API in router (apps/beacon_sample_web/lib/beacon_sample_web/router.ex
).
require BeaconWeb.PageManagement
require BeaconWeb.PageManagementApi
scope "/page_management", BeaconWeb.PageManagement do
pipe_through :browser
BeaconWeb.PageManagement.routes()
end
scope "/page_management_api", BeaconWeb.PageManagementApi do
pipe_through :api
BeaconWeb.PageManagementApi.routes()
end
Commit the Page Management router changes.
Add Page Management routes
Navigate to http://localhost:4000/beacon/home to view the main CMS page.
Header
, Some Values
, and Page Footer
with a zoom-in cursor over the page.Navigate to http://localhost:4000/beacon/blog/beacon_is_awesome to view the blog post.
Header
, A blog
, and Page Footer
with a zoom-in cursor over the page.Navigate to http://localhost:4000/page_management/pages to view the Page Management
section.
Listing Pages
, Reload Modules
, a list of pages, and New Page
.We should put the page management through its paces to determine weak points.
Add another more robust layout.
<main>
.<body>
section.stylesheet_urls
?Add another more robust component.
0.17.7
.A replica of Laravel Nova panel of pages. Welcome and Home are Laravel defaults. Users would be useful as we could integrate with phx gen auth
.
The dependency safe_code
was a problem during my first two attempts.
I ran into issues by failing to add a BeaconWeb
scope and adding it as BeaconSampleWeb
instead.
UndefinedFunctionError
as function BeaconSampleWeb.PageLive.__live__/0 is undefined (module BeaconSampleWeb.PageLive is not available)
.The sample isn't as "pristine" as I'd like due to the bug fix but it really shouldn't be a showstopper.
<head>
as inline <style>
tags.<body><div data-phx-main="true">
mix phx.server
) immediately boots our Beacon components before it shows the url.