<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Aleksandar Sabo</title>
    <description>The latest articles on Forem by Aleksandar Sabo (@alxsabo).</description>
    <link>https://forem.com/alxsabo</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1011175%2Fd08870f3-9a90-4f17-a98c-899d0b325456.jpeg</url>
      <title>Forem: Aleksandar Sabo</title>
      <link>https://forem.com/alxsabo</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/alxsabo"/>
    <language>en</language>
    <item>
      <title>Why Fixers Get Praise but Preventers Get Ignored</title>
      <dc:creator>Aleksandar Sabo</dc:creator>
      <pubDate>Tue, 06 Jan 2026 00:00:00 +0000</pubDate>
      <link>https://forem.com/alxsabo/why-fixers-get-praise-but-preventers-get-ignored-3jg</link>
      <guid>https://forem.com/alxsabo/why-fixers-get-praise-but-preventers-get-ignored-3jg</guid>
      <description>&lt;p&gt;We often forget an old truth: &lt;strong&gt;we don't appreciate what we have until we lose it&lt;/strong&gt;. Nowhere is this more evident than in the case of peace - whether in a society, an organization, or even a well-functioning machine. No one wonders why or how as long as things work as expected. But the moment something breaks, suddenly everyone wants to know who is responsible and what went wrong.&lt;/p&gt;

&lt;p&gt;This says a lot about how we think and operate as a society - we give more credit to those who fix problems than to those who stop them from happening in the first place. The people who quietly keep things from falling apart usually don't get noticed because nothing ever goes wrong on their watch.&lt;/p&gt;

&lt;h2&gt;
  
  
  How a Friend Taught Me the Cost of Prevention
&lt;/h2&gt;

&lt;p&gt;A friend once worked as a technician to maintain a hospital's X-ray equipment. His job was primarily preventative - he regularly inspected and fine-tuned the machines to ensure they ran smoothly. And they did. They ran so well for so long that some of his coworkers began questioning whether he had any real work. "Everything works perfectly - what does he even do here?"&lt;/p&gt;

&lt;p&gt;Frustrated, he changed his approach. He stopped intervening preventatively and began letting the inevitable minor issues occur. As soon as a machine failed - often due to something simple like a blown fuse or a misaligned sensor - he'd step in to "fix" the problem. But this time, everyone noticed. His role became visible. People began thanking him for "resolving" the issue quickly, unaware that the very problems they now praised him for fixing could have been prevented in the first place.&lt;/p&gt;

&lt;p&gt;What he did didn't change - just how he went about it. But suddenly, people saw him differently. That shift says a lot about us: &lt;strong&gt;we're quicker to praise those who clean up messes than those who ensure the mess never happens.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Taleb and the Hidden Cost of Prevention
&lt;/h2&gt;

&lt;p&gt;In &lt;em&gt;The Black Swan&lt;/em&gt;, Nassim Nicholas Taleb discusses this phenomenon in the context of uncertainty and decision-making. He describes a particular form of societal ingratitude - one where we fail to reward those who prevent negative outcomes because those outcomes never become visible.&lt;/p&gt;

&lt;p&gt;One of his key examples involves the idea of securing cockpit doors in airplanes before the 9/11 terrorist attacks. Imagine, Taleb writes, if someone had successfully pushed for reinforced cockpit doors in the 1990s. The tragic events of September 11 might never have occurred. But instead of being hailed as a hero, that person likely would have been dismissed as paranoid or criticized for wasting resources on an "unlikely" threat. Because no one would have seen what was prevented, &lt;strong&gt;the preventive action would have appeared unnecessary&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Taleb calls this "the problem of silent evidence" - we only see what happened, not what &lt;em&gt;didn't&lt;/em&gt; happen. In other words, &lt;strong&gt;prevention erases its proof&lt;/strong&gt;, and with it, the social rewards.&lt;/p&gt;

&lt;h2&gt;
  
  
  Crisis Managers vs Preventative Architects
&lt;/h2&gt;

&lt;p&gt;In almost every domain, from national security to IT systems, we lionize the crisis manager - the firefighter, the troubleshooter, the person who swoops in when everything is on fire. And they do deserve praise. But we often forget the invisible architects - those who built the fire-proofing in the first place or designed the systems to prevent chaos from emerging.&lt;/p&gt;

&lt;p&gt;This skewed reward system has dangerous consequences. It encourages people to &lt;strong&gt;allow problems to surface&lt;/strong&gt; so they can be seen fixing them. In politics, it results in reactive leadership - short-term solutions to long-term problems. In business, it leads to firefighting cultures where the loudest problem-solvers get promoted, even if their negligence or short-sightedness created the issues in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Do We Think This Way?
&lt;/h2&gt;

&lt;p&gt;Human cognition is wired for narrative and cause-effect thinking. We crave &lt;strong&gt;visible change&lt;/strong&gt;, dramatic moments, and heroes we can point to. Preventative action lacks that drama. It's the difference between the story of someone rescuing a child from drowning - and someone who quietly built a fence around the pool years ago.&lt;/p&gt;

&lt;p&gt;Peace, stability, and resilience don't make headlines. Disasters do.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Wisdom of Recognizing Peace
&lt;/h2&gt;

&lt;p&gt;To build better systems and societies, we need to &lt;strong&gt;value the unseen work&lt;/strong&gt;. We must learn to reward the uneventful day, the smooth operation, the long stretch of "nothing happening" - because that is often the result of effort we don't see.&lt;/p&gt;

&lt;p&gt;We can start by rethinking our workplace, school, and government encouragements. We can teach children and teams to solve problems and look for weak points before failure. We can publicly celebrate those who &lt;em&gt;design out&lt;/em&gt; the need for intervention, not just those who clean up the mess.&lt;/p&gt;

&lt;p&gt;Because ultimately, &lt;strong&gt;peace is not the default - it's an achievement. And its value becomes tragically clear only when it's gone.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let's not wait for war to understand the worth of peace.&lt;/p&gt;

</description>
      <category>engineering</category>
      <category>culture</category>
    </item>
    <item>
      <title>Setting Up PHPUnit and GitHub Actions for a PHP Project</title>
      <dc:creator>Aleksandar Sabo</dc:creator>
      <pubDate>Tue, 06 May 2025 17:21:00 +0000</pubDate>
      <link>https://forem.com/alxsabo/setting-up-phpunit-and-github-actions-for-a-php-project-1p52</link>
      <guid>https://forem.com/alxsabo/setting-up-phpunit-and-github-actions-for-a-php-project-1p52</guid>
      <description>&lt;p&gt;This tutorial shows you how to set up a basic PHP project with Composer, write tests using PHPUnit, and automate test execution with GitHub Actions. It’s intended for developers who want a clean starting point for building and testing PHP code.&lt;/p&gt;

&lt;p&gt;You’ll create the project structure, configure Composer, add a simple test case, and define a GitHub Actions workflow to run tests on each push or pull request. By the end, you'll have a fully working setup for local and automated testing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Setting Up a PHP Project
&lt;/h2&gt;

&lt;p&gt;Before you begin, ensure that &lt;a href="https://www.php.net/downloads" rel="noopener noreferrer"&gt;PHP&lt;/a&gt; and &lt;a href="https://getcomposer.org" rel="noopener noreferrer"&gt;Composer&lt;/a&gt; are installed on your computer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php &lt;span class="nt"&gt;--version&lt;/span&gt;
composer &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Initialise Your PHP Project
&lt;/h3&gt;

&lt;p&gt;Create a folder for your project.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;conditional-test-execution
&lt;span class="nb"&gt;cd &lt;/span&gt;conditional-test-execution
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Initialize a new PHP project by running &lt;code&gt;composer init&lt;/code&gt; in your project directory.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will create a &lt;code&gt;composer.json&lt;/code&gt; file. Ensure that your &lt;code&gt;composer.json&lt;/code&gt; has the following structure.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;  
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"software-witchcraft/conditional-test-execution"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  
    &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"A small demo project for showcasing conditional PHP test execution in GitHub Actions, created to support a tutorial."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"project"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  
    &lt;/span&gt;&lt;span class="nl"&gt;"license"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  
    &lt;/span&gt;&lt;span class="nl"&gt;"autoload"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;  
        &lt;/span&gt;&lt;span class="nl"&gt;"psr-4"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;  
            &lt;/span&gt;&lt;span class="nl"&gt;"SoftwareWitchcraft\\ConditionalTestExecution\\"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"src/"&lt;/span&gt;&lt;span class="w"&gt;  
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;  
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;  
    &lt;/span&gt;&lt;span class="nl"&gt;"authors"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;  
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;  
            &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Software Witchcraft"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  
            &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"info@softwarewitchcraft.com"&lt;/span&gt;&lt;span class="w"&gt;  
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;  
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;  
    &lt;/span&gt;&lt;span class="nl"&gt;"require"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;&lt;span class="w"&gt;  
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Add PHPUnit as a Dependency
&lt;/h3&gt;

