<?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: Khatai Huseynzada</title>
    <description>The latest articles on DEV Community by Khatai Huseynzada (@bilgegates).</description>
    <link>https://dev.to/bilgegates</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%2F3883065%2F9ae2042e-49f4-4d4d-b991-a01ab21ced16.jpg</url>
      <title>DEV Community: Khatai Huseynzada</title>
      <link>https://dev.to/bilgegates</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bilgegates"/>
    <language>en</language>
    <item>
      <title>What a chess parser taught me about trusting user input</title>
      <dc:creator>Khatai Huseynzada</dc:creator>
      <pubDate>Fri, 10 Jul 2026 17:04:49 +0000</pubDate>
      <link>https://dev.to/bilgegates/what-a-chess-parser-taught-me-about-trusting-user-input-l7l</link>
      <guid>https://dev.to/bilgegates/what-a-chess-parser-taught-me-about-trusting-user-input-l7l</guid>
      <description>&lt;p&gt;I build a chess diagram tool. You type a position, it draws a board, you export a nice image. Simple.&lt;/p&gt;

&lt;p&gt;The whole thing runs on one string: the FEN. 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;rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the starting position. Eight ranks separated by slashes, a letter per piece, a number for empty squares. Harmless.&lt;/p&gt;

&lt;p&gt;Except that string doesn't always come from a keyboard. It comes from a URL somebody shared. From &lt;code&gt;localStorage&lt;/code&gt; that could have been edited. From a cloud row that synced back down. From a database API I don't control. Every one of those is a door, and for a while I was leaving them open because the input &lt;em&gt;looked&lt;/em&gt; boring.&lt;/p&gt;

&lt;p&gt;Here's what I learned closing them.&lt;/p&gt;

&lt;h2&gt;
  
  
  "It's just a string" is the lie
&lt;/h2&gt;

&lt;p&gt;The trap with input like a FEN is that it feels too small to be dangerous. It's 40-ish characters of chess notation. What's the worst it can do?&lt;/p&gt;

&lt;p&gt;But your code doesn't treat it as 40 characters. It parses it. It stores it. It stringifies it into a filename. It renders it back into the DOM. Each of those steps trusts the string a little more than the last, and the string never earned that trust — you just assumed it.&lt;/p&gt;

&lt;p&gt;So I stopped asking "is this valid chess?" and started asking "what happens if this is &lt;em&gt;not&lt;/em&gt; what I expect, at every single place I touch it?"&lt;/p&gt;

&lt;h2&gt;
  
  
  Door 1: length, before anything else
&lt;/h2&gt;

&lt;p&gt;The first thing a parser should do is refuse to parse.&lt;/p&gt;

&lt;p&gt;A real FEN maxes out around 90 characters. So the cap comes first, before any splitting or looping:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MAX_FEN_LENGTH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;93&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;parseFEN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fenString&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;fenString&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;fenString&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FENParseError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invalid FEN string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fenString&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;MAX_FEN_LENGTH&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FENParseError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;FEN string exceeds maximum length&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// ...only now do we start reading it&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This isn't about chess. It's that any string arriving from outside can be a megabyte, and if the first thing I do is &lt;code&gt;.split()&lt;/code&gt; and loop over it, I've handed a stranger the ability to make my main thread chew on garbage. The length check is a bouncer at the door. Cheap, boring, first.&lt;/p&gt;

&lt;p&gt;The nice side effect: I enforce the &lt;em&gt;same&lt;/em&gt; cap everywhere a FEN enters — the URL handler, the share link, the input field's &lt;code&gt;maxLength&lt;/code&gt;. One number, checked at every door, instead of "I'm sure the parser handles it."&lt;/p&gt;

&lt;h2&gt;
  
  
  Door 2: parsing is validation, not decoration
&lt;/h2&gt;

&lt;p&gt;My early parser was optimistic. It split on &lt;code&gt;/&lt;/code&gt;, walked each rank, and mostly assumed the pieces were pieces. If something weird showed up, it kind of... limped along and produced a half-broken board.&lt;/p&gt;

