<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Gerardo Andrés Ruiz Castillo</title>
    <description>The latest articles on DEV Community by Gerardo Andrés Ruiz Castillo (@geanruca).</description>
    <link>https://dev.to/geanruca</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2362403%2F23c8e30e-3559-4269-9287-e8c095c40fae.webp</url>
      <title>DEV Community: Gerardo Andrés Ruiz Castillo</title>
      <link>https://dev.to/geanruca</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/geanruca"/>
    <language>en</language>
    <item>
      <title>Beyond Hardcoding: Ensuring Correct Payment Units in Reimpact's Platform</title>
      <dc:creator>Gerardo Andrés Ruiz Castillo</dc:creator>
      <pubDate>Mon, 25 May 2026 06:00:05 +0000</pubDate>
      <link>https://dev.to/geanruca/beyond-hardcoding-ensuring-correct-payment-units-in-reimpacts-platform-2dp4</link>
      <guid>https://dev.to/geanruca/beyond-hardcoding-ensuring-correct-payment-units-in-reimpacts-platform-2dp4</guid>
      <description>&lt;p&gt;On Reimpact's platform, we recently addressed a critical display inaccuracy related to service payment units. While seemingly minor, such details are crucial for user trust and clarity, especially in quoting and billing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Hardcoded Payment Units
&lt;/h2&gt;

&lt;p&gt;Initially, our system for displaying service quotes relied on a hardcoded payment unit, specifically 'UF/h'. This unit, representing 'Unidad de Fomento per hour', was hardcoded directly into the frontend display logic. While 'UF/h' was applicable for many services, it created a significant inconsistency for other services that operated on different billing cycles or units, such as 'UF/día' (Unidad de Fomento per day). This discrepancy meant that regardless of a service's actual database-defined payment unit, the user interface would always show 'UF/h'. This led to confusion and potential misinterpretations of service costs, undermining the accuracy our users expect.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix: Dynamic Unit Retrieval
&lt;/h2&gt;

&lt;p&gt;To rectify this, the solution was straightforward yet impactful: instead of embedding the unit directly in the frontend, we opted to dynamically fetch the correct &lt;code&gt;payment_unit&lt;/code&gt; from the database, associated with each specific service. This unit is now passed from the server-side controller to the view, ensuring that what the user sees precisely matches the service's actual billing structure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation Details
&lt;/h2&gt;

&lt;p&gt;The core of the fix involved modifying the controller to retrieve the &lt;code&gt;payment_unit&lt;/code&gt; alongside other service data and passing it to the view. The view then simply renders this dynamic value.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In your PHP Controller (e.g., ServiceQuoteController.php)&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ServiceQuoteController&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;showServiceQuote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$serviceId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Simulate fetching service data from a database or service layer&lt;/span&gt;
        &lt;span class="nv"&gt;$serviceData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;serviceRepository&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$serviceId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Example dynamic service data&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$serviceId&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$serviceData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'name'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Consulting'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'base_rate'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'payment_unit'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'UF/h'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;elseif&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$serviceId&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$serviceData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'name'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Project Management'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'base_rate'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'payment_unit'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'UF/día'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$serviceData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'name'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'General Service'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'base_rate'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'payment_unit'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'UF/unit'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'service.quote'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'service'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$serviceData&lt;/span&gt;
        &lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// In your Blade/PHP View (e.g., resources/views/service/quote.blade.php)&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;h1&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nc"&gt;Quote&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;$service&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'name'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;h1&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nc"&gt;Rate&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;$service&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'base_rate'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;strong&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;$service&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'payment_unit'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;strong&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;small&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nc"&gt;The&lt;/span&gt; &lt;span class="n"&gt;payment&lt;/span&gt; &lt;span class="n"&gt;unit&lt;/span&gt; &lt;span class="n"&gt;is&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="n"&gt;dynamically&lt;/span&gt; &lt;span class="n"&gt;displayed&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;small&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This simple adjustment removes the hardcoded dependency, making the system more robust and adaptable to various service types and billing models. The view no longer assumes a unit but rather uses the precise information provided by the backend.&lt;/p&gt;

&lt;h2&gt;
  
  
  Outcome and Benefits
&lt;/h2&gt;

&lt;p&gt;This change provides a significantly more accurate and flexible user experience. Each service's quoting now correctly reflects its unique payment unit, enhancing clarity and reducing potential misunderstandings for our users. It's a small but significant step towards greater precision and user trust in our platform's displayed information, ensuring that our interface accurately represents the underlying business logic and data.&lt;/p&gt;

</description>
      <category>php</category>
      <category>frontend</category>
      <category>database</category>
    </item>
    <item>
      <title>Enhancing User Surveys: From Single Choice to Multi-Select and Reliable Notifications</title>
      <dc:creator>Gerardo Andrés Ruiz Castillo</dc:creator>
      <pubDate>Fri, 22 May 2026 06:08:04 +0000</pubDate>
      <link>https://dev.to/geanruca/enhancing-user-surveys-from-single-choice-to-multi-select-and-reliable-notifications-g22</link>
      <guid>https://dev.to/geanruca/enhancing-user-surveys-from-single-choice-to-multi-select-and-reliable-notifications-g22</guid>
      <description>&lt;p&gt;Delivering a seamless and informative user experience is paramount for any digital product. Recently, the &lt;code&gt;Reimpact/platform&lt;/code&gt; project, specifically its &lt;code&gt;landing&lt;/code&gt; component, underwent significant updates to its user survey system. These changes focused on improving data capture flexibility and ensuring users received promised follow-up communications.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adapting Survey Input for Richer Data
&lt;/h2&gt;

&lt;p&gt;Initially, key questions within our survey, such as the &lt;code&gt;product_type&lt;/code&gt; inquiry, relied on a single-choice selection. While simple, this approach proved too restrictive when users needed to indicate multiple relevant options. For instance, if a product could fall into several categories, a single choice didn't accurately reflect their situation. This led to incomplete data and user frustration.&lt;/p&gt;

