<?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: Mejzun</title>
    <description>The latest articles on DEV Community by Mejzun (@mejzun).</description>
    <link>https://dev.to/mejzun</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%2F4159663%2F2b00c708-3d2f-4694-8db5-51aa057df3a6.png</url>
      <title>DEV Community: Mejzun</title>
      <link>https://dev.to/mejzun</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mejzun"/>
    <language>en</language>
    <item>
      <title>When to drop a game from your game backlog: what 770 dropped games in real backlogs look like</title>
      <dc:creator>Mejzun</dc:creator>
      <pubDate>Tue, 10 Nov 2026 11:14:05 +0000</pubDate>
      <link>https://dev.to/mejzun/when-to-drop-a-game-from-your-game-backlog-what-770-dropped-games-in-real-backlogs-look-like-1hc3</link>
      <guid>https://dev.to/mejzun/when-to-drop-a-game-from-your-game-backlog-what-770-dropped-games-in-real-backlogs-look-like-1hc3</guid>
      <description>&lt;p&gt;"Should I drop this game?" is the question that keeps a game backlog growing. I run a free &lt;a href="https://nxto.app" rel="noopener noreferrer"&gt;game backlog tracker&lt;/a&gt;, so I looked at what people actually drop. Data from 129 accounts and 25,276 games, as of 2026-11-10.&lt;/p&gt;

&lt;h2&gt;
  
  
  How rare is "Dropped"?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;770 games are marked Dropped, which is &lt;strong&gt;3% of all games&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;48 of the 129 people have dropped at least one. The other 81 never used the status.&lt;/li&gt;
&lt;li&gt;2,899 games are Done (11.5%), so people finish about 3.8 games for every one they drop.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Dropped games are longer
&lt;/h2&gt;

&lt;p&gt;For games with HowLongToBeat times (725 dropped, 2,770 done, 18,960 in backlog):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;th&gt;Average main story&lt;/th&gt;
&lt;th&gt;Median&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Done&lt;/td&gt;
&lt;td&gt;18.7 h&lt;/td&gt;
&lt;td&gt;11 h&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dropped&lt;/td&gt;
&lt;td&gt;24.7 h&lt;/td&gt;
&lt;td&gt;12 h&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backlog&lt;/td&gt;
&lt;td&gt;13.6 h&lt;/td&gt;
&lt;td&gt;4 h&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The medians for done and dropped games are almost the same, so length alone doesn't explain why a game gets dropped. The average is higher for dropped games (about 32% more), which means the very long games are where people give up. Games in the backlog are much shorter on the median: people add short, cheap or free games first and leave the big ones alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do with this
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Set a rule before you start.&lt;/strong&gt; "If I haven't touched it in a month, it goes to Dropped" is easier than deciding while you feel guilty.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Be extra careful with 25+ hour games.&lt;/strong&gt; That is where the average pulls away. Start those only when you have a free stretch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dropped is a status, not a failure.&lt;/strong&gt; A game backlog that never uses it can only grow. Moving a game out of the list is the cheapest way to make it shorter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You can come back.&lt;/strong&gt; The status is a label, not a delete button.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Caveats
&lt;/h2&gt;

&lt;p&gt;It's a small, self-selected sample, and some of the 770 may be games a person dropped before they started tracking. HowLongToBeat times exist for 725 of the 770 dropped games, so the table doesn't cover all of them.&lt;/p&gt;

&lt;p&gt;What is your rule for dropping a game in your game backlog?&lt;/p&gt;

</description>
      <category>data</category>
      <category>webdev</category>
      <category>productivity</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>My game search kept dying when RAWG went down, so I built a fallback from Steam and HowLongToBeat</title>
      <dc:creator>Mejzun</dc:creator>
      <pubDate>Tue, 10 Nov 2026 10:55:26 +0000</pubDate>
      <link>https://dev.to/mejzun/my-game-search-kept-dying-when-rawg-went-down-so-i-built-a-fallback-from-steam-and-howlongtobeat-5cgk</link>
      <guid>https://dev.to/mejzun/my-game-search-kept-dying-when-rawg-went-down-so-i-built-a-fallback-from-steam-and-howlongtobeat-5cgk</guid>
      <description>&lt;p&gt;How a small Express app keeps title search working when its main API is down or doesn't know a game: two keyless sources, a hard 3 second timeout each, and a cache that refuses to remember outages.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://nxto.app" rel="noopener noreferrer"&gt;NXTO Game Backlog&lt;/a&gt; is a free game backlog tracker. You type a title, pick the game, it lands in your list. The catalog (about 153,000 games) comes mostly from RAWG, and RAWG is the weak link: its free tier has a hard quota, it has outages, and it lags on new releases. When search breaks, a new user can't add their first game, and that's the one moment I can't afford to lose them.&lt;/p&gt;

