<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Product developer's blog. I know how to build products efficiently without overspending]]></title><description><![CDATA[Software engineer with broad experience in different areas and technologies - from web to embedded and mobile.]]></description><link>https://alexsinelnikov.blog</link><generator>RSS for Node</generator><lastBuildDate>Sun, 19 Apr 2026 21:38:25 GMT</lastBuildDate><atom:link href="https://alexsinelnikov.blog/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[How I switched from ruby to elixir and to learn it better - built a product]]></title><description><![CDATA[Hey everyone! In this post I wanted to share some of thoughts from my learning process. I'm developing apps for about 15 years, with main lang - ruby and ruby on rails framework. Over my career I worked with pretty much everything - embedded developm...]]></description><link>https://alexsinelnikov.blog/how-i-switched-from-ruby-to-elixir-and-to-learn-it-better-built-a-product</link><guid isPermaLink="true">https://alexsinelnikov.blog/how-i-switched-from-ruby-to-elixir-and-to-learn-it-better-built-a-product</guid><category><![CDATA[Elixir]]></category><category><![CDATA[SaaS]]></category><category><![CDATA[product]]></category><category><![CDATA[Developer Tools]]></category><dc:creator><![CDATA[Alex Sinelnikov]]></dc:creator><pubDate>Thu, 16 Oct 2025 08:21:49 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1760602819061/a06e85e6-611e-401f-a2ed-9d49033186f2.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hey everyone! In this post I wanted to share some of thoughts from my learning process. I'm developing apps for about 15 years, with main lang - ruby and ruby on rails framework. Over my career I worked with pretty much everything - embedded development, mobile, desktop, web.</p>
<p>I know about elixir since 2017 or so when I first saw Chris McCord vid on YouTube about Phoenix. Always wanted to try but never had a chance. Last year I decided to build a product for my own needs and thought what if I use Elixir/Phoenix for that.</p>
<p>To start - I decided to use boilerplate. I won't be sharing the name, but overall I wasn't really happy about it. I had to rewrite about 70% of code because it simply didnt work for my needs, even though my app isnt that special and doesnt have anything non standard. Its simply code wasn't really extendable or reusable, so for my next product I will probably just start with empty PHX app.</p>
<p>It took a bit of time to get used to Elixir functional approach. I could not understand Quote/Unqoute concept until very recently, but that didnt stop me from implementing most of my app with out it. Ecto concept was always not the most pleasant. While I understand why it was made that way, I had cheatsheets always with me simply because I could not memorize function names and arguments, esp when you can use macro syntax for things like select, etc.</p>
<p>LiveView is miles ahead of Rails's turbo. At some point I was even overusing it for simple UI interactions such as opening dropdown, etc. Later I refactored code to use Alpine.js for everything UI related and I'm happy about that. Hooks are really nice addition too, but I only used it once in my case. Just LV and Alpine.js was enough for me. I live in Europe, but I host app on DO in NYC region and have no latency issues with LV. I even tested it through few VPN connections to add some latency and it was working better than most modern react based apps. And overall I was happy with ease of use. I don't really understand complexity made with layouts(root, live, app) so took a bit to get used to it.</p>
<p>ObanJob was nice surprise for me. Finally I didnt need to run another instance of app for background jobs(hello sidekiq) and it required 0 extra infra or maintenance. Maybe for big queues it would made sense, but I have few jobs running every few mins, so it works well.</p>
<p>I had issues with deployment. There are few ways to deploy apps and I went with dockerizing compilation. Dockerfile was pretty simple multistage build, but when running I had OOM errors on my 4gb instance. After hours of googling and debugging I found this <code>ERL_MAX_PORTS=1024</code> which solved all my memory issues. It was just a message on elixir forum without much explanation.</p>
<p>Testing tools are a big rough. Rails has many useful gems to help with it like factory bot, etc. ElIxir/Phoenix seem like a bit behind in this terms(but I might just didnt find good tools or good approach).</p>
<p>What I really like - elixir's case statement. Handling different call results not much easier because of pattern match. So things like {:ok, result} -&gt; ... {:error, message} -&gt; helps to handle errors much easier. And overall pattern matching feature is super useful and helped me to write really good code comparing to same in ruby. It's also nice Phoenix has generated authentication code. Unlike from devise - it has minimal implementation, but it's really quick to add anything you need. In my case I added google/github authentication in just few hours.</p>
<p>Some of recent updates made regular controller/template/views a bit weird for me. For some reason now templates, views and controllers under same <code>controller</code> folder making it really hard to manage it, would be nice to have separate folder for templates/views outside of controllers.</p>
<p>The app I build - <a target="_blank" href="http://updatify.io/">updatify.io</a> is a release notes tool where you can embed widget to your web app. I also used LV to power the widget. I have some JS code to create modal, but then it just creates iframe inside with LV powered app. One of the features - blog which you can host on subdomain - took a bit of time to get sorted with subdomains. I came up with few plugs that helped me to serve requested blog on subdomain, and it was one of first things I covered with tests because I still feel like it could be done better. For some 3rd party services there isnt a package, so I had to write my own harness, but its not that hard and mostly can be done in matter of hour.</p>
<p>I also had few back and forth with image uploads. Originally I stored them in app, but eventually decided to move to CDN, because it was simply cheaper($5 for DO Spaces). Took a bit to understand ho presign_ function works and thats first time I used hooks. I still don't really like how its implemented and I feel like it could be done easier</p>
<p>Overall I'm really happy with my elixir/phoenix experience. I already pitched this tool for another paid project I'm about to start. The biggest complexity was to convince client there's enough developers on market to support it. For my own projects I plan to use it more. I'm not sure how well it will work just of API type of projects, since LV is a big part of framework and one of reasons people like it.</p>
<p>Added: I tried LiveViewNative few months ago. Saw Dockyard CEO post on twitter and gave it a try. Its in very early stages of development, but it can definitely has its own audience and niche. Its not be used for apps where you might be offline, but I feel like e-commerce type of apps could benefit from it</p>
]]></content:encoded></item><item><title><![CDATA[Advanced form components with Alpine and Rails]]></title><description><![CDATA[Sometime ago I posted article about alpine.js usage with Ruby on Rails app and also another article about ViewComponents
Today I want to show you how can you implement really advanced form(and not just) input components. By advanced I mean something ...]]></description><link>https://alexsinelnikov.blog/advanced-form-components-with-alpine-and-rails</link><guid isPermaLink="true">https://alexsinelnikov.blog/advanced-form-components-with-alpine-and-rails</guid><category><![CDATA[Ruby]]></category><category><![CDATA[alpinejs]]></category><category><![CDATA[Tailwind CSS]]></category><category><![CDATA[components]]></category><dc:creator><![CDATA[Alex Sinelnikov]]></dc:creator><pubDate>Mon, 09 Sep 2024 18:51:25 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1725908398085/0692cbbd-73b9-4e10-a4f3-adf0cda2d03f.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Sometime ago I posted <a target="_blank" href="https://alexsinelnikov.blog/alpinejs-for-ruby-on-rails-developers-simplifying-frontend-development">article</a> about alpine.js usage with Ruby on Rails app and also another article about <a target="_blank" href="https://alexsinelnikov.blog/using-component-based-approach-in-your-ruby-on-rails-app">ViewComponents</a></p>
<p>Today I want to show you how can you implement really advanced form(and not just) input components. By advanced I mean something like custom drop-downs, toggles, or anything other that you can't create/style with regular inputs</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1725895104035/5d2e2e00-0422-45a5-b0ac-9b1a51a5fae4.png" alt class="image--center mx-auto" /></p>
<p>The idea is to use Alpine.js together with hidden inputs which would hold your input state and then wrap it into ViewComponent, so it could be reused in any of your forms</p>
<h3 id="heading-implementation">Implementation</h3>
<p>As an example I use dropdown from screenshot above. This is tailwind styled component, but it is a good example of more advanced component.</p>
<p>This is how it would look like in simplest implementation</p>
<pre><code class="lang-ruby"><span class="hljs-comment"># custom_dropdown_component.rb</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CustomDropdownComponent</span> &lt; ViewComponent::Base</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">initialize</span><span class="hljs-params">(<span class="hljs-symbol">options:</span>, <span class="hljs-symbol">selected_option:</span> <span class="hljs-literal">nil</span>, <span class="hljs-symbol">form:</span>, <span class="hljs-symbol">field_name:</span>)</span></span>
    @options = options
    @selected_option = selected_option
    @form = form
    @field_name = field_name
  <span class="hljs-keyword">end</span>

  private

  <span class="hljs-keyword">attr_reader</span> <span class="hljs-symbol">:options</span>, <span class="hljs-symbol">:selected_option</span>, <span class="hljs-symbol">:form</span>, <span class="hljs-symbol">:field_name</span>
<span class="hljs-keyword">end</span>

<span class="hljs-comment"># custom_dropdown_component.html.erb</span>