&lt;p&gt;The solution involved transforming these single-choice fields into multi-select checkboxes. This change allowed users to choose all applicable options, providing a much richer and more accurate dataset. Alongside this, we introduced new options, such as "textiles," to broaden the scope of choices and updated question labels for clarity. Crucially, the backend logic for processing these responses also needed to evolve. Instead of direct equality checks, our outcome rules were updated to use &lt;code&gt;contains&lt;/code&gt; or &lt;code&gt;not_contains&lt;/code&gt; operators to properly evaluate array-based answers.&lt;/p&gt;

&lt;p&gt;Here's a simplified example of how you might validate and process multi-choice input in a PHP application:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In a Form Request or Controller validation logic&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'product_type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'required'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'array'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'min:1'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="s1"&gt;'product_type.*'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'string'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Rule&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;in&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'electronics'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'textiles'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'packaging'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'other'&lt;/span&gt;&lt;span class="p"&gt;])],&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// In your processing logic, checking conditions based on multi-choice answers&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;in_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'textiles'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$surveyResult&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;product_type&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Apply specific logic for textile products&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;handleTextileProductLogic&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;array_intersect&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'electronics'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'packaging'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nv"&gt;$surveyResult&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;product_type&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Apply logic for products in specific categories&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;handleSpecificCategoryLogic&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Fulfilling the Promise: Emailing Survey Results
&lt;/h2&gt;

&lt;p&gt;A critical element of user trust is following through on commitments. Our survey interface explicitly informed users that they would receive their results via email upon submission. However, this email notification was never actually implemented, creating a disconnect between expectation and reality.&lt;/p&gt;

&lt;p&gt;To rectify this, a new &lt;code&gt;SurveyResultMail&lt;/code&gt; Mailable class was introduced, complete with a dedicated email template for presenting the survey outcomes. The sending mechanism was then integrated into the &lt;code&gt;BlogController::submitSurvey()&lt;/code&gt; method, responsible for handling survey submissions. A &lt;code&gt;try/catch&lt;/code&gt; block was wrapped around the email sending operation to ensure robustness. This critical detail means that even if the email sending process encounters an unexpected error (e.g., mail server issues), the user's on-screen experience is unaffected, and they still see their results immediately, preventing a broken user flow.&lt;/p&gt;

&lt;p&gt;This approach prioritizes user experience while providing a fallback for potential system failures:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Mail\SurveyResultMail&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Support\Facades\Mail&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Inside your BlogController::submitSurvey method&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;submitSurvey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ... process survey data ...&lt;/span&gt;
    &lt;span class="nv"&gt;$surveyResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;saveSurveyData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Mail&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;user&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SurveyResultMail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$surveyResult&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="c1"&gt;// Log success or additional analytics&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Log the email sending error but do not block user flow&lt;/span&gt;
        &lt;span class="nc"&gt;Log&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Failed to send survey result email: '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getMessage&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'user_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;user&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Redirect or return view to show on-screen results regardless of email status&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'survey.result'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;compact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'surveyResult'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Actionable Takeaways
&lt;/h2&gt;

&lt;p&gt;When designing user interactions, always consider the flexibility of data capture methods and ensure all implied promises are met. Migrating from single-choice to multi-select can significantly enhance data quality and user satisfaction. Furthermore, when implementing external operations like email notifications, prioritize user flow with robust error handling (e.g., &lt;code&gt;try/catch&lt;/code&gt;) to prevent failures in one system from breaking the entire user experience. Always test these paths thoroughly to confirm both success and failure scenarios are handled gracefully.&lt;/p&gt;

</description>
      <category>php</category>
      <category>survey</category>
      <category>email</category>
    </item>
    <item>
      <title>Streamlining User Engagement: Direct Booking Integration on Reimpact's Platform</title>
      <dc:creator>Gerardo Andrés Ruiz Castillo</dc:creator>
      <pubDate>Thu, 21 May 2026 06:00:04 +0000</pubDate>
      <link>https://dev.to/geanruca/streamlining-user-engagement-direct-booking-integration-on-reimpacts-platform-1678</link>
      <guid>https://dev.to/geanruca/streamlining-user-engagement-direct-booking-integration-on-reimpacts-platform-1678</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In today's fast-paced digital landscape, minimizing friction in the user journey is paramount for converting interest into action. For platforms offering advisory or consultation services, enabling direct and immediate booking capabilities can significantly enhance user experience and operational efficiency.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Challenge: Indirect Scheduling
&lt;/h2&gt;

&lt;p&gt;Previously, users interested in scheduling an advisory session on Reimpact's platform, particularly through call-to-action (CTA) buttons in survey emails, were directed to a generic contact page. This multi-step process often introduced unnecessary friction, potentially leading to drop-offs before an appointment could be secured. The goal was to remove this intermediate step and provide a more direct path to engagement.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: Direct Google Calendar Booking
&lt;/h2&gt;

&lt;p&gt;To address this, we implemented a direct integration with Google Calendar's appointment scheduling feature. The "Agendar asesoría" (Schedule an appointment) button, prominent in our survey email CTAs on the landing page, now links directly to a pre-configured Google Calendar booking URL. This change allows users to immediately view available slots and book an appointment without navigating away to a separate contact form.&lt;/p&gt;

&lt;h3&gt;
  
  
  Implementation Example
&lt;/h3&gt;

&lt;p&gt;This change typically involves updating the target URL within a template or configuration file. For a PHP-based application, this might look like updating a variable or a direct link in a view:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="c1"&gt;// In a configuration file or a method handling CTA links&lt;/span&gt;
&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getAppointmentBookingUrl&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// This URL would come from a secure configuration or environment variable&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s1"&gt;'https://calendar.google.com/calendar/appointments/s/YOUR_APPOINTMENT_ID'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// In a Blade/PHP template for the email CTA button&lt;/span&gt;
&lt;span class="nv"&gt;$bookingUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getAppointmentBookingUrl&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;a href="'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;htmlspecialchars&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$bookingUrl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'" target="_blank" class="cta-button"&amp;gt;Agendar asesoría&amp;lt;/a&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="cp"&gt;?&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ensures that whenever the CTA button is rendered, it points directly to the active booking link, bypassing any intermediate pages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Immediate Impact
&lt;/h2&gt;