&lt;p&gt;So search now has a fallback that answers when RAWG is down or doesn't know the title.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two sources that need no API key
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Steam store search&lt;/strong&gt; knows every PC release the day it appears. It returns soundtracks, DLC and demos next to games, so each hit is checked with &lt;code&gt;appdetails&lt;/code&gt;, and only &lt;code&gt;type === 'game'&lt;/code&gt; survives. That same call gives genres, release year and a short description.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HowLongToBeat&lt;/strong&gt; adds playtime, and covers console-only games Steam has never heard of.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Results are merged by normalized title into the same shape the normal search returns, so the frontend doesn't know which path answered. Playtime from HLTB is attached to the Steam entry with the same title. HLTB titles Steam doesn't have are kept only if they are close enough to the query (similarity of at least 0.75), because below that they are usually unrelated games.&lt;/p&gt;

&lt;h2&gt;
  
  
  Never let a slow source hold the search up
&lt;/h2&gt;

&lt;p&gt;Both sources run in parallel with &lt;code&gt;Promise.allSettled&lt;/code&gt;, each with a 3 second timeout (&lt;code&gt;AbortSignal.timeout&lt;/code&gt; on the fetch, plus a &lt;code&gt;Promise.race&lt;/code&gt; on the whole call as a second belt). A source that times out or fails contributes an empty list, and the other one still answers.&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="n"&gt;steamRes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hltbRes&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;allSettled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="nf"&gt;withTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;searchSteam&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&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="nf"&gt;withTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;searchHltb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;steam&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;steamRes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;'fulfilled'&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;steamRes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&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="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;hltb&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hltbRes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;  &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;'fulfilled'&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;hltbRes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&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;Results are capped at 6, and the Steam cover falls back to the header image when the portrait art 404s, which it does for most small indies.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cache bug I avoided on purpose
&lt;/h2&gt;

&lt;p&gt;Answers are cached for an hour per normalized query. The detail that matters: &lt;strong&gt;a result is only cached when both sources succeeded.&lt;/strong&gt; If Steam was down and I cached the thin answer, the outage would stick for an hour after Steam came back. A failed source means "answer now, remember nothing".&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell myself earlier
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Decide up front what "degraded" looks like. For me it's fewer results and no playtime, never an error page.&lt;/li&gt;
&lt;li&gt;Bound every external call. Without the timeout, a hanging provider turns a partial failure into a total one.&lt;/li&gt;
&lt;li&gt;Cache successes, not outages.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;NXTO is free to try at &lt;a href="https://nxto.app" rel="noopener noreferrer"&gt;nxto.app&lt;/a&gt;, and if you track your backlog somewhere else I'd like to know what search feels like there.&lt;/p&gt;

</description>
      <category>node</category>
      <category>webdev</category>
      <category>api</category>
      <category>showdev</category>
    </item>
    <item>
      <title>My site's mobile PageSpeed went 86 77 99 in one afternoon.

The "smart" fix made it worse. The boring fix gave me +22 points.

What happened 🧵</title>
      <dc:creator>Mejzun</dc:creator>
      <pubDate>Thu, 29 Oct 2026 12:44:01 +0000</pubDate>
      <link>https://dev.to/mejzun/my-sites-mobile-pagespeed-went-86-77-99-in-one-afternoon-the-smart-fix-made-it-worse-307l</link>
      <guid>https://dev.to/mejzun/my-sites-mobile-pagespeed-went-86-77-99-in-one-afternoon-the-smart-fix-made-it-worse-307l</guid>
      <description></description>
      <category>frontend</category>
      <category>performance</category>
      <category>webdev</category>
    </item>
    <item>
      <title>PageSpeed 77 to 99 on a Vue site: the boring fixes that worked (and the clever one that made it worse)</title>
      <dc:creator>Mejzun</dc:creator>
      <pubDate>Thu, 29 Oct 2026 11:22:45 +0000</pubDate>
      <link>https://dev.to/mejzun/pagespeed-77-to-99-on-a-vue-site-the-boring-fixes-that-worked-and-the-clever-one-that-made-it-3nda</link>
      <guid>https://dev.to/mejzun/pagespeed-77-to-99-on-a-vue-site-the-boring-fixes-that-worked-and-the-clever-one-that-made-it-3nda</guid>
      <description>&lt;p&gt;I run &lt;a href="https://nxto.app" rel="noopener noreferrer"&gt;NXTO&lt;/a&gt;, a free game backlog tracker (Vue 3 + Express + SQLite on one small server). The landing page scored 86 on mobile PageSpeed and I wanted 100. Here is what I changed, in the order I tried it, including the change that made things worse.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempt 1: make Google Fonts async (86 → 77)