&lt;p&gt;Add &lt;a href="https://phpunit.de/index.html" rel="noopener noreferrer"&gt;PHPUnit&lt;/a&gt; via Composer. Run the following command in your project's root directory.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require &lt;span class="nt"&gt;--dev&lt;/span&gt; phpunit/phpunit ^12
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command installs PHPUnit and its dependencies. The &lt;code&gt;--dev&lt;/code&gt; flag indicates that PHPUnit is a development dependency, not required for the production environment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configure PHPUnit
&lt;/h3&gt;

&lt;p&gt;Create a &lt;code&gt;phpunit.xml&lt;/code&gt; file in your project root. This file will hold your PHPUnit configuration. Here's a basic example.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;phpunit&lt;/span&gt; &lt;span class="na"&gt;bootstrap=&lt;/span&gt;&lt;span class="s"&gt;"vendor/autoload.php"&lt;/span&gt; &lt;span class="na"&gt;colors=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;testsuites&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;testsuite&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"My Test Suite"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;directory&amp;gt;&lt;/span&gt;tests&lt;span class="nt"&gt;&amp;lt;/directory&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/testsuite&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/testsuites&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/phpunit&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this file, &lt;code&gt;bootstrap&lt;/code&gt; points to Composer's autoloader, and the &lt;code&gt;tests&lt;/code&gt; directory is where your test files will reside.&lt;/p&gt;

&lt;h3&gt;
  
  
  Write Your First Test
&lt;/h3&gt;

&lt;p&gt;Create file &lt;code&gt;Greeter.php&lt;/code&gt; in your &lt;code&gt;src&lt;/code&gt; folder ...&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;touch&lt;/span&gt; ./src/Greeter.php
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;... with following content&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;SoftwareWitchcraft\ConditionalTestExecution&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Greeter&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;greet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s1"&gt;'Hello, '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$name&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'!'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a &lt;code&gt;tests&lt;/code&gt; directory in your project root.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;tests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside this directory, create a test file, for example, &lt;code&gt;GreeterTest.php&lt;/code&gt; ...&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;touch&lt;/span&gt; ./tests/GreeterTest.php
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;... with a simple PHPUnit test class.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt; 

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;PHPUnit\Framework\TestCase&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;SoftwareWitchcraft\ConditionalTestExecution\Greeter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GreeterTest&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;TestCase&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;testGreetsWithName&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$greeter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Greeter&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="nv"&gt;$greeting&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$greeter&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;greet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Alice'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertSame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Hello, Alice!'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$greeting&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Running Tests Locally
&lt;/h3&gt;

&lt;p&gt;To run your tests, execute the following command in your project root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./vendor/bin/phpunit &lt;span class="nt"&gt;--testdox&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command runs all the tests defined in your &lt;code&gt;tests&lt;/code&gt; directory. PHPUnit will look for any files ending in &lt;code&gt;Test.php&lt;/code&gt; and execute them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setup Git Repository and Push Your Project to GitHub
&lt;/h3&gt;

&lt;p&gt;Init you git repository.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create &lt;code&gt;.gitignore&lt;/code&gt; file ...&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;touch&lt;/span&gt; .gitignore
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;... with following content&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/vendor/
/composer.lock
/.phpunit.result.cache
/.idea/
.DS_Store
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add and commit all your changes to your repository.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git add &lt;span class="nb"&gt;.&lt;/span&gt; 
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"initial commit"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a repository on GitHub, and link it to your local repository.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git remote add origin git@github.com:yourusername/your-repository-name.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Don't forget to update &lt;code&gt;yourusername/your-repository-name&lt;/code&gt;! &lt;/p&gt;

&lt;p&gt;Push your local repository to GitHub.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git push &lt;span class="nt"&gt;-u&lt;/span&gt; origin main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now that we have our PHP project and PHPUnit setup complete, let's move on to the next section where we will set up GitHub Actions to automate our testing process.&lt;/p&gt;




&lt;h2&gt;
  
  
  Setting Up the GitHub Actions Environment
&lt;/h2&gt;

&lt;p&gt;To start leveraging GitHub Actions, you need to set up a workflow. A workflow is a set of automated procedures and tasks that are executed based on defined events, like a push or a pull request.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create a GitHub Actions Workflow File
&lt;/h3&gt;

&lt;p&gt;In your project repository, create directory &lt;code&gt;.github/workflows&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ./.github/workflows
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside this directory, create a new file named &lt;code&gt;RunTests.yml&lt;/code&gt; (or any other name you prefer, but keep the &lt;code&gt;.yml&lt;/code&gt; extension).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;touch&lt;/span&gt; ./.github/workflows/RunTests.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Define the Workflow
&lt;/h3&gt;

&lt;p&gt;Start the YAML file with a name for the workflow and specify the trigger events. Commonly, workflows are triggered on push or pull request events. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run unit tests&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;main&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;main&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;

        &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Check out repository code&lt;/span&gt;
              &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install PHP&lt;/span&gt;
              &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shivammathur/setup-php@v2&lt;/span&gt;
              &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="na"&gt;php-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;8.5'&lt;/span&gt; &lt;span class="c1"&gt;# Specify the PHP version&lt;/span&gt;
                  &lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;composer:v2, phpunit:v9.6&lt;/span&gt;
                  &lt;span class="na"&gt;coverage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;none&lt;/span&gt;

            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Check PHP Version&lt;/span&gt;
              &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;php -v&lt;/span&gt;

            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install dependencies&lt;/span&gt;
              &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;composer install&lt;/span&gt; &lt;span class="c1"&gt;# Install PHP dependencies&lt;/span&gt;

            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run PHPUnit tests&lt;/span&gt;
              &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./vendor/bin/phpunit --testdox&lt;/span&gt; &lt;span class="c1"&gt;# Execute PHPUnit tests&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Commit and Push the Workflow File
&lt;/h3&gt;

&lt;p&gt;After setting up the &lt;code&gt;RunTests.yml&lt;/code&gt; file, commit it to your repository and push it to GitHub:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git add .github/workflows/phpunit.yml
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Add GitHub Actions workflow"&lt;/span&gt;
git push origin main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once you push these changes to GitHub, the &lt;code&gt;Run PHPUnit Tests&lt;/code&gt; workflow will be triggered on the next push or pull request to the main branch, automatically running your PHPUnit tests in the specified environment.&lt;/p&gt;




&lt;p&gt;You've now set up a complete PHP project with local testing using PHPUnit and automated test execution via GitHub Actions. This setup helps ensure your code stays reliable by catching issues early with every push or pull request. From here, you can continue building features, adding more tests, and extending your CI workflow as needed.&lt;/p&gt;

&lt;p&gt;The complete code for this project is available in the &lt;a href="https://github.com/SoftwareWitchcraft/conditional-test-execution" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt;. You can explore the structure, test setup, and workflow configuration in one place and use it as a reference or starting point for your own projects.&lt;/p&gt;

</description>
      <category>php</category>
      <category>githubactions</category>
      <category>testing</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>The Race That Debugged My Software</title>
      <dc:creator>Aleksandar Sabo</dc:creator>
      <pubDate>Thu, 17 Apr 2025 14:21:54 +0000</pubDate>
      <link>https://forem.com/alxsabo/the-race-that-debugged-my-software-4ljl</link>
      <guid>https://forem.com/alxsabo/the-race-that-debugged-my-software-4ljl</guid>
      <description>&lt;p&gt;More than 15 years ago, I worked for a company that produced gym training machines. One of those machines was a kayak ergometer—essentially an indoor kayaking simulator. My job was to write firmware for a device that connected to the ergometer and translated sensor data into something athletes could understand and see: speed, force, distance, and so on.&lt;/p&gt;

&lt;p&gt;One day, the company owner came to me with a new idea.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Now that we have all this data being sent to the computer, can you build a piece of software that simulates an actual kayak race?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Challenge accepted.&lt;/p&gt;

&lt;h3&gt;
  
  
  Turning Data Into a Real Race
&lt;/h3&gt;

&lt;p&gt;I went straight to the drawing board, and a few months later, we had something working. The app visualized each athlete as a moving figure in a simulated kayak race. As they paddled on their machines, their avatars raced forward on the screen. It worked beautifully, and we were all proud of the result.&lt;/p&gt;

&lt;p&gt;I wanted to test the app in the real world, so I suggested organizing an amateur race in my hometown. I knew some local kayak clubs and was confident people would show up, but my boss had a different idea.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Let's do a real race. In a big European city. Let's do it in Moscow.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We had a few weeks to prepare everything. I tested everything thoroughly—starting and stopping races a hundred times, trying different configurations, asking colleagues to help test in our company gym, and even spending long evenings paddling just to iron out any edge cases.&lt;/p&gt;

&lt;p&gt;Things moved fast. In under two months, we were standing in a sports arena in Moscow, ready for the first-ever simulated kayak competition.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem #1: Asleep at the Start Line
&lt;/h3&gt;