&lt;p&gt;This seemingly small adjustment has a significant impact on user engagement. By providing a direct link to the Google Calendar, we've achieved:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Reduced Friction:&lt;/strong&gt; Users can book appointments in fewer clicks.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Improved Conversion Rates:&lt;/strong&gt; A streamlined process increases the likelihood of users completing the booking.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Enhanced User Experience:&lt;/strong&gt; A more intuitive and efficient scheduling process reflects positively on the overall platform experience.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Operational Efficiency:&lt;/strong&gt; Reduces the need for manual follow-ups that might have been required for contact form submissions.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;p&gt;When designing user flows, especially for critical actions like scheduling or purchasing, always seek opportunities to simplify and shorten the path to completion. Leveraging direct integrations with third-party services like Google Calendar for scheduling can be a powerful way to achieve this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Insight
&lt;/h2&gt;

&lt;p&gt;Every click saved in a critical user journey contributes directly to better engagement and higher conversion rates. Optimize for directness wherever possible.&lt;/p&gt;

</description>
      <category>php</category>
      <category>web</category>
      <category>integration</category>
    </item>
    <item>
      <title>Streamlining SaaS Quoting: Enhanced User Experience and Granular Control</title>
      <dc:creator>Gerardo Andrés Ruiz Castillo</dc:creator>
      <pubDate>Wed, 20 May 2026 06:09:04 +0000</pubDate>
      <link>https://dev.to/geanruca/streamlining-saas-quoting-enhanced-user-experience-and-granular-control-2c61</link>
      <guid>https://dev.to/geanruca/streamlining-saas-quoting-enhanced-user-experience-and-granular-control-2c61</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In the &lt;code&gt;Reimpact platform&lt;/code&gt; project, our SaaS quote form is a critical component for potential clients to configure and estimate their service packages. A smooth, intuitive, and transparent quoting process is essential for user satisfaction and conversion. Recently, we focused on significant enhancements to this form to improve clarity, flexibility, and control over service selections and pricing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Improving the User Experience
&lt;/h2&gt;

&lt;p&gt;The previous iteration of our quote form, while functional, presented some areas for improvement in terms of user guidance and flexibility. Our recent updates addressed these directly:&lt;/p&gt;

&lt;h3&gt;
  
  
  Clearer Labels and Guidance
&lt;/h3&gt;

&lt;p&gt;To enhance user understanding, we refined several key labels. For instance, "Servicios Extra (Horas)" was simplified to "Servicios Extra", making it less prescriptive and more inviting. Similarly, module selection received clearer guidance with a new subtitle: "(Seleccionar al menos 1)", ensuring users understand the minimum requirement.&lt;/p&gt;

&lt;h3&gt;
  
  
  Flexible Module Selection
&lt;/h3&gt;

&lt;p&gt;A significant change involved the 'Packaging' module. Previously, it was designated as a mandatory base module, often creating friction for users who might not require it. We've now removed its 'required' status, making it, like all other modules, optional. This allows customers greater freedom to tailor their package precisely to their needs, without being forced into unnecessary selections.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enhanced Quoting Logic with Service Limits
&lt;/h2&gt;

&lt;p&gt;Beyond cosmetic and structural improvements, we introduced more robust logic for service quantity selection. To prevent over-selection or misconfiguration, we've implemented per-service maximum quantities on the slider inputs. This ensures that users select quantities within predefined business rules, providing both guardrails for the user and predictability for our operations.&lt;/p&gt;

&lt;p&gt;For example, services like 'Baseline', 'Audit', 'Support', 'Training', and 'Consulting' now have specific upper limits (e.g., Baseline at 20, Audit at 12, Support at 24, Training at 1, Consulting at 24). This not only guides the user but also ensures the integrity of the generated quote.&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ServiceLimits&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getLimits&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'baseline'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'min'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'max'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="s1"&gt;'audit'&lt;/span&gt;      &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'min'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'max'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="s1"&gt;'support'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'min'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'max'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="s1"&gt;'training'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'min'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'max'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="s1"&gt;'consulting'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'min'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'max'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;validateQuantity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$service&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$quantity&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$limits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;getLimits&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;isset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$limits&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$service&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$quantity&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nv"&gt;$limits&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$service&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'min'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;$quantity&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nv"&gt;$limits&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$service&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'max'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Service not found or no limits defined&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Example usage in a form processing logic:&lt;/span&gt;
&lt;span class="nv"&gt;$serviceName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'audit'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$userQuantity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// User attempts to select 15&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nc"&gt;ServiceLimits&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;validateQuantity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$serviceName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$userQuantity&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Error: Invalid quantity for &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$serviceName&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;."&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// This would trigger an error&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Quantity &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$userQuantity&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; for &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$serviceName&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; is valid."&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cp"&gt;?&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This PHP example illustrates how these limits could be defined and validated within the application, ensuring that user inputs conform to the business rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  Transparent Summary Details
&lt;/h2&gt;

&lt;p&gt;One of the most impactful improvements for user confidence is the new itemized breakdown in the summary panel. Instead of a high-level total, users now see a clear, detailed list of each selected module and service, along with their respective quantities and costs. This transparency allows users to easily review their choices, understand how the total is calculated, and build trust in the quoting process.&lt;/p&gt;

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

&lt;p&gt;These enhancements to the Reimpact platform's SaaS quote form collectively deliver a more intuitive, flexible, and transparent experience. By improving labels, offering greater module choice, implementing robust service limits, and providing a detailed summary, we empower users to configure their ideal service package with confidence, leading to a better overall user journey and more accurate quotes.&lt;/p&gt;

