<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[yoniakabecky]]></title><description><![CDATA[yoniakabecky]]></description><link>https://blog.yoniakabecky.com</link><generator>RSS for Node</generator><lastBuildDate>Sun, 12 Apr 2026 08:59:53 GMT</lastBuildDate><atom:link href="https://blog.yoniakabecky.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[How a Confirmation Modal Broke My Shopify Store's SEO]]></title><description><![CDATA[TL;DR

✅ If you add an age-check / confirmation modal, do not hide your entire page content behind it.

⚠️ If your main content is hidden (for example with display: none), Googlebot may see an “empty”]]></description><link>https://blog.yoniakabecky.com/how-a-confirmation-modal-broke-my-shopify-store-s-seo</link><guid isPermaLink="true">https://blog.yoniakabecky.com/how-a-confirmation-modal-broke-my-shopify-store-s-seo</guid><category><![CDATA[Frontend Development]]></category><category><![CDATA[shopify]]></category><category><![CDATA[SEO]]></category><dc:creator><![CDATA[Yoni Aoki]]></dc:creator><pubDate>Sat, 04 Apr 2026 00:50:39 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/65ab3be4d775f416312d4036/2f4e1cea-d517-430e-9866-f4870e9f709e.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>TL;DR</h2>
<ul>
<li><p>✅ If you add an age-check / confirmation modal, <strong>do not hide your entire page content behind it</strong>.</p>
</li>
<li><p>⚠️ If your main content is hidden (for example with <code>display: none</code>), <strong>Googlebot may see an “empty” page</strong> and indexing can fail.</p>
</li>
</ul>
<h2>Situation: the shop was live, but Google could not find it 🔎</h2>
<p>I launched my friend's Shopify store, removed the password protection, and waited. A month later, still couldn't find it on Google. Shop Instagram account come first then somebody else's website that talking about the shop.</p>
<p>At first, I assumed, “Shopify is a big platform. Google will find it soon.” I also remembered reading a post saying something similar.</p>
<p>I waited for a month. Still nothing.</p>
<h2>First guess: Search Console setup? 🤔</h2>
<p>My first instinct was to check the basics:</p>
<ul>
<li><p>Sitemap</p>
</li>
<li><p>Google Search Console setup</p>
</li>
<li><p>Indexing request</p>
</li>
</ul>
<p>Shopify generates a sitemap automatically, so that part was easy.</p>
<p>The tricky part was permissions: the store owner is my friend, and Search Console had to be set up under her Google account. So I told her how to set up, since shopify has good article: <a href="https://help.shopify.com/en/manual/promoting-marketing/seo/find-site-map">Sitemap setup</a></p>
<h2>Tried: Search Console setup + request indexing 🛠️</h2>
<p>Another month passed. Still not in search results. I searched for other reasons, but found nothing that matched my case.</p>
<p>So I visited my friend’s shop and asked her to open Search Console.</p>
<p>It turned out it was not set up yet. 🫠 So I set it up.</p>
<p>After we set it up, we used:</p>
<ul>
<li><p><strong>URL Inspection</strong></p>
</li>
<li><p><strong>Request indexing</strong></p>
</li>
</ul>
<h2>The real cause: code hid the entire page 🤦</h2>
<p>Even after that, the result did not change.</p>
<p>While checking the site again, I noticed something:<br />when the modal was shown, <strong>the page content was effectively empty</strong>.</p>
<p>The shop sells alcohol, so I added an age verification modal. While building it, I noticed the carousel behind the modal was still animating.<br />It looked a bit strange, so I asked an AI assistant for a fix.</p>
<p>The suggestion was: <strong>hide the page until the user clicks “Yes”</strong>.</p>
<p>I accepted it without thinking about SEO. What I did not realize — I hid the entire <code>main</code> content.</p>
<p>So when Googlebot visited the page, it could not see real content to index.</p>
<h3>Original (broken) 🚫</h3>
<p>The biggest cause was setting <code>main</code> as hidden.</p>
<pre><code class="language-liquid">  &lt;!-- layout/theme.liquid --&gt;
  &lt;body class="age-verification-pending"&gt;
    &lt;script&gt;
      if (localStorage.getItem('ageConfirmed') === 'true') {
        document.body.classList.remove('age-verification-pending');
      }
    &lt;/script&gt;
    &lt;!-- other contents --&gt;
  &lt;/body&gt;
</code></pre>
<pre><code class="language-css">body.age-verification-pending main {
  display: none;
}
</code></pre>
<p>This 👆 one rule was the problem. It removed <code>main</code> from the page entirely — so Googlebot loaded the page and found nothing to index.</p>
<p>So unless the visitor clicked <code>Yes</code>, the content was hidden.</p>
<pre><code class="language-javascript">const onYes = () =&gt; {
  localStorage.setItem("ageConfirmed", "true");
  document.body.classList.remove("age-verification-pending");
};
</code></pre>
<h3>The actual fix ✅</h3>
<p>Remove the CSS rule that hides <code>main</code>.</p>
<pre><code class="language-css">/* removed */
body.age-verification-pending main {
  display: none;
}
</code></pre>
<p>That's it for SEO. The page content now stays in the DOM whether the modal is open or not.</p>
<h3>While we're at it: use the native <code>&lt;dialog&gt;</code> element 💡</h3>
<p>The original code used <code>&lt;div role="dialog"&gt;</code> — a custom modal built by hand.<br />Since we no longer need <code>display: none</code> on <code>main</code>, we can switch to the browser's native <code>&lt;dialog&gt;</code> element.<br />It handles the visual overlay automatically, without touching page content.</p>
<pre><code class="language-html">&lt;!-- before --&gt;
&lt;div class="age-modal" role="dialog"&gt;
  &lt;!-- contents here --&gt;
&lt;/div&gt;

&lt;!-- after --&gt;
&lt;dialog class="age-modal"&gt;
  &lt;!-- contents here --&gt;
&lt;/dialog&gt;
</code></pre>
<p>What this improves:</p>
<ul>
<li><p><code>::backdrop</code> covers the page visually — no need to hide <code>main</code></p>
</li>
<li><p>Native accessibility (focus trapping, <code>aria-modal</code>) built in</p>
</li>
<li><p><code>showModal()</code> / <code>close()</code> replace manual class toggling</p>
</li>
</ul>
<pre><code class="language-css">.age-modal[open] {
  /* style here */
}
.age-modal::backdrop {
  background-color: rgba(0, 0, 0, 0.8);
  backdrop-filter: blur(10px);
}
</code></pre>
<pre><code class="language-js">if (document.body.classList.contains("age-verification-pending")) {
  ageModal.showModal();
}

const onYes = () =&gt; {
  localStorage.setItem("ageConfirmed", "true");
  document.body.classList.remove("age-verification-pending");
  ageModal.close();
};
</code></pre>
<h3>Why this works 💡</h3>
<p>The SEO fix was just one CSS rule removed.</p>
<p>Googlebot doesn't click buttons. It loads the page once and reads whatever is already visible in the DOM.</p>
<p>With <code>body.age-verification-pending main { display: none; }</code>, the page looked completely empty to the crawler.</p>
<p>The native <code>&lt;dialog&gt;</code> element uses <code>::backdrop</code> to visually cover the page — but the content underneath stays in the DOM. The overlay is cosmetic. The content is real.</p>
<h2>Conclusion ✨</h2>
<ul>
<li><p>I removed the “hide the whole page” logic.</p>
</li>
<li><p>I allowed the carousel to keep running behind the modal.</p>
</li>
<li><p>After that, the shop started to appear in Google search results.</p>
</li>
<li><p>If an AI suggests “hide the content”, double-check what it means for SEO.</p>
</li>
<li><p>Prefer UI patterns that <strong>overlay</strong> content instead of removing it.</p>
</li>
<li><p>When SEO looks broken, check the rendered page as a bot might see it: “Is there real content without clicks?”</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Use Gemini Code Assist as a PR Reviewer]]></title><description><![CDATA[TL;DR
Gemini Code Assist is a GitHub app that auto-reviews your PRs. It catches real issues (typos, accessibility, naming). You often need /gemini review more than once — it does not catch everything ]]></description><link>https://blog.yoniakabecky.com/use-gemini-code-assist-as-a-pr-reviewer</link><guid isPermaLink="true">https://blog.yoniakabecky.com/use-gemini-code-assist-as-a-pr-reviewer</guid><category><![CDATA[shopify]]></category><category><![CDATA[Frontend Development]]></category><category><![CDATA[gemini]]></category><dc:creator><![CDATA[Yoni Aoki]]></dc:creator><pubDate>Sat, 21 Mar 2026 11:47:20 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/65ab3be4d775f416312d4036/73b1ecfa-dc75-4e9f-969a-5c6c094806e8.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>TL;DR</h2>
<p>Gemini Code Assist is a GitHub app that auto-reviews your PRs. It catches real issues (typos, accessibility, naming). You often need <code>/gemini review</code> more than once — it does not catch everything in a single pass. Treat suggestions as hypotheses: test before you merge.</p>
<h2>Intro 🤖</h2>
<p>Reviewing AI-generated code is tedious. When Cursor writes 300 lines across 8 files, you either review it properly (slow) or trust it (risky).</p>
<p>Gemini Code Assist is the free option I tried — here's what it catches and where it falls short.</p>
<h2>Gemini Code Assist</h2>
<p>While I was researching Gemini, I found a GitHub app: <a href="https://github.com/apps/gemini-code-assist">Gemini Code Assist</a>.</p>
<p>Setup is straightforward. See: <a href="https://developers.google.com/gemini-code-assist/docs/set-up-code-assist-github">https://developers.google.com/gemini-code-assist/docs/set-up-code-assist-github</a></p>
<p>After I connected it to my repo, it runs automatically on new pull requests.</p>
<h2>What Gemini catches well ✅</h2>
<p>Once it was live, the practical value showed up quickly.</p>
<h3>Code quality</h3>
<ul>
<li><p>Typos in variable names and string literals</p>
</li>
<li><p>Missing or undefined variables in Liquid templates</p>
</li>
<li><p>Inconsistent naming across files</p>
</li>
</ul>
<p>For example, I added some styles</p>
<pre><code class="language-css">    height: 100%;
    width: 100%;