&lt;p&gt;That's the worst outcome. Not a crash — a &lt;em&gt;quiet&lt;/em&gt; wrong answer that flows downstream.&lt;/p&gt;

&lt;p&gt;The fix was to make the parser strict and loud. Every rank has to have exactly 8 squares. Every character is either a digit or a real piece letter, nothing else. Anything off the script throws:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;for &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;char&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;VALID_DIGITS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;char&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;char&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;squareCount&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;boardRow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;isPieceSymbol&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;char&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FENParseError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Invalid piece character '&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;char&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="nx"&gt;squareCount&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;boardRow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;char&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;squareCount&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FENParseError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Rank has &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;squareCount&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; squares instead of 8`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rule I follow now: a parser that returns a "kind of okay" result on bad input is a bug generator. Either you get a valid board or you get an error you can catch. There is no third thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Door 3: JSON.parse is not a safe way to read stored data
&lt;/h2&gt;

&lt;p&gt;This one actually surprised me.&lt;/p&gt;

&lt;p&gt;I sync positions and settings. They live in &lt;code&gt;localStorage&lt;/code&gt; and in a cloud row, and both come back as strings I &lt;code&gt;JSON.parse&lt;/code&gt;. Standard stuff. But &lt;code&gt;JSON.parse&lt;/code&gt; will happily reconstruct &lt;em&gt;anything&lt;/em&gt; in that string, including keys you never want to see:&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;"fen"&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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"__proto__"&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;"isAdmin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&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;Parse that naively and, depending on how the object flows through your code, you can end up polluting the prototype every object inherits from. This is a real, named class of bug — prototype pollution — and the entry point is as innocent as "read my saved settings."&lt;/p&gt;

&lt;p&gt;So I don't call &lt;code&gt;JSON.parse&lt;/code&gt; directly on anything from outside. It goes through one function that strips the poison keys with a reviver and falls back instead of throwing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;POISON&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;Set&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;__proto__&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;constructor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;prototype&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;safeJSONParse&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&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;jsonString&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;jsonString&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;jsonString&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;jsonString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;POISON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;fallback&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 I like about this. It drops the dangerous keys instead of trusting the input to be nice. And a broken string gives me the fallback, not an exception halfway through app startup. The rule became simple and absolute: raw &lt;code&gt;JSON.parse&lt;/code&gt; on external data is banned in the codebase. Every read goes through this door.&lt;/p&gt;

&lt;h2&gt;
  
  
  Door 4: sanitize at the boundary, based on where it's going
&lt;/h2&gt;

&lt;p&gt;Here's the part that took me longest to internalize: there isn't one "sanitize" function. Cleaning depends on the destination.&lt;/p&gt;

&lt;p&gt;The same user-supplied string might become a filename, a color, or text on the page — and each of those has different things that hurt it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Going into the DOM as text?&lt;/strong&gt; Escape the HTML so a &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; stays literal characters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sanitizeInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;maxLength&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&amp;amp;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;amp;amp;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&amp;lt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;amp;lt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&amp;gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/"/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;amp;quot;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/'/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;amp;#x27;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;amp;#x2F;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;maxLength&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&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;maxLength&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Becoming a filename?&lt;/strong&gt; Kill the path separators and reserved characters so a "name" can't climb out of the directory or break the download:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sanitizeFileName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fileName&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;fileName&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;fileName&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;chess-position&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fileName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[\\/&lt;/span&gt;&lt;span class="sr"&gt;:*?"&amp;lt;&amp;gt;|&amp;amp;&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;_&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;+/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;+$/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&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;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;chess-position&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;&lt;strong&gt;Being used as a color?&lt;/strong&gt; Don't "clean" it — allowlist it. Either it matches an exact hex pattern or it's replaced with a default. No trying to fix a bad value:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isValidHexColor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;color&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="kr"&gt;string&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;typeof&lt;/span&gt; &lt;span class="nx"&gt;color&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="sr"&gt;/^#&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;0-9A-Fa-f&lt;/span&gt;&lt;span class="se"&gt;]{6}&lt;/span&gt;&lt;span class="sr"&gt;$/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sanitizeHexColor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fallback&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#ffffff&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;isValidHexColor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;color&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fallback&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;That last one is the pattern I trust most: &lt;strong&gt;allowlist, don't blocklist.&lt;/strong&gt; A blocklist is you trying to imagine every bad input. An allowlist is you describing the one shape you accept and rejecting the infinite rest by default. You will lose the imagination game. You won't lose the allowlist game.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mental model that stuck
&lt;/h2&gt;

