<?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: chao xue</title>
    <description>The latest articles on DEV Community by chao xue (@chao_xue_34ee576abba0e8ed).</description>
    <link>https://dev.to/chao_xue_34ee576abba0e8ed</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%2F3845039%2F96d6ede9-434e-4849-b10c-55b09980db83.png</url>
      <title>DEV Community: chao xue</title>
      <link>https://dev.to/chao_xue_34ee576abba0e8ed</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/chao_xue_34ee576abba0e8ed"/>
    <language>en</language>
    <item>
      <title>I built a "should I block you?" friend quiz on Cloudflare Workers — here's what I learned</title>
      <dc:creator>chao xue</dc:creator>
      <pubDate>Wed, 14 Oct 2026 15:41:55 +0000</pubDate>
      <link>https://dev.to/chao_xue_34ee576abba0e8ed/i-built-a-should-i-block-you-friend-quiz-on-cloudflare-workers-heres-what-i-learned-22b0</link>
      <guid>https://dev.to/chao_xue_34ee576abba0e8ed/i-built-a-should-i-block-you-friend-quiz-on-cloudflare-workers-heres-what-i-learned-22b0</guid>
      <description>&lt;p&gt;A few weeks ago I kept seeing the same post on Threads and Instagram: &lt;em&gt;"I'm blocking my fake friends 🚫 should I block you?"&lt;/em&gt; followed by a quiz link. Someone answers ten questions about themselves, shares the link, and friends try to guess the answers. Score low and you're "blocked".&lt;/p&gt;

&lt;p&gt;It's a simple idea, and it spreads well because every player is one tap away from making their own quiz. I wanted to see how far I could take it as a side project, so I built my own version: &lt;strong&gt;&lt;a href="https://blockyoutestme.app" rel="noopener noreferrer"&gt;Block You Test Me&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This post covers the parts that turned out to be more interesting than I expected.&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%2F8g4ri79yg6kzqaajgz9r.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%2F8g4ri79yg6kzqaajgz9r.png" alt="Make a quiz, friends get instant feedback, then the verdict" width="799" height="607"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Next.js 16&lt;/strong&gt; with &lt;strong&gt;OpenNext&lt;/strong&gt; on &lt;strong&gt;Cloudflare Workers&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;D1&lt;/strong&gt; (SQLite) for quizzes and scores, &lt;strong&gt;KV&lt;/strong&gt; for rate limiting&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;next-intl&lt;/strong&gt; for 8 languages&lt;/li&gt;
&lt;li&gt;No accounts, no login. The quiz maker gets a private scoreboard link instead.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything runs in one Worker. There's no separate API server and no image service.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Instant feedback without handing out the answers
&lt;/h2&gt;

&lt;p&gt;Players see whether each answer is right as soon as they tap it. That makes the game much more fun, but it means the browser has to know the right answer before the quiz is submitted.&lt;/p&gt;

&lt;p&gt;I didn't want the answers sitting in the JSON as plain values, so each question ships two choices plus a short hash for each one:&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="c1"&gt;// server&lt;/span&gt;
&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;isCorrect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;sha256&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;quizId&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;questionId&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;choice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&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="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;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;sha256&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;quizId&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;questionId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:x:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;k&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="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;8&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// decoy&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The client hashes &lt;code&gt;quizId:questionId:index&lt;/code&gt; with Web Crypto and compares. Someone with DevTools open can still work it out, and that's fine for a party game. The score on the scoreboard is always calculated again on the server, so nobody can post a fake 10/10.&lt;/p&gt;

&lt;p&gt;The wrong choice is picked from the creator's other options using the quiz ID as a seed. Every player gets the same pair of options, so scores are comparable.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Dynamic OG images (and when not to use them)
&lt;/h2&gt;

&lt;p&gt;The share preview is what people see in WhatsApp before they tap, so each quiz gets its own image: &lt;em&gt;"Will Mia block you?"&lt;/em&gt;. It's rendered with &lt;code&gt;next/og&lt;/code&gt; inside the Worker.&lt;/p&gt;

