<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Florian Arens</title>
    <description>The latest articles on DEV Community by Florian Arens (@farens).</description>
    <link>https://dev.to/farens</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1283568%2F501368d2-f250-472b-977a-b213adc567cd.jpeg</url>
      <title>DEV Community: Florian Arens</title>
      <link>https://dev.to/farens</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/farens"/>
    <language>en</language>
    <item>
      <title>Building a Table of Contents Component for a Phoenix Blog</title>
      <dc:creator>Florian Arens</dc:creator>
      <pubDate>Sun, 04 Aug 2024 00:00:00 +0000</pubDate>
      <link>https://dev.to/farens/building-a-table-of-contents-component-for-a-phoenix-blog-eci</link>
      <guid>https://dev.to/farens/building-a-table-of-contents-component-for-a-phoenix-blog-eci</guid>
      <description>&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;p&gt; 1. Introduction&lt;br&gt;
 2. Steps&lt;br&gt;
       2.1. Configure MDEx&lt;br&gt;
       2.2. Parse and convert headings&lt;br&gt;
       2.3. Build the table of contents component&lt;br&gt;
       2.4. How to use the component&lt;br&gt;
 3. Conclusion&lt;/p&gt;

&lt;p&gt;This article shows how to parse &lt;a href="https://github.com/leandrocp/mdex" rel="noopener noreferrer"&gt;MDEx&lt;/a&gt;-generated HTML into a nested data structure that is used to build a table of contents component for a Phoenix blog.&lt;/p&gt;
&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In this article, we are going to build a table of contents component that can be used in blogs or other markdown based pages. The table of contents component will display a link to each section of your content that allows users to quickly navigate through the content.&lt;/p&gt;

&lt;p&gt;We assume that you have set up a Phoenix project and are using the MDEx markdown parser to render markdown content. We will configure MDEx to include IDs and anchor links for headings in the generated HTML that we can use to build the table of contents component. If you are not using MDEx, you can still follow along, but you will need to make some adjustments to the code to make it work with your markdown parser.&lt;/p&gt;
&lt;h2&gt;
  
  
  Steps
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Configure MDEx
&lt;/h3&gt;

&lt;p&gt;The first step is to configure the MDEx markdown parser to include IDs and anchor links for headings in the generated HTML. By default, MDEx does not include any IDs and or additional anchor links. To enable this feature, we need to set the &lt;code&gt;header_ids&lt;/code&gt; option in the &lt;code&gt;extensions&lt;/code&gt; keyword list when calling the &lt;code&gt;MDEx.to_html&lt;/code&gt; function. This tells the MDEx markdown parser to include IDs and anchor links for headings in the generated HTML.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="no"&gt;MDEx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to_html&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;extension:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;header_ids:&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the code snippet above, we pass an empty string to the &lt;code&gt;header_ids&lt;/code&gt; option. The string will be used as a prefix for the generated IDs. We do not necessarily need a prefix, but it can be useful to avoid conflicts with other IDs on the page.&lt;/p&gt;

&lt;p&gt;You can see a list of all available extension in the &lt;a href="https://docs.rs/comrak/latest/comrak/struct.ExtensionOptions.html" rel="noopener noreferrer"&gt;comrak documentation&lt;/a&gt; as MDEx uses Rust’s comrak crate under the hood.&lt;/p&gt;

&lt;p&gt;We can write a simple test to verify that the generated HTML includes the IDs and anchor links for headings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="no"&gt;ExUnit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Case&lt;/span&gt;

&lt;span class="n"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;"MDEx includes IDs and anchor links"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;assert&lt;/span&gt; &lt;span class="no"&gt;MDEx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to_html&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"## Introduction&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;extension:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;header_ids:&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt;
            &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;#introduction&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; aria-hidden=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;true&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; class=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;anchor&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; id=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;introduction&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;&amp;gt;&amp;lt;/a&amp;gt;Introduction&amp;lt;/h2&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, the generated HTML includes an anchor link with the ID &lt;code&gt;introduction&lt;/code&gt; for the heading &lt;code&gt;## Introduction&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Parse and convert headings
&lt;/h3&gt;

&lt;p&gt;The next step is to parse the generated HTML, extract the headings from it and transform them into a suitable data structure that we can use to build the table of contents component. We will use the &lt;a href="https://github.com/philss/floki" rel="noopener noreferrer"&gt;Floki&lt;/a&gt; library for this. From the README: “Floki is a simple HTML parser that enables search for nodes using CSS selectors.”&lt;/p&gt;

&lt;p&gt;We will write a function that takes the generated HTML content as input, parses the headings, and returns a list of headings with labels, hrefs and subheadings.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;defp&lt;/span&gt; &lt;span class="n"&gt;parse_headings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;content&lt;/span&gt;
  &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Floki&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse_fragment!&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Enum&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;([],&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"h2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;acc&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
      &lt;span class="n"&gt;acc&lt;/span&gt; &lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="p"&gt;[%{&lt;/span&gt;&lt;span class="ss"&gt;label:&lt;/span&gt; &lt;span class="no"&gt;Floki&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;href:&lt;/span&gt; &lt;span class="n"&gt;get_href&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;childs:&lt;/span&gt; &lt;span class="p"&gt;[]}]&lt;/span&gt;

    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"h3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;acc&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
      &lt;span class="no"&gt;List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;update_at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="ss"&gt;childs:&lt;/span&gt; &lt;span class="n"&gt;subs&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;h2&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="n"&gt;h2&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="ss"&gt;childs:&lt;/span&gt; &lt;span class="n"&gt;subs&lt;/span&gt; &lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="p"&gt;[%{&lt;/span&gt;&lt;span class="ss"&gt;label:&lt;/span&gt; &lt;span class="no"&gt;Floki&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;href:&lt;/span&gt; &lt;span class="n"&gt;get_href&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;childs:&lt;/span&gt; &lt;span class="p"&gt;[]}]}&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;_other&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;acc&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
      &lt;span class="n"&gt;acc&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;get_href&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;heading_element&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;attr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;heading_element&lt;/span&gt; &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Floki&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"a"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Floki&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"href"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;attr&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;href&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;href&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;parse_headings/1&lt;/code&gt; function takes the generated HTML content as input. Since we are dealing with a string we first use the &lt;code&gt;Floki.parse_fragment!&lt;/code&gt; function to parse the string into a Floki &lt;code&gt;html_tree()&lt;/code&gt; data structure. We then use &lt;code&gt;Enum.reduce/3&lt;/code&gt; to iterate over the html tree. We check if the element is an &lt;code&gt;h2&lt;/code&gt; or &lt;code&gt;h3&lt;/code&gt; element and extract the label and href. The label is the text content of the heading element and the href is generated using the &lt;code&gt;get_href/1&lt;/code&gt; helper function (the &lt;code&gt;get_href/1&lt;/code&gt; function searches for the anchor link in the heading element and returns the corresponding href attribute). If the element is an &lt;code&gt;h3&lt;/code&gt; element, we need to add it as a child of the last &lt;code&gt;h2&lt;/code&gt; element. This results in a nested data structure.&lt;/p&gt;