&lt;div x-data=<span class="hljs-string">"{ open: false, selectedOption: '&lt;%= selected_option %&gt;' }"</span> <span class="hljs-class"><span class="hljs-keyword">class</span>="<span class="hljs-title">relative</span> <span class="hljs-title">pointer</span>-<span class="hljs-title">events</span>-<span class="hljs-title">auto</span> <span class="hljs-title">w</span>-[28.125<span class="hljs-title">rem</span>] <span class="hljs-title">text</span>-[0.8125<span class="hljs-title">rem</span>] <span class="hljs-title">leading</span>-5 <span class="hljs-title">text</span>-<span class="hljs-title">slate</span>-700"&gt;</span>
  &lt;div <span class="hljs-class"><span class="hljs-keyword">class</span>="<span class="hljs-title">font</span>-<span class="hljs-title">semibold</span> <span class="hljs-title">text</span>-<span class="hljs-title">slate</span>-900"&gt;<span class="hljs-title">Assigned</span> <span class="hljs-title">to</span>&lt;/<span class="hljs-title">div</span>&gt;</span>
  &lt;div <span class="hljs-class"><span class="hljs-keyword">class</span>="<span class="hljs-title">relative</span> <span class="hljs-title">mt</span>-1"&gt;</span>
  &lt;button @click=<span class="hljs-string">"open = !open"</span> type=<span class="hljs-string">"button"</span> <span class="hljs-class"><span class="hljs-keyword">class</span>="<span class="hljs-title">w</span>-<span class="hljs-title">full</span> <span class="hljs-title">bg</span>-<span class="hljs-title">white</span> <span class="hljs-title">border</span> <span class="hljs-title">border</span>-<span class="hljs-title">gray</span>-300 <span class="hljs-title">rounded</span>-<span class="hljs-title">md</span> <span class="hljs-title">shadow</span>-<span class="hljs-title">sm</span> <span class="hljs-title">px</span>-4 <span class="hljs-title">py</span>-2 <span class="hljs-title">text</span>-<span class="hljs-title">left</span> <span class="hljs-title">cursor</span>-<span class="hljs-title">default</span> <span class="hljs-title">focus</span>:<span class="hljs-title">outline</span>-<span class="hljs-title">none</span> <span class="hljs-title">focus</span>:<span class="hljs-title">ring</span>-1 <span class="hljs-title">focus</span>:<span class="hljs-title">ring</span>-<span class="hljs-title">indigo</span>-500 <span class="hljs-title">focus</span>:<span class="hljs-title">border</span>-<span class="hljs-title">indigo</span>-500 <span class="hljs-title">sm</span>:<span class="hljs-title">text</span>-<span class="hljs-title">sm</span>"&gt;</span>
    &lt;span x-text=<span class="hljs-string">"selectedOption || 'Select an option'"</span>&gt;&lt;<span class="hljs-regexp">/span&gt;
    &lt;span class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none"&gt;
      &lt;!-- Heroicon name: solid/selector</span> --&gt;
      &lt;svg <span class="hljs-class"><span class="hljs-keyword">class</span>="<span class="hljs-title">h</span>-5 <span class="hljs-title">w</span>-5 <span class="hljs-title">text</span>-<span class="hljs-title">gray</span>-400" <span class="hljs-title">xmlns</span>="<span class="hljs-title">http</span>://<span class="hljs-title">www</span>.<span class="hljs-title">w3</span>.<span class="hljs-title">org</span>/2000/<span class="hljs-title">svg</span>" <span class="hljs-title">viewBox</span>="0 0 20 20" <span class="hljs-title">fill</span>="<span class="hljs-title">currentColor</span>" <span class="hljs-title">aria</span>-<span class="hljs-title">hidden</span>="<span class="hljs-title">true</span>"&gt;</span>
        &lt;path fill-rule=<span class="hljs-string">"evenodd"</span> d=<span class="hljs-string">"M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z"</span> clip-rule=<span class="hljs-string">"evenodd"</span> /&gt;
      &lt;<span class="hljs-regexp">/svg&gt;
    &lt;/span</span>&gt;
  &lt;<span class="hljs-regexp">/button&gt;
  &lt;/div</span>&gt;

  &lt;div x-show=<span class="hljs-string">"open"</span> @click.away=<span class="hljs-string">"open = false"</span> <span class="hljs-class"><span class="hljs-keyword">class</span>="<span class="hljs-title">absolute</span> <span class="hljs-title">z</span>-10 <span class="hljs-title">mt</span>-1 <span class="hljs-title">w</span>-<span class="hljs-title">full</span> <span class="hljs-title">bg</span>-<span class="hljs-title">white</span> <span class="hljs-title">shadow</span>-<span class="hljs-title">lg</span> <span class="hljs-title">max</span>-<span class="hljs-title">h</span>-60 <span class="hljs-title">rounded</span>-<span class="hljs-title">md</span> <span class="hljs-title">py</span>-1 <span class="hljs-title">text</span>-<span class="hljs-title">base</span> <span class="hljs-title">ring</span>-1 <span class="hljs-title">ring</span>-<span class="hljs-title">black</span> <span class="hljs-title">ring</span>-<span class="hljs-title">opacity</span>-5 <span class="hljs-title">overflow</span>-<span class="hljs-title">auto</span> <span class="hljs-title">focus</span>:<span class="hljs-title">outline</span>-<span class="hljs-title">none</span> <span class="hljs-title">sm</span>:<span class="hljs-title">text</span>-<span class="hljs-title">sm</span>"&gt;</span>
    &lt;% options.each <span class="hljs-keyword">do</span> <span class="hljs-params">|option|</span> %&gt;
      &lt;div @click=<span class="hljs-string">"selectedOption = '&lt;%= option[:value] %&gt;'; open = false"</span> <span class="hljs-class"><span class="hljs-keyword">class</span>="<span class="hljs-title">cursor</span>-<span class="hljs-title">pointer</span> <span class="hljs-title">select</span>-<span class="hljs-title">none</span> <span class="hljs-title">relative</span> <span class="hljs-title">py</span>-2 <span class="hljs-title">pl</span>-3 <span class="hljs-title">pr</span>-9 <span class="hljs-title">hover</span>:<span class="hljs-title">bg</span>-<span class="hljs-title">indigo</span>-600 <span class="hljs-title">hover</span>:<span class="hljs-title">text</span>-<span class="hljs-title">white</span>"&gt;</span>
        &lt;%= option[<span class="hljs-symbol">:value</span>] %&gt;
      &lt;<span class="hljs-regexp">/div&gt;
    &lt;% end %&gt;
  &lt;/div</span>&gt;
  &lt;%= form.hidden_field field_name, <span class="hljs-string">'x-model'</span>: <span class="hljs-string">"selectedOption"</span> %&gt;

  &lt;div <span class="hljs-class"><span class="hljs-keyword">class</span>="<span class="hljs-title">mt</span>-2 <span class="hljs-title">text</span>-<span class="hljs-title">slate</span>-900"&gt;<span class="hljs-title">Current</span> <span class="hljs-title">selected</span> <span class="hljs-title">value</span> <span class="hljs-title">inside</span> <span class="hljs-title">hidden_field</span>: &lt;span <span class="hljs-title">x</span>-<span class="hljs-title">text</span>="<span class="hljs-title">selectedOption</span>"&gt;&lt;/<span class="hljs-title">span</span>&gt;&lt;/<span class="hljs-title">div</span>&gt;</span>
&lt;<span class="hljs-regexp">/div&gt;

# Your template

&lt;%= form_with do |f| %&gt; # There can be your form for your model
  &lt;%= render(CustomDropdownComponent.new(form: f, field_name: :test, options: [{id: 1, value: 'John Doe'}, {id: 2, value: 'Jane Doe'}, { id: 3, value: 'Tobias Luetke'}]))%&gt;
&lt;% end %&gt;</span>
</code></pre>
<p>And final result would look like below</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1725907435274/cdf87ad1-19c0-48c1-940a-c36f9eab24fc.png" alt class="image--center mx-auto" /></p>
<p>All the magic happens thanks to this:</p>
<pre><code class="lang-ruby">&lt;%= form.hidden_field field_name, <span class="hljs-string">'x-model'</span>: <span class="hljs-string">"selectedOption"</span> %&gt;
</code></pre>
<p>where <code>x-model</code> is responsible to setting proper value to <code>hidden_field</code> upon any action from page. So when you click on any of dropdown elements</p>
<p><code>&lt;div @click="selectedOption = '&lt;%= option[:value] %&gt;'; open = false"&gt;&lt;%= option[:value]&lt;/div&gt;</code> we have <code>@click</code> handler which sets `<code>selectedOption</code>` to interpolated value of our option.</p>
<p>You can have any interface you prefer, either hash or specific DropdownItem class which has needed methods for your case.</p>
<h3 id="heading-conclusion">Conclusion</h3>
<p>With this approach you can have any form inputs you can come up with or find online. For example for tailwind there are few really cool UI kits which you can use right away. This can save time on your UI, especially if you work on MVP and you'd prefer to work on core features rather than UI</p>
<p>By using ViewComponent you also make your UI more reusable, since all form inputs can be used across all your forms not just one</p>
<p><strong>For more interesting content follow me on</strong> <a target="_blank" href="https://x.com/_avdept"><strong>X</strong></a> <strong>where I post daily</strong></p>
]]></content:encoded></item><item><title><![CDATA[White-label mobile apps with Flutter & Fastlane]]></title><description><![CDATA[For more interesting content follow me on Twitter, I post daily there!
Recently, I’ve been working on an app core intended for distribution as a white-label solution. For those unfamiliar, a white-label solution involves creating code that can be cus...]]></description><link>https://alexsinelnikov.blog/white-label-mobile-apps-with-flutter-fastlane</link><guid isPermaLink="true">https://alexsinelnikov.blog/white-label-mobile-apps-with-flutter-fastlane</guid><category><![CDATA[Flutter]]></category><category><![CDATA[deployment]]></category><category><![CDATA[white label cryptolaunchpad]]></category><dc:creator><![CDATA[Alex Sinelnikov]]></dc:creator><pubDate>Mon, 26 Aug 2024 08:35:47 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1724661296074/05426700-66b0-4162-a34b-37c4d69f8e94.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>For more interesting content follow me on</strong> <a target="_blank" href="https://x.com/_avdept"><strong>Twitter</strong></a><strong>, I post daily there!</strong></p>
<p>Recently, I’ve been working on an app core intended for distribution as a white-label solution. For those unfamiliar, a white-label solution involves creating code that can be customized into different apps for various clients. So basically it's the same app but can be with different design, backend endpoint, images/logos, sounds, etc.</p>
<p>Many restaurants run on same platform with just different color scheme, name and icon. There are many other industries where this approach is normal, as you don't need to develop app from scratch, but can use existing solutions.</p>
<p>In this article I want to show my approach to building white-label solutions with Flutter and Fastlane.</p>
<p>Why Fastlane you might ask? When you have fewer than five apps, manual deployment is manageable and doesn't take long. When you have few dozens of apps - you don't want to deploy it manually as it will take hours and hours of monotonous work.</p>
<h2 id="heading-flutter">Flutter</h2>
<p>Before automating the build, we need to prepare our app for white-labeling. There are few general approaches. Some folks prefer to go with Flavors/BuildSchemes. I prefer to go with ENV variables.</p>
<p>In my case, the build process looks like this:</p>
<pre><code class="lang-bash">flutter build ipa --dart-define=MY_VAR1=value --dart-define=MY_VAR2=value
</code></pre>
<p>You can also use <code>--dart-define-from-json</code> and pass a path to json, but you'll see why I went this way</p>
<p>And then inside my flutter app I extract all ENV vars as constants to use later in my app</p>
<pre><code class="lang-dart"><span class="hljs-keyword">static</span> <span class="hljs-keyword">const</span> primaryColorHex = <span class="hljs-built_in">String</span>.fromEnvironment(<span class="hljs-string">'MAIN_COLOR'</span>);
</code></pre>
<h2 id="heading-fastlane-amp-ci-builds">Fastlane &amp; CI builds</h2>
<p>Now to more complex part. Building with CI is a regular thing. Most of apps are built in CI as part of CI CD pipelines. Either for testing purposes or for App Store deployment.</p>
<p>Since we already had few dozens on clients willing to have their white-labelled versions I had to come up with solution when I didn't have to spend days adding new clients. The solution is very simple - use Fastlane to build all apps at once with Github Actions(or any other CI provider). Fastlane is basically a set of <strong>Ruby lang</strong> methods that does something. There are existing <code>lanes</code> or you can create your own for your specific needs. Its best to have someone familiar with <strong>Ruby</strong> while creating your own custom lanes/steps</p>
<p>I started with json config file which looked like following. Where <code>FIREBASE_PROJECT_ID</code> is uniq value per each client(yes we use firebase for auth and some other things).</p>
<pre><code class="lang-json">{ <span class="hljs-attr">"FIREBASE_PROJECT_ID"</span>: {
    <span class="hljs-attr">"APPSTORE_BUNDLE_ID"</span>: <span class="hljs-string">"com.myapp.client"</span>,
    <span class="hljs-attr">"DEV_API_URL"</span>: <span class="hljs-string">"https://dev.endpoint.com"</span>,
    <span class="hljs-attr">"PROD_API_URL"</span>: <span class="hljs-string">"https://prod.client.endpoint.com"</span>,
    <span class="hljs-attr">"DISPLAY_NAME"</span>: <span class="hljs-string">"App Name"</span>,
    <span class="hljs-attr">"COMPANY_NAME"</span>: <span class="hljs-string">"Company Name"</span>,
    <span class="hljs-attr">"MAIN_COLOR"</span>: <span class="hljs-string">"#000000"</span>,
    <span class="hljs-attr">"COUNTRY_CODE"</span>: <span class="hljs-string">"US"</span>,
    <span class="hljs-attr">"LOGO_DATA"</span>: <span class="hljs-string">"..ommited BASE64 encoded image"</span>
   }
}
</code></pre>
<p>To add a new client, I simply need to add a new section to this JSON file. Eventually this JSON file was moved to server and it takes few clicks to add new client via web form, but for ease of understanding lets stick to local JSON config.</p>
<p>Before actually deploying even test build we need manually create app bundle and app itself on appstoreconnect. We can automate this as well, but I haven't get there. Overall it takes 10-15 minutes to create new <code>bundle ID</code> and new app on <code>appstoreconnect</code>, fill in all information(it needs to be done only once per app). Same time applies to Google Play.</p>
<p>To save dev time I recorded few videos explaining how to do this, so even non tech staff can also do the job.</p>
<p>Now it's the most complicated step - building. Here's my <code>Fastfile</code> with comments on each step</p>
<pre><code class="lang-ruby">default_platform(<span class="hljs-symbol">:ios</span>)