</description>
      <category>php</category>
      <category>saas</category>
      <category>forms</category>
      <category>ux</category>
    </item>
    <item>
      <title>Enhancing UI Reactivity: Dynamic Editor Panels with Alpine.js and PHP</title>
      <dc:creator>Gerardo Andrés Ruiz Castillo</dc:creator>
      <pubDate>Mon, 18 May 2026 22:01:03 +0000</pubDate>
      <link>https://dev.to/geanruca/enhancing-ui-reactivity-dynamic-editor-panels-with-alpinejs-and-php-1hj4</link>
      <guid>https://dev.to/geanruca/enhancing-ui-reactivity-dynamic-editor-panels-with-alpinejs-and-php-1hj4</guid>
      <description>&lt;p&gt;Building highly interactive user interfaces often means juggling complex client-side state and ensuring seamless communication with the backend. For the Breniapp/brenia project, we recently tackled this by revamping the editor panel to offer a more fluid user experience, specifically focusing on dynamic layer expansion and real-time canvas preview updates. This overhaul leveraged Alpine.js for frontend reactivity, streamlining how users interact with generated content.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Challenge: Static Panels vs. Dynamic Needs
&lt;/h2&gt;

&lt;p&gt;Previously, parts of our editor panel might have relied on full page reloads or more cumbersome JavaScript to manage UI state. As features like iterative content generation and customizable layers became more central, the need for an instant, reactive UI grew. Users expect immediate visual feedback and the ability to expand and collapse detailed controls without disruption.&lt;/p&gt;

&lt;h2&gt;
  
  
  Alpine.js for Lightweight Reactivity
&lt;/h2&gt;

&lt;p&gt;The core of this enhancement lies in Alpine.js, a lightweight JavaScript framework that brings the power of reactive data binding directly to your HTML markup. We introduced &lt;code&gt;x-data&lt;/code&gt; to manage the state of the editor panel, specifically for &lt;code&gt;expandedLayer&lt;/code&gt; (controlling the visibility of layer details) and &lt;code&gt;canvasImageUrl&lt;/code&gt; (displaying the generated content preview). This declarative approach simplifies complex UI logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Event-Driven Updates: Seamless Canvas Previews
&lt;/h2&gt;

&lt;p&gt;A critical aspect was updating the canvas preview in real-time as content is generated. Rather than polling or manual DOM manipulation, we implemented an event-driven mechanism. After a generation process completes, a &lt;code&gt;generation-completed&lt;/code&gt; event is dispatched on the client-side. Alpine.js then listens for this event and updates the &lt;code&gt;canvasImageUrl&lt;/code&gt; state, instantly refreshing the preview without user intervention.&lt;/p&gt;

&lt;p&gt;This approach cleanly separates the backend generation logic from the frontend rendering, making the system more robust and easier to maintain. The PHP backend is responsible for processing the generation request and ultimately providing the new image URL, which is then picked up by the Alpine.js component.&lt;/p&gt;

&lt;p&gt;Consider a simplified PHP endpoint that might handle the image generation request and provide the updated URL:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Controllers&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Services\ImageGenerator&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Framework\Http\Response&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EditorApiController&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;ImageGenerator&lt;/span&gt; &lt;span class="nv"&gt;$generator&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;ImageGenerator&lt;/span&gt; &lt;span class="nv"&gt;$generator&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;generator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$generator&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;generateCanvasPreview&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;Response&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Assume request body contains necessary generation parameters&lt;/span&gt;
        &lt;span class="nv"&gt;$params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;json_decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;file_get_contents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'php://input'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Call a service to process and generate the image&lt;/span&gt;
        &lt;span class="nv"&gt;$newImageUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;generator&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;createImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$params&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Return the new URL to the client&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;json_encode&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
            &lt;span class="s1"&gt;'imageUrl'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$newImageUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'message'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Generation complete!'&lt;/span&gt;
        &lt;span class="p"&gt;]),&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'Content-Type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'application/json'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This PHP code snippet illustrates a backend endpoint that would receive a request, trigger the image generation process (e.g., in a &lt;code&gt;ImageGenerator&lt;/code&gt; service), and then return a JSON response containing the &lt;code&gt;imageUrl&lt;/code&gt;. On the frontend, an Alpine.js component would make an AJAX call to this endpoint, and upon receiving the &lt;code&gt;imageUrl&lt;/code&gt;, dispatch a custom &lt;code&gt;generation-completed&lt;/code&gt; event (or directly update its state if the AJAX call is handled within the component).&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion and Actionable Takeaway
&lt;/h2&gt;

&lt;p&gt;The shift to a more reactive editor panel using Alpine.js significantly enhanced the user experience within Breniapp/brenia. By leveraging &lt;code&gt;x-data&lt;/code&gt; for local component state and event listeners for inter-component communication, we achieved dynamic UI behavior with minimal JavaScript overhead. If you're looking to add interactive elements and real-time feedback to your web applications without the complexity of a full-blown framework, consider Alpine.js. Pair it with a robust PHP backend for data processing, and you can build highly responsive interfaces that delight users.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Actionable Takeaway:&lt;/strong&gt; When faced with static UI elements that need to become dynamic, explore lightweight JavaScript frameworks like Alpine.js for managing frontend state and implementing event-driven updates. Design your backend APIs to provide the necessary data payloads for these frontend interactions.&lt;/p&gt;

</description>
      <category>php</category>
      <category>alpinejs</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Manejo de Errores y Prevención de Publicaciones Duplicadas en devlog-ist/landing</title>
      <dc:creator>Gerardo Andrés Ruiz Castillo</dc:creator>
      <pubDate>Wed, 11 Mar 2026 07:04:04 +0000</pubDate>
      <link>https://dev.to/geanruca/manejo-de-errores-y-prevencion-de-publicaciones-duplicadas-en-devlog-istlanding-3l3d</link>
      <guid>https://dev.to/geanruca/manejo-de-errores-y-prevencion-de-publicaciones-duplicadas-en-devlog-istlanding-3l3d</guid>
      <description>&lt;h2&gt;
  
  
  Introducción
&lt;/h2&gt;

&lt;p&gt;En el proyecto devlog-ist/landing, que permite a los usuarios publicar contenido en diversas plataformas, se ha trabajado en mejorar la robustez del sistema y prevenir problemas comunes relacionados con la publicación de contenido.&lt;/p&gt;

&lt;h2&gt;
  
  
  El Desafío
&lt;/h2&gt;

