<?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: vyixor</title>
    <description>The latest articles on DEV Community by vyixor (@vyixor).</description>
    <link>https://dev.to/vyixor</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%2F4143148%2Fed41c51c-d223-45c6-9171-3ec95b5384e4.jpeg</url>
      <title>DEV Community: vyixor</title>
      <link>https://dev.to/vyixor</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vyixor"/>
    <language>en</language>
    <item>
      <title>I built ORBIT because reading Reddit felt like a job</title>
      <dc:creator>vyixor</dc:creator>
      <pubDate>Fri, 30 Oct 2026 09:37:05 +0000</pubDate>
      <link>https://dev.to/vyixor/i-built-orbit-because-reading-reddit-felt-like-a-job-1115</link>
      <guid>https://dev.to/vyixor/i-built-orbit-because-reading-reddit-felt-like-a-job-1115</guid>
      <description>&lt;p&gt;Okay so Reddit was the problem.&lt;/p&gt;

&lt;p&gt;Every time I finished a project, launched a site, shipped something, I'd go to Reddit to see if people were talking about it. Not just reading. Replying. Finding the important posts out of two hundred, opening each one, reading through comments to see if it was worth a response, drafting something that doesn't sound like a bot. Do that for an hour and tell me how you feel.&lt;/p&gt;

&lt;p&gt;I'm the kind of engineer who hates repeating the same task twice. So I built a scraper. That's it. That's what ORBIT started as. A Reddit scraper.&lt;/p&gt;

&lt;p&gt;Every morning a notification went off. I opened it, hit scan, and it read the posts. It pulled out the ones that were actually worth my time. If I wanted to reply, it drafted something and I tweaked it to sound like a human before posting. If I didn't, I moved on.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F44axhl79r5gmb4g7cf41.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F44axhl79r5gmb4g7cf41.png" alt="Orbit bot making reddit easier" width="800" height="519"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It worked. It saved me an hour a day. That was enough at first.&lt;/p&gt;

&lt;p&gt;Then Indie Hackers happened. Then dev.to. I read posts there but a lot of times I just wanted a summary. I didn't want to read a 2000 word post to find out it was a listicle. So I added chat. Gave it the ability to read the page and summarize.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F79kd79xt4y7utwusaskw.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F79kd79xt4y7utwusaskw.png" alt="Summarize a page using orbit" width="799" height="511"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then the thing grew on me. It became personal. It was on my right side, every page, every tab. That's when I realized I wanted it to feel alive. So I built the floating bot. A small orb thing that sits on the screen, blinks, talks back. Not because it's needed. Because when something is always there, you want it to feel like it's there.&lt;/p&gt;

&lt;p&gt;Then even that wasn't enough. Chat wasn't the interaction I wanted. I wanted commands. Something I could click and let it go. That's how OBT scripting got started. A tiny language that looks like shell commands but runs the browser. It compiles to an AST and executes through a proper interpreter. I didn't plan for it to become the center of the project but here we are.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The part people ask about&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;People ask what ORBIT actually does and I usually give a bad answer, because it's not one thing.&lt;/p&gt;

&lt;p&gt;It reads pages. It summarizes. It clicks, types, fills forms, runs scripts, talks out loud, remembers what you told it. But the reason it's worth keeping open over just switching to a ChatGPT tab is that it's right there. On the page you're already on. You ask a question and you don't have to leave.&lt;/p&gt;

&lt;p&gt;Switch tabs to get an answer once and you'll do it forever. Ask a question where you already are and you get used to it in a week. That "I got you" feeling is the whole product. Everything else is just features that make the feeling stick.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What broke&lt;/strong&gt;&lt;br&gt;
Now the part everyone skips.&lt;/p&gt;

&lt;p&gt;The compiler nearly ended me.&lt;/p&gt;

&lt;p&gt;I had Claude in the passenger seat and I still almost choked. String parsers, try blocks, run blocks, and mostly error handling. You'd think error handling is the easy part. It is not. It is the part that makes or breaks the language. Every command can fail in nine different ways and you have to decide which failures the user can catch and which ones they can't. Which ones are bugs and which ones are the user's fault. Which ones should stop the script and which ones should just log and move on.&lt;/p&gt;

&lt;p&gt;I rebuilt the error system three times. The current version has typed error codes, run blocks with onThrow and onFinish, a throw statement, a registry that third party code can extend. It's the thing I'm proudest of now. It's also the thing that had me staring at a screen at 2am wondering why a caught timeout wasn't stopping the loop it was supposed to stop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Then Google happened.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Chrome extensions are a minefield. The mic was in for a while. I had a voice input thing working. It crashed ORBIT every few hours. Not sometimes. Every few hours. I tried passing it from the real page into the extension, tried moving everything into the background worker, tried about four different workarounds. Every fix cost more than it gave. I stripped the mic out. It's gone. I don't miss it but I'd be lying if I said that didn't sting.&lt;/p&gt;

&lt;p&gt;JS mode is still a mess. Google strictly blocks raw JS execution from extensions on a lot of pages and I haven't found a clean way around it yet. Some pages work. Some don't. It's the one feature I ship with an asterisk. I'm still working on it. That's the honest version.&lt;/p&gt;

&lt;p&gt;The extension sandbox itself is a whole other problem. Anything you want to run needs to either happen in the side panel, in the content script inside the page, or in a background worker, and none of them can talk to each other the way you'd expect. I've written more message passing code than actual features at this point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's actually in it&lt;/strong&gt;&lt;br&gt;
ORBIT is a Chrome extension that lives in your side panel. It can chat, read pages, summarize, automate, run scripts, and there's a small floating bot that talks to you.&lt;/p&gt;

&lt;p&gt;Under that there's a scripting language called OBT. It looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"use obt";

navigate "https://example.com";
wait 2;
click "Sign In";
focus --type selector "input[name='email']";
type "user@example.com";
press Enter;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;var title = $run(get --type text --target selector "h1");
say "Page title: {$title}";
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can wrap commands in run blocks with onThrow and onFinish, throw your own typed errors, catch them by number or by name. There's $exec for JS logic in a sandboxed iframe, $run for capturing return values, ask for calling the AI from inside a script, dialog commands for alert, confirm, prompt. It does more than I planned for and it still surprises me when it works.&lt;/p&gt;

&lt;p&gt;The whole thing took three months and two weeks.&lt;/p&gt;

&lt;p&gt;That's not fast. I know people who would have shipped this in six weeks and shipped it worse. I also know people who would have shipped it in six weeks and shipped it better. I'm not sure which group I'm in. Somewhere in the middle, most days.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where it's at&lt;/strong&gt;&lt;br&gt;
Nothing dramatic. I want people to try it and tell me what's broken. I want the JS mode problem solved eventually. I want the Reddit thing to keep working because it's the only reason any of this exists and I still use it every morning before I open my editor.&lt;/p&gt;

&lt;p&gt;If you want to look at it: &lt;a href="https://github.com/vyixor/orbit" rel="noopener noreferrer"&gt;GITHUB ORBIT&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'm pushing a bug fix in the next two or three days and then calling it a stable release. Three months feels like a long time for one person to build a compiler, a browser agent, a scripting engine, a floating bot, and a Chrome extension at the same time. It also feels like I just started yesterday.&lt;/p&gt;

