Jekyll2024-10-07T19:13:58+00:00https://urishx.github.io/feed.xmlUri Sh.A portfolio centered site with a blog, to feature my adventures and projects in making, programming, and engineering. Built in Jekyll, to be served from Github pages.
Uri Shani[email protected]Enhancing the VPN Profile Switcher: Introducing Local DB Creation and a Self-Test Tool2024-08-18T00:00:00+00:002024-08-18T00:00:00+00:00https://urishx.github.io/2024/08/18/Introducing-Local-DB-Creation-and-a-Self-Test-Tool<p>In my last post, I mentioned upcoming enhancements to the VPN Profile Switcher, and today, I’m excited to share two new scripts that take this project to the next level. These scripts not only add functionality but also pave the way for a more robust and self-sufficient toolset.</p>
<h4 id="1-creating-a-local-database-with-get_groups_and_countries_to_tsvsh">1. Creating a Local Database with <code class="language-plaintext highlighter-rouge">get_groups_and_countries_to_tsv.sh</code></h4>
<p>One of the critical components of the VPN Profile Switcher is the ability to maintain an updated list of available VPN server types and countries. Previously, this information was stored in the <code class="language-plaintext highlighter-rouge">db</code> branch of the repository and updated through a GitHub action. However, this approach comes with a caveat: the GitHub action will be canceled after two months of inactivity in the repository, potentially leaving the database outdated.
<!-- more -->
To address this, I’ve developed the <code class="language-plaintext highlighter-rouge">get_groups_and_countries_to_tsv.sh</code> script. This script enables users to generate a local database of VPN server types (like TOR and P2P) and the countries where these services are available. By running this script, you can ensure that your local database is always up-to-date, independent of the GitHub action.</p>
<p>The script relies on <code class="language-plaintext highlighter-rouge">wget</code> and <code class="language-plaintext highlighter-rouge">jsonfilter</code> to mimic the functionality of the GitHub action. The action itself, housed at <a href="https://github.com/UriShX/curl-then-jq-shell-action">curl-then-jq-shell-action</a>, utilizes <code class="language-plaintext highlighter-rouge">curl</code> and <code class="language-plaintext highlighter-rouge">jq</code> for its operations. With this script, you no longer need to rely on the remote repository’s database, making the <code class="language-plaintext highlighter-rouge">db</code> branch redundant and streamlining your setup.</p>
<h4 id="2-building-a-self-test-tool-with-vpn-status-checker-openwrtsh">2. Building a Self-Test Tool with <code class="language-plaintext highlighter-rouge">vpn-status-checker-openwrt.sh</code></h4>
<p>Another essential aspect of managing VPN connections is ensuring that your VPN is functioning correctly and securely. To this end, I’ve started building a self-test tool with the <code class="language-plaintext highlighter-rouge">vpn-status-checker-openwrt.sh</code> script. This script is designed to perform basic checks such as DNS leak tests and connection status verification, similar to what you might find on the <a href="https://nordvpn.com/what-is-my-ip/">NordVPN What Is My IP page</a>, but in a command-line interface (CLI) format.</p>
<p>While this script is in its early stages, it lays the groundwork for a more comprehensive self-test tool that can be integrated into the VPN Profile Switcher. It’s a straightforward yet powerful way to ensure your VPN is operating as expected, providing peace of mind in an increasingly complex digital landscape.</p>
<h4 id="looking-ahead-wireguard-testing-and-beyond">Looking Ahead: WireGuard Testing and Beyond</h4>
<p>As I continue to enhance the VPN Profile Switcher, one area I plan to explore further is testing the connectivity and robustness of WireGuard. While I haven’t had the chance to dive into this yet, it’s on my radar, and I’m eager to see how WireGuard performs in various scenarios.</p>
<p>In the meantime, these two scripts represent significant steps forward in making the VPN Profile Switcher more self-reliant and user-friendly. Whether you’re looking to maintain an up-to-date local database or ensure your VPN connection is secure, these tools are here to help.</p>
<p>Stay tuned for more updates, including additional features and test results, as I continue to refine and expand the capabilities of the VPN Profile Switcher!</p>Uri Shani[email protected]In my last post, I mentioned upcoming enhancements to the VPN Profile Switcher, and today, I’m excited to share two new scripts that take this project to the next level. These scripts not only add functionality but also pave the way for a more robust and self-sufficient toolset. 1. Creating a Local Database with get_groups_and_countries_to_tsv.sh One of the critical components of the VPN Profile Switcher is the ability to maintain an updated list of available VPN server types and countries. Previously, this information was stored in the db branch of the repository and updated through a GitHub action. However, this approach comes with a caveat: the GitHub action will be canceled after two months of inactivity in the repository, potentially leaving the database outdated.Updating the `vpn-profile-switcher.sh` to Include WireGuard Support2024-07-30T00:00:00+00:002024-07-30T00:00:00+00:00https://urishx.github.io/2024/07/30/updating-the-vpn-swittching-script<p>In a recent update to the <code class="language-plaintext highlighter-rouge">vpn-profile-switcher.sh</code> script, I have added support for WireGuard, expanding its functionality beyond the initial OpenVPN compatibility. This update allows users to choose between OpenVPN and WireGuard for their VPN configurations on OpenWRT. Below, I will walk you through the changes and explain the new options and functionalities added to the script.
<!-- more --></p>
<h2 id="overview-of-the-script">Overview of the Script</h2>
<p>The <code class="language-plaintext highlighter-rouge">vpn-profile-switcher.sh</code> script automates the process of retrieving the recommended NordVPN server, downloading its configuration file, setting the credentials, and configuring OpenWRT to use this server. Initially designed for OpenVPN, the script now includes support for WireGuard, a faster and more efficient VPN protocol.</p>
<h2 id="key-updates-for-wireguard-support">Key Updates for WireGuard Support</h2>
<h3 id="new-options-in-the-usage-function">New Options in the Usage Function</h3>
<p>To accommodate WireGuard, new command-line options have been added:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">printf</span> <span class="s2">" -t | --type < openvpn | wireguard >,</span><span class="se">\t\t</span><span class="s2">Select either OpenVPN or WireGuard. Default is OpenVPN.</span><span class="se">\n</span><span class="s2">"</span>
<span class="nb">printf</span> <span class="s2">" -i | --interface < interface >,</span><span class="se">\t\t</span><span class="s2">Specify interface to use for WireGuard. Default is 'nordlynx'. Applicable only for WireGuard.</span><span class="se">\n</span><span class="s2">"</span>
</code></pre></div></div>
<p>The <code class="language-plaintext highlighter-rouge">--type</code> option allows the user to select the VPN type, either <code class="language-plaintext highlighter-rouge">openvpn</code> or <code class="language-plaintext highlighter-rouge">wireguard</code>. The <code class="language-plaintext highlighter-rouge">--interface</code> option specifies the interface to use for WireGuard, with <code class="language-plaintext highlighter-rouge">nordlynx</code> as the default.</p>
<h3 id="verification-functions">Verification Functions</h3>
<p>Two new functions ensure that valid values are provided for the <code class="language-plaintext highlighter-rouge">--type</code> and <code class="language-plaintext highlighter-rouge">--interface</code> options:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">function </span>verify_vpn_type<span class="o">()</span> <span class="o">{</span>
<span class="k">if</span> <span class="o">[</span> <span class="o">!</span> <span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"openvpn"</span> <span class="o">]</span> <span class="o">&&</span> <span class="o">[</span> <span class="o">!</span> <span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"wireguard"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
</span>logger <span class="nt">-s</span> <span class="s2">"(</span><span class="nv">$0</span><span class="s2">) VPN type must be either openvpn or wireguard, your input was: </span><span class="nv">$1</span><span class="s2">."</span>
<span class="nb">exit </span>1
<span class="k">else
</span><span class="nb">echo</span> <span class="nv">$1</span>
<span class="k">fi</span>
<span class="o">}</span>
<span class="k">function </span>verify_protocol<span class="o">()</span> <span class="o">{</span>
<span class="k">if</span> <span class="o">[</span> <span class="o">!</span> <span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"tcp"</span> <span class="o">]</span> <span class="o">&&</span> <span class="o">[</span> <span class="o">!</span> <span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"udp"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
</span>logger <span class="nt">-s</span> <span class="s2">"(</span><span class="nv">$0</span><span class="s2">) Protocol must be either udp or tcp, your input was: </span><span class="nv">$1</span><span class="s2">."</span>
<span class="nb">exit </span>1
<span class="k">else
</span><span class="nb">echo</span> <span class="nv">$1</span>
<span class="k">fi</span>
<span class="o">}</span>
</code></pre></div></div>
<h3 id="fetching-recommendations">Fetching Recommendations</h3>
<p>The <code class="language-plaintext highlighter-rouge">get_recommended</code> function fetches the recommended server configuration. If WireGuard is selected, it also retrieves the server’s public key:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">function </span>get_recommended<span class="o">()</span> <span class="o">{</span>
<span class="nv">_url</span><span class="o">=</span><span class="s2">"https://api.nordvpn.com/v1/servers/recommendations?"</span>
<span class="k">if</span> <span class="o">[</span> <span class="o">!</span> <span class="nt">-z</span> <span class="s2">"</span><span class="nv">$group_identifier</span><span class="s2">"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
</span><span class="nv">_url</span><span class="o">=</span><span class="k">${</span><span class="nv">_url</span><span class="k">}</span><span class="s2">"filters[servers_groups][identifier]="</span><span class="k">${</span><span class="nv">group_identifier</span><span class="k">}</span><span class="s2">"&"</span>
<span class="k">fi
if</span> <span class="o">[</span> <span class="o">!</span> <span class="nt">-z</span> <span class="s2">"</span><span class="nv">$country_id</span><span class="s2">"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
</span><span class="nv">_url</span><span class="o">=</span><span class="k">${</span><span class="nv">_url</span><span class="k">}</span><span class="s2">"filters[country_id]="</span><span class="k">${</span><span class="nv">country_id</span><span class="k">}</span><span class="s2">"&"</span>
<span class="k">fi
</span><span class="nv">_url</span><span class="o">=</span><span class="k">${</span><span class="nv">_url</span><span class="k">}</span><span class="s2">"filters[servers_technologies][identifier]="</span><span class="k">${</span><span class="nv">vpn_type</span><span class="k">}</span><span class="s2">"_"</span><span class="k">${</span><span class="nv">protocol</span><span class="k">}</span><span class="s2">"&limit="</span><span class="k">${</span><span class="nv">recommendations_n</span><span class="k">}</span>
logger <span class="nt">-s</span> <span class="s2">"(</span><span class="nv">$0</span><span class="s2">) Fetching VPN recommendations from: </span><span class="nv">$_url</span><span class="s2">"</span>
<span class="nv">_json</span><span class="o">=</span><span class="si">$(</span>wget <span class="nt">-q</span> <span class="nt">-O</span> - <span class="s2">"</span><span class="nv">$_url</span><span class="s2">"</span><span class="si">)</span> <span class="o">||</span> <span class="nb">true
</span><span class="nv">recommended</span><span class="o">=</span><span class="si">$(</span>jsonfilter <span class="nt">-s</span> <span class="s2">"</span><span class="nv">$_json</span><span class="s2">"</span> <span class="nt">-e</span> <span class="s1">'$[0].hostname'</span><span class="si">)</span> <span class="o">||</span> <span class="nb">true
</span><span class="nv">recommendations</span><span class="o">=</span><span class="si">$(</span>jsonfilter <span class="nt">-s</span> <span class="s2">"</span><span class="nv">$_json</span><span class="s2">"</span> <span class="nt">-e</span> <span class="s1">'$[*].hostname'</span><span class="si">)</span> <span class="o">||</span> <span class="nb">true
</span><span class="nv">loads</span><span class="o">=</span><span class="si">$(</span>jsonfilter <span class="nt">-s</span> <span class="s2">"</span><span class="nv">$_json</span><span class="s2">"</span> <span class="nt">-e</span> <span class="s1">'$[*].load'</span><span class="si">)</span> <span class="o">||</span> <span class="nb">true
</span><span class="k">if</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$vpn_type</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"wireguard"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
</span><span class="nv">public_key</span><span class="o">=</span><span class="si">$(</span>jsonfilter <span class="nt">-s</span> <span class="s2">"</span><span class="nv">$_json</span><span class="s2">"</span> <span class="nt">-e</span> <span class="s1">'$[0].technologies[*].metadata[*].value'</span><span class="si">)</span> <span class="o">||</span> <span class="nb">true
</span><span class="k">fi</span>
<span class="o">}</span>
</code></pre></div></div>
<h3 id="configuration-handling">Configuration Handling</h3>
<p>The script includes functions for checking existing configurations and enabling or disabling them as needed:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">function </span>check_in_configs<span class="o">()</span> <span class="o">{</span>
<span class="k">if</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$vpn_type</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"openvpn"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
</span><span class="nv">server_name</span><span class="o">=</span><span class="si">$(</span>uci show openvpn | <span class="nb">grep</span> <span class="nv">$recommended</span>.<span class="nv">$protocol</span> | <span class="nb">awk</span> <span class="nt">-F</span> <span class="s1">'\.'</span> <span class="s1">'/config/{print $2}'</span><span class="si">)</span>
<span class="k">elif</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$vpn_type</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"wireguard"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
</span><span class="nv">server_name</span><span class="o">=</span><span class="si">$(</span>uci show network | <span class="nb">grep</span> <span class="s2">"</span><span class="k">${</span><span class="nv">wg_iface</span><span class="k">}</span><span class="s2">.*endpoint_host.*</span><span class="k">${</span><span class="nv">recommended</span><span class="k">}</span><span class="s2">"</span> | <span class="nb">sed</span> <span class="nt">-E</span> <span class="s1">'s/.*_([a-z]{0,2}[0-9]{1,3}).*='</span><span class="se">\'</span><span class="s1">'(.*)'</span><span class="se">\'</span><span class="s1">'$/\1/'</span><span class="si">)</span>
<span class="k">fi</span>
<span class="o">}</span>
<span class="k">function </span>check_enabled<span class="o">()</span> <span class="o">{</span>
<span class="k">if</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$vpn_type</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"openvpn"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
</span><span class="nv">enabled_server</span><span class="o">=</span><span class="si">$(</span>uci show openvpn | <span class="nb">grep</span> <span class="s2">"enabled='1'"</span> | <span class="nb">awk</span> <span class="nt">-F</span> <span class="s1">'\.'</span> <span class="s1">'/.*/{print $2}'</span><span class="si">)</span>
<span class="k">elif</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$vpn_type</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"wireguard"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
</span><span class="nv">enabled_server</span><span class="o">=</span><span class="si">$(</span>uci show network | <span class="nb">grep</span> <span class="s2">"</span><span class="k">${</span><span class="nv">wg_iface</span><span class="k">}</span><span class="s2">.*disabled='0'"</span> | <span class="nb">sed</span> <span class="nt">-E</span> <span class="s1">'s/.*_([a-z]{0,2}[0-9]{1,3}).*='</span><span class="se">\'</span><span class="s1">'.*'</span><span class="se">\'</span><span class="s1">'$/\1/'</span><span class="si">)</span>
<span class="k">fi</span>
<span class="o">}</span>
</code></pre></div></div>
<h3 id="creating-and-enabling-entries">Creating and Enabling Entries</h3>
<p>For WireGuard, the script creates a new entry and configures it with the appropriate parameters:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">function </span>create_new_entry<span class="o">()</span> <span class="o">{</span>
<span class="k">if</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$vpn_type</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"openvpn"</span> <span class="o">]</span> <span class="k">then
</span><span class="nv">new_server</span><span class="o">=</span><span class="si">$(</span><span class="nb">echo</span> <span class="s2">"</span><span class="nv">$recommended</span><span class="s2">"</span> | <span class="nb">sed</span> <span class="s1">'s/[\.\ -]/_/g'</span> | <span class="nb">sed</span> <span class="s2">"s/com/</span><span class="nv">$protocol</span><span class="s2">/g"</span><span class="si">)</span>
uci <span class="nb">set </span>openvpn.<span class="nv">$new_server</span><span class="o">=</span>openvpn
uci <span class="nb">set </span>openvpn.<span class="nv">$new_server</span>.config<span class="o">=</span><span class="s2">"/etc/openvpn/</span><span class="nv">$recommended</span><span class="s2">.</span><span class="nv">$protocol</span><span class="s2">.ovpn"</span>
<span class="k">elif</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$vpn_type</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"wireguard"</span> <span class="o">]</span> <span class="k">then
</span><span class="nv">new_server</span><span class="o">=</span><span class="si">$(</span><span class="nb">echo</span> <span class="s2">"</span><span class="nv">$recommended</span><span class="s2">"</span> | <span class="nb">sed</span> <span class="nt">-E</span> <span class="s1">'s/([a-z]{2}[0-9]{1,3}).*''$/\1/'</span><span class="si">)</span>
uci <span class="nb">set </span>network.<span class="k">${</span><span class="nv">wg_iface</span><span class="k">}</span>_peer_<span class="k">${</span><span class="nv">new_server</span><span class="k">}</span><span class="o">=</span><span class="s2">"wireguard_</span><span class="nv">$wg_iface</span><span class="s2">"</span>
uci <span class="nb">set </span>network.<span class="k">${</span><span class="nv">wg_iface</span><span class="k">}</span>_peer_<span class="k">${</span><span class="nv">new_server</span><span class="k">}</span>.public_key<span class="o">=</span><span class="s2">"</span><span class="nv">$public_key</span><span class="s2">"</span>
uci <span class="nb">set </span>network.<span class="k">${</span><span class="nv">wg_iface</span><span class="k">}</span>_peer_<span class="k">${</span><span class="nv">new_server</span><span class="k">}</span>.endpoint_host<span class="o">=</span><span class="s2">"</span><span class="nv">$recommended</span><span class="s2">"</span>
uci <span class="nb">set </span>network.<span class="k">${</span><span class="nv">wg_iface</span><span class="k">}</span>_peer_<span class="k">${</span><span class="nv">new_server</span><span class="k">}</span>.endpoint_port<span class="o">=</span><span class="s2">"51820"</span>
uci <span class="nb">set </span>network.<span class="k">${</span><span class="nv">wg_iface</span><span class="k">}</span>_peer_<span class="k">${</span><span class="nv">new_server</span><span class="k">}</span>.persistent_keepalive<span class="o">=</span><span class="s2">"25"</span>
uci <span class="nb">set </span>network.<span class="k">${</span><span class="nv">wg_iface</span><span class="k">}</span>_peer_<span class="k">${</span><span class="nv">new_server</span><span class="k">}</span>.route_allowed_ips<span class="o">=</span><span class="s2">"1"</span>
uci add_list network.<span class="k">${</span><span class="nv">wg_iface</span><span class="k">}</span>_peer_<span class="k">${</span><span class="nv">new_server</span><span class="k">}</span>.allowed_ips<span class="o">=</span><span class="s2">"0.0.0.0/0"</span>
uci add_list network.<span class="k">${</span><span class="nv">wg_iface</span><span class="k">}</span>_peer_<span class="k">${</span><span class="nv">new_server</span><span class="k">}</span>.allowed_ips<span class="o">=</span><span class="s2">"::/0"</span>
uci <span class="nb">set </span>network.<span class="k">${</span><span class="nv">wg_iface</span><span class="k">}</span>_peer_<span class="k">${</span><span class="nv">new_server</span><span class="k">}</span>.description<span class="o">=</span><span class="s2">"</span><span class="nv">$recommended</span><span class="s2">"</span>
<span class="k">fi</span>
<span class="o">}</span>
</code></pre></div></div>
<h3 id="restarting-services">Restarting Services</h3>
<p>Depending on the VPN type selected, the script restarts the appropriate service:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">function </span>restart_openvpn<span class="o">()</span> <span class="o">{</span>
uci commit openvpn
/etc/init.d/openvpn restart
<span class="o">}</span>
<span class="k">function </span>restart_wireguard<span class="o">()</span> <span class="o">{</span>
uci commit network
/etc/init.d/network restart
<span class="o">}</span>
</code></pre></div></div>
<h3 id="execution">Execution</h3>
<p>Finally, the script’s main execution block processes the provided command-line arguments and invokes the necessary functions to configure the VPN:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>check_required
<span class="k">while</span> <span class="o">[</span> <span class="o">!</span> <span class="nt">-z</span> <span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span> <span class="o">]</span><span class="p">;</span> <span class="k">do
case</span> <span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span> <span class="k">in</span>
<span class="nt">-h</span> <span class="p">|</span> <span class="nt">--help</span><span class="p">)</span>
show_usage
<span class="p">;;</span>
<span class="nt">-t</span> <span class="p">|</span> <span class="nt">--type</span><span class="p">)</span>
<span class="nb">shift
</span><span class="nv">vpn_type</span><span class="o">=</span><span class="si">$(</span>verify_vpn_type <span class="nv">$1</span><span class="si">)</span>
<span class="p">;;</span>
<span class="nt">-p</span> <span class="p">|</span> <span class="nt">--protocol</span><span class="p">)</span>
<span class="nb">shift
</span><span class="nv">protocol</span><span class="o">=</span><span class="si">$(</span>verify_protocol <span class="nv">$1</span><span class="si">)</span>
<span class="p">;;</span>
<span class="nt">-c</span> <span class="p">|</span> <span class="nt">--country</span><span class="p">)</span>
<span class="nb">shift
</span><span class="nv">country_id</span><span class="o">=</span><span class="si">$(</span>country_code <span class="nv">$1</span><span class="si">)</span>
<span class="p">;;</span>
<span class="nt">-g</span> <span class="p">|</span> <span class="nt">--group</span><span class="p">)</span>
<span class="nb">shift
</span><span class="nv">group_identifier</span><span class="o">=</span><span class="si">$(</span>server_groups <span class="nv">$1</span><span class="si">)</span>
<span class="p">;;</span>
<span class="nt">-r</span> <span class="p">|</span> <span class="nt">--recommendations</span><span class="p">)</span>
<span class="nb">shift
</span><span class="nv">recommendations_n</span><span class="o">=</span><span class="si">$(</span>check_is_num <span class="nv">$1</span><span class="si">)</span>
<span class="p">;;</span>
<span class="nt">-d</span> <span class="p">|</span> <span class="nt">--distance</span><span class="p">)</span>
<span class="nb">shift
</span><span class="nv">load_distance</span><span class="o">=</span><span class="si">$(</span>check_is_num <span class="nv">$1</span><span class="si">)</span>
<span class="p">;;</span>
<span class="nt">-l</span> <span class="p">|</span> <span class="nt">--login-info</span><span class="p">)</span>
<span class="nb">shift
</span><span class="nv">secret</span><span class="o">=</span><span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span>
<span class="p">;;</span>
<span class="nt">-i</span> <span class="p">|</span> <span class="nt">--interface</span><span class="p">)</span>
<span class="nb">shift
</span><span class="nv">wg_iface</span><span class="o">=</span><span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span>
<span class="p">;;</span>
<span class="nt">-b</span> <span class="p">|</span> <span class="nt">--db-location</span><span class="p">)</span>
<span class="nb">shift
</span><span class="nv">db_location</span><span class="o">=</span><span class="si">$(</span>verify_db_location <span class="nv">$1</span><span class="si">)</span>
<span class="p">;;</span>
<span class="k">*</span><span class="p">)</span>
show_usage
<span class="p">;;</span>
<span class="k">esac</span>
<span class="nb">shift
</span><span class="k">done</span>
<span class="c"># Fetch the recommended server</span>
get_recommended
<span class="c"># Check if the recommended server is already configured</span>
check_in_configs
<span class="c"># Disable the current server configuration if it exists</span>
<span class="k">if</span> <span class="o">[</span> <span class="o">!</span> <span class="nt">-z</span> <span class="s2">"</span><span class="nv">$server_name</span><span class="s2">"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
</span>disable_current_entry <span class="nv">$server_name</span>
<span class="k">fi</span>
<span class="c"># Create a new entry for the recommended server if it doesn't already exist</span>
<span class="k">if</span> <span class="o">[</span> <span class="nt">-z</span> <span class="s2">"</span><span class="nv">$server_name</span><span class="s2">"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
</span>create_new_entry
<span class="k">else
</span>enable_existing_entry <span class="nv">$server_name</span>
<span class="k">fi</span>
<span class="c"># Restart the appropriate VPN service</span>
<span class="k">if</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$vpn_type</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"openvpn"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
</span>restart_openvpn
<span class="k">elif</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$vpn_type</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"wireguard"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
</span>restart_wireguard
<span class="k">fi</span>
<span class="c"># Clean up and exit</span>
unset_variables
</code></pre></div></div>
<h2 id="conclusion">Conclusion</h2>
<p>The <code class="language-plaintext highlighter-rouge">vpn-profile-switcher.sh</code> script now offers robust support for both OpenVPN and WireGuard, providing flexibility and efficiency for users. This update simplifies the process of configuring VPN profiles on OpenWRT, ensuring a seamless experience regardless of the chosen VPN protocol. For more details, refer to the <code class="language-plaintext highlighter-rouge">dev</code> branch on the <a href="https://github.com/UriShX/vpn-profile-switcher/tree/dev">repository on GitHub</a>.</p>
<p>In the <a href="/2024/08/18/Introducing-Local-DB-Creation-and-a-Self-Test-Tool.html">next post</a>, I will cover adding some nice to have features as separate scripts, and perhaps add some test results. Stay tuned for more updates and enhancements to the VPN Profile Switcher script.</p>Uri Shani[email protected]In a recent update to the vpn-profile-switcher.sh script, I have added support for WireGuard, expanding its functionality beyond the initial OpenVPN compatibility. This update allows users to choose between OpenVPN and WireGuard for their VPN configurations on OpenWRT. Below, I will walk you through the changes and explain the new options and functionalities added to the script.Upgrading the VPN Profile Switcher Script for OpenWRT2024-07-28T00:00:00+00:002024-07-28T00:00:00+00:00https://urishx.github.io/2024/07/28/upgrading-the-VPN-profile-switcher-script-for-OpenWRT<p>In this post, I will walk you through the process of updating and upgrading a shell script I originally wrote a few years ago for changing the VPN profile on OpenWRT routers. This script connects to NordVPN’s recommended servers, and I’ve recently added support for WireGuard (NordLynx) alongside the existing OpenVPN functionality.</p>
<p>The original script was intended for OpenWRT installations on TP-Link Archer C20i and Raspberry Pi 3B+ with 2019 firmware. You can find the <a href="https://github.com/UriShX/vpn-profile-switcher">VPN Profile Switcher script here</a>.</p>
<p>The script queries NordVPN’s API for the recommended server and then updates the OpenVPN or WireGuard configuration on the router accordingly.</p>
<!-- more -->
<p>Here, I’ll discuss the upgrade process, including adding support for WireGuard, and share the essential parts of the script without exposing application-specific details.</p>
<h2 id="understanding-the-scripts-purpose">Understanding the Script’s Purpose</h2>
<p>The primary goal of the script is to:</p>
<ol>
<li>Fetch the recommended NordVPN server using their API.</li>
<li>Check if the recommended profile exists on the router.</li>
<li>If the profile does not exist, download it and configure it with the saved username and password.</li>
<li>Update the router’s VPN configuration to use the new profile.</li>
<li>Clean up old profiles, keeping only the currently connected and previously connected profiles.</li>
</ol>
<h2 id="research-and-preparation">Research and Preparation</h2>
<p>Since NordVPN started offering WireGuard servers (NordLynx) but didn’t provide a tutorial for manual connection on OpenWRT, I had to spend some time researching how to set it up. I found an <a href="https://web.archive.org/web/20210127045908/https://nordvpn.com/">unofficial documentation</a> for NordVPN’s API long time ago, and used a <a href="https://gist.github.com/bluewalk/7b3db071c488c82c604baf76a42eaad3?permalink_comment_id=5075473#gistcomment-5075473">comment on a GitHub gist</a> as a starting point for extracting the necessary details.</p>
<h2 id="setting-up-wireguard">Setting Up WireGuard</h2>
<p>First, I set up WireGuard on my development machine (a Mac) and then ported it to OpenWRT. Since I was unfamiliar with WireGuard, this involved a lot of trial and error. I used Claude (Anthropic’s AI) to help convert cURL commands to the lighter <code class="language-plaintext highlighter-rouge">wget</code> command suitable for OpenWRT. Despite Claude’s help, I had to install the <code class="language-plaintext highlighter-rouge">wget-ssl</code> package to avoid errors from NordVPN’s servers.</p>
<p>With the necessary data extracted using <code class="language-plaintext highlighter-rouge">jsonfilter</code>, I created a WireGuard interface using LuCI, OpenWRT’s web interface. This initial success allowed me to formalize my approach in a script.</p>
<h2 id="script-details">Script Details</h2>
<p>Below is a simplified description of the script’s structure and logic. The actual script is more detailed and includes error handling and logging.</p>
<ol>
<li><strong>Check for Required Packages</strong>:
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="k">function </span>check_packages<span class="o">()</span> <span class="o">{</span>
opkg list-installed wget-ssl | <span class="nb">grep</span> <span class="nt">-q</span> <span class="nb">.</span> <span class="o">||</span> opkg list-installed curl | <span class="nb">grep</span> <span class="nt">-q</span> <span class="nb">.</span>
<span class="k">if</span> <span class="o">[</span> <span class="nv">$?</span> <span class="nt">-ne</span> 0 <span class="o">]</span><span class="p">;</span> <span class="k">then
</span>logger <span class="nt">-s</span> <span class="s2">"(</span><span class="nv">$0</span><span class="s2">) Error: either wget-ssl or curl are required to run this script. Please install either one of the packages."</span>
<span class="nb">exit </span>1
<span class="k">fi</span>
<span class="o">}</span>
</code></pre></div> </div>
</li>
<li><strong>Fetch Credentials</strong>:
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="k">function </span>get_credentials<span class="o">()</span> <span class="o">{</span>
<span class="k">if </span>which curl <span class="o">></span>/dev/null 2>&1<span class="p">;</span> <span class="k">then
</span><span class="nv">response_json</span><span class="o">=</span><span class="si">$(</span>curl <span class="nt">-s</span> <span class="nt">-u</span> token:<span class="s2">"</span><span class="nv">$access_token</span><span class="s2">"</span> https://api.nordvpn.com/v1/users/services/credentials<span class="si">)</span>
<span class="k">else
</span><span class="nv">response_json</span><span class="o">=</span><span class="si">$(</span>wget <span class="nt">-q</span> <span class="nt">-O</span> - <span class="nt">--auth-no-challenge</span> <span class="nt">--user</span><span class="o">=</span>token <span class="nt">--password</span><span class="o">=</span><span class="s2">"</span><span class="nv">$access_token</span><span class="s2">"</span> https://api.nordvpn.com/v1/users/services/credentials<span class="si">)</span>
<span class="k">fi
if</span> <span class="o">[</span> <span class="nv">$?</span> <span class="nt">-ne</span> 0 <span class="o">]</span><span class="p">;</span> <span class="k">then
</span>logger <span class="nt">-s</span> <span class="s2">"</span><span class="si">$(</span>0<span class="si">)</span><span class="s2"> Error: Failed to fetch credentials. Please check your access token and internet connection."</span>
<span class="nb">exit </span>1
<span class="k">fi</span>
<span class="o">}</span>
</code></pre></div> </div>
</li>
<li><strong>Save Credentials</strong>:
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="k">function </span>save_credentials<span class="o">()</span> <span class="o">{</span>
<span class="k">if</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$vpn_type</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"openvpn"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
</span><span class="nv">_username</span><span class="o">=</span><span class="si">$(</span>jsonfilter <span class="nt">-s</span> <span class="s2">"</span><span class="nv">$response_json</span><span class="s2">"</span> <span class="nt">-e</span> <span class="s1">'@.username'</span><span class="si">)</span>
<span class="nv">_password</span><span class="o">=</span><span class="si">$(</span>jsonfilter <span class="nt">-s</span> <span class="s2">"</span><span class="nv">$response_json</span><span class="s2">"</span> <span class="nt">-e</span> <span class="s1">'@.password'</span><span class="si">)</span>
<span class="nb">echo</span> <span class="s2">"</span><span class="nv">$_username</span><span class="s2">"</span> <span class="o">></span> /etc/openvpn/secret
<span class="nb">echo</span> <span class="s2">"</span><span class="nv">$_password</span><span class="s2">"</span> <span class="o">>></span> /etc/openvpn/secret
<span class="k">else
</span><span class="nv">_key</span><span class="o">=</span><span class="si">$(</span>jsonfilter <span class="nt">-s</span> <span class="s2">"</span><span class="nv">$response_json</span><span class="s2">"</span> <span class="nt">-e</span> <span class="s2">"@.nordlynx_private_key"</span><span class="si">)</span>
uci <span class="nb">set </span>network.<span class="nv">$wg_iface</span><span class="o">=</span>interface
uci <span class="nb">set </span>network.<span class="nv">$wg_iface</span>.proto<span class="o">=</span><span class="s1">'wireguard'</span>
uci <span class="nb">set </span>network.<span class="nv">$wg_iface</span>.private_key<span class="o">=</span><span class="nv">$_key</span>
uci <span class="nb">set </span>network.<span class="nv">$wg_iface</span>.listen_port<span class="o">=</span><span class="s1">'51820'</span>
uci <span class="nb">set </span>network.<span class="nv">$wg_iface</span>.addresses<span class="o">=</span><span class="s1">'10.5.0.2/32'</span>
uci commit network
<span class="k">fi</span>
<span class="o">}</span>
</code></pre></div> </div>
</li>
<li><strong>Script Execution</strong>:
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code> check_packages
get_credentials
save_credentials
</code></pre></div> </div>
</li>
</ol>
<p>For more details, you can review the full script in the <a href="https://github.com/UriShX/vpn-profile-switcher/blob/dev/get_nordvpn_credentials.sh">GitHub repository</a>.</p>
<p>In the next post, I’ll cover the final integration of the updated script into the VPN Profile Switcher, ensuring seamless switching between OpenVPN and WireGuard profiles.</p>Uri Shani[email protected]In this post, I will walk you through the process of updating and upgrading a shell script I originally wrote a few years ago for changing the VPN profile on OpenWRT routers. This script connects to NordVPN’s recommended servers, and I’ve recently added support for WireGuard (NordLynx) alongside the existing OpenVPN functionality. The original script was intended for OpenWRT installations on TP-Link Archer C20i and Raspberry Pi 3B+ with 2019 firmware. You can find the VPN Profile Switcher script here. The script queries NordVPN’s API for the recommended server and then updates the OpenVPN or WireGuard configuration on the router accordingly.Integrating Machines Using Android and Python - Part 32024-07-24T00:00:00+00:002024-07-24T00:00:00+00:00https://urishx.github.io/2024/07/24/Integrating-machines-using-Android-and-Python-Part-3<h3 id="overcoming-dynamic-screen-challenges-with-adb-and-appium">Overcoming Dynamic Screen Challenges with ADB and Appium</h3>
<p>When working with consumer-level machines integrated into larger systems, I faced a significant challenge: dynamic screens. These screens, which change frequently based on user interactions, made it impossible to control the machine using just ADB. This is where Appium came in handy.
<!--more--></p>
<h3 id="setting-up-appium-for-development">Setting Up Appium for Development</h3>
<p>Initially, I set up <a href="https://github.com/appium/appium-desktop">Appium Desktop</a> to explore the possibilities. This graphical interface allowed me to interact with the device and see the UI hierarchy in real-time, which was crucial for understanding how the app’s screens were structured. After gaining enough insight, I transitioned to using the Appium CLI version through NPM for more streamlined and automated control.</p>
<h3 id="crafting-the-adb-hierarchy-handler">Crafting the ADB Hierarchy Handler</h3>
<p>With the knowledge gained, I wrote a custom class to handle interactions with the device. This class, <code class="language-plaintext highlighter-rouge">ADB_Hierarchy_Handler</code>, encapsulates the logic for connecting to the device, parsing the UI hierarchy, and performing actions based on dynamic screen content. While I can’t share the full implementation, here’s an overview of its functionality:</p>
<ol>
<li><strong>Initialization and Connection</strong>: The class starts the ADB server, connects to the device, and initializes Appium. If no device is found, it raises a custom error and exits.</li>
<li><strong>UI Hierarchy Parsing</strong>: Using <code class="language-plaintext highlighter-rouge">xmltodict</code> and regular expressions, the class parses the XML representation of the UI hierarchy to locate elements dynamically.</li>
<li><strong>Action Execution</strong>: Based on the parsed data, it performs actions such as clicking buttons or entering text.</li>
</ol>
<p>Here’s a description of the critical sections of the class:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">time</span>
<span class="kn">import</span> <span class="nn">os</span>
<span class="kn">import</span> <span class="nn">subprocess</span>
<span class="kn">import</span> <span class="nn">re</span>
<span class="kn">from</span> <span class="nn">ppadb.client</span> <span class="kn">import</span> <span class="n">Client</span>
<span class="kn">from</span> <span class="nn">appium</span> <span class="kn">import</span> <span class="n">webdriver</span>
<span class="kn">from</span> <span class="nn">selenium.common</span> <span class="kn">import</span> <span class="n">exceptions</span> <span class="k">as</span> <span class="n">SE</span>
<span class="kn">import</span> <span class="nn">xmltodict</span>
<span class="k">class</span> <span class="nc">CustomError</span><span class="p">(</span><span class="nb">Exception</span><span class="p">):</span>
<span class="k">pass</span>
<span class="k">class</span> <span class="nc">ADB_Hierarchy_Handler</span><span class="p">:</span>
<span class="k">print</span><span class="p">(</span><span class="n">os</span><span class="p">.</span><span class="n">environ</span><span class="p">[</span><span class="s">'ANDROID_HOME'</span><span class="p">])</span>
<span class="n">os</span><span class="p">.</span><span class="n">system</span><span class="p">(</span><span class="s">'adb start-server'</span><span class="p">)</span>
<span class="n">adb</span> <span class="o">=</span> <span class="n">Client</span><span class="p">()</span>
<span class="n">devices</span> <span class="o">=</span> <span class="n">adb</span><span class="p">.</span><span class="n">devices</span><span class="p">()</span>
<span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">devices</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
<span class="k">print</span><span class="p">(</span><span class="s">'no device attached'</span><span class="p">)</span>
<span class="k">raise</span> <span class="n">CustomError</span><span class="p">(</span><span class="s">"no device attached to ADB server"</span><span class="p">)</span>
<span class="n">appium_server</span> <span class="o">=</span> <span class="n">subprocess</span><span class="p">.</span><span class="n">Popen</span><span class="p">(</span><span class="s">'appium'</span><span class="p">,</span> <span class="n">shell</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">stdout</span><span class="o">=</span><span class="n">subprocess</span><span class="p">.</span><span class="n">DEVNULL</span><span class="p">,</span> <span class="n">stderr</span><span class="o">=</span><span class="n">subprocess</span><span class="p">.</span><span class="n">DEVNULL</span><span class="p">)</span>
<span class="n">caps</span> <span class="o">=</span> <span class="p">{</span>
<span class="s">"platformName"</span><span class="p">:</span> <span class="s">"Android"</span><span class="p">,</span>
<span class="s">"automationName"</span><span class="p">:</span> <span class="s">"UiAutomator2"</span><span class="p">,</span>
<span class="s">"skipServerInstallation"</span><span class="p">:</span> <span class="bp">True</span><span class="p">,</span>
<span class="s">"disableWindowAnimation"</span><span class="p">:</span> <span class="bp">True</span>
<span class="p">}</span>
<span class="n">driver</span> <span class="o">=</span> <span class="n">webdriver</span><span class="p">.</span><span class="n">Remote</span><span class="p">(</span><span class="s">"http://localhost:4723/wd/hub"</span><span class="p">,</span> <span class="n">caps</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">parse_ui_hierarchy</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="n">xml_data</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">driver</span><span class="p">.</span><span class="n">page_source</span>
<span class="n">data_dict</span> <span class="o">=</span> <span class="n">xmltodict</span><span class="p">.</span><span class="n">parse</span><span class="p">(</span><span class="n">xml_data</span><span class="p">)</span>
<span class="k">return</span> <span class="n">data_dict</span>
<span class="k">def</span> <span class="nf">find_element</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">search_dict</span><span class="p">,</span> <span class="n">field</span><span class="p">):</span>
<span class="n">elements</span> <span class="o">=</span> <span class="n">get_recursively</span><span class="p">(</span><span class="n">search_dict</span><span class="p">,</span> <span class="n">field</span><span class="p">)</span>
<span class="k">return</span> <span class="n">elements</span>
<span class="k">def</span> <span class="nf">click_element</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">element</span><span class="p">):</span>
<span class="n">bounds</span> <span class="o">=</span> <span class="n">get_bounds_as_dict</span><span class="p">(</span><span class="n">element</span><span class="p">[</span><span class="s">'@bounds'</span><span class="p">])</span>
<span class="n">center</span> <span class="o">=</span> <span class="n">get_center</span><span class="p">(</span><span class="n">bounds</span><span class="p">)</span>
<span class="bp">self</span><span class="p">.</span><span class="n">driver</span><span class="p">.</span><span class="n">tap</span><span class="p">([(</span><span class="n">center</span><span class="p">[</span><span class="s">'x'</span><span class="p">],</span> <span class="n">center</span><span class="p">[</span><span class="s">'y'</span><span class="p">])])</span>
</code></pre></div></div>
<h3 id="utilizing-helper-functions-for-parsing">Utilizing Helper Functions for Parsing</h3>
<p>I leveraged several helper functions to streamline the process of parsing the UI hierarchy and extracting necessary information:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">re</span>
<span class="n">bounds_regex</span> <span class="o">=</span> <span class="n">re</span><span class="p">.</span><span class="nb">compile</span><span class="p">(</span><span class="s">"</span><span class="se">\\</span><span class="s">[(</span><span class="se">\\</span><span class="s">d+),(</span><span class="se">\\</span><span class="s">d+)</span><span class="se">\\</span><span class="s">]</span><span class="se">\\</span><span class="s">[(</span><span class="se">\\</span><span class="s">d+),(</span><span class="se">\\</span><span class="s">d+)</span><span class="se">\\</span><span class="s">]"</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">get_recursively</span><span class="p">(</span><span class="n">search_dict</span><span class="p">,</span> <span class="n">field</span><span class="p">):</span>
<span class="n">fields_found</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">for</span> <span class="n">key</span><span class="p">,</span> <span class="n">value</span> <span class="ow">in</span> <span class="n">search_dict</span><span class="p">.</span><span class="n">items</span><span class="p">():</span>
<span class="k">if</span> <span class="n">key</span> <span class="o">==</span> <span class="n">field</span><span class="p">:</span>
<span class="n">fields_found</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">value</span><span class="p">)</span>
<span class="k">elif</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">value</span><span class="p">,</span> <span class="nb">dict</span><span class="p">):</span>
<span class="n">results</span> <span class="o">=</span> <span class="n">get_recursively</span><span class="p">(</span><span class="n">value</span><span class="p">,</span> <span class="n">field</span><span class="p">)</span>
<span class="n">fields_found</span><span class="p">.</span><span class="n">extend</span><span class="p">(</span><span class="n">results</span><span class="p">)</span>
<span class="k">elif</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">value</span><span class="p">,</span> <span class="p">(</span><span class="nb">list</span><span class="p">,</span> <span class="nb">tuple</span><span class="p">)):</span>
<span class="k">for</span> <span class="n">item</span> <span class="ow">in</span> <span class="n">value</span><span class="p">:</span>
<span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">item</span><span class="p">,</span> <span class="nb">dict</span><span class="p">):</span>
<span class="n">more_results</span> <span class="o">=</span> <span class="n">get_recursively</span><span class="p">(</span><span class="n">item</span><span class="p">,</span> <span class="n">field</span><span class="p">)</span>
<span class="n">fields_found</span><span class="p">.</span><span class="n">extend</span><span class="p">(</span><span class="n">more_results</span><span class="p">)</span>
<span class="k">return</span> <span class="n">fields_found</span>
<span class="k">def</span> <span class="nf">get_bounds_as_dict</span><span class="p">(</span><span class="n">bounds_str</span><span class="p">):</span>
<span class="n">bounds</span> <span class="o">=</span> <span class="p">{}</span>
<span class="n">matches</span> <span class="o">=</span> <span class="n">bounds_regex</span><span class="p">.</span><span class="n">search</span><span class="p">(</span><span class="n">bounds_str</span><span class="p">)</span>
<span class="n">bounds</span><span class="p">[</span><span class="s">'left'</span><span class="p">]</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">matches</span><span class="p">.</span><span class="n">group</span><span class="p">(</span><span class="mi">1</span><span class="p">))</span>
<span class="n">bounds</span><span class="p">[</span><span class="s">'top'</span><span class="p">]</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">matches</span><span class="p">.</span><span class="n">group</span><span class="p">(</span><span class="mi">2</span><span class="p">))</span>
<span class="n">bounds</span><span class="p">[</span><span class="s">'right'</span><span class="p">]</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">matches</span><span class="p">.</span><span class="n">group</span><span class="p">(</span><span class="mi">3</span><span class="p">))</span>
<span class="n">bounds</span><span class="p">[</span><span class="s">'bottom'</span><span class="p">]</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">matches</span><span class="p">.</span><span class="n">group</span><span class="p">(</span><span class="mi">4</span><span class="p">))</span>
<span class="k">return</span> <span class="n">bounds</span>
<span class="k">def</span> <span class="nf">get_center</span><span class="p">(</span><span class="n">bounds_dict</span><span class="p">):</span>
<span class="n">coordinates</span> <span class="o">=</span> <span class="p">{}</span>
<span class="n">coordinates</span><span class="p">[</span><span class="s">'x'</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span><span class="n">bounds_dict</span><span class="p">[</span><span class="s">'right'</span><span class="p">]</span> <span class="o">-</span> <span class="n">bounds_dict</span><span class="p">[</span><span class="s">'left'</span><span class="p">])</span> <span class="o">//</span> <span class="mi">2</span> <span class="o">+</span> <span class="n">bounds_dict</span><span class="p">[</span><span class="s">'left'</span><span class="p">]</span>
<span class="n">coordinates</span><span class="p">[</span><span class="s">'y'</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span><span class="n">bounds_dict</span><span class="p">[</span><span class="s">'bottom'</span><span class="p">]</span> <span class="o">-</span> <span class="n">bounds_dict</span><span class="p">[</span><span class="s">'top'</span><span class="p">])</span> <span class="o">//</span> <span class="mi">2</span> <span class="o">+</span> <span class="n">bounds_dict</span><span class="p">[</span><span class="s">'top'</span><span class="p">]</span>
<span class="k">return</span> <span class="n">coordinates</span>
</code></pre></div></div>
<h3 id="wrapping-up-and-testing">Wrapping Up and Testing</h3>
<p>The class includes a main test case to ensure everything works as expected:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">"__main__"</span><span class="p">:</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">handler</span> <span class="o">=</span> <span class="n">ADB_Hierarchy_Handler</span><span class="p">()</span>
<span class="n">ui_data</span> <span class="o">=</span> <span class="n">handler</span><span class="p">.</span><span class="n">parse_ui_hierarchy</span><span class="p">()</span>
<span class="n">elements</span> <span class="o">=</span> <span class="n">handler</span><span class="p">.</span><span class="n">find_element</span><span class="p">(</span><span class="n">ui_data</span><span class="p">,</span> <span class="s">'desired_element_id'</span><span class="p">)</span>
<span class="k">if</span> <span class="n">elements</span><span class="p">:</span>
<span class="n">handler</span><span class="p">.</span><span class="n">click_element</span><span class="p">(</span><span class="n">elements</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span>
<span class="k">except</span> <span class="n">CustomError</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
<span class="k">print</span><span class="p">(</span><span class="s">f"An error occurred: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
</code></pre></div></div>
<p>This code initializes the handler, parses the UI hierarchy, finds the desired element by its ID, and clicks it. This approach enabled me to automate interactions with the device efficiently, overcoming the challenges posed by dynamic screens without relying on OpenCV.</p>
<h3 id="conclusion">Conclusion</h3>
<p>Through the use of Appium and careful parsing of the UI hierarchy, I successfully automated the control of machines with dynamic screens. This solution proved robust and adaptable, paving the way for more efficient integration of consumer-level machines into larger systems.</p>
<p>I also believe that using Appium and a virtual device runnning on the PC controlling the robot could have been a better solution for the first machine, instead of the unused cell phone we used to get that working. I should have tried to get it going, but I didn’t have the time to revive the old machine and tinker with it. I’ll definitely consider it for future projects.</p>Uri Shani[email protected]Overcoming Dynamic Screen Challenges with ADB and Appium When working with consumer-level machines integrated into larger systems, I faced a significant challenge: dynamic screens. These screens, which change frequently based on user interactions, made it impossible to control the machine using just ADB. This is where Appium came in handy.Openwrt On Raspberry Pi For Shell Script Development2024-07-11T00:00:00+00:002024-07-11T00:00:00+00:00https://urishx.github.io/2024/07/11/OpenWRT-on-raspberry-pi-for-shell-script-development<h3 id="setting-up-an-openwrt-dev-environment-on-a-raspberry-pi-3-b">Setting Up an OpenWRT Dev Environment on a Raspberry Pi 3 B+</h3>
<p>It’s been a while since I last tinkered with something fun, but I finally got around to setting up an OpenWRT development environment on my Raspberry Pi 3 B+. This time, it’s for version 2 of my VPN profile switcher script, which you can find on my <a href="https://github.com/urishx/vpn-profile-switcher">GitHub repo</a>.</p>
<p><strong>Note: this post is about setting up a shell script dev environment on OpenWRT, not an actual OpenWRT development environment. For that, please follow the official <a href="https://openwrt.org/docs/guide-developer/start">developer’s guide</a></strong></p>
<!--more-->
<h3 id="flashing-openwrt">Flashing OpenWRT</h3>
<p>To start, I obtained the most recent <a href="https://openwrt.org/toh/raspberry_pi_foundation/raspberry_pi">OpenWRT image for the Raspberry Pi</a> and flashed it onto an SD card. The process was simple - download the image, use Balena Etcher, and you’re ready to proceed. Afterward, I powered on the Pi, set a password, and added my SSH keys from my main machine. It’s important to prioritize security and accessibility, isn’t it?</p>
<p>To make sure I had enough space for everything, I followed OpenWRT’s guide to <a href="https://openwrt.org/docs/guide-user/installation/installation_methods/sd_card#expanding_the_filesystem">expand the filesystem</a> for ext4. This step is crucial, especially if you plan on installing packages or running scripts.</p>
<p>For some reason, I had to also install <code class="language-plaintext highlighter-rouge">e2fsck</code> as well, so my process is as follows:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>opkg update
opkg <span class="nb">install </span>parted tune2fs resize2fs e2fsck
parted
p
resizepart 2 8GB <span class="c"># or whatever size you want</span>
q
mount <span class="nt">-o</span> remount,ro / <span class="c">#Remount root as Read Only</span>
tune2fs <span class="nt">-O</span>^resize_inode /dev/mmcblk0p2 <span class="c">#Remove reserved GDT blocks</span>
<span class="c"># This failed, so I had to run e2fsck</span>
e2fsck <span class="nt">-f</span> /dev/mmcblk0p2 <span class="c">#Answer yes to all questions</span>
tune2fs <span class="nt">-O</span>^resize_inode /dev/mmcblk0p2 <span class="c">#Remove reserved GDT blocks</span>
fsck.ext4 /dev/mmcblk0p2 <span class="c">#Fix part, answer yes to remove GDT blocks remnants</span>
reboot
<span class="c"># After reboot</span>
resize2fs /dev/mmcblk0p2
</code></pre></div></div>
<h3 id="wifi-hotspot-and-wan-setup">WiFi Hotspot and WAN Setup</h3>
<p>Next, I turned the Pi into a WiFi hotspot and set the ethernet port as WAN. This means the Pi can share its internet connection wirelessly while staying wired to my main router.</p>
<p>I actually tried to set up the wireless interface on the pi as a STA (client) + AP (master), but it didn’t work out. I ended up using the USB WiFi adapter as a WWAN instead. You can find some threads on the topic <a href="https://forum.openwrt.org/t/how-to-configure-raspberry-pi-3b-as-a-repeater/46202/14">here</a>, and some more info on the <a href="https://openwrt.org/packages/pkgdata/travelmate">travelmate</a> <a href="https://forum.openwrt.org/t/travelmate-support-thread/5155/755">support thread</a>. <em>Apperantly it is possible w/ Raspoberry pi OS, here’s a <a href="https://raspberrypi.stackexchange.com/questions/89803/access-point-as-wifi-router-repeater-optional-with-bridge">how-to</a></em>.</p>
<h3 id="adding-a-wifi-wwan">Adding a WiFi WWAN</h3>
<p>Now, for a bit of fun – I had a spare TL-822N USB WiFi adapter and decided to add a WiFi WWAN. I followed what worked for someone <a href="https://forum.openwrt.org/t/configuring-tl-wn822n-usb-wifi-adapter/60889/6">on the OpenWRT forum</a>, but ran into some issues with the adapter repeatedly failing to connect. After some head-scratching, I figured it might be a power issue. Swapped out the power supply for a beefier 3W one - and the problem was solved.</p>
<p>So, assuming you have a beefy enough power supply, install the following packages:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>opkg update
opkg <span class="nb">install </span>kmod-usb2 rtl8192eu-firmware kmod-rtl8xxxu
</code></pre></div></div>
<h4 id="why-the-wwan">Why the WWAN?</h4>
<p>You might wonder why go through the hassle of adding a WWAN. Well, I wanted to keep the ethernet port on my main router free and have a ready-to-use travel router for future adventures.</p>
<h3 id="getting-openvpn-running">Getting OpenVPN Running</h3>
<p>With the networking sorted, it was time to get OpenVPN up and running with NordVPN. I followed my own instructions from the <a href="https://github.com/urishx/vpn-profile-switcher#readme">repo’s README</a> – always nice when past me makes things easier for future me.</p>
<h3 id="ssh-convenience">SSH Convenience</h3>
<p>To make my life easier, I ensured my SSH connection was rock-solid and installed <code class="language-plaintext highlighter-rouge">nano</code> on the Pi. On my main machine, I set up the <a href="https://marketplace.visualstudio.com/items?itemName=Kelvin.vscode-sshfs">SSH FS</a> extension in VS Code and installed the <code class="language-plaintext highlighter-rouge">openssh-sftp-server</code> package on the Pi, following OpenWRT’s documentation. This setup lets me edit files on the Pi directly from VS Code, which provides some useful quality of life features, such as switching between files easily, syntax highlighting, and Copilot.</p>
<h3 id="final-touches">Final Touches</h3>
<p>Lastly, I installed <code class="language-plaintext highlighter-rouge">git</code> and <code class="language-plaintext highlighter-rouge">git-http</code> packages on the Pi so I could clone and manage repos directly. With everything in place, my OpenWRT dev environment was ready to roll.</p>
<h4 id="edit-2024-07-24">Edit: 2024-07-24</h4>
<p>When trying to work on the script on the Pi, I had a bit of trouble pushing the changes to the repo. I decided to set up a new SSH key on the Pi and add it to my GitHub account. This way, I could push changes without any issues. Sicne the default SSH agentt on tthe standard OpenWRT is <code class="language-plaintext highlighter-rouge">dropbear</code>, I followed <a href="https://openwrt.org/docs/guide-user/security/dropbear.public-key.auth#generating_public_and_private_keys_on_the_openwrt_machine">this how-to</a> on the OpenWRT wiki. Here’s how I did it:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir</span> ~/.ssh
dropbearkey <span class="nt">-t</span> ed25519 <span class="nt">-f</span> ~/.ssh/id_dropbear
dropbearkey <span class="nt">-y</span> <span class="nt">-f</span> ~/.ssh/id_dropbear
</code></pre></div></div>
<p>I then copied the public key to my GitHub account and added the private key to my SSH agent. This way, I could push changes to the repo without any issues.</p>
<p>I also set the username and email for the git config:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git config <span class="nt">--global</span> user.name <span class="s2">"Your Name"</span>
git config <span class="nt">--global</span> user.email <span class="s2">"[email protected]"</span>
</code></pre></div></div>
<p>I had trouble working with source control from my remote <code class="language-plaintext highlighter-rouge">sshfs</code> connection, so I decided to clone the repo directly to the Pi. This way, I could work on the script without any issues. That’s a bit of a bummer, but it’s a small price to pay for a stable development environment.</p>
<h3 id="wrapping-up">Wrapping Up</h3>
<p>So there you have it – a solid OpenWRT development environment on a Raspberry Pi 3 B+. Perfect for freeing up your main router’s ethernet connection or having a travel-ready router. Happy tinkering!</p>
<p>Cheers,
Uri</p>Uri Shani[email protected]Setting Up an OpenWRT Dev Environment on a Raspberry Pi 3 B+ It’s been a while since I last tinkered with something fun, but I finally got around to setting up an OpenWRT development environment on my Raspberry Pi 3 B+. This time, it’s for version 2 of my VPN profile switcher script, which you can find on my GitHub repo. Note: this post is about setting up a shell script dev environment on OpenWRT, not an actual OpenWRT development environment. For that, please follow the official developer’s guideIntegrating Machines Using Android and Python - Part 22022-01-21T00:00:00+00:002022-01-21T00:00:00+00:00https://urishx.github.io/2022/01/21/Integrating-machines-using-Android-and-Python-Part-2<p>For the next iteration of our machine, a different machine was selected because the first one wasn’t quite up to the task. Besides functioning poorly (for our purposes, at least), the Bluetooth connection wasn’t reliable enough.</p>
<p>Asking around, one of the entrepreneurs’ CTO heard of a (supposedly) better machine that had been “hacked” by one of the distributor’s clients.
<!-- more -->
A few days passed, and we got to play with a new (secondhand) machine, already half disassembled by one of the mechanical engineers already modeling its parts.</p>
<h2 id="first-thoughts-again">First thoughts, again</h2>
<p>Since the machine’s electronics were mostly out, I first tried figuring out what’s what. I found a microcontroller board at the back of the machine, and found that it was driven by a microcontroller I sort of know - STM32F103. I tried figuring out what each IO was doing, and doing so I found the TX/RX lines going to the touch screen at the front of the machine. I then followed SparkFun’s tutorial on <a href="https://learn.sparkfun.com/tutorials/using-the-usb-logic-analyzer-with-sigrok-pulseview/all">using a logic analyzer with Sigrok</a>, to capture the UART comm.</p>
<p>At this point my goal was to record the communication sent back and forth between the uC and the touch screen, and to figure out how to trigger certain functions.</p>
<p>It turned out to be a good learning experience, but not a worthwhile one. The communication protocol used in this particular machine was too complicated for me to decipher, as it was using what seemed to be changing in length with every operation. Without some prior knowledge about the communication scheme, it seemed like a lot of work was still ahead of me if I were to continue down this road.</p>
<p>As the communication seemed too complex to replicate, I took another look at the machine. I found that what I assumed to be only a touch screen had actually a full microprocessor board driving it. When I first looked at the disassembled machine, I thought the whole machine was being driven by some firmware running on the STM32 board, and that the UART communication to the touch display was to a daughter board. But finding that the other board was sporting a microprocessor, in what seemed to be a smartphone development board form, changed my perspective. I was now thinking that the smartphone dev board was the actual computer doing the heavy lifting, while the STM32 board was functioning as a peripheral to it.</p>
<p>I shared my findings with the consultant who suggested we use Macrodroid for the first machine. He suggested we start the machine and prod around the GUI on the touch screen to get a sense of what was the OS running behind the scenes. He also suggested it would probably be Android of some sort, as it was relatively easy to get that working as a touch screen UI for the machine.</p>
<p>It took a while, but eventually, I got to the machine’s settings by trying to set the system language for the UI. It seems the developers of the GUI decided to use the OS language settings, and so I could confirm it was running a recent enough Android system.</p>
<p>I also talked with the distributor, and through him got to the guy who “hacked” the machine prior to me. The last guy got to about where I got, by prodding the UI of the machine set in his office. But this guy got blocked by the distributor’s password - whereas in the case of my machine the distributor had unlocked it specifically for our use.</p>
<h3 id="stay-tuned">Stay Tuned</h3>
<p>In the <a href="/2024/07/24/Integrating-machines-using-Android-and-Python-Part-3.html">next post</a>, I will cover how I used ADB and Appium to overcome the challenges posed by the dynamic screens of the second machine. Stay tuned for a detailed walkthrough of the process and the final solution!</p>Uri Shani[email protected]For the next iteration of our machine, a different machine was selected because the first one wasn’t quite up to the task. Besides functioning poorly (for our purposes, at least), the Bluetooth connection wasn’t reliable enough. Asking around, one of the entrepreneurs’ CTO heard of a (supposedly) better machine that had been “hacked” by one of the distributor’s clients.Integrating Machines Using Android and Python - Part 12022-01-11T00:00:00+00:002022-01-11T00:00:00+00:00https://urishx.github.io/2022/01/11/Integrating-machines-using-Android-and-Python-Part-1<p>As part of working for an engineering firm, I sometimes need to integrate machines into a system designed by one of the firm’s mechanical engineers. This could be part of a POC, so we can move things a bit quicker. Or it could be part of our final product - integrating proven products into our designs means we can focus on getting the final machines built quicker, and less prone to errors. This is done on a regular basis in different disciplines of engineering. In mechanical engineering, a lot of designs integrate standard parts such as bearings or motors, for example. In electronics engineering, relying on pre-built modules in designs could save a lot of hassle.</p>
<p>But in this one machine we’re building, I needed to integrate a couple of different machines that required a different set of skills, since they were (definitely) not meant to be included in any larger machine - and that’s what I want to write about.
<!--more-->
In this particular machine, we needed to incorporate a consumer-level machine into our bigger system. We ended up incorporating two different machines, one for the POC, and a different one for the first real iteration of the machine.</p>
<h2 id="what-are-my-options-anyway">What are my options, anyway?</h2>
<p>There are a few different options for controlling machines that have Android interfaces.</p>
<p>I’ve had a bit of a hard time figuring out how to get it working, so I wrote it down. Mostly, to not have to search for answers again in the future.</p>
<p>These options include:</p>
<ul>
<li>Automate the machine through macros I wrote with webhooks, controlling an app designed by the machine manufacturer, running on a phone hidden inside our machine.</li>
<li>Automate the machine by recording UART between the controller and the UI device (started, deemed too hard).</li>
<li>Automate the machine by reverse engineering the controller firmware (never tried, <em>WAYYYY</em> too hard for the specific machine).</li>
<li>Automate the machine by leveraging ADB:
<ul>
<li>Blindly.</li>
<li>UIAutomator (deprecated by Google, works only for static content).</li>
<li>OpenCV pattern matching (last resort).</li>
<li>Appium (UIAutomator2 driver, FOSS, QA tool).</li>
</ul>
</li>
</ul>
<h2 id="the-first-machine">The first machine</h2>
<p>The first machine was selected thanks to its ability to be controlled remotely via an Android app that sent commands through Bluetooth.</p>
<h3 id="initial-probing">Initial probing</h3>
<p>Since I knew it was Bluetooth-based, I first tried sniffing the communication, using Wireshark and Nordic nRF. I ordered an nRF52840 Feather from <a href="https://www.adafruit.com/product/4062">Adafruit</a>, and followed Adafruit’s great <a href="https://learn.adafruit.com/ble-sniffer-with-nrf52840?view=all">guide</a> for installing and using that to sniff BLE communication between the machine and the application installed on my phone. I also used Nordic’s own <a href="https://www.nordicsemi.com/Products/Development-tools/nrf-connect-for-mobile">nRF Connect</a> on my phone (there’s a desktop app as well) to see the BLE descriptors as well.</p>
<p>But while looking at the sniffed packets I realized the machine was only advertising its existence through BLE, then (probably) moving onto the more secure Bluetooth Classic communication for actually being controlled through the accompanying smartphone app.</p>
<p>A quick look through the code for the app (apk packages can mostly be unzipped) convinced us it was time to think of a simpler way for controlling this particular machine.</p>
<h3 id="second-best---but-winning">Second best - but winning</h3>
<p>As a quicker method to get the POC rolling, a consultant suggested we try installing a macro app for Android. Specifically, he suggested we use <a href="https://www.macrodroid.com/">Macrodroid</a>, since he’s had good results using it in the past.</p>
<p>The process was quite simple, if a bit tedious. I found a disposable phone from an earlier project, installed the application for controlling the machine as well as Macrodroid, and started setting up a macro that would start the app, wait for a bit, then press the desired button on the second page, then delay again, and finally the third button.</p>
<p>For selecting textual buttons, Macrodroid offers a search functionality. For moving sliders around, I enabled Android’s Pointer Location in the Developer’s options, and set up moving the slider based on the actual display of the device I used.</p>
<p>Finally, I set up a webhook with some query parameters to trigger the macro on the device - and my part of the job was done.</p>
<p>I passed the webhook addresses and the parameters to the guy writing the rest of the machine’s controller, made sure the phone was always on and had its Bluetooth, location, and WiFi, always on (and with power), and went on to the next project.</p>
<h3 id="stay-tuned">Stay Tuned</h3>
<p>In the <a href="/2022/01/21/Integrating-machines-using-Android-and-Python-Part-2.html">next post</a>, I’ll cover the next iteration of the machine, and how I had to use a (very) different approach to get it working. Stay tuned for a detailed walkthrough of the process and the final solution!</p>Uri Shani[email protected]As part of working for an engineering firm, I sometimes need to integrate machines into a system designed by one of the firm’s mechanical engineers. This could be part of a POC, so we can move things a bit quicker. Or it could be part of our final product - integrating proven products into our designs means we can focus on getting the final machines built quicker, and less prone to errors. This is done on a regular basis in different disciplines of engineering. In mechanical engineering, a lot of designs integrate standard parts such as bearings or motors, for example. In electronics engineering, relying on pre-built modules in designs could save a lot of hassle. But in this one machine we’re building, I needed to integrate a couple of different machines that required a different set of skills, since they were (definitely) not meant to be included in any larger machine - and that’s what I want to write about.Customizing Jekyll With A Portfolio2021-01-20T00:00:00+00:002021-01-20T00:00:00+00:00https://urishx.github.io/2021/01/20/Customizing-Jekyll-with-a-Portfolio<p>In my <a href="/2020/12/16/Customizing-Jekyll's-default-theme.html">last post</a>
I detailed how I used <a href="https://jekyllrb.com/">Jekyll</a> and the the new version (3) of the <a href="https://github.com/jekyll/minima">Minima</a> theme, to style my site so that it looked similar to the WordPress team’s <a href="https://wordpress.org/themes/twentythirteen/">Twentythirteen theme</a>. <br />
In this post, I am going to outline how I created a new layout portfolio that could showcase any project of my making/tinkering/engineering/enginerding - be it a woodworking project, a mechanical contraption, or a Python script.
<!--more--></p>
<h3 id="design-considerations">Design considerations</h3>
<p>The portfolio layout started with a basic need. I wanted portfolio to keep the layout I was using on my WordPress site, which uses a paid plugin called <a href="https://wpsofts.com/">GridKit</a>. I was quite happy with the layout this plugin allowed me to have, but I wanted a bit more. <br />
<img src="/assets/img/wp_portfolio_snapshot_201020.png" alt="My WordPress portfolio using GridKit" class="post-block-image" /> <em class="post-image-desc">My WordPress portfolio using GridKit</em> <br />
First, I wanted to be able to display code snippets and projects’ readme’s. Second, I wanted to have a better understanding of how things were built. Third, though the plugin has got a free version, I found that it was not enough for what I wanted to display, and it was costing me money. <br />
So, I set out to find how I could display a grid of pictures representing my projects, with a description scrolling from the top of each grid box describing the specific project on <code class="language-plaintext highlighter-rouge">:hover</code>, then on a click to open a modal displaying the project’s content - be it a <code class="language-plaintext highlighter-rouge">README.md</code>, a code snippet, or pictures. <br />
<img src="/assets/img/wp_portfolio_modal_snapshot_201020.png" alt="GridKit portfolio modal" class="post-block-image" /> <em class="post-image-desc">GridKit portfolio modal</em></p>
<h3 id="finding-solutions">Finding solutions</h3>
<h4 id="using-jekylls-structure-and-yaml-front-matter">Using Jekyll’s structure and YAML front matter</h4>
<p>I started out with adding a <a href="https://jekyllrb.com/docs/collections/">collection</a> to my <code class="language-plaintext highlighter-rouge">_config.yml</code>, then adding a new layout, <code class="language-plaintext highlighter-rouge">_layouts/portfolio.html</code>. A new page, <code class="language-plaintext highlighter-rouge">portfolio.md</code> was also added, as well as some markdown files in the <code class="language-plaintext highlighter-rouge">_portfolio/</code> folder, describing the project and adding some content for the new layout to parse. The example below is of one the projects, and displays nicely a lot of the capabilities. It has a title, and some cover picture, and a text displaying the project in a few words. It also has tags, so the layout can group several projects sharing the same tag for the person viewing the portfolio to filter by, and a link to the project’s web page (in this case a github repository for the project). The last part of the yaml front matter is an array containing pictures, code, or markdown, to be rendered in the specific project’s modal.</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">---</span>
<span class="na">cover</span><span class="pi">:</span> <span class="s">ble_slider_web_app_2.png</span>
<span class="na">title</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Web-BLE</span><span class="nv"> </span><span class="s">control</span><span class="nv"> </span><span class="s">app</span><span class="nv"> </span><span class="s">+</span><span class="nv"> </span><span class="s">esp32</span><span class="nv"> </span><span class="s">sketches"</span>
<span class="na">tags</span><span class="pi">:</span> <span class="s">iot control</span>
<span class="na">link</span><span class="pi">:</span> <span class="s">https://github.com/UriShX/ESP32_fader</span>
<span class="na">modal</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">md</span><span class="pi">:</span> <span class="s">https://raw.githubusercontent.com/UriShX/ESP32_fader/master/readme.md</span>
<span class="pi">-</span> <span class="na">img</span><span class="pi">:</span> <span class="s">ble_slider_web_app_2.png</span>
<span class="pi">-</span> <span class="na">img</span><span class="pi">:</span> <span class="s">ble_slider_web_app_1.png</span>
<span class="nn">---</span>
<span class="s">A web-ble responsive web app to demonstrate control over BLE of ESP32 projects.</span>
<span class="s">Includes links to github repo of web app, and to a couple of ESP32 sketches for testing.</span>
</code></pre></div></div>
<p>However, when displaying code, my layout needs a bit more information, so an object specifying the language to be displayed and whether line numbers should be rendered is also required. That is since code in one language can sometimes contain bits in some other language, such as a <code class="language-plaintext highlighter-rouge">HTML</code> page contained inside an embedded <code class="language-plaintext highlighter-rouge">C</code> code, <a href="https://github.com/UriShX/portfolio/blob/master/Roboclaw_control_over_ESP32_with_AP_for_control/roboclaw_esp32_w_AP_and_config.ino">for example</a>.</p>
<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">modal</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">code</span><span class="pi">:</span>
<span class="na">link</span><span class="pi">:</span> <span class="s">https://raw.githubusercontent.com/UriShX/portfolio/master/Roboclaw_control_over_ESP32_with_AP_for_control/roboclaw_esp32_w_AP_and_config.ino</span>
<span class="na">lang</span><span class="pi">:</span> <span class="s">cpp</span>
<span class="c1"># - linenos: linenos</span>
</code></pre></div></div>
<h4 id="adding-modals">Adding modals</h4>
<p>The two main characteristics of the <a href="https://wpsofts.com/">GridKit</a> portfolio I had on my WP site were its general layout, and that each project was displayed in its own modal window. Laying out the projects in a grid and animating their display is done through Jekyll’s own structure, and detailed below. But I had to come up with a way to display each project’s content in it’s own modal, which is not part of the theme I was using, <a href="https://github.com/jekyll/minima">Minima</a>.</p>
<p>I started out looking for a ready-made solution, and my search parameters were threefold:</p>
<ol>
<li>I wanted a modal library that will use the least amount of JavaScript - so, mostly implemented in CSS.</li>
<li>I wanted the modal library to be either pure CSS, or - preferably - in <a href="https://sass-lang.com/">Sass</a>.</li>
<li>I also did not want to include a whole CSS framework, mostly because I wanted to use the existing layout of the Minima theme.</li>
<li>And, naturally, I would prefer a library that will have ample examples, and is developed enough to use in production.</li>
</ol>
<p>Searching Google got me to <a href="https://drublic.github.io/css-modal/">CSS-Modal</a>, which checked all the boxes. All I had to do was figure out how to use it in my new layout. I copied it’s <code class="language-plaintext highlighter-rouge">.scss</code> files into Jekyll’s <code class="language-plaintext highlighter-rouge">_sass/</code> folder (under a fitting subfolder, <code class="language-plaintext highlighter-rouge">_sass/drublic-css-modal/</code>), and it’s few JS utilities into Jekyll’s <code class="language-plaintext highlighter-rouge">assets</code> folder, under <code class="language-plaintext highlighter-rouge">assets/script</code>.</p>
<h4 id="importing-and-displaying-remote-code">Importing and displaying remote code</h4>
<p>As I wrote above, I wanted to add the ability to display projects that consist of code or <a href="https://www.markdownguide.org/">markdown</a> files. Simply put, not all of my projects have a visual element to them, but I still wanted to display them in a way that will be similar to the more visually- centric projects. <br />
I wanted to use Jekyll’s statically built structure to my advantage, and import all the code snippets and MD files at build time. That way, the site contains all of it’s components, and can even be displayed off-line quite easily, which can be important when displaying my work in places with bad internet connection. In those cases, the site can be displayed directly from my phone’s cache.</p>
<p>For achieving that, I had to use a couple of Jekyll plugins. <br />
The first one because Jekyll’s <a href="https://jekyllrb.com/docs/includes/"><code class="language-plaintext highlighter-rouge">include</code> and <code class="language-plaintext highlighter-rouge">include_relative</code></a> tags were designed to include HTML files in layouts, and not content - thus they allow only for including files from the current Jekyll project. <br />
The second plugin was needed to allow passing parameters to Jekyll’s <a href="https://jekyllrb.com/docs/liquid/tags/#code-snippet-highlighting"><code class="language-plaintext highlighter-rouge">highlight</code></a> tag, in a similar way to what’s possible with the <code class="language-plaintext highlighter-rouge">include</code> tags. Jekyll uses <a href="http://rouge.jneen.net/">Rouge</a> to format code snippets, and it does so quite intelligently, figuring out the code’s language from its’ syntax. Since some of my projects mix languages (as I wrote above, HTML code in an embedded C program, for eg.), or use non standard file extensions (such as <code class="language-plaintext highlighter-rouge">.pde</code> for <a href="https://processing.org/">Processing</a>, or <code class="language-plaintext highlighter-rouge">.ino</code> for <a href="https://www.arduino.cc/">Arduino</a>), I wanted to have the same control over the rendered language as for the source itself.</p>
<p>I searched <a href="https://rubygems.org/">RubyGems</a> for Jekyll plugins that could quite fill my requirements, but couldn’t find any ‘out of the box’ solutions, so I set about adapting and writing some code.</p>
<p>For including files, I found <a href="https://rubygems.org/gems/jekyll-remote-include">jekyll-remote-include</a>. This plugin can include a remote file in a similar way to Jekyll’s <code class="language-plaintext highlighter-rouge">include</code> tag already, but I wanted to add to it a capability. I wanted to be able to use Liquid to render my portfolio from a template, so each included remote source should be passed on as a variable. To achieve that, I forked the plugin’s repository, and added a begin/rescue block (which is Ruby’s way of calling a <em>try/catch</em> block). In this block the plugin simply tries to parse the URI from the given parameter, and if it fails, it tries to parse the URI as a <code class="language-plaintext highlighter-rouge">context</code> variable. That seems to work in a decent enough manner, so <a href="https://github.com/netrics/jekyll-remote-include/pull/2">I opened a PR</a> with my addition, which I hope will be merged to the plugin repo some day. In the meantime, my revised plugin can be found <a href="https://github.com/UriShX/jekyll-remote-include/tree/context-variables">here</a>.</p>
<p>As for passing variables to the <code class="language-plaintext highlighter-rouge">highlight</code> tag, I <strong>really</strong> couldn’t find what I was looking for as a plugin, so I set about writing my own, or rather re-writing Jekyll’s <a href="https://github.com/jekyll/jekyll/blob/master/lib/jekyll/tags/highlight.rb">mainline implementation</a>. My first implementation was pretty rough. I basically tried to use the same method as I did with the remote-include plugin, using a <em>begin/rescue</em> block, but that only got me part way to my goal. What it actually do is fail gracefully, by checking if the code is a context variable, and trying to parse it. If it cannot do that, the code simply continues as the mainline tag does. Once I figured that didn’t quite do what I was expecting it to do, I set about implementing a better model for selecting the route to go by. <br />
Implementing a better model for detecting the passed parameters required a change to the <a href="https://ruby-doc.org/core-2.7.1/Regexp.html">regular expression</a> in the tag’s code. I wanted the syntax to be as close to Jekyll’s implementation of Liquid, so my implementation is looking for curly braces to filter passed variables, and if it does not find any - it considers the parameter as a constant. That means backwards compatibility is maintained with Jekyll’s <code class="language-plaintext highlighter-rouge">highlight</code> tag, while allowing for variables to be passed. I opened a <a href="https://github.com/jekyll/jekyll/issues/8290">feature request</a> for that in Jekyll’s repository, but I haven’t opened a PR yet. My implementation can be found on rubygems.org as <a href="https://rubygems.org/gems/jekyll-highlight-param">jekyll-highlight-param</a>.</p>
<p>Adding the plugins into a Jekyll site is quite simple, by directing the Gemfile to pull straight from Github:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># frozen_string_literal: true</span>
<span class="n">source</span> <span class="s2">"https://rubygems.org"</span>
<span class="n">gem</span> <span class="s2">"jekyll"</span><span class="p">,</span> <span class="s2">"~> 4.1"</span>
<span class="n">gem</span> <span class="s2">"minima"</span><span class="p">,</span> <span class="ss">:github</span> <span class="o">=></span> <span class="s1">'jekyll/minima'</span> <span class="c1">#:git => "[email protected]:jekyll/minima.git" # github v.3.0, latest build is 2.5.1</span>
<span class="n">group</span> <span class="ss">:jekyll_plugins</span> <span class="k">do</span>
<span class="n">gem</span> <span class="s2">"jekyll-remote-include"</span><span class="p">,</span> <span class="ss">:github</span> <span class="o">=></span> <span class="s1">'UriShX/jekyll-remote-include'</span><span class="p">,</span> <span class="ss">:branch</span> <span class="o">=></span> <span class="s1">'context-variables'</span> <span class="c1">#:git => "[email protected]:UriShX/jekyll-remote-include.git", :branch => "context-variables"</span>
<span class="n">gem</span> <span class="s2">"jekyll-highlight-param"</span><span class="p">,</span> <span class="ss">:github</span> <span class="o">=></span> <span class="s1">'UriShX/jekyll-highlight-param'</span> <span class="c1">#:git => "[email protected]:UriShX/jekyll-highlight-param.git"</span>
<span class="k">end</span>
</code></pre></div></div>
<p>Note, that to direct a plugin to a specific branch (like to my revised <code class="language-plaintext highlighter-rouge">jekyll-remote-include</code> plugin, which accepts variables, and is not yet merged to the main plugin repo), it is possible to pass <code class="language-plaintext highlighter-rouge">:branch =></code> to the gem.</p>
<h3 id="building-up-the-page">building up the page</h3>
<h4 id="building-the-page-using-liquid">Building the page using Liquid</h4>
<p>Building a portfolio from a <a href="https://jekyllrb.com/docs/collections/">collection</a> and a bunch of markdown was a bit convoluted. I used quite a lot of flow control logic, checking whether the collection was indeed a portfolio, looking through the files in the <code class="language-plaintext highlighter-rouge">_portfolio</code> directory, pulling the content of each (the text describing the project), and creating a modal for each sort of project - code based, a markdown file, a picture gallery, a combination of those, or even none at all. I then arranged the projects (programatically) by order of their place in Jekyll’s <code class="language-plaintext highlighter-rouge">_config.yml</code>, and push the projects that were found in the <code class="language-plaintext highlighter-rouge">_portfolio</code> folder but were not in the <code class="language-plaintext highlighter-rouge">_config.yml</code> to be sorted alphabetically at the end of the portfolio. <br />
Cover images and tags are also pulled from the project’s front matter, and by using some icons from Google’s Material Design <a href="https://material.io/resources/icons/?style=baseline">Icons</a>, (linked to in <a href="https://github.com/UriShX/Jekyll-portfolio/blob/master/_includes/custom-head.html"><code class="language-plaintext highlighter-rouge">_includes/custom-head.html</code></a>), links and modals are linked to each graphical representation of a project.</p>
<p>Most of the modal code for image galleries is based on <a href="https://github.com/drublic/css-modal/tree/master/examples">the examples</a> for the <a href="https://drublic.github.io/css-modal/">CSS modal library</a> I used. With some Liquid <a href="https://shopify.github.io/liquid/tags/control-flow/">control logic</a>, and with the aid of the two plugins I wrote of above, creating a nice modal view of markdown and code files was not that difficult to implement also.</p>
<p>The whole portfolio layout can be found in my <code class="language-plaintext highlighter-rouge">jekyll-portfolio</code> repository, under <a href="https://github.com/UriShX/Jekyll-portfolio/blob/master/_layouts/portfolio.html"><code class="language-plaintext highlighter-rouge">_layouts/portfolio.html</code></a>.</p>
<h4 id="layout-using-sass">Layout using Sass</h4>
<p>Since I’m using <a href="https://github.com/jekyll/minima">Minima</a> v.3, it was quite easy to link the <a href="https://drublic.github.io/css-modal/">CSS modal</a> library. All that’s needs to be done was linking the modal library’s <code class="language-plaintext highlighter-rouge">.scss</code> files, which I placed under <code class="language-plaintext highlighter-rouge">_sass/drublic-css-modal</code>, into Minima’s <a href="https://github.com/UriShX/Jekyll-portfolio/blob/master/_sass/minima/custom-styles.scss#L6"><code class="language-plaintext highlighter-rouge">_sass/minima/custom-styles.scss</code></a>, under an <code class="language-plaintext highlighter-rouge">@import</code> tag.</p>
<p>The rest of the portfolio styling can be found on lines <a href="https://github.com/UriShX/Jekyll-portfolio/blob/master/_sass/minima/custom-styles.scss#L226">226</a> onward:</p>
<p>I then used the new <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Grid_Layout">CSS grid layout</a> to style the layout of the <a href="https://github.com/UriShX/Jekyll-portfolio/blob/master/_sass/minima/custom-styles.scss#L266"><code class="language-plaintext highlighter-rouge">.portfolio-wrapper</code></a> class, in a similar way to how <a href="https://wpsofts.com/">GridKit</a> styles portfolios.</p>
<p>Some more styling for each <a href="https://github.com/UriShX/Jekyll-portfolio/blob/master/_sass/minima/custom-styles.scss#L326"><code class="language-plaintext highlighter-rouge">.portfolio-block</code></a> class to get the zoom-in and sliding animations for the text and links, darkening the background when a modal is open, as well as creating a satisfying layout for narrow screens such as phones, and that was it.</p>
<h4 id="filtering-using-js">Filtering using JS</h4>
<p>Finally, I needed a way to filter the projects by some key words. I used Jekyll’s <a href="https://jekyllrb.com/docs/front-matter/">tags</a> to group them using Liquid in <a href="https://github.com/UriShX/Jekyll-portfolio/blob/master/_layouts/portfolio.html#L10">_layouts/portfolio.html</a>, then <a href="https://github.com/UriShX/Jekyll-portfolio/blob/master/_layouts/portfolio.html#L20">display the tags</a> on top of the portfolio blocks.</p>
<p>I then wrote a <a href="https://github.com/UriShX/Jekyll-portfolio/blob/master/assets/script/portfolio-tag-filter.js">short JS script</a> for querying the list of <code class="language-plaintext highlighter-rouge">a href</code> links created by the Jekyll layout, generating an array of links to projects, and by adding an event listener to the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/hashchange_event"><code class="language-plaintext highlighter-rouge">hashchange</code></a> event, hiding or revealing the projects with the appropriate tags.</p>
<p>The script uses a mix of ES5 and ES6, though I believe it is quite easy to read and understand. I made a conscious decision to not provide support for older browsers, so using modern JS is easier for me.</p>
<h3 id="conclusions">Conclusions</h3>
<p>This has been a really long post, so I think I’ll end here. The whole layout can be found on the repository I opened for it, called <a href="https://github.com/UriShX/Jekyll-portfolio">jekyll-portfolio</a>.</p>
<p>Writing my own implementation for a portfolio layout in Jekyll was quite a learning experience. I still have some ways to go, as I still need to figure out a better way for sizing images better, and perhaps adding a <a href="https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps">PWA</a> capability.</p>
<p>Besides that, the versioning for <a href="https://github.com/UriShX/jekyll-highlight-param">jekyll-highlight-param</a>’s does not follow the <a href="https://semver.org/">semantic versioning</a> scheme, and my additions to <a href="https://github.com/UriShX/jekyll-remote-include/tree/context-variables">jekyll-remote-include</a> need some refining.</p>Uri Shani[email protected]In my last post I detailed how I used Jekyll and the the new version (3) of the Minima theme, to style my site so that it looked similar to the WordPress team’s Twentythirteen theme. In this post, I am going to outline how I created a new layout portfolio that could showcase any project of my making/tinkering/engineering/enginerding - be it a woodworking project, a mechanical contraption, or a Python script.Customizing Jekyll’s Default Theme2020-12-16T00:00:00+00:002020-12-16T00:00:00+00:00https://urishx.github.io/2020/12/16/Customizing-Jekyll's-default-theme<p>Jekyll installs with a nice, clean theme, called <a href="https://github.com/jekyll/minima">Minima</a>. While there are <em>a lot</em> of nice themes out there, I had both a clear view in my head of what I wanted to accomplish, and ample time thanks to CoVid19. So, armed with <a href="https://github.com/jekyll/minima">Minima’s</a> source and <a href="https://jekyllrb.com/docs/themes/">Jekyll’s documentation</a>, I decided to have a go with modifying the theme a bit, and in this post I’ll try to outline the way I went about it.
<!--more-->
I started off with the latest version of the <a href="https://github.com/jekyll/minima">Minima</a>, even though it’s not published as a stable version, and is not what you get ‘out of the box’ when you get when you start a new site with the <code class="language-plaintext highlighter-rouge">jekyll new <PATH></code> command. The main reason for that was that I had more of a desire for understanding how Jekyll themes were built, rather than a ‘getting the job done’ approach. So, I cloned the theme from its Github repository, and started to modify the way things looked and felt, to better fit my desired look and feel. If you’d like to follow along, you would need to change your <code class="language-plaintext highlighter-rouge">Gemfile</code> to point to Minima’s Github repository, like so:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gem "minima", :github => 'jekyll/minima' # github v.3.0, latest build is 2.5.1
</code></pre></div></div>
<p>Set your theme on your <code class="language-plaintext highlighter-rouge">_config.yml</code>, then run <code class="language-plaintext highlighter-rouge">bundle</code> from the command line.</p>
<h3 id="design-goals">Design goals</h3>
<p>I wanted my site to maintain the general look and feel of my existing WordPress blog, which uses the WordPress team’s <a href="https://wordpress.org/themes/twentythirteen/">Twentythirteen theme</a>. <br />
<img src="/assets/img/wp_blog_snapshot_211020.png" alt="My existing WordPress blog" class="post-block-image" /> <em class="post-image-desc">My existing WordPress blog</em> <br />
Starting with the general layout of this theme, I wanted to have a header that will showcase the site’s name, some subtitle with the site’s motto, a logo, and to have all that in front of some colorful banner. I grabbed the banner straight from the WP theme I was using, re-designed my site’s logo, and started working on the layout.</p>
<h2 id="modifying-minima">Modifying Minima</h2>
<p><a href="https://github.com/jekyll/minima">Minima’s</a> latest (unreleased) version, v.3.0, has got some nice features for easy customization. It imports some empty files while rendering both the theme’s <code class="language-plaintext highlighter-rouge">default.html</code> layout as well when compiling <a href="https://sass-lang.com/">Sass</a> (<code class="language-plaintext highlighter-rouge">.scss</code>) files. This complements Jekyll’s [theme building process]](https://jekyllrb.com/docs/themes/) nicely, as it allows the user to only modify what’s needed, without needing to copy and paste whole files. I found out that worked great with styling, but needed to make quite a few changes in the <code class="language-plaintext highlighter-rouge">_includes</code> HTML files, since there was no easy way for changing the page layouts themselves without that. <br />
You can click on the filename in the headings to go to the file the paragraph describes.</p>
<h3 id="modifying-_includes">Modifying <a href="https://github.com/UriShX/Jekyll-portfolio/blob/master/_includes/"><code class="language-plaintext highlighter-rouge">_includes</code></a></h3>
<h4 id="custom-headhtml"><a href="https://github.com/UriShX/Jekyll-portfolio/blob/master/_includes/custom-head.html"><code class="language-plaintext highlighter-rouge">custom-head.html</code></a></h4>
<p>This file is really all you need to change if all you want is a slightly different look and feel, and some basic branding. This file is included in the <code class="language-plaintext highlighter-rouge"><head></code> section in the generated site, so including a favicon, for example, is perfect here. I decided to change the fonts, so I grabbed the cdn link from <a href="https://fonts.google.com/">Google fonts</a> for two fonts I like (and used on my old site) - <a href="https://fonts.google.com/specimen/Alef?preview.text=&preview.text_type=custom&query=alef">Alef</a> and <a href="https://fonts.google.com/specimen/Tinos?preview.text=&preview.text_type=custom&query=tinos#standard-styles">Tinos</a>. These two fonts were originally selected since they both support Hebrew as well as English (and other European languages), though I don’t intend to implement Hebrew on this site in the near future. I just sorta got used to them in my old site, and decided to keep them. <br />
Those fonts will simply override Minima’s default font, by reassigning some variables in <code class="language-plaintext highlighter-rouge">_sass/minma/custom-variables.scss</code></p>
<h4 id="headerhtml"><a href="https://github.com/UriShX/Jekyll-portfolio/blob/master/_includes/header.html"><code class="language-plaintext highlighter-rouge">header.html</code></a></h4>
<p>Starting off from the top of the page, the branding for my site consists of a banner, which features my name, a subtitle, the site’s logo, and a navigation bar. Those are all inserted through <a href="https://jekyllrb.com/docs/liquid/">Liquid</a> tags, pulled from either Jekyll’s <code class="language-plaintext highlighter-rouge">_config.yml</code>, or from the Jekyll’s structure. <br />
The banner itself is a css background, using Liquid to insert a relative URL from the site’s assets folder:</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><header</span> <span class="na">class=</span><span class="s">"site-header"</span> <span class="na">style=</span><span class="s">"background-image: url('{{ site.banner | escape | prepend: "</span><span class="err">/</span><span class="na">assets</span><span class="err">/</span><span class="na">img</span><span class="err">/"</span> <span class="err">|</span> <span class="na">relative_url</span> <span class="err">}}');"</span><span class="nt">></span>
</code></pre></div></div>
<p>Then, inside a <code class="language-plaintext highlighter-rouge">banner</code> HTML class, the template checks if there’s a <code class="language-plaintext highlighter-rouge">site.logo</code> image in <code class="language-plaintext highlighter-rouge">_config.yml</code>, which (if it is configured) is inserted as a relative URL to the site’s assets folder again.</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><div</span> <span class="na">class=</span><span class="s">"img-container"</span><span class="nt">></span>
{% if site.logo %}
<span class="nt"><img</span> <span class="na">alt=</span><span class="s">"logo"</span> <span class="na">src=</span><span class="s">"{{ site.logo | escape | prepend: "</span><span class="err">/</span><span class="na">assets</span><span class="err">/</span><span class="na">img</span><span class="err">/"</span> <span class="err">|</span> <span class="na">relative_url</span> <span class="err">}}"</span><span class="nt">/></span>
{% endif %}
<span class="nt"></div></span>
</code></pre></div></div>
<p>After that, the site’s title and subtitle are also pulled from <code class="language-plaintext highlighter-rouge">_config.yml</code>:</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><h1</span> <span class="na">class=</span><span class="s">"site-title"</span><span class="nt">></span>
<span class="nt"><a</span> <span class="na">rel=</span><span class="s">"author"</span> <span class="na">href=</span><span class="s">"{{ '/' | relative_url }}"</span><span class="nt">></span>{{ site.title | escape }}<span class="nt"></a></span>
<span class="nt"></h1></span>
<span class="nt"><h2></span>{{ site.subtitle | escape}}<span class="nt"></h2></span>
</code></pre></div></div>
<p>The navigation bar remains unchanged in it’s structure from Minima’s.</p>
<h4 id="footerhtml"><a href="https://github.com/UriShX/Jekyll-portfolio/blob/master/_includes/footer.html"><code class="language-plaintext highlighter-rouge">footer.html</code></a></h4>
<p>I added a copyright symbol (©) and the year to my site’s footer (the year was also added to <code class="language-plaintext highlighter-rouge">_config.yml</code>):</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><li</span> <span class="na">class=</span><span class="s">"p-name"</span><span class="nt">></span><span class="ni">&#169;</span> {{ site.author.name | escape }} {{ site.author.copyright-year | escape }}<span class="nt"></li></span>
</code></pre></div></div>
<h4 id="socialhtml"><a href="https://github.com/UriShX/Jekyll-portfolio/blob/master/_includes/social.html"><code class="language-plaintext highlighter-rouge">social.html</code></a></h4>
<p>I changed the included <code class="language-plaintext highlighter-rouge">assets/minima-social-icons.svg</code> to include an icon for <a href="https://hackaday.io/">Hackaday.io</a>. Technically, the icon I included is for <a href="https://hackaday.com/">Hackaday.com</a>, but I think it’s the more easily recognizable of the two. <br />
To add it into my social icons, I simply added a single line to the list of social icons, which displays the <em>Hackaday</em> icon with the appropriate link, if it is configured in <code class="language-plaintext highlighter-rouge">_config.yml</code>:</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{%- if social.hackaday -%}<span class="nt"><li><a</span> <span class="na">rel=</span><span class="s">"me"</span> <span class="na">href=</span><span class="s">"https://hackaday.io/{{ social.hackaday | cgi_escape | escape }}"</span> <span class="na">title=</span><span class="s">"{{ social.hackaday | escape }}"</span><span class="nt">><svg</span> <span class="na">class=</span><span class="s">"svg-icon grey"</span><span class="nt">><use</span> <span class="na">xlink:href=</span><span class="s">"{{ '/assets/minima-social-icons.svg#hackaday' | relative_url }}"</span><span class="nt">></use></svg></a></li></span>{%- endif -%}
</code></pre></div></div>
<h5 id="adding-the-icon-for-hackadayio">Adding the icon for <a href="https://hackaday.io/">Hackaday.io</a></h5>
<p>I used ntembed’s <a href="https://hackaday.io/project/165314-hackaday-social-media-icon">Hackaday social media icon</a>, and simply added the svg to Minima’s <code class="language-plaintext highlighter-rouge">assets/minima-social-icons.svg</code> using <a href="https://inkscape.org/">Inkscape</a>. As I wrote above, the icon is actually for <a href="https://hackaday.com/">Hackaday.com</a>, but that was good enough for my purposes. <br />
I then added <code class="language-plaintext highlighter-rouge">hackaday: UriSh</code> (which is my username on <a href="https://hackaday.io/urish">Hackaday.io</a>) to Minima v3’s <code class="language-plaintext highlighter-rouge">social_links:</code> category, under <code class="language-plaintext highlighter-rouge">minima:</code>, in Jekyll’s <code class="language-plaintext highlighter-rouge">_config.yml</code>. You can have a look at a sample <code class="language-plaintext highlighter-rouge">_config.yml</code> file <a href="https://github.com/UriShX/Jekyll-portfolio/blob/master/_config.yml">here</a>.</p>
<h3 id="modifying-_sassminima">Modifying <a href="https://github.com/UriShX/Jekyll-portfolio/tree/master/_sass/minima"><code class="language-plaintext highlighter-rouge">_sass/minima</code></a></h3>
<p><a href="https://github.com/jekyll/minima">Minima’s</a> latest version (v.3.0) provides a few skins, to change it’s color scheme. It also, as I wrote above, gives the user hooks to modify the theme further. This is achieved by importing a couple of empty <code class="language-plaintext highlighter-rouge">scss</code> files, one right after some initial variable assignments, then another after most of the site’s styling is defined. <br />
I set up the Minima’s skin to be <code class="language-plaintext highlighter-rouge">skin:</code><i style="background-color: black; color: salmon; padding: 0 0.25rem; border-radius: 0.2rem;">solarized</i>, in <code class="language-plaintext highlighter-rouge">_config.yml</code> under <code class="language-plaintext highlighter-rouge">minima:</code>.</p>
<h4 id="custom-variablesscss"><a href="https://github.com/UriShX/Jekyll-portfolio/blob/master/_sass/minima/custom-styles.scss"><code class="language-plaintext highlighter-rouge">custom-variables.scss</code></a></h4>
<p>In this file one can override Minima’s defaults by reassigning certain variables. It is also possible to add custom variables, which I needed for controlling the position and styling of the navigation bar’s look and feel. <br />
I first overrode the default font for the site and adding a second font for headlines, using the fonts I linked to in <code class="language-plaintext highlighter-rouge">_includes/custom_head.html</code>, <a href="https://fonts.google.com/specimen/Alef?preview.text=&preview.text_type=custom&query=alef">Alef</a> and <a href="https://fonts.google.com/specimen/Tinos?preview.text=&preview.text_type=custom&query=tinos#standard-styles">Tinos</a>. <br />
I added a couple of navbar definitions, for background color and hover effects, linking them to Minima’s skin definitions, and I also added a couple more breaks for size, since I found out my subtitle was too long to handle the banner / navigation bar properly.</p>
<h4 id="custom-stylesscss"><a href="https://github.com/UriShX/Jekyll-portfolio/blob/master/_sass/minima/custom-styles.scss"><code class="language-plaintext highlighter-rouge">custom-styles.scss</code></a></h4>
<p>This file contains the styling for the site’s branding, as well as the styling for other layouts I will cover in future posts. <br />
First, I wanted to style code on my site in a way that will contrast the styling of other text in my site, to make it a bit more discernible. I did that by importing a stylesheet I got from <a href="https://github.com/jwarby/jekyll-pygments-themes">the repository</a> recommended in <a href="https://jekyllrb.com/docs/liquid/tags/#stylesheets-for-syntax-highlighting">Jekyll’s docs</a>.</p>
<div class="language-scss highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">@import</span> <span class="sx">url('zenburn.css')</span><span class="p">;</span>
</code></pre></div></div>
<p>Next, I defined the headings’ font to use <a href="https://fonts.google.com/specimen/Tinos?preview.text=&preview.text_type=custom&query=tinos#standard-styles">Tinos</a>, and since I decided to override Minima’s skin code highlighter defaults, I also redefined the highlighted area to fit.
After that, besides importing <a href="https://github.com/UriShX/Jekyll-portfolio/blob/master/_sass/minima/_mixins.scss">some useful mixins</a>, comes the actual styling for the site’s header, banner, logo, and navbar. <br />
Since this is quite a long list of changes from Minima’s header, I will only list a few, and if anyone’s interested in investigating further, the file is hosted <a href="https://github.com/UriShX/Jekyll-portfolio/blob/master/_sass/minima/custom-styles.scss">here</a>, or leave me a comment down below. <br />
I set the area for the banner (the background image defined in <a href="https://github.com/UriShX/Jekyll-portfolio/blob/master/_includes/header.html"><code class="language-plaintext highlighter-rouge">header.html</code></a>) under the class <code class="language-plaintext highlighter-rouge">.site-header</code>:</p>
<div class="language-scss highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nl">background-color</span><span class="p">:</span> <span class="nv">$brand-color-light</span><span class="p">;</span>
<span class="nl">background-size</span><span class="p">:</span> <span class="n">cover</span><span class="p">;</span>
<span class="nl">background-position</span><span class="p">:</span> <span class="nb">center</span><span class="p">;</span>
<span class="nl">min-height</span><span class="p">:</span> <span class="m">14</span><span class="mi">.1vw</span><span class="p">;</span>
</code></pre></div></div>
<p>The <code class="language-plaintext highlighter-rouge">min-height</code> attribute is defined to fit the WxH ratio of the banner image I used. <br />
Next, the <code class="language-plaintext highlighter-rouge">.banner</code> class defines the styling for the logo, title, and subtitle. <br />
The <code class="language-plaintext highlighter-rouge">.top-nav</code> class then defines the navbar, including background, position, and hover behavior. <br />
The class <code class="language-plaintext highlighter-rouge">.site-title</code> overrides Minima’s default by disabling <code class="language-plaintext highlighter-rouge">float</code> and <code class="language-plaintext highlighter-rouge">hover</code> decoration. <br />
The <code class="language-plaintext highlighter-rouge">.site-nav</code> class and its subclasses control the behavior of the navbar further, including it’s alignment according to window size, and also highlights and disables the link for the current page. \</p>
<p>One thing I found important while styling my site was setting the correct breaks for window width. I added a couple more <code class="language-plaintext highlighter-rouge">@media</code> queries under <code class="language-plaintext highlighter-rouge">.site-title</code>:</p>
<div class="language-scss highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">@media</span> <span class="n">screen</span> <span class="nf">and</span> <span class="p">(</span><span class="n">min-width</span><span class="o">:</span> <span class="nv">$on-med-wide</span><span class="p">)</span> <span class="p">{</span>
<span class="nl">top</span><span class="p">:</span> <span class="nf">unquote</span><span class="p">(</span><span class="s1">'calc(max(14.1vw, 165px) - 1.6rem)'</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">@media</span> <span class="n">screen</span> <span class="nf">and</span> <span class="p">(</span><span class="n">min-width</span><span class="o">:</span> <span class="nv">$on-medium</span><span class="p">)</span> <span class="nf">and</span> <span class="p">(</span><span class="n">max-width</span><span class="o">:</span> <span class="nv">$on-med-wide</span><span class="p">)</span> <span class="p">{</span>
<span class="nl">top</span><span class="p">:</span> <span class="nf">unquote</span><span class="p">(</span><span class="s1">'calc(max(14.1vw, 200.25px) - 1.6rem)'</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>
<p>to allow for better positioning of the navbar, while letting the banner grow in height to fit the long subtitle I chose.</p>
<h3 id="conclusion">Conclusion</h3>
<p>Styling <a href="https://github.com/jekyll/minima">Minima</a> v3 to follow an existing WP theme is quite straight forward mostly, though I did get into some ruts. <br />
Mostly, it’s a matter of moving overriding existing definitions, and figuring out what class an element is in, in order to change it’s behavior in a couple of <code class="language-plaintext highlighter-rouge">.scss</code> files Minima imports by default. <br />
Most of this work has been done in preparation for a more elaborate change I needed to implement, which is adding a portfolio page to this site. One of my next posts will be on that subject, as it was the driving force for moving from WP to Jekyll, even though it might not seem like an obvious decision.</p>
<p>You can find a ready template that includes the above changes I made to Minima in <a href="https://github.com/UriShX/Jekyll-portfolio">Jekyll-portfolio</a>, which also includes the portfolio page and static commenting.</p>
<p>If anything above is not clear, or if you find you have better ways to do what I outlined here, or if you find the template repo useful, please leave a comment.</p>Uri Shani[email protected]Jekyll installs with a nice, clean theme, called Minima. While there are a lot of nice themes out there, I had both a clear view in my head of what I wanted to accomplish, and ample time thanks to CoVid19. So, armed with Minima’s source and Jekyll’s documentation, I decided to have a go with modifying the theme a bit, and in this post I’ll try to outline the way I went about it.Adding Staticman To Jekylls Minima Theme2020-10-21T00:00:00+00:002020-10-21T00:00:00+00:00https://urishx.github.io/2020/10/21/Adding-Staticman-to-Jekylls-Minima-theme<p>After modifying the theme to fit my aesthetics, then adding a nice portfolio layout (which required writing and modifying a couple of plgugins), it was time to add a commenting system to my blog. Now, it’s not that I write a lot of blog posts, but for some projects I want to write some sort of how-to, or some sort of a thought process documentation - and for those, I really do wish for people to interact with me. Now, <a href="https://github.com/jekyll/minima">Minima</a> already comes with a layout ready for adding <a href="https://disqus.com/">Disqus</a> comments. But I don’t really like relying on the free tier of a paid service, so I did a quick search and found <a href="https://staticman.net/">Staticman</a>, which is a self hosted solution.
<!--more-->
I think Staticman works in quite a clever way, using Jekyll’s own structure to save the comments as <a href="https://jekyllrb.com/docs/datafiles/">data files</a>. Staticman’s back end is a self hosted endpoint which receives a <code class="language-plaintext highlighter-rouge">POST</code> request of the comment from a form displayed on each post. It then opens a <em>Pull Request</em> on the repository containing the blog with a new data file containing the comment - which in turn will be rendered with the related post when the site re-renders. <br />
Staticman’s <a href="https://github.com/eduardoboucas/staticman">Github repository</a> lists a couple of useful links for setting those up, which I followed. The first is Travis Downs’ <a href="https://travisdowns.github.io/blog/2020/02/05/now-with-comments.html#invite-and-accept-bot-to-blog-repo">tutorial</a>, which is great, and the second is Michael Rose’s <a href="https://mademistakes.com/articles/improving-jekyll-static-comments/">blog post</a> documenting how he implemented Staticman in his own theme. I followed these two, adjusting as I went along to fit with my blog’s layout. <br />
Required changes:</p>
<ul>
<li>Include files <code class="language-plaintext highlighter-rouge">comments.html</code>, <code class="language-plaintext highlighter-rouge">comment.html</code>, and <code class="language-plaintext highlighter-rouge">comment_form.html</code>. If my memory serves me right, I copied these from Michael Rose’s blog post (<em>MadeMistakes</em>).</li>
<li>Rewrite JS to not require JQuery. That was done since that’s the only part in my site that uses JQuery, so that’s an unnecessary dependency. Since I don’t want this blog post to be entirely about that, I’ll just mention that I used the <code class="language-plaintext highlighter-rouge">fetch</code> API to send the comment to the Staticman endpoint, and you can find the code <a href="https://raw.githubusercontent.com/UriShX/microblog/refs/heads/master/assets/script/staticman-comments.js?token=GHSAT0AAAAAACXKXELF6N3ODUA2MPPGXL7IZYEFQPA">here</a>.</li>
<li>Substitute colors in <code class="language-plaintext highlighter-rouge">comment-styles.scss</code> to work with Minima’s skins.</li>
<li>Add comments tag to <code class="language-plaintext highlighter-rouge">_layouts/post.html</code> as in adding the following to the bottom of the file:</li>
</ul>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code> {%- if page.comments == true -%}
{%- include comments.html -%}
{%- endif -%}
</code></pre></div></div>
<ul>
<li>Add the <code class="language-plaintext highlighter-rouge">comments</code> tag to the front matter of the posts I want to have comments on.
<del>I also found that using the Heroku deploy button does not work correctly with my browser, so I followed issue <a href="https://github.com/eduardoboucas/staticman/issues/394">#394</a> on the Staticman repo.</del> I used Heroku for the first deployment, but since the free tier is long gone, I did not use it for a good while. Since I recently got a VPS, I decided to deploy Staticman on it. I’ll document that process in the next section.</li>
</ul>
<h3 id="deploying-staticman-on-a-vps">Deploying Staticman on a VPS</h3>
<p>I looked online for resources on how to deploy Staticman on a VPS, using Dokku, and found <a href="https://shoreviewanalytics.github.io/Staticman-API-Dokku-Deployment/">this</a> blog post. Reading through it, I decided I didn’t like the idea of setting up Node.JS and NPM, so I re-read the post and found there’s a link to <a href="https://www.flyinggrizzly.net/2017/12/setting-up-staticman-server/">another post</a>, on another blog, that used the Docker image which Staticman ships with. So, I decided to document my own experience, to save myself the future headache of when I try to re-deploy it (which I know will happen sometime in the future, when I change VPS providers or something). <br />
Here’s what I did:</p>
<ul>
<li>Created a new app on my Dokku instance.
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dokku apps:create staticman
</code></pre></div> </div>
</li>
<li>Set up a URL for the app.
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dokku domains:set staticman staticman-subdomain.domain.com
</code></pre></div> </div>
</li>
<li>Set up letsencrypt for the app.
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dokku letsencrypt:set staticman email [email protected]
dokku letsencrypt:enable staticman
</code></pre></div> </div>
</li>
<li>Create a GitHub token, from my existing bot account (I used the same account for the Heroku deployment).
<ul>
<li>Go to <code class="language-plaintext highlighter-rouge">Settings</code> -> <code class="language-plaintext highlighter-rouge">Developer settings</code> -> <code class="language-plaintext highlighter-rouge">Personal access tokens</code> -> <code class="language-plaintext highlighter-rouge">Generate new token</code>.</li>
<li>Select the ‘Tokens (classic)’ option.</li>
<li>Give the token a name, and select the <code class="language-plaintext highlighter-rouge">repo</code> scope.</li>
<li>Copy the token.</li>
</ul>
</li>
<li>Create a RSA key pair for the app.
<ul>
<li>It’s important to use the <code class="language-plaintext highlighter-rouge">-m PEM</code> flag, since the default is <code class="language-plaintext highlighter-rouge">OPENSSH</code>.
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ssh-keygen <span class="nt">-m</span> PEM <span class="nt">-t</span> rsa <span class="nt">-b</span> 4096 <span class="nt">-C</span> <span class="s2">"[email protected]"</span> <span class="nt">-f</span> ~/.ssh/staticman_rsa.pem
<span class="c"># copy the public key to the clipboard (pbcopy works on MacOS)</span>
<span class="nb">cat</span> ~/.ssh/staticman_rsa.pem.pub | pbcopy
</code></pre></div> </div>
</li>
</ul>
</li>
<li>Add the public key to the GitHub bot account.
<ul>
<li>Go to <code class="language-plaintext highlighter-rouge">Settings</code> -> <code class="language-plaintext highlighter-rouge">SSH and GPG keys</code> -> <code class="language-plaintext highlighter-rouge">New SSH key</code>.</li>
<li>Paste the public key.</li>
<li>Give the key a name.</li>
<li>Click <code class="language-plaintext highlighter-rouge">Add SSH key</code>.</li>
</ul>
</li>
<li>Set up the required environment variables.
<ul>
<li>I <code class="language-plaintext highlighter-rouge">scp</code>‘d the private key to the Dokku instance from my local machine (<code class="language-plaintext highlighter-rouge">scp ~/.ssh/staticman_rsa.pem user@ip:~</code>) and then added it to the app.
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dokku config:set staticman <span class="nv">RSA_PRIVATE_KEY</span><span class="o">=</span><span class="s2">"</span><span class="si">$(</span><span class="nb">cat </span>staticman_rsa.pem<span class="si">)</span><span class="s2">"</span>
dokku config:set staticman <span class="nv">GITHUB_TOKEN</span><span class="o">=</span>your_github_token
</code></pre></div> </div>
</li>
</ul>
</li>
<li>Deploy the app (from my local machine).
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone https://github.com/eduardoboucas/staticman.git
<span class="nb">cd </span>staticman
git remote add dokku dokku@yourserver:staticman
git push dokku master
</code></pre></div> </div>
</li>
<li>Test the app exists.
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl https://staticman-subdomain.domain.com:port
<span class="c"># Should return 'Hello from Staticman version 3.0.0!'</span>
</code></pre></div> </div>
</li>
<li>Invite the bot to the repository.
<ul>
<li>Go to the repository settings.</li>
<li>Go to <code class="language-plaintext highlighter-rouge">Manage access</code>.
<ul>
<li>In my had to delete the bot’s access, since I had previously added it to the repository to work with Heroku.</li>
<li>Click <code class="language-plaintext highlighter-rouge">Delete</code> on the bot’s username.</li>
</ul>
</li>
<li>Click <code class="language-plaintext highlighter-rouge">Invite a collaborator</code>.</li>
<li>Paste the bot’s username.</li>
<li>Click <code class="language-plaintext highlighter-rouge">Add</code>.</li>
</ul>
</li>
<li>Accept the invitation using the Staicman app.
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl https://staticman-subdomain.domain.com/v2/connect/github/your_github_username/your_repository_name
<span class="c"># Should return 'OK'</span>
<span class="c"># If it returns 'Invitation not found' it might be that you've already accepted the invitation manually, or that your GITHUB_TOKEN is not right.</span>
</code></pre></div> </div>
</li>
<li>Add (or modify) the existing <code class="language-plaintext highlighter-rouge">_config.yml</code> file in the Jekyll blog to point to the correct Staticman endpoint.
<ul>
<li>I decided to stick to the <code class="language-plaintext highlighter-rouge">v2</code> endpoint, since I was using it before. From what I understand, the <code class="language-plaintext highlighter-rouge">v3</code> endpoint requires a GitHub app, which I did not want to set up at this point in time.
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">staticman_url</span><span class="pi">:</span> <span class="s">https://staticman-subdomain.domain.com:port/v2/entry/your_github_username/your_repository_name/master/comments</span>
</code></pre></div> </div>
</li>
</ul>
</li>
<li>Test the configuration.
<ul>
<li>Add a comment to a post.</li>
<li>If the comment does form does not seem to work, check the browser’s console for errors.</li>
<li>In my case, I noticed that in the ‘network’ tab, I got a ‘500’ error. I checked for possible causes until I remembered I had to set the ‘reCpatcha’ keys in the Staticman app correctly. Since I didn’t remember how I initially set them, a great help here was this <a href="https://travisdowns.github.io/blog/2020/02/05/now-with-comments.html#invite-and-accept-bot-to-blog-repo">blog post</a>, which I mentioned earlier, and <a href="https://spinningnumbers.org/a/staticman-heroku.html#encryption-overview">another post</a> I stumbled upon when trying to figure things up.</li>
</ul>
</li>
<li>Set up the reCaptcha keys.
<ul>
<li>Go to the <a href="https://www.google.com/recaptcha/admin/create">reCaptcha admin console</a>.</li>
<li>Register a new site.</li>
<li>Copy the <code class="language-plaintext highlighter-rouge">site key</code> to both the <code class="language-plaintext highlighter-rouge">_config.yml</code> file and the <code class="language-plaintext highlighter-rouge">staticman.yml</code> in the Jekyll blog.</li>
<li>Copy the <code class="language-plaintext highlighter-rouge">secret key</code>, and go to <code class="language-plaintext highlighter-rouge">https://staticman-subdomain.domain.com/v2/encrypt/[recaptcha-secret-key]</code> in your browser to get the encrypted key.</li>
<li>Copy the encrypted key to both the <code class="language-plaintext highlighter-rouge">_config.yml</code> file and the <code class="language-plaintext highlighter-rouge">staticman.yml</code> in the Jekyll blog.</li>
</ul>
</li>
<li>Save and re-deploy the Jekyll blog.</li>
<li>Test the comment form again.
<ul>
<li>If the comment form does not work, check the browser’s console for errors.</li>
<li>If the comment form does work, check the repository for the new pull request.</li>
</ul>
</li>
</ul>
<p>I suppose that’s it. I really hope this post helps someone in the future, or at least helps me when I need to re-deploy Staticman. I’m really happy with how it turned out, and I’m looking forward to interacting with people on my blog. I’m also looking forward to writing more blog posts, and maybe even finishing some of the drafts I have lying around.</p>Uri Shani[email protected]After modifying the theme to fit my aesthetics, then adding a nice portfolio layout (which required writing and modifying a couple of plgugins), it was time to add a commenting system to my blog. Now, it’s not that I write a lot of blog posts, but for some projects I want to write some sort of how-to, or some sort of a thought process documentation - and for those, I really do wish for people to interact with me. Now, Minima already comes with a layout ready for adding Disqus comments. But I don’t really like relying on the free tier of a paid service, so I did a quick search and found Staticman, which is a self hosted solution.