GIT_AUTHORIZATION = ENV[<span class="hljs-string">"GIT_AUTHORIZATION"</span>]
TEMP_KEYCHAIN_USER = <span class="hljs-string">'temp_keychain'</span>
TEMP_KEYCHAIN_PASSWORD = <span class="hljs-string">'temp_keychain_password'</span>


<span class="hljs-comment"># methods below are service methods that helps me to setup build config</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">create_temp_keychain</span><span class="hljs-params">(name, password)</span></span>
  create_keychain(
    <span class="hljs-symbol">name:</span> name,
    <span class="hljs-symbol">password:</span> password,
    <span class="hljs-symbol">unlock:</span> <span class="hljs-literal">false</span>,
    <span class="hljs-symbol">timeout:</span> <span class="hljs-number">0</span>
  )
<span class="hljs-keyword">end</span>

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">ensure_temp_keychain</span><span class="hljs-params">(name, password)</span></span>
  create_temp_keychain(name, password)
<span class="hljs-keyword">end</span>
<span class="hljs-comment"># This method provides me specific PROJECT configuration based on</span>
<span class="hljs-comment"># provided ENV variable PROJECT ID.</span>
<span class="hljs-comment"># This is done in order to build single app per fastlane launch</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">build_configuration</span></span>
  raise <span class="hljs-string">'PROJECT_ID is not set'</span> <span class="hljs-keyword">unless</span> ENV[<span class="hljs-string">'PROJECT_ID'</span>]

  config = build_configurations[ENV[<span class="hljs-string">'PROJECT_ID'</span>]]

  raise <span class="hljs-string">'Configuration not found'</span> <span class="hljs-keyword">unless</span> config

  config
<span class="hljs-keyword">end</span>

<span class="hljs-comment"># This just parses json file and provides me a ruby JSON object with</span>
<span class="hljs-comment"># all configurations</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">build_configurations</span></span>
  @configs <span class="hljs-params">||</span>= JSON.parse(File.read(<span class="hljs-string">'config.json'</span>))
<span class="hljs-keyword">end</span>

<span class="hljs-comment"># This method creates me string with all my dart defines ENV vars</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">build_args</span></span>
  args = {
    <span class="hljs-string">'PROJECT_ID'</span>: build_configuration[<span class="hljs-string">'PROJECT_ID'</span>],
    ...other env vars
  }
  raise <span class="hljs-string">'Build args is not set. Make sure you have following '</span> <span class="hljs-keyword">if</span> args.values.any?(&amp;<span class="hljs-symbol">:nil?</span>)
  args.map { <span class="hljs-params">|k, v|</span> <span class="hljs-string">"--dart-define=<span class="hljs-subst">#{k}</span>=\"<span class="hljs-subst">#{v}</span>\""</span> }.join(<span class="hljs-string">' '</span>)
<span class="hljs-keyword">end</span>


