<?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: coder om</title>
    <description>The latest articles on DEV Community by coder om (@coderom).</description>
    <link>https://dev.to/coderom</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%2F1182622%2F1596cb6b-e92a-40e5-8c35-607e1fb37895.png</url>
      <title>DEV Community: coder om</title>
      <link>https://dev.to/coderom</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/coderom"/>
    <language>en</language>
    <item>
      <title>🛍️ Building a Shopify Automation Script — Lessons &amp; Setup Reference</title>
      <dc:creator>coder om</dc:creator>
      <pubDate>Sat, 28 Feb 2026 04:47:31 +0000</pubDate>
      <link>https://dev.to/coderom/building-a-shopify-automation-script-lessons-setup-reference-3p62</link>
      <guid>https://dev.to/coderom/building-a-shopify-automation-script-lessons-setup-reference-3p62</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What this doc is:&lt;/strong&gt; A first-person reference log of everything I went through while building a Shopify product image optimization tool. The actual goal of the script doesn't matter — what matters is the journey to get the script &lt;em&gt;talking to Shopify at all&lt;/em&gt;. These steps, challenges, and fixes will apply to almost any Shopify automation I build in the future.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  🎯 What I Was Trying to Do
&lt;/h2&gt;

&lt;p&gt;My client had a Shopify store with 250+ products. Many product images were over 1MB in size, which was tanking their site performance. The goal was simple: &lt;strong&gt;automatically compress all images over 200KB down to under 200KB and re-upload them to Shopify&lt;/strong&gt; — no manual work, no mistakes, fully automated.&lt;/p&gt;




&lt;h2&gt;
  
  
  🗂️ The Stack I Chose
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Runtime:&lt;/strong&gt; Node.js (v20+)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Language:&lt;/strong&gt; TypeScript (with &lt;code&gt;ts-node&lt;/code&gt; for running directly)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Libraries:&lt;/strong&gt; &lt;code&gt;axios&lt;/code&gt; for HTTP, &lt;code&gt;sharp&lt;/code&gt; for image compression&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shopify interface:&lt;/strong&gt; Shopify Admin REST API&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  📋 Step-by-Step: What I Did, What Broke, and How I Fixed It
&lt;/h2&gt;




&lt;h3&gt;
  
  
  Step 1 — Writing the Script
&lt;/h3&gt;

&lt;p&gt;I started with a TypeScript script that would:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Fetch all products via the Shopify API (with pagination for 250+ products)&lt;/li&gt;
&lt;li&gt;Download each product image&lt;/li&gt;
&lt;li&gt;Compress images over 200KB using &lt;code&gt;sharp&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Re-upload the compressed image back to Shopify, replacing the original&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The script was fully typed with interfaces for all Shopify API shapes (&lt;code&gt;ShopifyProduct&lt;/code&gt;, &lt;code&gt;ShopifyImage&lt;/code&gt;, &lt;code&gt;SummaryRow&lt;/code&gt;, etc.) and had a final before/after summary report.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;✅ No issues here — the code was solid from the start.&lt;/strong&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 2 — First Run: &lt;code&gt;getaddrinfo ENOTFOUND https&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Error:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Fatal error: getaddrinfo ENOTFOUND https
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What happened:&lt;/strong&gt; I had set my &lt;code&gt;SHOPIFY_STORE&lt;/code&gt; env var to &lt;code&gt;https://coderom.myshopify.com&lt;/code&gt; — with the &lt;code&gt;https://&lt;/code&gt; prefix. But the script was already building the URL as &lt;code&gt;https://${STORE}/...&lt;/code&gt;, which resulted in &lt;code&gt;https://https://coderom.myshopify.com&lt;/code&gt;. DNS couldn't resolve &lt;code&gt;https&lt;/code&gt; as a hostname.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Added a one-liner at the top of the config to strip any protocol prefix automatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;STORE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SHOPIFY_STORE&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^https&lt;/span&gt;&lt;span class="se"&gt;?&lt;/span&gt;&lt;span class="sr"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\/\/&lt;/span&gt;&lt;span class="sr"&gt;/i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// strips https:// or http://&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;$/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;            &lt;span class="c1"&gt;// strips trailing slash&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Lesson learned:&lt;/strong&gt; Always sanitize env var inputs. Users (including myself) will paste full URLs. Strip the protocol defensively so the script never breaks regardless of how the var is set.&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 3 — Second Run: &lt;code&gt;connect ETIMEDOUT&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Error:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Fatal error: connect ETIMEDOUT 218.248.112.60:443
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What happened:&lt;/strong&gt; The script connected to an IP but timed out. I ran a &lt;code&gt;curl&lt;/code&gt; test to isolate whether this was a code issue or a network issue:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nt"&gt;--connect-timeout&lt;/span&gt; 10 https://solar-for-nature.myshopify.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result: same timeout, same IP — &lt;code&gt;218.248.112.60&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Root cause:&lt;/strong&gt; That IP is &lt;strong&gt;not a Shopify IP&lt;/strong&gt;. Shopify runs on Fastly CDN (&lt;code&gt;151.101.x.x&lt;/code&gt;, &lt;code&gt;23.227.x.x&lt;/code&gt;). My ISP (Indian ISP) was &lt;strong&gt;DNS hijacking&lt;/strong&gt; the request — intercepting the DNS resolution and pointing the domain to a local IP instead of Shopify's actual servers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Changed DNS resolver to Cloudflare (&lt;code&gt;1.1.1.1&lt;/code&gt;) in system network settings, which bypassed the ISP's DNS hijacking. Could also be fixed by connecting to a VPN or using mobile hotspot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson learned:&lt;/strong&gt; &lt;code&gt;ETIMEDOUT&lt;/code&gt; doesn't always mean the store is down or the code is wrong. Always run a &lt;code&gt;curl&lt;/code&gt; test to check the resolved IP. If it doesn't look like a CDN IP, the problem is DNS, not the script. This is especially common in India with ISPs like Jio, Airtel, and BSNL.&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 4 — The Shopify App Setup Problem
&lt;/h3&gt;

&lt;h3&gt;
  
  
  Wrong Turn 1 — Creating a Public App first
&lt;/h3&gt;

&lt;p&gt;My first attempt was creating an app through the &lt;strong&gt;Shopify Partners dashboard&lt;/strong&gt;. I built it there, but then I noticed a &lt;strong&gt;Distribution&lt;/strong&gt; field on the app's homepage inside the partner dashboard. I clicked it, which took me to the distribution section where Shopify presented two options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Public distribution&lt;/strong&gt; — Publish on the Shopify App Store (listed or unlisted). Reaches a global merchant audience with unlimited installs. Requires Shopify review.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom distribution&lt;/strong&gt; — Generate custom install links for one store or one organization. Installs are limited to that store/org. No App Store review.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I selected &lt;strong&gt;Custom distribution&lt;/strong&gt; since this was for one specific client store. Shopify showed a warning: &lt;em&gt;"This can't be undone"&lt;/em&gt; — once you select custom distribution, you can't switch it back to public. I confirmed, and it then asked me for the store domain (e.g. &lt;code&gt;shopone.myshopify.com&lt;/code&gt;). It generated a custom install link which I opened, and that installed the app on the store.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The problem:&lt;/strong&gt; Even after going through all this — Partners dashboard → custom distribution → install link → app installed on store — the token situation was still unclear and this whole flow added unnecessary complexity for what should have been a simple script setup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson:&lt;/strong&gt; The Shopify Partners dashboard is for building apps meant to be distributed to &lt;em&gt;multiple stores&lt;/em&gt; — even if you pick custom distribution and limit it to one store, it's still the wrong tool for a private client script. It creates overhead (distribution settings, install links, partner account dependency) that you don't need.&lt;/p&gt;

&lt;p&gt;For any client-specific automation script, &lt;strong&gt;always use a Custom App created directly inside the store's own admin&lt;/strong&gt;, not through the Partners dashboard. It lives inside one store, is invisible to the App Store, and gives an instant access token with zero distribution complexity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I learned:&lt;/strong&gt; There are two types of Shopify apps:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Use case&lt;/th&gt;
&lt;th&gt;Review needed?&lt;/th&gt;
&lt;th&gt;Time to token&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Public App&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Listed on App Store, used by many stores&lt;/td&gt;
&lt;td&gt;✅ Yes — Shopify reviews it&lt;/td&gt;
&lt;td&gt;Days/weeks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Custom App&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Built inside one specific store&lt;/td&gt;
&lt;td&gt;❌ No review&lt;/td&gt;
&lt;td&gt;Instant&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For any client-specific automation script, &lt;strong&gt;always use a Custom App&lt;/strong&gt;. It lives inside one store, is invisible to the App Store, and gives an instant access token.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to create a Custom App (the right way):&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to client's &lt;strong&gt;Shopify Admin → Settings → Apps&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Develop apps&lt;/strong&gt; → Allow custom app development (one-time)(if sees this)&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Build apps in Dev Dashboard&lt;/strong&gt; it will open dev dashboard&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Create app&lt;/strong&gt; → and you will see 2 ways to move forward:

&lt;ul&gt;
&lt;li&gt;i) &lt;strong&gt;Start with Shopify CLI&lt;/strong&gt; and ii) &lt;strong&gt;Start from Dev Dashboard&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;I went with Start from Dev Dashboard&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Name it (e.g. &lt;code&gt;Image Optimizer&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;In the version settings under the &lt;strong&gt;Access Scopes&lt;/strong&gt; → select scopes:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;read_products&lt;/code&gt; — for reading/auditing&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;write_products&lt;/code&gt; — for modifying/uploading&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Release&lt;/strong&gt; → So this is a 2nd release of the app which means with updates&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Settings&lt;/strong&gt; → Here you can get the Credentials i.e. &lt;code&gt;Client ID&lt;/code&gt; and &lt;code&gt;Secret&lt;/code&gt; and rest of the settings related to the app.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;⚠️ Common gotcha:&lt;/strong&gt; After this you might think you can now move forward to your app scripts because you are seeing here Credentials i.e. &lt;code&gt;Client ID&lt;/code&gt; and &lt;code&gt;Secret&lt;/code&gt; but no you need to get the the &lt;strong&gt;Access Token&lt;/strong&gt;. I missed this at first and thought I can use the this secret and it will work but it didn’t actually.&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 4b — The Access Token Wasn't There: OAuth Endpoint to the Rescue
&lt;/h3&gt;

&lt;p&gt;After completing all the Custom App steps above, I ran into a problem: &lt;strong&gt;the "Access Token is not correct"&lt;/strong&gt;. The API credentials section showed a Client ID and Client Secret, but no access token.&lt;/p&gt;

&lt;p&gt;I initially tried using the &lt;strong&gt;Client Secret&lt;/strong&gt; directly as the API token — that didn't work and caused auth failures.&lt;/p&gt;

&lt;p&gt;After going through Shopify's documentation, I found the actual solution: for this app setup, the access token isn't pre-generated in the UI — &lt;strong&gt;I had to request it programmatically&lt;/strong&gt; by hitting Shopify's OAuth endpoint using the &lt;code&gt;Client ID&lt;/code&gt; and Client &lt;code&gt;Secret&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The request:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;POST https://&lt;span class="o"&gt;{&lt;/span&gt;shop&lt;span class="o"&gt;}&lt;/span&gt;.myshopify.com/admin/oauth/access_token
Content-Type: application/x-www-form-urlencoded

&lt;span class="nv"&gt;grant_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;client_credentials
&amp;amp;client_id&lt;span class="o"&gt;={&lt;/span&gt;your_client_id&lt;span class="o"&gt;}&lt;/span&gt;
&amp;amp;client_secret&lt;span class="o"&gt;={&lt;/span&gt;your_client_secret&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The response:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"access_token"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"shpat_xxxxx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scope"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"read_products,write_products"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expires_in"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;86399&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;access_token&lt;/code&gt; value (&lt;code&gt;shpat_xxxxx&lt;/code&gt;) is what goes into &lt;code&gt;SHOPIFY_TOKEN&lt;/code&gt;. This is the token used in the &lt;code&gt;X-Shopify-Access-Token&lt;/code&gt; header for all API calls.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;⚠️ Important: Tokens expire after 24 hours.&lt;/strong&gt; This means for long-running automations or scheduled tasks, I need to re-request a new token before each run using this same OAuth call.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;⚠️ Client Secret ≠ Access Token.&lt;/strong&gt; This was my exact mistake — I copied the Client Secret from the app credentials page thinking that was the token. It's not. The Client Secret is only used &lt;em&gt;to obtain&lt;/em&gt; an access token via the OAuth endpoint. The actual token you use in API calls is the &lt;code&gt;access_token&lt;/code&gt; returned from that POST request.&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 5 — Building a Debug Script Before Going to Prod
&lt;/h3&gt;

&lt;p&gt;Before running the full optimizer (which modifies images), I built two safer scripts:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;shopify-debug.ts&lt;/code&gt;&lt;/strong&gt; — A pure connectivity tester that runs 5 progressive checks:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;DNS resolution&lt;/li&gt;
&lt;li&gt;TCP port 443 reachability&lt;/li&gt;
&lt;li&gt;HTTPS handshake&lt;/li&gt;
&lt;li&gt;API version detection (tries &lt;code&gt;2025-01&lt;/code&gt; → &lt;code&gt;2024-10&lt;/code&gt; → ... → &lt;code&gt;2024-01&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Reports exactly which version works and what error each one returns&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This was invaluable — it told me immediately &lt;em&gt;where&lt;/em&gt; in the chain things were breaking rather than just showing a generic fatal error.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;shopify-image-audit.ts&lt;/code&gt;&lt;/strong&gt; — A 100% read-only script that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fetches all products&lt;/li&gt;
&lt;li&gt;Checks each image size (using &lt;code&gt;HEAD&lt;/code&gt; requests first, falling back to full download)&lt;/li&gt;
&lt;li&gt;Lists every image URL over 200KB with its size&lt;/li&gt;
&lt;li&gt;Makes zero writes to the store&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why I built the audit script first:&lt;/strong&gt; Running the optimizer directly on a production store is risky. The audit script let me verify that connectivity, auth, and pagination all worked correctly, and showed me exactly what &lt;em&gt;would&lt;/em&gt; be changed — before any changes were made.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson learned:&lt;/strong&gt; For any Shopify automation, always build in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Debug/connectivity script first&lt;/li&gt;
&lt;li&gt;Read-only audit script second&lt;/li&gt;
&lt;li&gt;Write/modify script last — only after both above confirm everything works&lt;/li&gt;
&lt;/ol&gt;




&lt;h3&gt;
  
  
  Step 6 — The Optimizer: How Image Compression Works
&lt;/h3&gt;

&lt;p&gt;The core compression logic uses &lt;code&gt;sharp&lt;/code&gt; and works in two stages:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stage 1 — Quality reduction:&lt;/strong&gt;&lt;br&gt;
Start at quality 85 and step down by 5 until the image is under 200KB:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;quality&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;85&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;quality&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="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;compressed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sharp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;jpeg&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;progressive&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="nf"&gt;toBuffer&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="nx"&gt;compressed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;TARGET_SIZE_BYTES&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;compressed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;quality&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mi"&gt;5&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;&lt;strong&gt;Stage 2 — Dimension resize (if Stage 1 isn't enough):&lt;/strong&gt;&lt;br&gt;
If even quality 20 doesn't get it under 200KB, proportionally scale down the image dimensions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scaleFactor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;TARGET_SIZE_BYTES&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;compressed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;newWidth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;scaleFactor&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;compressed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sharp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resize&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;newWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;withoutEnlargement&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;jpeg&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;75&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;progressive&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBuffer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why this order?&lt;/strong&gt; Quality reduction is invisible to most viewers. Resizing dimensions is noticeable. So quality is always tried first, and dimension reduction is only the last resort.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Shopify rate limiting:&lt;/strong&gt; The Shopify API allows ~2 requests/second. The script adds a 600ms delay between image uploads to stay within this limit and avoid &lt;code&gt;429 Too Many Requests&lt;/code&gt; errors.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧱 Final Project Structure
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;image-optimizer/
├── src/
│   ├── shopify-debug.ts          # Step 1: Run this first — tests connectivity
│   ├── shopify-image-audit.ts    # Step 2: Read-only image size report
│   └── shopify-image-optimizer.ts # Step 3: Compresses &amp;amp; re-uploads images
├── package.json
└── tsconfig.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Always run in order: debug → audit → optimizer.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  ⚡ Quick Reference: Run Commands
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install dependencies (one time)&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt;

&lt;span class="c"&gt;# 1. Test connectivity and API access&lt;/span&gt;
&lt;span class="nv"&gt;SHOPIFY_STORE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;store.myshopify.com &lt;span class="nv"&gt;SHOPIFY_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;shpat_xxx npx ts-node src/shopify-debug.ts

&lt;span class="c"&gt;# 2. Audit images (read-only, safe on prod)&lt;/span&gt;
&lt;span class="nv"&gt;SHOPIFY_STORE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;store.myshopify.com &lt;span class="nv"&gt;SHOPIFY_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;shpat_xxx npx ts-node src/shopify-image-audit.ts

&lt;span class="c"&gt;# 3. Run optimizer (modifies images — run after audit confirms everything)&lt;/span&gt;
&lt;span class="nv"&gt;SHOPIFY_STORE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;store.myshopify.com &lt;span class="nv"&gt;SHOPIFY_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;shpat_xxx npx ts-node src/shopify-image-optimizer.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  📌 Master Checklist for Any Future Shopify Automation
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ]  Use a &lt;strong&gt;Custom App&lt;/strong&gt;, not a Public App — avoids review, instant token&lt;/li&gt;
&lt;li&gt;[ ]  Enable correct API scopes (at minimum: &lt;code&gt;read_products&lt;/code&gt;; add &lt;code&gt;write_products&lt;/code&gt; if modifying)&lt;/li&gt;
&lt;li&gt;[ ]  Hit the OAuth endpoint programmatically with Client ID + Secret to get the &lt;code&gt;access_token&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ]  &lt;strong&gt;Never use Client Secret directly as the API token&lt;/strong&gt; — it's only used to request the real token&lt;/li&gt;
&lt;li&gt;[ ]  Tokens obtained via OAuth &lt;strong&gt;expire after 24 hours&lt;/strong&gt; — re-request before each run&lt;/li&gt;
&lt;li&gt;[ ]  Set &lt;code&gt;SHOPIFY_STORE&lt;/code&gt; without &lt;code&gt;https://&lt;/code&gt; — or strip it in code&lt;/li&gt;
&lt;li&gt;[ ]  If &lt;code&gt;ETIMEDOUT&lt;/code&gt;, run &lt;code&gt;curl&lt;/code&gt; and check the resolved IP — may be DNS hijacking&lt;/li&gt;
&lt;li&gt;[ ]  Switch to &lt;code&gt;1.1.1.1&lt;/code&gt; DNS or use a VPN if ISP is hijacking DNS&lt;/li&gt;
&lt;li&gt;[ ]  Always run the debug script before the real script&lt;/li&gt;
&lt;li&gt;[ ]  Always build a read-only audit script before a write script&lt;/li&gt;
&lt;li&gt;[ ]  Respect the &lt;code&gt;~2 req/s&lt;/code&gt; rate limit — add 500–600ms delays between writes&lt;/li&gt;
&lt;li&gt;[ ]  Back up prod data before any destructive operation&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  💡 Key Lessons (TL;DR)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ETIMEDOUT&lt;/code&gt; ≠ code bug.&lt;/strong&gt; Check the IP with &lt;code&gt;curl -v&lt;/code&gt;. ISPs in India (and elsewhere) hijack DNS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never use a Public App for client scripts.&lt;/strong&gt; Custom App is always the right choice — instant, no review, scoped to one store.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Access Token never appears in the UI&lt;/strong&gt;, don't use the Client Secret directly — hit the OAuth endpoint (&lt;code&gt;POST /admin/oauth/access_token&lt;/code&gt;) with the Client ID and Secret to programmatically get the real &lt;code&gt;access_token&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client Secret ≠ Access Token.&lt;/strong&gt; The secret is a credential used to &lt;em&gt;request&lt;/em&gt; a token. The token is what you actually use in API calls. Confusing these two wastes a lot of time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build read-only first.&lt;/strong&gt; Debug → Audit → Optimizer. Never jump straight to writes on prod.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-detect API version.&lt;/strong&gt; Hardcoding it is fragile. Try newest-first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quality before resize.&lt;/strong&gt; When compressing images, always try quality reduction before touching dimensions.&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>shopify</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>automation</category>
    </item>
    <item>
      <title>Building a Reversible Bundle Transformation Engine in Shopify</title>
      <dc:creator>coder om</dc:creator>
      <pubDate>Thu, 26 Feb 2026 09:45:12 +0000</pubDate>
      <link>https://dev.to/coderom/building-a-reversible-bundle-transformation-engine-in-shopify-40fa</link>
      <guid>https://dev.to/coderom/building-a-reversible-bundle-transformation-engine-in-shopify-40fa</guid>
      <description>&lt;p&gt;&lt;strong&gt;How I solved the three-way conflict between merchandising UX, operations accuracy, and checkout correctness — without a backend service or third-party app.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem No One Tells You About Bundles
&lt;/h2&gt;

&lt;p&gt;When you first hear the requirement — &lt;em&gt;"we want to sell bundles"&lt;/em&gt; — it sounds straightforward. Put a few products together, wrap them in a bow, charge a price. Done.&lt;/p&gt;

&lt;p&gt;It isn't.&lt;/p&gt;

&lt;p&gt;The moment you go deeper, you hit a wall that every Shopify developer eventually collides with: &lt;strong&gt;Shopify's inventory model is variant-centric, but bundle UX is representation-centric.&lt;/strong&gt; Those two things want fundamentally different things from your cart.&lt;/p&gt;

&lt;p&gt;This is the story of how I designed and built a solution that satisfies both — a reversible transformation engine that keeps the customer-facing cart clean and bundled, while sending real, fulfillable component SKUs through to checkout and inventory.&lt;/p&gt;

&lt;p&gt;This article is written for Shopify developers. It assumes familiarity with Liquid, the Ajax Cart API, and basic theme architecture. I'll cover the full design rationale, the data contract, the implementation step by step, the tradeoffs I made, and the hardening I'd apply before going to production.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Business Requirement (And Its Hidden Complexity)
&lt;/h2&gt;

&lt;p&gt;The stated requirement had four parts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Show a bundle as a single product on the PDP with a clean add-to-cart experience.&lt;/li&gt;
&lt;li&gt;Keep the cart showing one tidy bundle line — not three or four component lines confusing the customer.&lt;/li&gt;
&lt;li&gt;At checkout, pass real component variant IDs so Shopify decrements inventory correctly.&lt;/li&gt;
&lt;li&gt;Avoid maintaining a separate inventory model for an abstract "bundle SKU" that doesn't physically exist.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;On paper, four clean bullets. In practice, each one creates a constraint that fights the others.&lt;/p&gt;

&lt;p&gt;The deeper complexity becomes clear when you think about the full lifecycle of a cart item, not just the PDP moment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PDP&lt;/strong&gt;: Customer selects bundle, adds to cart.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cart&lt;/strong&gt;: Customer sees one clean line item.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Checkout&lt;/strong&gt;: Backend needs real purchasable variant IDs — not a synthetic bundle ID.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Post-checkout return&lt;/strong&gt;: If the customer comes back to the storefront (abandoned checkout, payment failure, etc.), the cart should look bundled again, not like a disassembled pile of components.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most implementations I've seen solve one or two of these well and quietly break the others. The checkout split is easy. The reconstruction on return is almost always missing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Is Technically Hard in Shopify
&lt;/h2&gt;

&lt;p&gt;Shopify's checkout and inventory pipeline is built around variants. When checkout fires, Shopify's backend processes line items, decrements inventory, and generates order line items — all at the variant level. There is no native concept of a "bundle variant" that internally maps to multiple inventory units.&lt;/p&gt;

&lt;p&gt;That means any bundle abstraction you build in the storefront has to be transparent to Shopify's checkout. The checkout cart must contain real component variant IDs, in real quantities, with real inventory backing.&lt;/p&gt;

&lt;p&gt;But your customer doesn't care about any of that. They bought "The Starter Kit." They don't want to see four separate line items in their cart and wonder if they accidentally added duplicates.&lt;/p&gt;

&lt;p&gt;So the cart state your customer sees and the cart state Shopify's checkout needs are structurally different. You need a transformation layer that lives between them — one that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Keeps the customer-facing representation intact while the customer is browsing and adding items.&lt;/li&gt;
&lt;li&gt;Explodes the bundle into components exactly at the moment the customer initiates checkout.&lt;/li&gt;
&lt;li&gt;Optionally reconstructs the bundle representation when the customer returns to the storefront.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This transformation must be &lt;strong&gt;safe&lt;/strong&gt; (can't lose cart items on failure), &lt;strong&gt;reversible&lt;/strong&gt; (can reconstruct the original representation), and &lt;strong&gt;invisible&lt;/strong&gt; to the customer (all of this happens before the page redirects to checkout).&lt;/p&gt;




&lt;h2&gt;
  
  
  Important Distinction: Pack UI vs Bundle Engine
&lt;/h2&gt;

&lt;p&gt;Before going further, it's worth clarifying a naming collision that existed in this codebase.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pack UI&lt;/strong&gt; refers to the PDP display and variant selection experience — the card-style quantity selectors (buy 1 / buy 2 / buy 3) that live in &lt;code&gt;snippets/product-details.liquid&lt;/code&gt; and are powered by &lt;code&gt;assets/pdp-custom.js&lt;/code&gt;. This is a presentational layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bundle transformation&lt;/strong&gt; refers to the cart and checkout pipeline — the logic that reads cart state, splits bundle lines into component lines, and rebuilds them on return. This lives entirely in &lt;code&gt;assets/bundleCartBreaking.js&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;These two systems share a product but operate independently. This article covers only the bundle transformation engine. The Pack UI is a separate concern.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Architecture: A Three-Layer System
&lt;/h2&gt;

&lt;p&gt;The solution I designed has three distinct layers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 1: Data Contract
&lt;/h3&gt;

&lt;p&gt;Every bundle line item in the cart carries a set of private metadata properties. These are Shopify line item properties — a native feature of the Ajax Cart API — with keys prefixed by &lt;code&gt;_&lt;/code&gt; to mark them as internal and hide them from the customer-facing cart UI.&lt;/p&gt;

&lt;p&gt;The contract is:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property Key&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Example Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_Individual Product Variant IDs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Comma-separated component variant IDs for checkout split&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"44112233,44112244,44112255"&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_Bundle Id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The bundle's own variant ID, used as grouping key on reconstruction&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"43998877"&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Bundle Name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Human-readable label for the bundle, used on reconstruction&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"The Starter Kit"&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These three properties are set at add-to-cart time and travel with the line item throughout the cart lifecycle. They are the single source of truth for every transformation the engine performs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 2: Transformation Pipeline
&lt;/h3&gt;

&lt;p&gt;The engine has two directions:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Forward (cart → checkout):&lt;/strong&gt; On checkout click, read the cart, detect bundle lines by the presence of &lt;code&gt;_Individual Product Variant IDs&lt;/code&gt;, expand each bundle line into its component variant rows, preserve the &lt;code&gt;_Bundle Id&lt;/code&gt; and &lt;code&gt;Bundle Name&lt;/code&gt; on each component row (for later reconstruction), clear the cart, re-add all items (expanded components plus untouched non-bundle items), and redirect to &lt;code&gt;/checkout&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reverse (checkout return → cart):&lt;/strong&gt; On storefront load, check for a session flag (&lt;code&gt;userWentToCheckout&lt;/code&gt;) indicating the user came back from a checkout session. If the flag is present, read the cart (now containing component rows), group component rows by &lt;code&gt;_Bundle Id&lt;/code&gt;, reconstruct bundle line items with the full &lt;code&gt;_Individual Product Variant IDs&lt;/code&gt; value, clear the cart, and re-add the reconstructed bundle lines.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 3: Presentation Filtering
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;_&lt;/code&gt;-prefixed properties are hidden from the customer's cart UI via a filter in &lt;code&gt;snippets/line-item.liquid&lt;/code&gt;. The line item template loops over properties and skips any key beginning with &lt;code&gt;_&lt;/code&gt;. The customer only ever sees &lt;code&gt;Bundle Name&lt;/code&gt; — the friendly label — not the internal IDs.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Sequence, End to End
&lt;/h2&gt;

&lt;p&gt;Here is the complete lifecycle of a bundle from add-to-cart to checkout to return, visualized as a sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────────┐
│  CUSTOMER                                                   │
│                                                             │
│  1. Opens PDP for "The Starter Kit"                         │
│  2. Clicks "Add to Cart"                                    │
│     └─ Product form submits with hidden properties:         │
│        · _Individual Product Variant IDs: "111,222,333"     │
│        · _Bundle Id: "99887"                                │
│        · Bundle Name: "The Starter Kit"                     │
└─────────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────────┐
│  CART (Customer View)                                       │
│                                                             │
│  · "The Starter Kit" × 1           £49.00                   │
│    (internal properties hidden)                             │
│                                                             │
│  [Checkout]                                                 │
└─────────────────────────────────────────────────────────────┘
                          │
             Customer clicks [Checkout]
                          │
                          ▼
┌─────────────────────────────────────────────────────────────┐
│  BUNDLE TRANSFORMATION ENGINE (Invisible to Customer)       │
│                                                             │
│  1. Fetch GET /cart.js                                      │
│  2. Detect bundle lines via _Individual Product Variant IDs │
│  3. Expand to component rows:                               │
│     · Variant 111 × 1, _Bundle Id: "99887"                  │
│     · Variant 222 × 1, _Bundle Id: "99887"                  │
│     · Variant 333 × 1, _Bundle Id: "99887"                  │
│  4. POST /cart/clear.js                                     │
│  5. POST /cart/add.js (component rows)                      │
│  6. Set sessionStorage: userWentToCheckout = true           │
│  7. Redirect → /checkout                                    │
└─────────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────────┐
│  SHOPIFY CHECKOUT                                           │
│                                                             │
│  · Product A (Variant 111) × 1                              │
│  · Product B (Variant 222) × 1                              │
│  · Product C (Variant 333) × 1                              │
│                                                             │
│  Inventory decremented at component level ✓                 │
│  Fulfillment sees real SKUs ✓                               │
└─────────────────────────────────────────────────────────────┘
                          │
         Customer abandons or returns to storefront
                          │
                          ▼
┌─────────────────────────────────────────────────────────────┐
│  BUNDLE RECONSTRUCTION ENGINE (Invisible to Customer)       │
│                                                             │
│  1. Detect sessionStorage flag: userWentToCheckout          │
│  2. Fetch GET /cart.js (component rows visible)             │
│  3. Group rows by _Bundle Id                                │
│  4. Reconstruct: "The Starter Kit" with IDs "111,222,333"   │
│  5. POST /cart/clear.js                                     │
│  6. POST /cart/add.js (bundle row)                          │
└─────────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────────┐
│  CART (Customer View — Back to Bundle Representation)       │
│                                                             │
│  · "The Starter Kit" × 1           £49.00                   │
└─────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Implementation: Step by Step
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 0: Define the Bundle Metafield
&lt;/h3&gt;

&lt;p&gt;Before any code, set up the data model that connects a bundle product to its component products in Shopify Admin.&lt;/p&gt;

&lt;p&gt;Create a product metafield:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Owner type:&lt;/strong&gt; Product&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Namespace:&lt;/strong&gt; &lt;code&gt;custom&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key:&lt;/strong&gt; &lt;code&gt;bundle_components&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Type:&lt;/strong&gt; &lt;code&gt;list.product_reference&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;list.product_reference&lt;/code&gt; type is a native Shopify metafield type that stores references to other product objects. Its &lt;code&gt;.value&lt;/code&gt; in Liquid returns an iterable of product objects, which means you can loop over component products and resolve their current default variants without any Ajax calls.&lt;/p&gt;

&lt;p&gt;Only apply this metafield to products that are bundles. Non-bundle products leave it empty and the engine ignores them entirely.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Populate Metafield in Admin
&lt;/h3&gt;

&lt;p&gt;For each bundle product:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the product in Admin.&lt;/li&gt;
&lt;li&gt;Scroll to the metafields section.&lt;/li&gt;
&lt;li&gt;Fill &lt;code&gt;custom.bundle_components&lt;/code&gt; with the component products.&lt;/li&gt;
&lt;li&gt;Save.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is the merchant-facing configuration interface. No code is needed here. The metafield UI handles the product picker.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Include the Bundle Script Globally
&lt;/h3&gt;

&lt;p&gt;In &lt;code&gt;layout/theme.liquid&lt;/code&gt;, include the bundle engine near your other global JavaScript:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"{{ 'bundleCartBreaking.js' | asset_url }}"&lt;/span&gt; &lt;span class="na"&gt;defer&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;defer&lt;/code&gt; attribute ensures the DOM is available before the script initializes event listeners. This must be a global include — not scoped to the product template — because checkout button listeners need to be present on the cart drawer and cart page as well.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Wire Checkout Button Hooks
&lt;/h3&gt;

&lt;p&gt;The bundle engine needs to intercept checkout before the page redirects. It does this by listening to clicks on checkout buttons.&lt;/p&gt;

&lt;p&gt;The existing script uses these selectors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;ddCheckoutButton&lt;/span&gt;          &lt;span class="c1"&gt;// Cart drawer checkout&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ddCartPageCheckoutBtn&lt;/span&gt;     &lt;span class="c1"&gt;// Cart page checkout&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ddBuyNowButton&lt;/span&gt;            &lt;span class="c1"&gt;// Buy now&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These need to match your actual checkout buttons. If your theme uses different selectors (for example, a GoKwik checkout button), you have two options:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option A:&lt;/strong&gt; Add the expected IDs or classes to your existing buttons.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option B:&lt;/strong&gt; Update the selectors in &lt;code&gt;bundleCartBreaking.js&lt;/code&gt; to match your actual buttons.&lt;/p&gt;

&lt;p&gt;The more robust approach for any theme is to use stable &lt;code&gt;data-*&lt;/code&gt; attributes instead of class names or IDs, which are more likely to change across theme updates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;data-role=&lt;/span&gt;&lt;span class="s"&gt;"bundle-checkout"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Checkout&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[data-role="bundle-checkout"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This decouples the bundle engine from the visual/styling layer of your buttons.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Inject Bundle Properties into the Product Form
&lt;/h3&gt;

&lt;p&gt;In &lt;code&gt;snippets/buy-buttons.liquid&lt;/code&gt;, inside the &lt;code&gt;{% form 'product' %}&lt;/code&gt; block, add hidden inputs for the three bundle properties:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;{% if product.metafields.custom.bundle_components != blank %}
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt;
    &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"hidden"&lt;/span&gt;
    &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"properties[_Bundle Id]"&lt;/span&gt;
    &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"{{ product.selected_or_first_available_variant.id }}"&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt;
    &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"hidden"&lt;/span&gt;
    &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"properties[Bundle Name]"&lt;/span&gt;
    &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"{{ product.title | escape }}"&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt;
    &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"hidden"&lt;/span&gt;
    &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"properties[_Individual Product Variant IDs]"&lt;/span&gt;
    &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"bundle-component-ids"&lt;/span&gt;
    &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
{% endif %}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The conditional &lt;code&gt;{% if product.metafields.custom.bundle_components != blank %}&lt;/code&gt; ensures these properties are only added for bundle products. Non-bundle products go through the normal add-to-cart flow untouched.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;_Individual Product Variant IDs&lt;/code&gt; input starts empty. It will be populated by the next step.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Resolve Component Variant IDs at Page Load
&lt;/h3&gt;

&lt;p&gt;The hidden input for component variant IDs needs to be populated with the actual variant IDs of the bundle's component products. You resolve these from the metafield.&lt;/p&gt;

&lt;p&gt;Add this Liquid and JavaScript to the product template or buy-buttons snippet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{% if product.metafields.custom.bundle_components != blank %}
  {% assign component_ids = '' %}
  {% for component_product in product.metafields.custom.bundle_components.value %}
    {% assign variant_id = component_product.selected_or_first_available_variant.id %}
    {% assign component_ids = component_ids | append: variant_id %}
    {% unless forloop.last %}
      {% assign component_ids = component_ids | append: ',' %}
    {% endunless %}
  {% endfor %}

  &amp;lt;script&amp;gt;
    document.addEventListener('DOMContentLoaded', function () {
      var el = document.getElementById('bundle-component-ids');
      if (el &amp;amp;&amp;amp; !el.value) {
        el.value = "{{ component_ids }}";
      }
    });
  &amp;lt;/script&amp;gt;
{% endif %}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This resolves the default/first-available variant for each component product at page load. The resulting comma-separated string — for example &lt;code&gt;"44112233,44112244,44112255"&lt;/code&gt; — gets written into the hidden input before the customer clicks "Add to Cart."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Important edge case:&lt;/strong&gt; If your bundle variant options determine which component variant gets added (for example, a bundle size "M/L" maps to the Medium variant of Product A and the Large variant of Product B), this static Liquid resolution won't be sufficient. You'll need a JavaScript mapping table that listens to variant change events and updates the component IDs dynamically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Example mapping: bundle variant ID → component variant IDs&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;bundleVariantMap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;43001122&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;44112233,44112244&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// Bundle size S/M&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;43001133&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;44112255,44112266&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// Bundle size L/XL&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;variantChanged&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;bundleVariantMap&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;variantId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bundle-component-ids&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ids&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;For fixed-composition bundles with no variant options, the Liquid-only approach works cleanly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6: Cart UI Hides Internal Properties
&lt;/h3&gt;

&lt;p&gt;This step is already implemented via &lt;code&gt;snippets/line-item.liquid&lt;/code&gt;. The template loops over line item properties and skips any key beginning with &lt;code&gt;_&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{% for property in item.properties %}
  {% unless property.first == blank or property.first contains '_' %}
    &amp;lt;div class="cart-item__property"&amp;gt;
      &amp;lt;span&amp;gt;{{ property.first }}:&amp;lt;/span&amp;gt;
      &amp;lt;span&amp;gt;{{ property.last }}&amp;lt;/span&amp;gt;
    &amp;lt;/div&amp;gt;
  {% endunless %}
{% endfor %}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The customer only sees &lt;code&gt;Bundle Name&lt;/code&gt; in the cart. The &lt;code&gt;_Bundle Id&lt;/code&gt; and &lt;code&gt;_Individual Product Variant IDs&lt;/code&gt; values are invisible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 7: The Checkout Split Pipeline
&lt;/h3&gt;

&lt;p&gt;This is the core of the engine. When the customer clicks checkout, &lt;code&gt;bundleCartBreaking.js&lt;/code&gt; intercepts the click and runs the transformation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;breakBundlesForCheckout&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// 1. Fetch current cart&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cartResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/cart.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cartResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// 2. Determine if any bundles exist&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hasBundles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;properties&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;_Individual Product Variant IDs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="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="nx"&gt;hasBundles&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// No bundles — proceed to checkout normally&lt;/span&gt;
    &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/checkout&lt;/span&gt;&lt;span class="dl"&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="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// 3. Build expanded item list&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;expandedItems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

  &lt;span class="nx"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;componentIds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;_Individual Product Variant IDs&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;componentIds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Bundle item — split into components&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;componentIds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
      &lt;span class="nx"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;variantId&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;expandedItems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
          &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;variantId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="na"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// Multiply component qty by bundle qty&lt;/span&gt;
          &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;_Bundle Id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;_Bundle Id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bundle Name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bundle Name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="c1"&gt;// Note: _Individual Product Variant IDs intentionally excluded&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Non-bundle item — pass through unchanged&lt;/span&gt;
      &lt;span class="nx"&gt;expandedItems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;variant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;properties&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;// 4. Clear cart&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/cart/clear.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// 5. Re-add expanded items&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/cart/add.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;expandedItems&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// 6. Set session flag for return reconstruction&lt;/span&gt;
  &lt;span class="nx"&gt;sessionStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;userWentToCheckout&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;true&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// 7. Redirect to checkout&lt;/span&gt;
  &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/checkout&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The critical detail in step 3 is that &lt;code&gt;_Individual Product Variant IDs&lt;/code&gt; is &lt;strong&gt;not&lt;/strong&gt; included on the component rows. Each component row only carries &lt;code&gt;_Bundle Id&lt;/code&gt; and &lt;code&gt;Bundle Name&lt;/code&gt;. This is what allows the reconstruction to work on return — the absence of &lt;code&gt;_Individual Product Variant IDs&lt;/code&gt; on a row signals "this is a component, not a bundle."&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 8: Reconstruct Bundles on Return
&lt;/h3&gt;

&lt;p&gt;On every storefront page load, the engine checks for the &lt;code&gt;userWentToCheckout&lt;/code&gt; session flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DOMContentLoaded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;wentToCheckout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sessionStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;userWentToCheckout&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;wentToCheckout&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="c1"&gt;// Clear the flag immediately to prevent re-running on next page load&lt;/span&gt;
  &lt;span class="nx"&gt;sessionStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;userWentToCheckout&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;undoCartBreaking&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;undoCartBreaking&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cartResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/cart.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cartResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// Group component items by _Bundle Id&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bundleGroups&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nonBundleItems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

  &lt;span class="nx"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bundleId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;properties&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;_Bundle Id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hasComponentIds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;properties&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;_Individual Product Variant IDs&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;bundleId&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;hasComponentIds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// This is a component row (has bundle ID but no component IDs list)&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="nx"&gt;bundleGroups&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;bundleId&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;bundleGroups&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;bundleId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;bundleId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bundleId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;bundleName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bundle Name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
          &lt;span class="na"&gt;variantIds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
          &lt;span class="na"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quantity&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nx"&gt;bundleGroups&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;bundleId&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;variantIds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;variant_id&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="nx"&gt;nonBundleItems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;variant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;properties&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;// If no bundle groups found, nothing to reconstruct&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;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bundleGroups&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&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="c1"&gt;// Build reconstructed bundle items&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reconstructedItems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;nonBundleItems&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bundleGroups&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;group&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;reconstructedItems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bundleId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;_Bundle Id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bundleId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bundle Name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bundleName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;_Individual Product Variant IDs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;variantIds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Clear and rebuild&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/cart/clear.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/cart/add.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;reconstructedItems&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Trigger cart refresh event so drawer/page updates&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispatchEvent&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;CustomEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cart:refresh&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Why This Avoids Separate Bundle Inventory
&lt;/h2&gt;

&lt;p&gt;The key insight in this architecture is that the &lt;strong&gt;bundle product never enters the checkout cart&lt;/strong&gt;. The bundle variant is used as a cart representation vehicle and a grouping key — nothing more. Shopify's checkout always receives real component variant IDs.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Shopify decrements inventory against component variants, not a synthetic bundle SKU.&lt;/li&gt;
&lt;li&gt;Fulfillment receives real, physically pickable SKUs.&lt;/li&gt;
&lt;li&gt;You never have to maintain a shadow inventory model for bundles.&lt;/li&gt;
&lt;li&gt;The bundle product can have &lt;code&gt;"Continue selling when out of stock"&lt;/code&gt; enabled without consequence — stock is enforced at the component level.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the primary operational win. Merchandising gets their clean single-SKU bundle product on the storefront. Operations gets clean per-SKU inventory movement. Neither team has to compromise.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tradeoffs and Why This Pattern Was Worth It
&lt;/h2&gt;

&lt;p&gt;No architecture is without tradeoffs. I want to be transparent about where the risks are.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The risk in &lt;code&gt;clear + add&lt;/code&gt;:&lt;/strong&gt; The transformation involves clearing the entire cart before re-adding. If the &lt;code&gt;POST /cart/add.js&lt;/code&gt; call fails after the clear, the customer's cart is empty. This is the single most dangerous moment in the pipeline. It requires a robust failure rollback strategy (covered in the hardening section below).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Checkout button coverage must be exhaustive:&lt;/strong&gt; If there is a checkout path you haven't wired the bundle engine to — a secondary checkout button, a third-party checkout widget — bundles will slip through un-split. This requires deliberate QA across all cart entry points.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;More complex QA surface:&lt;/strong&gt; Compared to a simple product, a bundle with this transformation engine has significantly more states to test. The test matrix is real work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But the alternative is worse:&lt;/strong&gt; The common alternative — maintaining a separate bundle SKU with its own inventory that you then sync to component products — creates a permanent operational burden. Someone has to keep bundle inventory in sync with component inventory at all times. Overselling becomes a real risk. Fulfillment teams deal with phantom SKUs. This architecture front-loads engineering complexity in exchange for operational simplicity at scale. For stores running bundles as a significant revenue line, that's the right trade.&lt;/p&gt;




&lt;h2&gt;
  
  
  Production Hardening Checklist
&lt;/h2&gt;

&lt;p&gt;The core engine works. Before taking it to production, here is what I would implement:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Preserve cart note and attributes across transformation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;During &lt;code&gt;clear + add&lt;/code&gt;, Shopify loses &lt;code&gt;cart.note&lt;/code&gt; and &lt;code&gt;cart.attributes&lt;/code&gt;. Read them before clearing and restore them via &lt;code&gt;/cart/update.js&lt;/code&gt; after the re-add:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/cart.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cartNote&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;note&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cartAttributes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// ... clear and re-add ...&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/cart/update.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;note&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cartNote&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cartAttributes&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;&lt;strong&gt;2. Failure rollback&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Snapshot the cart before transformation. If the add fails after clear, restore from snapshot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;snapshot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// Deep copy before clear&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/cart/clear.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/cart/add.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;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="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Restore snapshot&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/cart/add.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="cm"&gt;/* map to add format */&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bundle split failed, cart restored:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&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="c1"&gt;// Do not redirect to checkout&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Button idempotency&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Disable checkout buttons during transformation to prevent double-clicks triggering duplicate pipeline runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;disabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Processing...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// Re-enable only on failure&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Skip non-bundle carts gracefully&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Check for bundles before running any transformation. If no bundle lines are present, skip the entire pipeline and redirect directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&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="nx"&gt;hasBundles&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/checkout&lt;/span&gt;&lt;span class="dl"&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5. Quantity multiplication&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When a customer adds a bundle with quantity 2, each component should also be added with quantity 2. Make sure the expansion step uses &lt;code&gt;item.quantity&lt;/code&gt; for component quantities.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Feature flag for gradual rollout&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Gate the entire engine behind a theme setting to enable safe rollout:&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;config/settings_schema.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"checkbox"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"enable_bundle_split_checkout"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Enable bundle split at checkout"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In &lt;code&gt;layout/theme.liquid&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{% if settings.enable_bundle_split_checkout %}
  &amp;lt;script src="{{ 'bundleCartBreaking.js' | asset_url }}" defer&amp;gt;&amp;lt;/script&amp;gt;
{% endif %}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enable on staging first, validate against the full test matrix, then enable on production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Use stable &lt;code&gt;data-*&lt;/code&gt; hooks&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Replace class-based or ID-based checkout button selectors with &lt;code&gt;data-role&lt;/code&gt; attributes. CSS classes change with theme updates. &lt;code&gt;data-*&lt;/code&gt; attributes are yours to control.&lt;/p&gt;




&lt;h2&gt;
  
  
  QA Test Matrix
&lt;/h2&gt;

&lt;p&gt;Run every case in this matrix before launch.&lt;/p&gt;

&lt;h3&gt;
  
  
  Functional Tests
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test Case&lt;/th&gt;
&lt;th&gt;Expected Outcome&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Add bundle to cart&lt;/td&gt;
&lt;td&gt;One cart line with all three private properties set&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;View cart&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;_&lt;/code&gt;-prefixed properties hidden, &lt;code&gt;Bundle Name&lt;/code&gt; visible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Checkout from cart drawer&lt;/td&gt;
&lt;td&gt;Cart splits, checkout receives component variants&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Checkout from cart page&lt;/td&gt;
&lt;td&gt;Same as above&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Buy now flow&lt;/td&gt;
&lt;td&gt;Same as above, single item path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Return after checkout&lt;/td&gt;
&lt;td&gt;Cart reconstructs bundle representation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Non-bundle product in cart&lt;/td&gt;
&lt;td&gt;Passes through checkout transformation untouched&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Quantity Tests
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test Case&lt;/th&gt;
&lt;th&gt;Expected Outcome&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Bundle qty = 1&lt;/td&gt;
&lt;td&gt;Each component qty = 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bundle qty = 2&lt;/td&gt;
&lt;td&gt;Each component qty = 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bundle qty = 1 + normal product qty = 2&lt;/td&gt;
&lt;td&gt;Bundle splits correctly, normal product unchanged&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Update bundle qty in cart before checkout&lt;/td&gt;
&lt;td&gt;Correct qty passed to split&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Error / Edge Cases
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test Case&lt;/th&gt;
&lt;th&gt;Expected Outcome&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;/cart/clear.js&lt;/code&gt; fails&lt;/td&gt;
&lt;td&gt;Cart unchanged, user shown error, not redirected&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;/cart/add.js&lt;/code&gt; fails after clear&lt;/td&gt;
&lt;td&gt;Snapshot restored, user shown error&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;_Individual Product Variant IDs&lt;/code&gt; is empty&lt;/td&gt;
&lt;td&gt;Skip split, proceed to checkout normally&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Component variant out of stock&lt;/td&gt;
&lt;td&gt;Handle gracefully, surface error before checkout&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Regression Tests
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test Case&lt;/th&gt;
&lt;th&gt;Expected Outcome&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GoKwik / third-party checkout button still works&lt;/td&gt;
&lt;td&gt;Bundle split fires before redirect&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cart drawer refresh event fires after reconstruction&lt;/td&gt;
&lt;td&gt;Drawer reflects updated cart without page reload&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cart note preserved through transformation&lt;/td&gt;
&lt;td&gt;Note intact after checkout and return&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  What I Would Do Differently If Rebuilding Today
&lt;/h2&gt;

&lt;p&gt;This architecture is sound, but with the benefit of hindsight there are several things I'd approach differently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consolidate split and reconstruct into a single module.&lt;/strong&gt; The current implementation has the two directions of transformation somewhat spread across the file. I'd build a single &lt;code&gt;BundleCartEngine&lt;/code&gt; module with clearly named methods: &lt;code&gt;splitForCheckout()&lt;/code&gt; and &lt;code&gt;reconstructFromReturn()&lt;/code&gt;. This makes the code dramatically easier to reason about and test in isolation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Add structured telemetry.&lt;/strong&gt; Every transformation step should emit an event — split initiated, split complete, split failed, reconstruction initiated, reconstruction complete. This data is invaluable when debugging production issues. Even a simple &lt;code&gt;console.group&lt;/code&gt; / &lt;code&gt;console.groupEnd&lt;/code&gt; structured log with timestamps helps enormously.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use deterministic transaction IDs.&lt;/strong&gt; When the split fires, generate a UUID and attach it to all the component rows as a property. This gives you a way to trace a specific checkout event end-to-end if something goes wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Introduce a proper state machine.&lt;/strong&gt; The current implementation has implicit states (cart is bundle-native, cart is component-native, transformation in progress). Making these explicit with an enum and enforced transitions removes entire categories of race condition bugs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stronger button hook contracts.&lt;/strong&gt; Using &lt;code&gt;data-role="bundle-checkout"&lt;/code&gt; as a convention across all checkout-adjacent buttons, documented in a theme README, means future developers can add new checkout entry points without breaking the bundle engine.&lt;/p&gt;




&lt;h2&gt;
  
  
  What This Implementation Demonstrates
&lt;/h2&gt;

&lt;p&gt;I've found this project comes up frequently in technical conversations, and the reason is that it touches almost every layer of the Shopify theme stack simultaneously.&lt;/p&gt;

&lt;p&gt;It required:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Understanding how Shopify's Ajax Cart API and line item properties work at a protocol level.&lt;/li&gt;
&lt;li&gt;Building a two-directional transformation pipeline that can safely mutate and restore cart state.&lt;/li&gt;
&lt;li&gt;Designing a data contract (the three-property bundle metadata schema) that is deterministic and survives round-trips through Shopify's cart storage.&lt;/li&gt;
&lt;li&gt;Working with Shopify's metafield data model to create a merchant-facing configuration interface.&lt;/li&gt;
&lt;li&gt;Navigating the gap between what Shopify's storefront presents and what Shopify's checkout requires.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If I had to describe it in one sentence: &lt;strong&gt;I built a reversible bundle transformation engine where the cart stays bundle-native for UX, checkout stays SKU-native for inventory truth, and merchant configuration stays metafield-native for scalability — all within Shopify's native primitives, with no backend service required.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://shopify.dev/docs/api/ajax/reference/cart" rel="noopener noreferrer"&gt;Shopify Cart Ajax API — private line item properties and cart endpoints&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://shopify.dev/docs/storefronts/themes/architecture/templates/product/overview" rel="noopener noreferrer"&gt;Shopify product template docs — line item properties in product forms&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://shopify.dev/docs/api/liquid/objects/metafield" rel="noopener noreferrer"&gt;Shopify metafield object — list &lt;code&gt;.value&lt;/code&gt; behavior in Liquid&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://shopify.dev/docs/apps/build/metafields/list-of-data-types" rel="noopener noreferrer"&gt;Shopify metafield data types — &lt;code&gt;list.product_reference&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>shopify</category>
      <category>ecommerce</category>
      <category>javascript</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>I Built a Tool That Turns GitHub Milestones Into Shareable Video Clips</title>
      <dc:creator>coder om</dc:creator>
      <pubDate>Fri, 06 Feb 2026 10:09:15 +0000</pubDate>
      <link>https://dev.to/coderom/i-built-a-tool-that-turns-github-milestones-into-shareable-video-clips-1nn5</link>
      <guid>https://dev.to/coderom/i-built-a-tool-that-turns-github-milestones-into-shareable-video-clips-1nn5</guid>
      <description>&lt;p&gt;&lt;strong&gt;Why I built Milestone Clip, how it works, and who it’s for&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I kept hitting milestones on GitHub—100 stars on a repo, 500 followers, 1k forks—and wanted to share them in a way that actually looked good. Screenshots felt flat. Making a proper video meant opening an editor, picking fonts, syncing numbers, and exporting. Most “milestone” tools were either generic templates, locked behind heavy subscriptions, or required more time than I had.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;Milestone Clip&lt;/strong&gt;: paste a repo or username, pick a milestone and a style, and get an MP4 you can post anywhere—usually in under a minute. No design skills, no watermark. This is the story behind it and how you can use it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: celebrating GitHub wins shouldn’t be a side project
&lt;/h2&gt;

&lt;p&gt;GitHub milestones are worth celebrating. Stars, forks, and followers are signals that your work is being seen and used. But turning those numbers into something you’d actually want to post is awkward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Screenshots&lt;/strong&gt; are quick but look unfinished.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generic “achievement” generators&lt;/strong&gt; often feel templated and samey.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real video editors&lt;/strong&gt; give control but cost time (and sometimes money).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SaaS “social video” tools&lt;/strong&gt; are often built for marketing teams, not developers, and pricing can be steep for occasional use.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wanted something in the middle: &lt;strong&gt;one input, one style choice, one download&lt;/strong&gt;. No account required to try it, no big learning curve, and output that looks intentional enough for X, LinkedIn, or Reels.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Milestone Clip does
&lt;/h2&gt;

&lt;p&gt;Milestone Clip is a web app that turns GitHub milestones into short, ready-to-post video clips.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You provide:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;GitHub repository&lt;/strong&gt; (e.g. &lt;code&gt;owner/repo&lt;/code&gt;) or a &lt;strong&gt;GitHub username&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;milestone type&lt;/strong&gt;: repo stars, repo forks, user followers, or total stars across a user’s repos&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;visual style&lt;/strong&gt; (minimal, cinematic, feed-style, etc.) and &lt;strong&gt;aspect ratio&lt;/strong&gt; (9:16 for Reels/Shorts, 1:1 for feed, 16:9 for YouTube or slides)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;You get:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An &lt;strong&gt;MP4&lt;/strong&gt; you can download and post immediately—no watermark on the free tier&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Light and dark themes&lt;/strong&gt; so the clip can match your brand or platform&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiple styles&lt;/strong&gt;—from simple “float” layouts to more visual options (e.g. spotlight, constellation, mosaic)—so it doesn’t all look the same&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The flow is deliberately short: enter repo or username → choose milestone and style → generate → download. No install, no code.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftb9epzly88dlsflrcod0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftb9epzly88dlsflrcod0.png" alt="Turn GitHub milestones into shareable videos. I built Milestone Clip so you can celebrate stars, forks, and followers with one-click clips. No editor needed." width="800" height="507"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Who it’s for
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Open-source maintainers&lt;/strong&gt; — Celebrate stars or forks when you hit 100, 1k, 10k, or any number you care about.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Developers&lt;/strong&gt; — Share follower growth or total stars across your profile when you hit a personal milestone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Communities and teams&lt;/strong&gt; — Create a quick clip when a shared project hits a milestone and post it on social or in Slack/Discord.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anyone&lt;/strong&gt; — Who’s proud of their GitHub numbers and wants a simple, professional way to show them off without opening a video editor.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How I think about the product
&lt;/h2&gt;

&lt;p&gt;A few principles that shaped the product:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Speed over flexibility&lt;/strong&gt; — Optimized for “I have a number, I want a clip” in under a minute, not for full creative control.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Developer-first&lt;/strong&gt; — Repo URLs and usernames are the main input; no “campaign” or “brand kit” setup required.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free to try&lt;/strong&gt; — One free video per day with core styles and standard quality so you can see the output before committing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No watermark on free tier&lt;/strong&gt; — The free clip is usable as-is; premium adds more styles, higher resolution (including 4K), and more generations.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The 10,000-stars open-source promise
&lt;/h2&gt;

&lt;p&gt;One more thing that matters to me: &lt;strong&gt;if the Milestone Clip repo reaches 10,000 GitHub stars, the full app will be open-sourced&lt;/strong&gt;—frontend, backend, video compositions, billing, and deployment. No “community edition” with features removed. The goal is to grow it with the community and then give the codebase back. So if you find it useful, starring the repo doesn’t just signal interest; it’s the mechanism for eventually opening the whole project.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F79i2bpj8yjq5h96myma1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F79i2bpj8yjq5h96myma1.png" alt="Milestone Clip preview: choose a video style and aspect ratio, then download your GitHub milestone clip." width="800" height="506"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;You can use Milestone Clip at &lt;strong&gt;&lt;a href="https://milestoneclip.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=milestone-clip-launch" rel="noopener noreferrer"&gt;milestoneclip.com&lt;/a&gt;&lt;/strong&gt;. No install, no code—just a repo or username and a few clicks.&lt;/p&gt;

&lt;p&gt;Watch Full Preview at - &lt;a href="https://youtu.be/hFbIECIOYTc" rel="noopener noreferrer"&gt;https://youtu.be/hFbIECIOYTc&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you’re a maintainer, creator, or community lead and you’ve ever skipped sharing a milestone because making a video felt like too much work, I built this for you. I’d love to hear how you’d use it or what you’d want next—other platforms, more milestone types, or different styles. You can reach me on X &lt;a href="https://x.com/1omsharma" rel="noopener noreferrer"&gt;@1omsharma&lt;/a&gt; or through the app.&lt;/p&gt;

&lt;p&gt;Thanks for reading — and here’s to the next milestone.&lt;/p&gt;

</description>
      <category>github</category>
      <category>opensource</category>
      <category>developertools</category>
      <category>indiehacker</category>
    </item>
    <item>
      <title>Why Tech Careers Feel So Emotionally Heavy Now</title>
      <dc:creator>coder om</dc:creator>
      <pubDate>Wed, 28 Jan 2026 04:02:48 +0000</pubDate>
      <link>https://dev.to/coderom/why-tech-careers-feel-so-emotionally-heavy-now-3dig</link>
      <guid>https://dev.to/coderom/why-tech-careers-feel-so-emotionally-heavy-now-3dig</guid>
      <description>&lt;p&gt;I’ve been thinking about this a lot lately.&lt;/p&gt;

&lt;p&gt;Not about salaries.&lt;br&gt;
Not about tools.&lt;br&gt;
Not about AI replacing jobs.&lt;/p&gt;

&lt;p&gt;But about something quieter.&lt;/p&gt;

&lt;p&gt;Why does a tech career feel &lt;strong&gt;so emotionally heavy&lt;/strong&gt; now?&lt;/p&gt;

&lt;p&gt;Even when you’re doing “okay”.&lt;br&gt;
Even when nothing is technically wrong.&lt;/p&gt;




&lt;h2&gt;
  
  
  It didn’t used to feel this way
&lt;/h2&gt;

&lt;p&gt;Earlier, tech felt stressful, yes.&lt;br&gt;
But it didn’t feel &lt;strong&gt;heavy&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;There was pressure to learn.&lt;br&gt;
Pressure to perform.&lt;br&gt;
Pressure to grow.&lt;/p&gt;

&lt;p&gt;But there was also time.&lt;/p&gt;

&lt;p&gt;Time to be average.&lt;br&gt;
Time to be slow.&lt;br&gt;
Time to make mistakes.&lt;/p&gt;

&lt;p&gt;Now it feels like the weight has increased.&lt;/p&gt;

&lt;p&gt;Not on our skills.&lt;br&gt;
But on our minds.&lt;/p&gt;




&lt;h2&gt;
  
  
  Everything is faster, all the time
&lt;/h2&gt;

&lt;p&gt;Learning cycles are shorter.&lt;br&gt;
Tools change faster.&lt;br&gt;
Expectations reset quicker.&lt;/p&gt;

&lt;p&gt;You don’t just feel behind.&lt;/p&gt;

&lt;p&gt;You feel &lt;strong&gt;constantly behind&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Even when you’re learning daily,&lt;br&gt;
there’s a sense that you’re late to something.&lt;/p&gt;

&lt;p&gt;Late to AI.&lt;br&gt;
Late to the right stack.&lt;br&gt;
Late to the right opportunity.&lt;/p&gt;

&lt;p&gt;That constant catching-up mode is exhausting.&lt;/p&gt;




&lt;h2&gt;
  
  
  The market uncertainty sits in your head
&lt;/h2&gt;

&lt;p&gt;Earlier, if one job didn’t work,&lt;br&gt;
another probably would.&lt;/p&gt;

&lt;p&gt;Now the question is always there:&lt;/p&gt;

&lt;p&gt;What if this doesn’t work out?&lt;br&gt;
What if I chose the wrong thing?&lt;br&gt;
What if the market doesn’t recover?&lt;/p&gt;

&lt;p&gt;You don’t think about it consciously all the time.&lt;/p&gt;

&lt;p&gt;But it sits quietly in the background.&lt;br&gt;
Like low-level anxiety running all day.&lt;/p&gt;




&lt;h2&gt;
  
  
  AI added pressure, not clarity
&lt;/h2&gt;

&lt;p&gt;AI was supposed to make things easier.&lt;/p&gt;

&lt;p&gt;In some ways, it did.&lt;/p&gt;

&lt;p&gt;But mentally, it added a new layer:&lt;/p&gt;

&lt;p&gt;Should I use this?&lt;br&gt;
Am I using it enough?&lt;br&gt;
Will this replace me?&lt;br&gt;
Do I need to pivot again?&lt;/p&gt;

&lt;p&gt;Instead of reducing stress,&lt;br&gt;
it often increases decision fatigue.&lt;/p&gt;

&lt;p&gt;Not because AI is bad.&lt;/p&gt;

&lt;p&gt;But because uncertainty multiplied.&lt;/p&gt;




&lt;h2&gt;
  
  
  Comparison is now unavoidable
&lt;/h2&gt;

&lt;p&gt;Earlier, comparison was limited.&lt;/p&gt;

&lt;p&gt;College friends.&lt;br&gt;
Office teammates.&lt;br&gt;
Maybe LinkedIn.&lt;/p&gt;

&lt;p&gt;Now it’s global.&lt;/p&gt;

&lt;p&gt;You compare with people from different countries,&lt;br&gt;
different backgrounds,&lt;br&gt;
different timelines.&lt;/p&gt;

&lt;p&gt;Someone is always younger.&lt;br&gt;
Someone is always faster.&lt;br&gt;
Someone is always ahead.&lt;/p&gt;

&lt;p&gt;So even normal progress feels insufficient.&lt;/p&gt;

&lt;p&gt;You’re moving, but it doesn’t feel like enough.&lt;/p&gt;




&lt;h2&gt;
  
  
  Identity and career got too close
&lt;/h2&gt;

&lt;p&gt;This is where it becomes emotionally heavy.&lt;/p&gt;

&lt;p&gt;Tech stopped being just a job.&lt;/p&gt;

&lt;p&gt;It became identity.&lt;br&gt;
Status.&lt;br&gt;
Self-worth.&lt;/p&gt;

&lt;p&gt;So when the career feels unstable,&lt;br&gt;
your sense of self feels unstable too.&lt;/p&gt;

&lt;p&gt;You’re not just worried about work.&lt;/p&gt;

&lt;p&gt;You’re worried about &lt;strong&gt;who you are becoming&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Or worse,&lt;br&gt;
who you might fail to become.&lt;/p&gt;




&lt;h2&gt;
  
  
  There is no clear “done” anymore
&lt;/h2&gt;

&lt;p&gt;Earlier, there were milestones.&lt;/p&gt;

&lt;p&gt;Get the job.&lt;br&gt;
Get promoted.&lt;br&gt;
Become senior.&lt;/p&gt;

&lt;p&gt;Now the finish line keeps moving.&lt;/p&gt;

&lt;p&gt;Even after reaching something,&lt;br&gt;
it doesn’t feel like arrival.&lt;/p&gt;

&lt;p&gt;It feels temporary.&lt;/p&gt;

&lt;p&gt;Like you need to keep proving yourself again and again.&lt;/p&gt;

&lt;p&gt;That constant proving is emotionally draining.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this matters
&lt;/h2&gt;

&lt;p&gt;This emotional weight doesn’t show up on resumes.&lt;/p&gt;

&lt;p&gt;But it shows up as:&lt;/p&gt;

&lt;p&gt;Feeling tired all the time.&lt;br&gt;
Losing joy in learning.&lt;br&gt;
Second-guessing yourself constantly.&lt;br&gt;
Feeling guilty for resting.&lt;/p&gt;

&lt;p&gt;And people think this is normal.&lt;/p&gt;

&lt;p&gt;They think they are weak.&lt;/p&gt;

&lt;p&gt;They are not.&lt;/p&gt;

&lt;p&gt;They are responding to a system that became faster, louder, and more uncertain.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final honest thought
&lt;/h2&gt;

&lt;p&gt;Tech careers feel emotionally heavy now&lt;br&gt;
because they ask too much from our minds,&lt;br&gt;
not just our skills.&lt;/p&gt;

&lt;p&gt;The pressure is not only to be good.&lt;/p&gt;

&lt;p&gt;It’s to be relevant.&lt;br&gt;
Fast.&lt;br&gt;
Adaptable.&lt;br&gt;
Always learning.&lt;br&gt;
Always visible.&lt;/p&gt;

&lt;p&gt;Feeling this weight doesn’t mean you are failing.&lt;/p&gt;

&lt;p&gt;It means you are paying attention.&lt;/p&gt;

&lt;p&gt;And the first step to surviving this industry&lt;br&gt;
is admitting that the pressure is real —&lt;br&gt;
even if nobody talks about it openly.&lt;/p&gt;

</description>
      <category>techcareer</category>
      <category>2026careers</category>
      <category>career</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The Hardest Part About Learning to Code in 2026 Is Not Learning Code</title>
      <dc:creator>coder om</dc:creator>
      <pubDate>Mon, 26 Jan 2026 03:33:59 +0000</pubDate>
      <link>https://dev.to/coderom/the-hardest-part-about-learning-to-code-in-2026-is-not-learning-code-59bc</link>
      <guid>https://dev.to/coderom/the-hardest-part-about-learning-to-code-in-2026-is-not-learning-code-59bc</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgn3zxhcw7w2046t3x04i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgn3zxhcw7w2046t3x04i.png" alt="Learning to Code in 2026" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I want to say something that sounds strange, but feels very true to me.&lt;/p&gt;

&lt;p&gt;The hardest part about learning to code in 2026 is &lt;strong&gt;not learning code&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It’s everything around it.&lt;/p&gt;




&lt;h2&gt;
  
  
  What people think learning code is
&lt;/h2&gt;

&lt;p&gt;Most people still think learning code means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Open laptop&lt;/li&gt;
&lt;li&gt;Watch tutorials&lt;/li&gt;
&lt;li&gt;Practice problems&lt;/li&gt;
&lt;li&gt;Build projects&lt;/li&gt;
&lt;li&gt;Get job&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That was maybe true some years ago.&lt;/p&gt;

&lt;p&gt;Now it feels very different.&lt;/p&gt;




&lt;h2&gt;
  
  
  The real problem is not syntax
&lt;/h2&gt;

&lt;p&gt;Today you can learn syntax in weeks.&lt;/p&gt;

&lt;p&gt;JavaScript, Python, React, whatever.&lt;br&gt;
AI can explain it.&lt;br&gt;
YouTube can explain it.&lt;br&gt;
Docs can explain it.&lt;/p&gt;

&lt;p&gt;Syntax is not the hard part anymore.&lt;/p&gt;

&lt;p&gt;The hard part is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What should I learn&lt;/li&gt;
&lt;li&gt;In what order&lt;/li&gt;
&lt;li&gt;For which role&lt;/li&gt;
&lt;li&gt;With which tools&lt;/li&gt;
&lt;li&gt;For which market&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This mental confusion is exhausting.&lt;/p&gt;




&lt;h2&gt;
  
  
  Too many paths, no clear direction
&lt;/h2&gt;

&lt;p&gt;Frontend.&lt;br&gt;
Backend.&lt;br&gt;
Full stack.&lt;br&gt;
DevOps.&lt;br&gt;
Data.&lt;br&gt;
AI.&lt;br&gt;
ML.&lt;br&gt;
Cloud.&lt;br&gt;
Security.&lt;/p&gt;

&lt;p&gt;Every path says:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;This is the future&lt;/li&gt;
&lt;li&gt;This is in demand&lt;/li&gt;
&lt;li&gt;This is dying&lt;/li&gt;
&lt;li&gt;This is saturated&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So you are always thinking:&lt;/p&gt;

&lt;p&gt;Am I late?&lt;br&gt;
Am I learning the wrong thing?&lt;br&gt;
Should I switch?&lt;br&gt;
Should I restart?&lt;/p&gt;

&lt;p&gt;This constant decision-making drains more energy than coding itself.&lt;/p&gt;




&lt;h2&gt;
  
  
  Comparison is the real killer
&lt;/h2&gt;

&lt;p&gt;You open X or LinkedIn.&lt;/p&gt;

&lt;p&gt;Someone built a startup at 19.&lt;br&gt;
Someone got into Google.&lt;br&gt;
Someone built 10 SaaS products.&lt;br&gt;
Someone learned AI in 3 months.&lt;/p&gt;

&lt;p&gt;And you are still debugging a basic feature.&lt;/p&gt;

&lt;p&gt;So even when you are learning, you feel behind.&lt;/p&gt;

&lt;p&gt;Not because you are slow.&lt;br&gt;
But because you are always seeing the &lt;strong&gt;top 1 percent&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Earlier you compared with your college friends.&lt;br&gt;
Now you compare with the whole internet.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tools change faster than skills
&lt;/h2&gt;

&lt;p&gt;You finally learn something.&lt;/p&gt;

&lt;p&gt;A new framework comes.&lt;br&gt;
A new library comes.&lt;br&gt;
A new AI tool comes.&lt;/p&gt;

&lt;p&gt;And suddenly you feel outdated again.&lt;/p&gt;

&lt;p&gt;So the feeling is:&lt;/p&gt;

&lt;p&gt;I am always running.&lt;br&gt;
But the finish line keeps moving.&lt;/p&gt;




&lt;h2&gt;
  
  
  The mental load nobody talks about
&lt;/h2&gt;

&lt;p&gt;The real difficulty is not code.&lt;/p&gt;

&lt;p&gt;It’s:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Overthinking&lt;/li&gt;
&lt;li&gt;Self-doubt&lt;/li&gt;
&lt;li&gt;Fear of missing out&lt;/li&gt;
&lt;li&gt;Fear of being replaced&lt;/li&gt;
&lt;li&gt;Fear of choosing the wrong path&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Earlier learning was slow but calm.&lt;/p&gt;

&lt;p&gt;Now learning is fast but stressful.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final honest thought
&lt;/h2&gt;

&lt;p&gt;In 2026, anyone can learn to code.&lt;/p&gt;

&lt;p&gt;But staying mentally stable while learning is the real skill.&lt;/p&gt;

&lt;p&gt;The hardest part is not writing functions.&lt;br&gt;
It’s managing your mind in a noisy world.&lt;/p&gt;

&lt;p&gt;Too much information.&lt;br&gt;
Too many examples.&lt;br&gt;
Too many opinions.&lt;/p&gt;

&lt;p&gt;So if someone feels tired, confused, lost:&lt;/p&gt;

&lt;p&gt;It does not mean they are bad at coding.&lt;/p&gt;

&lt;p&gt;It just means they are learning in the most chaotic time this industry has ever seen.&lt;/p&gt;

</description>
      <category>coding</category>
      <category>learncoding</category>
      <category>codingin2026</category>
      <category>codingwithai</category>
    </item>
    <item>
      <title>Software Engineering Is Shifting From a Mass Career to a High-Skill Profession</title>
      <dc:creator>coder om</dc:creator>
      <pubDate>Sun, 25 Jan 2026 04:38:35 +0000</pubDate>
      <link>https://dev.to/coderom/software-engineering-is-shifting-from-a-mass-career-to-a-high-skill-profession-4ecp</link>
      <guid>https://dev.to/coderom/software-engineering-is-shifting-from-a-mass-career-to-a-high-skill-profession-4ecp</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F61qyib2mx1udmembydb4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F61qyib2mx1udmembydb4.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;br&gt;
I want to talk about something I’ve been feeling for a long time, and I think many people like me feel it too, but we don’t always say it clearly.&lt;/p&gt;

&lt;p&gt;Software engineering is slowly shifting from a &lt;strong&gt;mass career&lt;/strong&gt; to a &lt;strong&gt;high-skill profession&lt;/strong&gt;.&lt;br&gt;
More like finance or medicine.&lt;br&gt;
Less like a factory pipeline.&lt;/p&gt;

&lt;p&gt;And this is not hype. This is not fear. This is just what I’m seeing around me.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where I am coming from
&lt;/h2&gt;

&lt;p&gt;I am from India.&lt;br&gt;
Small town.&lt;br&gt;
Tier-3 college.&lt;br&gt;
Graduated recently.&lt;/p&gt;

&lt;p&gt;Like most people around me, I was told one simple story growing up:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Learn coding, get into IT, life is set.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And for many years, that story was true.&lt;/p&gt;

&lt;p&gt;There was a clear path:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;learn basics&lt;/li&gt;
&lt;li&gt;get a junior job&lt;/li&gt;
&lt;li&gt;grow slowly&lt;/li&gt;
&lt;li&gt;become mid-level&lt;/li&gt;
&lt;li&gt;then senior&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The industry had space for &lt;strong&gt;a lot of average people&lt;/strong&gt;.&lt;br&gt;
Not dumb people. Just normal people who needed time.&lt;/p&gt;

&lt;p&gt;That time was provided by junior roles.&lt;/p&gt;




&lt;h2&gt;
  
  
  What changed (and everyone feels it)
&lt;/h2&gt;

&lt;p&gt;Now with AI, something big changed.&lt;/p&gt;

&lt;p&gt;The work that juniors used to do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CRUD&lt;/li&gt;
&lt;li&gt;simple features&lt;/li&gt;
&lt;li&gt;bug fixing&lt;/li&gt;
&lt;li&gt;writing boilerplate&lt;/li&gt;
&lt;li&gt;copying patterns&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AI does this extremely well.&lt;/p&gt;

&lt;p&gt;Not 50%.&lt;br&gt;
Not 70%.&lt;br&gt;
Honestly, &lt;strong&gt;very very well&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So companies are asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Why hire 10 juniors when 3 seniors + AI can do it?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And that one question is quietly reshaping the whole industry.&lt;/p&gt;




&lt;h2&gt;
  
  
  The ladder is disappearing
&lt;/h2&gt;

&lt;p&gt;Earlier the industry had a ladder:&lt;/p&gt;

&lt;p&gt;junior → mid → senior&lt;/p&gt;

&lt;p&gt;Now it feels like:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;prove-you’re-mid → maybe become senior&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The bottom layer is shrinking.&lt;/p&gt;

&lt;p&gt;Not because software is dying.&lt;br&gt;
But because &lt;strong&gt;human beginner work is dying&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is the part nobody wants to say openly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Expectations are insane now
&lt;/h2&gt;

&lt;p&gt;Even for “junior” roles, companies want:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;system design&lt;/li&gt;
&lt;li&gt;cloud&lt;/li&gt;
&lt;li&gt;DevOps&lt;/li&gt;
&lt;li&gt;AI tools&lt;/li&gt;
&lt;li&gt;good communication&lt;/li&gt;
&lt;li&gt;production experience&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So basically:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Be experienced, but with zero experience.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is not just India.&lt;br&gt;
This is US, Europe, everywhere.&lt;/p&gt;

&lt;p&gt;The title still says “Junior”.&lt;br&gt;
The reality says “Almost senior”.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why it feels like finance or medicine now
&lt;/h2&gt;

&lt;p&gt;Look at finance or medicine:&lt;/p&gt;

&lt;p&gt;Not everyone can enter.&lt;br&gt;
Not everyone survives.&lt;br&gt;
Not everyone gets rich.&lt;/p&gt;

&lt;p&gt;You need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fast learning&lt;/li&gt;
&lt;li&gt;strong base&lt;/li&gt;
&lt;li&gt;mental stamina&lt;/li&gt;
&lt;li&gt;continuous pressure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Software is moving in the same direction.&lt;/p&gt;

&lt;p&gt;Earlier it was a &lt;strong&gt;mass career&lt;/strong&gt;:&lt;br&gt;
millions could enter, slowly learn, find a place.&lt;/p&gt;

&lt;p&gt;Now it’s becoming a &lt;strong&gt;high-skill profession&lt;/strong&gt;:&lt;br&gt;
small percentage thrive, many struggle to enter.&lt;/p&gt;




&lt;h2&gt;
  
  
  The uncomfortable truth
&lt;/h2&gt;

&lt;p&gt;The world is not running out of software work.&lt;br&gt;
It is running out of &lt;strong&gt;safe learning jobs&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So what happens to all the graduates every year?&lt;/p&gt;

&lt;p&gt;In India alone, millions graduate in tech.&lt;/p&gt;

&lt;p&gt;But:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;entry roles are shrinking&lt;/li&gt;
&lt;li&gt;training budgets are shrinking&lt;/li&gt;
&lt;li&gt;patience is shrinking&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So many people will:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;move to non-dev roles&lt;/li&gt;
&lt;li&gt;leave tech silently&lt;/li&gt;
&lt;li&gt;feel “I’m not good enough”&lt;/li&gt;
&lt;li&gt;blame themselves&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even though it’s actually a &lt;strong&gt;structural change&lt;/strong&gt;, not personal failure.&lt;/p&gt;




&lt;h2&gt;
  
  
  This is not a cycle, this is a shift
&lt;/h2&gt;

&lt;p&gt;Earlier bad job markets were cycles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;2008&lt;/li&gt;
&lt;li&gt;2020&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This feels different.&lt;/p&gt;

&lt;p&gt;Because AI doesn’t go backwards.&lt;/p&gt;

&lt;p&gt;Once companies learn they can ship faster with fewer humans, they won’t suddenly go back to old ways just to be nice.&lt;/p&gt;




&lt;h2&gt;
  
  
  The real impact is psychological
&lt;/h2&gt;

&lt;p&gt;The biggest damage is not even technical.&lt;/p&gt;

&lt;p&gt;It’s this feeling:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;always late&lt;/li&gt;
&lt;li&gt;always behind&lt;/li&gt;
&lt;li&gt;always catching up&lt;/li&gt;
&lt;li&gt;always one tool away from being outdated&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Earlier you could relax for a year and still be okay.&lt;br&gt;
Now one year feels like ten.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final honest line
&lt;/h2&gt;

&lt;p&gt;Software engineering is not dying.&lt;br&gt;
But the &lt;strong&gt;easy entry version of it is dying&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It’s becoming:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;more elite&lt;/li&gt;
&lt;li&gt;more stressful&lt;/li&gt;
&lt;li&gt;more unequal&lt;/li&gt;
&lt;li&gt;more unforgiving&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And for people like me:&lt;br&gt;
small town, tier-3 college, normal background —&lt;br&gt;
the gap feels bigger than ever.&lt;/p&gt;

&lt;p&gt;Not because we are worse.&lt;br&gt;
But because the ladder itself is getting shorter.&lt;/p&gt;




&lt;p&gt;I’m not pessimistic.&lt;br&gt;
I’m not optimistic either.&lt;/p&gt;

&lt;p&gt;I’m just realistic.&lt;/p&gt;

&lt;p&gt;This industry is still powerful.&lt;br&gt;
Still exciting.&lt;br&gt;
Still meaningful.&lt;/p&gt;

&lt;p&gt;But it’s no longer a &lt;strong&gt;factory pipeline for mass middle-class dreams&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It’s becoming a &lt;strong&gt;high-skill arena&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And everyone entering now should at least know that truth —&lt;br&gt;
before blaming themselves for a system that quietly changed.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>techcarrier</category>
      <category>ai</category>
      <category>career</category>
    </item>
    <item>
      <title>LinkedIn Strips Formatting — So I Built a Tool to Preserve It</title>
      <dc:creator>coder om</dc:creator>
      <pubDate>Thu, 18 Dec 2025 09:09:58 +0000</pubDate>
      <link>https://dev.to/coderom/linkedin-strips-formatting-so-i-built-a-tool-to-preserve-it-1igf</link>
      <guid>https://dev.to/coderom/linkedin-strips-formatting-so-i-built-a-tool-to-preserve-it-1igf</guid>
      <description>&lt;p&gt;Have you ever pasted a post into LinkedIn and thought…&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;“Wait… where did all my formatting go?”&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Your &lt;strong&gt;bold&lt;/strong&gt; disappeared.&lt;br&gt;
Your &lt;em&gt;italics&lt;/em&gt; vanished.&lt;br&gt;
Your bullets turned into plain text.&lt;/p&gt;

&lt;p&gt;I hit this problem every single time I copied content from GPT, Notion, or Google Docs.&lt;/p&gt;

&lt;p&gt;So I dug into why this happens.&lt;/p&gt;

&lt;p&gt;Here’s the truth 👇&lt;br&gt;
LinkedIn does not support real text formatting like &lt;strong&gt;bold&lt;/strong&gt;, &lt;em&gt;italic&lt;/em&gt;, &lt;u&gt;underline&lt;/u&gt;, or &lt;del&gt;strikethrough&lt;/del&gt;.&lt;/p&gt;

&lt;p&gt;But you’ve probably noticed something interesting…&lt;/p&gt;

&lt;p&gt;Some posts do keep their formatting.&lt;br&gt;
They look cleaner.&lt;br&gt;
Easier to read.&lt;br&gt;
They stand out in the feed.&lt;/p&gt;

&lt;p&gt;That’s because they use Unicode characters, not actual formatting.&lt;/p&gt;

&lt;p&gt;So I built a small tool to fix this 👇&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🚀 LinkedIn AI Formatter&lt;/strong&gt;&lt;br&gt;
What it does (and what it does not do):&lt;/p&gt;

&lt;p&gt;✅ You paste content copied from GPT / Notion / Docs&lt;br&gt;
✅ The tool detects formatting already present&lt;br&gt;
✅ Converts only that formatting into Unicode equivalents&lt;br&gt;
✅ You copy the output&lt;br&gt;
✅ Paste into LinkedIn&lt;br&gt;
✅ Formatting stays 🎉&lt;/p&gt;

&lt;p&gt;❌ It does NOT add new formatting&lt;br&gt;
❌ It does NOT rewrite your content&lt;br&gt;
❌ It does NOT change structure or meaning&lt;/p&gt;

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

&lt;p&gt;Just preservation. Nothing more.&lt;/p&gt;

&lt;p&gt;Supported formatting:&lt;/p&gt;

&lt;p&gt;𝗕𝗼𝗹𝗱&lt;br&gt;
𝘐𝘵𝘢𝘭𝘪𝘤&lt;br&gt;
𝚄̲𝚗̲𝚍̲𝚎̲𝚛̲𝚕̲𝚒̲𝚗̲𝚎̲&lt;br&gt;
𝚂̶𝚝̶𝚛̶𝚒̶𝚔̶𝚎̶&lt;br&gt;
• Bullet points&lt;/p&gt;

&lt;p&gt;Numbered lists&lt;/p&gt;

&lt;p&gt;If writing on LinkedIn matters to you —&lt;br&gt;
readability matters.&lt;br&gt;
And formatting is a big part of that.&lt;/p&gt;

&lt;p&gt;Try it here 👇&lt;br&gt;
👉 &lt;a href="https://linkedin-ai-formatter.vercel.app" rel="noopener noreferrer"&gt;https://linkedin-ai-formatter.vercel.app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;💻 Open Source at &lt;a href="https://github.com/coderomm/linkedin-ai-formatter-app" rel="noopener noreferrer"&gt;https://github.com/coderomm/linkedin-ai-formatter-app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Would genuinely love feedback — especially from people who write regularly on LinkedIn.&lt;/p&gt;

&lt;p&gt;Building this in public 🚀&lt;br&gt;
More small improvements coming.&lt;/p&gt;

&lt;h1&gt;
  
  
  LinkedIn #Writing #CreatorTools #IndieHacker #BuildInPublic #AItools
&lt;/h1&gt;

</description>
      <category>linkedin</category>
      <category>ai</category>
      <category>contentwriting</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Hello coders 👋</title>
      <dc:creator>coder om</dc:creator>
      <pubDate>Wed, 11 Oct 2023 16:44:24 +0000</pubDate>
      <link>https://dev.to/coderom/hello-coders-58aj</link>
      <guid>https://dev.to/coderom/hello-coders-58aj</guid>
      <description>&lt;p&gt;I am very new to this platform. I’m currently working on .NET MVC . 🤝 I’m looking for help with Java development. Today is my first day and will definetly learn more about this and looking to share my little knowledge and experience related to coding.&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