&lt;/h2&gt;

&lt;p&gt;The report said render-blocking requests would cost 840 ms, and the biggest one was the Google Fonts stylesheet. The textbook fix is &lt;code&gt;media="print" onload="this.media='all'"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It made things worse: &lt;strong&gt;CLS went from 0.001 to 0.131&lt;/strong&gt; and FCP did not improve. The fonts arrived late, text re-flowed, and Lighthouse punished the shift. I reverted it.&lt;/p&gt;

&lt;p&gt;Lesson: deferring a font stylesheet does not remove the font from the critical path, it just moves the layout shift to after paint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 1: self-host the fonts and preload the two that matter
&lt;/h2&gt;

&lt;p&gt;I downloaded the woff2 files, inlined the &lt;code&gt;@font-face&lt;/code&gt; rules in &lt;code&gt;index.html&lt;/code&gt;, and preloaded only the Latin subsets of the two families used above the fold:&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;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"preload"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/fonts/inter-latin.woff2"&lt;/span&gt; &lt;span class="na"&gt;as=&lt;/span&gt;&lt;span class="s"&gt;"font"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"font/woff2"&lt;/span&gt; &lt;span class="na"&gt;crossorigin&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"preload"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/fonts/space-grotesk-latin.woff2"&lt;/span&gt; &lt;span class="na"&gt;as=&lt;/span&gt;&lt;span class="s"&gt;"font"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"font/woff2"&lt;/span&gt; &lt;span class="na"&gt;crossorigin&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;Other scripts (Cyrillic, Greek, Vietnamese) are separate files with &lt;code&gt;unicode-range&lt;/code&gt;, so the browser only downloads them when a page needs them. The site has 12 languages, so this matters. One more family (a monospace font used in a few components) is gone from the landing page; it falls back to the system monospace.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 2: the hero was hiding behind an animation
&lt;/h2&gt;

&lt;p&gt;The landing headline used a fade-up animation: &lt;code&gt;opacity: 0&lt;/code&gt; to &lt;code&gt;1&lt;/code&gt;, 0.8 s long, with a stagger delay of up to 0.85 s per element. Lighthouse cannot report LCP for something that is invisible, so the headline was literally waiting for its animation. I shortened it to 0.35 s, removed the stagger, and start from &lt;code&gt;opacity: .01&lt;/code&gt; with a 12 px offset.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 3: an &lt;code&gt;@import&lt;/code&gt; hiding in a component
&lt;/h2&gt;

&lt;p&gt;Round two of the report showed a second Google Fonts request: &lt;code&gt;family=Cookie&lt;/code&gt;. It came from a scoped &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; block in a "Buy me a coffee" button, via &lt;code&gt;@import url(...)&lt;/code&gt;. Vite had bundled it into the main CSS, so &lt;strong&gt;my main stylesheet was blocked on a third-party request&lt;/strong&gt;. I replaced that font with a system cursive stack.&lt;/p&gt;

&lt;p&gt;Search your built CSS for &lt;code&gt;@import&lt;/code&gt; and &lt;code&gt;googleapis&lt;/code&gt;. It takes ten seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 4: no compression, at all
&lt;/h2&gt;

&lt;p&gt;This was the big one. The audit said "text compression not applied", and &lt;code&gt;curl -I -H "Accept-Encoding: gzip"&lt;/code&gt; confirmed it: neither the HTML nor the 76 KB CSS had a &lt;code&gt;Content-Encoding&lt;/code&gt; header.&lt;/p&gt;