&lt;p&gt;Reddit was the cause of ORBIT. Everything else came after.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fztbubmi845apsgr2llfv.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fztbubmi845apsgr2llfv.jpg" alt="orbit and reddit" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>javascript</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I added image placement to a browser-based PDF editor. Here's what broke.</title>
      <dc:creator>vyixor</dc:creator>
      <pubDate>Tue, 20 Oct 2026 12:31:48 +0000</pubDate>
      <link>https://dev.to/vyixor/i-added-image-placement-to-a-browser-based-pdf-editor-heres-what-broke-mk5</link>
      <guid>https://dev.to/vyixor/i-added-image-placement-to-a-browser-based-pdf-editor-heres-what-broke-mk5</guid>
      <description>&lt;p&gt;I've been building a browser-based PDF editor on and off for a few months. Everything runs client-side, nothing gets uploaded. The tools include merge, split, rotate, and a reader mode where you can place movable, resizable elements on top of a page.&lt;/p&gt;

&lt;p&gt;Text, watermarks, and signatures were already working. This week I added images. It should have been an afternoon. It took two days. Here's what got in the way.&lt;/p&gt;

&lt;p&gt;The architecture that made it easy&lt;br&gt;
The key thing is that the tool doesn't bake anything into the PDF until the user clicks "Apply." Elements live in a JavaScript array, get rendered as absolutely positioned DOM nodes over a canvas, and only get embedded into the actual PDF bytes when the user is done arranging.&lt;/p&gt;

&lt;p&gt;Each element looks roughly like this:&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="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;pageIdx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;24.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// percentage of page width&lt;/span&gt;
  &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;61.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// percentage of page height&lt;/span&gt;
  &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;// percentage of page width&lt;/span&gt;
  &lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;12.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// percentage of page height&lt;/span&gt;
  &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;dataURL&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:image/png;base64,...&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;Percentages instead of pixels. That's the thing that makes everything else work. The same element array renders correctly whether the page is displayed at 50% zoom or 300%, and when it's finally embedded into the PDF, the percentages translate directly to PDF coordinates without any adjustment.&lt;/p&gt;

&lt;p&gt;The DOM overlay does the interaction: pointer events for drag, touch events for mobile, handles for resize. It's the same pattern the signature tool already used. All I had to do was teach the array about one more type.&lt;/p&gt;

&lt;p&gt;What broke first: pdf-lib only embeds PNG and JPEG&lt;br&gt;
pdf-lib has embedPng() and embedJpg(). That's it. No WebP, no AVIF, no GIF.&lt;/p&gt;

&lt;p&gt;Users don't know that. If you give them a file picker that accepts image/*, they'll hand you a WebP every second try. So the picker has to lie less than reality.&lt;/p&gt;

&lt;p&gt;The fix is a canvas re-encode step before the image ever reaches pdf-lib:&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;loadImageForPlacement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;)&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;img&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Image&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;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createObjectURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&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;URL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;revokeObjectURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="c1"&gt;// Downscale if the source is huge&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;maxDim&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2000&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;scale&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;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;maxDim&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="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;naturalWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;naturalHeight&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;w&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;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;naturalWidth&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;scale&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;h&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;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;naturalHeight&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;scale&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;c&lt;/span&gt; &lt;span class="o"&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;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;canvas&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;c&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="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;h&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;cx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2d&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;cx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;imageSmoothingEnabled&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;cx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;imageSmoothingQuality&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;high&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;cx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drawImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;dataURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toDataURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image/png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="na"&gt;aspect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;naturalWidth&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;naturalHeight&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onerror&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&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;URL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;revokeObjectURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nf"&gt;reject&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;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;decode failed&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="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;url&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;Two things are happening here. First, the image is decoded by the browser (which knows every format the user's OS knows) and re-encoded as PNG. Second, anything over 2000 pixels on the longest side gets downscaled before it's stored.&lt;/p&gt;

&lt;p&gt;The downscale matters more than I expected. Someone dropping a 12 megapixel phone photo into an invoice should not triple the PDF's file size. Cap it at 2000 and the user cannot tell the difference on a letter page, but the resulting PDF stays small.&lt;/p&gt;

&lt;p&gt;What broke second: the coordinate system&lt;br&gt;
PDF coordinates have their origin at the bottom-left. Browser coordinates have their origin at the top-left. Every element position needs to be flipped on the Y axis before it's drawn into the PDF.&lt;/p&gt;

&lt;p&gt;My overlay stores Y as a percentage from the top, because that's how the DOM works. When embedding, the Y needs to become a distance from the bottom:&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSize&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;xPt&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;width&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;yRaw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// flip&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;wPt&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;width&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;hPt&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;pg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drawImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;embeddedPng&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;xPt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;y&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;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;yRaw&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;hPt&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;// drawImage expects the bottom-left of the image&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;wPt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;hPt&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The extra yRaw - hPt is the second thing to get wrong. drawImage in pdf-lib takes the bottom-left corner of the image, not the top-left. So the flipped Y needs the element's own height subtracted from it.&lt;/p&gt;

&lt;p&gt;I got this wrong the first time and placed every image one image-height too high. It looked correct on small elements and hilariously wrong on big ones.&lt;/p&gt;

&lt;p&gt;What broke third: aspect ratio during placement&lt;br&gt;
The scale slider in the sidebar controls the element width. Height needs to derive from the source image's aspect ratio.&lt;/p&gt;

&lt;p&gt;But the overlay works in percentages of page width and page height, which are not the same number. A 30% width and a 30% height on an A4 page represent different pixel dimensions.&lt;/p&gt;