platform <span class="hljs-symbol">:ios</span> <span class="hljs-keyword">do</span>
  desc <span class="hljs-string">"Push a new beta build to TestFlight"</span>

  <span class="hljs-comment"># This lane launches github action that triggers </span>
  <span class="hljs-comment"># new actions to build each app separately</span>
  <span class="hljs-comment"># In case 1 app crashes - you dont want to have all following apps</span>
  <span class="hljs-comment"># to be skipped.</span>
  <span class="hljs-comment"># This also allows you to restart failed job later</span>
  lane <span class="hljs-symbol">:beta_all</span> <span class="hljs-keyword">do</span>
    api_endpoint = <span class="hljs-string">"https://api.github.com/repos/myrepo/myapp/actions/workflows/deploy_ios_project.yml/dispatches"</span>
    auth_token = ENV[<span class="hljs-string">"GIT_AUTHORIZATION"</span>]

    build_configurations.each <span class="hljs-keyword">do</span> <span class="hljs-params">|project_id, config|</span>
      payload = { <span class="hljs-string">"ref"</span> =&gt; <span class="hljs-string">"main"</span>, <span class="hljs-string">"inputs"</span> =&gt; { <span class="hljs-string">"project-id"</span> =&gt; project_id } }.to_json
      uri = URI.parse(api_endpoint)
      http = Net::HTTP.new(uri.host, uri.port)
      http.use_ssl = <span class="hljs-literal">true</span>
      request = Net::HTTP::Post.new(uri.request_uri, <span class="hljs-string">'Content-Type'</span> =&gt; <span class="hljs-string">'application/json'</span>, <span class="hljs-string">'Authorization'</span> =&gt; <span class="hljs-string">"Bearer <span class="hljs-subst">#{auth_token}</span>"</span>)
      request.body = payload
      response = http.request(request)

      <span class="hljs-keyword">unless</span> response.code == <span class="hljs-string">"204"</span>
        UI.error(<span class="hljs-string">"Failed to trigger GitHub Action: <span class="hljs-subst">#{response.code}</span> - <span class="hljs-subst">#{response.body}</span>"</span>)
      <span class="hljs-keyword">end</span>
    <span class="hljs-keyword">end</span>


  <span class="hljs-keyword">end</span>

  lane <span class="hljs-symbol">:beta</span> <span class="hljs-keyword">do</span>
    ENV[<span class="hljs-string">"APP_IDENTIFIER"</span>] = build_configuration[<span class="hljs-string">'APPSTORE_BUNDLE_ID'</span>]
    ENV[<span class="hljs-string">'APP_IDENTIFIERS'</span>] = build_configurations.values.map{ <span class="hljs-params">|e|</span> e[<span class="hljs-string">'APPSTORE_BUNDLE_ID'</span>] }.join(<span class="hljs-string">','</span>)
    keychain_name = TEMP_KEYCHAIN_USER
    keychain_password = TEMP_KEYCHAIN_PASSWORD
    setup_ci <span class="hljs-keyword">if</span> ENV[<span class="hljs-string">'CI'</span>]
    ensure_temp_keychain(keychain_name, keychain_password)

    <span class="hljs-comment"># IF BUILD_NUMBER is missing - we do not proceed</span>
    <span class="hljs-comment"># BUILD_NUMBER corresponds to CI build number</span>
    raise <span class="hljs-string">'BUILD_NUMBER is not set'</span> <span class="hljs-keyword">unless</span> ENV[<span class="hljs-string">'BUILD_NUMBER'</span>]

    increment_build_number(<span class="hljs-symbol">xcodeproj:</span> <span class="hljs-string">"Runner.xcodeproj"</span>, <span class="hljs-symbol">build_number:</span> ENV[<span class="hljs-string">'BUILD_NUMBER'</span>])

    update_info_plist( <span class="hljs-comment"># update app identifier string</span>
      <span class="hljs-symbol">plist_path:</span> <span class="hljs-string">"Runner/Info.plist"</span>,
      <span class="hljs-symbol">app_identifier:</span> build_configuration[<span class="hljs-string">'APPSTORE_BUNDLE_ID'</span>],
      <span class="hljs-symbol">display_name:</span> build_configuration[<span class="hljs-string">'COMPANY_NAME'</span>]
    )

    match(<span class="hljs-symbol">type:</span> <span class="hljs-string">'appstore'</span>, <span class="hljs-symbol">readonly:</span> <span class="hljs-literal">false</span>, <span class="hljs-symbol">git_basic_authorization:</span> Base64.strict_encode64(<span class="hljs-string">"name:<span class="hljs-subst">#{GIT_AUTHORIZATION}</span>"</span>), <span class="hljs-symbol">app_identifier:</span> build_configuration[<span class="hljs-string">'APPSTORE_BUNDLE_ID'</span>])

    update_project_provisioning(
      <span class="hljs-symbol">xcodeproj:</span> <span class="hljs-string">"Runner.xcodeproj"</span>,
      <span class="hljs-symbol">build_configuration:</span> <span class="hljs-string">"Release"</span>,
      <span class="hljs-symbol">target_filter:</span> <span class="hljs-string">"Runner"</span>,
      <span class="hljs-symbol">profile:</span> ENV[<span class="hljs-string">"sigh_<span class="hljs-subst">#{build_configuration[<span class="hljs-string">'APPSTORE_BUNDLE_ID'</span>]}</span>_appstore_profile-path"</span>]
    )

    <span class="hljs-comment"># In order to have own set of App Icons we need to generate IconSet for every client</span>
    filename = <span class="hljs-string">"<span class="hljs-subst">#{Time.now.to_i}</span>_logo.png"</span>
    <span class="hljs-comment"># Decode the base64 string and write it to a file</span>
    raise <span class="hljs-string">'LOGO_DATA is missing. Please encode your 1024*1024 logo to base64 and add to LOGO_DATA key'</span> <span class="hljs-keyword">unless</span> build_configuration[<span class="hljs-string">'LOGO_DATA'</span>]
    File.open(filename, <span class="hljs-string">"wb"</span>) <span class="hljs-keyword">do</span> <span class="hljs-params">|file|</span>
      file.write(Base64.decode64(build_configuration[<span class="hljs-string">'LOGO_DATA'</span>]))
    <span class="hljs-keyword">end</span>

    <span class="hljs-comment"># Create appicon for ios</span>
    base_dir = File.expand_path File.dirname(__FILE_<span class="hljs-number">_</span>)
    appicon_image_file = <span class="hljs-string">"<span class="hljs-subst">#{base_dir}</span>/<span class="hljs-subst">#{filename}</span>"</span>
    appicon(<span class="hljs-symbol">appicon_image_file:</span> appicon_image_file,
            <span class="hljs-symbol">appicon_path:</span> <span class="hljs-string">"Runner/Assets.xcassets"</span>,
            <span class="hljs-symbol">appicon_devices:</span> [<span class="hljs-symbol">:ipad</span>, <span class="hljs-symbol">:iphone</span>, <span class="hljs-symbol">:ios_marketing</span>])

    <span class="hljs-comment"># Create main_logo.png file which used as logo in app</span>
    Dir.chdir(<span class="hljs-string">"../../assets/images"</span>) <span class="hljs-keyword">do</span>
      File.open(<span class="hljs-string">'main_logo.png'</span>, <span class="hljs-string">"wb"</span>) <span class="hljs-keyword">do</span> <span class="hljs-params">|file|</span>
        file.write(Base64.decode64(build_configuration[<span class="hljs-string">'LOGO_DATA'</span>]))
      <span class="hljs-keyword">end</span>
    <span class="hljs-keyword">end</span>

    <span class="hljs-comment"># We need to run manual build since at the time of creating this lane there was no way to pass dart-define to xcode build</span>
    sh(<span class="hljs-string">"flutter pub get"</span>)
    sh(<span class="hljs-string">"dart pub global activate flutterfire_cli"</span>)
    <span class="hljs-keyword">begin</span>
      Dir.chdir <span class="hljs-string">"../.."</span> <span class="hljs-keyword">do</span>
        sh(<span class="hljs-string">"flutterfire configure --yes --project=<span class="hljs-subst">#{build_configuration[<span class="hljs-string">'PROJECT_ID'</span>]}</span> --web-app-id=<span class="hljs-subst">#{build_configuration[<span class="hljs-string">'APPSTORE_BUNDLE_ID'</span>]}</span> --macos-bundle-id=<span class="hljs-subst">#{build_configuration[<span class="hljs-string">'APPSTORE_BUNDLE_ID'</span>]}</span> --platforms=ios,android --ios-bundle-id=<span class="hljs-subst">#{build_configuration[<span class="hljs-string">'APPSTORE_BUNDLE_ID'</span>]}</span> --windows-app-id=<span class="hljs-subst">#{build_configuration[<span class="hljs-string">'APPSTORE_BUNDLE_ID'</span>]}</span> --ios-build-config=Release --ios-out=ios/Runner/GoogleService-Info.plist --android-package-name=<span class="hljs-subst">#{build_configuration[<span class="hljs-string">'APPSTORE_BUNDLE_ID'</span>]}</span> --token <span class="hljs-subst">#{ENV[<span class="hljs-string">'AUTH_TOKEN'</span>]}</span>"</span>)
      <span class="hljs-keyword">end</span>
    <span class="hljs-keyword">end</span>


    <span class="hljs-comment">### Set reversed client id to Info.plist. This needed for google sign in to work on ios platform</span>
    reversed_client_id = <span class="hljs-literal">nil</span>
    Dir.chdir <span class="hljs-string">"../"</span> <span class="hljs-keyword">do</span>
      google_service_info_path = <span class="hljs-string">"Runner/GoogleService-Info.plist"</span>
      google_service_info = Plist.parse_xml(google_service_info_path)

      reversed_client_id = google_service_info[<span class="hljs-string">"REVERSED_CLIENT_ID"</span>]
    <span class="hljs-keyword">end</span>

    update_info_plist( <span class="hljs-comment">#update CFBundleURLTypes</span>
      <span class="hljs-symbol">xcodeproj:</span> <span class="hljs-string">"Runner.xcodeproj"</span>,
      <span class="hljs-symbol">plist_path:</span> <span class="hljs-string">"Runner/Info.plist"</span>,
      <span class="hljs-symbol">block:</span> proc <span class="hljs-keyword">do</span> <span class="hljs-params">|plist|</span>
        url_types = plist[<span class="hljs-string">"CFBundleURLTypes"</span>]
        <span class="hljs-keyword">if</span> url_types &amp;&amp; !url_types.empty?
          url_type = url_types[<span class="hljs-number">0</span>]
          url_type[<span class="hljs-string">"CFBundleURLName"</span>] = build_configuration[<span class="hljs-string">'APPSTORE_BUNDLE_ID'</span>]
          <span class="hljs-keyword">if</span> url_type[<span class="hljs-string">"CFBundleURLSchemes"</span>] &amp;&amp; !url_type[<span class="hljs-string">"CFBundleURLSchemes"</span>].empty?
            url_type[<span class="hljs-string">"CFBundleURLSchemes"</span>][<span class="hljs-number">0</span>] = reversed_client_id
          <span class="hljs-keyword">end</span>
        <span class="hljs-keyword">end</span>
      <span class="hljs-keyword">end</span>
    )

    sh <span class="hljs-string">'pod install'</span>
    <span class="hljs-comment"># Run flutter build ios to generate proper flutter_export_environment.sh. No need to sign app yet</span>
    sh <span class="hljs-string">"flutter build ipa <span class="hljs-subst">#{build_args}</span> --no-codesign"</span>

    build_app(<span class="hljs-symbol">workspace:</span> <span class="hljs-string">"Runner.xcworkspace"</span>, <span class="hljs-symbol">scheme:</span> <span class="hljs-string">"Runner"</span>)
    upload_to_testflight(<span class="hljs-symbol">skip_waiting_for_build_processing:</span> <span class="hljs-literal">true</span>)
    delete_temp_keychain(keychain_name)
    puts <span class="hljs-string">"Build <span class="hljs-subst">#{lane_context[SharedValues::BUILD_NUMBER]}</span> was uploaded to Test Flight"</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>While this flow might look complex - it wasn't done all at once. I was doing step by step to get a build in the end. In your case you might not have firebase, or you might need an extra step to add something specific to your app.</p>