&lt;p&gt;The first race went perfectly. The second one, too.&lt;/p&gt;

&lt;p&gt;Then came the third race. There was a minor issue with the race list—someone mixed up the names—but it got sorted out. The athletes sat on their machines. Everyone was tense. The crowd was ready.&lt;/p&gt;

&lt;p&gt;And then... one by one, the machines started turning off.&lt;/p&gt;

&lt;p&gt;We had no idea what was going on. Panic. Confusion. What was happening?&lt;/p&gt;

&lt;p&gt;We quickly powered them back on and restarted the race. Crisis averted—for now. But I needed to understand what had gone wrong.&lt;/p&gt;

&lt;p&gt;The problem was simple: &lt;strong&gt;the devices had been idle for too long&lt;/strong&gt; before the race started, and the &lt;strong&gt;battery-saving algorithm kicked in&lt;/strong&gt;, shutting them down automatically. During development, we had assumed that five minutes of idle time was more than enough. But it wasn't in a live event with last-minute changes and delays.&lt;/p&gt;

&lt;p&gt;The fix was easy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;When the device receives the first &lt;code&gt;RACE INIT&lt;/code&gt; command, it disables the power-saving mode until it's manually turned off or runs out of battery.&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Problem #2: Paddling Nowhere
&lt;/h3&gt;

&lt;p&gt;With that issue fixed, we kept moving forward.&lt;/p&gt;

&lt;p&gt;But then another problem showed up.&lt;/p&gt;

&lt;p&gt;The athletes were sitting on their machines. The race was about to start. I sent the start signal. They started paddling—and… nothing. Some of their avatars weren't moving.&lt;/p&gt;

&lt;p&gt;I stared at the screen, completely confused. There were no errors or warnings. The athletes were clearly paddling, but their figures just stood still. The race continued like that until the end of the day, and I never figured it out on the spot.&lt;/p&gt;

&lt;p&gt;My temporary fix was to stop and restart the race, which usually worked. But it wasn't a solution—it was a workaround.&lt;/p&gt;

&lt;p&gt;Back in the office the following week, I finally found the root cause:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The application needed calibration data from each machine to calculate distance. And sometimes, the machine returned all zeros instead of valid calibration data.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If that happened, the race app could not calculate movement, so the avatar just stayed still.&lt;/p&gt;

&lt;p&gt;The fix? Also simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;If the app gets all-zero calibration data, send the request again.&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;If it happens twice, show a clear error message with instructions for resetting the device manually.&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From that point on, the issue never surprised us again.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem #3: How a Folder Stopped a Race
&lt;/h3&gt;

&lt;p&gt;With those bugs behind us, we gained confidence. We knew that once the race started and all avatars moved, the system would run flawlessly until the end. The critical moment was the start—after that, we could finally breathe.&lt;/p&gt;

&lt;p&gt;It was time for the final race of the event. Everyone was ready. The crowd was buzzing. Athletes sat on their machines, waiting. I gave the start signal.&lt;/p&gt;

&lt;p&gt;The race began. Avatars moved. It worked.&lt;/p&gt;

&lt;p&gt;I finally allowed myself to relax.&lt;/p&gt;

&lt;p&gt;One of the local hosts, who had been helping with logistics the entire day, was sitting next to me. He was holding a folder in his hands, and just as we both leaned back in our chairs, he dropped the folder.&lt;/p&gt;

&lt;p&gt;It flew through the air and landed directly on my laptop's keyboard.&lt;/p&gt;

&lt;p&gt;Right on the &lt;strong&gt;Escape&lt;/strong&gt; key.&lt;/p&gt;

&lt;p&gt;And what did I have programmed the Escape key to do?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;&lt;strong&gt;Cancel the race.&lt;/strong&gt;&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Just like that, the screen went blank. Race canceled.&lt;/p&gt;

&lt;p&gt;I turned to the guy and snapped:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You!? Idiot! Why would you do that?!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I'm still ashamed for flying off the handle and saying something hurtful to him.&lt;/p&gt;

&lt;p&gt;But really, I was the idiot—not him. &lt;/p&gt;

&lt;p&gt;Why would I assign such a critical action to a single keypress with no confirmation? It was a terrible design decision—a lazy shortcut.&lt;/p&gt;

&lt;p&gt;The athletes took a 15-minute break. We restarted the race, and this time, everything went fine. As soon as it ended, I removed the Escape-to-cancel functionality from the software.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I Learned
&lt;/h3&gt;

&lt;p&gt;This project taught me a lot, but one takeaway stands out:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;strong&gt;No amount of testing can fully replicate production. Real users will always break things in ways you never expected.&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Mocks, test suites, and staging environments are great and necessary—but they’ll never catch everything. The real bugs show up when your code meets reality. Shipping early is the fastest way to uncover what actually needs fixing.&lt;/p&gt;

&lt;p&gt;Once I tackled those unexpected issues, the app ran flawlessly for years—and as far as I know, it’s still out there, quietly doing its job somewhere in the world.&lt;/p&gt;

&lt;p&gt;Pro tip: don’t wire critical actions to a single keypress.&lt;br&gt;
And maybe keep folders out of keyboard range. Trust me.&lt;/p&gt;



&lt;p&gt;If you’re curious to see what it all looked like—the tension before the start, the athletes paddling hard, and the virtual avatars racing neck and neck—a short video from the competition in Moscow captures the atmosphere.&lt;/p&gt;

&lt;p&gt;Watch the video here and experience the race for yourself.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/VEYkB7jcu7M"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

</description>
      <category>programming</category>
      <category>testing</category>
      <category>learning</category>
    </item>
    <item>
      <title>How I Built a Free Static Website for Developers Club Using Next.js and Cloudflare</title>
      <dc:creator>Aleksandar Sabo</dc:creator>
      <pubDate>Mon, 07 Apr 2025 22:00:00 +0000</pubDate>
      <link>https://forem.com/alxsabo/how-i-built-a-free-static-website-for-developers-club-using-nextjs-and-cloudflare-55b6</link>
      <guid>https://forem.com/alxsabo/how-i-built-a-free-static-website-for-developers-club-using-nextjs-and-cloudflare-55b6</guid>
      <description>&lt;p&gt;Recently, I rebuilt the website for &lt;a href="https://developersclub.rs/" rel="noopener noreferrer"&gt;Developers Club&lt;/a&gt;—a local non-profit I co-run with a few friends. It’s a community for software developers where we regularly host meetups, workshops, and discussions. One of our goals with this rebuild was to &lt;strong&gt;reduce hosting costs&lt;/strong&gt; to zero, while keeping the setup simple enough for any member of the club to maintain.&lt;/p&gt;

&lt;p&gt;If you’re considering doing something similar—building a small, low-maintenance site for a side project, community, or event—this post will walk you through the approach I used and serve as a reference if I ever need to do it again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Go Static?
&lt;/h2&gt;

&lt;p&gt;The original site was hosted on a paid VPS. While it didn’t cost much, even small recurring costs add up for a non-profit. We didn’t need a CMS like WordPress—just a simple, fast, single-page website that we could update by pushing code to GitHub.&lt;/p&gt;

&lt;p&gt;Hosting a &lt;strong&gt;static website&lt;/strong&gt; on &lt;a href="https://pages.cloudflare.com/" rel="noopener noreferrer"&gt;Cloudflare Pages&lt;/a&gt; made perfect sense:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It’s &lt;strong&gt;free&lt;/strong&gt; for static sites.&lt;/li&gt;
&lt;li&gt;GitHub integration allows &lt;strong&gt;automatic rebuilds&lt;/strong&gt; on push.&lt;/li&gt;
&lt;li&gt;I’ve had &lt;strong&gt;great experiences&lt;/strong&gt; using Cloudflare for DNS, caching, and SSL in the past.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Writing and Designing the Site
&lt;/h2&gt;

&lt;p&gt;I wrote the copy for the website and asked my friend &lt;a href="https://github.com/daliboru" rel="noopener noreferrer"&gt;Dalibor&lt;/a&gt;—who’s great with front-end work—to build the UI using &lt;strong&gt;Tailwind CSS&lt;/strong&gt; and &lt;strong&gt;clean HTML&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;My initial plan was to integrate his HTML/CSS with &lt;a href="https://gohugo.io/" rel="noopener noreferrer"&gt;Hugo&lt;/a&gt;, since I’ve used it for other static sites (including this blog). Hugo is blazing fast, and deploying Hugo sites on Cloudflare is straightforward—something I’ve done several times already.&lt;/p&gt;

&lt;p&gt;However, Dalibor went the extra mile and, instead of delivering plain HTML, he built the site using Next.js.&lt;/p&gt;

&lt;p&gt;A bit unexpected—but not a big problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Making Next.js Work as a Static Site
&lt;/h2&gt;