&lt;p&gt;Two things I didn't expect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Non-Latin names.&lt;/strong&gt; The default font only covers Latin characters. An Arabic or emoji name made &lt;code&gt;next/og&lt;/code&gt; fetch glyphs at runtime, which added 1–2 seconds, and Arabic came out reversed. Now names outside Latin and Latin-1 get a redirect to a static image. That covers most of my users, and the edge cases fail gracefully.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bundle size.&lt;/strong&gt; Adding OG generation took the Worker from about 1.2 MB to about 2.0 MB gzipped. That still fit, but it became the theme of the whole project. More on that below.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. Story cards with a QR code
&lt;/h2&gt;

&lt;p&gt;Most of the similar quiz sites I looked at had no Story image to save, and none of them had a QR code, so I added two:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an &lt;strong&gt;invite card&lt;/strong&gt; for the quiz maker ("Will Mia block you?" + QR code)&lt;/li&gt;
&lt;li&gt;a &lt;strong&gt;result card&lt;/strong&gt; for players ("Mia will NEVER block me 💖 9/10")&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They're drawn on a 1080×1920 &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt; in the browser, with &lt;code&gt;qrcode-generator&lt;/code&gt; (MIT, no dependencies) for the QR code. On mobile I try &lt;code&gt;navigator.share({ files })&lt;/code&gt; first. Instagram and WhatsApp's in-app browsers often can't download files, so the fallback is a full-screen preview with a "long-press to save" hint.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Sound effects without audio files
&lt;/h2&gt;

&lt;p&gt;Every tap makes a small sound. There are no audio files: everything is synthesized with the Web Audio API, a few oscillators and gain envelopes. The whole sound module is about 120 lines.&lt;/p&gt;

&lt;p&gt;The one rule that matters: only create the &lt;code&gt;AudioContext&lt;/code&gt; inside a user gesture. My first version replayed the result fanfare when someone revisited a finished quiz, which counts as autoplay. Now results that are loaded from &lt;code&gt;localStorage&lt;/code&gt; render quietly.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Eight languages, chosen by the data
&lt;/h2&gt;

&lt;p&gt;I launched in English, then added Malay, Indonesian, Filipino, Spanish and Portuguese based on where the original trend came from.&lt;/p&gt;

&lt;p&gt;Then I looked at Google Analytics. In the first three days the site had about 600 visitors, and &lt;strong&gt;more than half of them had Turkish browsers&lt;/strong&gt;, mostly arriving from Google search. I hadn't planned for Turkish at all, so I added Turkish and Vietnamese the next day.&lt;/p&gt;

&lt;p&gt;A few i18n details that helped:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;English URLs have no prefix, and the other languages do (&lt;code&gt;/tr/create&lt;/code&gt;). Existing links never changed.&lt;/li&gt;
&lt;li&gt;Every page has hreflang links to every other language plus &lt;code&gt;x-default&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;No automatic redirects based on &lt;code&gt;Accept-Language&lt;/code&gt;. Visitors get a small "View this page in Türkçe" bar instead. Azerbaijani browsers are also offered Turkish, since the two languages are close.&lt;/li&gt;
&lt;li&gt;Quiz IDs and answers don't depend on language. A quiz made in Turkish can be played in English with the same options and the same scoring.&lt;/li&gt;
&lt;/ul&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%2Fr08l5wokl6o859s5zd6e.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%2Fr08l5wokl6o859s5zd6e.png" alt="Chat reveal before the score, and the Turkish version of the home page" width="800" height="900"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Staying under the 3 MB Worker limit
&lt;/h2&gt;

&lt;p&gt;The Workers free plan allows 3 MB (gzipped). Each language added about 70 KB, and the last feature, about 60 pop-culture questions split by region, pushed the bundle to 2.96 MB.&lt;/p&gt;