<h3 id="heading-running-in-ci">Running in CI</h3>
<p>In my Fastfile above you noticed I have 2 lanes, one to deploy app apps and 1 to deploy single app. That was done on purpose. It's simple - when I have new app version I want to deploy them all at once. But I also want to restart build if it fails for some reason(timeout, etc).</p>
<p>Note - GitHub actions uses 4x minutes to build iOS apps, so run debug your Fastfile locally until it completely succeeds.</p>
<h3 id="heading-conclusion">Conclusion</h3>
<p>I skipped part with App Store release, since it's just 2 lines of code calling according Lane. I also skipped google play, since its the same steps(just different on <code>build.gradle</code> configuration)</p>
<p>Setting up this pipeline helped me to offload all routine work to be done by CI and support team. When support team wants to add new client - they add it via web form and create according apps in appstore/google play. Whole process takes less than 30 minutes and has to be done only once. All next deploys are fully automatic.</p>
<p>With this flow we save hundreds of hours every month and the more clients we have more time we save.</p>
<p><strong>Follow me on</strong> <a target="_blank" href="https://x.com/_avdept"><strong>Twitter</strong></a> <strong>for more interesting content.</strong></p>
]]></content:encoded></item><item><title><![CDATA[Open Graph Protocol: Improving your SaaS visibility on social networks]]></title><description><![CDATA[Do you need it?
Yes. I, as indie hacker with no budget to promote my app updatify.io, use all available tools. OG protocol is one of them. It takes literally few minutes to add, but it greatly improves your visual visibility on social networks. Here'...]]></description><link>https://alexsinelnikov.blog/open-graph-protocol-improving-your-saas-visibility-on-social-networks</link><guid isPermaLink="true">https://alexsinelnikov.blog/open-graph-protocol-improving-your-saas-visibility-on-social-networks</guid><category><![CDATA[SEO]]></category><category><![CDATA[openGraph]]></category><category><![CDATA[SaaS]]></category><dc:creator><![CDATA[Alex Sinelnikov]]></dc:creator><pubDate>Mon, 19 Aug 2024 09:01:30 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1724057593395/7230145a-562f-4bdc-8ef2-eb4e7c86d13b.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3 id="heading-do-you-need-it">Do you need it?</h3>
<p>Yes. I, as indie hacker with no budget to promote my app <a target="_blank" href="https://updatify.io">updatify.io</a>, use all available tools. OG protocol is one of them. It takes literally few minutes to add, but it greatly improves your visual visibility on social networks. Here's few screenshots with/without and how it differs when you post links to your SaaS on social networks</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1724056850942/371f1eaa-9588-4def-b563-05119acaf234.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1724056790844/729031f0-6e07-4793-9937-ff5cb2ea1fe6.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-what-is-the-open-graph-protocol">What is the Open Graph Protocol?</h3>
<p>The Open Graph protocol is a set of meta tags that allows web developers to control how their web pages are represented when shared on social media platforms and other websites. Developed by Facebook in 2010, it has since become a widely adopted standard across the web.</p>
<h3 id="heading-why-is-it-useful-for-web-apps">Why is it Useful for Web Apps?</h3>
<ol>
<li><p><strong>Improved Social Sharing</strong>: When users share your web app's pages on social media, Open Graph tags ensure that the content appears in an attractive and informative way, increasing the likelihood of engagement.</p>
</li>
<li><p><strong>Better Brand Control</strong>: You can specify exactly which images, titles, and descriptions appear when your content is shared, maintaining consistent branding across platforms.</p>
</li>
<li><p><strong>Increased Click-through Rates</strong>: Well-crafted Open Graph tags can make your shared links more appealing, potentially increasing click-through rates from social media platforms.</p>
</li>
<li><p><strong>Enhanced SEO</strong>: While not a direct ranking factor, properly implemented Open Graph tags can indirectly benefit your SEO by improving social signals and increasing engagement.</p>
</li>
<li><p><strong>Cross-platform Compatibility</strong>: Many platforms beyond Facebook, including Twitter, LinkedIn, and Pinterest, recognize Open Graph tags, ensuring consistent presentation across various social networks.</p>
</li>
</ol>
<h3 id="heading-how-to-add-open-graph-tags-to-your-website">How to Add Open Graph Tags to Your Website</h3>
<p>To implement Open Graph tags on your website, you need to add specific <code>&lt;meta&gt;</code> tags within the <code>&lt;head&gt;</code> section of your HTML. Here are the basic Open Graph tags you should consider:</p>
<ol>
<li><p><strong>og:title</strong>: The title of your page.</p>
</li>
<li><p><strong>og:type</strong>: The type of your content (e.g., website, article, product).</p>
</li>
<li><p><strong>og:image</strong>: The URL of the image you want to appear when your content is shared.</p>
</li>
<li><p><strong>og:url</strong>: The canonical URL of your page.</p>
</li>
<li><p><strong>og:description</strong>: A brief description of your content.</p>
</li>
</ol>
<p>Here's an example of how to implement these tags:</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">property</span>=<span class="hljs-string">"og:title"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"Updatify | Instant product updates"</span>/&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">property</span>=<span class="hljs-string">"og:type"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"https://updatify.io"</span>/&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">property</span>=<span class="hljs-string">"og:image"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"https://link_to_image"</span>/&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">property</span>=<span class="hljs-string">"og:url"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"https://example.com/page"</span>/&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">property</span>=<span class="hljs-string">"og:description"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"A brief description of your page content."</span>/&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
</code></pre>
<p>You can also customize these values for each page separately. For ex if its blog - you might want to have different title or image or description.</p>
<h3 id="heading-twitter-specific-tags">Twitter specific tags</h3>
<p>Besides regular "og:" tags twitter has few extra tags you can use to improve visibility on twitter platform.</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"twitter:card"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"summary_large_image"</span> /&gt;</span> 
<span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"twitter:image"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"URL TO Image"</span>
&lt;<span class="hljs-attr">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"twitter:site"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"@handle for site in case you use separate account for your app on twitter"</span> /&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"twitter:creator"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"@handle of the author, usually yours"</span> /&gt;</span>
</code></pre>
<p>For "twitter:card" you can have either "summary" or "summary_large_image. Here on screenshot below you can see difference between "summary" and "summary_large_image" types of content</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1724056528516/e74eec06-9432-44cf-bc8f-42d0f23a0fa0.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-verification">Verification</h3>
<p>The easiest way to verify how your link looks on various platforms - simply post it. Some platforms provides preview even before its posted. Also if you even posted before - platform might have your link cached, so you might see old preview. To overcome this - simply add / to end of your link, so it would look like <a target="_blank" href="https://updatify.io/">https://updatify.io/</a> or even extra query param. It takes time for cache to expire.</p>
<h3 id="heading-summary">Summary</h3>
<p>By implementing Open Graph tags, you're taking a significant step towards improving your web app's visibility and engagement on social media platforms. As you saw on screenshots above - it changes how users see your links and you can provide more value to them even without making them click. It's also very important what kind of content provided. There are services which allows you to dynamically generate OG links based on your current page UI.</p>
<p>Follow me on <a target="_blank" href="http://x.com/_avdept">Twitter</a> where I provide more interesting content and my journey building <a target="_blank" href="https://updatify.io">updatify.io</a></p>
]]></content:encoded></item><item><title><![CDATA[Alpine.js for Ruby on Rails Developers: Simplifying Frontend Development]]></title><description><![CDATA[As a Ruby on Rails developer, you're used to the "convention over configuration" philosophy and the power of a full-stack framework. But when it comes to adding interactive elements to your frontend, you might feel torn between the simplicity of vani...]]></description><link>https://alexsinelnikov.blog/alpinejs-for-ruby-on-rails-developers-simplifying-frontend-development</link><guid isPermaLink="true">https://alexsinelnikov.blog/alpinejs-for-ruby-on-rails-developers-simplifying-frontend-development</guid><category><![CDATA[Ruby]]></category><category><![CDATA[alpinejs]]></category><category><![CDATA[Ruby on Rails]]></category><dc:creator><![CDATA[Alex Sinelnikov]]></dc:creator><pubDate>Thu, 15 Aug 2024 09:45:14 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1723714841340/a0bc4062-7122-402e-ae8c-47d098cc50da.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>As a Ruby on Rails developer, you're used to the "convention over configuration" philosophy and the power of a full-stack framework. But when it comes to adding interactive elements to your frontend, you might feel torn between the simplicity of vanilla JavaScript and the robustness of heavy JavaScript frameworks. Enter Alpine.js – a lightweight JavaScript framework that offers the perfect balance for Rails developers. In this article, we'll explore why Alpine.js is an excellent choice for Rails projects and why you might not need a separate UI framework at all.</p>
<h2 id="heading-the-rails-way-meets-modern-javascript">The Rails Way Meets Modern JavaScript</h2>
<p>Ruby on Rails has always been about developer productivity and happiness. Alpine.js shares this philosophy by providing a minimal yet powerful set of tools to create dynamic interfaces. Here's why it's a great fit for Rails developers:</p>
<ol>
<li><p><strong>Minimal Learning Curve</strong>: Alpine.js uses simple directives that feel familiar to anyone who has worked with Rails' ERB templates. You can start using it productively within minutes.</p>
</li>
<li><p><strong>Progressive Enhancement</strong>: Like Rails' Turbolinks, Alpine.js allows you to enhance your server-rendered HTML progressively. This aligns perfectly with the Rails approach to web development.</p>
</li>
<li><p><strong>No Build Step Required</strong>: Unlike many modern JavaScript frameworks, Alpine.js doesn't require a complex build process. You can include it with a single <code>&lt;script&gt;</code> tag, making it easy to integrate into your Rails asset pipeline.</p>
</li>
<li><p><strong>Plays Well with Turbolinks</strong>: Alpine.js works seamlessly with Turbolinks, maintaining state and event listeners even after page changes.</p>
</li>
<li><p><strong>Keeps Your Rails Views Clean</strong>: With Alpine.js, you can add interactivity directly in your HTML, keeping your JavaScript separate and your Rails views clean and readable.</p>
</li>
</ol>
<h2 id="heading-why-you-might-not-need-a-separate-ui-framework">Why You Might Not Need a Separate UI Framework</h2>
<p>When using Alpine.js with Rails, you may find that you don't need a separate UI framework at all. Here's why:</p>
<ol>
<li><p><strong>Rails Has Great Helpers</strong>: Rails comes with a rich set of view helpers that can generate most of the HTML structure you need. Alpine.js can then add the necessary interactivity.</p>
</li>
<li><p><strong>CSS Frameworks Are Enough</strong>: Most UI needs can be met with a good CSS framework like Bootstrap or Tailwind CSS. Alpine.js can handle the interactive parts, eliminating the need for JavaScript-heavy UI frameworks.</p>
</li>
<li><p><strong>Easier Maintenance</strong>: With fewer moving parts and external dependencies, your application becomes easier to maintain and upgrade over time.</p>
</li>
</ol>
<h2 id="heading-getting-started-with-alpinejs-in-your-rails-project">Getting Started with Alpine.js in Your Rails Project</h2>
<p>Adding Alpine.js to your Rails project is straightforward. Here's a quick guide:</p>
<ol>
<li><p>Add the Alpine.js script to your application layout:</p>
<pre><code class="lang-erb"><span class="xml"> <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> javascript_include_tag <span class="hljs-string">'https://cdn.jsdelivr.net/gh/alpinejs/alpine@v2.x.x/dist/alpine.min.js'</span>, <span class="hljs-symbol">defer:</span> <span class="hljs-literal">true</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span></span>
</code></pre>
</li>
<li><p>Start using Alpine.js directives in your views. For example, to create a simple dropdown:</p>
<pre><code class="lang-erb"><span class="xml"> <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">x-data</span>=<span class="hljs-string">"{ open: false }"</span>&gt;</span>
   <span class="hljs-tag">&lt;<span class="hljs-name">button</span> @<span class="hljs-attr">click</span>=<span class="hljs-string">"open = !open"</span>&gt;</span>Toggle Dropdown<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
   <span class="hljs-tag">&lt;<span class="hljs-name">ul</span> <span class="hljs-attr">x-show</span>=<span class="hljs-string">"open"</span> @<span class="hljs-attr">click.away</span>=<span class="hljs-string">"open = false"</span>&gt;</span>
     <span class="hljs-tag">&lt;<span class="hljs-name">li</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> link_to <span class="hljs-string">'Option 1'</span>, <span class="hljs-string">'#'</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">li</span>&gt;</span>
     <span class="hljs-tag">&lt;<span class="hljs-name">li</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> link_to <span class="hljs-string">'Option 2'</span>, <span class="hljs-string">'#'</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">li</span>&gt;</span>
     <span class="hljs-tag">&lt;<span class="hljs-name">li</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> link_to <span class="hljs-string">'Option 3'</span>, <span class="hljs-string">'#'</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">li</span>&gt;</span>
   <span class="hljs-tag">&lt;/<span class="hljs-name">ul</span>&gt;</span>
 <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
</code></pre>
</li>
</ol>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Alpine.js offers Ruby on Rails developers a pragmatic approach to adding interactivity to their applications. It aligns well with the Rails philosophy, integrates easily into existing projects, and often eliminates the need for heavier UI frameworks. By leveraging the power of Rails helpers, and Alpine.js for lightweight interactivity, you can create modern, responsive web applications without straying far from the Rails ecosystem you know and love. For more complex cases you can use Turbo frames to replace parts of UI and use alpine for interactivity, it works well together</p>
<p>Give Alpine.js a try in your next Rails project – you might be surprised at how much you can accomplish with so little.</p>
<p>Follow me on <a target="_blank" href="https://x.com/_avdept">Twitter</a> for more interesting content</p>
]]></content:encoded></item><item><title><![CDATA[Using component based approach in your Ruby on Rails app]]></title><description><![CDATA[Since the beginning of ages we were using partials in our Ruby on Rails apps. I think everyone remember good old app/views/shared folder where many of us kept parts of our UI. It was right approach when you wanted to reuse some parts of html. The onl...]]></description><link>https://alexsinelnikov.blog/using-component-based-approach-in-your-ruby-on-rails-app</link><guid isPermaLink="true">https://alexsinelnikov.blog/using-component-based-approach-in-your-ruby-on-rails-app</guid><category><![CDATA[Ruby on Rails]]></category><category><![CDATA[Design]]></category><category><![CDATA[UI]]></category><category><![CDATA[components]]></category><dc:creator><![CDATA[Alex Sinelnikov]]></dc:creator><pubDate>Wed, 07 Aug 2024 14:50:43 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1723041841961/e5628ec1-07cd-480f-a11c-0ad1d46492cc.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Since the beginning of ages we were using partials in our Ruby on Rails apps. I think everyone remember good old <code>app/views/shared</code> folder where many of us kept parts of our UI. It was right approach when you wanted to reuse some parts of html. The only problem with it - you can't really put logic there. Well you definitely can but you don't want your html file to be loaded with various if-else clauses.</p>
<p>That's where <a target="_blank" href="https://viewcomponent.org">https://viewcomponent.org</a> comes to stage. Essentially it's plain ruby object which contains some logic and html file which uses objects and logic from ruby class. Here's dropdown component I created some time ago.</p>
<pre><code class="lang-ruby"><span class="hljs-comment"># app/components/dropdown_component.rb</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">DropdownComponent</span> &lt; ViewComponent::Base</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">initialize</span><span class="hljs-params">(<span class="hljs-symbol">form:</span>, <span class="hljs-symbol">field_name:</span>, <span class="hljs-symbol">options:</span>, <span class="hljs-symbol">label:</span> <span class="hljs-literal">nil</span>, <span class="hljs-symbol">placeholder:</span> <span class="hljs-string">"Select an option"</span>)</span></span>
    @form = form
    @field_name = field_name
    @options = options
    @label = label <span class="hljs-params">||</span> field_name.to_s.humanize
    @placeholder = placeholder
  <span class="hljs-keyword">end</span>

  private

  <span class="hljs-keyword">attr_reader</span> <span class="hljs-symbol">:form</span>, <span class="hljs-symbol">:field_name</span>, <span class="hljs-symbol">:options</span>, <span class="hljs-symbol">:label</span>, <span class="hljs-symbol">:placeholder</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>followed by same name html file</p>