&lt;p&gt;Next.js can work as a static site generator with a few adjustments. Cloudflare Pages supports static Next.js deployments—but you need to &lt;strong&gt;configure the project properly&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The code Dalibor provided couldn’t be built with static export due to a module that wasn’t compatible. I first tried modifying the project settings to enable static export, but that didn’t solve the issue. In the end, I decided to rebuild the project from scratch using a clean Next.js static setup and then copied over the source files.&lt;/p&gt;

&lt;p&gt;I followed this official guide to get started:&lt;br&gt;&lt;br&gt;
🔗 &lt;a href="https://developers.cloudflare.com/pages/framework-guides/nextjs/deploy-a-static-nextjs-site/" rel="noopener noreferrer"&gt;Deploy a static Next.js site&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Basically, all you need to do is set output to ’export’ in &lt;code&gt;next.config.js&lt;/code&gt; like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;export&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3: Deploy the Website to Cloudflare
&lt;/h2&gt;

&lt;p&gt;With the static Next.js project ready, the next step was to deploy it to Cloudflare Pages.&lt;/p&gt;

&lt;p&gt;I connected the GitHub repository to Cloudflare and selected &lt;em&gt;“Next.js (Static HTML Export)”&lt;/em&gt; as the framework. However, the initial deployment failed due to a version mismatch: Cloudflare uses Node 16 by default, which installs Next.js v14—but our project required Next.js v15, which needs Node 20.&lt;/p&gt;

&lt;p&gt;To resolve this, I followed this helpful blog post: &lt;a href="https://blog.rampatra.com/how-to-run-a-website-built-with-next-js-15-on-cloudflare-pages" rel="noopener noreferrer"&gt;How to run a website built with Next.js 15 on Cloudflare Pages&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Fix
&lt;/h3&gt;

&lt;p&gt;To ensure that Cloudflare uses the correct Node version for building the project, I added the following to the package.json:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"engines"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"node"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;gt;20.0.0"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also created a &lt;code&gt;.nvmrc&lt;/code&gt; file with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;20
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This combination solved the problem and allowed the deployment to complete successfully.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Final Tweaks
&lt;/h2&gt;

&lt;p&gt;Once everything deployed successfully, I made a few final touches:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Added a &lt;strong&gt;favicon&lt;/strong&gt; and site description for better SEO.&lt;/li&gt;
&lt;li&gt;Slight &lt;strong&gt;color adjustments&lt;/strong&gt; for better contrast.&lt;/li&gt;
&lt;li&gt;Integrated &lt;a href="https://umami.is/" rel="noopener noreferrer"&gt;Umami Analytics&lt;/a&gt; for privacy-friendly tracking. It’s simple and lightweight—perfect for a community site.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary and Takeaways
&lt;/h2&gt;

&lt;p&gt;Here’s what I learned and what I’d do again:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cloudflare Pages&lt;/strong&gt; is a solid choice for free static site hosting.&lt;/li&gt;
&lt;li&gt;Even though I expected to use Hugo, &lt;strong&gt;Next.js works well&lt;/strong&gt; for this use case with the right setup.&lt;/li&gt;
&lt;li&gt;Setting the Node version is critical when using the latest Next.js.&lt;/li&gt;
&lt;li&gt;Having the project in GitHub makes collaboration and maintenance easy—our members can now contribute with simple pull requests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the end, we now have a &lt;strong&gt;modern, free, and maintainable website&lt;/strong&gt; for Developers Club. It’s fast, easy to update, and reflects the spirit of our community: practical and developer-friendly.&lt;/p&gt;

&lt;p&gt;If you want to explore the full implementation, check out the &lt;a href="https://github.com/DevelopersClubRS/DevelopersClub.rs" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt;. It’s open for contributions or suggestions if you’d like to help improve the site.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>cloudflare</category>
      <category>staticwebapps</category>
    </item>
    <item>
      <title>How a Designer Took Project Oktan to the Next Level</title>
      <dc:creator>Aleksandar Sabo</dc:creator>
      <pubDate>Wed, 19 Mar 2025 23:00:00 +0000</pubDate>
      <link>https://forem.com/alxsabo/how-a-designer-took-project-oktan-to-the-next-level-ji2</link>
      <guid>https://forem.com/alxsabo/how-a-designer-took-project-oktan-to-the-next-level-ji2</guid>
      <description>&lt;p&gt;Over the years, I’ve learned that working with skilled professionals always brings additional value to a project—sometimes in ways you wouldn’t expect. While we, as developers, focus on building functional and efficient solutions, professionals in other domains can refine and elevate our work beyond what we initially envisioned.&lt;/p&gt;

&lt;p&gt;One such experience was when I was creating a website for my project, &lt;a href="https://softwarewitchcraft.com/my-apps/oktan/" rel="noopener noreferrer"&gt;Oktan&lt;/a&gt;, a desktop application for tracking car expenses, fuel consumption, and service maintenance logs. I wanted the website to have a strong visual appeal, so I hired a designer to create an illustration for the hero section.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Initial Idea
&lt;/h2&gt;

&lt;p&gt;After a brief discussion about the job, my instructions were simple: create an illustration featuring a car and three people—one changing a tire, one working under the hood, and one taking notes. This scene was meant to convey the core functionality of Oktan: tracking and maintaining car-related expenses in a structured manner.&lt;/p&gt;

&lt;p&gt;When the designer sent me the initial sketch, I was already satisfied. In fact, I was ready to approve it as the final version. However, he insisted on continuing to refine it. At first, I didn’t see the necessity, but I trusted his expertise and let him proceed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Unexpected Improvement
&lt;/h2&gt;

&lt;p&gt;What followed was a transformation I hadn’t anticipated. The designer added finer details, such as the workers’ hair, toolbox items, headlights shade, the car’s interior, bringing the illustration to life. The final illustration was far beyond what I had imagined—it was polished, professional, and had a level of depth that I wouldn’t have achieved on my own.&lt;/p&gt;

&lt;p&gt;This experience reinforced an important lesson: quality professionals bring more than just technical execution; they bring vision, experience, and a commitment to excellence. Sometimes, they see opportunities for improvement that we, as project owners, might overlook simply because we’re too close to our own ideas.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;In software development, it’s tempting to do everything ourselves. But knowing when to bring in experts—whether they’re designers, copywriters, UX specialists, or other professionals—can make a significant difference in the final product. Here’s why:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;They Elevate the Project&lt;/strong&gt; – Skilled professionals don’t just do the job; they refine and enhance it beyond the initial requirements.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;They Save Time and Effort&lt;/strong&gt; – Instead of struggling to achieve a decent result on our own, we can rely on experts to get things done right, faster.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;They Bring a Fresh Perspective&lt;/strong&gt; – An outside perspective can highlight improvements we wouldn’t have considered.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;They Ensure Quality&lt;/strong&gt; – Professionals care about the details, which leads to a more polished and engaging final product.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;If you’re working on a project—whether it’s an app, a website, or any other venture—consider bringing in specialists when needed. The investment often pays off in ways you didn’t expect. My experience with the Oktan website illustration is just one example, but it serves as a reminder that working with the right people can take your work from “good enough” to something truly outstanding.&lt;/p&gt;

&lt;p&gt;I’m sharing the illustrations in this post, so you can see the difference for yourself.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flccuyk1moobiadcnvyec.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flccuyk1moobiadcnvyec.webp" alt="Initial sketch" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa2lr7fgdczc0ond62vnt.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa2lr7fgdczc0ond62vnt.webp" alt="Work in progress" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqgccp0z9vbtfq6dpcm02.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqgccp0z9vbtfq6dpcm02.webp" alt="Final version" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>learning</category>
    </item>
    <item>
      <title>Key-Value Stores</title>
      <dc:creator>Aleksandar Sabo</dc:creator>
      <pubDate>Mon, 10 Mar 2025 23:00:00 +0000</pubDate>
      <link>https://forem.com/alxsabo/key-value-stores-43h3</link>
      <guid>https://forem.com/alxsabo/key-value-stores-43h3</guid>
      <description>&lt;p&gt;Key-value stores provide a simple yet highly efficient way to manage data. Their speed and scalability make them invaluable for caching, session management, and other high-performance applications. While they may not be suitable for all use cases, they complement relational and document databases by offering rapid access to frequently used data. Understanding how they work, their advantages and limitations, and where they fit in modern software architecture is essential for developers.&lt;/p&gt;

&lt;h2&gt;
  
  
  How They Work
&lt;/h2&gt;

&lt;p&gt;A key-value store is a type of NoSQL database that manages data as a collection of key-value pairs. This structure is similar to a hash table, dictionary, or map in programming. Each data item is assigned a unique key, and retrieving the value requires looking up the corresponding key. This direct lookup mechanism makes key-value stores extremely fast compared to relational databases that require complex query processing.&lt;/p&gt;

