<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Code, Love &amp; Boards</title>
  <subtitle>A blog about coding web apps, loving life and board sports</subtitle>
  <id>http://codeloveandboards.com/blog</id>
  <link href="http://codeloveandboards.com/blog"/>
  <link href="http://codeloveandboards.com/feed.xml" rel="self"/>
  <updated>2020-07-27T07:00:00Z</updated>
  <author>
    <name>Blog Author</name>
  </author>
  <entry>
    <title>Headless CMS fun with Phoenix LiveView and Airtable (pt. 4)</title>
    <link rel="alternate" href="http://codeloveandboards.com/blog/2020/07/27/headless-cms-fun-with-phoenix-liveview-and-airtable-pt-4/"/>
    <id>http://codeloveandboards.com/blog/2020/07/27/headless-cms-fun-with-phoenix-liveview-and-airtable-pt-4/</id>
    <published>2020-07-27T07:00:00Z</published>
    <updated>2020-07-27T15:30:58+02:00</updated>
    <author>
      <name>Article Author</name>
    </author>
    <content type="html">&lt;div class="index"&gt;
  &lt;p&gt;This post belongs to the &lt;strong&gt;Headless CMS fun with Phoenix LiveView and Airtable&lt;/strong&gt; series.&lt;/p&gt;
  &lt;ol&gt;
    &lt;li&gt;&lt;a href="/blog/2020/07/02/headless-cms-fun-with-phoenix-liveview-and-airtable-pt-1"&gt;Introduction.&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href="/blog/2020/07/11/headless-cms-fun-with-phoenix-liveview-and-airtable-pt-2"&gt;The project set up and implementing the repository pattern.&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href="/blog/2020/07/19/headless-cms-fun-with-phoenix-liveview-and-airtable-pt-3"&gt;Content rendering using Phoenix LiveView.&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href="/blog/2020/07/27/headless-cms-fun-with-phoenix-liveview-and-airtable-pt-4"&gt;Adding a cache to the repository and broadcasting changes to the views..&lt;/a&gt;&lt;/li&gt;
  &lt;/ol&gt;

  &lt;a href="https://phoenixcms.herokuapp.com/" target="_blank" class="btn"&gt;&lt;i class="fa fa-cloud"&gt;&lt;/i&gt; Live demo&lt;/a&gt;
  &lt;a href="https://github.com/bigardone/phoenix-cms" target="_blank"&gt;&lt;i class="fa fa-github"&gt;&lt;/i&gt; Source code&lt;/a&gt;
&lt;/div&gt;

&lt;p&gt;In the &lt;a href="/blog/2020/07/19/headless-cms-fun-with-phoenix-liveview-and-airtable-pt-3"&gt;previous part&lt;/a&gt;, we talked about Phoenix LiveView and implemented all the necessary views and templates to render all the content on our website. Each live view requests its contents to Airtable on its mount function, assigning them in its state. At first, it sounds like a proper implementation, similar to what we would have done using Ecto and a database to store the contents. However, the current solution has three main issues:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Using HTTP to request content against an external service adds an overhead to the initial page load, slowing down page loads.&lt;/li&gt;
&lt;li&gt;If, for whatever reason, the external service is down, we won&amp;#39;t be able to display any content to our visitors.&lt;/li&gt;
&lt;li&gt;Last but not least, Airtable is limited to 5 requests per second per base. If we exceed this rate, it will return a 429 status code, and we&amp;#39;ll need to wait 30 seconds before a new request succeeds.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So, how can we solve these issues? The solution we are going to implement consists of adding an in-memory cache to the repository, which stores both the contents and the articles, and synchronizes every second with Airtable. This way, we remove the additional overhead, render content even if Airtable happens to be down, and we control the number of requests per second done in a single place. Let&amp;#39;s do this!&lt;/p&gt;

&lt;p&gt;&lt;img class="center" src="/images/blog/phoenix-cms-1/site.jpg"/&gt;&lt;/p&gt;

&lt;h2&gt;The repository cache&lt;/h2&gt;

&lt;p&gt;Erlang/OTP/Elixir comes with a robust in-memory term storage, called &lt;a href="http://erlang.org/doc/man/ets.html"&gt;ETS&lt;/a&gt;, out of the box. It can store large amounts of data offering constant time data access. &lt;strong&gt;ETS&lt;/strong&gt; organizes data as a set of dynamic tables, consisting of tuples, containing a key and the stored term. Tables are created by a process that is its owner and deleted when this process dies. In Elixir, &lt;strong&gt;ETS&lt;/strong&gt; tables are often managed using a &lt;a href="https://hexdocs.pm/elixir/GenServer.html"&gt;GenServer&lt;/a&gt; process, and in our particular case, we need two different tables, ergo two different cache processes, one to store &lt;code&gt;PhoenixCms.Content&lt;/code&gt; terms and the other to store &lt;code&gt;PhoenixCms.Article&lt;/code&gt;. Knowing this, let&amp;#39;s implement the generic &lt;strong&gt;GenServer&lt;/strong&gt; cache definition:&lt;/p&gt;

&lt;pre&gt;&lt;code class="elixir"&gt;# lib/phoenix_cms/repo/cache.ex

defmodule PhoenixCms.Repo.Cache do
  use GenServer

  @callback table_name :: atom
  @callback start_link(keyword) :: GenServer.on_start()

  @impl GenServer
  def init(name) do
    name
    |&amp;gt; table_for()
    |&amp;gt; :ets.new([:ordered_set, :protected, :named_table])

    {:ok, %{name: name}}
  end

  defp table_for(name), do: apply(name, :table_name, [])
end
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This module defines the behavior that any cache process must fulfill, which is initially a &lt;code&gt;table_name/0&lt;/code&gt; function, which returns the ETS table name for that cache and a &lt;code&gt;start_link/1&lt;/code&gt; function, which defines how the cache process starts. The module also defines the generic logic of a cache process, like the &lt;code&gt;init/1&lt;/code&gt; function, which using &lt;code&gt;name&lt;/code&gt; with the &lt;code&gt;private table_for/1&lt;/code&gt; function, gets the table name, depending on the current cache module, and creates the ETS table using the following parameters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;name&lt;/code&gt;, an atom representing the table&amp;#39;s name, in our case &lt;code&gt;:articles&lt;/code&gt; or &lt;code&gt;:contents&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:ordered_set&lt;/code&gt;, which specifies the type of the table, in our case, an ordered set that contains a value per unique key.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:protected&lt;/code&gt;, which sets the access control, in our case read is available for all processes, but write operations are permitted only from the owner process, avoiding race conditions.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:named_table&lt;/code&gt;, makes the table accessible by name instead of the by its PID.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Finally, it sets the initial process state to a map containing the cache module name, which we&amp;#39;ll use later on. Now let&amp;#39;s define the specific cache modules:&lt;/p&gt;

&lt;pre&gt;&lt;code class="elixir"&gt;# lib/phoenix_cms/article/cache.ex

defmodule PhoenixCms.Article.Cache do
  alias PhoenixCms.Repo.Cache

  @behaviour Cache

  def child_spec(opts) do
    %{
      id: __MODULE__,
      start: {__MODULE__, :start_link, [opts]},
      type: :worker,
      restart: :permanent,
      shutdown: 500
    }
  end

  @impl Cache
  def table_name, do: :articles

  @impl Cache
  def start_link(_args) do
    GenServer.start_link(Cache, __MODULE__, name: __MODULE__)
  end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;pre&gt;&lt;code class="elixir"&gt;# lib/phoenix_cms/content/cache.ex

defmodule PhoenixCms.Content.Cache do
  alias PhoenixCms.Repo.Cache

  @behaviour Cache

  def child_spec(opts) do
    %{
      id: __MODULE__,
      start: {__MODULE__, :start_link, [opts]},
      type: :worker,
      restart: :permanent,
      shutdown: 500
    }
  end

  @impl Cache
  def table_name, do: :contents

  @impl Cache
  def start_link(_args) do
    GenServer.start_link(Cache, __MODULE__, name: __MODULE__)
  end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Both modules fulfill the &lt;code&gt;PhoenixCms.Repo.Cache&lt;/code&gt; behavior by implementing the &lt;code&gt;table_name/0&lt;/code&gt; function, which returns the corresponding table name, and &lt;code&gt;start_link/1&lt;/code&gt;, starts a new &lt;code&gt;PhoenixCms.Repo.Cache&lt;/code&gt; &lt;code&gt;GenServer&lt;/code&gt;, and uses their module name to set the initial state and name the process. To start both cache processes when the application starts, let&amp;#39;s add them to the main supervision tree:&lt;/p&gt;

&lt;pre&gt;&lt;code class="elixir"&gt;# lib/phoenix_cms/application.ex

defmodule PhoenixCms.Application do
  use Application

  def start(_type, _args) do
    children = [
      # ...

      # Start cache processes
      PhoenixCms.Article.Cache,
      PhoenixCms.Content.Cache,

      # ...
    ]

    opts = [strategy: :one_for_one, name: PhoenixCms.Supervisor]
    Supervisor.start_link(children, opts)
  end

  # ...
end
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;To check that everything works fine, let&amp;#39;s start IEX and get the cache processes info:&lt;/p&gt;

&lt;pre&gt;&lt;code class="console"&gt;➜ iex -S mix
Erlang/OTP 23 [erts-11.0.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1]