&lt;p&gt;I had a Caddy config with &lt;code&gt;encode gzip zstd&lt;/code&gt;, but in production Caddy was not in front of the app. The Express server was serving raw bytes. The fix was one line:&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="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;compression&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Results (mobile, Moto G Power emulation, slow 4G)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Performance&lt;/td&gt;
&lt;td&gt;86&lt;/td&gt;
&lt;td&gt;99&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FCP&lt;/td&gt;
&lt;td&gt;3.0 s&lt;/td&gt;
&lt;td&gt;1.4 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LCP&lt;/td&gt;
&lt;td&gt;3.2 s&lt;/td&gt;
&lt;td&gt;2.0 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CLS&lt;/td&gt;
&lt;td&gt;0.001&lt;/td&gt;
&lt;td&gt;0.023&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Accessibility&lt;/td&gt;
&lt;td&gt;95&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Accessibility went from 95 to 100 after I raised the contrast of the gray footer text from &lt;code&gt;gray-500/600&lt;/code&gt; to &lt;code&gt;gray-400&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would check first next time
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;curl -sI -H "Accept-Encoding: gzip" yoursite&lt;/code&gt; and look for &lt;code&gt;Content-Encoding&lt;/code&gt;. Do this before anything else.&lt;/li&gt;
&lt;li&gt;Grep the built CSS for &lt;code&gt;@import&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Look for entrance animations on the LCP element.&lt;/li&gt;
&lt;li&gt;Self-host fonts before trying to defer them.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you have a Vue SPA with a server-rendered shell, I would love to hear what else you found. The full list of changes is in my git history; ask and I will share the diff.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>vue</category>
      <category>performance</category>
      <category>seo</category>
    </item>
    <item>
      <title>Question for builders: how long did it take your free side project to earn its first euro?

Mine: 103 signups in 5 months, $0 so far. ChatGPT is oddly my best traffic source.
#buildinginpublic</title>
      <dc:creator>Mejzun</dc:creator>
      <pubDate>Tue, 27 Oct 2026 09:07:20 +0000</pubDate>
      <link>https://dev.to/mejzun/question-for-builders-how-long-did-it-take-your-free-side-project-to-earn-its-first-euro-mine-kff</link>
      <guid>https://dev.to/mejzun/question-for-builders-how-long-did-it-take-your-free-side-project-to-earn-its-first-euro-mine-kff</guid>
      <description></description>
      <category>buildinpublic</category>
      <category>discuss</category>
      <category>sideprojects</category>
    </item>
    <item>
      <title>Devlog #2: SEO for 150,000 game pages, and why only ChatGPT recommends my site</title>
      <dc:creator>Mejzun</dc:creator>
      <pubDate>Tue, 27 Oct 2026 08:51:58 +0000</pubDate>
      <link>https://dev.to/mejzun/devlog-2-seo-for-150000-game-pages-and-why-only-chatgpt-recommends-my-site-4ofd</link>
      <guid>https://dev.to/mejzun/devlog-2-seo-for-150000-game-pages-and-why-only-chatgpt-recommends-my-site-4ofd</guid>
      <description>&lt;p&gt;In &lt;a href="https://dev.to/mejzun"&gt;Devlog #1&lt;/a&gt; I wrote about building a backlog tracker for myself. This one is about the part I underestimated: &lt;strong&gt;getting anyone to find it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My biggest problem was never the code. It was "where do I share this so people know it exists?" Reddit banned me for posting, so I stopped fighting it and went the other way: make &lt;a href="https://nxto.app/?utm_source=devto&amp;amp;utm_medium=devlog2" rel="noopener noreferrer"&gt;NXTO&lt;/a&gt; findable on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Every game gets a real page
&lt;/h2&gt;

&lt;p&gt;NXTO is a Vue single-page app, and SPAs are a bad deal for search engines and for AI crawlers, because many of them don't run JavaScript. So every game in the catalog gets its own server-rendered page at &lt;code&gt;/games/&amp;lt;slug&amp;gt;&lt;/code&gt; with the title, genres, tags, playtime and a description in plain HTML.&lt;/p&gt;