&lt;p&gt;The fix turned out to be one line of code. I had merged two translation tables with object spread:&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="c1"&gt;// before: the bundler can't drop the unused parts&lt;/span&gt;
&lt;span class="nx"&gt;QUESTIONS_I18N&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;trQuestions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;trPopQuestions&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That made every translation reachable from the Worker entry, so nothing could be tree-shaken. Keeping the tables separate and picking one at lookup time saved 60 KB:&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;table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;p&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;POP_I18N&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;QUESTIONS_I18N&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My takeaway: read the dry-run output before every deploy (&lt;code&gt;wrangler deploy --dry-run&lt;/code&gt; prints the gzipped size).&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Things I'd do the same way again
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Make the share flow the main feature.&lt;/strong&gt; Players finish 85% of the quizzes they start, and the "now it's your turn" button is where new quiz makers come from.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep data small.&lt;/strong&gt; I only store a nickname, answers and scores. Quizzes with no plays for 90 days are deleted by a cron trigger.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write the questions yourself.&lt;/strong&gt; Original, gender-neutral questions with emoji options read better than copied ones, and you can translate them cleanly.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Make a quiz, send it to a friend, and see who gets blocked: &lt;strong&gt;&lt;a href="https://blockyoutestme.app" rel="noopener noreferrer"&gt;blockyoutestme.app&lt;/a&gt;&lt;/strong&gt;. There are also a &lt;a href="https://blockyoutestme.app/best-friend-test" rel="noopener noreferrer"&gt;Best Friend Test&lt;/a&gt;, a &lt;a href="https://blockyoutestme.app/couple-test" rel="noopener noreferrer"&gt;Couple Test&lt;/a&gt; and a &lt;a href="https://blockyoutestme.app/crush-test" rel="noopener noreferrer"&gt;Crush Test&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you've built something viral on Workers, I'd like to hear how you handled bundle size and OG images. Questions are welcome in the comments.&lt;/p&gt;

&lt;p&gt;I hope you like this tiny game.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>webdev</category>
      <category>cloudflare</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>I built an AI face rating app that shows its work: MediaPipe in the browser, an LLM on the server</title>
      <dc:creator>chao xue</dc:creator>
      <pubDate>Wed, 14 Oct 2026 15:17:29 +0000</pubDate>
      <link>https://dev.to/chao_xue_34ee576abba0e8ed/i-built-an-ai-face-rating-app-that-shows-its-work-mediapipe-in-the-browser-an-llm-on-the-server-5a52</link>
      <guid>https://dev.to/chao_xue_34ee576abba0e8ed/i-built-an-ai-face-rating-app-that-shows-its-work-mediapipe-in-the-browser-an-llm-on-the-server-5a52</guid>
      <description>&lt;p&gt;A lot of young people want to know how attractive they are, so they ask Gemini or ChatGPT to "rate my face from 1 to 10". Ask twice and you often get two different numbers, and neither answer shows how the number was reached. So I built a more methodical rating tool, where the score comes with evidence you can check yourself.&lt;/p&gt;

&lt;p&gt;The result is &lt;a href="https://pslrating.me" rel="noopener noreferrer"&gt;PSL Rating&lt;/a&gt;, a small face analysis app. You upload one selfie and get a tier on the PSL scale, a 1–10 score, and measurement lines drawn on your photo.&lt;/p&gt;

&lt;h3&gt;
  
  
  Measure first, then judge
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Measurements run in the browser.&lt;/strong&gt; I use MediaPipe's Face Landmarker (&lt;code&gt;@mediapipe/tasks-vision&lt;/code&gt;), which returns 478 landmarks per face, including iris centers. From those points the client computes facial thirds, eye spacing, canthal tilt, jaw width and symmetry, then draws the lines on a canvas over the photo. None of this needs a server, and it's deterministic: the same photo always gives the same lines.&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%2F7eex9a243m9qgxlqugye.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%2F7eex9a243m9qgxlqugye.png" alt="Measurement lines drawn in the browser on a sample AI-generated face" width="800" height="479"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The rating comes from an LLM with a carefully tuned prompt.&lt;/strong&gt; The photo, a text summary of the measurements and a fixed rubric go to a vision model (Gemini, through a third-party API). The model returns structured JSON, which I validate with Zod before anything reaches the UI. If a field is missing or out of range, that section is dropped instead of invalidating the whole report.&lt;/p&gt;

&lt;p&gt;The prompt matters a lot. When the model sees "eye spacing ratio 1.04, canthal tilt +4.1°" next to the image, its comments stay much closer to what the overlay actually shows.&lt;/p&gt;

&lt;h3&gt;
  
  
  The canthal tilt bug I almost shipped
&lt;/h3&gt;