&lt;p&gt;El sistema presentaba dos problemas principales:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Errores 500 al alcanzar el límite diario de publicaciones:&lt;/strong&gt; Cuando un usuario intentaba publicar más contenido del permitido en un día, el sistema respondía con un error 500 genérico, lo cual no era informativo ni amigable.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Publicaciones duplicadas:&lt;/strong&gt; En ciertas ocasiones, el sistema programaba publicaciones y, al mismo tiempo, el usuario intentaba publicar el mismo contenido de forma inmediata, resultando en una doble publicación.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  La Solución
&lt;/h2&gt;

&lt;p&gt;Para abordar estos problemas, se implementaron las siguientes soluciones:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Manejo de &lt;code&gt;DailyPostLimitReachedException&lt;/code&gt;:&lt;/strong&gt; Se capturó la excepción &lt;code&gt;DailyPostLimitReachedException&lt;/code&gt; en todas las acciones de publicación (LinkedIn, Dev.to, individual y masiva). En lugar de mostrar un error 500, se muestra una notificación traducida al idioma del usuario, indicando que ha alcanzado su límite diario de publicaciones.&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Lógica de publicación&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DailyPostLimitReachedException&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Mostrar notificación traducida&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nf"&gt;__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'daily_publish_limit_reached'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Cancelación de publicaciones programadas pendientes:&lt;/strong&gt; Al publicar contenido de forma inmediata, se cancelan los registros de &lt;code&gt;ScheduledPost&lt;/code&gt; pendientes para evitar la doble publicación desde la cola de programación.&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Cancelar publicaciones programadas pendientes&lt;/span&gt;
&lt;span class="nc"&gt;ScheduledPost&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'post_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$postId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

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

&lt;h2&gt;
  
  
  Decisiones Clave
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Traducciones:&lt;/strong&gt; Se agregaron traducciones para el mensaje &lt;code&gt;daily_publish_limit_reached&lt;/code&gt; en los cuatro idiomas soportados (inglés, español, francés y alemán).&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Robustez:&lt;/strong&gt; Manejar la excepción específica mejora la experiencia del usuario al proporcionar información clara sobre el problema.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Prevención:&lt;/strong&gt; Cancelar las publicaciones programadas evita la duplicación de contenido, manteniendo la integridad del sistema.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Resultados
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  Se eliminaron los errores 500 relacionados con el límite diario de publicaciones, mejorando la experiencia del usuario.&lt;/li&gt;
&lt;li&gt;  Se previnieron las publicaciones duplicadas, asegurando que el contenido se publique solo una vez.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Lecciones Aprendidas
&lt;/h2&gt;

&lt;p&gt;Es crucial manejar las excepciones específicas en lugar de depender de errores genéricos. Además, la prevención de problemas a través de la cancelación de tareas redundantes puede mejorar significativamente la fiabilidad del sistema.&lt;/p&gt;

</description>
      <category>php</category>
    </item>
    <item>
      <title>Optimizing Deployments for Low-Resource VPS Environments</title>
      <dc:creator>Gerardo Andrés Ruiz Castillo</dc:creator>
      <pubDate>Sat, 07 Mar 2026 07:31:05 +0000</pubDate>
      <link>https://dev.to/geanruca/optimizing-deployments-for-low-resource-vps-environments-1eki</link>
      <guid>https://dev.to/geanruca/optimizing-deployments-for-low-resource-vps-environments-1eki</guid>
      <description>&lt;p&gt;Deploying applications to low-resource Virtual Private Servers (VPS) can be challenging. This post explores strategies for optimizing deployments to minimize resource consumption and improve reliability, specifically for the devlog-ist/landing project.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Challenge
&lt;/h2&gt;

&lt;p&gt;Low-resource VPS environments often suffer from limited RAM and CPU. Standard deployment procedures can easily exhaust these resources, leading to slow deployments, failed builds, and application instability. The goal is to reduce the memory footprint and optimize build processes to fit within the constraints of the VPS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Strategies for Optimization
&lt;/h2&gt;

&lt;p&gt;Several techniques can be employed to optimize deployments for low-resource environments:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Reusing &lt;code&gt;node_modules&lt;/code&gt;:&lt;/strong&gt; Similar to the &lt;code&gt;vendor/&lt;/code&gt; directory in PHP projects, the &lt;code&gt;node_modules/&lt;/code&gt; directory can be reused from the previous release. This avoids re-downloading and re-installing dependencies on every deploy, saving significant time and bandwidth. The principle here is that dependencies change less frequently than the application code itself.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Limiting Node RAM Usage:&lt;/strong&gt; Node.js can consume substantial RAM during builds. By setting the &lt;code&gt;NODE_OPTIONS=--max-old-space-size=512&lt;/code&gt; environment variable, we can limit the maximum memory Node.js can use. This prevents the build process from exhausting all available RAM and triggering out-of-memory errors.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Utilizing &lt;code&gt;npm ci --prefer-offline&lt;/code&gt;:&lt;/strong&gt; The &lt;code&gt;npm ci&lt;/code&gt; command performs a clean install from the &lt;code&gt;package-lock.json&lt;/code&gt; file, ensuring consistent dependency versions. The &lt;code&gt;--prefer-offline&lt;/code&gt; flag instructs npm to use the local cache whenever possible, further reducing network activity and improving speed.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Splitting Deployments into Phases:&lt;/strong&gt; Separating the deployment process into pre-activate and post-activate phases allows for more granular control and error handling. Pre-activate tasks can include copying necessary files and running database migrations, while post-activate tasks can involve clearing caches and restarting services.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Timing Deployment Phases:&lt;/strong&gt; Adding timing information to each deployment phase (e.g., using &lt;code&gt;time&lt;/code&gt; command) helps identify bottlenecks and areas for further optimization. This allows for data-driven decisions when making changes to the deployment process.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Implementation
&lt;/h2&gt;

&lt;p&gt;The following example demonstrates how to limit Node RAM usage in a deployment script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;NODE_OPTIONS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nt"&gt;--max-old-space-size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;512 npm ci &lt;span class="nt"&gt;--prefer-offline&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command sets the &lt;code&gt;NODE_OPTIONS&lt;/code&gt; environment variable to limit the maximum old space size to 512MB before running &lt;code&gt;npm ci&lt;/code&gt;. This prevents the Node.js process from consuming excessive memory during installation.&lt;/p&gt;

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