&lt;p&gt;A generic example of how key-value stores operate:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Storing Data&lt;/strong&gt;: The application assigns a value to a key.&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;set user:12345 "John Doe"
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Retrieving Data&lt;/strong&gt;: The application retrieves the value using the key.&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;get user:12345  # Returns "John Doe"
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Updating Data&lt;/strong&gt;: The value can be replaced by assigning a new one to the same key.&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;set user:12345 "Jane Doe"
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Deleting Data&lt;/strong&gt;: Some key-value stores support removing keys explicitly.&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;delete user:12345
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This simple approach allows for rapid access and modifications, making key-value stores efficient for many real-time applications.&lt;/p&gt;

&lt;h2&gt;
  
  
  Strengths of Key-Value Stores
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. &lt;strong&gt;Speed&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Key-value stores are optimized for ultra-fast lookups, making them an excellent choice for applications requiring real-time performance. Since retrieving a value requires only a direct key lookup, response times are significantly lower compared to traditional relational databases.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. &lt;strong&gt;Simplicity&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The data model is straightforward, making key-value stores easy to implement and maintain. Unlike relational databases, there’s no need to define schemas, indexes, or relationships.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. &lt;strong&gt;Scalability&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Many key-value databases, such as Redis and DynamoDB, are designed to scale horizontally across multiple servers. This makes them ideal for handling large-scale applications that require high availability and distributed storage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Weaknesses of Key-Value Stores
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. &lt;strong&gt;Limited Querying Capabilities&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Unlike relational databases, key-value stores do not support advanced queries such as filtering, joins, or aggregations. Developers must structure data retrieval around simple key lookups, which can be a limitation for some applications.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. &lt;strong&gt;Not Ideal for Complex Relationships&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;If your application requires structured relationships between data—such as users, orders, and products in an e-commerce system—a relational database is often a better choice.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. &lt;strong&gt;Limited Space&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Many key-value stores, particularly in-memory databases like Memcached, have constrained storage capacity. This means they are typically used for transient data rather than permanent storage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Use Cases
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. &lt;strong&gt;Caching Frequently Accessed Data&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Key-value stores excel at caching frequently accessed information. For example, a news website may cache trending articles, reducing the need to fetch data from a slower relational database repeatedly.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. &lt;strong&gt;Session Management&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Web applications often use key-value stores to manage user session data efficiently. Instead of storing session details in a relational database, storing them in a key-value store ensures quick access and reduces database overhead.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. &lt;strong&gt;Configuration Storage&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Applications can use key-value databases to manage configuration settings dynamically. This enables real-time updates without requiring a complete system restart or a complex database migration.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. &lt;strong&gt;Rate Limiting and Queues&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Many high-traffic applications use key-value stores to implement rate limiting. For instance, an API service can track requests per user with a key-value store, limiting excessive usage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Popular Key-Value Stores
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. &lt;strong&gt;Redis&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://redis.io/" rel="noopener noreferrer"&gt;Redis&lt;/a&gt; is an in-memory key-value store that supports additional data structures like lists, sets, and hashes. It is widely used for caching, session storage, and real-time analytics.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. &lt;strong&gt;Amazon DynamoDB&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://aws.amazon.com/dynamodb/" rel="noopener noreferrer"&gt;DynamoDB&lt;/a&gt; is a cloud-based, scalable key-value store offered by AWS. It is designed for high-speed transactions and is commonly used in large-scale applications.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. &lt;strong&gt;Memcached&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://memcached.org/" rel="noopener noreferrer"&gt;Memcached&lt;/a&gt; is a lightweight, high-performance caching system primarily used to accelerate web applications by storing frequently accessed data in memory.&lt;/p&gt;

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

&lt;p&gt;Key-value stores are an essential part of modern software architecture, providing fast, scalable, and efficient data storage solutions. While they have limitations in terms of querying and relational capabilities, their strengths in speed and simplicity make them invaluable for caching, session management, and other performance-critical tasks. Choosing the right key-value store depends on the specific needs of your application, whether it’s high-speed lookups, distributed caching, or real-time analytics.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Oktan: The Car Expense Tracker I Built 15 Years Ago</title>
      <dc:creator>Aleksandar Sabo</dc:creator>
      <pubDate>Sat, 01 Mar 2025 23:00:00 +0000</pubDate>
      <link>https://forem.com/alxsabo/oktan-the-car-expense-tracker-i-built-15-years-ago-3ng0</link>
      <guid>https://forem.com/alxsabo/oktan-the-car-expense-tracker-i-built-15-years-ago-3ng0</guid>
      <description>&lt;p&gt;Over fifteen years ago, I created &lt;strong&gt;Oktan&lt;/strong&gt;, a desktop application designed to track car expenses, fuel consumption, and service maintenance logs. This wasn’t just another project—it was my way of bringing structure to something I deeply cared about: understanding and managing expenses in a digital format.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdxqoec5q4xs6sophfii9.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdxqoec5q4xs6sophfii9.webp" alt="Oktan screenshot" width="800" height="548"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At the time, smartphones were just emerging, but I didn’t own one. That’s why I built &lt;strong&gt;Oktan&lt;/strong&gt; as a &lt;strong&gt;desktop application using C++ and the Qt framework&lt;/strong&gt;, with &lt;strong&gt;SQLite&lt;/strong&gt; as the database. It started as a personal tool but later became publicly available, even earning a mention in &lt;strong&gt;Serbia’s leading computer magazine&lt;/strong&gt;.&lt;/p&gt;

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

&lt;p&gt;Owning a car comes with many hidden costs—fuel, maintenance, insurance, repairs—and I wanted to track them all. The goal was simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Log every fuel refill and service cost.&lt;/li&gt;
&lt;li&gt;Get insights into &lt;strong&gt;how much the car was actually costing&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Make better decisions when it was time to upgrade or replace the vehicle.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Back then, there weren’t many apps available for this purpose, and certainly not as easily accessible as today’s mobile apps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Development &amp;amp; Challenges
&lt;/h2&gt;

&lt;p&gt;I developed &lt;strong&gt;Oktan in my free time over several months&lt;/strong&gt;, handling all the coding myself while hiring a designer for marketing graphics. The hardest part wasn’t writing the code—it was pushing through to &lt;strong&gt;actually release the app&lt;/strong&gt;. Finishing a project and making it available to users is a challenge many developers face, and &lt;strong&gt;Oktan taught me the importance of persistence&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Features &amp;amp; Functionality
&lt;/h3&gt;

&lt;p&gt;Oktan included:&lt;br&gt;&lt;br&gt;
✅ &lt;strong&gt;Fuel consumption tracking&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
✅ &lt;strong&gt;Service and maintenance logs&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
✅ &lt;strong&gt;Graphical reports to visualize spending&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Outcome
&lt;/h2&gt;

&lt;p&gt;Though I didn’t track user numbers, those who used Oktan gave me positive feedback. However, I quickly realized a major issue:&lt;br&gt;&lt;br&gt;
💡 &lt;strong&gt;People are more interested in knowing where they can save money rather than tracking where they already spent it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;budgeting-focused app&lt;/strong&gt; would have likely gained more traction than an expense-tracking tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;p&gt;Building Oktan was an invaluable experience that helped me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Learn &lt;strong&gt;how to ship a complete product from idea to release&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Understand &lt;strong&gt;how to create Windows installers&lt;/strong&gt; and package Qt apps with all necessary dependencies.&lt;/li&gt;
&lt;li&gt;Gain experience with &lt;strong&gt;data visualization and charting in Qt&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Develop a &lt;strong&gt;structured approach to app architecture&lt;/strong&gt;, which I later reused in projects like &lt;strong&gt;Silos&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Would I Rebuild Oktan Today?
&lt;/h2&gt;

&lt;p&gt;Short answer: &lt;strong&gt;No.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Long answer: The market is now flooded with similar apps, and the business potential for such a project is minimal. If I were to rebuild it, I’d &lt;strong&gt;choose a mobile-first approach using Flutter&lt;/strong&gt;, ensuring it runs on both Android and iOS.&lt;/p&gt;

&lt;p&gt;Oktan may not be relevant today, but it was a &lt;strong&gt;crucial stepping stone&lt;/strong&gt; in my journey as a developer. It reinforced the importance of finishing projects, understanding user behavior, and choosing the right platform for the problem at hand.&lt;/p&gt;

</description>
      <category>learning</category>
    </item>
    <item>
      <title>How to Count and Track Checkboxes in Obsidian with Dataview</title>
      <dc:creator>Aleksandar Sabo</dc:creator>
      <pubDate>Thu, 20 Feb 2025 23:00:00 +0000</pubDate>
      <link>https://forem.com/alxsabo/how-to-count-and-track-checkboxes-in-obsidian-with-dataview-336a</link>
      <guid>https://forem.com/alxsabo/how-to-count-and-track-checkboxes-in-obsidian-with-dataview-336a</guid>
      <description>&lt;p&gt;As a developer juggling multiple projects and responsibilities, I rely on Obsidian for personal life management (PLM). One of the ways I keep track of tasks is through simple checklists in various notes. Over time, I found that manually checking each note to see my progress was inefficient. I wanted a way to display an overview of my progress—specifically, the number of completed tasks out of the total—directly on my dashboard.&lt;/p&gt;