<pre><code class="lang-ruby">&lt;!-- app/components/dropdown_component.html.erb --&gt;
&lt;div x-data=<span class="hljs-string">"{ open: false, selected: '' }"</span> <span class="hljs-class"><span class="hljs-keyword">class</span>="<span class="hljs-title">relative</span>"&gt;</span>
  &lt;%= form.label field_name, label, <span class="hljs-class"><span class="hljs-keyword">class</span>: "<span class="hljs-title">block</span> <span class="hljs-title">text</span>-<span class="hljs-title">sm</span> <span class="hljs-title">font</span>-<span class="hljs-title">medium</span> <span class="hljs-title">text</span>-<span class="hljs-title">gray</span>-700 <span class="hljs-title">mb</span>-1" %&gt;</span>
  &lt;div @click.away=<span class="hljs-string">"open = false"</span> <span class="hljs-class"><span class="hljs-keyword">class</span>="<span class="hljs-title">relative</span>"&gt;</span>
    &lt;button @click=<span class="hljs-string">"open = !open"</span> type=<span class="hljs-string">"button"</span> 
            <span class="hljs-class"><span class="hljs-keyword">class</span>="<span class="hljs-title">relative</span> <span class="hljs-title">w</span>-<span class="hljs-title">full</span> <span class="hljs-title">bg</span>-<span class="hljs-title">white</span> <span class="hljs-title">border</span> <span class="hljs-title">border</span>-<span class="hljs-title">gray</span>-300 <span class="hljs-title">rounded</span>-<span class="hljs-title">md</span> <span class="hljs-title">shadow</span>-<span class="hljs-title">sm</span> <span class="hljs-title">pl</span>-3 <span class="hljs-title">pr</span>-10 <span class="hljs-title">py</span>-2 <span class="hljs-title">text</span>-<span class="hljs-title">left</span> <span class="hljs-title">cursor</span>-<span class="hljs-title">default</span> <span class="hljs-title">focus</span>:<span class="hljs-title">outline</span>-<span class="hljs-title">none</span> <span class="hljs-title">focus</span>:<span class="hljs-title">ring</span>-1 <span class="hljs-title">focus</span>:<span class="hljs-title">ring</span>-<span class="hljs-title">indigo</span>-500 <span class="hljs-title">focus</span>:<span class="hljs-title">border</span>-<span class="hljs-title">indigo</span>-500 <span class="hljs-title">sm</span>:<span class="hljs-title">text</span>-<span class="hljs-title">sm</span>"&gt;</span>
      &lt;span x-text=<span class="hljs-string">"selected || '&lt;%= placeholder %&gt;'"</span> <span class="hljs-class"><span class="hljs-keyword">class</span>="<span class="hljs-title">block</span> <span class="hljs-title">truncate</span>"&gt;&lt;/<span class="hljs-title">span</span>&gt;</span>
      &lt;span <span class="hljs-class"><span class="hljs-keyword">class</span>="<span class="hljs-title">absolute</span> <span class="hljs-title">inset</span>-<span class="hljs-title">y</span>-0 <span class="hljs-title">right</span>-0 <span class="hljs-title">flex</span> <span class="hljs-title">items</span>-<span class="hljs-title">center</span> <span class="hljs-title">pr</span>-2 <span class="hljs-title">pointer</span>-<span class="hljs-title">events</span>-<span class="hljs-title">none</span>"&gt;</span>
        &lt;svg <span class="hljs-class"><span class="hljs-keyword">class</span>="<span class="hljs-title">h</span>-5 <span class="hljs-title">w</span>-5 <span class="hljs-title">text</span>-<span class="hljs-title">gray</span>-400" <span class="hljs-title">xmlns</span>="<span class="hljs-title">http</span>://<span class="hljs-title">www</span>.<span class="hljs-title">w3</span>.<span class="hljs-title">org</span>/2000/<span class="hljs-title">svg</span>" <span class="hljs-title">viewBox</span>="0 0 20 20" <span class="hljs-title">fill</span>="<span class="hljs-title">currentColor</span>" <span class="hljs-title">aria</span>-<span class="hljs-title">hidden</span>="<span class="hljs-title">true</span>"&gt;</span>
          &lt;path fill-rule=<span class="hljs-string">"evenodd"</span> d=<span class="hljs-string">"M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z"</span> clip-rule=<span class="hljs-string">"evenodd"</span> /&gt;
        &lt;<span class="hljs-regexp">/svg&gt;
      &lt;/span</span>&gt;
    &lt;<span class="hljs-regexp">/button&gt;
    &lt;div x-show="open" 
         x-transition:enter="transition ease-out duration-100" 
         x-transition:enter-start="transform opacity-0 scale-95" 
         x-transition:enter-end="transform opacity-100 scale-100" 
         x-transition:leave="transition ease-in duration-75" 
         x-transition:leave-start="transform opacity-100 scale-100" 
         x-transition:leave-end="transform opacity-0 scale-95" 
         class="absolute z-10 mt-1 w-full bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm"&gt;
      &lt;% options.each do |option| %&gt;
        &lt;div @click="selected = '&lt;%= option[:label] %&gt;'; open = false" 
             class="cursor-default select-none relative py-2 pl-3 pr-9 hover:bg-indigo-600 hover:text-white"&gt;
          &lt;%= option[:label] %&gt;
        &lt;/div</span>&gt;
      &lt;% <span class="hljs-keyword">end</span> %&gt;
    &lt;<span class="hljs-regexp">/div&gt;
  &lt;/div</span>&gt;
  &lt;%= form.hidden_field field_name, <span class="hljs-string">'x-model'</span>: <span class="hljs-string">'selected'</span> %&gt;
&lt;<span class="hljs-regexp">/div&gt;</span>
</code></pre>
<p>and then, in order to use it simply call following anywhere in your html.</p>
<pre><code class="lang-ruby">&lt;%= render(DropdownComponent.new(
  <span class="hljs-symbol">form:</span> form,
  <span class="hljs-symbol">field_name:</span> <span class="hljs-symbol">:category</span>,
  <span class="hljs-symbol">options:</span> Category.all.map { <span class="hljs-params">|c|</span> { <span class="hljs-symbol">value:</span> c.id, <span class="hljs-symbol">label:</span> c.name } }
  <span class="hljs-symbol">label:</span> <span class="hljs-string">'Select a category'</span>,
  <span class="hljs-symbol">placeholder:</span> <span class="hljs-string">'Choose a category'</span>
)) %&gt;
</code></pre>
<p>Having just 1 component won't make much difference, but when you create/migrate majority of your components - it will start to save you time when start reusing components. Another benefit - is <strong>when you decide to restyle your app, you'll have to do it only once - in your components.</strong></p>
<p>You are not limited to just classes. You can also add JS code or anything else that handles your UI. I usually work with Alpine.JS and have components such as Dropdown, etc, where everything UI handled inside and the code above is a great example to show how its used.</p>
<p>Once you have many components you can add <a target="_blank" href="https://github.com/lookbook-hq/lookbook">https://github.com/lookbook-hq/lookbook</a> which is as name states - lookbook for your components. Instead of going to component sources, you can predefine a set of variants and just copy paste them when you need, just like for any UI kit.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>While both partials and ViewComponents serve the purpose of creating reusable UI elements, ViewComponents offer significant advantages in terms of organization, testability, and performance. By using ViewComponents, you can create more maintainable and scalable view layers in your Rails applications and using Lookbook you can save yourself time by copy pasting instead of writing from scratch.</p>
<p>Follow me on <a target="_blank" href="https://x.com/_avdept">Twitter</a> for more interesting stuff!</p>
]]></content:encoded></item><item><title><![CDATA[Desktop app development with flutter]]></title><description><![CDATA[For a long time desktop app development was hard and expensive. If you were creating app, you probably had to create 3 apps at once - windows, macOS and linux. There were tools such as QT, wxWidgets, copperspice and other frameworks which allowed to ...]]></description><link>https://alexsinelnikov.blog/desktop-app-development-with-flutter</link><guid isPermaLink="true">https://alexsinelnikov.blog/desktop-app-development-with-flutter</guid><category><![CDATA[Flutter]]></category><category><![CDATA[flutter desktop]]></category><category><![CDATA[desktop]]></category><dc:creator><![CDATA[Alex Sinelnikov]]></dc:creator><pubDate>Wed, 31 Jul 2024 05:41:06 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1722404393491/ec87d03a-5c94-4564-9cc8-2c28ea59a7f9.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>For a long time desktop app development was hard and expensive. If you were creating app, you probably had to create 3 apps at once - windows, macOS and linux. There were tools such as QT, wxWidgets, copperspice and other frameworks which allowed to build cross platform apps. They worked but until some extent. Some of them were missing features or was really complex in terms on implementing UI. Eventually there was Electron.js which had huge impact on creating desktop apps. Now you could really just using html/css/js implement pretty much any app. And many companies just dropped their web versions into electron making it desktop. That’s why you can use web version and then switch to same desktop using Slack. However because its made in html/css it usually lacks fluid animations, and generally UX feel more like its browser than a native app.</p>
<p>With flutter and you can build really performance apps thanks to the way flutter renders contents. Recently I’ve created a cross platform app and open sourced it - [<a target="_blank" href="http://github.com/avdept/JellyBoxPlayer">http://github.com/avdept/JellyBoxPlayer</a>] The app runs on iOS/android but also on macOS. Windows and linux version still in works as it miss 1 feature that macOS has. And now a bit of my experience using flutter to create both mobile and desktop app.</p>
<p>Originally idea was to have just mobile app, but having iPad design I decided to add desktop support. I only had few minor changes to do and this is the result -</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1722403881739/75e3b1b6-ed95-4d98-aaa2-4de2ac3f3867.png" alt class="image--center mx-auto" /></p>
<p>Tools and packages Turned out there’s only one 3rd party package I used - <a target="_blank" href="https://pub.dev/packages/responsive_builder">https://pub.dev/packages/responsive_builder</a> which provides you a tools to check what kind of device you run it on, screen breakpoints, etc. So my code started to look like following</p>
<pre><code class="lang-dart">ScrollablePageScaffold(
      useGradientBackground: <span class="hljs-keyword">true</span>,
      navigationBar: PreferredSize(
        preferredSize: Size.fromHeight(_isMobile ? <span class="hljs-number">60</span> : <span class="hljs-number">100</span>),
        child: Padding(
          padding: EdgeInsets.symmetric(horizontal: _isMobile ? <span class="hljs-number">16</span> : <span class="hljs-number">30</span>),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              _pageViewToggle(),
              _filterButton(),
            ],
          ),
        ),
      ),
      loadMoreData: loadMore,
      contentPadding: EdgeInsets.only(
        left: _isMobile ? <span class="hljs-number">16</span> : <span class="hljs-number">30</span>,
        right: _isMobile ? <span class="hljs-number">16</span> : <span class="hljs-number">30</span>,
        bottom: <span class="hljs-number">30</span>,
      ),
)
</code></pre>
<p>Depending on if it’s mobile or desktop I was able to use different paddings. Another case - hide/show element completely depending on platform</p>
<pre><code class="lang-dart">Visibility(
  visible: _isDesktop,
  child: CustomNavigationRail()
 )