&lt;p&gt;Optimizing deployments for low-resource VPS environments requires careful consideration of resource consumption and build processes. By reusing &lt;code&gt;node_modules&lt;/code&gt;, limiting Node RAM usage, utilizing &lt;code&gt;npm ci --prefer-offline&lt;/code&gt;, splitting deployments into phases, and timing deployment steps, we can significantly improve the reliability and speed of deployments, even on constrained hardware.&lt;/p&gt;

</description>
      <category>node</category>
      <category>php</category>
      <category>javascript</category>
      <category>vps</category>
    </item>
    <item>
      <title>Tenant-Specific Scheduling in devlog-ist/landing</title>
      <dc:creator>Gerardo Andrés Ruiz Castillo</dc:creator>
      <pubDate>Sat, 07 Mar 2026 06:00:05 +0000</pubDate>
      <link>https://dev.to/geanruca/tenant-specific-scheduling-in-devlog-istlanding-2b8k</link>
      <guid>https://dev.to/geanruca/tenant-specific-scheduling-in-devlog-istlanding-2b8k</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In the &lt;code&gt;devlog-ist/landing&lt;/code&gt; project, which likely involves a landing page generator with publishing capabilities, a key requirement is the ability to tailor content scheduling to individual tenants. This post explores how configurable publishing schedules were introduced to provide a more natural and less bot-like content delivery experience, enhancing the platform's flexibility and user experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Challenge: Hardcoded Scheduling
&lt;/h2&gt;

&lt;p&gt;Previously, the post scheduling mechanism relied on hardcoded values within the &lt;code&gt;PostStaggeringService&lt;/code&gt;. This approach lacked the flexibility needed to accommodate the diverse needs of different tenants. All tenants were subject to the same rigid schedule, leading to potentially unnatural publishing patterns and a one-size-fits-all experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: Configurable Tenant Settings
&lt;/h2&gt;

&lt;p&gt;The solution involved introducing configurable publishing schedule settings for each tenant. Specifically, tenants can now adjust the following parameters via the AutomationSettings page:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Publish Window Hours (Start/End):&lt;/strong&gt; Define the time frame during which posts should be published.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Maximum Posts Per Hour:&lt;/strong&gt; Control the density of posts published within the defined window.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Jitter Minutes:&lt;/strong&gt; Introduce randomness to the exact publishing time, making the schedule less predictable and more organic.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Implementation Details
&lt;/h2&gt;

&lt;p&gt;The hardcoded values in &lt;code&gt;PostStaggeringService&lt;/code&gt; were replaced with these tenant-specific settings. This allows each tenant to customize their publishing schedule according to their preferences and audience behavior. For example, consider a hypothetical scenario where a tenant wants to publish content primarily during business hours, with a limited number of posts per hour and a slight variation in timing to avoid appearing robotic. The configuration might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$tenantSettings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'publish_start_hour'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'publish_end_hour'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'max_posts_per_hour'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'jitter_minutes'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="c1"&gt;// The PostStaggeringService would then use these settings to determine the&lt;/span&gt;
&lt;span class="c1"&gt;// appropriate time to publish each post.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Benefits of Configurable Scheduling
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Improved User Experience:&lt;/strong&gt; Tenants can tailor content delivery to their specific audience, leading to higher engagement.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;More Natural Publishing Patterns:&lt;/strong&gt; The introduction of jitter and customizable windows prevents the appearance of automated bot-like behavior.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Increased Flexibility:&lt;/strong&gt; The platform becomes more adaptable to the diverse needs of different tenants.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;By introducing configurable publishing schedules, the &lt;code&gt;devlog-ist/landing&lt;/code&gt; project has significantly enhanced its content delivery capabilities. This change provides tenants with the flexibility they need to create a more engaging and natural user experience, moving away from rigid, hardcoded schedules towards a more dynamic and personalized approach.&lt;/p&gt;

</description>
      <category>php</category>
    </item>
    <item>
      <title>Enhancing Security Audits: Avoiding False Positives in File Path Detection</title>
      <dc:creator>Gerardo Andrés Ruiz Castillo</dc:creator>
      <pubDate>Fri, 06 Mar 2026 12:33:04 +0000</pubDate>
      <link>https://dev.to/geanruca/enhancing-security-audits-avoiding-false-positives-in-file-path-detection-4o63</link>
      <guid>https://dev.to/geanruca/enhancing-security-audits-avoiding-false-positives-in-file-path-detection-4o63</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In the &lt;code&gt;devlog-ist/landing&lt;/code&gt; project, we're continually working to improve our security posture. A recent focus has been on refining our security auditing tools to reduce false positives, particularly around the detection of potentially sensitive file paths.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Challenge
&lt;/h2&gt;

&lt;p&gt;Our automated security audits sometimes flagged placeholder file paths as potential exposures of sensitive information. For example, paths like &lt;code&gt;/path/to/certificate&lt;/code&gt; or &lt;code&gt;/path/to/private/key&lt;/code&gt; were incorrectly identified as containing actual private keys or certificates. This was due to the LLM misinterpreting these paths, which were intended only as examples, as real file locations containing sensitive data.&lt;/p&gt;

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

&lt;p&gt;To address this, we've reinforced the rule that paths matching the &lt;code&gt;/path/to/&lt;/code&gt; pattern are always examples. This helps the LLM to correctly interpret these paths and avoid flagging them as potential security risks. Here's an example of how we might handle this in code:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SecurityAudit&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;isSensitivePath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Check if the path matches the /path/to/ pattern and exclude it from sensitive checks&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;preg_match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/^\/path\/to\//'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// It's an example path, not a real one&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Perform other checks for sensitive files (e.g., checking for known certificate extensions)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;strpos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'.pem'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;strpos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'.key'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Potentially sensitive file&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code snippet demonstrates how we can use a regular expression to identify placeholder paths and exclude them from further security checks. By explicitly excluding paths that match the &lt;code&gt;/path/to/&lt;/code&gt; pattern, we prevent false positives and ensure that our security audits focus on real potential vulnerabilities.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Decisions
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Regular Expression Matching&lt;/strong&gt;: Using &lt;code&gt;preg_match&lt;/code&gt; to identify example paths based on the &lt;code&gt;/path/to/&lt;/code&gt; pattern.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Exclusion Logic&lt;/strong&gt;: Implementing logic to exclude identified example paths from sensitive file checks.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;By implementing this change, we've significantly reduced the number of false positives in our security audits. This allows our security team to focus on real potential vulnerabilities, improving our overall security posture.&lt;/p&gt;

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