&lt;p&gt;I used to think of security as a thing you bolt on — a review at the end, a linter rule, a checklist.&lt;/p&gt;

&lt;p&gt;What actually changed my code was a smaller idea: &lt;strong&gt;every place data crosses from "outside" to "inside" is a boundary, and boundaries have jobs.&lt;/strong&gt; The parser's job is to reject anything that isn't a real board. The storage reader's job is to never reconstruct a poison key. The DOM writer's job is to escape. The filename builder's job is to strip paths.&lt;/p&gt;

&lt;p&gt;None of these are clever. That's the point. They're boring, they live at the edges, and they mean that by the time a value reaches the interesting part of my app, it has already passed through the right door for where it's headed.&lt;/p&gt;

&lt;p&gt;The chess part was incidental. The lesson is: the more harmless your input looks, the more likely you are to trust it without earning it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This all came out of an open-source chess diagram tool I've been building. If you want to see the parser and the sanitizers in their natural habitat, the repo is &lt;a href="https://github.com/chessviewer-org/chess-viewer" rel="noopener noreferrer"&gt;chessviewer-org/chess-viewer&lt;/a&gt; and the utilities live in their own package, &lt;a href="https://www.npmjs.com/package/@chessviewer-org/chess-viewer" rel="noopener noreferrer"&gt;@chessviewer-org/chess-viewer&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>typescript</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I built a Chess Diagram Generator from scratch using React and Raw SVG (0 external chess libraries)</title>
      <dc:creator>Khatai Huseynzada</dc:creator>
      <pubDate>Thu, 09 Jul 2026 22:26:31 +0000</pubDate>
      <link>https://dev.to/bilgegates/i-built-a-chess-diagram-generator-from-scratch-using-react-and-raw-svg-0-external-chess-libraries-5c11</link>
      <guid>https://dev.to/bilgegates/i-built-a-chess-diagram-generator-from-scratch-using-react-and-raw-svg-0-external-chess-libraries-5c11</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fca5qvhiu25emadndiqgn.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%2Fca5qvhiu25emadndiqgn.png" alt=" " width="799" height="506"&gt;&lt;/a&gt;Hey everyone! 👋&lt;/p&gt;

&lt;p&gt;Over the last 7 months, I’ve been working on a personal project: a fully open-source, privacy-first Chess Diagram Generator called &lt;strong&gt;Chess Viewer&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Usually, when developers build chess apps, they rely on heavy libraries like &lt;code&gt;chess.js&lt;/code&gt; or &lt;code&gt;react-chessboard&lt;/code&gt;. I wanted to challenge myself. So, I built the entire board, FEN (Forsyth-Edwards Notation) parsing, and piece rendering logic completely from scratch using &lt;strong&gt;React&lt;/strong&gt; and &lt;strong&gt;Raw SVG&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;It mathematically calculates where each SVG piece should go, supports advanced FEN previews, and exports high-quality diagrams.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tech Stack:&lt;/strong&gt; React, TypeScript, Vite, PWA.&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;Live Demo:&lt;/strong&gt; &lt;a href="https://chessvision.org" rel="noopener noreferrer"&gt;https://chessvision.org&lt;/a&gt;&lt;br&gt;
💻 &lt;strong&gt;GitHub Repo:&lt;/strong&gt; &lt;a href="https://github.com/chessviewer-org/chess-viewer" rel="noopener noreferrer"&gt;https://github.com/chessviewer-org/chess-viewer&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  🔥 Roast My Code! (I need your feedback)
&lt;/h3&gt;

&lt;p&gt;I'm 17 years old and mostly self-taught. Since I built the entire architecture and SVG logic myself without tutorials, I’m 100% sure there are better ways to handle some of the state management and re-renders. &lt;/p&gt;