&lt;p&gt;Obsidian’s &lt;strong&gt;Dataview&lt;/strong&gt; plugin provided the perfect solution for this.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Goal
&lt;/h2&gt;

&lt;p&gt;My objective was to automatically count checkboxes from a specific file and show a summary like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;3 / 12 | My Todos
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This way, I could quickly assess my progress without opening multiple files. The count should dynamically update whenever tasks are checked or added.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution
&lt;/h2&gt;

&lt;p&gt;Using &lt;strong&gt;Dataview inline queries&lt;/strong&gt;, I wrote the following snippet and placed it in my dashboard note:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="sb"&gt;`$= dv.page(dv.current().file.folder + "/todo/My Todos").file.tasks?.filter(t =&amp;gt; t.completed).length || 0`&lt;/span&gt; / &lt;span class="sb"&gt;`$= dv.page(dv.current().file.folder + "/todo/My Todos").file.tasks?.length || 0`&lt;/span&gt; | [[My Todos]]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Breakdown of the Code:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;dv.page(...)&lt;/code&gt; loads the referenced file (&lt;code&gt;My Todos&lt;/code&gt; in this case).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;.file.tasks&lt;/code&gt; accesses the tasks within that file.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;.filter(t =&amp;gt; t.completed).length&lt;/code&gt; counts the number of completed checkboxes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;.length&lt;/code&gt; counts the total number of tasks.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The &lt;code&gt;|| 0&lt;/code&gt; ensures the display remains stable even if no tasks exist, preventing errors.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;With this snippet in place, my dashboard now displays:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbyqulsur7ccfhwbx2ron.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbyqulsur7ccfhwbx2ron.webp" alt="Aleksandar’s tasks" width="770" height="722"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This provides a real-time overview of my task completion status without requiring manual updates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Works Well
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Dynamic Updates&lt;/strong&gt; – As tasks are checked off or added, the numbers update automatically.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Minimal Setup&lt;/strong&gt; – No additional scripting or plugins beyond Dataview are required.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Clear Overview&lt;/strong&gt; – Helps track progress across multiple files without opening each one manually.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Potential Enhancements
&lt;/h2&gt;

&lt;p&gt;If you manage multiple task lists, you can modify the query to aggregate counts across multiple files or folders. Additionally, Dataview’s table view could be used to show progress for several projects at once.&lt;/p&gt;

&lt;p&gt;If you use Obsidian for task management, consider implementing a similar solution to enhance visibility and productivity.&lt;/p&gt;

</description>
      <category>obisdian</category>
    </item>
    <item>
      <title>Determining the Date of Monday in the First Week of the Year</title>
      <dc:creator>Aleksandar Sabo</dc:creator>
      <pubDate>Tue, 04 Feb 2025 23:00:00 +0000</pubDate>
      <link>https://forem.com/alxsabo/determining-the-date-of-monday-in-the-first-week-of-the-year-4ch0</link>
      <guid>https://forem.com/alxsabo/determining-the-date-of-monday-in-the-first-week-of-the-year-4ch0</guid>
      <description>&lt;p&gt;I had to create a script that generates all weeks in a year and writes their start and end dates. While searching for a solution, I discovered an interesting algorithm that determines the date of Monday in the first week of the year. This seemingly simple task becomes more complex when considering international standards and varying definitions of the “first week.”&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem Definition
&lt;/h3&gt;

&lt;p&gt;The problem is straightforward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Input:&lt;/strong&gt; A year (e.g., 2025).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt; The date of Monday in the first week of that year.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This isn’t as simple as selecting January 1st because the first week of the year isn’t always the one containing January 1st. This brings us to the ISO 8601 standard.&lt;/p&gt;

&lt;h3&gt;
  
  
  First week in the ISO 8601 Standard
&lt;/h3&gt;

&lt;p&gt;ISO 8601 defines the first week of the year as &lt;strong&gt;the week that contains the first Thursday of the year&lt;/strong&gt;. This is equivalent to the week that contains January 4th. According to this standard:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;A week starts on &lt;strong&gt;Monday&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The first week of the year is the one that includes &lt;strong&gt;January 4th&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;If January 1st falls on a Monday through Thursday, it’s in the first week of the year.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If January 1st falls on a Friday, Saturday, or Sunday, the first Monday of the first ISO week will be in the following calendar week.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Simple Solution in Any Programming Language
&lt;/h3&gt;