&lt;p&gt;This experience highlights the importance of context in security auditing. Automated tools must be able to distinguish between example paths and real file locations to avoid generating unnecessary alerts. By carefully crafting our audit rules and incorporating contextual awareness, we can improve the accuracy and effectiveness of our security audits.&lt;/p&gt;

</description>
      <category>php</category>
      <category>security</category>
      <category>llm</category>
    </item>
    <item>
      <title>Tenant Landing Page Optimization: Caching for Performance</title>
      <dc:creator>Gerardo Andrés Ruiz Castillo</dc:creator>
      <pubDate>Fri, 06 Mar 2026 10:55:03 +0000</pubDate>
      <link>https://dev.to/geanruca/tenant-landing-page-optimization-caching-for-performance-18nf</link>
      <guid>https://dev.to/geanruca/tenant-landing-page-optimization-caching-for-performance-18nf</guid>
      <description>&lt;p&gt;The &lt;code&gt;devlog-ist/landing&lt;/code&gt; project delivers landing pages for tenants. A recent optimization focused on reducing database load by implementing a caching strategy for tenant-specific landing page data. This change significantly improves performance by minimizing redundant database queries.&lt;/p&gt;

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

&lt;p&gt;Previously, each request to a tenant's landing page triggered multiple database queries to fetch stats, badges, recommendations, and post languages. In particular, loading all posts to generate statistics resulted in a substantial performance bottleneck. This "load ALL posts for stats" query ran on every landing page hit, even if the data hadn't changed. This approach was clearly unsustainable as the number of tenants and posts grew.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: Caching Tenant Landing Page Data
&lt;/h2&gt;

&lt;p&gt;To address this, a caching mechanism was introduced. Now, tenant-specific landing page data is cached with a Time-To-Live (TTL) of one hour. This means that the first request for a tenant's landing page will still incur the database queries, but subsequent requests within the hour will be served from the cache. The cache is automatically invalidated whenever a post is saved or deleted, ensuring data consistency.&lt;/p&gt;

&lt;p&gt;Here's a simplified example of how the caching might be implemented in PHP:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\Cache\Adapter\FilesystemAdapter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FilesystemAdapter&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nv"&gt;$cacheKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'tenant_'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$tenantId&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'_landing_page_data'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$landingPageData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$cache&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$cacheKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$tenantId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Fetch data from the database&lt;/span&gt;
    &lt;span class="nv"&gt;$stats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getTenantStatsFromDatabase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$tenantId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$badges&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getTenantBadgesFromDatabase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$tenantId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$recommendations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getTenantRecommendationsFromDatabase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$tenantId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$postLanguages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getTenantPostLanguagesFromDatabase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$tenantId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nv"&gt;$data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'stats'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$stats&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'badges'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$badges&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'recommendations'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$recommendations&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'postLanguages'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$postLanguages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Use $landingPageData to render the landing page&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code snippet demonstrates how the Symfony cache component can be used to store and retrieve tenant landing page data. The &lt;code&gt;get&lt;/code&gt; method either returns the cached data or executes the provided closure to fetch fresh data from the database if the cache is empty or expired.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Impact
&lt;/h2&gt;

&lt;p&gt;This caching strategy significantly reduces the number of database queries per request, particularly on cache warm. More importantly, it eliminates the expensive "load ALL posts for stats" query that previously ran on every landing page hit. This leads to improved landing page load times and reduced database load, resulting in a better user experience and improved system scalability.&lt;/p&gt;

</description>
      <category>php</category>
      <category>caching</category>
      <category>performance</category>
    </item>
    <item>
      <title>Geolocation Optimization: Caching and Local Database Lookup</title>
      <dc:creator>Gerardo Andrés Ruiz Castillo</dc:creator>
      <pubDate>Fri, 06 Mar 2026 10:43:05 +0000</pubDate>
      <link>https://dev.to/geanruca/geolocation-optimization-caching-and-local-database-lookup-3c5g</link>
      <guid>https://dev.to/geanruca/geolocation-optimization-caching-and-local-database-lookup-3c5g</guid>
      <description>&lt;p&gt;This post discusses optimizing geolocation lookups in the &lt;code&gt;devlog-ist/landing&lt;/code&gt; project, which enhances user experience by personalizing content based on location.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Challenge
&lt;/h2&gt;

&lt;p&gt;Initially, the application relied on an external HTTP service (&lt;code&gt;ip-api.com&lt;/code&gt;) to determine a visitor's country based on their IP address. This approach introduced significant latency, adding approximately 1 second to each page load. Furthermore, relying on an external service meant potential rate limits and service disruptions.&lt;/p&gt;

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

&lt;p&gt;To address these issues, we implemented two key optimizations:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Local Database Lookup:&lt;/strong&gt; Replaced the external HTTP calls with lookups against a local MaxMind GeoLite2-City database. This eliminates network latency and reliance on an external service.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Caching:&lt;/strong&gt; Introduced a caching layer to minimize repeat lookups. Resolved country codes are cached for 7 days, while unresolvable IPs are cached for 1 hour.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Implementation Details
&lt;/h2&gt;