&lt;p&gt;Canthal tilt comes up constantly in face rating discussions. It's the angle of the line from the inner corner of the eye to the outer corner. The most direct way to compute it is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Straighten the face so the eyes are level.&lt;/li&gt;
&lt;li&gt;Measure the angle of each eye.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;But that's wrong. If you level the face using the eye line, you rotate away the exact tilt you're trying to measure, and everyone comes out near 0°.&lt;/p&gt;

&lt;p&gt;The fix is to correct head roll with a line that doesn't go through the eyes. I use the facial midline, from the top of the forehead (landmark 10) to the chin (landmark 152):&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="c1"&gt;// Roll of the face itself: how far the midline (forehead -&amp;gt; chin) leans from vertical.&lt;/span&gt;
&lt;span class="c1"&gt;// Don't use the eye line here, or you cancel out the real canthal tilt.&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;rollRad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;px&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Pt&lt;/span&gt;&lt;span class="p"&gt;[]):&lt;/span&gt; &lt;span class="kr"&gt;number&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;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;px&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;LM&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FOREHEAD_TOP&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="c1"&gt;// 10&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="nx"&gt;px&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;LM&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CHIN&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;         &lt;span class="c1"&gt;// 152&lt;/span&gt;
  &lt;span class="k"&gt;return&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;atan2&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;x&lt;/span&gt; &lt;span class="o"&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;x&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;y&lt;/span&gt; &lt;span class="o"&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;y&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;canthalTilt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;P&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Pt&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// P = pixel landmarks already rotated so the midline is vertical&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tilt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;outer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&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;dx&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;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;P&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;outer&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="nx"&gt;P&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;inner&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="c1"&gt;// y grows downward, so inner.y - outer.y &amp;gt; 0 means the outer corner is higher&lt;/span&gt;
    &lt;span class="k"&gt;return&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;atan2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;P&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;inner&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="nx"&gt;P&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;outer&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;180&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="nx"&gt;PI&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;right&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tilt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;133&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;33&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;left&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tilt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;362&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;263&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;right&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;avg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;left&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;right&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&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 more things to watch out for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Convert normalized coordinates to pixels before measuring angles.&lt;/strong&gt; MediaPipe returns x and y in 0–1, scaled by width and height separately. On a non-square image, angles computed on normalized coordinates are skewed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Corner landmarks are noisy.&lt;/strong&gt; They wobble by a pixel or two between frames. On a small image that's about ±1°, so the UI rounds and labels small values as "neutral" instead of pretending 0.6° means something.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you just want to try this part, there's a standalone &lt;a href="https://pslrating.me/canthal-tilt-calculator" rel="noopener noreferrer"&gt;canthal tilt calculator&lt;/a&gt; that runs entirely in the browser.&lt;/p&gt;

&lt;h3&gt;
  
  
  Photo quality moves the score more than the face does
&lt;/h3&gt;

&lt;p&gt;Early testing showed the same person getting very different results from different selfies. The cause was the photo angle: the camera below eye level, a wide-angle lens too close to the face, overhead lighting, a tilted head.&lt;/p&gt;

&lt;p&gt;So before rating, the client runs a quick quality check (face size in the frame, head turn and tilt estimated from landmarks, brightness), and the AI also returns a short "photo check" with up to three issues and three retake steps. If the verdict is "retake", the result page says so at the top. That one card probably did more for fairness than any prompt change.&lt;/p&gt;

&lt;p&gt;For the live camera, I first built an auto-capture countdown. It didn't work well: the damn countdown kept restarting. Now it's a large oval guide plus a shutter button the user presses themselves.&lt;/p&gt;

&lt;h3&gt;
  
  
  Privacy as an architecture decision
&lt;/h3&gt;