Interactive Elixir (1.10.4) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)&amp;gt; PhoenixCms.Article.Cache |&amp;gt; GenServer.whereis() |&amp;gt; Process.info()
[
  registered_name: PhoenixCms.Article.Cache,
  current_function: {:gen_server, :loop, 7},
  initial_call: {:proc_lib, :init_p, 5},
  status: :waiting,
  message_queue_len: 0,
  links: [#PID&amp;lt;0.318.0&amp;gt;],
  dictionary: [
    &amp;quot;$initial_call&amp;quot;: {PhoenixCms.Repo.Cache, :init, 1},
    &amp;quot;$ancestors&amp;quot;: [PhoenixCms.Supervisor, #PID&amp;lt;0.317.0&amp;gt;]
  ],
  trap_exit: false,
  error_handler: :error_handler,
  priority: :normal,
  group_leader: #PID&amp;lt;0.316.0&amp;gt;,
  total_heap_size: 233,
  heap_size: 233,
  stack_size: 12,
  reductions: 44,
  garbage_collection: [
    max_heap_size: %{error_logger: true, kill: true, size: 0},
    min_bin_vheap_size: 46422,
    min_heap_size: 233,
    fullsweep_after: 65535,
    minor_gcs: 0
  ],
  suspending: []
]
iex(2)&amp;gt; PhoenixCms.Content.Cache |&amp;gt; GenServer.whereis() |&amp;gt; Process.info()
[
  registered_name: PhoenixCms.Content.Cache,
  current_function: {:gen_server, :loop, 7},
  initial_call: {:proc_lib, :init_p, 5},
  status: :waiting,
  message_queue_len: 0,
  links: [#PID&amp;lt;0.318.0&amp;gt;],
  dictionary: [
    &amp;quot;$initial_call&amp;quot;: {PhoenixCms.Repo.Cache, :init, 1},
    &amp;quot;$ancestors&amp;quot;: [PhoenixCms.Supervisor, #PID&amp;lt;0.317.0&amp;gt;]
  ],
  trap_exit: false,
  error_handler: :error_handler,
  priority: :normal,
  group_leader: #PID&amp;lt;0.316.0&amp;gt;,
  total_heap_size: 233,
  heap_size: 233,
  stack_size: 12,
  reductions: 44,
  garbage_collection: [
    max_heap_size: %{error_logger: true, kill: true, size: 0},
    min_bin_vheap_size: 46422,
    min_heap_size: 233,
    fullsweep_after: 65535,
    minor_gcs: 0
  ],
  suspending: []
]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Since the processes are up, both &lt;code&gt;ETS&lt;/code&gt; tables must have been created as well, let&amp;#39;s confirm it:&lt;/p&gt;

&lt;pre&gt;&lt;code class="console"&gt;iex(3)&amp;gt; :ets.info(:articles)
[
  id: #Reference&amp;lt;0.2785746427.4276748289.246266&amp;gt;,
  decentralized_counters: false,
  read_concurrency: false,
  write_concurrency: false,
  compressed: false,
  memory: 145,
  owner: #PID&amp;lt;0.326.0&amp;gt;,
  heir: :none,
  name: :articles,
  size: 0,
  node: :nonode@nohost,
  named_table: true,
  type: :ordered_set,
  keypos: 1,
  protection: :protected
]
iex(4)&amp;gt; :ets.info(:contents)
[
  id: #Reference&amp;lt;0.2785746427.4276748289.246267&amp;gt;,
  decentralized_counters: false,
  read_concurrency: false,
  write_concurrency: false,
  compressed: false,
  memory: 145,
  owner: #PID&amp;lt;0.327.0&amp;gt;,
  heir: :none,
  name: :contents,
  size: 0,
  node: :nonode@nohost,
  named_table: true,
  type: :ordered_set,
  keypos: 1,
  protection: :protected
]
iex(5)&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;There we go! And if we check both cache processes PIDs, they should match their corresponding table owner&amp;#39;s PID:&lt;/p&gt;

&lt;pre&gt;&lt;code class="console"&gt;iex(5)&amp;gt; GenServer.whereis(PhoenixCms.Article.Cache)
#PID&amp;lt;0.326.0&amp;gt;
iex(6)&amp;gt; GenServer.whereis(PhoenixCms.Content.Cache)
#PID&amp;lt;0.327.0&amp;gt;
iex(7)&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Let&amp;#39;s add some helper functions to the cache module to get and set data from the corresponding ETS table:&lt;/p&gt;

&lt;pre&gt;&lt;code class="elixir"&gt;# lib/phoenix_cms/repo/cache.ex

defmodule PhoenixCms.Repo.Cache do
  # ...

  def all(cache) do
    cache
    |&amp;gt; table_for()
    |&amp;gt; :ets.tab2list()
    |&amp;gt; case do
      values when values != [] -&amp;gt;
        {:ok, Enum.map(values, &amp;amp;elem(&amp;amp;1, 1))}

      _ -&amp;gt;
        {:error, :not_found}
    end
  end

  def get(cache, key) do
    cache
    |&amp;gt; table_for()
    |&amp;gt; :ets.lookup(key)
    |&amp;gt; case do
      [{^key, value} | _] -&amp;gt;
        {:ok, value}

      _ -&amp;gt;
        {:error, :not_found}
    end
  end

  def set_all(cache, items), do: GenServer.cast(cache, {:set_all, items})

  def set(cache, id, item), do: GenServer.cast(cache, {:set, id, item})

  # ...

  @impl GenServer
  def handle_cast({:set_all, items}, %{name: name} = state)
      when is_list(items) do
    Enum.each(items, &amp;amp;:ets.insert(table_for(name), {&amp;amp;1.id, &amp;amp;1}))

    {:noreply, state}
  end

  def handle_cast({:set, id, item}, %{name: name} = state) do
    name
    |&amp;gt; table_for()
    |&amp;gt; :ets.insert({id, item})

    {:noreply, state}
  end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Let&amp;#39;s take a closer look at them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;all/1&lt;/code&gt; takes the cache module, which uses to get the table name and calls &lt;code&gt;:ets.tab2list/1&lt;/code&gt;, which returns all the entry tuples of a given table, which maps to a list of values, or an &lt;code&gt;{:error, :not_found}&lt;/code&gt; if empty.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;get/2&lt;/code&gt; receives the cache module and a key, and does the same as &lt;code&gt;all/1&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;set_all/2&lt;/code&gt; and &lt;code&gt;set/3&lt;/code&gt; are different tho. Since we configured the table as protected, we can only insert data from the process which created the table. Therefore, it sends the corresponding messages to the processes using &lt;code&gt;GensServer.cast/2&lt;/code&gt;. It implements both message callback functions, which insert all of the given entries or a given entry by its ID correspondently.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let&amp;#39;s refactor the repository module to include the cache in its logic, and avoid unnecessary HTTP requests when the data already exists in the cache:&lt;/p&gt;

&lt;pre&gt;&lt;code class="elixir"&gt;# lib/phoenix_cms/repo.ex

defmodule PhoenixCms.Repo do
  alias PhoenixCms.{Article, Content}
  # ...

  def articles(skip_cache \\ false)
  def articles(false), do: all(Article)
  def articles(true), do: @adapter.all(Article)

  def contents(skip_cache \\ false)
  def contents(false), do: all(Content)
  def contents(true), do: @adapter.all(Content)

  def get_article(id), do: get(Article, id)

  defp all(entity) do
    with cache &amp;lt;- cache_for(entity),
         {:error, :not_found} &amp;lt;- Cache.all(cache),
         {:ok, items} &amp;lt;- @adapter.all(entity) do
      Cache.set_all(cache, items)
      {:ok, items}
    end
  end

  defp get(entity, id) do
    with cache &amp;lt;- cache_for(entity),
         {:error, :not_found} &amp;lt;- Cache.get(cache, id),
         {:ok, item} &amp;lt;- @adapter.get(entity, id) do
      Cache.set(cache, id, item)
      {:ok, item}
    end
  end

  defp cache_for(Article), do: PhoenixCms.Article.Cache
  defp cache_for(Content), do: PhoenixCms.Content.Cache
end
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;We are adding a new &lt;code&gt;skip_cache&lt;/code&gt; parameter that when is &lt;code&gt;false&lt;/code&gt;, instead of directly calling the adapter in the public functions, now it checks if the requested items exist in the cache, calling the adapter if not and populating them. Hence, the next request has the data already cached. When it is &lt;code&gt;true&lt;/code&gt;, it uses the adapter directly, skipping the cache, and we&amp;#39;ll use this variation in a minute. Let&amp;#39;s navigate through the application checking out the logs:&lt;/p&gt;

&lt;pre&gt;&lt;code class="console"&gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; Homepage first visit
[info] GET /
[info] GET https://api.airtable.com/v0/appXTw8FgG3h55fk6/contents -&amp;gt; 200 (663.964 ms)
[info] Sent 200 in 834ms

&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; Blog page first visit
[info] GET /blog
[info] Sent 200 in 400µs
[info] GET https://api.airtable.com/v0/appXTw8FgG3h55fk6/articles -&amp;gt; 200 (225.874 ms)

&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; Blog detail first visit
[info] GET /blog/rec1osLptzsXfWg5g/lorem-ipsum
[info] Sent 200 in 422µs

&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; Homepage second visit
[info] GET /
[info] Sent 200 in 456µs

&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; Blog page second visit
[info] GET /blog
[info] Sent 200 in 531µs
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The first time we visit the home and blog pages, it performs the corresponding HTTP requests against Airtable. However, when visiting an article page, since the article has already been cached from the previous request, it does not need to request it. The same happens when we visit both the home and blog pages a second time.&lt;/p&gt;

&lt;h3&gt;Synchronizing the caches&lt;/h3&gt;

&lt;p&gt;Although the current cache implementation covers the issues that we have using the HTTP client directly, we now face a new problem. If we update any data stored in Airtable, the changes will not be reflected in our application, as the cache is already populated. Currently, Airtable does not have any mechanism to report data changes, such as webhooks. Nevertheless, we can achieve this using services like Zapier, but it only works for new rows or row deletions, not for updates, which is not suitable for our needs. Therefore, let&amp;#39;s build or own cache synchronization solution, which is very easy, thanks to Elixir.&lt;/p&gt;

&lt;p&gt;The main idea is to have two new processes, one for each cache, that periodically make requests against Airtable, updating their corresponding cache if there are new changes. Let&amp;#39;s define the new module:&lt;/p&gt;

&lt;pre&gt;&lt;code class="elixir"&gt;# lib/phoenix_cms/repo/cache/synchronizer.ex

defmodule PhoenixCms.Repo.Cache.Synchronizer do
  alias PhoenixCms.Repo.Cache

  use GenServer

  @refresh_time :timer.seconds(1)

  def start_link(opts) do
    GenServer.start_link(__MODULE__, opts)
  end

  @impl GenServer
  def init(opts) do
    cache = Keyword.fetch!(opts, :cache)

    send(self(), :sync)

    {:ok, cache}
  end

  @impl GenServer
  def handle_info(:sync, cache) do
    with {:ok, items} &amp;lt;- apply(cache, :fetch_fn, []).() do
      Cache.set_all(cache, items)
    end

    schedule(cache)

    {:noreply, cache}
  end

  defp schedule(cache) do
    Process.send_after(self(), :sync, @refresh_time)
  end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;One more time, we are using &lt;a href="https://hexdocs.pm/elixir/GenServer.html"&gt;GenServer&lt;/a&gt; for the new process. Its&lt;code&gt;init/1&lt;/code&gt; function takes the mandatory cache key from the options, which can contain either &lt;code&gt;PhoenixCms.Article.Cache&lt;/code&gt; or &lt;code&gt;PhoenixCms.Content.Cache&lt;/code&gt;, and sets it as the initial state of the process. It also sends itself a &lt;code&gt;:sync&lt;/code&gt; message, which handles in the &lt;code&gt;handle_info(:sync, cache)&lt;/code&gt; callback, applying the &lt;code&gt;:fetch_fn&lt;/code&gt; function to the cache module, and setting the items in the cache if the request succeeds. Finally, it calls the &lt;code&gt;schedule/1&lt;/code&gt; private function, which sends the &lt;code&gt;:sync&lt;/code&gt; message again after one second. So, we now have a process that requests the corresponding data to Airbrake every second, updating the corresponding cache table. Now we need to start these processes, so let&amp;#39;s refactor the cache module:&lt;/p&gt;

&lt;pre&gt;&lt;code class="elixir"&gt;# lib/phoenix_cms/repo/cache.ex

defmodule PhoenixCms.Repo.Cache do
  use GenServer

  alias __MODULE__.Synchronizer

  @callback fetch_fn :: fun

  # ...

  @impl GenServer
  def init(name) do
    Process.flag(:trap_exit, true)

    name
    |&amp;gt; table_for()
    |&amp;gt; :ets.new([:ordered_set, :protected, :named_table])

    {:ok, pid} = Synchronizer.start_link(cache: name)
    ref = Process.monitor(pid)

    {:ok, %{name: name, synchronizer_ref: ref}}
  end

  # ...

  @impl GenServer
  def handle_info(
        {:DOWN, ref, :process, _object, _reason},
        %{synchronizer_ref: ref, name: name} = state
      ) do
    {:ok, pid} = Synchronizer.start_link(cache: name)
    ref = Process.monitor(pid)

    {:noreply, %{state | synchronizer_ref: ref}}
  end

  def handle_info({:EXIT, _, _}, state) do
    {:noreply, state}
  end

  # ...
end
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Once the cache process initializes, it creates the corresponding ETS table as it was doing before and starts its synchronizer process. Since the synchronizer process links to the cache process, it does the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;First of all, it traps exits, so if the synchronizer dies, it does not kill the cache process, implementing &lt;code&gt;handle_info({:EXIT, _, _}, state)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Secondly, it monitors the synchronizer process, storing the monitor ref in its state, so in case the synchronizer process dies, it spawns a new one in &lt;code&gt;handle_info( {:DOWN, ref, :process, _, _}, %{synchronizer_ref: ref)&lt;/code&gt;, so that the cache keeps up to date with its source of truth, which is Airtable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Finally, we need to implement the &lt;code&gt;fetch_fn/0&lt;/code&gt; function in the cache modules:&lt;/p&gt;

&lt;pre&gt;&lt;code class="elixir"&gt;# lib/phoenix_cms/article/cache.ex

defmodule PhoenixCms.Article.Cache do
  alias PhoenixCms.{Repo, Repo.Cache}

  @behaviour Cache

  # ...

  @impl Cache
  def fetch_fn, do: fn -&amp;gt; Repo.articles(true) end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;pre&gt;&lt;code class="elixir"&gt;# lib/phoenix_cms/content/cache.ex

defmodule PhoenixCms.Content.Cache do
  alias PhoenixCms.{Repo, Repo.Cache}

  @behaviour Cache

  # ...

  @impl Cache
  def fetch_fn, do: fn -&amp;gt; Repo.contents(true) end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Each function calls the corresponding repository, passing true as the &lt;code&gt;skip_cache&lt;/code&gt; parameter so that it always checks against Airtable. Let&amp;#39;s go back to IEX and start &lt;code&gt;:observer&lt;/code&gt; to check the application tree:&lt;/p&gt;

&lt;p&gt;&lt;img class="center" src="/images/blog/phoenix-cms-1/application-tree.png"/&gt;&lt;/p&gt;

&lt;p&gt;We can see both the &lt;code&gt;PhoenixCms.Article.Cache&lt;/code&gt; and &lt;code&gt;PhoenixCms.Content.Cache&lt;/code&gt; processes, each of them linked to their &lt;code&gt;PhoenixCms.Repo.Cache.Synchronizer&lt;/code&gt; process. And if we check the logs, we can see the following:&lt;/p&gt;

&lt;pre&gt;&lt;code class="console"&gt;iex(3)&amp;gt; [info] GET https://api.airtable.com/v0/appXTw8FgG3h55fk6/articles -&amp;gt; 200 (126.214 ms)
[info] GET https://api.airtable.com/v0/appXTw8FgG3h55fk6/contents -&amp;gt; 200 (121.886 ms)
[info] GET https://api.airtable.com/v0/appXTw8FgG3h55fk6/articles -&amp;gt; 200 (123.290 ms)
[info] GET https://api.airtable.com/v0/appXTw8FgG3h55fk6/contents -&amp;gt; 200 (125.372 ms)
[info] GET https://api.airtable.com/v0/appXTw8FgG3h55fk6/articles -&amp;gt; 200 (140.528 ms)
[info] GET https://api.airtable.com/v0/appXTw8FgG3h55fk6/contents -&amp;gt; 200 (116.197 ms)
[info] GET https://api.airtable.com/v0/appXTw8FgG3h55fk6/articles -&amp;gt; 200 (123.440 ms)
[info] GET https://api.airtable.com/v0/appXTw8FgG3h55fk6/contents -&amp;gt; 200 (121.408 ms)
[info] GET https://api.airtable.com/v0/appXTw8FgG3h55fk6/articles -&amp;gt; 200 (121.811 ms)
[info] GET https://api.airtable.com/v0/appXTw8FgG3h55fk6/contents -&amp;gt; 200 (118.887 ms)
[info] GET https://api.airtable.com/v0/appXTw8FgG3h55fk6/articles -&amp;gt; 200 (437.112 ms)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If we now change anything in Airtable, and we refresh the browser tab with our application, it should render the modifications correctly.&lt;/p&gt;

&lt;h3&gt;Real-time UI updates&lt;/h3&gt;

&lt;p&gt;We currently have everything working as planned. All the content is stored in an external service, and we get the content using an HTTP client pointing to its API. We also have a cache mechanism that auto-updates the stored data, and which prevents the repository from making additional HTTP requests. Finally, we display all the content using Phoenix LiveView, which lets us render updates in the UI in real-time. But, wait a sec! With our current implementation, that we have to refresh the browser manually to display content updates, we could have used regular Phoenix views. So what&amp;#39;s the point of using LiveView anyways? The point is that we can broadcast changes to live views, which will render them to the visitor without having to refresh the browser whatsoever.&lt;/p&gt;

&lt;p&gt;To broadcast changes to the view, we are going to be using &lt;a href="https://hexdocs.pm/phoenix_pubsub/Phoenix.PubSub.html"&gt;Phoenix.PubSub&lt;/a&gt;, which comes by default with Phoenix.  Let&amp;#39;s do some refactoring to the cache module:&lt;/p&gt;

&lt;pre&gt;&lt;code class="elixir"&gt;# lib/phoenix_cms/repo/cache.ex

defmodule PhoenixCms.Repo.Cache do
  use GenServer

  alias __MODULE__.Synchronizer

  # ...

  @callback topic :: String.t()

  @secret &amp;quot;cache secret&amp;quot;

  # ...

  @impl GenServer
  def init(name) do
    # ...

    {:ok, %{name: name, synchronizer_ref: ref, hash: &amp;quot;&amp;quot;}}
  end

  # ...

  @impl GenServer
  def handle_cast({:set_all, items}, %{name: name, hash: hash} = state)
      when is_list(items) do
    new_hash = generate_hash(items)

    if hash != new_hash do
      Enum.each(items, &amp;amp;:ets.insert(table_for(name), {&amp;amp;1.id, &amp;amp;1}))
      PhoenixCmsWeb.Endpoint.broadcast(apply(name, :topic, []), &amp;quot;update&amp;quot;, %{})
    end

    {:noreply, %{state | hash: new_hash}}
  end

  # ...

  defp generate_hash(items) do
    :sha256
    |&amp;gt; :crypto.hmac(@secret, :erlang.term_to_binary(items))
    |&amp;gt; Base.encode64()
  end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;First of all, we are defining a new callback function to the repository behavior, &lt;code&gt;topic&lt;/code&gt;, which must return the topic in which we are going the broadcast the changes in the current cache. In the initial state, we are also adding a new hash empty string. While handling {:set_all, items} messages, we generate a new hash of with the items, and if the hash is different to the one stored in the state, it inserts all the items in the ETS table, like it previously did, and calls &lt;code&gt;PhoenixCmsWeb.Endpoint.broadcast/3&lt;/code&gt;, using the &lt;code&gt;:topic&lt;/code&gt; function from the cache module. Finally, it sets the new hash in its state. This way, we are reporting to any subscriber of &lt;code&gt;topic&lt;/code&gt; that there are changes when the hashes are different. Moreover, it is also storing them and preventing unnecessary writes when there are no data differences.&lt;/p&gt;

&lt;p&gt;Let&amp;#39;s implement the topic function in both cache modules:&lt;/p&gt;

&lt;pre&gt;&lt;code class="elixir"&gt;# lib/phoenix_cms/article/cache.ex

defmodule PhoenixCms.Article.Cache do
  alias PhoenixCms.{Repo, Repo.Cache}

  @behaviour Cache

  @topic &amp;quot;articles&amp;quot;

  # ...

  @impl Cache
  def topic, do: @topic
end
&lt;/code&gt;&lt;/pre&gt;

&lt;pre&gt;&lt;code class="elixir"&gt;# lib/phoenix_cms/content/cache.ex

defmodule PhoenixCms.Content.Cache do
  alias PhoenixCms.{Repo, Repo.Cache}

  @behaviour Cache

  @topic &amp;quot;contents&amp;quot;

  # ...

  @impl Cache
  def topic, do: @topic
end
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The last step is to subscribe to the corresponding topics in the live views:&lt;/p&gt;

&lt;pre&gt;&lt;code class="elixir"&gt;# lib/phoenix_cms_web/live/page_live.ex

defmodule PhoenixCmsWeb.PageLive do
  use PhoenixCmsWeb, :live_view

  @topic &amp;quot;contents&amp;quot;

  @impl true
  def mount(_params, _session, socket) do
    PhoenixCmsWeb.Endpoint.subscribe(@topic)

    {:ok, assign_socket(socket)}
  end

  @impl true
  def handle_info(%{event: &amp;quot;update&amp;quot;}, socket) do
    {:noreply, assign_socket(socket)}
  end

  # ...
end

# lib/phoenix_cms_web/live/articles_live.ex

defmodule PhoenixCmsWeb.ArticlesLive do
  use PhoenixCmsWeb, :live_view

  alias PhoenixCmsWeb.LiveEncoder

  @topic &amp;quot;articles&amp;quot;

  @impl true
  def mount(_params, _session, socket) do
    PhoenixCmsWeb.Endpoint.subscribe(@topic)

    {:ok, assign_socket(socket)}
  end

  @impl true
  def handle_info(%{event: &amp;quot;update&amp;quot;}, socket) do
    {:noreply, assign_socket(socket)}
  end

  # ...
end

# lib/phoenix_cms_web/live/show_article_live.ex

defmodule PhoenixCmsWeb.ShowArticleLive do
  use PhoenixCmsWeb, :live_view

  @topic &amp;quot;articles&amp;quot;

  @impl true
  def mount(%{&amp;quot;id&amp;quot; =&amp;gt; id}, _session, socket) do
    PhoenixCmsWeb.Endpoint.subscribe(@topic)

    {:ok, assign_socket(socket, id)}
  end

  @impl true
  def handle_info(%{event: &amp;quot;update&amp;quot;}, socket) do
    id = socket.assigns.article.id

    {:noreply, assign_socket(socket, id)}
  end

  # ...
end
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;In the &lt;code&gt;mount/3&lt;/code&gt; function, each view subscribes to the relevant topic. Once the view process adheres to the topic, it needs to handle incoming messages using &lt;code&gt;handle_info/2&lt;/code&gt;, which reassigns the socket contents, triggering a new render in the visitor&amp;#39;s screen. Let&amp;#39;s jump back to the browser, change something in Airtable, and watch what happens in our application:&lt;/p&gt;

&lt;p&gt;&lt;img class="center" src="/images/blog/phoenix-cms-1/real-time.gif"/&gt;&lt;/p&gt;

&lt;p&gt;It works! We finally have finished our simple CMS using Phoenix and Airtable, yay! I hope you enjoyed this tutorial as much as I enjoyed writing it and implementing it. You can check the final result &lt;a href="https://phoenixcms.herokuapp.com/"&gt;here&lt;/a&gt;, or have a look at the &lt;a href="https://github.com/bigardone/phoenix-cms"&gt;source code&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

&lt;div class="btn-wrapper"&gt;
  &lt;a href="https://phoenixcms.herokuapp.com/" target="_blank" class="btn"&gt;&lt;i class="fa fa-cloud"&gt;&lt;/i&gt; Live demo&lt;/a&gt;
  &lt;a href="https://github.com/bigardone/phoenix-cms" target="_blank" class="btn"&gt;&lt;i class="fa fa-github"&gt;&lt;/i&gt; Source code&lt;/a&gt;
&lt;/div&gt;
</content>
  </entry>
  <entry>
    <title>Headless CMS fun with Phoenix LiveView and Airtable (pt. 3)</title>
    <link rel="alternate" href="http://codeloveandboards.com/blog/2020/07/19/headless-cms-fun-with-phoenix-liveview-and-airtable-pt-3/"/>
    <id>http://codeloveandboards.com/blog/2020/07/19/headless-cms-fun-with-phoenix-liveview-and-airtable-pt-3/</id>
    <published>2020-07-20T06:14:00Z</published>
    <updated>2020-07-27T15:31:19+02:00</updated>
    <author>
      <name>Article Author</name>
    </author>
    <content type="html">&lt;div class="index"&gt;
  &lt;p&gt;This post belongs to the &lt;strong&gt;Headless CMS fun with Phoenix LiveView and Airtable&lt;/strong&gt; series.&lt;/p&gt;
  &lt;ol&gt;
    &lt;li&gt;&lt;a href="/blog/2020/07/02/headless-cms-fun-with-phoenix-liveview-and-airtable-pt-1"&gt;Introduction.&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href="/blog/2020/07/11/headless-cms-fun-with-phoenix-liveview-and-airtable-pt-2"&gt;The project set up and implementing the repository pattern.&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href="/blog/2020/07/19/headless-cms-fun-with-phoenix-liveview-and-airtable-pt-3"&gt;Content rendering using Phoenix LiveView.&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href="/blog/2020/07/27/headless-cms-fun-with-phoenix-liveview-and-airtable-pt-4"&gt;Adding a cache to the repository and broadcasting changes to the views..&lt;/a&gt;&lt;/li&gt;
  &lt;/ol&gt;

  &lt;a href="https://phoenixcms.herokuapp.com/" target="_blank" class="btn"&gt;&lt;i class="fa fa-cloud"&gt;&lt;/i&gt; Live demo&lt;/a&gt;
  &lt;a href="https://github.com/bigardone/phoenix-cms" target="_blank"&gt;&lt;i class="fa fa-github"&gt;&lt;/i&gt; Source code&lt;/a&gt;
&lt;/div&gt;

&lt;p&gt;In the &lt;a href="/blog/2020/07/11/headless-cms-fun-with-phoenix-liveview-and-airtable-pt-2"&gt;previous part&lt;/a&gt;, we generated the base application, and the Airtable API HTTP client to request both contents and blog articles. We also defined the &lt;code&gt;Article&lt;/code&gt; and &lt;code&gt;Content&lt;/code&gt; domain models, and implemented the repository pattern with two different adapters, one returning fake data for testing purposes, and the other using the Airtable HTTP client to request and convert the returned data into our domain. It&amp;#39;s time for some front-end fun, so let&amp;#39;s get cracking.&lt;/p&gt;

&lt;p&gt;&lt;img class="center" src="/images/blog/phoenix-cms-1/site.jpg"/&gt;&lt;/p&gt;

&lt;h2&gt;Rendering content using LiveView&lt;/h2&gt;

&lt;p&gt;One thing before continuing, though. I&amp;#39;m using &lt;a href="https://bulma.io/"&gt;Bulma&lt;/a&gt;, which is very good looking and easy to use CSS framework for the UI styles. To use it, you need to add &lt;a href="https://github.com/bigardone/phoenix-cms/blob/50c2fa1c83df5ffb9d13b92e9f2742fe1e013b55/lib/phoenix_cms_web/templates/layout/root.html.leex#L8"&gt;this line&lt;/a&gt; in the &lt;code&gt;root.html.leex&lt;/code&gt; template, and &lt;a href="https://github.com/bigardone/phoenix-cms/blob/master/assets/css/app.scss"&gt;here&lt;/a&gt; you can find the CSS file with the custom styles.&lt;/p&gt;

&lt;p&gt;What is &lt;a href="https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html"&gt;Phoenix.LiveView&lt;/a&gt;? The short definition would be: a library which provides rich, real-time user experiences with server-rendered HTML, without having to write almost any JS whatsoever, only using plain Elixir. But in reality, it is a bit more complicated.&lt;/p&gt;

&lt;p&gt;&lt;a href="LiveView"&gt;LiveView&lt;/a&gt; initially renders static HTML, which is fast and optimal for search and indexing engines. After the first rendering, it upgrades to a persistent connection, with its state, and is capable of listening to messages from both other processes and the browser, and update its state. Once the state is updated, it re-renders the parts of the HTML corresponding to these changes.&lt;/p&gt;

&lt;p&gt;LiveView is currently so well integrated into Phoenix, that we can use them anywhere, including the router file as if they were controllers. Since we created the project with the &lt;code&gt;--live&lt;/code&gt; option, we already have everything we need to start using it, so let&amp;#39;s go ahead and edit the route file to add the three different live view that we need:&lt;/p&gt;

&lt;pre&gt;&lt;code class="elixir"&gt;# lib/phoenix_cms_web/router.ex

defmodule PhoenixCmsWeb.Router do
  use PhoenixCmsWeb, :router

  # ...


  scope &amp;quot;/&amp;quot;, PhoenixCmsWeb do
    pipe_through :browser

    live &amp;quot;/&amp;quot;, PageLive
    live &amp;quot;/blog&amp;quot;, ArticlesLive
    live &amp;quot;/blog/:id/:slug&amp;quot;, ShowArticleLive
  end

  # ...
end
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;We have three different routes in our application:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/&lt;/code&gt;: which renders the home page using the &lt;code&gt;PageLive&lt;/code&gt; live view.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/blog&lt;/code&gt;: which renders all the articles using the &lt;code&gt;ArticlesLive&lt;/code&gt; live view.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/blog/:id/:slug&lt;/code&gt;: which renders a given article using the &lt;code&gt;ShowArticleLive&lt;/code&gt; live view.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;Live navigation&lt;/h3&gt;

&lt;p&gt;LiveView provides support for live navigation using the browser&amp;#39;s pushState API, making it possible to navigate between pages without full page reloads. Let&amp;#39;s use this feature by adding links to both the home and the blog page in the main navigation bar:&lt;/p&gt;

&lt;pre&gt;&lt;code class="elixir"&gt;# lib/phoenix_cms_web/templates/layout/root.html.leex

&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&amp;quot;en&amp;quot;&amp;gt;
  # ...

    &amp;lt;nav class=&amp;quot;navbar has-shadow&amp;quot; role=&amp;quot;navigation&amp;quot; aria-label=&amp;quot;main navigation&amp;quot;&amp;gt;
      &amp;lt;div class=&amp;quot;container&amp;quot;&amp;gt;
        &amp;lt;div class=&amp;quot;navbar-brand&amp;quot;&amp;gt;
          &amp;lt;%= live_patch &amp;quot;PhoenixCMS&amp;quot;, to: Routes.live_path(@conn, PhoenixCmsWeb.PageLive), class: &amp;quot;navbar-item has-text-weight-bold has-text-link&amp;quot; %&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div class=&amp;quot;navbar-end&amp;quot;&amp;gt;
          &amp;lt;%= live_patch &amp;quot;Blog&amp;quot;, to: Routes.live_path(@conn, PhoenixCmsWeb.ArticlesLive), class: &amp;quot;navbar-item&amp;quot; %&amp;gt;
        &amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/nav&amp;gt;

    # ...
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;a href="https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.Helpers.html#live_patch/2"&gt;live_patch&lt;/a&gt; renders a link which patches the current &lt;code&gt;LiveView&lt;/code&gt; with the one specified in the &lt;code&gt;to&lt;/code&gt; option, without reloading the whole page and adding a new entry in the browser&amp;#39;s history. Now that we can navigate through our views let&amp;#39;s implement the home page.&lt;/p&gt;

&lt;h3&gt;The PageLive live view&lt;/h3&gt;

&lt;p&gt;&lt;img class="center" src="/images/blog/phoenix-cms-1/home.png"/&gt;&lt;/p&gt;

&lt;p&gt;Let&amp;#39;s start with the main home page:&lt;/p&gt;

&lt;pre&gt;&lt;code class="elixir"&gt;# lib/phoenix_cms_web/live/page_live.ex

defmodule PhoenixCmsWeb.PageLive do
  use PhoenixCmsWeb, :live_view

  alias PhoenixCmsWeb.LiveEncoder

  @impl true
  def mount(_params, _session, socket) do
    {:ok, assign_socket(socket)}
  end

  # Missing assign_socket function...
end
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The &lt;code&gt;mount/3&lt;/code&gt; function receives &lt;code&gt;params&lt;/code&gt;, the current &lt;code&gt;session&lt;/code&gt;, and the &lt;code&gt;socket&lt;/code&gt;, returning it with the assigned contents. Let&amp;#39;s implement the &lt;code&gt;assign_socket/1&lt;/code&gt; private function:&lt;/p&gt;

&lt;pre&gt;&lt;code class="elixir"&gt;# lib/phoenix_cms_web/live/page_live.ex

defmodule PhoenixCmsWeb.PageLive do
  use PhoenixCmsWeb, :live_view

  # ...

  defp assign_socket(socket) do
    case fetch_contents() do
      {:ok, contents} -&amp;gt;
        socket
        |&amp;gt; assign(:page_title, &amp;quot;Home&amp;quot;)
        |&amp;gt; assign(:contents, contents)
        |&amp;gt; put_flash(:error, nil)

      _ -&amp;gt;
        socket
        |&amp;gt; assign(:page_title, &amp;quot;Home&amp;quot;)
        |&amp;gt; assign(:contents, nil)
        |&amp;gt; put_flash(:error, &amp;quot;Error fetching data&amp;quot;)
    end
  end

  # Missing fetch_contents function...
end
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Depending on the result of the &lt;code&gt;fetch_contents/0&lt;/code&gt; function, it assigns &lt;code&gt;:contents&lt;/code&gt; or a flash &lt;code&gt;:error&lt;/code&gt;. The &lt;code&gt;fetch_contents/0&lt;/code&gt; looks like this:&lt;/p&gt;

&lt;pre&gt;&lt;code class="elixir"&gt;# lib/phoenix_cms_web/live/page_live.ex

defmodule PhoenixCmsWeb.PageLive do
  use PhoenixCmsWeb, :live_view

  # ...

  defp fetch_contents do
    with {:ok, contents} &amp;lt;- PhoenixCms.contents() do
      contents =
        contents
        |&amp;gt; Enum.sort_by(&amp;amp; &amp;amp;1.position)
        |&amp;gt; LiveEncoder.contents()

      {:ok, contents}
    end
  end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This function calls &lt;code&gt;PhoenixCms.contents/0&lt;/code&gt;, which we haven&amp;#39;t implemented yet, sorts contents by &lt;code&gt;position&lt;/code&gt; and calls &lt;code&gt;LiveEncoder.contents/1&lt;/code&gt;, which converts these &lt;code&gt;Content&lt;/code&gt; structs into the payload which the live view template is expecting. When working with Pheonix apps, I like to delegate any business logic functions that need the &lt;code&gt;*Web&lt;/code&gt; namespace from the main module, in our case &lt;code&gt;PhoenixCms&lt;/code&gt;, acting as the public API between business logic and presentation. Let&amp;#39;s go ahead and expose the functions that we need:&lt;/p&gt;

&lt;pre&gt;&lt;code class="elixir"&gt;# lib/phoenix_cms.ex

defmodule PhoenixCms do
  defdelegate articles, to: PhoenixCms.Repo

  defdelegate contents, to: PhoenixCms.Repo

  defdelegate get_article(id), to: PhoenixCms.Repo
end
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now we need to implement the &lt;code&gt;PhoenixCmsWeb.LiveEncoder&lt;/code&gt; module and convert the list of &lt;code&gt;PhoenixCms.Content&lt;/code&gt; into the payload that the live template needs to render:&lt;/p&gt;

&lt;pre&gt;&lt;code class="elixir"&gt;# lib/phoenix_cms_web/live/encoder.ex

defmodule PhoenixCmsWeb.LiveEncoder do
  alias PhoenixCms.Content

  def contents(items) when is_list(items) do
    {features, rest} =
      items
      |&amp;gt; Enum.map(&amp;amp;encode/1)
      |&amp;gt; Enum.split_with(&amp;amp;(&amp;amp;1.type == &amp;quot;feature&amp;quot;))

    rest
    |&amp;gt; Enum.concat([%{features: features}])
    |&amp;gt; List.flatten()
  end

  def encode(%Content{} = content) do
    Map.take(content, [:id, :type, :title, :content, :image, :styles])
  end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;We want to render every content in its HTML section node, except for content with type feature, which we want to group them in the same section. Therefore, we split the contents into two different lists, extracting the ones with type &lt;code&gt;feature&lt;/code&gt; and appending it as a map with a &lt;code&gt;features&lt;/code&gt; key.&lt;/p&gt;

&lt;p&gt;To render HTML in &lt;code&gt;LiveView&lt;/code&gt;, you can either implement the &lt;code&gt;render/1&lt;/code&gt; callback function or create a &lt;code&gt;your_view_template.html.leex&lt;/code&gt; template in your live view folder. Let&amp;#39;s take the second choice:&lt;/p&gt;

&lt;pre&gt;&lt;code class="elixir"&gt;# lib/phoenix_cms_web/live/page_live.html.leex

&amp;lt;%= if @contents do %&amp;gt;
  &amp;lt;%= for content &amp;lt;- @contents, do: render_section(content) %&amp;gt;
&amp;lt;% end %&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Iterating over the assigned contents, it calls the &lt;code&gt;render_section/1&lt;/code&gt; function, which we need to add to the &lt;code&gt;PageLive&lt;/code&gt; module:&lt;/p&gt;

&lt;pre&gt;&lt;code class="elixir"&gt;# lib/phoenix_cms_web/live/page_live.ex

defmodule PhoenixCmsWeb.PageLive do
  use PhoenixCmsWeb, :live_view

  # ...

  def render_section(%{type: &amp;quot;hero&amp;quot;} = content) do
    Phoenix.View.render(PhoenixCmsWeb.PageView, &amp;quot;hero.html&amp;quot;, content: content)
  end

  def render_section(%{type: &amp;quot;text_and_image&amp;quot;} = content) do
    Phoenix.View.render(PhoenixCmsWeb.PageView, &amp;quot;text_and_image.html&amp;quot;, content: content)
  end

  def render_section(%{features: content}) do
    Phoenix.View.render(PhoenixCmsWeb.PageView, &amp;quot;features.html&amp;quot;, content: content)
  end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;As we have three different content types (&lt;code&gt;hero&lt;/code&gt;, &lt;code&gt;text_and_image&lt;/code&gt;, and &lt;code&gt;feature&lt;/code&gt;), we want to give them their layout and style, so we render them using different templates:&lt;/p&gt;

&lt;pre&gt;&lt;code class="elixir"&gt;# lib/phoenix_cms_web/templates/page/hero.html.eex

&amp;lt;section class=&amp;quot;hero is-link is-medium&amp;quot;&amp;gt;
  &amp;lt;div class=&amp;quot;hero-body&amp;quot;&amp;gt;
    &amp;lt;div class=&amp;quot;container&amp;quot;&amp;gt;
      &amp;lt;header class=&amp;quot;hero__header&amp;quot;&amp;gt;
        &amp;lt;h1 class=&amp;quot;title is-1 mb-6&amp;quot;&amp;gt;&amp;lt;%= @content.title %&amp;gt;&amp;lt;/h1&amp;gt;
        &amp;lt;p class=&amp;quot;subtitle is-3 mb-6&amp;quot;&amp;gt;&amp;lt;%= @content.content %&amp;gt;&amp;lt;/p&amp;gt;
      &amp;lt;/header&amp;gt;
      &amp;lt;figure class=&amp;quot;image&amp;quot;&amp;gt;
        &amp;lt;img class=&amp;quot;&amp;quot; src=&amp;quot;&amp;lt;%= @content.image %&amp;gt;&amp;quot; alt=&amp;quot;Placeholder image&amp;quot;&amp;gt;
      &amp;lt;/figure&amp;gt;
    &amp;lt;/div&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/section&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;pre&gt;&lt;code class="elixir"&gt;# lib/phoenix_cms_web/templates/page/text_and_image.html.eex

&amp;lt;div class=&amp;quot;container text-and-image&amp;quot;&amp;gt;
  &amp;lt;div class=&amp;quot;columns is-variable is-mobile is-8&amp;quot;&amp;gt;
    &amp;lt;div class=&amp;quot;column is-half&amp;quot;&amp;gt;
      &amp;lt;header class=&amp;quot;mb-4&amp;quot;&amp;gt;&amp;lt;h2 class=&amp;quot;title&amp;quot;&amp;gt;&amp;lt;%= @content.title %&amp;gt;&amp;lt;/h2&amp;gt;&amp;lt;/header&amp;gt;
      &amp;lt;p class=&amp;quot;subtitle&amp;quot;&amp;gt;&amp;lt;%= @content.content %&amp;gt;&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;div class=&amp;quot;column is-half image-container&amp;quot;&amp;gt;
      &amp;lt;figure class=&amp;quot;image&amp;quot;&amp;gt;
        &amp;lt;img src=&amp;quot;&amp;lt;%= @content.image %&amp;gt;&amp;quot; alt=&amp;quot;Placeholder image&amp;quot;&amp;gt;
      &amp;lt;/figure&amp;gt;
    &amp;lt;/div&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;pre&gt;&lt;code class="elixir"&gt;# lib/phoenix_cms_web/templates/page/features.html.eex

&amp;lt;section class=&amp;quot;section&amp;quot;&amp;gt;
  &amp;lt;div class=&amp;quot;container mb-6 features&amp;quot;&amp;gt;
    &amp;lt;header class=&amp;quot;mb-6&amp;quot;&amp;gt;
      &amp;lt;h2 class=&amp;quot;title is-2&amp;quot;&amp;gt;Features&amp;lt;/h2&amp;gt;
    &amp;lt;/header&amp;gt;
    &amp;lt;div class=&amp;quot;columns is-multiline is-mobile is-8&amp;quot;&amp;gt;
      &amp;lt;%= for item &amp;lt;- @content do %&amp;gt;
        &amp;lt;div class=&amp;quot;column is-one-third feature&amp;quot;&amp;gt;
          &amp;lt;figure class=&amp;quot;image feature__image&amp;quot;&amp;gt;
            &amp;lt;img src=&amp;quot;&amp;lt;%= item.image %&amp;gt;&amp;quot; alt=&amp;quot;Placeholder image&amp;quot;&amp;gt;
          &amp;lt;/figure&amp;gt;
          &amp;lt;header class=&amp;quot;mb-4&amp;quot;&amp;gt;&amp;lt;h4 class=&amp;quot;title is-4&amp;quot;&amp;gt;&amp;lt;%= item.title %&amp;gt;&amp;lt;/h4&amp;gt;&amp;lt;/header&amp;gt;
          &amp;lt;p class=&amp;quot;subtitle&amp;quot;&amp;gt;&amp;lt;%= item.content %&amp;gt;&amp;lt;/p&amp;gt;
        &amp;lt;/div&amp;gt;
      &amp;lt;% end %&amp;gt;
    &amp;lt;/div&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/section&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;The ArticlesLive live view&lt;/h3&gt;

&lt;p&gt;&lt;img class="center" src="/images/blog/phoenix-cms-1/blog.png"/&gt;&lt;/p&gt;

&lt;p&gt;To render the articles list corresponding to the &lt;code&gt;/blog&lt;/code&gt; route, let&amp;#39;s implement the &lt;code&gt;ArticlesLive&lt;/code&gt; module:&lt;/p&gt;

&lt;pre&gt;&lt;code class="elixir"&gt;# lib/phoenix_cms_web/live/articles_live.ex

defmodule PhoenixCmsWeb.ArticlesLive do
  use PhoenixCmsWeb, :live_view

  alias PhoenixCmsWeb.LiveEncoder

  @impl true
  def mount(_params, _session, socket) do
    {:ok, assign_socket(socket)}
  end

  defp assign_socket(socket) do
    case fetch_articles() do
      {:ok, articles} -&amp;gt;
        socket
        |&amp;gt; assign(:page_title, &amp;quot;Blog&amp;quot;)
        |&amp;gt; assign(:articles, articles)
        |&amp;gt; put_flash(:error, nil)

      _ -&amp;gt;
        socket
        |&amp;gt; assign(:page_title, &amp;quot;Blog&amp;quot;)
        |&amp;gt; assign(:articles, nil)
        |&amp;gt; put_flash(:error, &amp;quot;Error fetching data&amp;quot;)
    end
  end

  defp fetch_articles do
    with {:ok, articles} &amp;lt;- PhoenixCms.articles() do
      articles
      |&amp;gt; Enum.sort_by(&amp;amp; &amp;amp;1.published_at)
      |&amp;gt; LiveEncoder.articles()

      {:ok, articles}
    end
  end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Just like in the &lt;code&gt;PageLive&lt;/code&gt; module, it fetches the articles using &lt;code&gt;PhoenixCms.articles/0&lt;/code&gt;, which delegates its call to the &lt;code&gt;PhoenixCms.Repo&lt;/code&gt; module. If everything goes fine, it encodes the items and assigns them to the socket. This step is important because since the socket process stores the assigned elements in memory, we only want to store the necessary values:&lt;/p&gt;

&lt;pre&gt;&lt;code class="elixir"&gt;# lib/phoenix_cms_web/live/encoder.ex

defmodule PhoenixCmsWeb.LiveEncoder do
  alias PhoenixCms.{Article, Content }

  # ...

  def articles(articles) do
    Enum.map(articles, &amp;amp;encode/1)
  end

  def encode(%Article{} = article) do
    Map.take(article, [:id, :slug, :title, :description, :image, :author, :published_at])
  end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Note that we are not taking the full article &lt;code&gt;content&lt;/code&gt; for this page, because we don&amp;#39;t want to render it. Now let&amp;#39;s write its template:&lt;/p&gt;

&lt;pre&gt;&lt;code class="elixir"&gt;# lib/phoenix_cms_web/live/articles_live.html.leex

&amp;lt;%= if @articles  do %&amp;gt;
  &amp;lt;section class=&amp;quot;section&amp;quot;&amp;gt;
    &amp;lt;div class=&amp;quot;container&amp;quot;&amp;gt;
      &amp;lt;header class=&amp;quot;mb-6&amp;quot;&amp;gt;&amp;lt;h2 class=&amp;quot;title&amp;quot;&amp;gt;Blog&amp;lt;/h2&amp;gt;&amp;lt;/header&amp;gt;
      &amp;lt;div class=&amp;quot;columns is-variable is-multiline is-mobile is-8&amp;quot;&amp;gt;
        &amp;lt;%= for article &amp;lt;- @articles, do: render_article(@socket, article) %&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  &amp;lt;/section&amp;gt;
&amp;lt;% end %&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;As we did with the contents list, we have to add the &lt;code&gt;render_article/2&lt;/code&gt; to the view:&lt;/p&gt;

&lt;pre&gt;&lt;code class="elixir"&gt;# lib/phoenix_cms_web/live/articles_live.ex

defmodule PhoenixCmsWeb.ArticlesLive do
  use PhoenixCmsWeb, :live_view

  # ...


  def render_article(socket, %{id: _id, slug: _slug} = article) do
    Phoenix.View.render(PhoenixCmsWeb.PageView, &amp;quot;article.html&amp;quot;, socket: socket, article: article)
  end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And we can&amp;#39;t forget about its article item template:&lt;/p&gt;

&lt;pre&gt;&lt;code class="elixir"&gt;# lib/phoenix_cms_web/templates/page/article.html.eex

&amp;lt;%= live_patch to: Routes.live_path(@socket, PhoenixCmsWeb.ShowArticleLive, @article.id, @article.slug), class: &amp;quot;column is-half article-list__article&amp;quot; do %&amp;gt;
  &amp;lt;img class=&amp;quot;article__image&amp;quot; src=&amp;quot;&amp;lt;%= @article.image %&amp;gt;&amp;quot;&amp;gt;
  &amp;lt;header&amp;gt;
    &amp;lt;h3 class=&amp;quot;title is-4&amp;quot;&amp;gt;&amp;lt;%= @article.title %&amp;gt;&amp;lt;/h3&amp;gt;
    &amp;lt;h4 class=&amp;quot;subtitle is-5&amp;quot;&amp;gt;&amp;lt;%= @article.description %&amp;gt;&amp;lt;/h4&amp;gt;
    &amp;lt;div class=&amp;quot;media&amp;quot;&amp;gt;
      &amp;lt;div class=&amp;quot;media-left&amp;quot;&amp;gt;
        &amp;lt;figure class=&amp;quot;image is-48x48&amp;quot;&amp;gt;
          &amp;lt;img class=&amp;quot;is-rounded avatar&amp;quot; src=&amp;quot;&amp;lt;%= &amp;quot;https://avatars.dicebear.com/api/avataaars/#{@article.author}.svg&amp;quot; %&amp;gt;&amp;quot; alt=&amp;quot;Placeholder image&amp;quot;&amp;gt;
        &amp;lt;/figure&amp;gt;
      &amp;lt;/div&amp;gt;
      &amp;lt;div class=&amp;quot;media-content&amp;quot;&amp;gt;
        &amp;lt;p class=&amp;quot;title is-6&amp;quot;&amp;gt;&amp;lt;%= @article.author %&amp;gt;&amp;lt;/p&amp;gt;
        &amp;lt;p class=&amp;quot;subtitle is-6&amp;quot;&amp;gt;&amp;lt;%= @article.published_at %&amp;gt;&amp;lt;/p&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  &amp;lt;/header&amp;gt;
&amp;lt;% end %&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Using the same &lt;code&gt;live_patch&lt;/code&gt; function as in the main navigation section, we create a link around the article summary to navigate to the article detail page, in which we can read the full version of the article.&lt;/p&gt;

&lt;h3&gt;The ShowArticleLive live view&lt;/h3&gt;

&lt;p&gt;&lt;img class="center" src="/images/blog/phoenix-cms-1/show-blog.png"/&gt;&lt;/p&gt;

&lt;p&gt;Last but not least, this LiveView renders the full version of an article:&lt;/p&gt;

&lt;pre&gt;&lt;code class="elixir"&gt;# lib/phoenix_cms_web/live/show_article_live.ex

defmodule PhoenixCmsWeb.ShowArticleLive do
  use PhoenixCmsWeb, :live_view

  @impl true
  def mount(%{&amp;quot;id&amp;quot; =&amp;gt; id}, _session, socket) do
    {:ok, assign_socket(socket, id)}
  end

  defp assign_socket(socket, id) do
    case PhoenixCms.get_article(id) do
      {:ok, article} -&amp;gt;
        socket
        |&amp;gt; assign(:page_title, article.title)
        |&amp;gt; assign(:article, article)
        |&amp;gt; put_flash(:error, nil)

      {:error, _} -&amp;gt;
        socket
        |&amp;gt; assign(:page_title, &amp;quot;Blog&amp;quot;)
        |&amp;gt; assign(:article, nil)
        |&amp;gt; put_flash(:error, &amp;quot;Error fetching data&amp;quot;)
    end
  end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Following the same pattern as in the previous views, it calls &lt;code&gt;PhoenixCms.get_article/1&lt;/code&gt; passing the article id received in its mount parameters, and assigning the result to the socket. The corresponding template looks like this:&lt;/p&gt;

&lt;pre&gt;&lt;code class="elixir"&gt;# lib/phoenix_cms_web/live/show_article_live.html.leex

&amp;lt;%= if @article  do %&amp;gt;
  &amp;lt;article class=&amp;quot;article&amp;quot;&amp;gt;
    &amp;lt;div class=&amp;quot;container mt-6&amp;quot;&amp;gt;
      &amp;lt;header class=&amp;quot;article__header&amp;quot;&amp;gt;
        &amp;lt;h1 class=&amp;quot;title&amp;quot;&amp;gt;&amp;lt;%= @article.title %&amp;gt;&amp;lt;/h1&amp;gt;
        &amp;lt;div class=&amp;quot;media&amp;quot;&amp;gt;
          &amp;lt;div class=&amp;quot;media-left&amp;quot;&amp;gt;
            &amp;lt;figure class=&amp;quot;image is-48x48&amp;quot;&amp;gt;
              &amp;lt;img class=&amp;quot;is-rounded avatar&amp;quot; src=&amp;quot;&amp;lt;%= &amp;quot;https://avatars.dicebear.com/api/avataaars/#{@article.author}.svg&amp;quot; %&amp;gt;&amp;quot; alt=&amp;quot;Placeholder image&amp;quot;&amp;gt;
            &amp;lt;/figure&amp;gt;
          &amp;lt;/div&amp;gt;
          &amp;lt;div class=&amp;quot;media-content&amp;quot;&amp;gt;
            &amp;lt;p class=&amp;quot;title is-6&amp;quot;&amp;gt;&amp;lt;%= @article.author %&amp;gt;&amp;lt;/p&amp;gt;
            &amp;lt;p class=&amp;quot;subtitle is-7&amp;quot;&amp;gt;&amp;lt;%= @article.published_at %&amp;gt;&amp;lt;/p&amp;gt;
          &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
      &amp;lt;/header&amp;gt;
      &amp;lt;figure class=&amp;quot;image main-image&amp;quot;&amp;gt;
        &amp;lt;img src=&amp;quot;&amp;lt;%= @article.image %&amp;gt;&amp;quot;&amp;gt;
      &amp;lt;/figure&amp;gt;
      &amp;lt;p class=&amp;quot;subtitle is-italic&amp;quot;&amp;gt;&amp;lt;%= @article.description %&amp;gt;&amp;lt;/p&amp;gt;
      &amp;lt;section class=&amp;quot;article__content&amp;quot;&amp;gt;
        &amp;lt;%= raw(@article.content) %&amp;gt;
      &amp;lt;/section&amp;gt;
    &amp;lt;/div&amp;gt;
  &amp;lt;/article&amp;gt;
&amp;lt;% end %&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;Almost there&lt;/h3&gt;

&lt;p&gt;Now that we have everything ready, let&amp;#39;s start the application and navigate through its pages, checking out the logs in the console:&lt;/p&gt;

&lt;pre&gt;&lt;code class="console"&gt;iex(2)&amp;gt; [info] GET https://api.airtable.com/v0/appXTw8FgG3h55fk6/articles -&amp;gt; 200 (653.723 ms)
[info] GET /
[info] Sent 200 in 20ms
[info] GET https://api.airtable.com/v0/appXTw8FgG3h55fk6/contents -&amp;gt; 200 (153.722 ms)
[info] GET /blog
[info] Sent 200 in 426µs
[info] GET https://api.airtable.com/v0/appXTw8FgG3h55fk6/articles -&amp;gt; 200 (218.254 ms)
[info] GET /blog/rec1osLptzsXfWg5g/lorem-ipsum
[info] Sent 200 in 384µs
[info] GET https://api.airtable.com/v0/appXTw8FgG3h55fk6/articles/rec1osLptzsXfWg5g -&amp;gt; 200 (193.594 ms)
[info] GET /blog
[info] Sent 200 in 581µs
[info] GET https://api.airtable.com/v0/appXTw8FgG3h55fk6/articles -&amp;gt; 200 (211.392 ms)
[info] GET /
[info] Sent 200 in 519µs
[info] GET https://api.airtable.com/v0/appXTw8FgG3h55fk6/contents -&amp;gt; 200 (129.278 ms)
[info] GET /blog
[info] Sent 200 in 427µs
[info] GET https://api.airtable.com/v0/appXTw8FgG3h55fk6/articles -&amp;gt; 200 (224.131 ms)
[info] GET /blog/rec1osLptzsXfWg5g/lorem-ipsum
[info] Sent 200 in 381µs
[info] GET https://api.airtable.com/v0/appXTw8FgG3h55fk6/articles/rec1osLptzsXfWg5g -&amp;gt; 200 (118.158 ms)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;As we can see, every time we visit a page, the view makes the corresponding HTTP request to get its necessary contents. Although working fine for a single user, if we had many users visiting our site, it could easily overcome Airtable&amp;#39;s rate limit of five requests per second. Not to mention the overhead that adds making an HTTP request on every page and what would happen if Airtable is down for whatever reason. In the next and last part of the series, we will look for a solution to all these problems, by implementing an automated cache mechanism using ETS. In the meantime, you can check the end result &lt;a href="https://phoenixcms.herokuapp.com/"&gt;here&lt;/a&gt;, or have a look at the &lt;a href="https://github.com/bigardone/phoenix-cms"&gt;source code&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

&lt;div class="btn-wrapper"&gt;
  &lt;a href="https://phoenixcms.herokuapp.com/" target="_blank" class="btn"&gt;&lt;i class="fa fa-cloud"&gt;&lt;/i&gt; Live demo&lt;/a&gt;
  &lt;a href="https://github.com/bigardone/phoenix-cms" target="_blank" class="btn"&gt;&lt;i class="fa fa-github"&gt;&lt;/i&gt; Source code&lt;/a&gt;
&lt;/div&gt;
</content>
  </entry>
  <entry>
    <title>Headless CMS fun with Phoenix LiveView and Airtable (pt. 2)</title>
    <link rel="alternate" href="http://codeloveandboards.com/blog/2020/07/11/headless-cms-fun-with-phoenix-liveview-and-airtable-pt-2/"/>
    <id>http://codeloveandboards.com/blog/2020/07/11/headless-cms-fun-with-phoenix-liveview-and-airtable-pt-2/</id>
    <published>2020-07-11T07:54:00Z</published>
    <updated>2020-07-27T15:31:27+02:00</updated>
    <author>
      <name>Article Author</name>
    </author>
    <content type="html">&lt;div class="index"&gt;
  &lt;p&gt;This post belongs to the &lt;strong&gt;Headless CMS fun with Phoenix LiveView and Airtable&lt;/strong&gt; series.&lt;/p&gt;
  &lt;ol&gt;
    &lt;li&gt;&lt;a href="/blog/2020/07/02/headless-cms-fun-with-phoenix-liveview-and-airtable-pt-1"&gt;Introduction.&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href="/blog/2020/07/11/headless-cms-fun-with-phoenix-liveview-and-airtable-pt-2"&gt;The project set up and implementing the repository pattern.&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href="/blog/2020/07/19/headless-cms-fun-with-phoenix-liveview-and-airtable-pt-3"&gt;Content rendering using Phoenix LiveView.&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href="/blog/2020/07/27/headless-cms-fun-with-phoenix-liveview-and-airtable-pt-4"&gt;Adding a cache to the repository and broadcasting changes to the views..&lt;/a&gt;&lt;/li&gt;
  &lt;/ol&gt;

  &lt;a href="https://phoenixcms.herokuapp.com/" target="_blank" class="btn"&gt;&lt;i class="fa fa-cloud"&gt;&lt;/i&gt; Live demo&lt;/a&gt;
  &lt;a href="https://github.com/bigardone/phoenix-cms" target="_blank"&gt;&lt;i class="fa fa-github"&gt;&lt;/i&gt; Source code&lt;/a&gt;
&lt;/div&gt;

&lt;p&gt;In the &lt;a href="/blog/2020/07/02/headless-cms-fun-with-phoenix-liveview-and-airtable-pt-1"&gt;previous part&lt;/a&gt; of these series, we talked about what we are going to be building and its two main parts. Today, we will focus on the Phoenix application, but you need the Airtable base to follow up on the tutorial. Therefore, if you don&amp;#39;t have an Airtable account, &lt;a href="https://airtable.com/invite/r/uJ5KeMkl"&gt;sign up&lt;/a&gt;, and click on the &lt;em&gt;Copy base&lt;/em&gt; link located at the top right corner of the source base. Once you have imported it into your workspace, we can continue creating the Phoenix application.&lt;/p&gt;

&lt;p&gt;&lt;img class="center" src="/images/blog/phoenix-cms-1/site.jpg"/&gt;&lt;/p&gt;

&lt;h2&gt;Creating the Phoenix application&lt;/h2&gt;

&lt;p&gt;Before generating the project scaffold, let&amp;#39;s install the latest version of &lt;a href="https://hex.pm/packages/phx_new"&gt;phx_new&lt;/a&gt;, which by the time I&amp;#39;m writing this part is &lt;code&gt;v1.5.3&lt;/code&gt;.&lt;/p&gt;

&lt;pre&gt;&lt;code class="console"&gt;mix archive.install hex phx_new 1.5.3
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now we can generate the project by running the &lt;code&gt;mix phx.new&lt;/code&gt; with the following options:&lt;/p&gt;

&lt;pre&gt;&lt;code class="console"&gt;mix phx.new phoenix_cms --no-ecto --no-gettext --no-dashboard --live
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If you are not familiar with the options that we are using, here&amp;#39;s a quick description of them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--no-ecto&lt;/code&gt;: we are not using any database connection, so let&amp;#39;s get rid of the Ecto files and configuration.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--no-gettext&lt;/code&gt;: we can also remove any translation-related dependency and files.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--no-dashboard&lt;/code&gt;: Phoenix has a brand new &lt;a href="https://github.com/phoenixframework/phoenix_live_dashboard"&gt;live dashboard&lt;/a&gt; where you can see all the metrics related to your application. We are going to be installing it, later on, so let&amp;#39;s remove it for now.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--live&lt;/code&gt;: includes support for &lt;a href="https://github.com/phoenixframework/phoenix_live_view"&gt;Phoenix LiveView&lt;/a&gt;, which is essential for this project.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once the task finishes generating the project files and installing the necessary dependencies, I like to do some cleanup, removing the extra content that the generator creates for you, usually these &lt;a href="https://github.com/bigardone/phoenix-cms/commit/cc718f7e2fff17a4126ab2cb4ef643ee25023ce7"&gt;CSS files and HTML&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;The Airtable client&lt;/h2&gt;

&lt;p&gt;Before continuing any further, let&amp;#39;s define our domain entities, which are going to map the data stored in Airtable, starting with the &lt;code&gt;Content&lt;/code&gt; struct which represents a content section, from the &lt;a href="https://airtable.com/shribMafJ0mAR7nic/tblLCFjonKFPr4yiN/viwgxDq0PyWSRs8N4?blocks=hide"&gt;contents table&lt;/a&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code class="elixir"&gt;# lib/phoenix_cms/content.ex

defmodule PhoenixCms.Content do
  alias __MODULE__

  @type t :: %Content{
          id: String.t(),
          position: non_neg_integer,
          type: String.t(),
          title: String.t(),
          content: String.t(),
          image: String.t(),
          styles: String.t()
        }

  defstruct [:id, :position, :type, :title, :content, :image, :styles]
end
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Let&amp;#39;s continue by defining the &lt;code&gt;Article&lt;/code&gt; struct, corresponding to the blog posts stored in the &lt;a href="https://airtable.com/shribMafJ0mAR7nic/tbli19sQuKyiKOwVL/viwbGrlrj6obHy0jl?blocks=hide"&gt;articles table&lt;/a&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code class="elixir"&gt;# lib/phoenix_cms/article.ex

defmodule PhoenixCms.Article do
  alias __MODULE__

  @type t :: %Article{
          id: String.t(),
          slug: String.t(),
          title: String.t(),
          description: String.t(),
          image: String.t(),
          content: String.t(),
          author: String.t(),
          published_at: Date.t()
        }

  defstruct [:id, :slug, :title, :description, :image, :content, :author, :published_at]
end
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The next step is to request the data to Airtable, taking advantage of its API, and convert the received data into the domain entities we have just defined. To implement the HTTP client, let&amp;#39;s add &lt;a href="https://hex.pm/packages/tesla"&gt;Tesla&lt;/a&gt; to the project&amp;#39;s dependencies, and install them running &lt;code&gt;mix deps.get&lt;/code&gt;.&lt;/p&gt;

&lt;pre&gt;&lt;code class="elixir"&gt;# mix.exs

defmodule PhoenixCms.MixProject do
  use Mix.Project

  # ...
  # ...


  defp deps do
    [
      # ...

      # Http client
      {:tesla, &amp;quot;~&amp;gt; 1.3&amp;quot;},
      {:hackney, &amp;quot;~&amp;gt; 1.16.0&amp;quot;}
    ]
  end

  # ...
end
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Tesla suggests setting &lt;code&gt;hackney&lt;/code&gt; as its default adapter, so let&amp;#39;s go ahead and do that:&lt;/p&gt;

&lt;pre&gt;&lt;code class="elixir"&gt;# config/config.exs

use Mix.Config

# ...

# Tesla configuration
config :tesla, adapter: Tesla.Adapter.Hackney

# ...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Once we have everything ready we can start implementing the client. When I have to use external services, such as Airtable, I like to separate any related logic in a different namespace, such as &lt;code&gt;Services&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code class="elixir"&gt;# lib/services/airtable.ex

defmodule Services.Airtable do
  # We are going to implement the public interface in a minute...

  defp client do
    middleware = [
      {Tesla.Middleware.BaseUrl, api_url() &amp;lt;&amp;gt; base_id()},
      Tesla.Middleware.JSON,
      Tesla.Middleware.Logger,
      {Tesla.Middleware.Headers, [{&amp;quot;authorization&amp;quot;, &amp;quot;Bearer &amp;quot; &amp;lt;&amp;gt; api_key()}]}
    ]

    Tesla.client(middleware)
  end

  defp do_get(path) do
    client()
    |&amp;gt; Tesla.get(path)
    |&amp;gt; case do
      {:ok, %{status: 200, body: body}} -&amp;gt;
        {:ok, body}

      {:ok, %{status: status}} -&amp;gt;
        {:error, status}

      other -&amp;gt;
        other
    end
  end

  defp api_url, do: Application.get_env(:phoenix_cms, __MODULE__)[:api_url]

  defp api_key, do: Application.get_env(:phoenix_cms, __MODULE__)[:api_key]

  defp base_id, do: Application.get_env(:phoenix_cms, __MODULE__)[:base_id]
end
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The &lt;code&gt;client&lt;/code&gt; function returns a &lt;code&gt;Tesla.Client&lt;/code&gt; using the following middleware:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Tesla.Middleware.BaseUrl&lt;/code&gt;, which sets the base URL for all the requests.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Tesla.Middleware.JSON&lt;/code&gt;, which encodes requests and decodes responses as JSON.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Tesla.Middleware.Logger&lt;/code&gt;, which logs requests and responses.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Tesla.Middleware.Headers&lt;/code&gt;, which sets headers for all requests, and in this particular case, the &lt;code&gt;authorization&lt;/code&gt; header with the bearer token from Airtable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the base URL, we need to set both the &lt;code&gt;api_url&lt;/code&gt; and &lt;code&gt;base_id&lt;/code&gt; keys in the application&amp;#39;s configuration. The same happens for &lt;code&gt;api_key&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code class="elixir"&gt;# config/config.exs

use Mix.Config

# ...


# Airtable configuration
config :phoenix_cms, Services.Airtable,
  api_key: &amp;quot;YOUR API KEY&amp;quot;,
  base_id: &amp;quot;YOUR BASE ID&amp;quot;,
  api_url: &amp;quot;https://api.airtable.com/v0/&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You can find your &lt;code&gt;api_key&lt;/code&gt; in your Airtable account page, and the &lt;code&gt;base_id&lt;/code&gt; in your API documentation page.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;do_get&lt;/code&gt; function takes a &lt;code&gt;path&lt;/code&gt; and performs a &lt;code&gt;GET&lt;/code&gt; request using the client. Since we don&amp;#39;t want to deal with anything related to Tesla outside this module, it returns either a &lt;code&gt;{:ok, body}&lt;/code&gt; or a &lt;code&gt;{:error, reason}&lt;/code&gt; tuple. There&amp;#39;s one thing left: to add the public interface, so let&amp;#39;s go ahead and add two functions, one for getting all records from a table and the other for getting a table record by its ID:&lt;/p&gt;

&lt;pre&gt;&lt;code class="elixir"&gt;# lib/services/airtable.ex

defmodule Services.Airtable do
  def all(table), do: do_get(&amp;quot;/#{table}&amp;quot;)

  def get(table, record_id), do: do_get(&amp;quot;/#{table}/#{record_id}&amp;quot;)

  # ...
end
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Let&amp;#39;s jump into &lt;code&gt;iex&lt;/code&gt; and test the client, limiting the response to a single record:&lt;/p&gt;

&lt;pre&gt;&lt;code class="console"&gt;➜ iex -S mix
Erlang/OTP 23 [erts-11.0.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1]

Interactive Elixir (1.10.4) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)&amp;gt; Services.Airtable.all(&amp;quot;contents?maxRecords=1&amp;quot;)
[info] GET https://api.airtable.com/v0/YOUR_TABLE_ID/contents?maxRecords=1 -&amp;gt; 200 (614.224 ms)
[debug]
&amp;gt;&amp;gt;&amp;gt; REQUEST &amp;gt;&amp;gt;&amp;gt;
(Ommited request headers)

&amp;lt;&amp;lt;&amp;lt; RESPONSE &amp;lt;&amp;lt;&amp;lt;
(Ommited response headers)

(Ommited response payload)
{:ok,
 %{
   &amp;quot;records&amp;quot; =&amp;gt; [
     %{
       &amp;quot;createdTime&amp;quot; =&amp;gt; &amp;quot;2020-07-01T05:27:44.000Z&amp;quot;,
       &amp;quot;fields&amp;quot; =&amp;gt; %{
         &amp;quot;content&amp;quot; =&amp;gt; &amp;quot;Lorem ipsum dolor sit amet, consectetur adipiscing elit&amp;quot;,
         &amp;quot;id&amp;quot; =&amp;gt; &amp;quot;feature_4&amp;quot;,
         &amp;quot;image&amp;quot; =&amp;gt; [
           %{
             &amp;quot;filename&amp;quot; =&amp;gt; &amp;quot;pipe.png&amp;quot;,
             &amp;quot;id&amp;quot; =&amp;gt; &amp;quot;attJxlSNbmLRra4qx&amp;quot;,
             &amp;quot;size&amp;quot; =&amp;gt; 11828,
             &amp;quot;thumbnails&amp;quot; =&amp;gt; %{
               &amp;quot;full&amp;quot; =&amp;gt; %{
                 &amp;quot;height&amp;quot; =&amp;gt; 3000,
                 &amp;quot;url&amp;quot; =&amp;gt; &amp;quot;https://dl.airtable.com/.attachmentThumbnails/fe2e0dcd3e2a969f1816570e02dad366/7c9e2246&amp;quot;,
                 &amp;quot;width&amp;quot; =&amp;gt; 3000
               },
               &amp;quot;large&amp;quot; =&amp;gt; %{
                 &amp;quot;height&amp;quot; =&amp;gt; 512,
                 &amp;quot;url&amp;quot; =&amp;gt; &amp;quot;https://dl.airtable.com/.attachmentThumbnails/2651c43ab85e28d2ba0c574f36ee7a1a/fe4a5495&amp;quot;,
                 &amp;quot;width&amp;quot; =&amp;gt; 512
               },
               &amp;quot;small&amp;quot; =&amp;gt; %{
                 &amp;quot;height&amp;quot; =&amp;gt; 36,
                 &amp;quot;url&amp;quot; =&amp;gt; &amp;quot;https://dl.airtable.com/.attachmentThumbnails/0d717bf44d9552c7e25482496bc30c3c/6e29a1ad&amp;quot;,
                 &amp;quot;width&amp;quot; =&amp;gt; 36
               }
             },
             &amp;quot;type&amp;quot; =&amp;gt; &amp;quot;image/png&amp;quot;,
             &amp;quot;url&amp;quot; =&amp;gt; &amp;quot;https://dl.airtable.com/.attachments/70ff8a20d056c7dfb677f1fc6bc79771/abea3535/pipe.png&amp;quot;
           }
         ],
         &amp;quot;position&amp;quot; =&amp;gt; 10,
         &amp;quot;title&amp;quot; =&amp;gt; &amp;quot;Feature 4&amp;quot;,
         &amp;quot;type&amp;quot; =&amp;gt; &amp;quot;feature&amp;quot;
       },
       &amp;quot;id&amp;quot; =&amp;gt; &amp;quot;rec7VPdanrfUyvYnw&amp;quot;
     }
   ]
 }}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;It works! Now let&amp;#39;s confirm that the &lt;code&gt;get/2&lt;/code&gt; function works as well using the previous record ID:&lt;/p&gt;

&lt;pre&gt;&lt;code class="console"&gt;iex(2)&amp;gt; Services.Airtable.get(&amp;quot;contents&amp;quot;, &amp;quot;rec7VPdanrfUyvYnw&amp;quot;)
[info] GET https://api.airtable.com/v0/YOUR_TABLE_ID/contents/rec7VPdanrfUyvYnw -&amp;gt; 200 (6455.924 ms)
[debug]
&amp;gt;&amp;gt;&amp;gt; REQUEST &amp;gt;&amp;gt;&amp;gt;
(Ommited request headers)
&amp;lt;&amp;lt;&amp;lt; RESPONSE &amp;lt;&amp;lt;&amp;lt;
(Ommited response headers)

(Ommited response payload)
{:ok,
 %{
   &amp;quot;createdTime&amp;quot; =&amp;gt; &amp;quot;2020-07-01T05:27:44.000Z&amp;quot;,
   &amp;quot;fields&amp;quot; =&amp;gt; %{
     &amp;quot;content&amp;quot; =&amp;gt; &amp;quot;Lorem ipsum dolor sit amet, consectetur adipiscing elit&amp;quot;,
     &amp;quot;id&amp;quot; =&amp;gt; &amp;quot;feature_4&amp;quot;,
     &amp;quot;image&amp;quot; =&amp;gt; [
       %{
         &amp;quot;filename&amp;quot; =&amp;gt; &amp;quot;pipe.png&amp;quot;,
         &amp;quot;id&amp;quot; =&amp;gt; &amp;quot;attJxlSNbmLRra4qx&amp;quot;,
         &amp;quot;size&amp;quot; =&amp;gt; 11828,
         &amp;quot;thumbnails&amp;quot; =&amp;gt; %{
           &amp;quot;full&amp;quot; =&amp;gt; %{
             &amp;quot;height&amp;quot; =&amp;gt; 3000,
             &amp;quot;url&amp;quot; =&amp;gt; &amp;quot;https://dl.airtable.com/.attachmentThumbnails/fe2e0dcd3e2a969f1816570e02dad366/7c9e2246&amp;quot;,
             &amp;quot;width&amp;quot; =&amp;gt; 3000
           },
           &amp;quot;large&amp;quot; =&amp;gt; %{
             &amp;quot;height&amp;quot; =&amp;gt; 512,
             &amp;quot;url&amp;quot; =&amp;gt; &amp;quot;https://dl.airtable.com/.attachmentThumbnails/2651c43ab85e28d2ba0c574f36ee7a1a/fe4a5495&amp;quot;,
             &amp;quot;width&amp;quot; =&amp;gt; 512
           },
           &amp;quot;small&amp;quot; =&amp;gt; %{
             &amp;quot;height&amp;quot; =&amp;gt; 36,
             &amp;quot;url&amp;quot; =&amp;gt; &amp;quot;https://dl.airtable.com/.attachmentThumbnails/0d717bf44d9552c7e25482496bc30c3c/6e29a1ad&amp;quot;,
             &amp;quot;width&amp;quot; =&amp;gt; 36
           }
         },
         &amp;quot;type&amp;quot; =&amp;gt; &amp;quot;image/png&amp;quot;,
         &amp;quot;url&amp;quot; =&amp;gt; &amp;quot;https://dl.airtable.com/.attachments/70ff8a20d056c7dfb677f1fc6bc79771/abea3535/pipe.png&amp;quot;
       }
     ],
     &amp;quot;position&amp;quot; =&amp;gt; 10,
     &amp;quot;title&amp;quot; =&amp;gt; &amp;quot;Feature 4&amp;quot;,
     &amp;quot;type&amp;quot; =&amp;gt; &amp;quot;feature&amp;quot;
   },
   &amp;quot;id&amp;quot; =&amp;gt; &amp;quot;rec7VPdanrfUyvYnw&amp;quot;
 }}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Yay! The Airtable client is ready. However, we still have to convert the returned payload into the domain entities we created previously, and for that, we are going to make use of the &lt;em&gt;Repository pattern&lt;/em&gt;.&lt;/p&gt;

&lt;h3&gt;The Repository pattern&lt;/h3&gt;

&lt;p&gt;This pattern provides an abstraction of the data layer, which decouples it from its source or persistence layer, making it accessible through a series of straightforward functions. The basic idea is to have a public interface as the primary repository module that relies on different adapters, using the most suitable one depending on the situation or environment. The two adapters that we are going to implement are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An HTTP adapter, powered by the &lt;code&gt;Services.Airtable&lt;/code&gt; client, which we are going to be using while developing and in the production environment.&lt;/li&gt;
&lt;li&gt;A fake adapter that returns hardcoded results, which we can use in our tests, prevents unnecessary HTTP requests against Airtable&amp;#39;s API.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let&amp;#39;s go ahead and implement the main repository module:&lt;/p&gt;

&lt;pre&gt;&lt;code class="elixir"&gt;# lib/phoenix_cms/repo.ex

defmodule PhoenixCms.Repo do
  alias PhoenixCms.{Article, Content}

  # Behaviour callbacks

  @type entity_types :: Article.t() | Content.t()

  @callback all(Article | Content) :: {:ok, [entity_types]} | {:error, term}
  @callback get(Article | Content, String.t()) :: {:ok, entity_types} | {:error, term}

  # Sets the adapter
  @adapter Application.get_env(:phoenix_cms, __MODULE__)[:adapter]

  # Public API functions
  def articles, do: @adapter.all(Article)

  def contents, do: @adapter.all(Content)

  def get_article(id), do: @adapter.get(Article, id)
end
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;In this module we are doing three different things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;First of all, it is describing the necessary callback functions that any module needs to implement to become a repository adapter. These functions are, &lt;code&gt;all&lt;/code&gt; which receives an Article or Content atom and returns a &lt;code&gt;{:ok, items}&lt;/code&gt; tuple on success or a &lt;code&gt;{:error, reason}&lt;/code&gt; tuple on error.&lt;/li&gt;
&lt;li&gt;It&amp;#39;s also setting the current &lt;code&gt;@adapter&lt;/code&gt; module variable from the application configuration.&lt;/li&gt;
&lt;li&gt;Finally, it also implements three different functions, the public API of the repository, which internally use the corresponding adapter functions thanks to the previous dependency injection.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Knowing the repository interface, let&amp;#39;s implement the HTTP adapter that relies on the &lt;code&gt;Services.Airtable&lt;/code&gt; client that we created before:&lt;/p&gt;

&lt;pre&gt;&lt;code class="elixir"&gt;# lib/phoenix_cms/repo/http.ex

defmodule PhoenixCms.Repo.Http do
  alias __MODULE__.Decoder
  alias PhoenixCms.{Article, Content, Repo}
  alias Services.Airtable

  @behaviour Repo

  @articles_table &amp;quot;articles&amp;quot;
  @contents_table &amp;quot;contents&amp;quot;

  @impl Repo
  def all(Article), do: do_all(@articles_table)
  def all(Content), do: do_all(@contents_table)

  @impl Repo
  def get(Article, id), do: do_get(@articles_table, id)
  def get(Content, id), do: do_get(@contents_table, id)

  defp do_all(table) do
    case Airtable.all(table) do
      {:ok, %{&amp;quot;records&amp;quot; =&amp;gt; records}} -&amp;gt;
        {:ok, Decoder.decode(records)}

      {:error, 404} -&amp;gt;
        {:error, :not_found}

      other -&amp;gt;
        other
    end
  end

  defp do_get(table, id) do
    case Airtable.get(table, id) do
      {:ok, response} -&amp;gt;
        {:ok, Decoder.decode(response)}

      {:error, 404} -&amp;gt;
        {:error, :not_found}

      other -&amp;gt;
        other
    end
  end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The module implements the necessary callback functions from the &lt;code&gt;Repo&lt;/code&gt; behavior, using the &lt;code&gt;Services.Airtable&lt;/code&gt; client to fetch the data from the corresponding table. Since the behaviour specifies that both of these functions return &lt;code&gt;Article&lt;/code&gt; or &lt;code&gt;Contents&lt;/code&gt; structs, it uses a &lt;code&gt;Decoder&lt;/code&gt; module to convert the raw HTTP response items into these domain data structures:&lt;/p&gt;

&lt;pre&gt;&lt;code class="elixir"&gt;# lib/phoenix_cms/repo/http/decoder.ex

defmodule PhoenixCms.Repo.Http.Decoder do
  @moduledoc false

  alias PhoenixCms.{Article, Content}

  def decode(response) when is_list(response) do
    Enum.map(response, &amp;amp;decode/1)
  end

  def decode(%{
        &amp;quot;id&amp;quot; =&amp;gt; id,
        &amp;quot;fields&amp;quot; =&amp;gt;
          %{
            &amp;quot;slug&amp;quot; =&amp;gt; slug
          } = fields
      }) do
    %Article{
      id: id,
      slug: slug,
      title: Map.get(fields, &amp;quot;title&amp;quot;, &amp;quot;&amp;quot;),
      description: Map.get(fields, &amp;quot;description&amp;quot;, &amp;quot;&amp;quot;),
      image: decode_image(Map.get(fields, &amp;quot;image&amp;quot;)),
      content: Map.get(fields, &amp;quot;content&amp;quot;, &amp;quot;&amp;quot;),
      author: Map.get(fields, &amp;quot;author&amp;quot;, &amp;quot;&amp;quot;),
      published_at: Date.from_iso8601!(Map.get(fields, &amp;quot;published_at&amp;quot;))
    }
  end

  def decode(%{
        &amp;quot;fields&amp;quot; =&amp;gt;
          %{
            &amp;quot;type&amp;quot; =&amp;gt; type
          } = fields
      }) do
    %Content{
      id: Map.get(fields, &amp;quot;id&amp;quot;, &amp;quot;&amp;quot;),
      position: Map.get(fields, &amp;quot;position&amp;quot;, &amp;quot;&amp;quot;),
      type: type,
      title: Map.get(fields, &amp;quot;title&amp;quot;, &amp;quot;&amp;quot;),
      content: Map.get(fields, &amp;quot;content&amp;quot;, &amp;quot;&amp;quot;),
      image: decode_image(Map.get(fields, &amp;quot;image&amp;quot;, &amp;quot;&amp;quot;)),
      styles: Map.get(fields, &amp;quot;styles&amp;quot;, &amp;quot;&amp;quot;)
    }
  end

  defp decode_image([%{&amp;quot;url&amp;quot; =&amp;gt; url}]), do: url
  defp decode_image(_), do: &amp;quot;&amp;quot;
end
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Using pattern matching, it takes the necessary data to build the structs. Airtable does not send empty values, thus defaulting missing keys to empty strings. Let&amp;#39;s jump back into &lt;code&gt;iex&lt;/code&gt; and try it out:&lt;/p&gt;

&lt;pre&gt;&lt;code class="console"&gt;➜ iex -S mix
Erlang/OTP 23 [erts-11.0.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1]

Interactive Elixir (1.10.4) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)&amp;gt; PhoenixCms.Repo.Http.all(PhoenixCms.Article)
{:ok,
 [
   %PhoenixCms.Article{
     author: &amp;quot;author-1@phoenixcms.com&amp;quot;,
     ...
     ...
  ]
}

iex(2)&amp;gt; PhoenixCms.Repo.Http.all(PhoenixCms.Content)
{:ok,
 [
   %PhoenixCms.Content{
     content: &amp;quot;Lorem ipsum dolor sit amet, consectetur adipiscing elit&amp;quot;,
     id: &amp;quot;feature_4&amp;quot;,
     ...
     ...
  ]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;It works as expected! Let&amp;#39;s continue with the fake adapter definition:&lt;/p&gt;

&lt;pre&gt;&lt;code class="elixir"&gt;# lib/phoenix_cms/repo/fake.ex

defmodule PhoenixCms.Repo.Fake do
  @moduledoc false

  alias PhoenixCms.{Article, Content, Repo}

  @behaviour Repo

  @impl Repo
  def all(Content) do
    {:ok,
     [
       %PhoenixCms.Content{
         id: &amp;quot;contents-1&amp;quot;,
         # ...
       },
       %PhoenixCms.Content{
         id: &amp;quot;contents-2&amp;quot;,
         # ...
       }
     ]}
  end

  def all(Article) do
    {:ok,
     [
       %Article{
         id: &amp;quot;article-1&amp;quot;,
         # ..
       },
       %Article{
         id: &amp;quot;article-2&amp;quot;,
         # ..
       }
     ]}
  end

  def all(_), do: {:error, :not_found}

  @impl Repo
  def get(entity, id) when entity in [Article, Content] do
    with {:ok, items} &amp;lt;- all(entity),
         {:ok, nil} &amp;lt;- {:ok, Enum.find(items, &amp;amp;(&amp;amp;1.id == id))} do
      {:error, :not_found}
    end
  end

  def get(_, _), do: {:error, :not_found}
end
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This is the most basic implementation that we can make. However, since we are not going to be using it during the tutorial, it&amp;#39;s good enough.&lt;/p&gt;

&lt;p&gt;We are missing something tho, which is configuring the adapter module we want to use in our different environments:&lt;/p&gt;

&lt;pre&gt;&lt;code class="elixir"&gt;# config/config.exs

use Mix.Config

# ...

# Repo configuration
config :phoenix_cms, PhoenixCms.Repo, adapter: PhoenixCms.Repo.Http
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;I don&amp;#39;t want to extend the articles more than the necessary, so we are not going to be implementing any tests. Nevertheless, if you&amp;#39;re going to write your own, add the fake adapter to the test environment configuration to prevent unnecessary HTTP requests against Airtable:&lt;/p&gt;

&lt;pre&gt;&lt;code class="elixir"&gt;# config/test.exs

use Mix.Config

# ...

# Repo configuration
config :phoenix_cms, PhoenixCms.Repo, adapter: PhoenixCms.Repo.Fake
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And that&amp;#39;s all for today. In the next part, we will focus on the front-end, rendering the &lt;code&gt;Phoenix.LiveView&lt;/code&gt; pages using the data returned by the repository, and eventually discovering that this is not a very good solution, and thinking about a more performant one. In the meantime, you can check the end result &lt;a href="https://phoenixcms.herokuapp.com/"&gt;here&lt;/a&gt;, or have a look at the &lt;a href="https://github.com/bigardone/phoenix-cms"&gt;source code&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

&lt;div class="btn-wrapper"&gt;
  &lt;a href="https://phoenixcms.herokuapp.com/" target="_blank" class="btn"&gt;&lt;i class="fa fa-cloud"&gt;&lt;/i&gt; Live demo&lt;/a&gt;
  &lt;a href="https://github.com/bigardone/phoenix-cms" target="_blank" class="btn"&gt;&lt;i class="fa fa-github"&gt;&lt;/i&gt; Source code&lt;/a&gt;
&lt;/div&gt;
</content>
  </entry>
  <entry>
    <title>Headless CMS fun with Phoenix LiveView and Airtable (pt. 1)</title>
    <link rel="alternate" href="http://codeloveandboards.com/blog/2020/07/02/headless-cms-fun-with-phoenix-liveview-and-airtable-pt-1/"/>
    <id>http://codeloveandboards.com/blog/2020/07/02/headless-cms-fun-with-phoenix-liveview-and-airtable-pt-1/</id>
    <published>2020-07-03T05:19:00Z</published>
    <updated>2020-07-27T15:31:38+02:00</updated>
    <author>
      <name>Article Author</name>
    </author>
    <content type="html">&lt;div class="index"&gt;
  &lt;p&gt;This post belongs to the &lt;strong&gt;Headless CMS fun with Phoenix LiveView and Airtable&lt;/strong&gt; series.&lt;/p&gt;
  &lt;ol&gt;
    &lt;li&gt;&lt;a href="/blog/2020/07/02/headless-cms-fun-with-phoenix-liveview-and-airtable-pt-1"&gt;Introduction.&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href="/blog/2020/07/11/headless-cms-fun-with-phoenix-liveview-and-airtable-pt-2"&gt;The project set up and implementing the repository pattern.&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href="/blog/2020/07/19/headless-cms-fun-with-phoenix-liveview-and-airtable-pt-3"&gt;Content rendering using Phoenix LiveView.&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href="/blog/2020/07/27/headless-cms-fun-with-phoenix-liveview-and-airtable-pt-4"&gt;Adding a cache to the repository and broadcasting changes to the views..&lt;/a&gt;&lt;/li&gt;
  &lt;/ol&gt;

  &lt;a href="https://phoenixcms.herokuapp.com/" target="_blank" class="btn"&gt;&lt;i class="fa fa-cloud"&gt;&lt;/i&gt; Live demo&lt;/a&gt;
  &lt;a href="https://github.com/bigardone/phoenix-cms" target="_blank"&gt;&lt;i class="fa fa-github"&gt;&lt;/i&gt; Source code&lt;/a&gt;
&lt;/div&gt;

&lt;p&gt;Last year, I built a static website for a friend of mine who has an Italian restaurant, which has been vital for her business since then. The first year the site performed really well businesswise. However, issues have started to appear as my friend needed to change the site&amp;#39;s content to showcase the new season products, menu, and schedule. I started implementing the changes by hand, but we suddenly realized that this was not convenient at all, since she needed to change the content on the fly without having to rely on me. After considering many different possibilities, I solved the problem by implementing a simple solution in a single weekend, thanks to &lt;a href="https://www.phoenixframework.org/"&gt;Phoenix&lt;/a&gt; and &lt;a href="https://airtable.com/"&gt;Airtable&lt;/a&gt;. Here&amp;#39;s who I did it.&lt;/p&gt;

&lt;h2&gt;Introduction&lt;/h2&gt;

&lt;p&gt;In this new tutorial, we are going to be building a headless content management system consisting of two main elements, which are an Airtable base and a Phoenix application powered by LiveView. Let&amp;#39;s take a more in-depth look at them:&lt;/p&gt;

&lt;h3&gt;Airtable&lt;/h3&gt;

&lt;p&gt;Airtable is a really cool service based on spreadsheets grouped in bases, that act as a database. Using a very intuitive and friendly UI, it lets you design your own data structures, add attachments, handle relationships between different tables, design different data views for your tables, and much more. It also exposes all the data through a very convenient API, which is crucial for this tutorial.&lt;/p&gt;

&lt;p&gt;But why do we need such a service? The main reason is that we want to externalize the content of our website and the management of it by our users, letting us focus only on building the presentation layer, which is a simple Phoenix application. And Airtable is perfect for that.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://airtable.com/shribMafJ0mAR7nic/tblLCFjonKFPr4yiN/viwgxDq0PyWSRs8N4?blocks=hide"&gt;Here you can find the base&lt;/a&gt; that we are using for this tutorial, which consists of two tables:&lt;/p&gt;

&lt;h4&gt;The contents table&lt;/h4&gt;

&lt;p&gt;&lt;img class="center" src="/images/blog/phoenix-cms-1/airtable-1.png"/&gt;&lt;/p&gt;

&lt;p&gt;This table stores the sections of the main page. Its structure is straightforward, structuring the data with the following fields:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;id&lt;/code&gt;: the ID of the section.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;position&lt;/code&gt;: An auto-increment number that specifies the order of the section within the page.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;type&lt;/code&gt;: the type of the section, which can have three different values:

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;hero&lt;/code&gt;: for a hero section containing a big title and subtitle.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;text_and_image&lt;/code&gt;: for sections that have some text and an image.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feature&lt;/code&gt;: for a section that has a list of items with some text and an icon.&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;title&lt;/code&gt;: the title of the section.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;content&lt;/code&gt;: the main content of the section. It can store HTML.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;image&lt;/code&gt;: the main image of the section. Stored as an attachment.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;styles&lt;/code&gt;: any additional styles that we want to add to the section.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;The articles table&lt;/h4&gt;

&lt;p&gt;&lt;img class="center" src="/images/blog/phoenix-cms-1/airtable-2.png"/&gt;&lt;/p&gt;

&lt;p&gt;This table stores all the blog articles of our website, and each article consists of the following attributes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;slug&lt;/code&gt;: the SEO friendly slug for the article.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;title&lt;/code&gt;: the main title of the article.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;description&lt;/code&gt;: the article&amp;#39;s excerpt.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;image&lt;/code&gt;: the main image of the article. Stored as an attachment.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;content&lt;/code&gt;: the main content of the article. It can store HTML.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;author&lt;/code&gt;: the email of the article&amp;#39;s author.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;published_at&lt;/code&gt;: the publication date of the article.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;The Phoenix application&lt;/h3&gt;

&lt;p&gt;The presentation layer of our CMS is a Phoenix application which supervision tree looks something like this:&lt;/p&gt;

&lt;p&gt;&lt;img class="center" src="/images/blog/phoenix-cms-1/app-diagram.png"/&gt;&lt;/p&gt;

&lt;p&gt;Let&amp;#39;s have a closer look at some of its components.&lt;/p&gt;

&lt;h4&gt;Cache&lt;/h4&gt;

&lt;p&gt;Airtable has a limit of five requests per second for free accounts, so we can&amp;#39;t just send requests on every page visit, because if we have a lot of users, the API is likely going to start returning rate limit errors. Using Erlang&amp;#39;s &lt;a href="https://erlang.org/doc/man/ets.html"&gt;ETS&lt;/a&gt; to store successful responses from the API helps to prevent rate limiting issues. Once a page is mounted, the data is taken from the cache instead of performing an HTTP request. However, this is not enough, because we need to keep the cache data synced with the latest changes in Airtable.&lt;/p&gt;

&lt;h4&gt;Synchronizer&lt;/h4&gt;

&lt;p&gt;To keep the cache data in sync with Airtable, it spawns a &lt;a href="https://hexdocs.pm/elixir/GenServer.html"&gt;GenServer&lt;/a&gt; process, which periodically makes requests to the API every second, updating its stored data if needed and broadcasting the new data to the live views using &lt;a href="https://hexdocs.pm/phoenix_pubsub/Phoenix.PubSub.html"&gt;PubSub&lt;/a&gt;. This way, we limit the number of interactions with the API to two requests per second, no matter the number of users are currently visiting our site.&lt;/p&gt;

&lt;h4&gt;LiveView&lt;/h4&gt;

&lt;p&gt;Instead of using regular views and templates, the application takes advantage of Phoenix &lt;a href="https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html"&gt;LiveView&lt;/a&gt;, subscribing to specific PubSub topics, updating its data when needed, and refreshing its content on the browser without requiring a reload from the user.&lt;/p&gt;

&lt;p&gt;Here you can see the three views that it has:&lt;/p&gt;

&lt;p&gt;&lt;img class="center" src="/images/blog/phoenix-cms-1/site.jpg"/&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A main landing page that renders the sections stored in the contents table. These sections are the main hero section, some image and text sections, and a feature list section.&lt;/li&gt;
&lt;li&gt;A blog page, listing all the articles stored in the articles table.&lt;/li&gt;
&lt;li&gt;An article detail page that renders a complete article.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And I think this is all for the introduction. In the next part, we will start building the Phoenix application and implementing an HTTP client to retrieve the data stored in Airtable. In the meantime, you can check the end result &lt;a href="https://phoenixcms.herokuapp.com/"&gt;here&lt;/a&gt;, or have a look at the &lt;a href="https://github.com/bigardone/phoenix-cms"&gt;source code&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

&lt;div class="btn-wrapper"&gt;
  &lt;a href="https://phoenixcms.herokuapp.com/" target="_blank" class="btn"&gt;&lt;i class="fa fa-cloud"&gt;&lt;/i&gt; Live demo&lt;/a&gt;
  &lt;a href="https://github.com/bigardone/phoenix-cms" target="_blank" class="btn"&gt;&lt;i class="fa fa-github"&gt;&lt;/i&gt; Source code&lt;/a&gt;
&lt;/div&gt;
</content>
  </entry>
  <entry>
    <title>elmcsspatterns.io</title>
    <link rel="alternate" href="http://codeloveandboards.com/blog/2020/04/27/elmcsspatterns-io/"/>
    <id>http://codeloveandboards.com/blog/2020/04/27/elmcsspatterns-io/</id>
    <published>2020-04-27T07:00:00Z</published>
    <updated>2020-04-28T09:02:33+02:00</updated>
    <author>
      <name>Article Author</name>
    </author>
    <content type="html">&lt;h2&gt;The inspiration&lt;/h2&gt;

&lt;p&gt;A couple of weeks ago, while searching for a convenient CSS pattern that I needed to implement in one of my elm projects, I stumbled upon &lt;a href="https://csslayout.io/"&gt;csslayout.io&lt;/a&gt; and felt in love with it instantly. His author, &lt;a href="https://dev.to/phuocng"&gt;phuocng&lt;/a&gt;, has done a fantastic job not only collecting such a massive collection of patterns but making them easy to find and implement.&lt;/p&gt;

&lt;h2&gt;The motivation&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://csslayout.io/"&gt;csslayout.io&lt;/a&gt; is the kind of resource that I like to keep handy while working on my front-end, as I tend to write my styles without using any CSS framework whatsoever. Moreover, for the last year, I only use &lt;a href="https://github.com/rtfeldman/elm-css"&gt;elm-css&lt;/a&gt; to generate the CSS, which feels to me like &lt;a href="https://sass-lang.com/"&gt;Sass&lt;/a&gt; but functional and statically typed, which is just awesome. Unfortunately, there aren&amp;#39;t any similar resources for &lt;a href="https://github.com/rtfeldman/elm-css"&gt;elm-css&lt;/a&gt; that I&amp;#39;m aware of, so I couldn&amp;#39;t resist writing my version, collecting and sharing the common patterns that I often use.&lt;/p&gt;

&lt;p&gt;On the other hand, I&amp;#39;ve been looking for an excuse to play around with &lt;a href="https://github.com/ryannhg/elm-spa"&gt;elm-spa&lt;/a&gt; lately, which I think is going to be one of the next big things in the elm ecosystem. If you are not familiar with &lt;a href="https://github.com/ryannhg/elm-spa"&gt;elm-spa&lt;/a&gt;, it basically consists of an elm library and a JS client, which automagically takes care of generating all the boilerplate regarding elm single-page applications, letting you focus on the fun part. His author, &lt;a href="https://github.com/ryannhg"&gt;ryannhg&lt;/a&gt;, is doing an excellent job, keep it up!&lt;/p&gt;

&lt;h2&gt;The result&lt;/h2&gt;

&lt;p&gt;So having &lt;a href="https://csslayout.io/"&gt;csslayout.io&lt;/a&gt; as inspiration, and &lt;a href="https://github.com/rtfeldman/elm-css"&gt;elm-css&lt;/a&gt; + &lt;a href="https://github.com/ryannhg/elm-spa"&gt;elm-spa&lt;/a&gt; as motivation, I have started working on &lt;a href="https://elmcsspatterns.io/"&gt;elmcsspatterns.io&lt;/a&gt;. It is still an early version, and I will probably change everything now and then, but if you are into &lt;strong&gt;elm&lt;/strong&gt; and &lt;strong&gt;elm-css&lt;/strong&gt; I hope you find it useful, and if not, I hope it makes you want to try them :)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://elmcsspatterns.io/" target="_blank" class="btn"&gt;
&lt;img class="center" src="/images/blog/elmcsspatterns/home.png"/&gt;
&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

&lt;div class="btn-wrapper"&gt;
  &lt;a href="https://elmcsspatterns.io/" target="_blank" class="btn"&gt;&lt;i class="fa fa-cloud"&gt;&lt;/i&gt; Site&lt;/a&gt;
  &lt;a href="https://github.com/bigardone/elm-css-patterns" target="_blank" class="btn"&gt;&lt;i class="fa fa-github"&gt;&lt;/i&gt; Source code&lt;/a&gt;
&lt;/div&gt;
</content>
  </entry>
  <entry>
    <title>Dynamic base path for an Elm SPA</title>
    <link rel="alternate" href="http://codeloveandboards.com/blog/2019/06/06/dynamic-base-path-for-an-elm-spa/"/>
    <id>http://codeloveandboards.com/blog/2019/06/06/dynamic-base-path-for-an-elm-spa/</id>
    <published>2019-06-06T07:00:00Z</published>
    <updated>2019-06-15T11:31:45+02:00</updated>
    <author>
      <name>Article Author</name>
    </author>
    <content type="html">&lt;p&gt;While building an Elm SPA dashboard, I faced the following problem. In the local development environment, the URL to access it is &lt;code&gt;http://localhost:1234&lt;/code&gt;, which is Parcel&amp;#39;s default URL, and the Elm SPA gets mounted in &lt;code&gt;/&lt;/code&gt;, so Elm navigation handles as expected any internal routes like &lt;code&gt;/projects&lt;/code&gt; or &lt;code&gt;/tasks&lt;/code&gt;. The problem came while deploying it into production because the base URL didn&amp;#39;t match the root path. In other words, it looked something like &lt;a href="https://nifty-minsky-538aab.netlify.com/private/admin/"&gt;https://nifty-minsky-538aab.netlify.com/private/admin/&lt;/a&gt; where &lt;code&gt;/private/admin/&lt;/code&gt; was the base path for the application, and this path could change depending on the environment, which made Elm navigation tricky, especially while parsing URLs to get the current route. I wanted to avoid using URL fragments, so this is how I solved it.&lt;/p&gt;

&lt;h3&gt;The &amp;lt;base&amp;gt; HTML element&lt;/h3&gt;

&lt;p&gt;First of all, I needed a way to prepend the dynamic base URL to any of the internal Elm routes. After some research I found the handy &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base"&gt;&amp;lt;base&amp;gt;&lt;/a&gt; HTML element, which specifies the base URL to use for all relative URLs contained within a document. This means that if you set &lt;code&gt;&amp;lt;base href=&amp;quot;http://localhost:1234/private/admin/&amp;quot;&amp;gt;&lt;/code&gt;, any relative link I would add like &lt;code&gt;&amp;lt;a href=&amp;quot;projects&amp;quot;&amp;gt;Projects&amp;lt;/a&amp;gt;&lt;/code&gt;, automatically points to &lt;code&gt;http://localhost:1234/private/admin/projects&lt;/code&gt;, and that was exactly what I was looking for.&lt;/p&gt;

&lt;pre&gt;&lt;code class="html"&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&amp;quot;en&amp;quot;&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;base href=&amp;quot;{{ BASE_URL }}&amp;quot;&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;main&amp;gt;&amp;lt;/main&amp;gt;
    &amp;lt;script src=&amp;quot;./js/index.js&amp;quot;&amp;gt;&amp;lt;/script&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Setting the &lt;code&gt;href&lt;/code&gt; value for the current environment is easy using environment variables, depending on the technology stack you are using.&lt;/p&gt;

&lt;h3&gt;Passing the base path to the Elm application&lt;/h3&gt;

&lt;p&gt;Now that I had a way to set the base URL to all the internal links of the application, I needed a way to make Elm aware of this base path, which was pretty straightforward using flags and the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Node/baseURI"&gt;baseURI&lt;/a&gt; property:&lt;/p&gt;

&lt;pre&gt;&lt;code class="js"&gt;import { Elm } from &amp;#39;../src/Main.elm&amp;#39;;

const basePath = new URL(document.baseURI).pathname;

Elm.Main.init({
  node: document.querySelector(&amp;#39;main&amp;#39;),
  flags: { basePath },
});
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;baseURI&lt;/code&gt; basically returns the document&amp;#39;s location, unless you set &lt;code&gt;&amp;lt;base&amp;gt;&lt;/code&gt; in which case it always returns the value set. I only needed the path, therefore taking it from &lt;code&gt;URL(document.baseURI).pathname&lt;/code&gt; and passing it to the &lt;code&gt;Elm.Main.init&lt;/code&gt; function as a flag.&lt;/p&gt;

&lt;h3&gt;Elm routing and the base path&lt;/h3&gt;

&lt;p&gt;I always like defining the application routes as soon as possible, which helps me understand how to structure it. Moreover, in this particular case, routing was the source of the issue and the solution ifself, so let&amp;#39;s have a look at the &lt;code&gt;Route&lt;/code&gt; module I implemented:&lt;/p&gt;

&lt;pre&gt;&lt;code class="elm"&gt;-- src/Route.elm

module Route exposing
    ( Route(..)
    , fromUrl
    , toString
    )

import Url exposing (Url)
import Url.Parser as Parser exposing (Parser)


type Route
    = Home
    | Projects
    | Tasks
    | NotFound


parser : Parser (Route -&amp;gt; b) b
parser =
    Parser.oneOf
        [ Parser.map Home Parser.top
        , Parser.map Projects (Parser.s &amp;quot;projects&amp;quot;)
        , Parser.map Tasks (Parser.s &amp;quot;tasks&amp;quot;)
        ]

-- ...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This is pretty much the standard way of defining routes and their parser in Elm, and there wasn&amp;#39;t any particular change I had to implement to make it work. However, both &lt;code&gt;fromUrl&lt;/code&gt; and &lt;code&gt;toString&lt;/code&gt; functions needed to be slightly different than usual:&lt;/p&gt;

&lt;pre&gt;&lt;code class="elm"&gt;-- src/Route.elm

-- ...


fromUrl : String -&amp;gt; Url -&amp;gt; Route
fromUrl basePath url =
    { url | path = String.replace basePath &amp;quot;&amp;quot; url.path }
        |&amp;gt; Parser.parse parser
        |&amp;gt; Maybe.withDefault NotFound


toString : Route -&amp;gt; String
toString route =
    case route of
        Home -&amp;gt;
            &amp;quot;&amp;quot;

        Projects -&amp;gt;
            &amp;quot;projects&amp;quot;

        Tasks -&amp;gt;
            &amp;quot;tasks&amp;quot;

        NotFound -&amp;gt;
            &amp;quot;not-found&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;fromUrl&lt;/code&gt; takes a &lt;code&gt;basePath&lt;/code&gt; and a &lt;code&gt;Url&lt;/code&gt; parameter and returns a &lt;code&gt;Route&lt;/code&gt;. The first parameter is the flag passed to the Elm application on its initialization, and to get the corresponding &lt;code&gt;Route&lt;/code&gt;, we only need to remove &lt;code&gt;basePath&lt;/code&gt; from its path and parse it as usually. Bear in mind, that this only works with URLs built using the &lt;code&gt;&amp;lt;base&amp;gt;&lt;/code&gt; element set in the document header. Last but not least, the &lt;code&gt;toString&lt;/code&gt; function offers a convenient way of building a relative path for a given &lt;code&gt;Route&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;Gluing it all together&lt;/h3&gt;

&lt;p&gt;Having the parsing of URLs solved, building the rest of the application was quite simple. Let&amp;#39;s take a look at some of the implementation details:&lt;/p&gt;

&lt;pre&gt;&lt;code class="elm"&gt;-- src/Main.elm

module Main exposing (main)


import Browser exposing (Document)
import Browser.Navigation as Navigation
import Html as Html exposing (Html)
import Route exposing (Route)
import Url exposing (Url)

-- MODEL


type alias Flags =
    { basePath : String }


type alias Model =
    { flags : Flags
    , navigation : Navigation
    }


type alias Navigation =
    { key : Navigation.Key
    , route : Route
    }


init : Flags -&amp;gt; Url -&amp;gt; Navigation.Key -&amp;gt; ( Model, Cmd Msg )
init ({ basePath } as flags) url key =
    ( { flags = flags
      , navigation =
            { key = key
            , route = Route.fromUrl basePath url
            }
      }
    , Cmd.none
    )

-- ...

-- MAIN


main : Program Flags Model Msg
main =
    Browser.application
        { init = init
        , update = update
        , view = view
        , subscriptions = subscriptions
        , onUrlRequest = UrlRequested
        , onUrlChange = UrlChange
        }
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;I usually store the flags passed to the application in the model using a custom type named &lt;code&gt;Flags&lt;/code&gt;, which in this particular example only contains &lt;code&gt;basePath&lt;/code&gt;. I also like to store a &lt;code&gt;Navigation&lt;/code&gt; custom element which contains a &lt;code&gt;Navigation.Key&lt;/code&gt;, necessary for navigating, and the current route. The &lt;code&gt;init&lt;/code&gt; function is using the previously defined &lt;code&gt;Route.fromUrl&lt;/code&gt; function to set the current route from the browser&amp;#39;s URL and the &lt;code&gt;basePath&lt;/code&gt; flag. However, it also needs to set it every time the URL changes:&lt;/p&gt;

&lt;pre&gt;&lt;code class="elm"&gt;-- src/Main.elm

-- ...

-- UPDATE


type Msg
    = UrlRequested Browser.UrlRequest
    | UrlChange Url


update : Msg -&amp;gt; Model -&amp;gt; ( Model, Cmd Msg )
update msg ({ flags, navigation } as model) =
    case msg of
        UrlRequested urlRequest -&amp;gt;
-- ...

        UrlChange url -&amp;gt;
            ( { model
                | navigation =
                    { navigation
                        | route = Route.fromUrl flags.basePath url
                    }
              }
            , Cmd.none
            )
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And this is how I created the navigation links using the &lt;code&gt;Route.toString&lt;/code&gt; function:&lt;/p&gt;

&lt;pre&gt;&lt;code class="elm"&gt;Html.div
    []
    [ Html.a
        [ Html.href &amp;lt;| Route.toString Route.Home ]
        [ Html.text &amp;quot;Home&amp;quot; ]
    , Html.a
        [ Html.href &amp;lt;| Route.toString Route.Projects ]
        [ Html.text &amp;quot;Projects&amp;quot; ]
    , Html.a
        [ Html.href &amp;lt;| Route.toString Route.Tasks ]
        [ Html.text &amp;quot;Tasks&amp;quot; ]
    ]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And that&amp;#39;s it; everything worked like a charm. Being honest, I tried different approaches before getting to this solution, including custom &lt;code&gt;Url&lt;/code&gt; parsers, which is something difficult to understand for me. Have you faced the same issue? If so, I hope this solution helps you on the next occasion, and if you have solved differently, please share it :)&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

&lt;div class="btn-wrapper"&gt;
  &lt;a href="https://nifty-minsky-538aab.netlify.com/private/admin/" target="_blank" class="btn"&gt;&lt;i class="fa fa-cloud"&gt;&lt;/i&gt; Live demo&lt;/a&gt;
  &lt;a href="https://github.com/bigardone/elm-dynamic-base-path" target="_blank" class="btn"&gt;&lt;i class="fa fa-github"&gt;&lt;/i&gt; Source code&lt;/a&gt;
&lt;/div&gt;
</content>
  </entry>
</feed>