&lt;p&gt;The local database lookup is performed using the &lt;code&gt;maxminddb&lt;/code&gt; package in Go. Here's an example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"fmt"&lt;/span&gt;
    &lt;span class="s"&gt;"net"&lt;/span&gt;

    &lt;span class="s"&gt;"github.com/oschwald/geoip2/maxminddb"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;maxminddb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"GeoLite2-City.mmdb"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;panic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;ip&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;net&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ParseIP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"8.8.8.8"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Country&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;IsoCode&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"iso_code"`&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="s"&gt;`json:"country"`&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;


    &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Lookup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;panic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Country code: %v&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Country&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsoCode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code snippet demonstrates how to open the MaxMind database, perform a lookup for a given IP address, and extract the country code.  A similar function integrates with a caching layer (e.g., Redis) to store the results.  When an IP address needs to be geolocated, the cache is checked first; if the result is not in the cache, the local database lookup is performed, and the result is then stored in the cache with the appropriate expiration time.&lt;/p&gt;

&lt;p&gt;To simplify the process of keeping the GeoLite2-City database up-to-date, an artisan command was added to download the latest version.&lt;/p&gt;

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

&lt;p&gt;By switching to a local database and implementing caching, we significantly reduced the latency associated with geolocation lookups. This resulted in faster page load times and an improved user experience.  The application is also more resilient to external service outages and rate limits.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Takeaway
&lt;/h2&gt;

&lt;p&gt;Optimizing geolocation lookups can have a significant impact on application performance and reliability.  By leveraging local databases and caching, developers can minimize latency and improve the user experience. Regularly updating the local database is crucial to ensure accuracy.&lt;/p&gt;

</description>
      <category>go</category>
      <category>database</category>
      <category>caching</category>
      <category>optimization</category>
    </item>
    <item>
      <title>Optimizing Tenant Schema Access in Go Applications</title>
      <dc:creator>Gerardo Andrés Ruiz Castillo</dc:creator>
      <pubDate>Fri, 06 Mar 2026 06:01:04 +0000</pubDate>
      <link>https://dev.to/geanruca/optimizing-tenant-schema-access-in-go-applications-37lp</link>
      <guid>https://dev.to/geanruca/optimizing-tenant-schema-access-in-go-applications-37lp</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In multi-tenant applications, efficient management of database schemas is crucial for performance. This post explores how caching and schema tracking can significantly reduce database load in Go applications.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Challenge
&lt;/h2&gt;

&lt;p&gt;Accessing tenant-specific data often involves repetitive database queries to check for schema existence and set the appropriate search path. These redundant operations can lead to performance bottlenecks, especially when dealing with a large number of tenants. Specifically, the &lt;code&gt;devlog-ist/landing&lt;/code&gt; project experienced a high volume of &lt;code&gt;pg_class&lt;/code&gt; queries and redundant &lt;code&gt;SET search_path&lt;/code&gt; statements, impacting overall application responsiveness.&lt;/p&gt;

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

&lt;p&gt;To address these performance issues, the following optimizations were implemented:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Caching Tenant Table Existence:&lt;/strong&gt; The result of the &lt;code&gt;tenantsTableExists()&lt;/code&gt; check is now cached using a persistent cache with a Time-To-Live (TTL) of one hour. This eliminates a significant number of &lt;code&gt;pg_class&lt;/code&gt; queries.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Tracking Applied Schema:&lt;/strong&gt; The application now tracks the currently applied schema. Before executing a &lt;code&gt;SET search_path&lt;/code&gt; statement, it checks if the desired tenant schema is already set. If so, the redundant statement is skipped.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Skipping Redundant Schema Reset:&lt;/strong&gt; Similarly, the &lt;code&gt;resetTenantSchema()&lt;/code&gt; function is skipped when the schema is already null, avoiding unnecessary database interactions.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These optimizations are implemented in Go, leveraging caching mechanisms and state management to minimize database load. Here's an example illustrating the concept of caching the schema existence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"fmt"&lt;/span&gt;
    &lt;span class="s"&gt;"time"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;schemaCache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;cacheTTL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Hour&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;checkSchemaExists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;schemaName&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// Check if the schema exists in the cache&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;schemaCache&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;schemaName&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Schema existence retrieved from cache for:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schemaName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;exists&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c"&gt;// Simulate a database call to check schema existence&lt;/span&gt;
    &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Checking schema existence in database for:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schemaName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c"&gt;// In real implementation, this would be a database query&lt;/span&gt;
    &lt;span class="n"&gt;exists&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;simulateDatabaseCheck&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;schemaName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c"&gt;// Store the result in the cache with a TTL&lt;/span&gt;
     &lt;span class="n"&gt;schemaCache&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;schemaName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;exists&lt;/span&gt;

    &lt;span class="c"&gt;// Invalidate cache after TTL&lt;/span&gt;
    &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
         &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cacheTTL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
         &lt;span class="nb"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;schemaCache&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schemaName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
         &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Cache invalidated for:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schemaName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}()&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;exists&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;simulateDatabaseCheck&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;schemaName&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// Replace with actual database query&lt;/span&gt;
    &lt;span class="c"&gt;// For example:&lt;/span&gt;
    &lt;span class="c"&gt;// err := db.QueryRow("SELECT 1 FROM pg_namespace WHERE nspname = $1", schemaName).Scan(nil)&lt;/span&gt;
    &lt;span class="c"&gt;// return err == nil&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt; &lt;span class="c"&gt;// Simulating schema exists&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="n"&gt;schemaName&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="s"&gt;"tenant_123"&lt;/span&gt;
     &lt;span class="n"&gt;exists&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;checkSchemaExists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;schemaName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
     &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Schema exists:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c"&gt;// Subsequent check will use the cache&lt;/span&gt;
     &lt;span class="n"&gt;exists&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;checkSchemaExists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;schemaName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
     &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Schema exists:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

     &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// Give some time for goroutine to execute&lt;/span&gt;


&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;These optimizations resulted in a significant reduction in database queries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Approximately 13,000 &lt;code&gt;pg_class&lt;/code&gt; queries per day were eliminated.&lt;/li&gt;
&lt;li&gt;  Around 10,000 redundant &lt;code&gt;SET search_path&lt;/code&gt; statements per day were skipped.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This leads to improved application performance and reduced load on the database server.&lt;/p&gt;

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

&lt;p&gt;Caching and state management are powerful tools for optimizing database-intensive applications. By identifying and eliminating redundant database operations, developers can achieve significant performance gains. Regular monitoring and profiling of database queries can help identify areas for optimization.&lt;/p&gt;

</description>
      <category>go</category>
      <category>caching</category>
      <category>optimization</category>
      <category>database</category>
    </item>
  </channel>
</rss>