&lt;p&gt;Two things made that harder than it sounds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Missing slugs.&lt;/strong&gt; Links built from Steam titles don't always match my slugs. Backlog links now carry the Steam app id and the game page resolves it before guessing from the slug. If a slug is still missing, it redirects to the clearly most popular game whose slug starts with it, instead of a 404.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Thin content.&lt;/strong&gt; 150,000 pages with one sentence each is a good way to be ignored. For popular games without Steam text I pull a short Wikipedia excerpt (first paragraph, 350-650 characters, with the CC BY-SA credit) and fall back to a longer AI-written description. Unique, useful text per page was the whole point.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 2: Tell the search engines when something changes
&lt;/h2&gt;

&lt;p&gt;Google finds pages slowly. Bing supports IndexNow, so every time the catalog changes the server pings it with the changed URLs. Cheap to build, and it's probably why Bing-based products notice me first (more on that below).&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: "Where to buy" without a paywall
&lt;/h2&gt;

&lt;p&gt;Hosting costs money, so game pages now show where a key is cheapest, converted to your currency (guessed from your country, otherwise from the interface language).&lt;/p&gt;

&lt;p&gt;The boring engineering behind it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A nightly job streams a store's product feed (a CSV of hundreds of MB) into SQLite, one row per store, game and currency. Streaming matters, because loading it into memory is not an option on a small server.&lt;/li&gt;
&lt;li&gt;Games are matched by a normalized title. That's imperfect, so not every game gets an offer.&lt;/li&gt;
&lt;li&gt;Clicks go through my own &lt;code&gt;/go/...&lt;/code&gt; redirect, so I can count them and the page itself never contains raw affiliate URLs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some links earn me a small commission. You never pay more.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: What I learned about getting recommended by AI
&lt;/h2&gt;

&lt;p&gt;Right now the only assistant that recommends NXTO is ChatGPT. That isn't random. Different assistants pull from different search indexes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ChatGPT leans on Bing's index (which IndexNow feeds).&lt;/li&gt;
&lt;li&gt;Gemini and Google AI Overviews use Google's.&lt;/li&gt;
&lt;li&gt;Claude uses Brave Search.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Links matter much less than in classic SEO. What matters is being in the index the assistant searches, and being mentioned in places it trusts. So my to-do list is: get into Google Search Console and Brave, keep the pages server-rendered, and collect mentions from people who aren't me. This post is part of that.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Better matching between games and store offers.&lt;/li&gt;
&lt;li&gt;More languages for the game pages.&lt;/li&gt;
&lt;li&gt;Whatever you ask for on the &lt;a href="https://nxto.app/ideas?utm_source=devto&amp;amp;utm_medium=devlog2" rel="noopener noreferrer"&gt;ideas page&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Questions for you:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;If you've launched a side project, where did your first 100 users actually come from?&lt;/li&gt;
&lt;li&gt;Do you check whether AI assistants mention your product? If so, how?&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>devlog</category>
      <category>webdev</category>
      <category>seo</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Devlog #1: I built a backlog tracker because I kept forgetting what to play</title>
      <dc:creator>Mejzun</dc:creator>
      <pubDate>Mon, 19 Oct 2026 17:41:52 +0000</pubDate>
      <link>https://dev.to/mejzun/devlog-1-i-built-a-backlog-tracker-because-i-kept-forgetting-what-to-play-4f3p</link>
      <guid>https://dev.to/mejzun/devlog-1-i-built-a-backlog-tracker-because-i-kept-forgetting-what-to-play-4f3p</guid>
      <description>&lt;p&gt;I kept forgetting which games I planned to play. So I built a simple backlog: what I'm playing, what's next, what I've finished.&lt;/p&gt;

&lt;p&gt;That was the whole idea. Here's how it grew into &lt;a href="https://nxto.app/?utm_source=devto&amp;amp;utm_medium=devlog" rel="noopener noreferrer"&gt;NXTO&lt;/a&gt;, and what I learned on the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: A list that works
&lt;/h2&gt;

&lt;p&gt;The first version did one thing: three lists. Playing, planned, finished. No accounts to connect, no clever features. Just somewhere to put games so I'd stop losing them in notes, wishlists and my own memory.&lt;/p&gt;

&lt;p&gt;It solved the problem. But once I was tracking finished games, a new question showed up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: "What should I play next?"
&lt;/h2&gt;

&lt;p&gt;Having a history of what I'd finished made recommendations the obvious next feature. And I had a specific reason to build my own: I own games on several platforms, and Steam's recommendations can't see what I played elsewhere. Every store only knows its own corner of my library.&lt;/p&gt;