&lt;p&gt;I would absolutely love it if some experienced React developers could check out the repo and roast my code. Tell me what I did wrong or how I can optimize it!&lt;/p&gt;

&lt;h3&gt;
  
  
  🛠️ Want to contribute? (Good First Issues)
&lt;/h3&gt;

&lt;p&gt;I want to make this a community-driven tool. I've set up several beginner-friendly issues in the repo if you want to make your first open-source contribution or just practice your React skills.&lt;/p&gt;

&lt;p&gt;We currently need help with bugs like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;#186&lt;/strong&gt; - Piece entrance animation replays on back-navigation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;#187&lt;/strong&gt; - Export file size estimate does not match actual exported file&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Just look for the &lt;code&gt;good first issue&lt;/code&gt; or &lt;code&gt;help wanted&lt;/code&gt; tags in the repo! &lt;/p&gt;

&lt;p&gt;Any feedback, Pull Requests, or stars ⭐ would mean the world to me. Thanks for reading!&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>react</category>
      <category>opensource</category>
      <category>webdev</category>
    </item>
    <item>
      <title>7 months building an open-source chess tool. Here's what actually happened.</title>
      <dc:creator>Khatai Huseynzada</dc:creator>
      <pubDate>Sun, 28 Jun 2026 16:53:19 +0000</pubDate>
      <link>https://dev.to/bilgegates/7-months-building-an-open-source-chess-tool-heres-what-actually-happened-1g60</link>
      <guid>https://dev.to/bilgegates/7-months-building-an-open-source-chess-tool-heres-what-actually-happened-1g60</guid>
      <description>&lt;p&gt;I started this project in December 2025. Not because anyone asked. Not because I saw a gap in the market. Because I compose chess problems as a hobby and every tool I tried to export a diagram with either watermarked the output, required an account, or exported at 72 DPI like it was still 2003.&lt;/p&gt;

&lt;p&gt;So I built my own. Seven months later: 5 stars on GitHub, 6 forks, a handful of npm downloads, about $13 spent on hosting.&lt;/p&gt;

&lt;p&gt;That's the whole story, numbers-wise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I kept going
&lt;/h2&gt;

&lt;p&gt;The tool actually works. That sounds like a low bar, but a lot of side projects don't clear it.&lt;/p&gt;

&lt;p&gt;You paste in a FEN string, configure the board however you want — colors, piece set, whether to show coordinates — and export a PNG at up to 1200 DPI with real physical dimensions. You tell it "6cm board at 600 DPI" and it gives you a file you can drop straight into a print layout. No watermark. No account. No ads.&lt;/p&gt;

&lt;p&gt;I added cloud sync, batch export (up to 10 positions as a ZIP), a FEN history, a position database search. Probably spent too long on some of that. But the core thing works exactly as I wanted it to.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bugs that actually taught me something
&lt;/h2&gt;

&lt;p&gt;Safari has a GPU memory limit on canvas elements that Chrome doesn't. If you don't reset &lt;code&gt;canvas.width = 0; canvas.height = 0&lt;/code&gt; after every blob export, iOS will crash — silently, with no useful error. I hit this, spent a while confused about why it only happened on iPhones, then traced it down and fixed it. Now it's a documented invariant in the codebase: you touch the canvas export path, you reset the dimensions after. No exceptions.&lt;/p&gt;

&lt;p&gt;The FEN parser has co-located unit tests and a rule: every change requires a new test case. It sounds strict but it's saved me twice.&lt;/p&gt;

&lt;p&gt;These are the parts I'm actually proud of.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I got wrong
&lt;/h2&gt;

&lt;p&gt;I thought if the tool was good, people would find it. That's not how it works.&lt;/p&gt;

&lt;p&gt;GitHub has no discovery mechanism. npm sorts by download count — which means new packages are invisible by default. The people who need your tool don't know it exists and aren't searching for it by name.&lt;/p&gt;

&lt;p&gt;I also built for a narrow audience without fully accepting what that meant. Chess composers are a small group. Most chess players don't need to export diagrams to print. I knew this going in, but I still somehow expected more traction.&lt;/p&gt;