&lt;p&gt;We can validate the function by writing a test that also demonstrates the resulting data structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="no"&gt;ExUnit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Case&lt;/span&gt;

&lt;span class="n"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;"parse_headings"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;headings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="sd"&gt;"""
    ## Section 1

    ### Subsection 1.1

    ### Subsection 1.2

    ## Section 2
    """&lt;/span&gt;
    &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;MDEx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to_html&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;extension:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;header_ids:&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;parse_headings&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;%{&lt;/span&gt;
      &lt;span class="ss"&gt;label:&lt;/span&gt; &lt;span class="s2"&gt;"Section 1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;childs:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="ss"&gt;label:&lt;/span&gt; &lt;span class="s2"&gt;"Subsection 1.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;childs:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="ss"&gt;href:&lt;/span&gt; &lt;span class="s2"&gt;"#subsection-11"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="ss"&gt;label:&lt;/span&gt; &lt;span class="s2"&gt;"Subsection 1.2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;childs:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="ss"&gt;href:&lt;/span&gt; &lt;span class="s2"&gt;"#subsection-12"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="ss"&gt;href:&lt;/span&gt; &lt;span class="s2"&gt;"#section-1"&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="ss"&gt;label:&lt;/span&gt; &lt;span class="s2"&gt;"Section 2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;childs:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="ss"&gt;href:&lt;/span&gt; &lt;span class="s2"&gt;"#section-2"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="n"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;headings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, the &lt;code&gt;parse_headings/1&lt;/code&gt; function returns a list of headings with labels, hrefs and subheadings. This data structure is suitable for building the table of contents component because it represents the hierarchy of the headings in the content.&lt;/p&gt;

&lt;p&gt;Note that we only parse &lt;code&gt;h2&lt;/code&gt; and &lt;code&gt;h3&lt;/code&gt; elements in this example. You can extend the function to include deeper heading levels if needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Build the table of contents component
&lt;/h3&gt;

&lt;p&gt;The last step is to build the actual table of contents component. It takes the list of parsed headings from the previous steps and an optional list of classes. Since we are using &lt;a href="https://daisyui.com/" rel="noopener noreferrer"&gt;daisyUI&lt;/a&gt; in our Phoenix project, we can use the &lt;code&gt;menu&lt;/code&gt; class to make the table of contents look like a sidebar menu by default (see &lt;a href="https://daisyui.com/components/menu/" rel="noopener noreferrer"&gt;daisyUI menu component&lt;/a&gt;). We also add some other default classes. You can customize the component to fit your design.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;toc/1&lt;/code&gt; component renders an unordered list with list items and a link for each heading. If the current heading has child headings, the &lt;code&gt;toc&lt;/code&gt; component is called recursively with them as input. This creates a nested HTML list structure that represents the headings data structure in the UI.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="nv"&gt;@doc&lt;/span&gt; &lt;span class="sd"&gt;"""
Renders a table of contents from a list of headings.
"""&lt;/span&gt;
&lt;span class="n"&gt;attr&lt;/span&gt; &lt;span class="ss"&gt;:headings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:list&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;required:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
&lt;span class="n"&gt;attr&lt;/span&gt; &lt;span class="ss"&gt;:class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;default:&lt;/span&gt; &lt;span class="s2"&gt;"menu w-56 p-0 opacity-60"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;toc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;assigns&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="sx"&gt;~H""&lt;/span&gt;&lt;span class="s2"&gt;"
  &amp;lt;ul class={@class}&amp;gt;
    &amp;lt;li :for={%{label: label, href: href, childs: childs} &amp;lt;- @headings}&amp;gt;
      &amp;lt;.link href={href}&amp;gt;
        &amp;lt;%= label %&amp;gt;
      &amp;lt;/.link&amp;gt;
      &amp;lt;.toc :if={childs != []} headings={childs} class={nil} /&amp;gt;
    &amp;lt;/li&amp;gt;
  &amp;lt;/ul&amp;gt;
  """&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How to use the component
&lt;/h3&gt;

&lt;p&gt;We are now ready to use the component in our layout. On my blog, I only show the table of contents on larger screens, so I can place it next to the main content. I also made it sticky so that it stays visible while scrolling. Here is an example of how I placed the component in my layout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"relative"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"absolute top-16 bottom-16 -left-14 w-10 text-xs"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"sticky top-6 hidden xl:block"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toc&lt;/span&gt; &lt;span class="na"&gt;headings=&lt;/span&gt;&lt;span class="s"&gt;{@article.heading_links}&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The above code uses &lt;code&gt;absolute&lt;/code&gt; positioning to place the table of contents on the left side of the main content. It is only visible on larger screens (&lt;code&gt;xl:block&lt;/code&gt;) and is sticky so that it stays visible while scrolling. To not stick to the top of the page, we use the &lt;code&gt;top-6&lt;/code&gt; class to save some space.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In this article, we configured the Elixir MDEx markdown parser to include IDs and anchor links for headings in the generated HTML. We then parsed the generated HTML into a nested data structure we can pass to a table of contents component we built. This component renders a list of links to the headings in the content, creating a table of contents that allows users to quickly navigate through the content.&lt;/p&gt;

</description>
      <category>floki</category>
      <category>mdex</category>
      <category>phoenix</category>
    </item>
    <item>
      <title>How to integrate Tabler Icons into your Phoenix project</title>
      <dc:creator>Florian Arens</dc:creator>
      <pubDate>Thu, 20 Jun 2024 00:00:00 +0000</pubDate>
      <link>https://dev.to/farens/how-to-integrate-tabler-icons-into-your-phoenix-project-g6b</link>
      <guid>https://dev.to/farens/how-to-integrate-tabler-icons-into-your-phoenix-project-g6b</guid>
      <description>&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;p&gt; 1. Introduction&lt;br&gt;
 2. Tracking the Tabler Icons source repository&lt;br&gt;
 3. Updating the Tailwind CSS config&lt;br&gt;
       3.1. Reading the SVG files&lt;br&gt;
       3.2. Generating the CSS classes&lt;br&gt;
 4. Remove width and height from the SVG&lt;br&gt;
 5. Build an icon component&lt;br&gt;
 6. Conclusion&lt;br&gt;
 7. Credits&lt;/p&gt;

&lt;p&gt;&lt;a href="https://tablericons.com/"&gt;Tabler Icons&lt;/a&gt; is one of the most popular icon libraries. This article shows how to integrate the icon library into Phoenix projects. We will track the Tabler Icons source repository using Mix and use the Tailwind CSS plugin feature to create an icon component.&lt;/p&gt;
&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In almost every web application you will need icons to represent different actions or states. There are many icon libraries available, but one of the most popular is Tabler Icons. Tabler Icons is a set of over 5000 free, MIT-licensed, high quality SVG icons. The icons are maintained by &lt;a href="https://twitter.com/codecalm"&gt;Paweł Kuna&lt;/a&gt; and come in two versions: filled and outlined.&lt;/p&gt;

&lt;p&gt;This article shows how to integrate Tabler Icons into an existing Phoenix project.&lt;/p&gt;
&lt;h2&gt;
  
  
  Tracking the Tabler Icons source repository
&lt;/h2&gt;

&lt;p&gt;The first step is to track the Tabler Icons source repository using Mix. This will allow us to easily update the icons in our project when new icons are added or existing icons are updated.&lt;/p&gt;

&lt;p&gt;To track the Tabler Icons source repository, we need to add the following to the &lt;code&gt;deps&lt;/code&gt; function in the &lt;code&gt;mix.exs&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:tabler_icons&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;github:&lt;/span&gt; &lt;span class="s2"&gt;"tabler/tabler-icons"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;sparse:&lt;/span&gt; &lt;span class="s2"&gt;"icons"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;app:&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;compile:&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will add the Tabler Icons repository as a dependency to our project. The &lt;code&gt;sparse&lt;/code&gt; option is used to only download the &lt;code&gt;icons&lt;/code&gt; directory from the repository. We set &lt;code&gt;app&lt;/code&gt; to &lt;code&gt;false&lt;/code&gt; because we don't want to read the app file. We also set &lt;code&gt;compile&lt;/code&gt; to &lt;code&gt;false&lt;/code&gt; because we don't want to compile the icons. We just want to download the icons so we can use them later in our Tailwind CSS config.&lt;/p&gt;

&lt;p&gt;If you have not seen the above options before, you can find a detailed explanation of them in the &lt;a href="https://hexdocs.pm/mix/1.17.1/Mix.Tasks.Deps.html"&gt;Mix documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;After adding the dependency, we need to run &lt;code&gt;mix deps.get&lt;/code&gt; to download the icons from the Tabler Icons repository. The icons will be downloaded to the &lt;code&gt;deps/tabler_icons/icons&lt;/code&gt; directory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Updating the Tailwind CSS config
&lt;/h2&gt;

&lt;p&gt;Next, we need to update the&lt;code&gt;tailwind.config.js&lt;/code&gt;. We create a custom plugin that generates the CSS classes for the icons.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reading the SVG files
&lt;/h3&gt;

&lt;p&gt;The first step is to make the plugin read the SVG files from the &lt;code&gt;deps/tabler_icons/icons&lt;/code&gt; directory.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nf"&gt;plugin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;iconsDir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../deps/tabler_icons/icons&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;icons&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/outline&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-filled&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/filled&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="nx"&gt;icons&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(([&lt;/span&gt;&lt;span class="nx"&gt;suffix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readdirSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;iconsDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;basename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.svg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;suffix&lt;/span&gt;
          &lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;fullPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;iconsDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The above code reads the SVG files from the &lt;code&gt;deps/tabler_icons/icons&lt;/code&gt; directory and creates an object with the icon names and their full paths. This way, we can easily reference the icons later.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;values&lt;/code&gt; object will look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fullPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/path/to/deps/tabler_icons/icons/outline/user.svg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user-filled&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user-filled&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fullPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/path/to/deps/tabler_icons/icons/filled/book.svg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We append the suffix &lt;code&gt;-filled&lt;/code&gt; to filled icon names to distinguish between the filled and outlined versions of the icons. Since outline should be the default, we don't append any suffix to the outline icons.&lt;/p&gt;

&lt;h3&gt;
  
  
  Generating the CSS classes
&lt;/h3&gt;

&lt;p&gt;Next, we need to get the plugin to generate the CSS classes for the icons. We want to add the CSS for elements that contain a &lt;code&gt;hero-*&lt;/code&gt; class. For example, if we have an element with a &lt;code&gt;hero-user&lt;/code&gt; class, we want to add the CSS for the user icon. To do this, we use the &lt;code&gt;matchComponent&lt;/code&gt; function provided by Tailwind CSS.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nf"&gt;plugin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;matchComponents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;theme&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
      &lt;span class="c1"&gt;// read icons and add to values object&lt;/span&gt;

      &lt;span class="nf"&gt;matchComponents&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tabler&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fullPath&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fullPath&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\r?\n&lt;/span&gt;&lt;span class="sr"&gt;|&lt;/span&gt;&lt;span class="se"&gt;\r&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;`--tabler-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="s2"&gt;`url('data:image/svg+xml;utf8,&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;')`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-webkit-mask&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`var(--tabler-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mask&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`var(--tabler-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mask-repeat&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;no-repeat&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;background-color&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;currentColor&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vertical-align&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;middle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;display&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;inline-block&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;width&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;spacing.5&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;height&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;spacing.5&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code matches items with the &lt;code&gt;tabler-*&lt;/code&gt;. It extracts the name and full path of the icon from the &lt;code&gt;values&lt;/code&gt; object created earlier. It then reads the contents of the SVG file and generates the CSS classes for the icon. The CSS classes set the icon as the element's background image and set the width and height of the element to &lt;code&gt;theme("spacing.5")&lt;/code&gt;. This way, we can easily control the size of the icons using Tailwind's CSS spacing utilities.&lt;/p&gt;

&lt;h2&gt;
  
  
  Remove width and height from the SVG
&lt;/h2&gt;

&lt;p&gt;The icons provided by the Tabler Icons library have width and height attributes set in the SVG files. We need to remove these attributes so that we can control the size of the icons using Tailwind CSS.&lt;/p&gt;

&lt;p&gt;We already have a regex that removes all line breaks and carriage returns from the path string. We can extend this to also remove the width and height attributes from the SVG files.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fullPath&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\r?\n&lt;/span&gt;&lt;span class="sr"&gt;|&lt;/span&gt;&lt;span class="se"&gt;\r&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/width="&lt;/span&gt;&lt;span class="se"&gt;[^&lt;/span&gt;&lt;span class="sr"&gt;"&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;*"/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/height="&lt;/span&gt;&lt;span class="se"&gt;[^&lt;/span&gt;&lt;span class="sr"&gt;"&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;*"/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The final plugin code will look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nf"&gt;plugin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;matchComponents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;theme&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;iconsDir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../deps/tabler_icons/icons&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;icons&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/outline&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-filled&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/filled&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="nx"&gt;icons&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(([&lt;/span&gt;&lt;span class="nx"&gt;suffix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readdirSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;iconsDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;basename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.svg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;suffix&lt;/span&gt;
          &lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;fullPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;iconsDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="nf"&gt;matchComponents&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tabler&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fullPath&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fullPath&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\r?\n&lt;/span&gt;&lt;span class="sr"&gt;|&lt;/span&gt;&lt;span class="se"&gt;\r&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/width="&lt;/span&gt;&lt;span class="se"&gt;[^&lt;/span&gt;&lt;span class="sr"&gt;"&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;*"/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/height="&lt;/span&gt;&lt;span class="se"&gt;[^&lt;/span&gt;&lt;span class="sr"&gt;"&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;*"/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;`--tabler-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="s2"&gt;`url('data:image/svg+xml;utf8,&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;')`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-webkit-mask&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`var(--tabler-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mask&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`var(--tabler-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mask-repeat&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;no-repeat&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;background-color&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;currentColor&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vertical-align&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;middle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;display&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;inline-block&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;width&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;spacing.5&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;height&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;spacing.5&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Build an icon component
&lt;/h2&gt;

&lt;p&gt;Now that we have the CSS classes for the icons ready, we can create an icon component that makes it easy to use the icons in our Phoenix project.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;MyAppWeb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;CoreComponents&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="no"&gt;Phoenix&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Component&lt;/span&gt;

  &lt;span class="n"&gt;attr&lt;/span&gt; &lt;span class="ss"&gt;:name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;required:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
  &lt;span class="n"&gt;attr&lt;/span&gt; &lt;span class="ss"&gt;:class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;default:&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;(%{&lt;/span&gt;&lt;span class="ss"&gt;name:&lt;/span&gt; &lt;span class="s2"&gt;"tabler-"&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;assigns&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="sx"&gt;~H""&lt;/span&gt;&lt;span class="s2"&gt;"
    &amp;lt;span class={[@name, @class]} /&amp;gt;
    """&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This component takes the name of the icon as an argument and renders a &lt;code&gt;span&lt;/code&gt; element with the icon name as a class. We also allow the user to pass additional classes.&lt;/p&gt;

&lt;p&gt;We can use this component in our templates like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;lt;.&lt;/span&gt;&lt;span class="n"&gt;icon&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"hero-user"&lt;/span&gt; &lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"bg-blue-600"&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tailwind CSS will generate the appropriate CSS classes for the icon based on the plugin we built in the previous step, and the icon will be displayed with a blue background.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In this article, we have shown how to integrate Tabler Icons into a Phoenix project. We tracked the Tabler Icons source repository using Mix, built an icon component that makes it easy to use the icons in markup, and used the Tailwind CSS plugin feature to add the appropriate CSS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Credits
&lt;/h2&gt;

&lt;p&gt;I would like to credit the Phoenix team for the inspiration for this article. They are already using the same approach to integrate &lt;a href="https://heroicons.com/"&gt;Heroicons&lt;/a&gt; into Phoenix projects. I just adapted it to work with Tabler Icons.&lt;/p&gt;

</description>
      <category>phoenix</category>
      <category>tailwindcss</category>
      <category>tablericons</category>
    </item>
    <item>
      <title>Collaboration in Git: A comparison of different workflows</title>
      <dc:creator>Florian Arens</dc:creator>
      <pubDate>Wed, 24 Apr 2024 00:00:00 +0000</pubDate>
      <link>https://dev.to/farens/collaboration-in-git-a-comparison-of-different-workflows-197p</link>
      <guid>https://dev.to/farens/collaboration-in-git-a-comparison-of-different-workflows-197p</guid>
      <description>&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;p&gt; 1. Introduction&lt;br&gt;
 2. The Problem&lt;br&gt;
 3. What is a Git workflow?&lt;br&gt;
 4. Why is a Git workflow Important?&lt;br&gt;
 5. Git workflows&lt;br&gt;
       5.1. Centralized workflow&lt;br&gt;
       5.2. Gitflow workflow&lt;br&gt;
       5.3. Feature branch workflow&lt;br&gt;
       5.4. Trunk-based workflow&lt;br&gt;
       5.5. Forking workflow&lt;br&gt;
 6. Which Git workflow to choose?&lt;br&gt;
 7. Conclusion&lt;/p&gt;

&lt;p&gt;Choosing the right Git workflow is crucial for the success of a project. This article breaks down the nuances of different Git workflows and provides insight into the pros and cons of each.&lt;/p&gt;

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

&lt;p&gt;Git is a distributed version control system that is mainly used in software development to track changes in source code during the development process. It was created by by Linus Torvalds in 2005 to manage the Linux kernel development. Git is a free and open-source tool that is used by millions of developers worldwide. Altough it is mainly used in software development, Git can be used to track changes in any set of files.&lt;/p&gt;

&lt;p&gt;No technology has revolutionized the way developers work together quite like Git. The &lt;a href="https://survey.stackoverflow.co/2022"&gt;stack overflow developer survey in 2022&lt;/a&gt; showed that almost 94% of developers use Git as their version control system. Git has become the de facto standard for version control systems. Thats why it is important to understand how to work with Git and how to collaborate with other developers.&lt;/p&gt;

&lt;p&gt;This articles assumes that you are already familiar with Git and have a basic understanding of how it works. If you are new to Git, you can learn more about it in the &lt;a href="https://git-scm.com/doc"&gt;official Git documentation&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;When working on a software project with multiple developers, it is important to have a system in place to manage changes to the code base. Without a version control system, developers would not be able to collaborate properly. A version control system like Git makes it easy to track changes to the codebase, collaborate with other developers, and manage the development process.&lt;/p&gt;

&lt;p&gt;However, working with Git can be challenging, especially when working with multiple developers on the same codebase. Git only provides a set of tools for developers to use, but it does not provide guidelines on how to use them and how to work together. This is where Git workflows come in.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is a Git workflow?
&lt;/h2&gt;

&lt;p&gt;A Git workflow is a set of rules that define how developers work with Git. It defines how changes are made, reviewed, and integrated into the codebase. There are many different Git workflows, but the most popular are the Gitflow, feature branch, and forking workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why is a Git workflow Important?
&lt;/h2&gt;

&lt;p&gt;You may be wondering why you need a Git workflow at all. Why not just let developers decide how they want to work with Git?&lt;/p&gt;

&lt;p&gt;The answer is simple: the Git workflow used in a project has an impact on the success of the project. The reason is that a suitable Git workflow helps developers collaborate more effectively, reduces the risk of conflicts and errors, and makes it easier to work together.&lt;/p&gt;

&lt;p&gt;The prerequisite is that the Git workflow is well-defined, followed by all developers, and meets the needs of the project. This is why it is important to choose the right Git workflow for your project. In the following sections, we will provide an overview of the most popular Git workflows and help you choose the right one for your project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Git workflows
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Centralized workflow
&lt;/h3&gt;

&lt;p&gt;Propably the simplest Git workflow is the centralized workflow. In this workflow, there is a single central repository that all developers work from. Developers clone the repository to their local machine, make changes, and push them back to the central repository. This workflow is suitable for small teams or projects where there is no need for complex branching and merging. In addition, it is easy to set up and understand, making it ideal for beginners or teams that are new to Git. Especially for teams that are used to working with a centralized version control system like Subversion, the centralized workflow is a good starting point.&lt;/p&gt;

&lt;p&gt;However, the centralized workflow has some limitations. As there are no branches, conflicts will occur frequently when multiple developers are involved. Therefore, the centralized workflow is not suitable for large beginner teams.&lt;/p&gt;

&lt;p&gt;A typical centralized workflow looks like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Clone the central repository to your local machine.&lt;/li&gt;
&lt;li&gt;Make changes to the code.&lt;/li&gt;
&lt;li&gt;Push the changes to the central repository.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Gitflow workflow
&lt;/h3&gt;

&lt;p&gt;Probably the most popular Git workflow is the Gitflow workflow. It was created by Vincent Driessen in 2010 and has since become the standard for many software development teams. The Gitflow workflow is based on the idea of using branches to manage the development process. The workflow itself defines a set of branches and rules for how to use them. Gitflow provides a clear structure for how changes are made, reviewed, and integrated into the code base.&lt;/p&gt;

&lt;p&gt;A list of branches along with their purpose in the Gitflow workflow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;main&lt;/code&gt; branch: Contains the latest stable version of the code base. Used for production releases.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;develop&lt;/code&gt; branch: Contains the latest development version of the code base. Used for integrating new features and bug fixes.&lt;/li&gt;
&lt;li&gt;Feature branches: Created from the &lt;code&gt;develop&lt;/code&gt; branch and used to develop new features.&lt;/li&gt;
&lt;li&gt;Release branches: Created from the &lt;code&gt;develop&lt;/code&gt; branch and used to prepare a new release.&lt;/li&gt;
&lt;li&gt;Hotfix branches: Created from the &lt;code&gt;main&lt;/code&gt; branch and used to fix critical bugs in the production code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In Gitflow, the team works with multiple branches. This allows developers to work on different features and bug fixes at the same time, without interfering with each other. In addition, code reviews can be done on the feature branches before they are merged into the &lt;code&gt;develop&lt;/code&gt; branch. This helps to catch bugs and errors early in the development process. In addition, it is possible to fix critical bugs in the production code without having to include new features that are still in development because of the separation of the &lt;code&gt;main&lt;/code&gt; and &lt;code&gt;develop&lt;/code&gt; branches.&lt;/p&gt;

&lt;p&gt;However, the Gitflow workflow is more complex than the centralized workflow and requires more discipline from developers. It is important that all developers follow the rules of the workflow and use the correct branches for their changes. For small teams or projects with simple requirements, the Gitflow workflow may be too complex.&lt;/p&gt;

&lt;p&gt;Also, the Gitflow workflow can lead to long-lived branches, which can make it difficult to merge changes back into the main codebase. This can lead to conflicts and errors, especially if the branches are not kept up to date with the latest changes in the codebase. Long-lived branches also make it difficult to perform continuous integration and continuous deployment, because changes are not merged back into the main codebase frequently. Typically, you do not perform continuous integration and continuous deployment using the Gitflow workflow.&lt;/p&gt;

&lt;p&gt;A typical Gitflow workflow looks like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a new branch from the &lt;code&gt;develop&lt;/code&gt; branch to start working on a new feature.&lt;/li&gt;
&lt;li&gt;Make changes to the code in the new branch.&lt;/li&gt;
&lt;li&gt;When the feature is complete, merge the branch into the &lt;code&gt;develop&lt;/code&gt; branch.&lt;/li&gt;
&lt;li&gt;Once the &lt;code&gt;develop&lt;/code&gt; branch is stable, create a new release branch from the &lt;code&gt;develop&lt;/code&gt; branch.&lt;/li&gt;
&lt;li&gt;Make any necessary changes to the code on the release branch.&lt;/li&gt;
&lt;li&gt;When the release is complete, merge the release branch into the &lt;code&gt;main&lt;/code&gt; and &lt;code&gt;develop&lt;/code&gt; branches.&lt;/li&gt;
&lt;li&gt;If a critical bug is found in the production code, create a new hotfix branch from the &lt;code&gt;main&lt;/code&gt; branch, and merge it back into the &lt;code&gt;main&lt;/code&gt; and &lt;code&gt;develop&lt;/code&gt; branches.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Due to the complexity and overhead of the Gitflow workflow, it is usually not suitable for web development projects that often require continuous integration and continuous deployment. The Gitflow workflow is more appropriate for projects with long release cycles, such as desktop applications or embedded systems.&lt;/p&gt;

&lt;p&gt;However, because of its popularity, Gitflow is used by many software development teams for all kinds of projects.&lt;/p&gt;

&lt;p&gt;Read more about the Gitflow workflow in the &lt;a href="https://nvie.com/posts/a-successful-git-branching-model/"&gt;original blog post&lt;/a&gt; by Vincent Driessen.&lt;/p&gt;

&lt;h3&gt;
  
  
  Feature branch workflow
&lt;/h3&gt;

&lt;p&gt;A more modern workflow that is a mix between the centralized and Gitflow workflow is the feature branch workflow. In this workflow, every new change begins by creating a new branch. There are no strict rules about how to use branches like in Gitflow, but the general idea is to create a new branch for every new feature or bugfix. Each branch is created based on the latest version of the codebase and is used to make changes. When the changes are complete, the branch is merged back into the main codebase.&lt;/p&gt;

&lt;p&gt;Typically, feature branches are short-lived. This reduces the risk of conflicts and bugs. Most teams also perform code reviews on feature branches before merging them back into the main codebase.&lt;/p&gt;

&lt;p&gt;A typical Gitflow workflow looks like this&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a new branch based on the latest version of the codebase.&lt;/li&gt;
&lt;li&gt;Make changes to the code on the new branch.&lt;/li&gt;
&lt;li&gt;When the changes are complete, merge the branch back into the main codebase.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The feature Branch workflow is suitable for various types of projects, as it provides a good balance between the simplicity of the centralized workflow and the complexity of the Gitflow workflow, but it may not be suitable for projects that require continuous integration and deployment because changes are not always merged back into the main codebase quickly, even though the goal is to keep branches as short-lived as possible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Trunk-based workflow
&lt;/h3&gt;

&lt;p&gt;In the trunk-based workflow, there is a single main branch called trunk (or simply main), which contains the latest version of the code base. Developers clone the trunk to their local machine, make changes, and push them back to the trunk. This workflow is very similar to the centralized workflow, but while the centralized workflow is often used by beginners, the trunk-based workflow is used by more experienced developers and can also be used by larger teams. In addition, the trunk-based workflow defines a set of rules and guidelines for how to work with the trunk and how to develop new features or bug fixes.&lt;/p&gt;

&lt;p&gt;As only one branch is used in the trunk-based workflow, it requires responsibility and discipline from the teamn. It is important that each developer frequently commits to the trunk and keep changes as small as possible. The test coverage should be high to ensure that changes do not introduce bugs or errors as they are integrated directly into the trunk.&lt;/p&gt;

&lt;p&gt;The trunk-based workflow is often used in projects that require continuous integration and deployment. The deployment process is often automated and each push to the trunk triggers a build and deployment process. When working on new features, the workflow often requires the use of feature flags to hide unfinished features. This allows developers to work on new features without affecting the production environment.&lt;/p&gt;

&lt;p&gt;The trunk-based workflow also forces developers to perform code reviews synchronously, because changes are pushed directly to the trunk and changes can not be reviewed later on in a dedicated pull request like in other workflows. This enforces collaboration and communication between team members and prevents long-lived pull requests that wait for review.&lt;/p&gt;

&lt;p&gt;A typical trunk-based workflow looks like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Clone the trunk to your local machine.&lt;/li&gt;
&lt;li&gt;Make changes to the code.&lt;/li&gt;
&lt;li&gt;Push the changes to the trunk.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The main benefit of the trunk-based workflow is that it eliminates the &lt;em&gt;merge hell&lt;/em&gt; that can occur when working with multiple long-lived branches that need to be merged into the main branch. This significantly reduces release cycle time. It also encourages collaboration and communication between team members and ensures that changes are integrated into the codebase frequently. However, the trunk-based workflow requires discipline and responsibility from the team and may not be suitable for beginners or teams that are new to Git.&lt;/p&gt;

&lt;h3&gt;
  
  
  Forking workflow
&lt;/h3&gt;

&lt;p&gt;The forking workflow is mainly used in open source projects or projects with external contributors. In this workflow, there is a central repository that contains the main codebase and each contributor has their own fork of the repository. Contributors clone their fork to their local machine, make changes and commit back to their fork. When the changes are complete, they create a pull request to merge the changes back into the main codebase.&lt;/p&gt;

&lt;p&gt;A typical forking workflow looks like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fork the central repository to your own account.&lt;/li&gt;
&lt;li&gt;Clone your fork to your local machine.&lt;/li&gt;
&lt;li&gt;Make changes to the code.&lt;/li&gt;
&lt;li&gt;Push the changes to your fork.&lt;/li&gt;
&lt;li&gt;Create a pull request to merge the changes into the main / original codebase.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The forking workflow provides a clear separation between the main codebase and contributions from external contributors. This makes it easy to restrict access to the main repository. Maintainers do not need to grant access to the main repository, since contributors can work on their own fork. This is why the forking workflow is often used in open source projects, where many external contributors are involved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which Git workflow to choose?
&lt;/h2&gt;

&lt;p&gt;Choosing the right Git workflow for your project depends on many factors, such as the size of your team, the complexity of your project, and your development process. Here are some guidelines to help you choose the right Git workflow for your project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For small teams or projects with simple requirements, the centralized workflow is a good place to start. It is easy to set up and understand, making it ideal for beginners or teams new to Git. Especially for teams that are used to working with a centralized version control system before.&lt;/li&gt;
&lt;li&gt;For medium to large teams or projects with complex requirements and long release cycles, the Gitflow workflow is suitable. It provides a clear structure for how changes are made, reviewed, and integrated into the code base.&lt;/li&gt;
&lt;li&gt;For all kind of projects that does not require "real" CI/CD, the feature branch workflow may a good choice. It provides a good balance between the simplicity of the centralized workflow and the complexity of the Gitflow workflow.&lt;/li&gt;
&lt;li&gt;For more experienced developers and teams, the trunk-based workflow may be appropriate. It reduces release cycle time, allows real continuous integration and deployment, and can greatly increase development speed.&lt;/li&gt;
&lt;li&gt;For open source projects or projects with external contributors, the forking workflow is the way to go. It provides a clear separation between the main codebase and contributions from external contributors.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is important to note that above guidelines and workflows from this article are not set in stone. Of course, you may customize workflows or even think of new workflows that better suit your project. They key is to find a workflow that works for your team and your project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In this article, we have provided an overview of the most popular Git workflows. We have discussed the pros and cons of each workflow and provided some guidelines to choose the right Git workflow for your project. Let me know what workflow you use in your project.&lt;/p&gt;

</description>
      <category>collaboration</category>
      <category>git</category>
      <category>gitworkflows</category>
      <category>versioncontrol</category>
    </item>
    <item>
      <title>Display the number of online users in real-time using Phoenix Presence</title>
      <dc:creator>Florian Arens</dc:creator>
      <pubDate>Sat, 18 Feb 2023 20:00:00 +0000</pubDate>
      <link>https://dev.to/farens/display-the-number-of-online-users-in-real-time-using-phoenix-presence-2afm</link>
      <guid>https://dev.to/farens/display-the-number-of-online-users-in-real-time-using-phoenix-presence-2afm</guid>
      <description>&lt;p&gt;Adding real-time functionality is often a challenge, but in Phoenix we already have built-in functionality to make our application live. We dive into how Phoenix Presence can be used to build a real-time user count.&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;p&gt; 1. Introduction&lt;br&gt;
 2. The Problem&lt;br&gt;
 3. Background&lt;br&gt;
       3.1. LiveView Lifecycle&lt;br&gt;
       3.2. Phoenix PubSub&lt;br&gt;
       3.3. Phoenix Presence&lt;br&gt;
 4. Real-time User Count&lt;br&gt;
 5. Conclusion&lt;/p&gt;
&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;I recently redesigned my website and published the first article on my blog. Along with the redesign, I added a live reader counter to blog posts that show the number of active sessions in real time. As soon as you open the article page, the counter increases for each user currently online on that page and it decreases as soon as a session ends.&lt;/p&gt;

&lt;p&gt;In this post, we will dive into how &lt;a href="https://hexdocs.pm/phoenix/Phoenix.Presence.html"&gt;Phoenix Presence&lt;/a&gt; was used to build this feature in a few lines of code. I will also provide some background on the Phoenix LiveView lifecycle and the real-time publisher/subscriber service in Phoenix (called PubSub).&lt;/p&gt;
&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Adding real-time functionality to a web application for the first time is usually a challenge. While there are many packages and services available today that provide APIs and infrastructure for real-time functionality, you almost always have to rely on third-party applications.&lt;/p&gt;

&lt;p&gt;You can decide whether to use a managed infrastructure, for example provided by &lt;a href="https://pusher.com/"&gt;Pusher&lt;/a&gt;, &lt;a href="https://www.ably.io/"&gt;Ably&lt;/a&gt; or &lt;a href="https://liveblocks.io/"&gt;LiveBlocks&lt;/a&gt; or use a self-hosted solution (e.g. built with &lt;a href="https://socket.io/"&gt;Socket.io&lt;/a&gt;). In either case, you will need to integrate the services or libraries into your application and learn how to use them. This process can be time-consuming and frustrating. In addition, manged services can be expensive and self-hosted solutions require maintenance.&lt;/p&gt;

&lt;p&gt;In Phoenix, we already have built-in functionality and infrastructure to make our application live. This is not surprising when we talk about building our application with &lt;strong&gt;Live&lt;/strong&gt; Views.&lt;/p&gt;

&lt;p&gt;You might say that some applications don't necessarily need real-time functionality, but in my opinion, value can be added to almost any application by providing real-time functionality. Even for a small blog that serves static articles, it is an eye-catcher to show the number of live readers. Adding real-time functionality to an application makes it more interactive and therefore more fun to use!&lt;/p&gt;
&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;
&lt;h3&gt;
  
  
  LiveView Lifecycle
&lt;/h3&gt;

&lt;p&gt;To understand why our LiveViews in Phoenix are live by design, we first need to understand the &lt;strong&gt;Phoenix LiveView Lifecycle&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When you send an HTTP request to a LiveView, you receive a server-rendered HTML response. After the initial HTML response, a Websocket connection is established between the client and the LiveView. When the connection is established successfully, the view turns into a stateful view that can handle events and push updates to the client. In other words, a LiveView is a long-lived process that can handle multiple requests and events over time. The process is killed when the client leaves the page.&lt;/p&gt;

&lt;p&gt;Elixir, and therefore LiveView processes, are lightweight and can handle many concurrent connections, making it possible to have one LiveView process for each client. Each LiveView contains stateful values called socket assigns. The assigns are maintained on the server side and are used to render dynamic content in the view. Whenever the assigns change, the LiveView sends a message to the client to update the DOM (minimal JavaScript code that comes with LiveView takes care of the DOM updates for us). Clients can also send events to the LiveView process, which can be used to update the assigns and thus the view. It is important to note that only the necessary parts of the DOM are updated, which makes the application very efficient. LiveView only patches the DOM with the necessary changes.&lt;/p&gt;

&lt;p&gt;The following example shows how to create a simple counter with LiveView. The counter is incremented by clicking a button and the value is displayed in the view.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;MyAppeWeb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;CounterLive&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Index&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="no"&gt;MyAppeWeb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:live_view&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;mount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;counter:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;assigns&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="sx"&gt;~H""&lt;/span&gt;&lt;span class="s2"&gt;"
    &amp;lt;p&amp;gt;Counter: &amp;lt;%= @counter %&amp;gt;&amp;lt;/p&amp;gt;
    &amp;lt;button phx-click="&lt;/span&gt;&lt;span class="n"&gt;increment&lt;/span&gt;&lt;span class="s2"&gt;"&amp;gt;Increment&amp;lt;/button&amp;gt;
    """&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;handle_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"increment"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:noreply&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;counter:&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assigns&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First the counter is set to 0 in the &lt;code&gt;mount/3&lt;/code&gt; function. The &lt;code&gt;render/1&lt;/code&gt; function is used to render the view and the &lt;code&gt;handle_event/3&lt;/code&gt; function is used to handle the event when the button is clicked. The &lt;code&gt;handle_event/3&lt;/code&gt; function increments the counter and updates the assigns. The LiveView process then sends a message to the client to update the DOM with the new counter value.&lt;/p&gt;

&lt;p&gt;This architecture allows us to minimize the JavaScript code in our application while still being able to build interactive applications. Because of the Websocket connection, the LiveView process can push updates to the client, which makes it easy to add real-time functionality to our application. We do not need to fetch data from the server at regular intervals. Instead, the server can push updates to the client as the data changes.&lt;/p&gt;

&lt;p&gt;The LiveView lifecycle makes it possible to build efficient real-time applications. The LiveView process can push updates to the client, and the client can send events to the LiveView process. If you want to learn more about the LiveView Lifecycle, I recommend reading the &lt;a href="https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html"&gt;Phoenix LiveView Documentation&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phoenix PubSub
&lt;/h3&gt;

&lt;p&gt;Phoenix PubSub is a real-time publisher/subscriber service that is used to broadcast messages to multiple subscribers. A subscriber can be a process and therefore can be a LiveView. If you have created a Phoenix application with the official generator, you probably have a PubSub server in your application.&lt;/p&gt;

&lt;p&gt;If not, you can add the PubSub server to your application by adding the following line to your supervision tree:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# lib/my_app/application.ex&lt;/span&gt;
&lt;span class="n"&gt;children&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="o"&gt;...&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="no"&gt;Phoenix&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PubSub&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;name:&lt;/span&gt; &lt;span class="no"&gt;MyApp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PubSub&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can now use the PubSub server to broadcast messages to subscribers. The following example shows how to subscribe to a topic and broadcast a message to that topic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="no"&gt;Phoenix&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PubSub&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;MyApp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PubSub&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"topic"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="no"&gt;Phoenix&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PubSub&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;broadcast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;MyApp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PubSub&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"topic"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Hello"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For more information about Phoenix PubSub and a list of all available features, see the &lt;a href="https://hexdocs.pm/phoenix_pubsub/Phoenix.PubSub.html"&gt;Phoenix PubSub documentation&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phoenix Presence
&lt;/h3&gt;

&lt;p&gt;Phoenix Presence is built on top of Phoenix PubSub and is used to track the presence of users in a channel or process along with some metadata. The metadata can be used to track the state of the user, for example, if the user is typing or online. Phoenix Presence also provides us with features like handling diffs of join and leave events in real time or fetching the current presence state. As we learned in the previous section, LiveViews are long-lived processes. This makes it possible to use Phoenix Presence to track the presence of users in a LiveView.&lt;/p&gt;

&lt;p&gt;Phoenix Presence is easy to integrate into our application. We just need to add a presence module to our application and add it to our supervision tree. The following example shows how a presence module could look:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;MyAppWeb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Presence&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="no"&gt;Phoenix&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Presence&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;otp_app:&lt;/span&gt; &lt;span class="ss"&gt;:my_app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;pubsub_server:&lt;/span&gt; &lt;span class="no"&gt;MyApp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PubSub&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We use the &lt;code&gt;Phoenix.Presence&lt;/code&gt; module to define our presence module. We need to specify the &lt;code&gt;otp_app&lt;/code&gt; and the &lt;code&gt;pubsub_server&lt;/code&gt;. The &lt;code&gt;otp_app&lt;/code&gt; is the name of our application and the &lt;code&gt;pubsub_server&lt;/code&gt; is the PubSub server we are using in our application. We then add the presence module to our supervision tree:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="n"&gt;children&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="o"&gt;...&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="no"&gt;Phoenix&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PubSub&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;name:&lt;/span&gt; &lt;span class="no"&gt;MyApp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PubSub&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="no"&gt;MyAppWeb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Presence&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="no"&gt;MyAppWeb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Endpoint&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make sure it is defined after the &lt;code&gt;Phoenix.PubSub&lt;/code&gt; process in the supervision tree and before the &lt;code&gt;MyAppWeb.Endpoint&lt;/code&gt; process.&lt;/p&gt;

&lt;p&gt;That's it! We now have Phoenix Presence integrated into our application.&lt;/p&gt;

&lt;p&gt;We can now use Phoenix Presence features like listing all presences in a process, getting the current presence state, or handling diffs of join and leave events in real time.&lt;/p&gt;

&lt;p&gt;You can learn more about Phoenix Presence in the &lt;a href="https://hexdocs.pm/phoenix/Phoenix.Presence.html"&gt;Phoenix Presence documentation&lt;/a&gt;. There you will find a list of all the features and how to use them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-time User Count
&lt;/h2&gt;

&lt;p&gt;Let us return to live reader counting and combine the knowledge we gained in the previous sections to build the feature. We will use Phoenix Presence to track the presence of users in a LiveView.&lt;/p&gt;

&lt;p&gt;To prepare, we need to add Phoenix Presence to our application. We will create a presence module that uses &lt;code&gt;Phoenix.Presence&lt;/code&gt; and add it to our supervision tree. We will use the same code as shown in the Phoenix Presence section of this article.&lt;/p&gt;

&lt;p&gt;Since Phoenix Presence uses Phoenix PubSub under the hood, we subscribe to a topic in our LiveView process that will be used to broadcast the presence events. Since we want to have a live reader count for each article, we need to subscribe to a unique topic for each article. We can use the article id as the topic along with a prefix to make the topic unique.&lt;/p&gt;

&lt;p&gt;Let's define a function that returns the topic for a given article:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;defp&lt;/span&gt; &lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;(%{&lt;/span&gt;&lt;span class="ss"&gt;id:&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_article&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="s2"&gt;"article:&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can now subscribe to the topic in the &lt;code&gt;mount/3&lt;/code&gt; function of our LiveView process. We want to do this after the websocket connection between the client and LiveView has been established and not during the first render. We can use the &lt;code&gt;connected?/1&lt;/code&gt; function to check this. We also need to tell Phoenix Presence that we would like to track the presence of the user in the topic. We use the &lt;code&gt;track/3&lt;/code&gt; function to do this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;connected?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="no"&gt;Phoenix&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PubSub&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;MyApp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PubSub&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;article&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_ref&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Presence&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;track&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;article&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s2"&gt;"live_reading"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;%{})&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We pass the topic and PubSub name to the &lt;code&gt;subscribe/2&lt;/code&gt; function. The &lt;code&gt;track/3&lt;/code&gt; function takes the process, the topic, a key, and metadata. The key is used to identify the presence and the metadata can be used to track the state of the user. In our case, we only need to track the presence of the user, so we use an empty map as metadata.&lt;/p&gt;

&lt;p&gt;We now have the user's presence tracked in the topic. We can use the &lt;code&gt;list/1&lt;/code&gt; function to retrieve the presences in a given topic. We write a function that lists the presences for the topic of a given article and counts the number of presences under the given key. The returned value is the live readers count for the article, or more generally the number of online users on the page.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;defp&lt;/span&gt; &lt;span class="n"&gt;get_live_reading_count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;article&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="no"&gt;Presence&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;article&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="s2"&gt;"live_reading"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="ss"&gt;metas:&lt;/span&gt; &lt;span class="n"&gt;list&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Enum&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;_other&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We assign this value to the socket and use it to represent the number of live readers in the view.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="n"&gt;live_reading&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;get_live_reading_count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;article&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;socket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;live_reading:&lt;/span&gt; &lt;span class="n"&gt;live_reading&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the live reader count can be displayed in the view. We now need to handle presence diffs to update the live reader count in real time. We use the &lt;code&gt;handle_info/2&lt;/code&gt; function to listen for the &lt;code&gt;presence_diff&lt;/code&gt; event sent by Phoenix Presence when a user joins or leaves.&lt;/p&gt;

&lt;p&gt;In the event handler, we call the &lt;code&gt;get_live_reading_count/1&lt;/code&gt; function and update the assigns with the new live reader count.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;handle_info&lt;/span&gt;&lt;span class="p"&gt;(%&lt;/span&gt;&lt;span class="no"&gt;Broadcast&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;event:&lt;/span&gt; &lt;span class="s2"&gt;"presence_diff"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;live_reading&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;get_live_reading_count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assigns&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;article&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:noreply&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:live_reading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;live_reading&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it! We now have a live reader counter for each article. The live reader count is updated in real time when a user enters or leaves the article page. You can fetch the value from the assigns in the view and display it wherever you want.&lt;/p&gt;

&lt;p&gt;The full code for the live counter might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;MyAppWeb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;BlogLive&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Show&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="no"&gt;MyAppWeb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:live_view&lt;/span&gt;

  &lt;span class="n"&gt;alias&lt;/span&gt; &lt;span class="no"&gt;Phoenix&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Socket&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Broadcast&lt;/span&gt;
  &lt;span class="n"&gt;alias&lt;/span&gt; &lt;span class="no"&gt;MyApp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Blog&lt;/span&gt;
  &lt;span class="n"&gt;alias&lt;/span&gt; &lt;span class="no"&gt;MyAppWeb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Presence&lt;/span&gt;

  &lt;span class="nv"&gt;@presence_key&lt;/span&gt; &lt;span class="s2"&gt;"live_reading"&lt;/span&gt;

  &lt;span class="nv"&gt;@impl&lt;/span&gt; &lt;span class="no"&gt;Phoenix&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;LiveView&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;mount&lt;/span&gt;&lt;span class="p"&gt;(%{&lt;/span&gt;&lt;span class="s2"&gt;"slug"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;article&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;MyApp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Blog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_article_by_slug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;live_reading&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;get_live_reading_count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;article&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;connected?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="no"&gt;Phoenix&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PubSub&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;MyApp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PubSub&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;article&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_ref&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Presence&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;track&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;article&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nv"&gt;@presence_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;%{})&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="n"&gt;socket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
      &lt;span class="n"&gt;socket&lt;/span&gt;
      &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:article&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;article&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:live_reading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;live_reading&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="nv"&gt;@impl&lt;/span&gt; &lt;span class="no"&gt;Phoenix&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;LiveView&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;handle_info&lt;/span&gt;&lt;span class="p"&gt;(%&lt;/span&gt;&lt;span class="no"&gt;Broadcast&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;event:&lt;/span&gt; &lt;span class="s2"&gt;"presence_diff"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;live_reading&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;get_live_reading_count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assigns&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;article&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:noreply&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:live_reading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;live_reading&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;defp&lt;/span&gt; &lt;span class="n"&gt;get_live_reading_count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;article&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="no"&gt;Presence&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;article&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="nv"&gt;@presence_key&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="ss"&gt;metas:&lt;/span&gt; &lt;span class="n"&gt;list&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Enum&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;_other&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;defp&lt;/span&gt; &lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;(%{&lt;/span&gt;&lt;span class="ss"&gt;id:&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_article&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"article:&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You are welcome to use the code as a starting point for your own online user count indicator. You can also use the code to add other real-time features to your application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;This article showed how to create a live reader count with Phoenix LiveView and Phoenix Presence in just a few lines of code. We also learned about the Phoenix LiveView Lifecycle and how it makes it easy to add real-time functionality to our application.&lt;/p&gt;

</description>
      <category>elixir</category>
      <category>phoenix</category>
      <category>liveview</category>
      <category>realtime</category>
    </item>
  </channel>
</rss>
