<?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: Follower Change</title>
    <description>The latest articles on DEV Community by Follower Change (@followerchange).</description>
    <link>https://dev.to/followerchange</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4200285%2Faa56de78-bc2d-4e5e-889a-0ce35c239ac9.png</url>
      <title>DEV Community: Follower Change</title>
      <link>https://dev.to/followerchange</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/followerchange"/>
    <language>en</language>
    <item>
      <title>Seven platforms, seven export formats, one rule: the files never leave the browser</title>
      <dc:creator>Follower Change</dc:creator>
      <pubDate>Thu, 22 Oct 2026 11:44:47 +0000</pubDate>
      <link>https://dev.to/followerchange/seven-platforms-seven-export-formats-one-rule-the-files-never-leave-the-browser-2646</link>
      <guid>https://dev.to/followerchange/seven-platforms-seven-export-formats-one-rule-the-files-never-leave-the-browser-2646</guid>
      <description>&lt;p&gt;When we built &lt;a href="https://followerchange.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=dev-client-side-parsing" rel="noopener noreferrer"&gt;Follower Change&lt;/a&gt;, the first architectural decision was also the constraint everything else had to survive: &lt;strong&gt;the analysis runs entirely in the user's browser.&lt;/strong&gt; A follower export is someone's whole social graph. We didn't want it on our servers, so the parser, the comparison engine, and the results all live client-side. No upload endpoint exists for follower data — there's nothing to call.&lt;/p&gt;

&lt;p&gt;That decision sounds simple until you meet the actual export formats. "Download your data" turns out to mean something different on every platform:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Instagram&lt;/strong&gt; gives you JSON, but the shape depends on which list you asked for. Followers arrive as a top-level array of entries, each carrying &lt;code&gt;string_list_data&lt;/code&gt; with a &lt;code&gt;value&lt;/code&gt; (the username) and a timestamp. Following comes wrapped in a &lt;code&gt;relationships_following&lt;/code&gt; object instead. And on larger accounts the followers list is split across shard files — &lt;code&gt;followers_1.json&lt;/code&gt;, &lt;code&gt;followers_2.json&lt;/code&gt;, and so on — which a user may upload one at a time. (An early lesson: shards from different platforms behave differently; our parser merges Facebook's shards and X's parts automatically, and we had to be explicit with users that Instagram's split files each need uploading.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;X&lt;/strong&gt; doesn't give you JSON at all. Its archive contains JavaScript files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;YTD&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;follower&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;part0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;follower&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;accountId&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;1252851&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="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Executing that in the browser would be an obvious non-starter — uploaded bytes are untrusted input. We strip the &lt;code&gt;window.YTD.&amp;lt;key&amp;gt;.part&amp;lt;N&amp;gt; =&lt;/code&gt; prefix as &lt;em&gt;text&lt;/em&gt; and &lt;code&gt;JSON.parse&lt;/code&gt; the remainder. Same rule everywhere in the pipeline: no &lt;code&gt;eval&lt;/code&gt;, no DOM injection, no executing anything a user uploads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HTML exports&lt;/strong&gt; (Instagram offers one) get parsed inertly: we harvest &lt;code&gt;instagram.com&lt;/code&gt; profile links and nothing else — no scripts run, no markup is rendered.&lt;/p&gt;

&lt;p&gt;Underneath the formats, the pipeline is deliberately boring:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;upload → validation → format detection → parser → normalize → dedupe → compare
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three details from that pipeline are worth stealing for any file-processing tool:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Never trust the MIME type or the extension.&lt;/strong&gt; Format detection goes magic bytes → content sniffing → extension, in that order. Users rename files; platforms mislabel them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Normalize usernames aggressively, but honestly.&lt;/strong&gt; &lt;code&gt;@Alice&lt;/code&gt;, &lt;code&gt;Alice&lt;/code&gt;, and a full profile URL all canonicalize to &lt;code&gt;alice&lt;/code&gt; before deduplication, or your comparison fills with phantom differences. But normalization has rules you can't invent: when we documented the spreadsheet version of this comparison, the correct formula removes &lt;code&gt;@&lt;/code&gt; &lt;em&gt;only when it's the first character&lt;/em&gt; — blanket replaces corrupt usernames that legitimately contain odd characters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cap everything.&lt;/strong&gt; ZIPs (we unzip in-browser with fflate) get a hard entry cap — ours is 200 entries — plus traversal guards, because an archive is just a polite name for a bundle of untrusted files. Oversized full-account archives are the common failure, so the error message tells users to upload the follower files directly instead.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What does the comparison itself look like once the data is clean? Two set operations, honestly labeled. Followers minus following (and vice versa) on a &lt;em&gt;current&lt;/em&gt; export tells you who doesn't follow you back. Comparing two &lt;em&gt;snapshots&lt;/em&gt; from different dates tells you which accounts were present before and absent later — and we label that "may have unfollowed" / "lost followers," never "unfollowed you," because absence from a later export doesn't prove intent, timing, or even that the account still exists. An analyzer that claims more than the data supports is lying to its users in a friendly font.&lt;/p&gt;

&lt;p&gt;The browser-only constraint costs us things a server would make easy: no background processing for huge archives, no cross-device history unless a user saves a report to their account. We think it's the right trade. The data was never ours to hold.&lt;/p&gt;

&lt;p&gt;If you want to try it: Follower Change works from the official exports of Instagram, Threads, TikTok, X, Facebook, YouTube, and LinkedIn. A free account includes 100 credits and an analysis costs 10 — the account exists for credits and saved reports; your follower files still never leave your browser.&lt;/p&gt;

&lt;p&gt;I'm happy to answer questions about any of the export formats — especially if you've fought the X archive or TikTok's JSON before. Those two have the most surprises per kilobyte.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>privacy</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