&lt;p&gt;So NXTO recommends based on &lt;em&gt;everything&lt;/em&gt; I've played, with my ratings as the signal. (I wrote up the technical side of that, including why my first version suggested junk, &lt;a href="https://dev.to/mejzun/why-my-game-recommender-kept-suggesting-junk-and-how-idf-weighting-fixed-it-3aep"&gt;here&lt;/a&gt;.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Sharing it
&lt;/h2&gt;

&lt;p&gt;Once I had this working for myself, I wanted to share it. I put it online on a random domain I already owned, nothing polished, just a link I could send to people.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: People signed up
&lt;/h2&gt;

&lt;p&gt;To my surprise, people started registering. That meant it needed a proper home, so I bought nxto.app.&lt;/p&gt;

&lt;p&gt;Hosting costs money, so I looked for a way to cover it without putting up a paywall. I added a Buy Me a Coffee link, and a price comparison for game keys: open a game and NXTO shows where it's cheapest (Eneba, G2A, Instant Gaming, Kinguin), converted to your currency. Some of those links earn me a small commission, and you never pay more.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest numbers
&lt;/h2&gt;

&lt;p&gt;Since the first signup in May:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;79&lt;/strong&gt; accounts, &lt;strong&gt;64&lt;/strong&gt; of them have added games&lt;/li&gt;
&lt;li&gt;about &lt;strong&gt;10,400&lt;/strong&gt; games tracked in total&lt;/li&gt;
&lt;li&gt;a catalog of around &lt;strong&gt;150,000&lt;/strong&gt; games to search from&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'm building this in the open, with a public ideas page where you can tell me what to build next: &lt;a href="https://nxto.app/ideas?utm_source=devto&amp;amp;utm_medium=devlog" rel="noopener noreferrer"&gt;nxto.app/ideas&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you track a backlog, what's the one feature you'd want? And how do you decide what to play next?&lt;/p&gt;

</description>
      <category>devlog</category>
      <category>webdev</category>
      <category>programming</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Nxto story #1: I kept forgetting which games i planned to play. So i build a simple backlog to track what i'm playnig, what i plan to play and what i've finished. #buildinginpublic #indiedev #gamingbacklog</title>
      <dc:creator>Mejzun</dc:creator>
      <pubDate>Mon, 19 Oct 2026 17:23:31 +0000</pubDate>
      <link>https://dev.to/mejzun/nxto-story-1-i-kept-forgetting-which-games-i-planned-to-play-so-i-build-a-simple-backlog-to-4c16</link>
      <guid>https://dev.to/mejzun/nxto-story-1-i-kept-forgetting-which-games-i-planned-to-play-so-i-build-a-simple-backlog-to-4c16</guid>
      <description></description>
    </item>
    <item>
      <title>Why my game recommender kept suggesting junk (and how IDF weighting fixed it)</title>
      <dc:creator>Mejzun</dc:creator>
      <pubDate>Mon, 19 Oct 2026 17:22:12 +0000</pubDate>
      <link>https://dev.to/mejzun/why-my-game-recommender-kept-suggesting-junk-and-how-idf-weighting-fixed-it-3aep</link>
      <guid>https://dev.to/mejzun/why-my-game-recommender-kept-suggesting-junk-and-how-idf-weighting-fixed-it-3aep</guid>
      <description>&lt;p&gt;I play on several platforms, but every store only recommends games based on what I did &lt;em&gt;there&lt;/em&gt;. Steam had no idea what I'd finished elsewhere, so its suggestions kept missing. I wanted one place that knows my whole library, so I built it: &lt;a href="https://nxto.app/?utm_source=devto&amp;amp;utm_medium=article" rel="noopener noreferrer"&gt;NXTO&lt;/a&gt;, a backlog tracker with its own recommendations.&lt;/p&gt;

&lt;p&gt;The stack is boring on purpose: Vue 3, Express, SQLite via &lt;code&gt;better-sqlite3&lt;/code&gt;. Game metadata and tags come from RAWG. The interesting part was making the recommendations not suck.&lt;/p&gt;

&lt;h2&gt;
  
  
  First attempt: Jaccard on tags
&lt;/h2&gt;

&lt;p&gt;The obvious approach: each game has tags, and two games are similar if their tag sets overlap. Jaccard similarity does exactly that.&lt;/p&gt;