</code></pre>
<p>One of the issues I had - flutter doesn’t allow setting min size for window when on desktop platforms. To fix this I found a window manager package and came up with this solution:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">if</span> (Platform.isMacOS || Platform.isLinux || Platform.isWindows) {
    <span class="hljs-keyword">await</span> windowManager.ensureInitialized();

    <span class="hljs-keyword">const</span> windowOptions = WindowOptions(
      size: Size(<span class="hljs-number">1440</span>, <span class="hljs-number">1000</span>),
      minimumSize: Size(<span class="hljs-number">1280</span>, <span class="hljs-number">800</span>),
      center: <span class="hljs-keyword">true</span>,
      backgroundColor: Colors.transparent,
      skipTaskbar: <span class="hljs-keyword">false</span>,
      titleBarStyle: TitleBarStyle.hidden,
    );

    <span class="hljs-keyword">await</span> windowManager.waitUntilReadyToShow(
      windowOptions,
      () <span class="hljs-keyword">async</span> {
        <span class="hljs-keyword">await</span> windowManager.<span class="hljs-keyword">show</span>();
        <span class="hljs-keyword">await</span> windowManager.focus();
      },
    );
  }
</code></pre>
<p>The idea here to set size and also min size, but also prevent flickering when app starts, for this you got to wait until window is ready and then you can show and focus it.</p>
<h3 id="heading-the-missing-tools">The missing tools</h3>
<p>Unfortunately flutter doesn’t know how to work with global keybindings. In my case it was media keys. There aren’t many available packages so I created my own. Since my main platform is macOS I started with it. I found MediaKeyTap swift package and based on that build flutter plugin. Main functions looks like below.</p>
<pre><code class="lang-swift"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">register</span><span class="hljs-params">(with registrar: FlutterPluginRegistrar)</span></span> {
    <span class="hljs-keyword">let</span> channel = <span class="hljs-type">FlutterMethodChannel</span>(name: <span class="hljs-string">"mediakeys_proxy"</span>, binaryMessenger: registrar.messenger)
    <span class="hljs-keyword">let</span> instance = <span class="hljs-type">MediakeysProxyPlugin</span>(channel: channel)
    registrar.addMethodCallDelegate(instance, channel: channel)
    instance.startMonitoring()
    <span class="hljs-keyword">let</span> options: <span class="hljs-type">NSDictionary</span> = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() <span class="hljs-keyword">as</span> <span class="hljs-type">String</span> : <span class="hljs-literal">true</span>]
    <span class="hljs-type">AXIsProcessTrustedWithOptions</span>(options)
  }

  <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">handle</span><span class="hljs-params">(mediaKey: MediaKey, event: KeyEvent)</span></span> {

    <span class="hljs-keyword">switch</span> mediaKey {
    <span class="hljs-keyword">case</span> .playPause:
        <span class="hljs-keyword">self</span>.sendEventToFlutter(eventName: <span class="hljs-string">"playPause"</span>)
    <span class="hljs-keyword">case</span> .previous, .rewind:
        <span class="hljs-keyword">self</span>.sendEventToFlutter(eventName: <span class="hljs-string">"prev"</span>)
    <span class="hljs-keyword">case</span> .next, .fastForward:
        <span class="hljs-keyword">self</span>.sendEventToFlutter(eventName: <span class="hljs-string">"next"</span>)
    }
  }