&lt;p&gt;To solve this problem, follow these simple steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Identify January 4th of the given year.&lt;/strong&gt; This date is guaranteed to be in the first ISO week.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Determine the weekday of January 4th.&lt;/strong&gt; (0 for Monday, 1 for Tuesday, …, 6 for Sunday, depending on the language.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Subtract the necessary number of days to get back to Monday.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  Pseudocode Implementation
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function getFirstMondayOfYear(year):
    jan4 = Date(year, 1, 4)
    dayOfWeek = jan4.weekday()  // Assuming 0 = Monday, 6 = Sunday
    firstMonday = jan4 - (dayOfWeek) days
    return firstMonday
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  PHP Implementation
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function getFirstMondayOfYear($year) {
    $jan4 = new DateTime("$year-01-04");
    $dayOfWeek = (int)$jan4-&amp;gt;format('N') - 1; // N: 1 (Monday) to 7 (Sunday), adjust to 0-6
    $jan4-&amp;gt;modify("-{$dayOfWeek} days");
    return $jan4-&amp;gt;format('Y-m-d');
}

echo getFirstMondayOfYear(2025);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  JavaScript Implementation
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function getFirstMondayOfYear(year) {
    const jan4 = new Date(year, 0, 4); // January is month 0
    const dayOfWeek = jan4.getDay() === 0 ? 6 : jan4.getDay() - 1; // Adjusting Sunday (0) to 6, others to 0-5
    jan4.setDate(jan4.getDate() - dayOfWeek);
    return jan4.toISOString().split('T')[0];
}

console.log(getFirstMondayOfYear(2025));
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Using Built-in Methods in Some Languages
&lt;/h3&gt;

&lt;p&gt;Some programming languages offer built-in methods to get the Monday of the first ISO week directly.&lt;/p&gt;

&lt;h4&gt;
  
  
  Python (Using &lt;code&gt;isocalendar&lt;/code&gt;)
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from datetime import date

def get_first_monday_of_year(year):
    return date.fromisocalendar(year, 1, 1)  # Year, ISO week 1, Monday (1)

print(get_first_monday_of_year(2025))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Java (Using &lt;code&gt;java.time&lt;/code&gt; API)
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import java.time.LocalDate;
import java.time.temporal.WeekFields;
import java.util.Locale;

public class Main {
    public static void main(String[] args) {
        LocalDate firstMonday = LocalDate.of(2025, 1, 1)
            .with(WeekFields.ISO.weekOfYear(), 1)
            .with(WeekFields.ISO.dayOfWeek(), 1);
        System.out.println(firstMonday);
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  C# (.NET) Using &lt;code&gt;ISOWeek&lt;/code&gt;
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;using System;
using System.Globalization;

class Program {
    static void Main() {
        var firstMonday = ISOWeek.ToDateTime(2025, 1, DayOfWeek.Monday);
        Console.WriteLine(firstMonday.ToString("yyyy-MM-dd"));
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Example
&lt;/h4&gt;

&lt;p&gt;For &lt;strong&gt;2025&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;January 4, 2025, is a &lt;strong&gt;Saturday&lt;/strong&gt; (day 5 if Monday=0).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Subtract 5 days from January 4:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;January 4 - 5 days = &lt;strong&gt;Monday, December 30, 2024&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;So, the first Monday of the first ISO week of 2025 is December 30, 2024.&lt;/p&gt;

&lt;p&gt;To find the first Monday of the year, anchor to January 4th and count backward to the nearest Monday. This simple approach works consistently across different programming languages.&lt;/p&gt;

</description>
      <category>php</category>
      <category>python</category>
      <category>java</category>
      <category>csharp</category>
    </item>
    <item>
      <title>Extend Hugo/Congo Theme: Adding Links to Unsupported Platforms</title>
      <dc:creator>Aleksandar Sabo</dc:creator>
      <pubDate>Sat, 11 Jan 2025 23:00:00 +0000</pubDate>
      <link>https://forem.com/alxsabo/extend-hugocongo-theme-adding-links-to-unsupported-platforms-3j58</link>
      <guid>https://forem.com/alxsabo/extend-hugocongo-theme-adding-links-to-unsupported-platforms-3j58</guid>
      <description>&lt;p&gt;The &lt;a href="https://jpanther.github.io/congo/" rel="noopener noreferrer"&gt;Congo theme&lt;/a&gt; for &lt;a href="https://gohugo.io/" rel="noopener noreferrer"&gt;Hugo&lt;/a&gt; website builder, provides a convenient way to add links to your profiles on various platforms. However, you might encounter a situation where the platform you want to link to isn’t supported by default. For instance, I wanted to add a link to my Goodreads profile, but there was no built-in support for it. In this guide, I’ll show you how to add custom links to the Congo theme with minimal effort.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Important Note&lt;/strong&gt;: This process leverages features of the Congo theme, not Hugo itself. Keep this distinction in mind as you follow the steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step-by-Step Guide
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Find the Icon&lt;/strong&gt; Head over to &lt;a href="https://www.iconfinder.com/search?q=goodreads" rel="noopener noreferrer"&gt;Iconfinder&lt;/a&gt; and search for an SVG icon representing the platform you want to link to (e.g., Goodreads).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Download the SVG Icon&lt;/strong&gt; Once you’ve found the appropriate icon, download it and save it to the following directory in your Hugo project:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;assets/icons
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: The icon must be in SVG format, and its filename must match the platform key you use. For example: &lt;code&gt;goodreads.svg&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Edit the SVG Icon&lt;/strong&gt; Open the SVG file in a text editor or an SVG editor. Change the &lt;code&gt;fill&lt;/code&gt; attributes value to &lt;code&gt;currentColor&lt;/code&gt; and remove any &lt;code&gt;xml&lt;/code&gt; metadata tags to ensure the icon will adapt to the theme’s colors. Here’s an example of a simplified SVG:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"&amp;gt;
    &amp;lt;path fill="currentColor" d="M..."/&amp;gt;
&amp;lt;/svg&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Add the Profile Link&lt;/strong&gt; Open the &lt;code&gt;config/_default/languages.en.toml&lt;/code&gt; file (or some other language file depending on your project’s configuration) and locate the &lt;code&gt;params.authors&lt;/code&gt; section. Add an entry for the new profile link:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[params.author]&lt;/span&gt;
&lt;span class="py"&gt;links&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="err"&gt;...&lt;/span&gt;
  &lt;span class="err"&gt;{&lt;/span&gt; &lt;span class="py"&gt;goodreads&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"https://www.goodreads.com/your-profile"&lt;/span&gt; &lt;span class="err"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="err"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Test the Changes&lt;/strong&gt; Restart your local Hugo server and verify that the new link appears correctly in the profile links section of your site. The icon should display in your theme’s colors.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Repeat the steps&lt;/strong&gt; To add links to other platforms.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This approach ensures consistency with the Congo theme’s design while allowing you to personalize your site with links to your favorite platforms. Happy customizing!&lt;/p&gt;

</description>
      <category>hugo</category>
      <category>staticwebapps</category>
      <category>go</category>
    </item>
    <item>
      <title>Automating Text File Encoding Conversion with Python</title>
      <dc:creator>Aleksandar Sabo</dc:creator>
      <pubDate>Sun, 05 Jan 2025 23:00:00 +0000</pubDate>
      <link>https://forem.com/alxsabo/automating-text-file-encoding-conversion-with-python-4k4d</link>
      <guid>https://forem.com/alxsabo/automating-text-file-encoding-conversion-with-python-4k4d</guid>
      <description>&lt;p&gt;If you’ve ever worked with text files from various sources, you’ve probably encountered issues with file encoding. For me, this problem became evident when I downloaded subtitles for TV shows that weren’t in UTF-8 encoding. All my devices are set to use UTF-8, so dealing with encoding mismatches can quickly become frustrating — especially when you have to update subtitles for your favorite TV show with 24 episodes per season and 6 seasons in total! Manually opening each file in a text editor and saving it with the correct encoding was a waste of time.&lt;/p&gt;

&lt;p&gt;To save time and effort, I decided to create a Python package to automate the process of converting text file encodings.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Problem: Non-UTF-8 Encodings
&lt;/h3&gt;

&lt;p&gt;Subtitles for older or region-specific TV shows often come in non-UTF-8 encodings, like Windows-1250 or ISO-8859-2. While these encodings might work on most systems, they usually require switching to the correct encoding and then reverting the settings afterward. To avoid this hassle, I decided to standardize everything to UTF-8 and convert all subtitles to this format. Manually converting files every time is tedious and error-prone, so I needed a tool that could:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Handle batch processing of multiple files.&lt;/li&gt;
&lt;li&gt;Automatically detect and process all files in a folder.&lt;/li&gt;
&lt;li&gt;Be easy to use and configurable for different encodings.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Solution: &lt;code&gt;change_encoding&lt;/code&gt; package
&lt;/h3&gt;

&lt;p&gt;The result of my efforts is a Python package simply called &lt;code&gt;change_encoding&lt;/code&gt;. It’s designed to be straightforward and effective. Here’s what it does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Converts all &lt;code&gt;.srt&lt;/code&gt; files in a specified folder from one encoding to another.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Creates a separate folder for the converted files, named based on the destination encoding, preserving the originals.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Handles encoding errors gracefully, ensuring the process doesn’t break halfway.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Installation
&lt;/h3&gt;

&lt;p&gt;You can install the package by cloning the GitHub repository and install it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git clone https://github.com/SoftwareWitchcraft/change_encoding
cd change_encoding
pip3 install .
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Usage
&lt;/h3&gt;

&lt;p&gt;Once installed, &lt;code&gt;chenc&lt;/code&gt; command will be available on your computer, so you can use the package directly from the command line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;chenc /path/to/folder windows-1250 utf-8
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;/path/to/folder&lt;/code&gt; is the directory containing the text files to convert.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;windows-1250&lt;/code&gt; is the source encoding (you can change this based on your files).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;utf-8&lt;/code&gt; is the target encoding.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you don’t specify the encodings, the package defaults to converting from &lt;code&gt;windows-1250&lt;/code&gt; to &lt;code&gt;utf-8&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;NOTE: Please refer to the &lt;a href="https://docs.python.org/3/library/codecs.html#standard-encodings" rel="noopener noreferrer"&gt;Python documentation&lt;/a&gt; for a list of all supported encodings.&lt;/p&gt;

&lt;h3&gt;
  
  
  How It Works
&lt;/h3&gt;

&lt;p&gt;The package works by:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Scanning the specified folder for &lt;code&gt;.srt&lt;/code&gt; files.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Reading each file with the specified source encoding.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Writing the content to a new file in the target encoding.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Saving the converted files in a subfolder, named based on the destination encoding, within the original directory (for example: in &lt;code&gt;utf-8&lt;/code&gt; subfolder).&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here’s a simple code snippet that forms the core functionality:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;convert_file_encoding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input_file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output_file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;from_encoding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to_encoding&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input_file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;from_encoding&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;infile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;infile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output_file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;w&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;to_encoding&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;outfile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;outfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Extending the Package
&lt;/h3&gt;

&lt;p&gt;While the package is currently tailored for my needs, there’s potential for extending its functionality:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Auto-detection of source encoding&lt;/strong&gt;: Using libraries like &lt;code&gt;chardet&lt;/code&gt; or &lt;code&gt;charset-normalizer&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Support for additional file types&lt;/strong&gt;: Beyond &lt;code&gt;.srt&lt;/code&gt;, the package could handle &lt;code&gt;.txt&lt;/code&gt; or &lt;code&gt;.csv&lt;/code&gt; files.&lt;/li&gt;
&lt;li&gt;Recursively &lt;strong&gt;scanning the subfolders&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;…&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;change_encoding&lt;/code&gt; package has simplified a tedious process in my workflow, allowing me to enjoy my TV shows without hassle. Now I can process an entire folder of subtitle files in seconds, ensuring they’re ready to use on all my devices.&lt;/p&gt;

&lt;p&gt;If you’re facing similar encoding issues, feel free to try it out or adapt it to your needs. You can find the code on &lt;a href="https://github.com/SoftwareWitchcraft/change_encoding" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Track your Net Worth in Obsidian</title>
      <dc:creator>Aleksandar Sabo</dc:creator>
      <pubDate>Sat, 07 Dec 2024 23:00:00 +0000</pubDate>
      <link>https://forem.com/alxsabo/track-your-net-worth-in-obsidian-4cko</link>
      <guid>https://forem.com/alxsabo/track-your-net-worth-in-obsidian-4cko</guid>
      <description>&lt;p&gt;I update my net worth document every three months to assess my financial progress. I only tracked my savings and debts in the past, but this approach provided a partial picture of my financial health. For instance, my savings might increase, but I could still be losing money overall—or the reverse could happen when I make investments.&lt;/p&gt;

&lt;p&gt;Over time, I realized I needed to track my &lt;a href="https://www.investopedia.com/terms/n/networth.asp" rel="noopener noreferrer"&gt;net worth&lt;/a&gt; to understand my finances truly. Tracking my net worth has helped me identify trends, assess financial decisions, and focus on my goals.&lt;/p&gt;

&lt;p&gt;In this article, I’ll explain how I track my personal net worth in Obsidian. This method is not set in stone - it’s adaptable to your needs and preferences. If you’re looking for a simple and effective way to manage your finances, this method might work for you, too.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Net worth?
&lt;/h2&gt;

&lt;p&gt;Net worth is the difference between your assets (what you own) and liabilities (what you owe). It’s a snapshot of your financial health. Assets include things like cash, investments, property, and other valuables, while liabilities include debts like loans and credit card balances.&lt;/p&gt;

&lt;p&gt;Tracking your net worth over time gives you a clear view of how your finances are changing. It helps you understand if you’re accumulating wealth, managing debt effectively, or losing ground. By regularly reviewing your net worth, you can make more informed decisions about spending, saving, and investing to improve your financial health.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I track my net worth
&lt;/h2&gt;

&lt;p&gt;To track my net worth I use Obsidian.&lt;/p&gt;

&lt;p&gt;First I created a simple document with two main sections: assets and liabilities.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Assets&lt;/strong&gt;: This section includes everything I own that has value, such as cash, investments, properties, and other valuables.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Liabilities&lt;/strong&gt;: This section lists all the money I owe, like loans or credit card debt.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I then calculate the total value for both assets and liabilities. From there, I create four key pieces of information that are saved in the file’s tag properties:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Date&lt;/strong&gt;: The date the snapshot was taken.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assets&lt;/strong&gt;: The total value of everything I own.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Liabilities&lt;/strong&gt;: The total value of what I owe.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Net worth&lt;/strong&gt;: The difference between assets and liabilities.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is the example of this file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;note&lt;/span&gt;
&lt;span class="na"&gt;date&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2024-12-01&lt;/span&gt;
&lt;span class="na"&gt;assets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;525000&lt;/span&gt;
&lt;span class="na"&gt;liabilities&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;305000&lt;/span&gt;
&lt;span class="na"&gt;net-worth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;220000&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
Parent: [[My NetWorth]]
&lt;span class="p"&gt;
---&lt;/span&gt;
&lt;span class="gu"&gt;## Net worth: $220.000&lt;/span&gt;

Total = Asset - Liabilities
= $525.000 - $305.000
&lt;span class="p"&gt;
---&lt;/span&gt;
&lt;span class="gu"&gt;## Assets: $525.000&lt;/span&gt;

cash:
&lt;span class="p"&gt;-&lt;/span&gt; Bank: $10.000

investments:
&lt;span class="p"&gt;-&lt;/span&gt; Awesome startup: $25.000

properties:
&lt;span class="p"&gt;-&lt;/span&gt; home: $475.000

other valuables:
&lt;span class="p"&gt;-&lt;/span&gt; car: $15.000
&lt;span class="p"&gt;
---&lt;/span&gt;
&lt;span class="gu"&gt;## Liabilities: $305.000&lt;/span&gt;

loans:
&lt;span class="p"&gt;-&lt;/span&gt; $300.000

credit card debt:
&lt;span class="p"&gt;-&lt;/span&gt; $5.000
&lt;span class="p"&gt;
---
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every three months, I update my net worth by duplicating the most recent file, renaming it, and entering the latest asset and liability values. This ensures I maintain an accurate and up-to-date record of my finances.&lt;/p&gt;

&lt;p&gt;Over time, these files create a timeline of my net worth, allowing me to track financial progress and observe how my wealth changes. Using Obsidian, this system makes it easy to access and process the data programmatically, enabling simple visualization of trends and insights.&lt;/p&gt;

&lt;h2&gt;
  
  
  Visualizing Net Worth Timeline in Obsidian
&lt;/h2&gt;

&lt;p&gt;In Obsidian, you can visualize your net worth timeline in two ways: as a table or as a chart. The table provides a straightforward view of the data, while the chart offers a more visually engaging representation.&lt;/p&gt;

&lt;p&gt;Here’s how to set it up.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Install Necessary Plugins
&lt;/h3&gt;

&lt;p&gt;To get started, you’ll need to install two community plugins in Obsidian:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://blacksmithgu.github.io/obsidian-dataview/" rel="noopener noreferrer"&gt;Dataview&lt;/a&gt;: This plugin lets you query and display data from your notes in a structured format.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/phibr0/obsidian-charts" rel="noopener noreferrer"&gt;Charts&lt;/a&gt;: This plugin allows you to create visual representations of your data with customizable charts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To install these plugins, follow these steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open Obsidian’s settings and navigate to the &lt;strong&gt;Community Plugins&lt;/strong&gt; section.&lt;/li&gt;
&lt;li&gt;Enable &lt;strong&gt;Restricted Mode&lt;/strong&gt; if it’s turned off.&lt;/li&gt;
&lt;li&gt;Use the &lt;strong&gt;Browse&lt;/strong&gt; option to search for “Dataview” and “Charts.”&lt;/li&gt;
&lt;li&gt;Install and enable each plugin.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For more detailed setup instructions, refer to the documentation on their respective websites.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Organize Your Data in Files
&lt;/h3&gt;

&lt;p&gt;In Obsidian, start by creating a folder called &lt;code&gt;My Net Worth&lt;/code&gt;. Inside this folder, create another folder named &lt;code&gt;notes&lt;/code&gt;, which will hold all your quarterly net worth calculations.&lt;/p&gt;

&lt;p&gt;Each file in the &lt;code&gt;notes&lt;/code&gt; folder should follow the naming format &lt;code&gt;yyyy-mm-dd net worth.md&lt;/code&gt; (e.g., &lt;code&gt;2024-12-01 net worth.md&lt;/code&gt;). This consistent naming ensures your files are organized chronologically, making it easy to track your net worth over time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Create a Summary File
&lt;/h3&gt;

&lt;p&gt;In the &lt;code&gt;My Net Worth&lt;/code&gt; folder, create a summary file named &lt;code&gt;My Net Worth.md&lt;/code&gt;. This file will serve as a dashboard for visualizing your financial progress using both a table and a chart.&lt;/p&gt;

&lt;p&gt;At the top of the file, include the following markdown header:&lt;/p&gt;

&lt;pre&gt;
# My net worth



```dataviewjs
  ... you will add your code here ... 
```


&lt;/pre&gt;

&lt;p&gt;Insert the following &lt;strong&gt;DataviewJS&lt;/strong&gt; code to pull data from the &lt;code&gt;notes&lt;/code&gt; folder, process it, and generate a timeline of your net worth.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Query Notes from the &lt;code&gt;notes&lt;/code&gt; Folder&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Get all pages from subfolder: notes&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;currentFilePath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;current&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;currentFolder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;currentFilePath&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;substring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;currentFilePath&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lastIndexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;folderName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;notes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Replace with your folder name&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;targetFolder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;currentFolder&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;folderName&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`"&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;targetFolder&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"`&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;asc&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Extract Data for Visualization&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Extract data from pages&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dateNames&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;substring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;netWorth&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;net-worth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;assets&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;assets&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;liabilities&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;liabilities&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Create and Render the Chart&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Create and render chart&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chartData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;line&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;dateNames&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;datasets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Net worth&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;netWorth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rgba(33, 150, 243, 0.2)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="na"&gt;borderColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rgba(33, 150, 243, 1)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="na"&gt;borderWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Assets&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;assets&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rgba(76, 175, 80, 0.2)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="na"&gt;borderColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rgba(76, 175, 80, 1)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="na"&gt;borderWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Liabilities&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;liabilities&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rgba(244, 67, 54, 0.2)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="na"&gt;borderColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rgba(244, 67, 54, 1)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="na"&gt;borderWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;renderChart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chartData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Create and Render the Table&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Create and render the table &lt;/span&gt;
&lt;span class="nx"&gt;dv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Date&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Assets&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Liabilities&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Net Worth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="nx"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toReversed&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;assets&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;liabilities&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;net-worth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Final Thoughts: Visualizing Your Financial Progress
&lt;/h2&gt;

&lt;p&gt;When you open the summary file in Obsidian, you’ll see:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Chart&lt;/strong&gt;: A visually appealing line chart displaying trends for Net Worth, Assets, and Liabilities. Each dataset is color-coded for easy identification.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqpa4lue9z9y4en3o899s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqpa4lue9z9y4en3o899s.png" alt="Net wort chart" width="660" height="355"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Table&lt;/strong&gt;: A detailed table showing the Date, Assets, Liabilities, and Net Worth for each quarter.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7jvv6bykwroytlmbbvww.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7jvv6bykwroytlmbbvww.png" alt="Net worth table" width="660" height="471"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As you add more quarterly records, both the chart and table automatically update, providing an up-to-date view of your financial progress.&lt;/p&gt;

&lt;p&gt;This setup creates a clear, visual timeline of your net worth over time, helping you easily assess your financial health and track your progress toward financial goals.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get Started with Sample Files
&lt;/h2&gt;

&lt;p&gt;To make it even easier for you to set up your own net worth tracking system in Obsidian, I’ve prepared a set of sample files that you can download and import directly into your Obsidian vault.&lt;/p&gt;

&lt;p&gt;You can download the files &lt;a href="https://softwarewitchcraft.com/posts/track-your-net-worth-in-obsidian/my-net-worth.zip" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>obsidian</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