&lt;p&gt;Face photos are sensitive, so the design principle is to not keep them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The photo is sent for the rating request and never written to storage. The database keeps the report's text and numbers only.&lt;/li&gt;
&lt;li&gt;The full report's AI style previews (for example the same face with a different haircut, one change per image) are kept in the user's own browser via IndexedDB for 30 days and cleared on logout.&lt;/li&gt;
&lt;li&gt;It's 18+ only. Every photo gets an AI age check, and generated previews go through a moderation pass before they're shown. Of course the model gets it wrong sometimes. When I tested with a photo of my cousin, it decided she was under 18. She didn't seem too upset about it.&lt;/li&gt;
&lt;/ul&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%2F7z0muouiblnfqwa9p57y.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%2F7z0muouiblnfqwa9p57y.png" alt="AI style previews, one change per image" width="800" height="479"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The trade-off is that a user who clears their browser loses the preview images. For this kind of data, I think that's the right call.&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%2F5rjm6y3w2eljcwlbjswf.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%2F5rjm6y3w2eljcwlbjswf.png" alt="Privacy and safety summary" width="800" height="479"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Stack
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Next.js 16 deployed to Cloudflare Workers with OpenNext&lt;/li&gt;
&lt;li&gt;D1 (SQLite) for users, credits and reports; KV for small cached values&lt;/li&gt;
&lt;li&gt;Google sign-in, one free rating per account, then paid reports&lt;/li&gt;
&lt;li&gt;Vitest for tests, with an in-memory store and a &lt;code&gt;node:sqlite&lt;/code&gt; store so the D1 SQL runs in CI&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;I'd write the measurement code with pixel coordinates and roll correction from the start, with unit tests on synthetic landmark sets. Most of my early bugs were coordinate-system bugs.&lt;/li&gt;
&lt;li&gt;I'd build the photo check before tuning prompts. Bad input made the prompt work look worse than it was.&lt;/li&gt;
&lt;li&gt;I'd be more careful with wording. A face score can easily read as a judgment of someone's worth. The app calls ratings AI estimates for fun and self-improvement, and every suggestion is tagged by effort (today, weeks, months, structural) so people can see what's realistic to change.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've built something with MediaPipe or vision LLMs, I'd love to hear how you handled noisy landmarks or inconsistent model output. Feedback on the app is welcome too: &lt;a href="https://pslrating.me" rel="noopener noreferrer"&gt;pslrating.me&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>nextjs</category>
      <category>cloudflare</category>
    </item>
    <item>
      <title>I built a free bedroom feng shui checker that doesn’t give generic advice</title>
      <dc:creator>chao xue</dc:creator>
      <pubDate>Sat, 05 Sep 2026 08:57:50 +0000</pubDate>
      <link>https://dev.to/chao_xue_34ee576abba0e8ed/i-built-a-free-bedroom-feng-shui-checker-that-doesnt-give-generic-advice-17ig</link>
      <guid>https://dev.to/chao_xue_34ee576abba0e8ed/i-built-a-free-bedroom-feng-shui-checker-that-doesnt-give-generic-advice-17ig</guid>
      <description>&lt;p&gt;When I first tried to arrange my bedroom according to feng shui, I quickly discovered how vague the rules can be. “Put your bed in the commanding position,” every article said. But what counts as commanding when the door is beside a sloping ceiling and the only wall wide enough for a bed also contains a window?&lt;/p&gt;

&lt;p&gt;I wanted a tool that could look at an actual room—not an ideal square diagram—and tell me whether the bed, mirror, desk, and windows conflicted with each other. After reading too many conflicting guides, I built a small JavaScript app that lets you draw your floor plan and checks it against the most common feng shui bedroom principles right in the browser. It is now live as &lt;a href="https://fengshuibedroomdesign.com" rel="noopener noreferrer"&gt;a free feng shui bedroom layout checker&lt;/a&gt; with illustrated guides, and this post explains the geometry and product thinking behind it.&lt;/p&gt;

&lt;h2&gt;Why generic answers are useless for real rooms&lt;/h2&gt;

&lt;p&gt;Feng shui bedroom advice usually takes the form of “the bed should not face the door” or “do not put a mirror where it reflects the bed.” Those rules assume a rectangular room with clear layouts. Actual bedrooms have angled walls, two doors, sliding doors, radiators, or shared-wall windows. A rule like “the bed should not be in line with the door” depends entirely on where the door and bed are, so an abstract rule cannot describe a room.&lt;/p&gt;

&lt;p&gt;That signaled a great software problem. Instead of writing a list of static tips, I could create a simple geometric model of the room and let the model answer questions. If the user draws the walls and places a bed, the code can calculate relative positions. The tool checks what actually applies to that floor plan. This approach also avoids superstition-heavy claims: the interface only highlights traditional concerns and lets the user decide whether it matters to them.&lt;/p&gt;

&lt;h2&gt;Sketching a room with canvas&lt;/h2&gt;