&lt;p&gt;It looked fine until I read the results. Games matched on filler tags like &lt;code&gt;2d&lt;/code&gt;, &lt;code&gt;atmospheric&lt;/code&gt; and &lt;code&gt;rpg&lt;/code&gt;, which are shared by thousands of titles. The tags that actually define a game, like &lt;code&gt;farming&lt;/code&gt; or &lt;code&gt;souls-like&lt;/code&gt;, were drowned out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 1: IDF weighting
&lt;/h2&gt;

&lt;p&gt;Borrowing from search: a rare tag should count more than a common one. I weight every tag by inverse document frequency and compare games as vectors with cosine similarity. Two games sharing &lt;code&gt;souls-like&lt;/code&gt; now look far more alike than two games sharing &lt;code&gt;2d&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 2: the long tail of junk tags
&lt;/h2&gt;

&lt;p&gt;IDF created a new problem. The catalog has about 48k distinct tags, and 90% of them occur fewer than 10 times: typos, one-off labels, joke tags. Under plain IDF those rare tags score &lt;em&gt;higher&lt;/em&gt; than any real tag. One tag with a document frequency of 1 got a weight of 8.5 and hijacked the cosine.&lt;/p&gt;

&lt;p&gt;The fix is a minimum document frequency (&lt;code&gt;MIN_DF = 5&lt;/code&gt;). Anything below the floor is dropped from the vocabulary entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 3: ratings are signal, not just "finished"
&lt;/h2&gt;

&lt;p&gt;Finishing a game doesn't mean you liked it. I turn the user's rating into a taste weight:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rating&lt;/th&gt;
&lt;th&gt;Weight&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;5★&lt;/td&gt;
&lt;td&gt;+2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4★&lt;/td&gt;
&lt;td&gt;+1.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3★&lt;/td&gt;
&lt;td&gt;+0.6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2★&lt;/td&gt;
&lt;td&gt;-1.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1★&lt;/td&gt;
&lt;td&gt;-2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;unrated&lt;/td&gt;
&lt;td&gt;+0.8&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;3★ means "fine, not a signal", and 1-2★ actively push the taste vector &lt;em&gt;away&lt;/em&gt; from similar games. A user's profile is the weighted sum of their games' tag vectors.&lt;/p&gt;

&lt;p&gt;I also only rank games with at least 10 ratings. Below that, the catalog is full of unrated jam entries that no ranking can order honestly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caching without serving stale results
&lt;/h2&gt;

&lt;p&gt;Scoring a whole catalog per request is expensive, so I cache the sorted pool per user. The key includes &lt;code&gt;MAX(updated_at)&lt;/code&gt; &lt;em&gt;and&lt;/em&gt; the row count for the user's games. Both are needed. &lt;code&gt;updated_at&lt;/code&gt; has one-second resolution, and deleting a game doesn't move the maximum at all, so a timestamp-only key would keep serving a pool that still contains the game you just removed.&lt;/p&gt;

&lt;p&gt;I also bump an &lt;code&gt;ALGO_VERSION&lt;/code&gt; in the key whenever scoring changes, so a deploy never serves pools built by the old algorithm.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Look at the output early. The tag problem was invisible in the code and obvious in the results.&lt;/li&gt;
&lt;li&gt;Treat the vocabulary as data to clean, not just math to tune.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you track a backlog, I'd love to know what it recommends for you. And if you've built a recommender, what did you do about noisy tags?&lt;/p&gt;

&lt;p&gt;Try it: &lt;a href="https://nxto.app/?utm_source=devto&amp;amp;utm_medium=article" rel="noopener noreferrer"&gt;nxto.app&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>buildinpublic</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>ive made game recommendation app nxto.app, it base on your game backlog and ur rating</title>
      <dc:creator>Mejzun</dc:creator>
      <pubDate>Sat, 03 Oct 2026 11:43:50 +0000</pubDate>
      <link>https://dev.to/mejzun/ive-made-game-recommendation-app-nxtoapp-it-base-on-your-game-backlog-and-ur-rating-3d88</link>
      <guid>https://dev.to/mejzun/ive-made-game-recommendation-app-nxtoapp-it-base-on-your-game-backlog-and-ur-rating-3d88</guid>
      <description></description>
    </item>
  </channel>
</rss>