&lt;h2&gt;
  
  
  The npm package
&lt;/h2&gt;

&lt;p&gt;A month ago I extracted the rendering core into a separate package: &lt;code&gt;@chessviewer-org/chess-viewer&lt;/code&gt;. Zero dependencies, works in Node.js, Deno, Bun, and the browser. Pure SVG output — no DOM, no canvas.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;generateDiagram&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@chessvision-org/chess-vision&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;svg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;generateDiagram&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;fen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;showCoords&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The idea was that even if the web app never takes off, the package might be useful to someone building a chess blog or generating diagrams server-side. It's only been on npm a week so I have no idea yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I'm at
&lt;/h2&gt;

&lt;p&gt;The code is the best it's ever been. The tool does what I wanted it to do. I've learned more about Canvas, Safari memory behavior, Supabase RLS, and FEN parsing edge cases than I ever would have otherwise.&lt;/p&gt;

&lt;p&gt;But I haven't figured out how to reach the people who would actually use it. That part is harder than the technical stuff. I'm more comfortable writing a parser than writing a cold message to a chess magazine editor.&lt;/p&gt;

&lt;p&gt;I'm going to keep working on it. Just with less time on features and more time actually talking to people.&lt;/p&gt;




&lt;p&gt;Repo: &lt;a href="https://github.com/chessviewer-org/chess-viewer" rel="noopener noreferrer"&gt;github.com/chessviewer-org/chess-viewer&lt;/a&gt;&lt;br&gt;&lt;br&gt;
Tool: &lt;a href="https://chessvision.org" rel="noopener noreferrer"&gt;chessvision.org&lt;/a&gt;&lt;br&gt;&lt;br&gt;
Package: &lt;a href="https://www.npmjs.com/package/@chessviewer-org/chess-viewer" rel="noopener noreferrer"&gt;@chessviewer-org/chess-viewer&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you've been through something similar — built something real, got nowhere with distribution — I'd like to hear what you did.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>typescript</category>
      <category>career</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Stop Using console.log(): 5 Hidden Chrome DevTools Superpowers 🚀</title>
      <dc:creator>Khatai Huseynzada</dc:creator>
      <pubDate>Thu, 16 Apr 2026 19:25:27 +0000</pubDate>
      <link>https://dev.to/bilgegates/stop-using-consolelog-5-hidden-chrome-devtools-superpowers-628</link>
      <guid>https://dev.to/bilgegates/stop-using-consolelog-5-hidden-chrome-devtools-superpowers-628</guid>
      <description>&lt;p&gt;Let's be honest: as web developers, whether we are working on the front-end or back-end, we spend half of our lives staring at the browser's Developer Tools. However, the vast majority of us only scratch the surface of what this massive Swiss Army knife can actually do.&lt;/p&gt;

&lt;p&gt;If your primary debugging strategy is still littering your codebase with &lt;code&gt;console.log("here 1")&lt;/code&gt; and &lt;code&gt;console.log("data", data)&lt;/code&gt;, you are losing valuable time. &lt;/p&gt;

&lt;p&gt;Let's dive into 5 lesser-known Chrome DevTools features that will instantly speed up your workflow and make your debugging process a lot less painful.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Convert Network Requests to Code Instantly ("Copy as Fetch") 🌐
&lt;/h2&gt;

&lt;p&gt;Imagine a complex API request is firing on your website, and you want to test that exact same request in your terminal, a Node.js script, or Postman. Manually typing out all the headers, authorization tokens, and stringified payloads is a nightmare.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Hack:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to the &lt;strong&gt;Network&lt;/strong&gt; tab in DevTools.&lt;/li&gt;
&lt;li&gt;Right-click the request you want to duplicate.&lt;/li&gt;
&lt;li&gt;Hover over &lt;strong&gt;Copy&lt;/strong&gt; and select &lt;strong&gt;Copy as fetch&lt;/strong&gt; (or &lt;code&gt;Copy as cURL&lt;/code&gt;).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Boom! The browser instantly generates the exact code needed to replicate that request. You can paste it directly into your code editor or console and tweak it as needed.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Visualize Arrays and Objects with &lt;code&gt;console.table()&lt;/code&gt; 📊
&lt;/h2&gt;