&lt;p&gt;To make the tool mobile-friendly and reduce friction, I used the browser's canvas with pointer events. Users draw their room by clicking or tapping corners to create walls. Unlike a general room-planner, it intentionally keeps the tool simple: walls, door, window, and furniture are the only object types needed for a feng shui assessment.&lt;/p&gt;

&lt;p&gt;Each object is stored as a set of coordinates. Walls are polygons; doors and windows become line segments placed on walls; the bed, mirror, desk, and screen are rectangles with a front edge so the code can tell which way the user would sleep or look. The drawing layer may feel like a rudimentary CAD, but it produces enough information to run a useful rule engine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a door is a segment with an opening direction&lt;/li&gt;
&lt;li&gt;a window is a segment with a normal side pointing outside&lt;/li&gt;
&lt;li&gt;a bed stores its orientation and the side where the headboard sits&lt;/li&gt;
&lt;li&gt;a mirror is represented by the wall or object it is mounted on&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I deliberately skipped straight lines and perfect right angles because real rooms are rarely perfect. For every placement, the code compares vector angles and distances instead of assuming a grid. The result is a lightweight sketch tool where the visual output is not a polished render but a meaningful map for the checks that follow.&lt;/p&gt;

&lt;h2&gt;Commanding position as a vector calculation&lt;/h2&gt;

&lt;p&gt;The central feng shui concept in the bedroom is the commanding position: the bed should have a clear view of the door, but not be straight in line with it. In a normal room, the most commanding bed position is diagonal from the door, often with an angle between 30 and 60 degrees from the line between the door and the bed.&lt;/p&gt;

&lt;p&gt;Implementing that rule requires three pieces of information:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the center of the bed&lt;/li&gt;
&lt;li&gt;the center of the door opening&lt;/li&gt;
&lt;li&gt;the bed's headboard direction&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With those coordinates, the app calculates whether the door is visible from a seated or lying position, and whether the headboard points toward the door. It flags the bed if the door is behind the headboard line, or if opening the door would hit the bed frame. It also checks whether the body position aligns directly with the door; if that happens, the layout gets a warning.&lt;/p&gt;

&lt;p&gt;This is where the tool becomes more than a checklist. A bed that points toward the door on one side of a room is problematic under the traditional rule, but the same bed angle in a bigger room may be perfectly acceptable because an extra wall adds protection. A rules engine that understands room shape can see that distinction automatically.&lt;/p&gt;

&lt;h2&gt;What about mirrors and windows?&lt;/h2&gt;

&lt;p&gt;Mirrors have boggled many readers. Some traditions say they should never reflect the bed; others focus on not having a mirror directly behind a headboard. In the app, a mirror object stores its surface normal. The checker can then determine whether the mirror's reflected view can reach the bed.&lt;/p&gt;

&lt;p&gt;The implementation is not a naïve “rectangle intersection.” It finds the closest reflecting rectangle that covers the mirror, computes the line from the bed's front edge to the mirror center, reflects it across the mirror plane, and checks whether the reflected ray intersects another object. This gives a believable answer for an oddly-sized mirror placed on a closet door or side wall. If the result is false, the tool suggests simpler fixes: move the mirror, cover it at night, or place a screen between the mirror and the bed.&lt;/p&gt;

&lt;p&gt;Windows go through a separate check. A window above the headboard creates a draft and adds outside light in many homes. The tool flags this not as a taboo, but as a practical concern, using the bed's headboard side, the window segment, and the wall width. It will also warn if a desk faces a window and the user is working at night because screen glare is a more direct problem than any mystical principle.&lt;/p&gt;

&lt;h2&gt;Rules, not magic&lt;/h2&gt;

&lt;p&gt;One of the design decisions I am happiest about is keeping the language grounded. The long descriptions deliberately say “traditional preference” or “practical safety” instead of promising wealth or luck. That matters because a developer-made tool should earn trust through clarity. The illustrated guides also show floor-plan diagrams rather than abstract quotes, so a user can see why a change is recommended.&lt;/p&gt;

&lt;p&gt;The feng shui labels are just culturally learned heuristics for comfort, light, direction, and room flow. Behind them are useful questions I can phrase to a browser: Is this spot visible from the door? Does a mirror reflect a sleeping person? Is there enough clearance on both sides of the bed? Developers can rarely agree on what “comfortable” means, but we can agree on vector distances. Framing features that way helped me build a tool that feels actionable instead of mystical.&lt;/p&gt;