</code></pre>
<p>The review:</p>
<blockquote>
<p>Your stylesheet is already using modern CSS logical properties like <code>inline-size</code> and <code>block-size</code> in other places. For consistency, it would be best to use them here as well instead of the physical properties <code>height</code> and <code>width</code>.</p>
</blockquote>
<h3>Readability</h3>
<ul>
<li><p>Restructuring overly nested Liquid logic</p>
</li>
<li><p>More descriptive variable names</p>
</li>
</ul>
<h3>Accessibility</h3>
<ul>
<li><p>Missing <code>alt</code> attributes on images</p>
</li>
<li><p>Semantic HTML (for example, correct heading hierarchy)</p>
</li>
</ul>
<p>These catches alone make it worthwhile.</p>
<h2>Limitations You Should Know ⚠️</h2>
<p>No tool is perfect. Gemini as a PR reviewer has a few friction points worth naming clearly.</p>
<h3>It does not review everything in one pass</h3>
<p>The biggest pain point for me: Gemini does not always list all issues in the first review.</p>
<p>For example, I got feedback about a CSS class name. I fixed it, then commented <code>/gemini review</code> to run it again. A few minutes later, I got more comments on other lines.</p>
<p>In practice, I trigger several reviews with <code>/gemini review</code> to collect most of the feedback.</p>
<h3>Not always right</h3>
<p>AI makes mistakes — that is common knowledge, and keep it in mind. Treat every suggestion as a <strong>hypothesis</strong>, not a final answer. Read it, understand it, then test it — especially when it changes logic, not only style.</p>
<h4>Situation: richtext field and a suggestion I applied</h4>
<p>For example, when I was implementing a richtext field in the section, I got a review message:</p>
<blockquote>
<p>For richtext fields, a simple != blank check might not be sufficient. If a user clears the text in the editor, it can leave behind empty HTML tags like</p>
<p>, which would pass the blank check and render an empty element, potentially causing unwanted layout spacing. It's more robust to strip HTML tags and whitespace before checking if the content is blank.</p>
</blockquote>
<pre><code class="language-plaintext">{% if section.settings.paragraph | strip_html | strip != blank %}
  {{ section.settings.paragraph }}
{% endif %}
</code></pre>
<p>I used this suggested code. What I didn't know was that it would be a syntax error. (my liquid knowledge is limited...) I also forgot to run theme check somehow.</p>
<h4>What went wrong</h4>
<p>Later I checked the website; the change wasn't there. I checked the Shopify log and realized that the change caused a syntax error. Luckily the error was caught by Shopify's theme deployment system, so the change wasn't released.</p>
<p>In Liquid, filters (<code>|</code>) cannot be used directly inside <code>{% if %}</code> conditions. You need to assign the filtered value to a variable, then use it in the condition.</p>
<h4>The Fix</h4>
<pre><code class="language-plaintext">{%- assign stripped_paragraph = section.settings.paragraph | strip_html | strip -%}
{% if stripped_paragraph != blank %}
  {{ stripped_paragraph }}
{% endif %}
</code></pre>
<p>Test every suggestion before merging — even the ones that look obviously right.</p>
<h3>Hard to dismiss or ignore comments</h3>
<p>I might be missing a feature, but I have not found a clean way to mark a comment as “intentionally ignored.” After I resolve a thread and push, Gemini often <strong>raises the same suggestion again</strong> on the next review.</p>
<p><strong>Note:</strong> While writing this, I saw the <code>@gemini-code-assist</code> mention in the <a href="https://github.com/marketplace/gemini-code-assist">marketplace docs</a>. I will try that next time to see if it reduces repeated comments.</p>
<h2>So Is It Worth It? 🤔</h2>
<p>Gemini as a PR reviewer is a useful <strong>first pass</strong> for Shopify theme work. It catches small, high-impact problems that are easy to skip — especially accessibility and readability, where a normal linter helps less.</p>
<p>Repeated comments and multi-pass reviews add steps. But if you expect an <strong>automated first reviewer</strong> — not a replacement for you or your team — it is worth installing and trying. 😉👉 <a href="https://github.com/marketplace/gemini-code-assist">Gemini Code Assist on GitHub Marketplace.</a></p>
]]></content:encoded></item><item><title><![CDATA[Svelte transitions – tried all 7 built-in ones]]></title><description><![CDATA[TL;DR
Svelte has 7 built-in transitions (fade, blur, fly, slide, scale, draw, crossfade). I tried each one with short examples and GIFs. Use this as a quick reference when you need a transition.
Intro]]></description><link>https://blog.yoniakabecky.com/svelte-transitions-tried-all-7-built-in-ones</link><guid isPermaLink="true">https://blog.yoniakabecky.com/svelte-transitions-tried-all-7-built-in-ones</guid><category><![CDATA[Svelte]]></category><category><![CDATA[transition]]></category><category><![CDATA[Frontend Development]]></category><dc:creator><![CDATA[Yoni Aoki]]></dc:creator><pubDate>Tue, 03 Mar 2026 12:26:53 GMT</pubDate><content:encoded><![CDATA[<h2><strong>TL;DR</strong></h2>
<p>Svelte has 7 built-in transitions (fade, blur, fly, slide, scale, draw, crossfade). I tried each one with short examples and GIFs. Use this as a quick reference when you need a transition.</p>
<h2>Intro</h2>
<p>While working on a Svelte project, I learned that Svelte has its own transition syntax. I tried all seven built-in transitions and noted how each behaves.</p>
<ul>
<li>Docs: <a href="https://svelte.dev/docs/svelte/transition">Template syntax</a>, <a href="https://svelte.dev/docs/svelte/svelte-transition">Reference</a></li>
</ul>
<h2>The transitions</h2>
<h3>1. fade</h3>
<pre><code class="language-svelte">&lt;button onclick={() =&gt; (visibleFade = !visibleFade)}&gt;Toggle&lt;/button&gt;
{#if visibleFade}
  &lt;div transition:fade={{ duration: 300 }}&gt;Fade&lt;/div&gt;
{/if}
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/65ab3be4d775f416312d4036/41562c1a-fc21-4176-803e-6837eb24f2aa.gif" alt="" style="display:block;margin:0 auto" />

<h3>2. blur</h3>
<pre><code class="language-svelte">&lt;button onclick={() =&gt; (visibleBlur = !visibleBlur)}&gt;Toggle&lt;/button&gt;
{#if visibleBlur}
  &lt;div transition:blur={{ duration: 400, amount: 8 }}&gt;Blur&lt;/div&gt;
{/if}
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/65ab3be4d775f416312d4036/b94ba11b-e50b-410d-b35f-27d13d73f666.gif" alt="" style="display:block;margin:0 auto" />

<h3>3. fly</h3>
<pre><code class="language-svelte">&lt;button onclick={() =&gt; (visibleFly = !visibleFly)}&gt;Toggle&lt;/button&gt;
{#if visibleFly}
  &lt;div transition:fly={{ y: 20, duration: 400 }}&gt;Fly&lt;/div&gt;
{/if}
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/65ab3be4d775f416312d4036/fddf34d5-2680-4fad-b074-7055c3c8d50e.gif" alt="" style="display:block;margin:0 auto" />

<h3>4. slide</h3>
<pre><code class="language-svelte">&lt;button onclick={() =&gt; (visibleSlide = !visibleSlide)}&gt;Toggle&lt;/button&gt;
{#if visibleSlide}
  &lt;div transition:slide={{ duration: 400, axis: 'y' }}&gt;Slide&lt;/div&gt;
{/if}
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/65ab3be4d775f416312d4036/43b8bb1c-b2e5-4a57-a874-678cbd9300cf.gif" alt="" style="display:block;margin:0 auto" />

<h3>5. scale</h3>
<pre><code class="language-svelte">&lt;button onclick={() =&gt; (visibleScale = !visibleScale)}&gt;Toggle&lt;/button&gt;
{#if visibleScale}
  &lt;div transition:scale={{ duration: 400, start: 0, opacity: 0.5 }}&gt;Scale&lt;/div&gt;
{/if}
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/65ab3be4d775f416312d4036/953e0039-da59-43c7-aa7e-16c95f54d41b.gif" alt="" style="display:block;margin:0 auto" />

<h3>6. draw</h3>
<pre><code class="language-svelte">&lt;button onclick={() =&gt; (visibleDraw = !visibleDraw)}&gt;Toggle&lt;/button&gt;
&lt;svg viewBox="0 0 100 40" width="120" height="48" aria-hidden="true"&gt;
  {#if visibleDraw}
    &lt;path
      transition:draw={{ duration: 800 }}
      fill="none"
      stroke="var(--color-accent, #89b4fa)"
      stroke-width="2"
      stroke-linecap="round"
      d="M 10 20 Q 50 5 90 20"
    /&gt;
  {/if}
&lt;/svg&gt;
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/65ab3be4d775f416312d4036/2f173d6c-01ab-4d81-b3ad-cd7c9a70bc6d.gif" alt="" style="display:block;margin:0 auto" />

<h3>7. crossfade</h3>
<p>As the docs say:</p>
<blockquote>
<p>The crossfade function creates a pair of transitions called send and receive.</p>
</blockquote>
<p>So crossfade is different: it works with <strong>two</strong> elements (one leaving, one entering). Use the same <code>key</code> on both so Svelte treats them as the same content moving. <strong>Fallback</strong> is used when there’s no pair (e.g. first load); here we use <code>fade</code> so it still fades in/out instead of popping.</p>
<pre><code class="language-svelte">&lt;script&gt;
	const [send, receive] = crossfade({
		duration: 400,
		fallback: (node, _params, intro) =&gt; fade(node, { duration: 400 })
	});
&lt;/script&gt;
&lt;button onclick={() =&gt; (crossfadePosition = crossfadePosition === 0 ? 1 : 0)}&gt;
  Move left ↔ right
&lt;/button&gt;
&lt;div&gt;
  &lt;div&gt;
    {#if crossfadePosition === 0}
      &lt;div in:receive={{ key: 'same' }} out:send={{ key: 'same' }}&gt;
        Moves
      &lt;/div&gt;
    {/if}
  &lt;/div&gt;
  &lt;div&gt;
    {#if crossfadePosition === 1}
      &lt;div in:receive={{ key: 'same' }} out:send={{ key: 'same' }}&gt;
        Moves
      &lt;/div&gt;
    {/if}
  &lt;/div&gt;
&lt;/div&gt;
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/65ab3be4d775f416312d4036/26bd992f-d331-4941-8d9a-94e257698c21.gif" alt="" style="display:block;margin:0 auto" />

<h3>8. Bonus - Custom transition</h3>
<p>If you need something custom, you can use the <code>transition</code> contract and build your own.</p>
<pre><code class="language-svelte">&lt;script lang="ts"&gt;
	function spinFade(node: HTMLElement, { duration = 500, delay = 0, turns = 1 }: SpinFadeParams) {
		const style = getComputedStyle(node);
		const opacity = +style.opacity;
		return {
			delay,
			duration,
			css: (t: number) =&gt; {
				const angle = (1 - t) * turns * 360;
				return `opacity: \({t * opacity}; transform: rotate(\){angle}deg);`;
			}
		};
	}
&lt;/script&gt;

&lt;button onclick={() =&gt; (visibleCustom = !visibleCustom)}&gt;Toggle&lt;/button&gt;
{#if visibleCustom}
  &lt;div transition:spinFade={{ duration: 500, turns: 1 }}&gt;Custom&lt;/div&gt;
{/if}
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/65ab3be4d775f416312d4036/76227eb3-f3d1-40a8-8ec4-9128635c54e0.gif" alt="" style="display:block;margin:0 auto" />

<h2>Quick comparison</h2>
<table>
<thead>
<tr>
<th>Transition</th>
<th>Best for</th>
<th>Note</th>
</tr>
</thead>
<tbody><tr>
<td>fade</td>
<td>Simple show/hide</td>
<td>Easiest, good default</td>
</tr>
<tr>
<td>blur</td>
<td>Focus / attention</td>
<td>Use <code>amount</code> to control</td>
</tr>
<tr>
<td>fly</td>
<td>Element “moves into” the place</td>
<td>x, y, opacity</td>
</tr>
<tr>
<td>slide</td>
<td>Element “expands/collapses”</td>
<td>axis: 'x' or 'y'</td>
</tr>
<tr>
<td>scale</td>
<td>Zoom in/out feel</td>
<td>start, opacity</td>
</tr>
<tr>
<td>draw</td>
<td>SVG paths only</td>
<td>Stroke animation</td>
</tr>
<tr>
<td>crossfade</td>
<td>Moving content between containers</td>
<td>Needs <code>send</code> / <code>receive</code></td>
</tr>
</tbody></table>
<h2>Conclusion</h2>
<ul>
<li><strong>fly</strong> = element moves in; <strong>slide</strong> = element grows/shrinks in place.</li>
</ul>
<p>Easy to wire up and no extra dependency – Probably I’d use them in a real product. For simple show/hide or list changes, the built-ins are enough; if you need something fancy, the custom transition API is still straightforward. Worth trying when you want a bit of polish without much code. 😆</p>
]]></content:encoded></item><item><title><![CDATA[Fixing Cropped Images Not Displaying Correctly]]></title><description><![CDATA[TL;DR 😼
When you replace an image with a cropped version but keep the same URL, the browser may show the old image because of caching. Add a timestamp (or unique suffix) to the filename so the URL changes—the image updates correctly. 📸
Situation
I ...]]></description><link>https://blog.yoniakabecky.com/fixing-cropped-images-not-displaying-correctly</link><guid isPermaLink="true">https://blog.yoniakabecky.com/fixing-cropped-images-not-displaying-correctly</guid><category><![CDATA[Frontend Development]]></category><category><![CDATA[React]]></category><dc:creator><![CDATA[Yoni Aoki]]></dc:creator><pubDate>Wed, 18 Feb 2026 08:52:54 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/aV5xrpB0bwQ/upload/ea5074a41bd66f723edf174ad0076be4.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-tldr">TL;DR 😼</h2>
<p>When you replace an image with a cropped version but keep the same URL, the browser may show the old image because of caching. Add a timestamp (or unique suffix) to the filename so the URL changes—the image updates correctly. 📸</p>
<h2 id="heading-situation">Situation</h2>
<p>I was building an <strong>image crop feature</strong> using <a target="_blank" href="https://github.com/ValentinH/react-easy-crop">react-easy-crop</a>. The library is simple and easy to use.</p>
<p>User selects an saved image → Crops it → Save the cropped image</p>
<p><strong>Problem:</strong> After saving, the component did <strong>not</strong> re-render with the new image. The old (uncropped) image kept showing.</p>
<h2 id="heading-digging-the-cause">Digging the Cause 🧐</h2>
<p>After <strong>reloading the page</strong>, the new image was displayed––so the cropped image was being saved correctly. I opened the browser's developer console and checked the image element. When I hovered over the URL, it showed the cropped image.</p>
<p>So...</p>
<ul>
<li><p>The image file content was different (cropped), but the URL was identical.</p>
</li>
<li><p>The browser treated it as the same resource and showed its <strong>cached</strong> version.</p>
</li>
<li><p><strong>Conclusion:</strong> If the URL does not change, the browser may not reload the image.</p>
</li>
</ul>
<h2 id="heading-how-i-fixed-it">How I fixed it 👩‍💻</h2>
<p>Make the URL <strong>different</strong> when the image is cropped so the browser fetches the new file. I went with a simple fix—we don't have many images in the app, also we don't use the crop feature often.</p>
<ul>
<li><p>Change the filename when you save the cropped image by adding a <strong>timestamp</strong> (or any unique value) before the extension.</p>
</li>
<li><p>New URL → No cache hit → Correct image is displayed.</p>
</li>
</ul>
<p>Example helper in TypeScript:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">/**
 * Adds a timestamp to the filename so the URL becomes unique.
 * Useful when replacing an image (e.g. after crop) to avoid browser cache.
 */</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> addTimestampToFileName = (fileName: <span class="hljs-built_in">string</span>): <span class="hljs-function"><span class="hljs-params">string</span> =&gt;</span> {
  <span class="hljs-keyword">const</span> timestamp = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>().getTime();
  <span class="hljs-keyword">const</span> lastDotIndex = fileName.lastIndexOf(<span class="hljs-string">"."</span>);

  <span class="hljs-keyword">if</span> (lastDotIndex === <span class="hljs-number">-1</span>) {
    <span class="hljs-keyword">return</span> <span class="hljs-string">`<span class="hljs-subst">${fileName}</span>_<span class="hljs-subst">${timestamp}</span>`</span>;
  }

  <span class="hljs-keyword">const</span> nameWithoutExtension = fileName.substring(<span class="hljs-number">0</span>, lastDotIndex);
  <span class="hljs-keyword">const</span> extension = fileName.substring(lastDotIndex + <span class="hljs-number">1</span>);
  <span class="hljs-keyword">return</span> <span class="hljs-string">`<span class="hljs-subst">${nameWithoutExtension}</span>_<span class="hljs-subst">${timestamp}</span>.<span class="hljs-subst">${extension}</span>`</span>;
};
</code></pre>
<p>Usage idea:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// Before: "photo.jpg"  →  After: "photo_1708234567890.jpg"</span>
<span class="hljs-keyword">const</span> newFileName = addTimestampToFileName(originalFileName);
<span class="hljs-comment">// Use newFileName when saving the cropped image and when setting the image URL</span>
</code></pre>
<p>When you use this new filename for the cropped image URL, the browser sees a new resource and shows the updated image. ✅</p>
<h2 id="heading-wrap-up">Wrap up 😎</h2>
<ul>
<li><p><strong>Same URL + changed file content</strong> → browser can keep showing the cached (old) image.</p>
</li>
<li><p><strong>Different URL</strong> → browser loads the new image.</p>
</li>
<li><p>For replaced images (e.g. after crop), make the URL unique (e.g. timestamp in the filename) so the UI updates correctly without hacks like forcing a component reload.</p>
</li>
</ul>
<p>If the cropped (or otherwise replaced) image does not update on screen, check whether the image URL changed. If it stays the same, the browser cache is likely the cause. Adding a timestamp to the filename is a simple way to get a new URL and reliable updates. 🎯</p>
]]></content:encoded></item><item><title><![CDATA[Creating Shopify Theme Sections with Cursor]]></title><description><![CDATA[This is not a tutorial. I'm not writing every detail of what I did, but I'll try to cover the main points.

TL;DR 😊
I asked AI (Cursor) to build a shopify theme section. It generated working Liquid, CSS, and JS and matched the theme structure I had ...]]></description><link>https://blog.yoniakabecky.com/creating-shopify-theme-sections-with-cursor</link><guid isPermaLink="true">https://blog.yoniakabecky.com/creating-shopify-theme-sections-with-cursor</guid><category><![CDATA[Shopify Development]]></category><category><![CDATA[cursor]]></category><category><![CDATA[Frontend Development]]></category><dc:creator><![CDATA[Yoni Aoki]]></dc:creator><pubDate>Sat, 07 Feb 2026 03:04:32 GMT</pubDate><content:encoded><![CDATA[<blockquote>
<p>This is not a tutorial. I'm not writing every detail of what I did, but I'll try to cover the main points.</p>
</blockquote>
<h2 id="heading-tldr">TL;DR 😊</h2>
<p>I asked AI (Cursor) to build a shopify theme section. It generated working Liquid, CSS, and JS and matched the theme structure I had in mind.<br />The downside: without clear instructions, it added more files and options than I needed. Review the output and refine your prompts is bit tough, so I decided make it small and make it grow.</p>
<hr />
<h2 id="heading-where-we-left-off">Where we left off</h2>
<p>In the <a target="_blank" href="https://blog.yoniakabecky.com/i-failed-to-create-a-shopify-theme-from-scratch">last post</a>, I set up the Skeleton theme with</p>
<pre><code class="lang-bash">shopify theme init
</code></pre>
<p>While cloning, you can also select an LLM instruction file. If you plan to use AI, pick one or use all of them.</p>
<blockquote>
<p>Skeleton is a minimal Shopify starter theme with very few opinions, which makes it easier to experiment and learn the structure.</p>
</blockquote>
<h2 id="heading-digging-into-the-theme-structure">Digging into the theme structure</h2>
<p>The readme describes each directory like this:</p>
<pre><code class="lang-bash">├── assets          <span class="hljs-comment"># Stores static assets (CSS, JS, images, fonts, etc.)</span>
├── blocks          <span class="hljs-comment"># Reusable, nestable, customizable UI components</span>
├── config          <span class="hljs-comment"># Global theme settings and customization options</span>
├── layout          <span class="hljs-comment"># Top-level wrappers for pages (layout templates)</span>
├── locales         <span class="hljs-comment"># Translation files for theme internationalization</span>
├── sections        <span class="hljs-comment"># Modular full-width page components</span>
├── snippets        <span class="hljs-comment"># Reusable Liquid code or HTML fragments</span>
└── templates       <span class="hljs-comment"># Templates combining sections to define page</span>
</code></pre>
<p>To me it feels similar to atomic design:</p>
<ul>
<li><p><strong>snippets</strong> → Atoms</p>
</li>
<li><p><strong>blocks</strong> → Molecules</p>
</li>
<li><p><strong>sections</strong> → Organisms</p>
</li>
<li><p><strong>templates</strong> → Templates</p>
</li>
</ul>
<p>(This is how I understand it; it might not be the official mapping.)</p>
<p>One advantage of the Skeleton theme is the default files. For example, it comes with:</p>
<ul>
<li><p>assets/critical.css</p>
</li>
<li><p>snippets/css-variables.liquid</p>
</li>
</ul>
<p>They give you basic styles without going overboard, so it's easy to overwrite. They also help you set up variables and such. (Before cloning Skeleton theme, I wasn't sure where to put variables😅)</p>
<h2 id="heading-using-ai">Using AI</h2>
<p>In normal frontend work I would build most snippets first. But I was new to Shopify theme development, and I had only designed the top page. So I decided to ask AI what it would do.</p>
<p>I use the Cursor editor (Auto) for most of the work.</p>
<p>The theme already has a default LLM instruction file, so I didn't add extra rules at first. I could have added my own coding preferences, but since I was new to Shopify and Liquid, I chose not to. (I added some later.)</p>
<h2 id="heading-first-try-slideshow-section">First try: slideshow section</h2>
<p>The client wanted a slideshow (carousel) on the top page. So I tried this prompt first:</p>
<pre><code class="lang-bash">I want to implement a slideshow on the top page.
Make it simple and easy to use.

Ref: https://github.com/Shopify/dawn
</code></pre>
<p>I know it's pretty general. But I wanted to see what would come out and give myself time to understand the result.</p>
<p>It generated:</p>
<ul>
<li><p>assets/component-slider.css</p>
</li>
<li><p>assets/section-slideshow.css</p>
</li>
<li><p>assets/slider.js</p>
</li>
<li><p>sections/slideshow.liquid</p>
</li>
<li><p>snippets/icon-next.liquid</p>
</li>
<li><p>snippets/icon-prev.liquid</p>
</li>
</ul>
<p>Yep, it worked 🤯 (It also modified related files.)</p>
<blockquote>
<p><strong>Note:</strong> As of Feb 6, 2026 (when I'm writing this), the same prompt may not create the top three files anymore, because the LLM instructions have been updated since I first tried.</p>
</blockquote>
<h2 id="heading-what-worked-well">What worked well ✅</h2>
<ol>
<li><p><strong>It worked.</strong> The slideshow ran as expected. That matters. 😏</p>
</li>
<li><p><strong>The structure matched what I had in mind.</strong> Files landed in the right places (sections, snippets, assets). So my mental model of the theme (snippets vs sections vs assets) was mostly correct.</p>
</li>
<li><p><strong>Icons as snippets made sense.</strong> I wasn't sure whether icons should live in <code>assets</code> or <code>snippets</code>. Seeing the AI put <code>icon-next</code> and <code>icon-prev</code> in snippets helped me decide: small, reusable UI bits go in snippets.</p>
</li>
<li><p><strong>Liquid objects and filters.</strong> Not in the slideshow, but in other sections, the AI used Liquid objects and filters well. I don't have time to read the whole Liquid docs. When it uses filters or objects I don't know, it helps me catch up and learn.</p>
</li>
<li><p><strong>I could see how the AI behaved.</strong> By looking at the generated code, I noticed patterns (e.g. separate CSS/JS files, lots of schema options). That helped me write better prompts and rules later.</p>
</li>
</ol>
<h2 id="heading-what-was-less-ideal-and-i-fixed-it">What was less ideal (and I fixed it) 😥</h2>
<p>If I don't set rules or write clear instructions, the AI tends to:</p>
<ol>
<li><p><strong>Create CSS and JavaScript in separate files.</strong><br /> Sometimes separate files help readability, but for simple sections they're often not necessary. You can keep styles and scripts inside the section with <code>{% stylesheet %}</code> and <code>{% javascript %}</code> instead.</p>
</li>
<li><p><strong>Rely on JavaScript more than I wanted.</strong><br /> Pure CSS can do a lot nowadays, so many effects don't need JS.</p>
</li>
<li><p><strong>Add many options in the schema</strong> (settings in the theme editor).<br /> Saying "keep it simple" in the prompt isn't enough to minimize options. You have to be explicit about what you want (or don't want).<br /> For example, I got something like adjustable height with <em>min, max, step</em>, and <em>unit</em>. For reusability, a few presets (e.g. <em>sm</em>, <em>md</em>, <em>lg</em>) are enough. Too many settings in the theme editor can confuse non-developer users.</p>
</li>
</ol>
<p>I had attached the Dawn theme as a reference, so that likely pushed it toward “full-featured” output. 😅 I had to trim the generated code to remove what I didn't need.</p>
<p><strong>Takeaway:</strong> Be clear about what you want for each section. Otherwise you might accept the first result without thinking. (I'm not aiming for “vibe coding”; I want to stay in control.)</p>
<p>After reviewing the code for a while, I merged the change.</p>
<h2 id="heading-conclusion">Conclusion ✨</h2>
<p>That first slideshow was a good way to learn how Liquid and the theme structure fit together. These days I often do the opposite: I ask AI to create small parts first, add an option, add another, and let it grow into a section step by step. So you don't have to go "one big section, then trim." You can also start small and build up. 🛠️</p>
]]></content:encoded></item><item><title><![CDATA[Remove Spam Notification on GitHub]]></title><description><![CDATA[What happened
I got a GitHub notification that I couldn't see nor remove. The repository name was plasma-network/plasma.to. At first, I thought one of my coworkers created a new repository. But it wasn't. I tried to open the repository, but I couldn'...]]></description><link>https://blog.yoniakabecky.com/remove-spam-notification-on-github</link><guid isPermaLink="true">https://blog.yoniakabecky.com/remove-spam-notification-on-github</guid><category><![CDATA[GitHub]]></category><category><![CDATA[gh]]></category><dc:creator><![CDATA[Yoni Aoki]]></dc:creator><pubDate>Fri, 23 Jan 2026 23:23:19 GMT</pubDate><content:encoded><![CDATA[<h2 id="heading-what-happened">What happened</h2>
<p>I got a GitHub notification that I couldn't see nor remove. The repository name was <code>plasma-network/plasma.to</code>. At first, I thought one of my coworkers created a new repository. But it wasn't. I tried to open the repository, but I couldn't.</p>
<p>So I googled for a few days.</p>
<p>I found <a target="_blank" href="https://github.com/orgs/community/discussions/174831">this discussion</a> on GitHub's community forum where others were experiencing the similar issue.</p>
<p>So it was spam. 👾</p>
<h2 id="heading-the-problem">The problem</h2>
<p>The notification badge appeared in my GitHub interface, but clicking on it didn't reveal the actual notification. The repository name remained visible in my notifications page, but I couldn't access or remove it through the web interface. This was frustrating because the badge kept showing up, and I couldn't get rid of it.</p>
<h2 id="heading-the-solution">The solution</h2>
<p>I followed the solution from <a target="_blank" href="https://github.com/orgs/community/discussions/174831">the GitHub community discussion</a>. The contributors there explained that GitHub's CLI (<code>gh</code>) provides the tools needed to manage notifications programmatically. Here's how I followed their solution step by step:</p>
<h3 id="heading-1-remove-the-notification-badge">1. Remove the notification badge</h3>
<p>First, I marked all notifications as read to remove the blue badge:</p>
<pre><code class="lang-bash">gh api /notifications -X PUT -f <span class="hljs-built_in">read</span>=<span class="hljs-literal">true</span>
</code></pre>
<p>This removed the blue badge 🔵 from the interface.</p>
<p>But the repository name still remained in repositories section.</p>
<h3 id="heading-2-check-the-notification-list">2. Check the notification list</h3>
<p>To see all my notifications, I ran:</p>
<pre><code class="lang-bash">gh api --paginate <span class="hljs-string">'notifications?all=true&amp;per_page=100'</span> | jq -r <span class="hljs-string">'.[].repository.full_name'</span> | sort -u
</code></pre>
<p>Then I got:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">command</span> not found: jq
</code></pre>
<p>...🫠</p>
<h3 id="heading-3-install-jq">3. Install jq</h3>
<p>I needed <code>jq</code> to parse the JSON output. <code>jq</code> is a lightweight command-line JSON processor.</p>
<p>Visit <a target="_blank" href="https://jqlang.org/">https://jqlang.org/</a> for more information.</p>
<p>On macOS, I installed it with:</p>
<pre><code class="lang-bash">brew install jq
</code></pre>
<h3 id="heading-4-check-the-notification-list-again">4. Check the notification list again</h3>
<p>After installing <code>jq</code>, I ran the same command again:</p>
<pre><code class="lang-bash">gh api --paginate <span class="hljs-string">'notifications?all=true&amp;per_page=100'</span> | jq -r <span class="hljs-string">'.[].repository.full_name'</span> | sort -u
</code></pre>
<p>Now I got a list of repositories:</p>
<pre><code class="lang-bash">...
plasma-network/plasma.to
yoniakabecky/my-personal-repository
</code></pre>
<p>Yay! 🙌 I could see the spam repository in the list.</p>
<h3 id="heading-5-get-the-notification-id">5. Get the notification ID</h3>
<p>To delete the specific notification, I needed its ID. I filtered for the spam repository:</p>
<pre><code class="lang-bash">gh api --paginate <span class="hljs-string">'notifications?all=true&amp;per_page=100'</span> | jq -r <span class="hljs-string">'.[] | select(.repository.full_name == "plasma-network/plasma.to") | "\(.id) - \(.subject.title) - unread: \(.unread)"'</span>
</code></pre>
<p>The output showed:</p>
<pre><code class="lang-bash">19200066437 - Plasma Foundation | Over USD 2.4B TVL &amp; 54.02% APY, XPL and Staking Rewards - unread: <span class="hljs-literal">false</span>
</code></pre>
<p>I had the notification ID: <code>19200066437</code>.</p>
<h3 id="heading-6-delete-the-notification">6. Delete the notification</h3>
<p>Finally, I deleted the notification thread:</p>
<pre><code class="lang-bash">gh api --method DELETE <span class="hljs-string">"notifications/threads/19200066437"</span>
</code></pre>
<p>The spam notification was gone! 🎉</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>If you encounter a persistent GitHub notification that you can't remove through the web interface, use GitHub CLI. The combination of <code>gh api</code> and <code>jq</code> gives you full control over your notifications, even when the web interface fails.</p>
<p>The key commands to remember:</p>
<ul>
<li><p>Mark all as read: <code>gh api /notifications -X PUT -f read=true</code></p>
</li>
<li><p>List all notifications: <code>gh api --paginate 'notifications?all=true&amp;per_page=100' | jq -r '.[].repository.full_name' | sort -u</code></p>
</li>
<li><p>Delete a specific notification: <code>gh api --method DELETE "notifications/threads/{THREAD_ID}"</code></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[I Failed to Create a Shopify Theme from Scratch]]></title><description><![CDATA[Probably this will be a series - Creating Shopify Theme with AI ✍️

TL;DR 😊
If you are new to creating a Shopify theme, clone the skeleton theme to start. It saves time, avoids missing files, and keeps the project simple.
Beginning
I was curious abo...]]></description><link>https://blog.yoniakabecky.com/i-failed-to-create-a-shopify-theme-from-scratch</link><guid isPermaLink="true">https://blog.yoniakabecky.com/i-failed-to-create-a-shopify-theme-from-scratch</guid><category><![CDATA[shopify]]></category><category><![CDATA[Frontend Development]]></category><dc:creator><![CDATA[Yoni Aoki]]></dc:creator><pubDate>Sat, 17 Jan 2026 04:01:20 GMT</pubDate><content:encoded><![CDATA[<blockquote>
<p>Probably this will be a series - Creating Shopify Theme with AI ✍️</p>
</blockquote>
<h2 id="heading-tldr">TL;DR 😊</h2>
<p>If you are new to creating a Shopify theme, clone the <a target="_blank" href="https://github.com/Shopify/skeleton-theme">skeleton theme</a> to start. It saves time, avoids missing files, and keeps the project simple.</p>
<h2 id="heading-beginning">Beginning</h2>
<p>I was curious about Shopify, but I hesitated to study Liquid even though I already knew the Shopify developer market is kinda huge.</p>
<p>When my friend asked me to help with her e-commerce site, we compared a few options.</p>
<p>My first idea was <strong>Square</strong> because her shop already uses it for payments. But when she researched it, some features were missing for her needs. She checked <strong>Shopify</strong> and found the features she wanted. So we picked Shopify for the project.</p>
<h2 id="heading-why-im-creating-theme-from-scratch">Why I'm creating theme from scratch</h2>
<p>I explored default themes and realized they offer too many settings for one simple section.</p>
<p>Because default themes are fully customizable, you can adjust almost anything. That level of customization confused me and felt overwhelming. And my friend is not familiar with any of that.</p>
<p>My goal is to let my friend update the Shopify theme easily. So the default theme was no longer an option.</p>
<h2 id="heading-liquid-vs-remix">Liquid vs Remix</h2>
<p>As a React developer, building with Remix feels easier (I am talking about v2, not v3). But this is my first time using Shopify, and I was unsure how much we could customize in the admin panel if I used Remix. Since my goal was creating an easy-to-use theme for my friend, I decided to study Liquid.</p>
<p>Around early 2025, one of my coworkers was very passionate about the Cursor editor. He kept telling me about it. I decided this was a perfect moment to try Cursor. (This was another reason I chose Liquid.)</p>
<h2 id="heading-first-hurdles">First hurdles</h2>
<p>At first, I watched several YouTube videos about Shopify theme development. At a glance, it did not look too complicated. So I just started development.</p>
<p>I followed the <a target="_blank" href="https://shopify.dev/docs/storefronts/themes/getting-started/create">official tutorial</a>.</p>
<ol>
<li><p>Install CLI</p>
</li>
<li><p>Create partner account</p>
</li>
<li><p>Create a store</p>
</li>
<li><p>Initialize theme</p>
</li>
</ol>
<p>Everything was fine up to this point.</p>
<p>I learned the hard way that the theme will not run unless you keep certain files and directories. I struggled to start the Shopify server because I missed some required directories and files.</p>
<p>After a few rounds of trial and error, I finally managed to start a local development server. 🎉 But getting the server running was just the beginning. Things got messy as I continued building the theme.</p>
<h2 id="heading-the-messy-middle">The messy middle</h2>
<p>I worked on the theme little by little between other jobs. I kept referring to the Dawn theme, which has a lot of complex code. I also used the Cursor editor to build pages and features.</p>
<p>The result became too complicated. At first, I was just experimenting, but as the code grew, it became messy. After a few months, I realized that key files were still missing to run the shop.</p>
<p>At that moment, I felt I needed to start over... 😵‍💫</p>
<h2 id="heading-skeleton-theme">Skeleton theme</h2>
<p>I did not know that Shopify released the Skeleton theme in May 2025. (Maybe it was there before. I just didn't know.) When I ran <code>shopify theme init</code> again, it offered me Skeleton theme which is a minimal theme (plus some rules for the IDEs).</p>
<p>It solved my biggest problem: required files. Now I only need to customize the pages.</p>
<p>Since I reset my theme to the Skeleton theme, I needed to add snippets and sections again, but that was not a big issue. I just had to add simplified code carefully.</p>
<h2 id="heading-conclusion">Conclusion ✨</h2>
<p>I failed my first attempt at building a Shopify theme from scratch, but the Skeleton theme gave me a fresh start. Nothing fancy––just a minimal theme.If you're starting your first Shopify theme, don't make the same mistake I did.😉 Start with the Skeleton theme, so you can focus on customization instead of configuration.</p>
]]></content:encoded></item><item><title><![CDATA[How to Fix "Claude Command Not Found" Error 🔧]]></title><description><![CDATA[TL;DR
After updating Cursor editor, Claude Code CLI stopped working with a command not found error. The solution is to reinstall Claude Code and fix PATH configuration.
Quick fix:

Add to PATH: export PATH="$HOME/.local/bin:$PATH"

Reload shell: sour...]]></description><link>https://blog.yoniakabecky.com/how-to-fix-claude-command-not-found-error</link><guid isPermaLink="true">https://blog.yoniakabecky.com/how-to-fix-claude-command-not-found-error</guid><category><![CDATA[claude.ai]]></category><dc:creator><![CDATA[Yoni Aoki]]></dc:creator><pubDate>Sat, 09 Aug 2025 14:13:22 GMT</pubDate><content:encoded><![CDATA[<h2 id="heading-tldr">TL;DR</h2>
<p>After updating Cursor editor, Claude Code CLI stopped working with a <code>command not found</code> error. The solution is to reinstall Claude Code and fix PATH configuration.</p>
<p><strong>Quick fix:</strong></p>
<ol>
<li><p>Add to PATH: <code>export PATH="$HOME/.local/bin:$PATH"</code></p>
</li>
<li><p>Reload shell: <code>source ~/.zshrc</code></p>
</li>
<li><p>Reinstall: <code>curl -fsSL</code> <a target="_blank" href="https://claude.ai/install.sh"><code>https://claude.ai/install.sh</code></a> <code>| bash -s latest</code></p>
</li>
</ol>
<h2 id="heading-the-problem">The Problem 🚨</h2>
<p>I use Claude Code CLI for my work projects through my company account. Yesterday everything was working fine. Today I updated Cursor editor, and suddenly:</p>
<pre><code class="lang-typescript">zsh: command not found: claude
</code></pre>
<p>Wait... what? 🤔</p>
<h2 id="heading-debugging">Debugging 🕵️</h2>
<h3 id="heading-following-the-error-messages">Following the Error Messages</h3>
<p>I saw this auto-update error message in Cursor:</p>
<pre><code class="lang-typescript"> ✗ Auto-update failed · Try claude doctor or npm i -g <span class="hljs-meta">@anthropic</span>-ai/claude-code
</code></pre>
<p>Naturally, I tried both suggestions:</p>
<ul>
<li><p><code>claude doctor</code> → Of course, <code>claude</code> wasn't working, this also not working.</p>
</li>
<li><p><code>npm i -g @anthropic-ai/claude-code</code> → Got a directory conflict error.</p>
<pre><code class="lang-typescript">  ENOTEMPTY: directory not empty, rename <span class="hljs-string">'/Users/xyz/.nvm/versions/node/v22.14.0/lib/node_modules/@anthropic-ai/claude-code'</span> -&gt; <span class="hljs-string">'/Users/xyz/.nvm/versions/node/v22.14.0/lib/node_modules/@anthropic-ai/.claude-code-MgzGe2Q4'</span>
</code></pre>
</li>
</ul>
<h3 id="heading-more-digging">More digging</h3>
<p>I started searching this error. I found a Japanese tech blog <a target="_blank" href="https://zenn.dev/shusaku009/articles/claude-code-update">Zenn</a>, which I tried but it didn't fix the problem. But I got anthropic url and that helps.</p>
<h3 id="heading-install-latest-version">Install latest version</h3>
<p>I read official troubleshooting.</p>
<p><a target="_blank" href="https://docs.anthropic.com/en/docs/claude-code/troubleshooting">https://docs.anthropic.com/en/docs/claude-code/troubleshooting</a></p>
<p>I tried their <a target="_blank" href="https://docs.anthropic.com/en/docs/claude-code/troubleshooting#recommended-solution%3A-native-claude-code-installation">recommended solution</a>.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Install latest version</span>
curl -fsSL https://claude.ai/install.sh | bash -s latest
</code></pre>
<p>result is...</p>
<pre><code class="lang-typescript">⚠ Setup notes:
  • ~<span class="hljs-regexp">/.local/</span>bin is not <span class="hljs-keyword">in</span> your PATH
  • Add it by running: <span class="hljs-keyword">export</span> PATH=<span class="hljs-string">"~/.local/bin:$PATH"</span>


✔  Claude Code successfully installed!

  Version: <span class="hljs-number">1.0</span><span class="hljs-number">.72</span>

  Location: ~<span class="hljs-regexp">/.local/</span>bin/claude


  Next: Run claude --help to get started

✅ Installation complete!
</code></pre>
<p>Looks good! 🙌 So I tried to verify the installation.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Verify the installation</span>
<span class="hljs-built_in">which</span> claude
</code></pre>
<p>result is...</p>
<pre><code class="lang-bash">claude not found
</code></pre>
<p>I kind of knew it 🫠 But this time I got helpful <em>setup notes</em>, which was a plus!</p>
<h3 id="heading-update-path">Update PATH</h3>
<pre><code class="lang-typescript">⚠ Setup notes:
  • ~<span class="hljs-regexp">/.local/</span>bin is not <span class="hljs-keyword">in</span> your PATH
  • Add it by running: <span class="hljs-keyword">export</span> PATH=<span class="hljs-string">"~/.local/bin:$PATH"</span>
</code></pre>
<p>So I added it.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Open file in edit mode</span>
nano ~/.zshrc

<span class="hljs-comment"># Add this line and save it</span>
<span class="hljs-built_in">export</span> PATH=<span class="hljs-string">"<span class="hljs-variable">$HOME</span>/.local/bin:<span class="hljs-variable">$PATH</span>"</span>

<span class="hljs-comment">#Then apply the change</span>
<span class="hljs-built_in">source</span> ~/.zshrc

<span class="hljs-comment">#Check if the path is saved</span>
<span class="hljs-built_in">echo</span> <span class="hljs-variable">$PATH</span>
</code></pre>
<p>After I confirmed <code>.local/bin:</code> is added, I run install command again.</p>
<pre><code class="lang-typescript">curl -fsSL https:<span class="hljs-comment">//claude.ai/install.sh | bash -s latest</span>
</code></pre>
<p>This time there is no setup notes. 🥳</p>
<pre><code class="lang-typescript">✔  Claude Code successfully installed!

  Version: <span class="hljs-number">1.0</span><span class="hljs-number">.72</span>

  Location: ~<span class="hljs-regexp">/.local/</span>bin/claude


  Next: Run claude --help to get started

✅ Installation complete!
</code></pre>
<p>And when I run <code>claude</code>, it started without <code>Auto-update failed</code> message.</p>
<h2 id="heading-conclusion-amp-lessons-learned">Conclusion &amp; Lessons Learned 🎓</h2>
<p><strong>What happened:</strong></p>
<ul>
<li>Updating Cursor editor somehow interfered with my existing Claude Code CLI installation.</li>
</ul>
<p><strong>What I learned:</strong></p>
<ul>
<li><p>Editor updates can sometimes affect CLI tools unexpectedly</p>
</li>
<li><p>Always check official documentation when troubleshooting</p>
</li>
<li><p>PATH configuration is crucial for CLI tools to work properly</p>
</li>
<li><p>Clean reinstallation often works better than trying to fix corrupted installs</p>
</li>
</ul>
<p>Now I'm ready to get back to productive coding! 🚀</p>
]]></content:encoded></item><item><title><![CDATA[A Pitfall: Date Sorting]]></title><description><![CDATA[To speed up my blogging and be more honest about my learning process, I've decided to start sharing even the small (and sometimes silly) mistakes I make. Thanks for reading with kind eyes. 😅

Quick question: Can you spot what's wrong with this code?...]]></description><link>https://blog.yoniakabecky.com/a-pitfall-date-sorting</link><guid isPermaLink="true">https://blog.yoniakabecky.com/a-pitfall-date-sorting</guid><category><![CDATA[TypeScript]]></category><category><![CDATA[JavaScript]]></category><dc:creator><![CDATA[Yoni Aoki]]></dc:creator><pubDate>Tue, 27 May 2025 14:22:58 GMT</pubDate><content:encoded><![CDATA[<blockquote>
<p><em>To speed up my blogging and be more honest about my learning process, I've decided to start sharing even the small (and sometimes silly) mistakes I make. Thanks for reading with kind eyes. 😅</em></p>
</blockquote>
<p><strong>Quick question: Can you spot what's wrong with this code?</strong></p>
<pre><code class="lang-typescript"><span class="hljs-keyword">type</span> Data = {
  value: <span class="hljs-built_in">string</span>;
  createdAt: <span class="hljs-built_in">string</span>;
};

<span class="hljs-keyword">const</span> sorted = (data <span class="hljs-keyword">as</span> Data[]).sort(<span class="hljs-function">(<span class="hljs-params">a, b</span>) =&gt;</span> b.createdAt - a.createdAt);
</code></pre>
<p>After my team asked me to use the Cursor editor, this was the code it suggested with a single Tab. I couldn't notice the mistake at first. It worked fine on my local environment, so I pushed it to the staging. Then, we noticed the sorting wasn’t working correctly. 😱</p>
<h2 id="heading-the-problem">The Problem</h2>
<p>The first thing I noticed was the cache data difference. When reloading the query, the created time cache always has a time zone. However, after overwriting the cache with subscription data, the created time doesn't have a time zone. And that is causing wrong results.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// Query data</span>
createdAt: <span class="hljs-string">"2024-03-20T10:00:00+09:00"</span>

<span class="hljs-comment">// Subscription data</span>
createdAt: <span class="hljs-string">"2024-03-20T01:00:00Z"</span>
</code></pre>
<p>Most readers probably noticed that the time is actually the same. Just format is different. However, I focused too much on why the time format changed. So it took way more time to figure out.</p>
<h3 id="heading-why-different-time-formats">Why Different Time Formats?</h3>
<p>This wasn't a direct issue, but the reason for the different time formats lies in how the data is handled:</p>
<ul>
<li><p>Subscription data comes through WebSocket connections</p>
</li>
<li><p>WebSocket protocols often standardize on UTC (`Z`) to avoid timezone confusion</p>
</li>
</ul>
<p>So when I use the subscription value to update the cache, it uses UTC.</p>
<h2 id="heading-the-solution">The Solution</h2>
<p>In short, sorting with string value is the problem. (Format is not the problem)</p>
<p>Here's the correct way to sort date strings:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// Wrong ❌</span>
<span class="hljs-keyword">const</span> sorted = (data <span class="hljs-keyword">as</span> Data[]).sort(<span class="hljs-function">(<span class="hljs-params">a, b</span>) =&gt;</span> b.createdAt - a.createdAt);

<span class="hljs-comment">// Right ✅</span>
<span class="hljs-keyword">const</span> sorted = (data <span class="hljs-keyword">as</span> Data[]).sort(
  <span class="hljs-function">(<span class="hljs-params">a, b</span>) =&gt;</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(b.createdAt).getTime() - <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(a.createdAt).getTime()
);
</code></pre>
<h3 id="heading-note-string-subtraction">Note: String Subtraction</h3>
<p>The subtraction operator (`-`) attempts to convert strings to numbers. If both sides are strings results in <code>NaN</code> because these strings can't be converted to valid numbers.</p>
<pre><code class="lang-typescript"><span class="hljs-built_in">console</span>.log(<span class="hljs-string">"5"</span> - <span class="hljs-number">3</span>);
<span class="hljs-comment">// Expected output: 2</span>

<span class="hljs-built_in">console</span>.log(<span class="hljs-string">"hello"</span> - <span class="hljs-string">"hello"</span>);
<span class="hljs-comment">// Expected output: NaN</span>
</code></pre>
<p>This raises another question: "If string subtraction results in <code>NaN</code>, why did it seem to work in my local environment?"</p>
<p>I really don't know the reason for this. Maybe it just API result was sorted as I expected... 🤷‍♀️</p>
<h2 id="heading-ai-suggestion-distraction">AI Suggestion Distraction</h2>
<p>Another reason that I couldn't point out the problem is ... I think ... AI suggestion.</p>
<p>The Cursor editor is powerful — it helps me modify multiple lines with a single Tab press and automatically suggests code.</p>
<p>After I fixed the problem, I realized that a few lines up from the problem code, I was doing a similar sort. Which I wrote before I switched to the cursor editor. So if I was writing a code, this might not have happened.</p>
<p>The thing is, I probably haven’t been using it properly. That said, I don’t really have the option not to use it...</p>
<h2 id="heading-wrap-up">Wrap up</h2>
<ol>
<li><p><strong>Always Convert to Date Objects</strong></p>
</li>
<li><p><strong>Don't rely too much on convenient features</strong></p>
</li>
</ol>
<p>Small bugs can sneak in easily, but I think writing about it will help me next time 😊</p>
<h2 id="heading-resources">Resources</h2>
<ul>
<li><p><a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Subtraction">MDN Web Docs: Subtraction</a></p>
</li>
<li><p><a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date">MDN Web Docs: Date</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Improving Code Readability Using GraphQL Fragment Colocation]]></title><description><![CDATA[TL;DR

☝
It will eliminate over-fetching, improve readability, and make it easier to maintain



✌
It ensures that only necessary data is queried and that your code remains modular and efficient


Background
At my job, I worked on a Next.js applicati...]]></description><link>https://blog.yoniakabecky.com/making-graphql-more-readable-with-fragment-colocation</link><guid isPermaLink="true">https://blog.yoniakabecky.com/making-graphql-more-readable-with-fragment-colocation</guid><category><![CDATA[GraphQL]]></category><category><![CDATA[React]]></category><category><![CDATA[Next.js]]></category><dc:creator><![CDATA[Yoni Aoki]]></dc:creator><pubDate>Sun, 02 Feb 2025 15:43:32 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/xrVDYZRGdw4/upload/5b0f6d5c1299ce37e728d5d4288ceb93.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-tldr">TL;DR</h2>
<div data-node-type="callout">
<div data-node-type="callout-emoji">☝</div>
<div data-node-type="callout-text">It will eliminate over-fetching, improve readability, and make it easier to maintain</div>
</div>

<div data-node-type="callout">
<div data-node-type="callout-emoji">✌</div>
<div data-node-type="callout-text">It ensures that only necessary data is queried and that your code remains modular and efficient</div>
</div>

<h2 id="heading-background">Background</h2>
<p>At my job, I worked on a Next.js application that used GraphQL. We also used <a target="_blank" href="https://the-guild.dev/graphql/codegen/docs/getting-started">GraphQL codegen</a> for TypeScript types.</p>
<p>Initially, a senior developer with a mobile development background introduced a module-based pattern, which led us to create models for each GraphQL type.</p>
<h3 id="heading-the-initial-approach-using-models">The Initial Approach: Using Models</h3>
<p>This is a simplified version of the <code>UserModel</code>:</p>
<pre><code class="lang-ts"><span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> UserModel <span class="hljs-keyword">implements</span> User {
  <span class="hljs-keyword">public</span> <span class="hljs-keyword">readonly</span> id: Scalars[<span class="hljs-string">'ID'</span>];
  <span class="hljs-keyword">public</span> <span class="hljs-keyword">readonly</span> firstName: Scalars[<span class="hljs-string">'String'</span>];
  <span class="hljs-keyword">public</span> <span class="hljs-keyword">readonly</span> lastName: Scalars[<span class="hljs-string">'String'</span>];

  <span class="hljs-keyword">constructor</span>(<span class="hljs-params">user: User | <span class="hljs-literal">undefined</span></span>) {
    <span class="hljs-built_in">this</span>.id = user?.id ?? <span class="hljs-string">''</span>;
    <span class="hljs-built_in">this</span>.firstName = user?.firstName ?? <span class="hljs-string">''</span>;
    <span class="hljs-built_in">this</span>.lastName = user?.lastName ?? <span class="hljs-string">''</span>;
  }

  get fullName(): <span class="hljs-built_in">string</span> {
    <span class="hljs-keyword">return</span> <span class="hljs-string">`<span class="hljs-subst">${<span class="hljs-built_in">this</span>.firstName}</span> <span class="hljs-subst">${<span class="hljs-built_in">this</span>.lastName}</span>`</span>;
  }
}
</code></pre>
<p>This <code>UserModel</code> implements auto generated <code>User</code> type. (So eventually includes all user tables fields.) And defined some private methods.</p>
<p>And for the GraphQL, we kept queries in a single folder:</p>
<pre><code class="lang-graphql">// src/graphql/queries/user.graphql

<span class="hljs-keyword">query</span> User {
  user {
    <span class="hljs-punctuation">...UserDetails
</span>  }
}

<span class="hljs-keyword">fragment</span> UserDetails {
  id
  firstName
  lastName
  email
  address
  phoneNumber
  <span class="hljs-comment"># Fetching all fields</span>
}
</code></pre>
<p>We were using fragments but read all fields to make it easier to reference them in the models.</p>
<p>And this is how we refer to user’s full name:</p>
<pre><code class="lang-ts"><span class="hljs-keyword">const</span> user = <span class="hljs-keyword">new</span> UserModel(graphql_result);
user.fullName();
</code></pre>
<p>For this example, using only <code>firstName</code> and <code>lastName</code> is straightforward. However, in real applications, models tend to be more complex.</p>
<p>Since we were querying all fields and including them in the models, we were unsure which properties were necessary in different parts of the app. As a result, we often ended up over-fetching data, which negated the benefits of GraphQL. 🫣</p>
<h2 id="heading-the-shift-seeking-a-better-approach">The Shift: Seeking a better approach</h2>
<p>After a few years, the original project was canceled. However, I got the opportunity to build a new app from scratch with the same tech stack. The senior developer had left, and I became the lead front-end developer. 😎 This time, I decided to get rid of the module-based pattern entirely.</p>
<p>Initially, I wrote helper functions:</p>
<pre><code class="lang-ts"><span class="hljs-comment">// src/helpers/user.ts</span>

<span class="hljs-keyword">const</span> getFullName = <span class="hljs-function">(<span class="hljs-params">user: User</span>) =&gt;</span> {
  <span class="hljs-keyword">return</span> <span class="hljs-string">`<span class="hljs-subst">${user.firstName}</span> <span class="hljs-subst">${user.lastName}</span>`</span>;
};
</code></pre>
<p>However, I still kept using the generated <code>User</code> type. Also, GraphQL queries are still in a single folder, leading to the same inefficiencies.</p>
<p>Then, I got a advisor who introduced me to the power of <strong>GraphQL fragment colocation</strong>. ✨</p>
<h2 id="heading-what-is-graphql-fragment-colocation">What is GraphQL Fragment Colocation?</h2>
<p>GraphQL fragment colocation is the practice of defining fragments near the components or functions that consume them. Instead of keeping all GraphQL queries in a centralized location, colocating fragments ensures that only the necessary fields are queried and makes the code more modular and maintainable.</p>
<p>For more details, check out:</p>
<ul>
<li><p><a target="_blank" href="https://www.apollographql.com/docs/react/data/fragments#colocating-fragments">Apollo Docs on Colocating fragments</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/Quramy/gql-study-workshop/blob/main/chapters/03_colocation.md">Advisor's note (Japanese)</a></p>
</li>
</ul>
<h2 id="heading-implementing-colocation-a-new-approach">Implementing Colocation: A New Approach</h2>
<p>We decided on new rules based on my advisor’s advice.</p>
<p>New Coding Rules are:</p>
<ul>
<li><p>Write fragments in the same file as the functions/components using them</p>
</li>
<li><p>Query only the fields needed in that file</p>
</li>
<li><p>Avoid using generated types directly</p>
</li>
</ul>
<p>Example:</p>
<pre><code class="lang-ts"><span class="hljs-comment">// src/helpers/user.tsx</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> USER_FULLNAME = graphql(<span class="hljs-string">`
  fragment UserFullName on User {
    firstName
    lastName
    middleName
  }
`</span>);

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> getFullName = <span class="hljs-function">(<span class="hljs-params">user: {
  firstName: <span class="hljs-built_in">string</span>;
  lastName: <span class="hljs-built_in">string</span>;
  middleName: <span class="hljs-built_in">string</span>;
}</span>) =&gt;</span> {
  <span class="hljs-keyword">return</span> <span class="hljs-string">`<span class="hljs-subst">${user.firstName}</span> <span class="hljs-subst">${user.middleName}</span> <span class="hljs-subst">${user.lastName}</span>`</span>;
};
</code></pre>
<p>Now, the helper function defines its required fields explicitly via a fragment. This makes it clear which properties are used and avoids unnecessary data fetching.</p>
<p>Or we could use <code>DocumentType</code> which is generated from <code>codegen</code>:</p>
<pre><code class="lang-ts"><span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { DocumentType } <span class="hljs-keyword">from</span> <span class="hljs-string">"~/__generated__"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> getFullName = <span class="hljs-function">(<span class="hljs-params">user: DocumentType&lt;<span class="hljs-keyword">typeof</span> USER_FULLNAME&gt;</span>) =&gt;</span> {
  <span class="hljs-keyword">return</span> <span class="hljs-string">`<span class="hljs-subst">${user.firstName}</span> <span class="hljs-subst">${user.middleName}</span> <span class="hljs-subst">${user.lastName}</span>`</span>;
};
</code></pre>
<p>* Note that we are not using <code>User</code> type here.</p>
<p>When calling the function, we ensure the required data is passed:</p>
<pre><code class="lang-ts"><span class="hljs-comment">// src/page/user.tsx</span>
<span class="hljs-keyword">import</span> { getFullName } <span class="hljs-keyword">from</span> <span class="hljs-string">'../helpers/user'</span>;

<span class="hljs-keyword">const</span> { data } = useQuery(
  graphql(<span class="hljs-string">`
    query UserFullNameQuery {
      user {
        ...UserFullName
      }
    }
  `</span>)
);

<span class="hljs-keyword">const</span> fullName = getFullName(data.user);
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<p>By colocating GraphQL fragments and only fetching necessary fields, we achieve:</p>
<ul>
<li><p><strong>Improved Readability:</strong> Queries are easier to understand and maintain.</p>
</li>
<li><p><strong>Reduced Over-fetching:</strong> We fetch only the data we actually use.</p>
</li>
<li><p><strong>Better Maintainability:</strong> Changes to data structures remain localized.</p>
</li>
</ul>
<p>This approach makes GraphQL development cleaner and more efficient, ensuring that our frontend remains both performant and easy to work with. 🥳</p>
]]></content:encoded></item><item><title><![CDATA[Fixing Table Double Borders in 
Screenshots]]></title><description><![CDATA[Intro
I work at a startup company that develops several web applications. The app's code is older than my developer experience. If it is working, I rarely touch it. 😜 (Of course, I will change it if necessary.)
One of the apps I am developing has a ...]]></description><link>https://blog.yoniakabecky.com/fixing-table-double-borders-in-screenshots</link><guid isPermaLink="true">https://blog.yoniakabecky.com/fixing-table-double-borders-in-screenshots</guid><category><![CDATA[React]]></category><category><![CDATA[Screenshot]]></category><dc:creator><![CDATA[Yoni Aoki]]></dc:creator><pubDate>Tue, 11 Jun 2024 14:27:33 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/_eoobUKLFAI/upload/a4b9d96b5e2ea189892f2013ef964cff.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-intro">Intro</h2>
<p>I work at a startup company that develops several web applications. The app's code is older than my developer experience. If it is working, I rarely touch it. 😜 (Of course, I will change it if necessary.)</p>
<p>One of the apps I am developing has a function to capture a screen and save it as a jpeg. When I got a new task, which was implementing a table to the component, the style collapsed. 🙈 I need to figure out a way to keep it in style.</p>
<p>The original code uses <a target="_blank" href="https://github.com/niklasvh/html2canvas"><code>html2canvas</code></a> to make screenshots. As I said earlier, if it works, there is no need to change it. 🙏 To begin with, I start searching how to capture a table with <code>html2canvas</code>.</p>
<h2 id="heading-tldr">TL;DR</h2>
<div data-node-type="callout">
<div data-node-type="callout-emoji">☝</div>
<div data-node-type="callout-text">Reduce overlapping borders for simple tables</div>
</div>

<div data-node-type="callout">
<div data-node-type="callout-emoji">✌</div>
<div data-node-type="callout-text">Change library to <code>html-to-image</code></div>
</div>

<h2 id="heading-the-problem">The Problem</h2>
<p>This is a simple code for capturing an element using <code>html2canvas</code> and <code>file-saver</code>.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { saveAs } <span class="hljs-keyword">from</span> <span class="hljs-string">"file-saver"</span>;
<span class="hljs-keyword">import</span> html2canvas <span class="hljs-keyword">from</span> <span class="hljs-string">"html2canvas"</span>;

<span class="hljs-keyword">const</span> element = <span class="hljs-built_in">document</span>.querySelector(<span class="hljs-string">"#capture"</span>) <span class="hljs-keyword">as</span> HTMLElement;
html2canvas(element, {
  scale: <span class="hljs-number">1</span>, <span class="hljs-comment">// default is 2</span>
}).then(<span class="hljs-function">(<span class="hljs-params">canvas</span>) =&gt;</span> {
  saveAs(canvas.toDataURL(<span class="hljs-string">"image/jpeg"</span>), <span class="hljs-string">"capture.jpeg"</span>);
});
</code></pre>
<p>This works fine for most of the elements but not for the table.</p>
<p>This is what we get. The outside border width is 1px, but the inside borders are 2px.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1716727362548/7bb7845e-252f-44d0-8f38-862f88dffce5.png" alt="original table capture" class="image--center mx-auto" /></p>
<p>You might say... "Make the outer border 2px to match the inside". Of course, that's a solution too, but this wasn't my personal project. I have a design for it and I do not have the power to change the design... 🫠</p>
<p>* I found some of the stack overflow talks about <code>border-collapse: collapse;</code>. This only eliminate nested border look. So this wasn't my solution.</p>
<h2 id="heading-delete-overlapping-borders">Delete Overlapping Borders</h2>
<p>One common solution I found on the Internet is reducing overlaps.</p>
<p>Most of the browsers automatically reduce overlapping borders. Therefore, when I see my code on a browser, there are no double borders. (Open the developer tool and check the cell styles. It will show that only the left and bottom borders are active even though I added borders for all sides)</p>
<p>But the capture won't reduce automatically. So... need to style it manually.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1716892202899/1d206860-bc85-48c2-b788-ff6d05b1fd45.png" alt class="image--center mx-auto" /></p>
<p>I added <strong>a top border</strong> for <code>thead</code>, and <strong>a left border</strong> and <strong>a bottom border</strong> for cells. And <strong>a right border</strong> for the last cell. (I think if the border does not overlap, anything is fine... but if I am wrong, please tell me 🙏)</p>
<p>So this is the final result, all borders are the same width 🙌</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1716728281281/bcc396c1-d2a5-44a2-b8c3-533a13b7beb4.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-complex-table">Complex Table</h2>
<p>It worked fine until I encountered <strong>merged cells</strong> and <strong>striped rows.</strong> When I use <code>html2canvas</code>, the text in merged cells disappears. Additionally, the borders of the merged cells are not displaying correctly... 😭 (I didn't remove overlapping borders for this example)</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1717202141180/e5fca23e-e8cd-4e88-9faf-6989f493dbc6.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-use-dom-to-image">Use <code>dom-to-image</code></h3>
<p>I made the decision to search for a new library instead of modifying the styles (don't ask me why 🫣). While researching, I came across <a target="_blank" href="https://github.com/tsayen/dom-to-image"><code>dom-to-image</code></a>. Although it hasn't updated since 2017, but.... I decided to give it a try.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { saveAs } <span class="hljs-keyword">from</span> <span class="hljs-string">"file-saver"</span>;
<span class="hljs-keyword">import</span> domtoimage <span class="hljs-keyword">from</span> <span class="hljs-string">"dom-to-image"</span>;

<span class="hljs-keyword">let</span> element = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">"capture"</span>) <span class="hljs-keyword">as</span> HTMLElement;

domtoimage
  .toBlob(element, {
    width: element.clientWidth,
    height: element.clientHeight,
  })
  .then(<span class="hljs-function">(<span class="hljs-params">blob: Blob</span>) =&gt;</span> saveAs(blob, <span class="hljs-string">"capture.jpeg"</span>))
  .catch(<span class="hljs-function">(<span class="hljs-params">error: <span class="hljs-built_in">Error</span></span>) =&gt;</span> {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Fail to save"</span>, error);
  });
</code></pre>
<p>And you know what? It works perfectly. 🙌 No overlapped borders or disappearing text. 🥳</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1717204448411/a059f584-4d0a-401b-ad7d-90a639c25602.png" alt class="image--center mx-auto" /></p>
<p>After a brief moment of excitement, the project manager is hesitant to use a library that hasn't been updated in a long time. I get it, but... but... 😭🫠🙈</p>
<h3 id="heading-use-dom-to-image-more">Use <code>dom-to-image-more</code></h3>
<p>The project manager suggested using <a target="_blank" href="https://github.com/1904labs/dom-to-image-more">dom-to-image-more</a> which is a fork of the dom-to-image with some important fixes. As of writing this article, it seems that it is still being updated.</p>
<p>Just change the import and reuse other part.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { saveAs } <span class="hljs-keyword">from</span> <span class="hljs-string">"file-saver"</span>;
<span class="hljs-keyword">import</span> domtoimage <span class="hljs-keyword">from</span> <span class="hljs-string">"dom-to-image-more"</span>;

<span class="hljs-keyword">let</span> element = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">"capture"</span>) <span class="hljs-keyword">as</span> HTMLElement;

domtoimage
  .toBlob(element)
  .then(<span class="hljs-function">(<span class="hljs-params">blob: Blob</span>) =&gt;</span> saveAs(blob, <span class="hljs-string">"capture.jpeg"</span>));
</code></pre>
<p>And result is...</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1718108430290/89c1326d-1cad-47ae-8683-481b09e9018f.png" alt class="image--center mx-auto" /></p>
<p>What...🫢 I did not expect this result.</p>
<p>The text inside the cell did not disappear, but the border and spacing were changed. I might be able to set print styles for the element. I asked my project manager if he wanted me to change the style and make it work, but he said no. Therefore, we decided to skip using this library.</p>
<h3 id="heading-use-html-to-image">Use <code>html-to-image</code></h3>
<p>Luckily I found another library through <a target="_blank" href="https://betterprogramming.pub/heres-why-i-m-replacing-html2canvas-with-html-to-image-in-our-react-app-d8da0b85eadf">a medium post by <em>Code to Coin</em></a>.</p>
<p>In the article, he was comparing the performance time of the 2 libraries.</p>
<blockquote>
<p>The average difference in performance was 86.3985 ms. This means that on average, <code>html-to-image</code> was almost 71 times faster than <code>html2canvas</code>!</p>
</blockquote>
<p>I was having time issues as well, so if this works fine, I might replace all html2canvas later🤔.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { saveAs } <span class="hljs-keyword">from</span> <span class="hljs-string">"file-saver"</span>;
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> htmlToImage <span class="hljs-keyword">from</span> <span class="hljs-string">"html-to-image"</span>;

<span class="hljs-keyword">let</span> element = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">"capture"</span>) <span class="hljs-keyword">as</span> HTMLElement;

htmlToImage
  .toBlob(element, {
    canvasWidth: element.clientWidth / <span class="hljs-number">2</span>,
    canvasHeight: element.clientHeight / <span class="hljs-number">2</span>,
  })
  .then(<span class="hljs-function">(<span class="hljs-params">blob: Blob</span>) =&gt;</span> saveAs(blob, <span class="hljs-string">"capture.jpeg"</span>));
</code></pre>
<p>I use <code>toBlob</code> and added <code>canvasWidth</code> and <code>canvasHeight</code> to match with other results. (There is a <code>toPng</code>, <code>toJpeg</code>, <code>toSvg</code> functions available)</p>
<p>And the result is this 👇</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1718112331678/2eedaf18-c99b-45e5-bc00-83efd552be09.png" alt class="image--center mx-auto" /></p>
<p>It looks fine. There is no difference from the <code>dom-to-image</code> result.</p>
<p>The last release was a year ago, but my manager is okay with it, so finally I could finish my task. 😭</p>
<h2 id="heading-wrap-up">Wrap up</h2>
<p>I ended up replace <code>html2canvas</code> with <code>html-to-image</code> so that I don't have to change existing table styles. However, if it's not a complex table, I think I'd reduce the overlapping borders.</p>
<p>If I had more time to experiment with styles, I might have found another way, or maybe I could fork the library and do something? If you know a better way or a different library, please let me know. 🤩</p>
<p>* Find the code for this article on <a target="_blank" href="https://github.com/yoniakabecky/blog-sample-code/tree/main/app/demo/01-table-borders">GitHub</a>.</p>
<h1 id="heading-references"><strong>References</strong></h1>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://betterprogramming.pub/heres-why-i-m-replacing-html2canvas-with-html-to-image-in-our-react-app-d8da0b85eadf">https://betterprogramming.pub/heres-why-i-m-replacing-html2canvas-with-html-to-image-in-our-react-app-d8da0b85eadf</a></div>
<p> </p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/yoniakabecky/blog-sample-code/">https://github.com/yoniakabecky/blog-sample-code/</a></div>
]]></content:encoded></item><item><title><![CDATA[Why Am I Starting A Blog]]></title><description><![CDATA[TL;DR

☝
I will write technical notes to keep track of my work.


I just wrote about my enthusiasm to begin, so you don't have to go through it. But if you want, you're welcome to read it 😉
Starting Point
One of my teachers in programming school ass...]]></description><link>https://blog.yoniakabecky.com/why-am-i-starting-a-blog</link><guid isPermaLink="true">https://blog.yoniakabecky.com/why-am-i-starting-a-blog</guid><category><![CDATA[motivation]]></category><category><![CDATA[writing]]></category><dc:creator><![CDATA[Yoni Aoki]]></dc:creator><pubDate>Sun, 21 Jan 2024 12:57:44 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/1ixT36dfuSQ/upload/c4e441f37226172629bfad3d7e48ef21.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-tldr">TL;DR</h2>
<div data-node-type="callout">
<div data-node-type="callout-emoji">☝</div>
<div data-node-type="callout-text">I will write technical notes to keep track of my work.</div>
</div>

<p>I just wrote about my enthusiasm to begin, so you don't have to go through it. But if you want, you're welcome to read it 😉</p>
<h2 id="heading-starting-point">Starting Point</h2>
<p>One of my teachers in programming school assigned Medium posts as homework to someone who had failed to submit their programming assignment. Of course, I did not get that assignment. That is why I have not written any posts until now. 🙈</p>
<p>After completing my programming course, a lot of people suggested that I should start writing. However, I hesitated because</p>
<ul>
<li><p>I am not good at writing 🙅‍♀️</p>
</li>
<li><p>I did not want to spend time writing ⏳ (unsure if the benefits would outweigh the time invested)</p>
</li>
<li><p>I thought that no one read my blog because my level of knowledge was insufficient 🥺</p>
</li>
</ul>
<p>Moreover, there were other reasons, but most of them stemmed from my low self-esteem.</p>
<p>I must admit that some things have remained the same 🫠, but some have changed.</p>
<h2 id="heading-the-changes">The Changes</h2>
<p>The biggest change is that I started working as a front-end developer for several companies. 🚀 Collaborating with other developers and reviewing their code helped me to learn more. Additionally, working on released products and hearing customers' feedback made me realize that I cannot compromise my skills. 👻</p>
<h3 id="heading-problem-solving">Problem-solving</h3>
<p>Whenever I find problems while working on tasks, I usually search for solutions by Googling and often end up on Stack Overflow or someone's blog. However, as my skill improves, problems become more difficult, and the more difficult they become, the harder it is to find a solution.</p>
<p>Then I need to look deeper. Check the library documents, changes, issues, etc...</p>
<p>As I could not find it easily, I thought it would be helpful to post about the problem I faced and how I solved it. (Since gaining experience, I have also gained a bit of self-esteem, and my thinking has changed 😎)</p>
<h3 id="heading-improve-my-reputation">Improve My Reputation</h3>
<p>Initially, my main focus was to improve my coding skills, and I did not pay much attention to improving my reputation. Now, I have been working for companies for a while, my priorities have shifted. I have more time to reflect on what I want to achieve in my career.</p>
<p>All the work I have done so far has been private. Therefore, I need to showcase my expertise, knowledge, and problem-solving skills somehow... 🤔</p>
<p>I am not a social media person. I created an X (Twitter) account, I rarely use it. LinkedIn has turned into a platform for recruiters' spam (IMO🤪). So I do not expect much from myself. But I think I can do it as long as make it simple (like make a note 📝 for what I researched).</p>
<h3 id="heading-bonus">Bonus 😁</h3>
<p>As English is my second language, the rapid growth of AI has been a big help. It has certainly lowered the bar for writing. 😇</p>
<h2 id="heading-wrap-up">Wrap Up</h2>
<p>Now that I have more experience, I have more time to think about what I want to do next. As a start, I would like to make some technical notes.</p>
<p>Oh, by the way, this is my first post and I have been spending a day writing this 3 mins read post. 😂 If I keep doing this, I won't be able to continue writing.</p>
<p>So my blog will be</p>
<ul>
<li><p>as simple as possible ☝️</p>
</li>
<li><p>about one problem/technique⚡️per post</p>
</li>
<li><p>take it easy 🦅 do it when I can</p>
</li>
</ul>
<p>I would be happy if you could leave any advice or comments.<br />Thank you for reading this far 😃</p>
]]></content:encoded></item></channel></rss>