&lt;p&gt;When fetching an array of objects from an API, a standard &lt;code&gt;console.log()&lt;/code&gt; results in a deeply nested, messy output in the console that requires endless clicking to expand.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Hack:&lt;/strong&gt;&lt;br&gt;
Swap out your &lt;code&gt;console.log(data)&lt;/code&gt; for &lt;code&gt;console.table(data)&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;The browser will automatically parse your array of objects and render it as a beautifully formatted, highly readable table. Even better? You can click the column headers in the console to sort the data!&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Turn Your Browser into a Live Text Editor (&lt;code&gt;designMode&lt;/code&gt;) ✍️
&lt;/h2&gt;

&lt;p&gt;A designer or client asks you to "quickly tweak the wording" on a landing page to see how it looks. Switching back to your IDE, finding the exact text node, changing it, and waiting for the page to reload is incredibly tedious.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Hack:&lt;/strong&gt;&lt;br&gt;
Open your Console and type this magic spell:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;document.designMode = "on";&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Hit Enter. Now, you can click on &lt;em&gt;any&lt;/em&gt; text element on the webpage and edit it live, exactly like a Microsoft Word document. It’s an absolute lifesaver for rapid UI testing and typography adjustments.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Lightning-Fast DOM Access in the Console (&lt;code&gt;$0&lt;/code&gt;) ⚡
&lt;/h2&gt;

&lt;p&gt;You are inspecting an HTML element in the &lt;strong&gt;Elements&lt;/strong&gt; tab and want to manipulate it using JavaScript in the console. Most developers waste time writing &lt;code&gt;document.querySelector('.my-long-class-name')&lt;/code&gt; to grab it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Hack:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Simply click on any element in the &lt;strong&gt;Elements&lt;/strong&gt; tab so it's highlighted.&lt;/li&gt;
&lt;li&gt;Switch over to the &lt;strong&gt;Console&lt;/strong&gt; tab.&lt;/li&gt;
&lt;li&gt;Type &lt;code&gt;$0&lt;/code&gt; and hit Enter.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's it! &lt;code&gt;$0&lt;/code&gt; is a magic variable that holds the reference to the most recently inspected element. You can immediately start calling methods on it, like &lt;code&gt;$0.classList.add('hidden')&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Bonus: DevTools remembers your history! You can use &lt;code&gt;$1&lt;/code&gt;, &lt;code&gt;$2&lt;/code&gt;, etc., to reference previously inspected elements).&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Debug Loops Painlessly with Conditional Breakpoints 🛑
&lt;/h2&gt;

&lt;p&gt;You have a &lt;code&gt;for&lt;/code&gt; loop that iterates 1,000 times, but a bug only occurs when &lt;code&gt;i === 499&lt;/code&gt;. If you drop a standard breakpoint inside the loop, execution will pause on the very first iteration, and you'll have to click "Resume" 499 times. No thanks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Hack:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to the &lt;strong&gt;Sources&lt;/strong&gt; tab.&lt;/li&gt;
&lt;li&gt;Right-click on the line number where you want to pause.&lt;/li&gt;
&lt;li&gt;Select &lt;strong&gt;Add conditional breakpoint...&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Type your condition (e.g., &lt;code&gt;i === 499&lt;/code&gt; or &lt;code&gt;user.role === 'admin'&lt;/code&gt;).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Execution will completely ignore the breakpoint &lt;em&gt;unless&lt;/em&gt; your exact condition evaluates to true. This reduces your bug-hunting time from hours to seconds.&lt;/p&gt;




&lt;h3&gt;
  
  
  Wrapping Up
&lt;/h3&gt;

&lt;p&gt;Chrome DevTools is much more than just a place to read error messages. By incorporating these small hacks into your daily routine, you'll drastically reduce friction in your development process.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which DevTools feature can you not live without? Did I miss your favorite? Let me know in the comments below! 👇&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>productivity</category>
      <category>debugging</category>
    </item>
  </channel>
</rss>