</code></pre>
<h3 id="heading-conclusion">Conclusion</h3>
<p>Over last few years flutter became really mature framework. Out of box it covers 90% possible use cases and it’s possible to use it without any 3rd party packages, but they surely make some things easier. Desktop platform still missing few features, but because of how flutter plugins works its really easy to come up with custom plugin solution to solve your problem.</p>
<p>Follow me on <a target="_blank" href="https://x.com/_avdept">X(Twitter)</a></p>
]]></content:encoded></item><item><title><![CDATA[What is self hosting and how your software engineer career can benefit from it?]]></title><description><![CDATA[This might not be the most popular topic during your coffee breaks, but it’s something every engineer should try at least once. Here’s why...
What is Self-Hosting and How Does It Work?
When people hear "self-hosting," they often imagine server racks ...]]></description><link>https://alexsinelnikov.blog/what-is-self-hosting-and-how-your-software-engineer-career-can-benefit-from-it</link><guid isPermaLink="true">https://alexsinelnikov.blog/what-is-self-hosting-and-how-your-software-engineer-career-can-benefit-from-it</guid><category><![CDATA[SelfHosting]]></category><category><![CDATA[Career]]></category><category><![CDATA[Open Source]]></category><category><![CDATA[software development]]></category><dc:creator><![CDATA[Alex Sinelnikov]]></dc:creator><pubDate>Mon, 15 Jul 2024 09:04:16 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/_MauPmUJJ08/upload/b200c4fb35803a947b297533dba630c3.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This might not be the most popular topic during your coffee breaks, but it’s something every engineer should try at least once. Here’s why...</p>
<h3 id="heading-what-is-self-hosting-and-how-does-it-work">What is Self-Hosting and How Does It Work?</h3>
<p>When people hear "self-hosting," they often imagine server racks filled with expensive equipment that offer little to no real benefits. But that’s not the case.</p>
<p>Self-hosting means you host cloud applications yourself on your own (or rented) hardware. This can be as affordable as a DigitalOcean droplet or another VPS provider. Only you and the people you grant access to can use the software. With cloud providers, everything is straightforward—you pay a monthly subscription of $5-10 and get the hardware. True self-hosting, however, means you manage everything on your own hardware.</p>
<p>To get started, you don't need anything beyond your own computer. If you want more stability, consider a Raspberry Pi. This inexpensive microcomputer can run Linux distributions (like Raspbian) and has enough computing power to run multiple apps with no problems. It’s energy-efficient and portable, perfect for taking your self-hosted software wherever you go.</p>
<p>There are plenty of different software you can self-host. To start, check out this awesome <a target="_blank" href="https://github.com/awesome-selfhosted/awesome-selfhosted">GitHub curated list of apps</a>.</p>
<h3 id="heading-my-self-hosting-journey">My Self-Hosting Journey</h3>
<p>When I started, I went with Pi-hole, an app that blocks ads across your entire network. Later, I added several Servarr apps to help manage my music collection, which I’ve been building since the mid-2000s. To play my music, I also installed Jellyfin. Nowadays, I’ve developed a client app for Jellyfin called <a target="_blank" href="https://github.com/avdept/JellyBoxPlayer">JellyBox</a>, which is also open source.</p>
<p>As of now I run about 20 different apps, which handles content playing and managing, backups, content creation and various networking tools. I managed to get myself used HP DL 380G9 Server which has enough bays for storage and enough computing power to run everything I have with no issues, but be careful - this route can be a rabbit hole, and you end up with 32U rack cabinet with a bunch of hardware.</p>
<h3 id="heading-why-should-you-as-a-software-engineer-care">Why Should You, as a Software Engineer, Care?</h3>
<ol>
<li><p><strong>Hands-on Docker Experience</strong>: Most self-hosted apps have Docker images, so you can install them quickly without needing additional OS packages.</p>
</li>
<li><p><strong>Command Line Proficiency</strong>: You’ll get more comfortable with the command line, reading logs, and troubleshooting.</p>
</li>
<li><p><strong>Networking Knowledge</strong>: Learn about networking, reverse proxies (like Traefik, which I discovered through self-hosting), and security by securing your services.</p>
</li>
<li><p><strong>Problem-Solving Skills</strong>: Discover various solutions for different problems, potentially saving you money.</p>
</li>
<li><p><strong>Open Source Contributions</strong>: Many self-hosted apps are open source. Contributing to these projects can enhance your CV and help you land better job opportunities.</p>
</li>
</ol>
<h3 id="heading-the-business-benefits">The Business Benefits</h3>
<p>Self-hosting isn’t just for personal use. At my company, <a target="_blank" href="https://prodigytech.dev">ProdigyTech</a>, I’ve installed several apps that save us significant money:</p>
<ul>
<li><p><strong>Mattermost</strong>: A Slack alternative that’s been working perfectly for almost 1.5 years.</p>
</li>
<li><p><strong>Plausible</strong>: A website analytics service.</p>
</li>
<li><p><strong>Gitea</strong>: For git repository management and CI runners.</p>
</li>
<li><p><strong>Plane</strong>: A Jira-like project management tool.</p>
</li>
</ul>
<p>These tools run on a $20 DigitalOcean droplet and save us about $700 monthly in subscription fees.</p>
]]></content:encoded></item><item><title><![CDATA[How we built mobile MVP app for free]]></title><description><![CDATA[At ProdigyTech, we prioritize our clients' success and sometimes offer free services when we see opportunities to enhance their existing products.
While working as a Ruby on Rails engineer on the StatusGator app, I also engaged in several side projec...]]></description><link>https://alexsinelnikov.blog/how-we-built-mobile-mvp-app-for-free</link><guid isPermaLink="true">https://alexsinelnikov.blog/how-we-built-mobile-mvp-app-for-free</guid><category><![CDATA[mvp]]></category><category><![CDATA[SaaS]]></category><category><![CDATA[Mobile Development]]></category><dc:creator><![CDATA[Alex Sinelnikov]]></dc:creator><pubDate>Thu, 11 Jul 2024 09:30:29 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/AK3BXL-1AFw/upload/a7a60502ed3977aeaa4e08b3a690f1ab.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>At <a target="_blank" href="https://prodigytech.dev">ProdigyTech</a>, we prioritize our clients' success and sometimes offer free services when we see opportunities to enhance their existing products.</p>
<p>While working as a Ruby on Rails engineer on the <a target="_blank" href="https://statusgator.com">StatusGator</a> app, I also engaged in several side projects using Flutter. For those unfamiliar, Flutter is a powerful framework that allows you to build a mobile app once and deploy it on both iOS and Android platforms. This means you save on costs by not needing separate engineers for each platform. Changes made on one platform are reflected on the other, eliminating the need to duplicate efforts. I even built an open-source desktop client for Jellyfin called <a target="_blank" href="https://github.com/avdept/JellyBoxPlayer">Jellybox Player</a> using Flutter, demonstrating its versatility.</p>
<p>Drawing from this experience, I recognized that the features we were developing for StatusGator could also be effectively utilized via a mobile app, particularly for delivering push notifications. I took the initiative to propose this idea during one of our weekly meetings. It wasn't a detailed plan, just a simple suggestion on how our clients could benefit from a mobile app.</p>
<p>Due to my full-time job, I worked on this project during weekends, gradually building a Minimum Viable Product (MVP). Without a dedicated designer, I leveraged Flutter’s Material UI to replicate our web interface, making necessary adjustments for a mobile-friendly experience. The result was a functional MVP.</p>
<p>Once the MVP was ready, we distributed it via TestFlight within our team for internal testing and feedback. During this period, we also began mentioning the upcoming app in our sales calls, which generated significant interest from potential clients. The promise of a mobile app became a strong selling point, helping us secure additional deals even before the app was officially released.</p>
<p>What lessons did I learn from this experience? As a contractor, you are fully responsible for the impressions your client gets when working with you. One way to build long-term relationships is by offering additional value without extra cost. This approach pays off in the long run.</p>
<p>As a product owner, never underestimate the value of mobile apps. Regardless of the industry, everyone uses smartphones. Mobile apps often offer a quicker, more efficient way to perform tasks compared to websites, sometimes significantly boosting productivity.</p>
<p>For outsourcing companies, it's crucial to encourage engineers to be proactive. When engineers take initiative, everyone benefits.</p>
<p>In conclusion, any working business can benefit from a mobile app, and it's never too late to create one. Plus, building an MVP doesn't have to be expensive—it can cost as little as $2,000 to $3,000. With the right tools and approach, you can create something valuable without breaking the bank.</p>
]]></content:encoded></item><item><title><![CDATA[Going for #1 ProductHunt]]></title><description><![CDATA[Going financial....
About a year ago I've joined a new startup called Cashews. I really liked the idea of providing users easy way to know how many can still they spend today without going broke by next salary, however I had 0 idea how should it work...]]></description><link>https://alexsinelnikov.blog/going-for-1-producthunt</link><guid isPermaLink="true">https://alexsinelnikov.blog/going-for-1-producthunt</guid><category><![CDATA[product hunt]]></category><category><![CDATA[Startups]]></category><category><![CDATA[Mobile Development]]></category><category><![CDATA[fintech]]></category><dc:creator><![CDATA[Alex Sinelnikov]]></dc:creator><pubDate>Mon, 25 Jul 2022 11:18:34 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/unsplash/fmCOUAj1QHk/upload/v1662529663283/Jinw9FGvz.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1658495212127/CKSfXzCug.png" alt="Screenshot 2022-07-22 at 16.06.37.png" /></p>
<h1 id="heading-going-financial">Going financial....</h1>
<p>About a year ago I've joined a new startup called <a target="_blank" href="https://cashews.finance">Cashews</a>. I really liked the idea of providing users easy way to know how many can still they spend today without going broke by next salary, however I had 0 idea how should it work, since I dont have any financial education(and well, I've been broke by end of month many times) that seemed challenging to me and so I said - "Challenge accepted!"</p>
<p>Because it was a startup - we didn't have big budgets, team of managers, researchers, and all decisions were done by a team of 3(Hi Jonathan and Romano) and it resulted in really successful outcome, we were awarded with <a target="_blank" href="https://www.producthunt.com/products/cashews-for-ios-android?utm_source=badge-top-post-badge&amp;utm_medium=badge#cashews-for-ios-android">#1 Product of the day on PH</a> badge.</p>
<p>What did #1 give us? First and most important - ProductHunt is a community of early adopters and people who wants to try new products before they reach wide audience or people who looking for better tools that currently exists on market. That means it's easy to get real feedback from users, without actually blaming you for doing something incorrectly or just showing wrong results. That also helped us to fix some bugs we weren't aware of, change UX to be even more user-friendly and generally it helped us to understand how awesome the Cashews is.</p>
<p>I was really anxious on how will it go, because the math behind calculations really complicated and the numbers not always obvious due to many variables involved. Surely, before releasing on ProductHunt we had long beta with lots of changes to our calculations, polishing the UI and UX(the UI/UX on most financial apps kinda complicated and usually flooded by features you most likely won't be using since the beginning). One of our beta users even reported that we helped him to find subscription which he wasn't using, but somehow it wasn't cancelled(that was a big win for us at that period)</p>
<h2 id="heading-how-did-we-make-it">How did we make it?</h2>
<p>I had no ideas how to calculate user's spendable per period. First iteration was straightforward - take all user's income, take all expenses, subtract and divide by amount of days till end of month. But then - you understand that some people gets paid twice a month, or once in few months. Some people gets paid weekly. Some people gets paid in mid of month. First part of solution was - find proper, realistic spendings dataset. There are few publicly available online with depersonalised transactions. We tuned formula - however there still were edge cases when it didn't work. Another set of discussions, tuning and we end up with few different formulas for different user spending profiles. Looks good(more or less), but then we launch our closed beta, get some real user's and find out how differently people use their money and even how do they store them on their accounts or even having different banks for different purposes, such as - Bank1 for mortgage, Bank2 for leisure, Bank3 for savings, etc.</p>
<p>All previous work was discarded. We started over. Google spreadsheet was our best friend for couple weeks of trials and errors. That resulted in some very promising values that seemed to be correct according to our hand calculations. What also helped us a lot - visual debuggers. While working with list of database rows - its really hard to see whole picture of  calculations. Our debuggers were simple SPAs which would actually breakdown calculations into smaller chunks, so we could understand where does the error comes from without need to export rows into spreadsheet and spending hours trying to understand why is it $1 instead of $25.</p>
<p>After months of work we finally got what we wanted. But it's not finished yet. With every new user we get more data. While 90% users have predictable spending patterns, rest 10% are different. And we constantly work to improve our calculations to cover as many spending patterns as possible. I won't go into details, but current calculations consists of more than 20 steps, and its growing up as we find new patterns and trying to make calculations more correct according to user's spending approach and their lifestyle.</p>
<h2 id="heading-few-technical-and-not-so-lessons-ive-learned-working-on-cashews">Few technical and not so lessons I've learned working on Cashews</h2>
<ul>
<li><p>Always use objects for api responses instead of arrays, even for most basic api endpoints, such as:</p>
<pre><code>{
<span class="hljs-string">'items'</span>: [<span class="hljs-string">'A'</span>, <span class="hljs-string">'B'</span>, <span class="hljs-string">'C'</span>]
}
</code></pre><p>You never know when your client app will demand more data or just different format. Having object helps you to return even different responses depending on client app version.</p>
</li>
<li><p>Always process requests in user's timezone. Make sure every request has user timezone, or you end up having many complaints that data shows incorrectly(most databases works in UTC timezone).</p>
</li>
<li><p>Do not spend time on database query optimisations. Most likely database schema will change multiple times before you get final result. Save your time.</p>
</li>
<li><p>I don't like to be Cpt. Obvious, but - write tests, cover api responses format, make sure that client will always get same response format as before.</p>
</li>
<li><p>Do not be afraid to checkout your 2 days of work in favour of better solution. While working on cashews we discarded at least 2 different formulas which costed us literally weeks or even months in development time.</p>
</li>
<li><p>Ruby on Rails still good choice for startups. Fast iterations to verify your ideas. Most likely you won't face any performance issues till you get your first 1M users.</p>
</li>
<li><p>Google spreadsheet usually can save a lot of time when prototyping calculations.</p>
</li>
<li><p>Do not overcomplicate your infrastructure until you get final result. Heroku will work well for most startups.</p>
</li>
</ul>
<h2 id="heading-summary">Summary</h2>
<p>Am I happy with the final result? Yes, definitely. I've learned a lot not just about math and finances, but also how to plan your work when you not always know how to achieve best result. There's still lots of work to be done to cover as much users as possible, but the foundation we created makes it easy to introduce new variables into final calculation, add new integrations and basically just grow app but be bigger than just spendings predicting.</p>
]]></content:encoded></item><item><title><![CDATA[Introduction]]></title><description><![CDATA[Have you even been in situation when you have blog, blog1, blog_api, blog_fe projects stored on your computer ? And each of them has different template, built using different language/framework ? Most of them like 40-50% done, but not ready for deplo...]]></description><link>https://alexsinelnikov.blog/introduction</link><guid isPermaLink="true">https://alexsinelnikov.blog/introduction</guid><category><![CDATA[introduction]]></category><category><![CDATA[Intro]]></category><category><![CDATA[first post]]></category><dc:creator><![CDATA[Alex Sinelnikov]]></dc:creator><pubDate>Fri, 24 Jun 2022 09:17:35 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/unsplash/qzgN45hseN0/upload/v1656430086507/3Yx6r0Vs3.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Have you even been in situation when you have <code>blog</code>, <code>blog1</code>, <code>blog_api</code>, <code>blog_fe</code> projects stored on your computer ? And each of them has different template, built using different language/framework ? Most of them like 40-50% done, but not ready for deployment ? If you've been there - then we have something in common. Lets meet - my name is Alex and I'm engineer who never had a blog, but always wanted to!</p>
<p>Now, when I pulled your attention, let me introduce myself. I'm engineer with over 10 years production experience in building different products, starting from small e-commerce and up to enterprise grade CRMs. I do not call myself  engineer because I tend to use "most appropriate" technology that solves client's current problem in most efficient way. I know, that sounds like in those $5 online courses, but reality is - if you want to get paid more - be ready to adapt to client's needs, his team, his requests and even how exactly he approaches to his business.</p>
<p>For past 6 years I've been working as freelancer or leading small teams in cases where there are more work that I have time to work on it. During this time I did a lot of mistakes, learned tons of knowledge that you won't find anywhere online nor any courses will teach you.</p>
<p>The intention of this blog - to share my experience, not just coding, but also how do I approach to work, how do I do preproduction work and why it's necessary to do it, especially if you work alone or in small team. How to properly communicate with your client and get more work in future and many other things we all encounter in our everyday work</p>
<p>First blog post cannot go without list of daily technologies I use, so I'll share just so you could know, there will be some interesting stuff.</p>
<ul>
<li>Go</li>
<li>Elixir</li>
<li>Ruby(rails)</li>
<li>Ember, Vue, Angular, React</li>
<li>Flutter</li>
<li>A bit of embedded development with STM32, NXP, NRF</li>
</ul>
<p>Follow my blog to get some really interesting insights and useful knowledge which you can use in your everyday work!</p>
]]></content:encoded></item></channel></rss>