&lt;h2&gt;What I learned from shipping it&lt;/h2&gt;

&lt;p&gt;Building the checker taught me that a niche browser tool can be valuable if it gives people a feeling of control. Users do not need a room design service; they need a quick answer with a visual explanation. The free tool at the start of this post includes all of this logic and more, works on mobile, and requires no signup. You can draw your actual room in under a minute and experiment with moving the bed around.&lt;/p&gt;

&lt;p&gt;If you have ever struggled with a bedroom that never feels right—and if you know enough JavaScript to be curious how “feng shui” can be represented as coordinates—I hope this gives you an idea for your next weekend project: the best tools are not the ones with endless options, but the ones that tell you what is wrong with the layout you already have.&lt;/p&gt;

</description>
      <category>buildinpublic</category>
      <category>javascript</category>
      <category>showdev</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The Power of Simple Tools: Why Sometimes All You Need is a Counter</title>
      <dc:creator>chao xue</dc:creator>
      <pubDate>Thu, 26 Mar 2026 17:00:54 +0000</pubDate>
      <link>https://dev.to/chao_xue_34ee576abba0e8ed/the-power-of-simple-tools-why-sometimes-all-you-need-is-a-counter-2f3j</link>
      <guid>https://dev.to/chao_xue_34ee576abba0e8ed/the-power-of-simple-tools-why-sometimes-all-you-need-is-a-counter-2f3j</guid>
      <description>&lt;p&gt;As developers and tech enthusiasts, we often get caught up in the "feature race." We build complex systems, integrate AI into everything, and obsess over syncing data across every possible cloud. But over time, I’ve realized that the tools people actually stick with are often the ones that do exactly one thing—and do it perfectly.&lt;/p&gt;

&lt;p&gt;Think about the small, repetitive tasks that pop up throughout the day: counting inventory in a warehouse, tracking sets at the gym, or even just tallying attendance at a local meetup. These aren't massive engineering problems, but they are frequent ones. When you’re in the middle of a task, the last thing you want is to wait for a bloated app to load, sit through a 5-second ad, or—worst of all—be forced to "Create an Account" just to start counting.&lt;/p&gt;

&lt;p&gt;This is why lightweight, browser-based utilities still feel like magic in 2026. They are "zero-friction." You open a tab, you do the work, and you close it. No strings attached.&lt;/p&gt;

&lt;p&gt;Lately, I’ve been focusing on this philosophy of "minimalist utility." I wanted to see how much I could strip away while still making a tool incredibly useful. The result is onlinecounter.org.&lt;/p&gt;

&lt;p&gt;In building it, I focused on two specific pain points I noticed in other tools:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mobile-First Interaction&lt;/strong&gt;: Most web counters feel clunky on a phone. I wanted something that felt like a native app—responsive, snappy, and easy to tap with one hand while you're on the move.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The "Copy" Workflow&lt;/strong&gt;: Usually, after you finish counting, you need that number somewhere else—a spreadsheet, a text message, or a note. Instead of making users manually type it out, I added a "one-click copy" feature. It’s a small detail, but it saves a massive amount of hassle.&lt;/p&gt;

&lt;p&gt;I also made a conscious choice to skip "cloud sync." By keeping the data strictly in your local browser, the site stays lightning-fast and respects your privacy. No data leaves your device; it’s just you and your tally.&lt;/p&gt;

&lt;p&gt;At the end of the day, not every problem needs a "platform." Sometimes, the most effective tools are the ones that quietly do their job, stay out of your way, and let you get back to what really matters.&lt;/p&gt;

&lt;p&gt;If you’re looking for a straightforward, ad-free way to keep track of... well, anything... feel free to give it a try:&lt;br&gt;
&lt;strong&gt;&lt;a href="https://onlinecounter.org/" rel="noopener noreferrer"&gt;https://onlinecounter.org/&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Because sometimes, less really is more.&lt;/p&gt;

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

</description>
      <category>productivity</category>
      <category>softwaredevelopment</category>
      <category>tooling</category>
      <category>ux</category>
    </item>
  </channel>
</rss>