&lt;p&gt;The math that ends up correct:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ov&lt;/span&gt; &lt;span class="o"&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;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`.page-overlay[data-pi="&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;pageIdx&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;pw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="nx"&gt;ov&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pw&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;ph&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="nx"&gt;ov&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ph&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;pageAspect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pw&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;ph&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;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;TS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// e.g. 30&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;pageAspect&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;imageAspect&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the user picks a wide landscape image and the page is portrait, the height comes out small. If they pick a tall portrait image on a landscape page, the height comes out large. That's what you want.&lt;/p&gt;

&lt;p&gt;If you forget the pageAspect term and just do h = w / imageAspect, everything looks fine on square pages and progressively wrong as the page gets wider or narrower.&lt;/p&gt;

&lt;p&gt;What I'd do differently&lt;br&gt;
Cache the embedded PNG. Right now every Apply cycle re-decodes every dataURL and re-embeds every image. For a single-document session that's fine, but if a user places the same logo on twenty pages, it's embedded twenty times. pdf-lib doesn't deduplicate.&lt;/p&gt;

&lt;p&gt;The fix would be a small cache keyed by dataURL, but for the current use case it isn't worth the code yet.&lt;/p&gt;

&lt;p&gt;Handle transparency more explicitly. PDF viewers render alpha in PNG correctly. But if the user exports to JPEG later, the transparency turns black. I could warn on JPEG export. I haven't.&lt;/p&gt;

&lt;p&gt;Test with the weird stuff. A 4:1 panoramic image, a 1:4 vertical banner, a 1×1 pixel transparent PNG. All of them work but each one was a small surprise while I was building.&lt;/p&gt;

&lt;p&gt;The point&lt;br&gt;
The pattern that made this feature possible is separating "the thing the user sees" from "the thing the PDF contains." Once the overlay is a data model, adding a new type of element is additive. Text, watermark, signature, and now image are all the same architecture with different rendering.&lt;/p&gt;

&lt;p&gt;That's the part I got right early. It's also the part that made the two days of debugging tolerable.&lt;/p&gt;

&lt;p&gt;The editor is part of SORO, a set of browser-based file tools I've been building. It's at &lt;a href="https://soroflix.xyz" rel="noopener noreferrer"&gt;SORO File Tools&lt;/a&gt; if you want to look.&lt;/p&gt;

&lt;p&gt;If you've got a similar overlay system in a browser tool and you're adding a new element type, the two things that will cost you hours are PDF's bottom-left origin and pdf-lib's PNG/JPEG-only embed API. Everything else is repainting what you already built.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>showdev</category>
      <category>pdf</category>
    </item>
    <item>
      <title>Two weeks of trying to teach Google my site isn't a movie streaming platform</title>
      <dc:creator>vyixor</dc:creator>
      <pubDate>Tue, 06 Oct 2026 13:53:09 +0000</pubDate>
      <link>https://dev.to/vyixor/two-weeks-of-trying-to-teach-google-my-site-isnt-a-movie-streaming-platform-1l6n</link>
      <guid>https://dev.to/vyixor/two-weeks-of-trying-to-teach-google-my-site-isnt-a-movie-streaming-platform-1l6n</guid>
      <description>&lt;p&gt;The domain is soroflix.xyz.&lt;/p&gt;

&lt;p&gt;I bought it about six months ago for a movie rating side project. Something like a personal TMDB with watch links and ratings. Built a bit of it, lost interest, moved on.&lt;/p&gt;

&lt;p&gt;Then I started working on something completely different. A set of browser based file tools. PDF editor, EXIF remover, image editor, that kind of thing. Everything runs locally in the browser, nothing gets uploaded. When the tools were ready to ship, the movie idea was dead. So I renamed the project SORO File Tools and pointed it at the domain I already owned.&lt;/p&gt;

&lt;p&gt;Which is how I ended up with a file tools site on a domain called soroflix.&lt;/p&gt;

&lt;p&gt;Three weeks after launch, I opened Search Console for the first time. Around 22 impressions, 4 clicks. Fine for a brand new site. Then I looked at the Queries tab.&lt;/p&gt;

&lt;p&gt;One query. "soro films."&lt;/p&gt;

&lt;p&gt;Every impression. Every country. Every day. People searching for movie streaming, landing on a page about stripping GPS coordinates from photographs.&lt;/p&gt;

&lt;p&gt;Google read "flix" and stopped there.&lt;/p&gt;

&lt;p&gt;What I did about it&lt;br&gt;
I spent the next two weeks trying to override the domain signal with content signals. Here is the actual list, in the order I tried things.&lt;/p&gt;

&lt;p&gt;Rewrote every title&lt;br&gt;
Before:&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;SORO&lt;/span&gt; &lt;span class="err"&gt;—&lt;/span&gt; &lt;span class="nx"&gt;Free&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt; &lt;span class="nx"&gt;tools&lt;/span&gt; &lt;span class="nx"&gt;that&lt;/span&gt; &lt;span class="nx"&gt;never&lt;/span&gt; &lt;span class="nx"&gt;see&lt;/span&gt; &lt;span class="nx"&gt;your&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/title&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After:&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;title&amp;gt;&lt;/span&gt;SORO File Tools — Free browser-based PDF, image, text utilities&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The hypothesis was simple. Google builds an entity from repeated mentions of a name across pages. If every title says the same two word brand, the classifier has more material to work with. Ten pages, ten titles, all leading with the exact string.&lt;/p&gt;

&lt;p&gt;Rewrote every meta description to include the full brand&lt;br&gt;
Before:&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;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"description"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"EXIF remover, PDF editor, image editor..."&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After:&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;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"description"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"SORO File Tools is a free suite of ten browser-based utilities..."&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same idea. Every meta description now contains the exact phrase. Combined with the titles, that is 20 mentions of the full brand name across the site before Google even parses the body content.&lt;/p&gt;

&lt;p&gt;Changed the nav and footer logos&lt;br&gt;
Every page used to say:&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;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"nav-logo"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;SORO&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now it says:&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;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"nav-logo"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;SORO File Tools&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That sounds cosmetic. It is not. The nav logo appears on every single page, in the same position, at the top of the DOM. It is the single most repeated string on the entire site. Making it say the full brand name means every page reinforces the same entity ten times per page load.&lt;/p&gt;

&lt;p&gt;Added alternateName to the WebSite schema&lt;br&gt;
The original schema was minimal:&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;"@context"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://schema.org"&lt;/span&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;"WebSite"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SORO"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://soroflix.xyz"&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;Rewritten to include every variant:&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;"@context"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://schema.org"&lt;/span&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;"WebSite"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SORO File Tools"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"alternateName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"SORO"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Soroflix"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SORO File Utilities"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SORO Tools"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://soroflix.xyz/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"publisher"&lt;/span&gt;&lt;span class="p"&gt;:&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;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;"Organization"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SORO File Tools"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://soroflix.xyz/"&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;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 alternateName array tells Google that all four names point to the same entity. This is the piece I am most confident in, because it directly addresses the classifier problem. Google's entity model already knows what Soroflix is. The alternateName block is how you tell it what Soroflix actually means.&lt;/p&gt;

&lt;p&gt;Added an ItemList schema to the homepage&lt;br&gt;
This one is direct. It tells Google exactly what the site contains:&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;"@context"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://schema.org"&lt;/span&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;"ItemList"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SORO File Tools Catalog"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"numberOfItems"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"itemListElement"&lt;/span&gt;&lt;span class="p"&gt;:&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;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;"ListItem"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"position"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"EXIF Remover"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&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;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;"ListItem"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"position"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"EXIF Viewer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&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;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;more&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;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;Each tool gets its own ListItem with a name and a URL. Google can now read the site as "a catalog of file utilities" from the schema alone, without needing to parse the HTML.&lt;/p&gt;

&lt;p&gt;Added isPartOf to every tool page schema&lt;br&gt;
Each tool already had a WebApplication schema. I added a back reference:&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;"WebApplication"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PDF Editor"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://soroflix.xyz/pdf-editor/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"isPartOf"&lt;/span&gt;&lt;span class="p"&gt;:&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;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;"WebSite"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SORO File Tools"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://soroflix.xyz/"&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;span class="nl"&gt;"publisher"&lt;/span&gt;&lt;span class="p"&gt;:&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;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;"Organization"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SORO File Tools"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://soroflix.xyz/"&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;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;Now every tool page explicitly tells Google "I am part of SORO File Tools." Ten pages, ten back references. That is a lot of signal all pointing at the same parent entity.&lt;/p&gt;

&lt;p&gt;Added a definition paragraph to the homepage&lt;br&gt;
The single most direct thing on the site:&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;p&amp;gt;&lt;/span&gt;
  SORO File Tools is a free collection of browser-based utilities 
  for PDFs, images, text files, and links. Every tool runs locally 
  on your device with no uploads and no accounts.
&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Plain text, right under the hero. Not hidden, not in the footer. Google reads page content, and this is the sentence that tells it, in human language, what the site is.&lt;/p&gt;

&lt;p&gt;What actually happened&lt;br&gt;
Two weeks later, Search Console still shows "soro films" as the only query.&lt;/p&gt;

&lt;p&gt;Impressions have ticked up slightly. Around 40 now. Still zero organic clicks from outside Benin, where I live and where my friends are. The title changes have not shown up in the SERP yet either, which the Search Console documentation says can take one to three weeks for a fresh crawl.&lt;/p&gt;

&lt;p&gt;Some things are moving. Ranked number two for "soro" on SaaSHub. AlternativeTo approved the listing. A few Dev.to posts are pulling visitors. But Google, which is the one that matters, has not updated its classification.&lt;/p&gt;

&lt;p&gt;The honest answer is I do not know if this is going to work. The theory is sound. The execution is thorough. But Google's entity model is a black box and I have no way to know if I am on a two week timeline or a six month one.&lt;/p&gt;

&lt;p&gt;What I learned about the entity model&lt;br&gt;
A few things worth knowing if you ever end up in this situation.&lt;/p&gt;

&lt;p&gt;Domain names carry more weight than I expected. The classifier runs before the content parser. If the domain has a strong enough prior, the content never gets a chance to override it in the initial pass.&lt;/p&gt;

&lt;p&gt;Schema is a signal, not a command. Adding alternateName does not force Google to update its entity model. It gives Google more material to work with the next time it re-evaluates the entity, which happens on Google's schedule, not yours.&lt;/p&gt;

&lt;p&gt;Every repeated string helps. Title, meta description, nav logo, footer, schema name fields, body copy. The classifier is looking for a consistent pattern. The more places the same phrase appears, the stronger the signal.&lt;/p&gt;

&lt;p&gt;You can't test this locally. Unlike most web development, there is no way to see what Google sees. You submit, you wait, you check. Each cycle is days or weeks. It is the slowest feedback loop I have ever worked in.&lt;/p&gt;

&lt;p&gt;The thing that is still broken&lt;br&gt;
The sitemap has never been successfully fetched by Google. sitemap.xml returns a 200 in the browser, renders as valid XML, passes every validator I have tried. Search Console says "could not fetch" every single time.&lt;/p&gt;

&lt;p&gt;It is hosted on Cloudflare Pages. I have tried removing it and resubmitting. I have tried submitting the full URL instead of the path. I have checked the content-type header, which is correct. I have disabled Bot Fight Mode in Cloudflare, which was the most common fix suggested.&lt;/p&gt;

&lt;p&gt;Still could not fetch. If anyone has dealt with this specific problem on Cloudflare Pages, I would genuinely like to know what fixed it.&lt;/p&gt;

&lt;p&gt;The open question&lt;br&gt;
Has anyone else tried to fix a Google entity misclassification where the domain name was the primary signal. How long did it take for the entity to update. Did it stick, or did the classifier drift back to the domain signal after a while.&lt;/p&gt;

&lt;p&gt;I am not looking for "buy a new domain." I know. I own this one and I want to make it work.&lt;/p&gt;

&lt;p&gt;I am looking for the person who has actually walked through this and can tell me what the timeline looks like. Or the person who tried and failed, so I know when to give up and start over.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>google</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>PDF is a terrible file format and every alternative is worse</title>
      <dc:creator>vyixor</dc:creator>
      <pubDate>Thu, 01 Oct 2026 15:05:54 +0000</pubDate>
      <link>https://dev.to/vyixor/pdf-is-a-terrible-file-format-and-every-alternative-is-worse-16ke</link>
      <guid>https://dev.to/vyixor/pdf-is-a-terrible-file-format-and-every-alternative-is-worse-16ke</guid>
      <description>&lt;p&gt;I've spent the last two months building a PDF editor that runs in the browser. Merge, split, rotate, sign, watermark, add text. The kind of thing people do every day without thinking about what's underneath.&lt;/p&gt;

&lt;p&gt;Six weeks in, I understood something about PDFs that I wish I'd known before I started. The format isn't a document format. It's a page description format. And the difference matters for everything you try to do with it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What a PDF actually is&lt;/strong&gt;&lt;br&gt;
A PDF isn't a document with paragraphs and headings and a reading order. It's a list of instructions for a printer.&lt;/p&gt;

&lt;p&gt;When you open a PDF, what you're looking at is a set of drawing commands. "Move to this X, Y coordinate. Set the font to Helvetica. Draw the glyph at index 47. Advance 12 units. Draw the glyph at index 82." It's a script. The file tells a rendering engine how to paint pixels on a page, in the order they should be painted, at exact coordinates.&lt;/p&gt;

&lt;p&gt;That's it. Everything else we think a document has, we're inferring.&lt;/p&gt;

&lt;p&gt;There is no concept of a paragraph in a PDF. There is no concept of a sentence, a word, a heading, a line break, or a link between text and what it represents. Text isn't stored as text in the traditional sense. It's stored as positioned glyphs with a font reference. If you wanted to extract the sentence "hello world" from a PDF, you don't read it from a string. You find every glyph, get its position, sort by position, group by proximity, and hope the author didn't do anything weird with kerning or line spacing.&lt;/p&gt;

&lt;p&gt;This is why PDF text extraction is so unreliable. This is why copying from a PDF sometimes gives you letters in the wrong order. This is why two-column layouts break every parser. The information is technically there, but not in the form you want.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The heritage problem&lt;/strong&gt;&lt;br&gt;
PDF comes from PostScript, which comes from the 1980s, which comes from a world where the goal was to describe how a printer should physically lay ink on paper. The whole format is built around that purpose. Long before "documents" and "content" and "accessibility" were things anyone cared about.&lt;/p&gt;

&lt;p&gt;PostScript was designed in 1982 at Adobe, mostly to solve one problem: how do you tell a printer to render a page precisely, with the same output every time, regardless of the hardware? The answer was a stack-based programming language that draws things. PDF is that language, plus a container format and some compression.&lt;/p&gt;

&lt;p&gt;Every quirk in PDF comes from this inheritance. Text isn't text, it's glyph positioning. Fonts aren't references, they're embedded subsets. Images aren't images, they're streams of pixels with specific encoding. Layout isn't layout, it's coordinates.&lt;/p&gt;

&lt;p&gt;And here's the thing. It works. It works incredibly well. Twenty-five years later, PDF is the only format that reliably renders the same on every device, every printer, every screen. That's exactly what it was built for, and it still does that job better than anything else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What this means when you try to edit&lt;/strong&gt;&lt;br&gt;
You cannot "edit" a PDF in any real sense. What you can do is draw new things on top of the existing drawing instructions.&lt;/p&gt;

&lt;p&gt;When a tool says it can edit the text in a PDF, it's lying in a specific way. It's locating the glyphs of the text you want to change, removing those glyphs from the instruction list, and drawing new glyphs in the same coordinates. The result looks correct. The underlying file has no idea what happened. If someone opens it in a different renderer with different font handling, the new text may sit slightly off from where the old text was.&lt;/p&gt;

&lt;p&gt;For anything else in the file, it's the same story. Move an image and you're really copying the image's drawing command to a new position. Delete a page and you're removing that page's instructions from the tree. Rotate a page and you're changing the viewport orientation.&lt;/p&gt;

&lt;p&gt;There is no semantic model. There is no "this is a heading" or "this is a signature block." Everything is coordinates. Every edit is a manipulation of the instruction stream.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Once you understand this, three things become obvious:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Text reflow is impossible. When you delete text from a Word document, the rest of the paragraph moves to fill the gap. When you delete text from a PDF, you create a hole. The other text doesn't know it exists. It sits at its coordinates, exactly where it was drawn.&lt;/p&gt;

&lt;p&gt;Editing breaks layout in subtle ways. Change a font size and the text may overlap what's next to it. The PDF doesn't know to reposition anything. It just draws what you told it to draw.&lt;/p&gt;

&lt;p&gt;Text search is a heuristic. When you search a PDF, the renderer is guessing where words start and end based on proximity and spacing. It's right most of the time. When it's wrong, you get the mess we've all seen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The alternatives&lt;/strong&gt;&lt;br&gt;
Here's where it gets frustrating. Every alternative to PDF has a fatal flaw that's worse than PDF's flaws.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DOCX.&lt;/strong&gt; The obvious one. Real document structure, real text, real editability. But: proprietary format owned by Microsoft, changes behavior between Word versions, renders differently in LibreOffice, and requires a specific application to open. People send PDFs because PDFs look the same everywhere. DOCX files look slightly different on every machine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ODT.&lt;/strong&gt; The open-source answer. Same structural benefits, same open standard problem: it's a standard that almost nobody uses outside of LibreOffice. Compatibility with Word is imperfect. Nobody wants to receive a .odt file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HTML.&lt;/strong&gt; Can carry real structure, real text, real semantics. But: it doesn't print correctly, it doesn't paginate, it renders differently in every browser, and you can't email someone an HTML file and expect them to open it as a document. HTML is a delivery mechanism, not a document format.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PostScript.&lt;/strong&gt; The ancestor of PDF. Same underlying model, worse container format, more verbose, less common. Strictly worse than PDF for everything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Markdown and LaTeX.&lt;/strong&gt; Both produce documents from structured source. Markdown is too limited for anything with real layout. LaTeX is powerful but the barrier to entry is enormous and the output is still PDF at the end.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plain text.&lt;/strong&gt; No layout, no fonts, no images. Perfect for programs, useless for contracts.&lt;/p&gt;

&lt;p&gt;Every single one of these fails at the specific thing PDF succeeds at: rendering identically everywhere. That's the whole reason PDF won in the first place.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why PDF won anyway&lt;/strong&gt;&lt;br&gt;
Because the actual problem people needed solved wasn't editability. It was "I need this document to look the same on your screen as it does on mine, and if you print it, it should print the same way."&lt;/p&gt;

&lt;p&gt;Contracts, invoices, tickets, tax forms, boarding passes, university transcripts, medical records. None of these need to be editable by the recipient. They need to be final. They need to render correctly on any device, print identically on any printer, and preserve their visual appearance for years.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PDF does that.&lt;/strong&gt; It does it better than any other format that has ever existed. That's why every alternative fails. They all solve a different problem and hope you didn't notice.&lt;/p&gt;

&lt;p&gt;The tradeoff is that a PDF isn't really a document. It's a picture of a document, with some text embedded in the pixels for accessibility. When you accept that, everything else makes sense.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I do about it&lt;/strong&gt;&lt;br&gt;
I built the PDF editor to work with the format instead of against it. The tools do things PDFs are good at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Merge (combine instruction streams)&lt;/li&gt;
&lt;li&gt;Split (divide instruction streams)&lt;/li&gt;
&lt;li&gt;Rotate (change viewport)&lt;/li&gt;
&lt;li&gt;Reorder (rearrange page tree)&lt;/li&gt;
&lt;li&gt;Delete pages (remove from tree)&lt;/li&gt;
&lt;li&gt;Compress (recompress object streams)&lt;/li&gt;
&lt;li&gt;Watermark (draw over the existing content)&lt;/li&gt;
&lt;li&gt;Sign (draw an image at specified coordinates)&lt;/li&gt;
&lt;li&gt;Add text (draw new text at specified coordinates)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these are "editing the document." They're all operating on the file as what it actually is: a container of instructions.&lt;/p&gt;

&lt;p&gt;That's the honest version of what a PDF editor does. It doesn't edit the document because there is no document. It edits the instruction stream. The result looks correct because the instructions produce the right output. But if you look under the hood, it's just drawings on top of drawings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The open question&lt;/strong&gt;&lt;br&gt;
Is there a format that could replace PDF if someone actually tried?&lt;/p&gt;

&lt;p&gt;Something with the universality of PDF plus real document structure. Fixed layout for print, live text for editing, standard fonts, deterministic rendering.&lt;/p&gt;

&lt;p&gt;I don't think it exists. Every attempt I've seen either sacrifices the deterministic rendering for editability (Word) or sacrifices editability for deterministic rendering (PDF). Nobody's built the one that does both, and I'm not sure anyone can.&lt;/p&gt;

&lt;p&gt;But I'd like to be wrong. If you've seen a format that threads the needle, tell me what it is.&lt;/p&gt;

&lt;p&gt;check out my pdf editor here. &lt;a href="https://soroflix.xyz/pdf-editor/" rel="noopener noreferrer"&gt;[SORO pdf editor]&lt;/a&gt;&lt;/p&gt;

</description>
      <category>discuss</category>
      <category>webdev</category>
      <category>pdf</category>
      <category>programming</category>
    </item>
    <item>
      <title>the senior developer who can't get hired</title>
      <dc:creator>vyixor</dc:creator>
      <pubDate>Thu, 01 Oct 2026 12:13:24 +0000</pubDate>
      <link>https://dev.to/vyixor/the-senior-developer-who-cant-get-hired-83i</link>
      <guid>https://dev.to/vyixor/the-senior-developer-who-cant-get-hired-83i</guid>
      <description>&lt;p&gt;I had lunch with a friend last week who has been writing software professionally for eleven years. Staff-level at his last two companies. Has shipped systems that handle millions of requests a day. Can read an unfamiliar codebase and know where the bodies are buried within an afternoon.&lt;/p&gt;

&lt;p&gt;He's been out of work for five months.&lt;/p&gt;

&lt;p&gt;Not because he got laid off in a mass event. He wasn't. His company didn't do anything dramatic. He left on his own, in March, thinking he'd take six weeks off and then pick something up. He's had four onsites. All rejected.&lt;/p&gt;

&lt;p&gt;I asked him what happened in the interviews. He said the same thing each time in different words: the conversations didn't go where he expected them to.&lt;/p&gt;

&lt;p&gt;He's spent years learning how to explain complex systems. How to reason about tradeoffs. How to look at a piece of infrastructure and say "this will fall over in six months when traffic triples, here's why, here's what to change." That used to be the whole interview. You'd get asked to design something, you'd talk through the options, you'd land somewhere reasonable, done.&lt;/p&gt;

&lt;p&gt;Now the first round is a screen with an AI copilot enabled. The question is some variation of "build this feature." The candidate who finishes fastest wins. The candidate who uses the tool fluently wins. My friend, who spent a decade earning the right to write code slowly and carefully, was slower than a mid-level developer who'd never worked on anything serious but knew every keyboard shortcut in Cursor.&lt;/p&gt;

&lt;p&gt;He said it plainly: I've been doing this for eleven years and I couldn't keep up.&lt;/p&gt;

&lt;p&gt;I've been thinking about that sentence all week.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What actually changed&lt;/strong&gt;&lt;br&gt;
The easy answer is that AI tools got good. Everyone knows that. The harder question is which skills got devalued, specifically, because that's what nobody wants to say out loud.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ten years ago the value of a senior developer was concentrated in three things:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deep debugging&lt;/strong&gt;. The ability to look at a production incident and reason from symptoms to cause without needing to read code. This is a skill that takes years to build and it's the reason companies pay seniors a premium.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Systems thinking&lt;/strong&gt;. The ability to look at a product and know how it should be structured, where the boundaries should be, which decisions are reversible and which are one-way doors. This is the thing that separates someone who can build a feature from someone who can build a product.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pattern recall&lt;/strong&gt;. Knowing the language, the framework, the libraries, the idioms. Being able to write a Redux reducer from memory, knowing the correct way to structure a reducer or a saga. The long tail of syntax and convention you accumulate.&lt;/p&gt;

&lt;p&gt;The first two skills are still valuable. Deep debugging and systems thinking are genuinely better when a person has been through ten years of production incidents. AI doesn't replace the judgment that comes from having been on call for a payment system at 3am.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The third one is gone.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not gone gone. But heavily devalued. Pattern recall was never the interesting part of being a senior, but it was the part that made you fast, and speed is what the current market rewards. A junior with good AI tooling can now scaffold a feature in the time a senior used to spend opening files. The senior still understands the feature better. But the feature exists in both timelines, and the one that exists faster is the one that gets shipped.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why this is uncomfortable to talk about&lt;/strong&gt;&lt;br&gt;
Because the older developers don't want to admit that a skill they spent a decade on is now worth less than it was. And because everyone who points this out gets accused of being doom-merchant. And because the people doing the hiring have convinced themselves that "using AI well" is a skill, and it is, but it's a different skill than the one they used to hire for.&lt;/p&gt;

&lt;p&gt;There's a version of this where the whole thing is fine. The senior adapts, learns the new tools, and leverages their judgment to get twice the output of a junior. That's the version everyone tells in public. And for a lot of seniors, that's what's happening.&lt;/p&gt;

&lt;p&gt;But there's another version where the senior gets caught in a trap. Too senior to take a junior role. Too slow to compete for the senior role. Too expensive to justify against two mid-level developers using AI. That version exists too and nobody talks about it.&lt;/p&gt;

&lt;p&gt;I've heard from three people in the last month who are in some version of that trap. All of them are between eight and fifteen years into their careers. None of them are bad developers. All of them are having a harder time getting hired than they expected to.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What the market actually wants now&lt;/strong&gt;&lt;br&gt;
I've been trying to figure this out honestly and I don't have a clean answer. But here's what I've observed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Companies are hiring for three things right now, in this order:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Speed to ship&lt;/strong&gt;. Someone who can take a feature description and turn it into working code. Fast. The fewer meetings, the better. The AI-assisted developer who finishes the ticket in an afternoon is worth more to the business than the careful senior who finishes it in three days and does it more correctly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Breadth&lt;/strong&gt;. A developer who can move across the stack because the AI can help them in whatever language they're working in that day. Language expertise is less important than the ability to pick up a new one in an afternoon with Claude as a co-pilot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Judgment&lt;/strong&gt;. Knowing what to build and what not to build. This is still the senior's edge, but it's the last thing interviewed for. Nobody asks the "should we build this at all" question until you're four rounds deep, and by then you've already been filtered out for not finishing the coding screen fast enough.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The order matters&lt;/strong&gt;. &lt;br&gt;
Speed first, breadth second, judgment third. It used to be the opposite. The senior developer's edge was in the third category, and everything about their career was optimized to prove it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The honest part&lt;/strong&gt;&lt;br&gt;
Some of this is adaptation anxiety and some of it is real. Let me try to separate them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Adaptation anxiety:&lt;/strong&gt; The market is going through a shift and the people most affected by it are the ones who built their careers around the old shape of the work. Every generation goes through this. Cobol programmers went through it. Front-end developers went through it when jQuery died. This is what it feels like from inside a transition, and it's uncomfortable but it isn't new.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Real:&lt;/strong&gt; The specific thing that made a mid-career developer valuable in the past. deep framework knowledge, pattern recall, the ability to write fluent code in your head is genuinely worth less today than it was two years ago. Not worthless. Worth less. And the loss of value is permanent, not cyclical. This isn't a bad market that will recover. This is the market finding a new equilibrium.&lt;/p&gt;

&lt;p&gt;Both of those things are true and I think we need to hold both.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The thing I don't want to say&lt;/strong&gt;&lt;br&gt;
The developers who are struggling the most right now are the ones who've been coding the longest without needing to adapt. Not because they were lazy. Because they didn't have to. The market rewarded them for what they already knew, for ten or fifteen years, and now it doesn't reward the same thing anymore.&lt;/p&gt;

&lt;p&gt;I know how that sounds. I know it reads as "the seniors were coasting." That's not what I mean. Deep expertise in a specific stack is a real thing, and it took years to build, and the fact that the market doesn't value it as much today doesn't mean it was wasted. It means the market changed.&lt;/p&gt;

&lt;p&gt;The senior developer's job right now is the same as everyone else's: figure out what's still valuable and lean into it. Systems thinking, judgment, the ability to look at a design and know whether it's going to work. Those things haven't lost value. They might have actually gained value, in a world where anyone can produce code and few people can tell which code is worth keeping.&lt;/p&gt;

&lt;p&gt;But the interview doesn't test for any of that yet. That's the trap. The skills that matter most are the last ones the interview process asks about.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I'd want to ask&lt;/strong&gt;&lt;br&gt;
If you're a senior developer who's recently been through a hiring process, what was the actual gap? Was it that the interview tested the wrong things, or was it that you didn't have the new skills the interview was testing for?&lt;/p&gt;

</description>
      <category>discuss</category>
      <category>ai</category>
      <category>career</category>
      <category>webdev</category>
    </item>
    <item>
      <title>even legit coders are being forced to vibe code</title>
      <dc:creator>vyixor</dc:creator>
      <pubDate>Tue, 29 Sep 2026 10:07:05 +0000</pubDate>
      <link>https://dev.to/vyixor/even-legit-coders-are-being-forced-to-vibe-code-3cc7</link>
      <guid>https://dev.to/vyixor/even-legit-coders-are-being-forced-to-vibe-code-3cc7</guid>
      <description>&lt;p&gt;Something shifted in the last year and I don't think we've named it properly yet.&lt;/p&gt;

&lt;p&gt;I know a lot of developers. Not Twitter developers, real ones. People who've been writing code for a decade, who read source code for fun, who can debug a memory leak by staring at a heap dump. Serious people.&lt;/p&gt;

&lt;p&gt;Eighteen months ago they laughed at Copilot. Twelve months ago they tolerated it. Six months ago they started using it quietly. Today some of them are shipping features they don't fully understand, generated by tools they don't fully trust, into codebases they're responsible for, because if they don't, the person next to them will.&lt;/p&gt;

&lt;p&gt;Nobody says this out loud. Not in standups, not in performance reviews. But it's happening.&lt;/p&gt;

&lt;p&gt;The pressure isn't coming from managers saying "use AI or else." It's subtler. It's the junior who joined six months ago shipping features at a rate that doesn't make sense. It's the team down the hall that went from four engineers to three and is somehow shipping faster. It's the recruiter who asks in the screening call whether you're "comfortable with modern AI tooling" and everyone knows what that means.&lt;/p&gt;

&lt;p&gt;The market decided something before any of us had time to have an opinion about it.&lt;/p&gt;

&lt;p&gt;Here's the honest part. I've been writing code for years. I trust my instincts. I can read a stack trace and know what's wrong before I open the file. That's a skill that took a long time to build and I'm proud of it.&lt;/p&gt;

&lt;p&gt;And yet. In the last six months there have been moments where I opened Cursor, described what I wanted, and got a working implementation before I would've finished typing the first function signature by hand.&lt;/p&gt;

&lt;p&gt;Not always. Not for anything subtle. But for the boring 70 percent of code that is CRUD and glue and boilerplate and wiring, yes. Every time.&lt;/p&gt;

&lt;p&gt;So I use it. I use it reluctantly, and I use it while feeling like I'm cheating at something that used to be a fair game, and I use it while knowing that the thing it's good at is exactly the thing that pays my bills.&lt;/p&gt;

&lt;p&gt;The phrase "vibe coding" bothered me when I first heard it. It sounds unserious. It sounds like somebody describing playing with a toy. Then I watched a friend of mine, a senior engineer at a large company, describe a feature to Claude, paste the result into his editor, tweak two lines, and commit. He didn't read most of it. He didn't need to. It worked.&lt;/p&gt;

&lt;p&gt;He's not a bad developer. He's a very good one. He just figured out that reading every line of generated code is the same as reading every line of a library you installed. Nobody does that. You trust it or you don't.&lt;/p&gt;

&lt;p&gt;The line I keep hearing, and I've started repeating, is: AI won't take your job. Someone who uses AI better will.&lt;/p&gt;

&lt;p&gt;I resisted that line for a while. It felt like cope. It felt like something middle managers say to make the transition sound less brutal than it is. But I've started believing it because I've watched the mechanics play out.&lt;/p&gt;

&lt;p&gt;The developers who are thriving right now aren't the ones who are best at writing code. They're the ones who are best at deciding what code should exist, describing it clearly, and then evaluating whether the result is correct. That's a different skill set. It's not lesser. It's just different from the one the industry spent twenty years rewarding.&lt;/p&gt;

&lt;p&gt;The people getting squeezed are the ones who were paid for typing speed and pattern recall. The ones who could write a Redux reducer from memory or scaffold a React component in their sleep. That knowledge is worth less today than it was eighteen months ago. Not worthless. Less.&lt;/p&gt;

&lt;p&gt;The people doing well are the ones who were already thinking in systems, who care about architecture more than syntax, who can look at a spec and know whether it's going to work before anyone writes a line. Those people are faster now. Much faster. They're not writing code they don't understand. They're just not writing the boring parts.&lt;/p&gt;

&lt;p&gt;I don't have a clean conclusion here. I don't think there is one.&lt;/p&gt;

&lt;p&gt;The uncomfortable truth is that the tool is real, the productivity gain is real, and the disruption is real. Anyone who tells you to just ignore it and keep coding by hand is giving you bad advice, and anyone who tells you to trust the output blindly is giving you worse advice.&lt;/p&gt;

&lt;p&gt;The middle path is harder than either. Use the tools, but don't stop understanding the code. Ship faster, but don't ship what you can't debug. Be willing to be the person who reads the generated diff line by line and rejects it, and also be willing to be the person who accepts it when it's genuinely fine.&lt;/p&gt;

&lt;p&gt;That's a narrow window to work in and I don't know anyone who's gotten the balance perfect yet.&lt;/p&gt;

&lt;p&gt;What I do know is that every developer I respect has quietly moved to the middle of that window in the last six months, regardless of what they post publicly.&lt;/p&gt;

&lt;p&gt;I'd like to hear from people who are resisting. What's the argument that keeps you writing everything by hand? Is it principle, is it craft, is it distrust, or is it just that the current tools aren't good enough for what you build?&lt;/p&gt;

&lt;p&gt;I'm genuinely curious. Every answer I've gotten so far has been more interesting than the takes I see on Twitter.&lt;/p&gt;

</description>
      <category>discuss</category>
      <category>ai</category>
      <category>career</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I stripped metadata from AI images to see if YouTube would still flag them</title>
      <dc:creator>vyixor</dc:creator>
      <pubDate>Mon, 28 Sep 2026 11:01:30 +0000</pubDate>
      <link>https://dev.to/vyixor/i-stripped-metadata-from-ai-images-to-see-if-youtube-would-still-flag-them-3b5l</link>
      <guid>https://dev.to/vyixor/i-stripped-metadata-from-ai-images-to-see-if-youtube-would-still-flag-them-3b5l</guid>
      <description>&lt;p&gt;I've been curious about this for a while. Every AI image generator I've tried writes metadata into the output files. Sometimes a full C2PA manifest, sometimes just a "made with X" tag, sometimes a pixel-level watermark buried in the image. The point is that platforms know what they're looking at.&lt;/p&gt;

&lt;p&gt;YouTube started relying on this in a serious way this year. If a video or thumbnail has AI metadata attached, YouTube shows a disclosure under it. That's fine for people who want to be transparent. But the way YouTube determines whether content is AI is largely by reading the metadata on the file.&lt;/p&gt;

&lt;p&gt;So I ran a test. I generated a batch of images with a common AI tool, ran them through SORO's EXIF remover (a tool I built for a completely different reason), and uploaded the cleaned files to a few platforms to see what happened.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Round one&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The flags never appeared. Files with empty EXIF were treated as normal uploads. YouTube, X, and Instagram all missed them. The metadata was the only signal being used, and without it, the systems had nothing to work with.&lt;/p&gt;

&lt;p&gt;That held for about six weeks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Round two&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Then things shifted. Same batch of files, same cleaning process, but now they were getting flagged again. Not every time, but often enough that it was obvious something had changed. The platforms had added a second layer that doesn't rely on metadata.&lt;/p&gt;

&lt;p&gt;That second layer does something else entirely. It looks at the image itself:&lt;/p&gt;

&lt;p&gt;Frequency domain analysis. AI-generated images have statistical quirks in high-frequency detail that natural photos don't have.&lt;/p&gt;

&lt;p&gt;Pixel watermarks. Some generators embed invisible marks that survive re-encoding.&lt;/p&gt;

&lt;p&gt;Heuristic detection. Compression artifacts, sharpness patterns, the way the model handles faces and text.&lt;/p&gt;

&lt;p&gt;None of these are perfect. All of them have false positive rates. But they exist, and they're getting better fast.&lt;/p&gt;

&lt;p&gt;The interesting part&lt;/p&gt;

&lt;p&gt;Metadata being strippable isn't news. Anyone who works with image files knows it. What's interesting is what this reveals about how these detection systems are built.&lt;/p&gt;

&lt;p&gt;Metadata is a soft signal. It's easy to remove, easy to forge, easy to lose by re-saving a file. Any detection that depends on it is one canvas re-encode away from being useless. The C2PA spec, which is the standard everyone is adopting right now, acknowledges this openly. Metadata can be removed, and once it is, the provenance is gone.&lt;/p&gt;

&lt;p&gt;The platforms know. Which is why they're shifting to image-based detection. Which is creating its own problems.&lt;/p&gt;

&lt;p&gt;Here's the tension nobody wants to name. The same tool that removes metadata for privacy also removes metadata for detection. There's no version of this where those two use cases can be separated.&lt;/p&gt;

&lt;p&gt;I strip metadata from photos I post. Not to hide anything. My phone writes GPS coordinates into every image I take, and I'd rather not publish where I live. That's a normal privacy practice. Every security guide says to do it.&lt;/p&gt;

&lt;p&gt;But when I strip metadata from an AI image, it defeats the detection too. Same tool, same action, opposite intent. Both uses are legitimate. Both uses are covered by the same button.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which brings me to the actual point&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;AI content isn't inherently harmful. I keep seeing that framing pushed and it's wrong. An AI-generated illustration for a personal project, a language model draft of a contract clause, a synthetic voiceover for a video essay. None of these things are bad. Some of them are genuinely useful and would take a human days to produce.&lt;/p&gt;

&lt;p&gt;The problem is volume. The internet is drowning. Reddit is a quarter AI slop, YouTube is worse in some categories, Google results are getting harder to read every month. The issue isn't that AI exists. It's that there's no cheap way to tell the good from the noise.&lt;/p&gt;

&lt;p&gt;Detection systems are trying to solve that. But they're solving it with a signal that's easy to remove and easy to defeat, and the same tools people use to protect their privacy are the same tools that defeat it. Which means the problem isn't technical. It's structural, and it's not going away.&lt;/p&gt;

&lt;p&gt;I don't have a solution. I have a test, a set of results, and a bunch of thoughts that don't fit anywhere else. If you're building in this space, I'd like to hear what you're seeing, especially if you have real numbers on detection accuracy.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>privacy</category>
      <category>discuss</category>
    </item>
    <item>
      <title>I built a PDF editor that can't see your files</title>
      <dc:creator>vyixor</dc:creator>
      <pubDate>Fri, 25 Sep 2026 15:08:00 +0000</pubDate>
      <link>https://dev.to/vyixor/i-built-a-pdf-editor-that-cant-see-your-files-3fc</link>
      <guid>https://dev.to/vyixor/i-built-a-pdf-editor-that-cant-see-your-files-3fc</guid>
      <description>&lt;p&gt;For the last two months I've been building a set of file tools that run entirely in the browser. The PDF editor is the one I use most, so that's what this post is about.&lt;/p&gt;

&lt;p&gt;The problem -&amp;gt;&lt;/p&gt;

&lt;p&gt;Every online PDF tool I tried did the same thing. You upload the file, they process it on a server, they send it back. Smallpdf, iLovePDF, Adobe's free tools, all of them. That's fine for a random utility bill, but it's a problem when the PDF is a contract, a medical form, or anything with personal info on it.&lt;/p&gt;

&lt;p&gt;The other option is a desktop app. LibreOffice Draw works, but it's heavy, and it doesn't handle signing well. So I had this gap where nothing fit: something light, browser-based, that didn't require handing my documents to a stranger.&lt;/p&gt;

&lt;p&gt;What I did about it&lt;/p&gt;

&lt;p&gt;I built the tool so it can't upload your files. Not "we promise not to" but "the architecture doesn't allow it."&lt;/p&gt;

&lt;p&gt;It's a static HTML page. There's no backend, no API, no database. The whole thing is JavaScript running in your browser tab. When you drop a PDF in, the File API reads it into memory, and pdf-lib processes it right there. When you download the result, your browser writes the file back out. At no point does anything leave your machine.&lt;/p&gt;

&lt;p&gt;You can verify this. Open DevTools, go to the Network tab, and use any tool. Zero outbound requests related to your file.&lt;/p&gt;

&lt;p&gt;What the editor actually does&lt;/p&gt;

&lt;p&gt;The main operations people seem to want:&lt;/p&gt;

&lt;p&gt;Merge multiple PDFs into one&lt;/p&gt;

&lt;p&gt;Split by range, or every N pages, or extract individual pages&lt;/p&gt;

&lt;p&gt;Rotate pages (the amount of sideways scans in the world is staggering)&lt;/p&gt;

&lt;p&gt;Reorder pages by dragging thumbnails&lt;/p&gt;

&lt;p&gt;Delete pages&lt;/p&gt;

&lt;p&gt;Compress with three quality levels&lt;/p&gt;

&lt;p&gt;Add a text watermark, rotated and semi-transparent&lt;/p&gt;

&lt;p&gt;Draw a signature with a pen pad, then place and resize it on the page&lt;/p&gt;

&lt;p&gt;Add text anywhere on a page, move it around until it sits right&lt;/p&gt;

&lt;p&gt;The reader mode was the part I spent the most time on. You open a page, click to place an element, then drag it, resize it with handles, rotate it, delete it, undo and redo. It behaves like a lightweight editor rather than a form filler, which is what most free tools feel like.&lt;/p&gt;

&lt;p&gt;The stack, briefly&lt;/p&gt;

&lt;p&gt;Plain HTML, CSS, and JavaScript. No framework, no bundler, no build step.&lt;/p&gt;

&lt;p&gt;pdf-lib for PDF manipulation&lt;/p&gt;

&lt;p&gt;pdf.js for rendering page thumbnails and the reader view&lt;/p&gt;

&lt;p&gt;Canvas API for the signature pad&lt;/p&gt;

&lt;p&gt;Cloudflare Pages for hosting. It's a static site so there's nothing to scale.&lt;/p&gt;

&lt;p&gt;The whole thing is around 2,500 lines of JavaScript in the PDF editor alone, most of which is the overlay element system for the reader. The rest of the site is a handful of smaller tools that share the same pattern.&lt;/p&gt;

&lt;p&gt;What I learned along the way&lt;/p&gt;

&lt;p&gt;The hardest part wasn't the PDF manipulation. It was the browser quirks. Mobile touch handling for drag and resize on elements that live on top of a canvas. Text input focus on iOS. Making sure that opening DevTools didn't break drag interactions. The libraries handle the file format stuff well; the interaction layer is where you spend all your time.&lt;/p&gt;

&lt;p&gt;The honest limitations&lt;/p&gt;

&lt;p&gt;It won't open password-protected PDFs. If you know the password, you have to decrypt it elsewhere first.&lt;/p&gt;

&lt;p&gt;It can't OCR scanned pages. That would need Tesseract.js, which is a much bigger dependency and slower to load.&lt;/p&gt;

&lt;p&gt;It can't edit the existing text in a PDF. PDFs aren't structured documents, they're a series of drawing instructions. Inserting new text is easy, changing existing text is a research project.&lt;/p&gt;

&lt;p&gt;It's also browser-memory-bound. On a phone with 4GB of RAM, a 200 MB PDF is going to struggle. On desktop it handles everything I've thrown at it.&lt;/p&gt;

&lt;p&gt;Why I'm posting this&lt;/p&gt;

&lt;p&gt;Partly to show the approach, since "just do it in the browser" solves a category of problems that don't need a server at all. Partly because I want to know what other people are doing for the same task. LibreOffice Draw, PDFgear, and some browser extensions all handle parts of this, and I'd like to know what I'm missing.&lt;/p&gt;

&lt;p&gt;The full editor is at soroflix.xyz/pdf-editor/ if you want to try it. No account, no signup, no limits. It's free.&lt;/p&gt;

&lt;p&gt;Happy to answer questions about the implementation, the libraries, or why the overlay system was the hardest part.&lt;/p&gt;

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