<?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: Cristina Rodriguez</title>
    <description>The latest articles on DEV Community by Cristina Rodriguez (@yosolita1978).</description>
    <link>https://dev.to/yosolita1978</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F569284%2F4cb7d3ec-e243-4a1f-9624-335dfab6ee6a.jpeg</url>
      <title>DEV Community: Cristina Rodriguez</title>
      <link>https://dev.to/yosolita1978</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yosolita1978"/>
    <language>en</language>
    <item>
      <title>Have you used GitHub Actions as a cron job yet? I didn’t need AI for this project—I needed something that runs without me remembering. One YAML file, one cron line, and my weekly “2-minute task” disappeared.</title>
      <dc:creator>Cristina Rodriguez</dc:creator>
      <pubDate>Tue, 03 Feb 2026 20:20:46 +0000</pubDate>
      <link>https://dev.to/yosolita1978/have-you-used-github-actions-as-a-cron-job-yet-i-didnt-need-ai-for-this-project-i-needed-3nkd</link>
      <guid>https://dev.to/yosolita1978/have-you-used-github-actions-as-a-cron-job-yet-i-didnt-need-ai-for-this-project-i-needed-3nkd</guid>
      <description>&lt;p&gt;

&lt;/p&gt;
&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/yosolita1978/i-didnt-need-ai-for-this-project-i-needed-a-cron-job-so-i-used-github-actions-52o9" class="crayons-story__hidden-navigation-link"&gt;I Didn’t Need AI for This Project. I Needed a Cron Job (So I Used GitHub Actions)&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/yosolita1978" class="crayons-avatar  crayons-avatar--l  "&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%2Fuser%2Fprofile_image%2F569284%2F4cb7d3ec-e243-4a1f-9624-335dfab6ee6a.jpeg" alt="yosolita1978 profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/yosolita1978" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Cristina Rodriguez
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Cristina Rodriguez
                
              
              &lt;div id="story-author-preview-content-3227868" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/yosolita1978" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&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%2Fuser%2Fprofile_image%2F569284%2F4cb7d3ec-e243-4a1f-9624-335dfab6ee6a.jpeg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Cristina Rodriguez&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/yosolita1978/i-didnt-need-ai-for-this-project-i-needed-a-cron-job-so-i-used-github-actions-52o9" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Feb 3&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/yosolita1978/i-didnt-need-ai-for-this-project-i-needed-a-cron-job-so-i-used-github-actions-52o9" id="article-link-3227868"&gt;
          I Didn’t Need AI for This Project. I Needed a Cron Job (So I Used GitHub Actions)
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/automation"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;automation&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/devops"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;devops&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/github"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;github&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/tutorial"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;tutorial&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
            &lt;a href="https://dev.to/yosolita1978/i-didnt-need-ai-for-this-project-i-needed-a-cron-job-so-i-used-github-actions-52o9#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              Comments


              &lt;span class="hidden s:inline"&gt;Add Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            3 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;




</description>
      <category>automation</category>
      <category>devops</category>
      <category>github</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I Didn’t Need AI for This Project. I Needed a Cron Job (So I Used GitHub Actions)</title>
      <dc:creator>Cristina Rodriguez</dc:creator>
      <pubDate>Tue, 03 Feb 2026 20:19:29 +0000</pubDate>
      <link>https://dev.to/yosolita1978/i-didnt-need-ai-for-this-project-i-needed-a-cron-job-so-i-used-github-actions-52o9</link>
      <guid>https://dev.to/yosolita1978/i-didnt-need-ai-for-this-project-i-needed-a-cron-job-so-i-used-github-actions-52o9</guid>
      <description>&lt;p&gt;If you need something to run &lt;strong&gt;every week&lt;/strong&gt; and you’re tired of being the scheduler…&lt;/p&gt;

&lt;p&gt;Use &lt;strong&gt;GitHub Actions&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You’ll create one YAML file. Add a cron line. Push. Done.&lt;/p&gt;

&lt;p&gt;No server. No “remember to run it.” Just automation.&lt;/p&gt;

&lt;h2&gt;
  
  
  ✅ The Fix: a scheduled workflow (copy/paste)
&lt;/h2&gt;

&lt;p&gt;Create this file in your repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.github/workflows/cron.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Paste this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Weekly automation&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Every Monday at 09:00 UTC (cron runs in UTC)&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;9&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;1"&lt;/span&gt;

  &lt;span class="c1"&gt;# Bonus: run it manually from the Actions tab&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;run-task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout repo&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run a command&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;echo "Hello from a scheduled workflow 👋"&lt;/span&gt;
          &lt;span class="s"&gt;# Replace this with your real script/command&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s the core idea: &lt;strong&gt;cron triggers a workflow&lt;/strong&gt;, the workflow runs your steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Apply it: run a real script (Node example)
&lt;/h2&gt;

&lt;p&gt;I like keeping the logic outside YAML, so my workflow stays tiny.&lt;/p&gt;

&lt;p&gt;Repo layout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;scripts/
  weekly-task.js
.github/
  workflows/
    cron.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Update the workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Weekly automation&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;9&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;1"&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;run-task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout repo&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Setup Node&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;20"&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install deps&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run script&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node scripts/weekly-task.js&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why this works
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;schedule&lt;/code&gt; runs on a timer (cron).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;workflow_dispatch&lt;/code&gt; gives you a “Run workflow” button for testing.&lt;/li&gt;
&lt;li&gt;The runner checks out your code and executes your commands in a clean environment.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Cron in 20 seconds (so you don’t surprise yourself)
&lt;/h2&gt;

&lt;p&gt;Cron format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;minute hour day-of-month month day-of-week
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Daily at 09:00 UTC → &lt;code&gt;0 9 * * *&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Mondays at 09:00 UTC → &lt;code&gt;0 9 * * 1&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Every 15 minutes → &lt;code&gt;*/15 * * * *&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One important detail: &lt;strong&gt;cron is UTC&lt;/strong&gt; in GitHub Actions.&lt;/p&gt;

&lt;p&gt;So if you care about “9am my time,” you’ll need to convert it (and DST will mess with you a little). My solution: choose a time where an hour shift won’t ruin your life.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tiny but important: secrets don’t go in YAML
&lt;/h2&gt;

&lt;p&gt;Even if your script is simple today, it’ll eventually call &lt;em&gt;something&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Don’t hardcode tokens. Use repo secrets:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Repo → &lt;strong&gt;Settings&lt;/strong&gt; → &lt;strong&gt;Secrets and variables&lt;/strong&gt; → &lt;strong&gt;Actions&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;API_TOKEN&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run script&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;API_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.API_TOKEN }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node scripts/weekly-task.js&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Make failures obvious (future-you will thank you)
&lt;/h2&gt;

&lt;p&gt;Silent failures are the worst kind of “automation.”&lt;/p&gt;

&lt;p&gt;This tiny pattern helps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run and fail loudly&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;set -e&lt;/span&gt;
          &lt;span class="s"&gt;node scripts/weekly-task.js&lt;/span&gt;
          &lt;span class="s"&gt;echo "✅ Job finished"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it errors, the workflow run turns red and you’ll see it in the Actions tab.&lt;/p&gt;

&lt;h2&gt;
  
  
  Now the story (because yes, this is why I did it)
&lt;/h2&gt;

&lt;p&gt;I love AI tools.&lt;/p&gt;

&lt;p&gt;I really do.&lt;/p&gt;

&lt;p&gt;But this time the project didn’t need intelligence. It needed consistency.&lt;/p&gt;

&lt;p&gt;I had this tiny task that took &lt;em&gt;maybe&lt;/em&gt; 2–5 minutes. Nothing dramatic. Just repetitive. The kind of thing you can totally do manually…&lt;/p&gt;

&lt;p&gt;…until you forget once.&lt;/p&gt;

&lt;p&gt;Then twice.&lt;/p&gt;

&lt;p&gt;Then it becomes that background guilt you carry around like a cursed keychain.&lt;/p&gt;

&lt;p&gt;I almost did the classic moves:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“I’ll set a reminder” (lol)&lt;/li&gt;
&lt;li&gt;“I’ll run cron locally” (also valid, but now my laptop is part of production?)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What I wanted was: &lt;strong&gt;no server, no extra service, no babysitting.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So I did the boring thing that works: a scheduled GitHub Action.&lt;/p&gt;

&lt;p&gt;And it’s honestly the best kind of automation: the kind you forget exists because it just keeps showing up on time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;If you only take a few things from this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create &lt;code&gt;.github/workflows/cron.yml&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;on.schedule.cron&lt;/code&gt; for the timer&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;workflow_dispatch&lt;/code&gt; so you can test manually&lt;/li&gt;
&lt;li&gt;Keep logic in a script, keep YAML simple&lt;/li&gt;
&lt;li&gt;Put tokens in &lt;code&gt;secrets&lt;/code&gt;, not in the file&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Have you used GitHub Actions for cron stuff yet—and what’s the first “2-minute task” you’d love to delete from your life?&lt;/p&gt;

</description>
      <category>automation</category>
      <category>devops</category>
      <category>github</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>I always thought making videos meant fighting a timeline UI.
Turns out you can build an MP4 with React components using Remotion — scenes are components, timing is &lt;Sequence /&gt;, and animations are just useCurrentFrame() + math.</title>
      <dc:creator>Cristina Rodriguez</dc:creator>
      <pubDate>Thu, 22 Jan 2026 06:09:03 +0000</pubDate>
      <link>https://dev.to/yosolita1978/i-always-thought-making-videos-meant-fighting-a-timeline-ui-turns-out-you-can-build-an-mp4-with-4ee7</link>
      <guid>https://dev.to/yosolita1978/i-always-thought-making-videos-meant-fighting-a-timeline-ui-turns-out-you-can-build-an-mp4-with-4ee7</guid>
      <description>&lt;p&gt;

&lt;/p&gt;
&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/yosolita1978/videos-in-react-yep-each-scene-is-a-component-and-it-costs-0-22ec" class="crayons-story__hidden-navigation-link"&gt;Videos in React?! Yep. Each scene is a component (and it costs $0)&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/yosolita1978" class="crayons-avatar  crayons-avatar--l  "&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%2Fuser%2Fprofile_image%2F569284%2F4cb7d3ec-e243-4a1f-9624-335dfab6ee6a.jpeg" alt="yosolita1978 profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/yosolita1978" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Cristina Rodriguez
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Cristina Rodriguez
                
              
              &lt;div id="story-author-preview-content-3189729" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/yosolita1978" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&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%2Fuser%2Fprofile_image%2F569284%2F4cb7d3ec-e243-4a1f-9624-335dfab6ee6a.jpeg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Cristina Rodriguez&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/yosolita1978/videos-in-react-yep-each-scene-is-a-component-and-it-costs-0-22ec" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Jan 22&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/yosolita1978/videos-in-react-yep-each-scene-is-a-component-and-it-costs-0-22ec" id="article-link-3189729"&gt;
          Videos in React?! Yep. Each scene is a component (and it costs $0)
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/webdev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;webdev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/react"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;react&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/programming"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;programming&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ai&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
            &lt;a href="https://dev.to/yosolita1978/videos-in-react-yep-each-scene-is-a-component-and-it-costs-0-22ec#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              Comments


              &lt;span class="hidden s:inline"&gt;Add Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            5 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;




</description>
      <category>webdev</category>
      <category>react</category>
      <category>programming</category>
      <category>ai</category>
    </item>
    <item>
      <title>Videos in React?! Yep. Each scene is a component (and it costs $0)</title>
      <dc:creator>Cristina Rodriguez</dc:creator>
      <pubDate>Thu, 22 Jan 2026 06:07:57 +0000</pubDate>
      <link>https://dev.to/yosolita1978/videos-in-react-yep-each-scene-is-a-component-and-it-costs-0-22ec</link>
      <guid>https://dev.to/yosolita1978/videos-in-react-yep-each-scene-is-a-component-and-it-costs-0-22ec</guid>
      <description>&lt;p&gt;I used to think “making a video” meant one of two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open a heavyweight editor
&lt;/li&gt;
&lt;li&gt;Drag tiny clips around a timeline until your soul leaves your body&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I’m a developer. I like code. I like components. I like being able to undo mistakes without having to negotiate with a mysterious UI panel from 2002.&lt;/p&gt;

&lt;p&gt;So when I heard there’s a tool called &lt;a href="https://www.remotion.dev/" rel="noopener noreferrer"&gt;&lt;strong&gt;Remotion&lt;/strong&gt;&lt;/a&gt; that lets you create videos using &lt;strong&gt;React components&lt;/strong&gt;, my first thought was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Sure. And my CSS is also perfectly centered.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then I tried it.&lt;/p&gt;

&lt;p&gt;And the moment it &lt;em&gt;really&lt;/em&gt; clicked wasn’t even the rendering part…&lt;/p&gt;

&lt;p&gt;It was running the project like a normal React app.&lt;/p&gt;




&lt;h2&gt;
  
  
  The idea that makes it click
&lt;/h2&gt;

&lt;p&gt;Remotion works because a video is just:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A bunch of frames&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rendered in sequence&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;At a fixed &lt;strong&gt;fps&lt;/strong&gt; (frames per second)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your video is 30fps, then:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;frame &lt;code&gt;30&lt;/code&gt; = 1 second
&lt;/li&gt;
&lt;li&gt;frame &lt;code&gt;900&lt;/code&gt; = 30 seconds
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And here’s the fun part:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Every frame is just React rendering your component&lt;/strong&gt; (with a different “current frame” number).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So animation becomes… math.&lt;/p&gt;

&lt;p&gt;No timeline. No keyframes. Just &lt;code&gt;interpolate()&lt;/code&gt; and &lt;code&gt;spring()&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  “NO COST!!!” (but let’s be precise)
&lt;/h2&gt;

&lt;p&gt;This post is about building and rendering locally on your machine — no SaaS subscriptions, no hosted rendering costs, no paid editor required.&lt;/p&gt;

&lt;p&gt;(If you’re using Remotion in a larger company context, double-check licensing. For a personal project / small setup like this, you can build and render locally for free.)&lt;/p&gt;




&lt;h2&gt;
  
  
  Create the project
&lt;/h2&gt;

&lt;p&gt;Remotion has a starter generator:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx create-video@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It creates a project that looks suspiciously like any other React project.&lt;/p&gt;

&lt;p&gt;Like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;picasyfijas-video/
├── public/
│   └── icon-512.png          ← Your logo
├── src/
│   ├── Root.tsx              ← Registers the composition (dimensions, fps, duration)
│   ├── PicasFijasVideo.tsx   ← Main video with all scenes
│   ├── Composition.tsx       ← Original template (unused)
│   ├── index.ts              ← Entry point
│   └── index.css             ← Styles
└── out/
    └── picasyfijas.mp4       ← Rendered video
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, you’re thinking: “Okay. Cute structure. But where’s the ‘video’ part?”&lt;/p&gt;

&lt;p&gt;This is where the brain-melt happens.&lt;/p&gt;




&lt;h2&gt;
  
  
  The moment it clicked: &lt;code&gt;npm run dev&lt;/code&gt; 🤯
&lt;/h2&gt;

&lt;p&gt;I did the most “React developer” thing possible:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And… it just &lt;strong&gt;ran like any React project&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The dev server started, I opened the URL, and suddenly I’m staring at a &lt;strong&gt;video timeline in my browser&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Scrub the playhead → frames update.&lt;br&gt;&lt;br&gt;
Components appear and disappear.&lt;br&gt;&lt;br&gt;
Animations play.&lt;br&gt;&lt;br&gt;
It’s all live.&lt;/p&gt;

&lt;p&gt;And my brain goes:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Hold up. I’m not &lt;em&gt;editing&lt;/em&gt; a video… I’m &lt;em&gt;rendering&lt;/em&gt; React.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That’s the “ohhhhhh” moment.&lt;/p&gt;

&lt;p&gt;Because once you realize the preview is just your components rendering over time, everything becomes familiar:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Want a new “scene”? Make a component.&lt;/li&gt;
&lt;li&gt;Want it to appear later? Wrap it in a &lt;code&gt;&amp;lt;Sequence from={...} /&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Want animation? Use &lt;code&gt;useCurrentFrame()&lt;/code&gt; + math.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Same workflow. Same comfort.&lt;/p&gt;

&lt;p&gt;But the output is a legit MP4.&lt;/p&gt;


&lt;h2&gt;
  
  
  Your “video settings” live in one place
&lt;/h2&gt;

&lt;p&gt;Inside &lt;code&gt;src/Root.tsx&lt;/code&gt;, you register a &lt;strong&gt;Composition&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This defines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;width&lt;/code&gt; / &lt;code&gt;height&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fps&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;durationInFrames&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here’s the &lt;strong&gt;vertical&lt;/strong&gt; setup I used for a short social video:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Composition&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"PicasFijasVideo"&lt;/span&gt;
  &lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;PicasFijasVideo&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;durationInFrames&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;fps&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;1080&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;1920&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s &lt;strong&gt;10 seconds&lt;/strong&gt; (300 frames at 30fps).&lt;/p&gt;

&lt;h3&gt;
  
  
  Want 30 seconds?
&lt;/h3&gt;

&lt;p&gt;Same idea:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;30 seconds * 30 fps = 900 frames&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;durationInFrames&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;900&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s it. Your video is now 30 seconds long.&lt;/p&gt;




&lt;h2&gt;
  
  
  Scenes = components, and timing = &lt;code&gt;&amp;lt;Sequence /&amp;gt;&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Your main video component (mine is &lt;code&gt;PicasFijasVideo.tsx&lt;/code&gt;) is basically a timeline built with &lt;code&gt;&amp;lt;Sequence /&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Each scene is a component. Each component gets a start frame and duration.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Sequence&lt;/span&gt; &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;durationInFrames&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;IntroScene&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Sequence&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Sequence&lt;/span&gt; &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;durationInFrames&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;240&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;MainScene&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Sequence&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One very nice detail:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Inside a &lt;code&gt;&amp;lt;Sequence&amp;gt;&lt;/code&gt;, &lt;code&gt;useCurrentFrame()&lt;/code&gt; starts at 0 again.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So each scene feels self-contained and easy to reason about.&lt;/p&gt;




&lt;h2&gt;
  
  
  The 10-second timeline I built (as a reference)
&lt;/h2&gt;

&lt;p&gt;I created a 10-second vertical video (1080×1920, 9:16) for social media using Remotion.&lt;/p&gt;

&lt;p&gt;Here’s the timeline:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Frames&lt;/th&gt;
&lt;th&gt;Seconds&lt;/th&gt;
&lt;th&gt;Scene&lt;/th&gt;
&lt;th&gt;What Happens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0–30&lt;/td&gt;
&lt;td&gt;0–1s&lt;/td&gt;
&lt;td&gt;Logo&lt;/td&gt;
&lt;td&gt;Logo springs in, title fades in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;30–90&lt;/td&gt;
&lt;td&gt;1–3s&lt;/td&gt;
&lt;td&gt;GuessScene&lt;/td&gt;
&lt;td&gt;Shows "????" and types guess "1456"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;90–150&lt;/td&gt;
&lt;td&gt;3–5s&lt;/td&gt;
&lt;td&gt;BullExplanation&lt;/td&gt;
&lt;td&gt;Highlights matching "1", shows "🐂 Bull/Fija"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;150–210&lt;/td&gt;
&lt;td&gt;5–7s&lt;/td&gt;
&lt;td&gt;CowExplanation&lt;/td&gt;
&lt;td&gt;Highlights "4" in wrong position, shows "🐄 Cow/Pica"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;210–300&lt;/td&gt;
&lt;td&gt;7–10s&lt;/td&gt;
&lt;td&gt;CTA&lt;/td&gt;
&lt;td&gt;Logo + "Play now!" + URL&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For a &lt;strong&gt;30-second video&lt;/strong&gt;, you simply add more “beats” (more scenes) or stretch durations.&lt;/p&gt;

&lt;p&gt;A simple 30-second structure that works for &lt;em&gt;any&lt;/em&gt; topic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Intro (2–3s)&lt;/li&gt;
&lt;li&gt;5–7 beats (3–5s each)&lt;/li&gt;
&lt;li&gt;CTA (3–5s)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Animation is just math (and two hooks)
&lt;/h2&gt;

&lt;p&gt;Most scenes start like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;frame&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useCurrentFrame&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;  &lt;span class="c1"&gt;// Current frame number (0, 1, 2... )&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;fps&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useVideoConfig&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Frames per second (30)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;useCurrentFrame()&lt;/code&gt; → tells you which frame is being rendered
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;useVideoConfig()&lt;/code&gt; → gives you &lt;code&gt;fps&lt;/code&gt;, dimensions, etc.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now you can animate anything.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fade in with &lt;code&gt;interpolate()&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;contentOpacity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;interpolate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;,&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;15&lt;/span&gt;&lt;span class="p"&gt;],&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;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;extrapolateLeft&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;clamp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;extrapolateRight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;clamp&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;What it does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Input: frames &lt;code&gt;[0..15]&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Output: opacity &lt;code&gt;[0..1]&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Result: fade in over &lt;strong&gt;0.5 seconds&lt;/strong&gt; (at 30fps)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;clamp&lt;/code&gt;: prevents values below 0 or above 1&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Bounce with &lt;code&gt;spring()&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;highlightScale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;spring&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;frame&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;// Start at frame 15&lt;/span&gt;
  &lt;span class="nx"&gt;fps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;damping&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;// Higher = less bounce&lt;/span&gt;
    &lt;span class="na"&gt;stiffness&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="c1"&gt;// Higher = snappier&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;What it does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Creates a natural bounce from 0 → 1&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;frame - 15&lt;/code&gt; delays the animation start&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Deep Dive: BullExplanation scene (a “real” example)
&lt;/h2&gt;

&lt;p&gt;This scene highlights the matching digit and shows the “🐂 Bull / Fija” explanation.&lt;/p&gt;

&lt;h3&gt;
  
  
  1) Bilingual text toggle
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;showSpanish&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;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;frame&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;15&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="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;frame&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What it does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Switches language every 15 frames (0.5 seconds)&lt;/li&gt;
&lt;li&gt;Starts toggling after frame 45 (so the content is already visible)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2) Conditional styling for highlights
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&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;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;digit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;
    &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;color&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="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#EBEBD3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#38618C&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;backgroundColor&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="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#38618C&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;transparent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;transform&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="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`scale(&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;highlightScale&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;scale(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;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;inline-block&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0 6px&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;borderRadius&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="si"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;digit&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;))}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;i === 0&lt;/code&gt; highlights the first digit (the “bull”)&lt;/li&gt;
&lt;li&gt;Highlighted digit gets inverted colors + bounce scale&lt;/li&gt;
&lt;li&gt;Others stay normal&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Key Remotion concepts (the cheat sheet)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concept&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Composition&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Defines video dimensions, fps, duration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Sequence&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Controls when a component appears/disappears on the timeline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AbsoluteFill&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Full-screen container that fills the canvas&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;useCurrentFrame()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Gets current frame number&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;interpolate()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Maps frame numbers to any value (opacity, position, etc.)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spring()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Creates natural bounce animations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Img&lt;/code&gt; + &lt;code&gt;staticFile()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Loads images from &lt;code&gt;public/&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Render to MP4
&lt;/h2&gt;

&lt;p&gt;You can render from the UI… but the CLI is the “ship it” button.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx remotion render src/index.ts PicasFijasVideo out/video.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That command turns your React composition into a real MP4 file.&lt;/p&gt;




&lt;h2&gt;
  
  
  Demo + repo
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Demo video: &lt;a href="https://github.com/Yosolita1978/picasyfijas-video/blob/main/out/picasyfijas.mp4" rel="noopener noreferrer"&gt;https://github.com/Yosolita1978/picasyfijas-video/blob/main/out/picasyfijas.mp4&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/Yosolita1978/picasyfijas-video" rel="noopener noreferrer"&gt;https://github.com/Yosolita1978/picasyfijas-video&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;Let’s recap:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Videos are just &lt;strong&gt;React renders&lt;/strong&gt; — one frame at a time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time = frames&lt;/strong&gt; (30fps → 900 frames = 30 seconds).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scenes = components&lt;/strong&gt;, and &lt;code&gt;&amp;lt;Sequence /&amp;gt;&lt;/code&gt; is your timeline.&lt;/li&gt;
&lt;li&gt;Animations are mostly &lt;strong&gt;&lt;code&gt;useCurrentFrame()&lt;/code&gt; + math&lt;/strong&gt; (&lt;code&gt;interpolate()&lt;/code&gt;, &lt;code&gt;spring()&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Rendering is one command: &lt;code&gt;npx remotion render …&lt;/code&gt; → MP4.&lt;/li&gt;
&lt;li&gt;And the wildest part? You can literally start with &lt;code&gt;npm run dev&lt;/code&gt; and preview it like… a normal React app. Still feels illegal.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now I’m curious: &lt;strong&gt;what would you make first if “a video” was just a folder of components?&lt;/strong&gt; A tutorial? A product promo? A meme? A “how to center a div” documentary?&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>react</category>
      <category>programming</category>
      <category>ai</category>
    </item>
    <item>
      <title>PWAs are underrated. I skipped the $99 Apple fee and the $25 Google fee by adding two files to my vanilla JS game. Now it works offline and installs like a native app. Here's the code 👇</title>
      <dc:creator>Cristina Rodriguez</dc:creator>
      <pubDate>Tue, 16 Dec 2025 19:51:15 +0000</pubDate>
      <link>https://dev.to/yosolita1978/pwas-are-underrated-i-skipped-the-99-apple-fee-and-the-25-google-fee-by-adding-two-files-to-my-2923</link>
      <guid>https://dev.to/yosolita1978/pwas-are-underrated-i-skipped-the-99-apple-fee-and-the-25-google-fee-by-adding-two-files-to-my-2923</guid>
      <description>&lt;p&gt;

&lt;/p&gt;
&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/yosolita1978/bulls-cows-goes-offline-how-i-turned-my-game-into-a-pwa-and-skipped-the-100-app-store-fee-3h4o" class="crayons-story__hidden-navigation-link"&gt;Bulls &amp;amp; Cows Goes Offline — How I Turned My Game Into a PWA (and Skipped the $100 App Store Fee)&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/yosolita1978" class="crayons-avatar  crayons-avatar--l  "&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%2Fuser%2Fprofile_image%2F569284%2F4cb7d3ec-e243-4a1f-9624-335dfab6ee6a.jpeg" alt="yosolita1978 profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/yosolita1978" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Cristina Rodriguez
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Cristina Rodriguez
                
              
              &lt;div id="story-author-preview-content-3109562" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/yosolita1978" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&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%2Fuser%2Fprofile_image%2F569284%2F4cb7d3ec-e243-4a1f-9624-335dfab6ee6a.jpeg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Cristina Rodriguez&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/yosolita1978/bulls-cows-goes-offline-how-i-turned-my-game-into-a-pwa-and-skipped-the-100-app-store-fee-3h4o" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Dec 16 '25&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/yosolita1978/bulls-cows-goes-offline-how-i-turned-my-game-into-a-pwa-and-skipped-the-100-app-store-fee-3h4o" id="article-link-3109562"&gt;
          Bulls &amp;amp; Cows Goes Offline — How I Turned My Game Into a PWA (and Skipped the $100 App Store Fee)
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/javascript"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;javascript&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/webdev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;webdev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/pwa"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;pwa&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/beginners"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;beginners&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
            &lt;a href="https://dev.to/yosolita1978/bulls-cows-goes-offline-how-i-turned-my-game-into-a-pwa-and-skipped-the-100-app-store-fee-3h4o#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              Comments


              &lt;span class="hidden s:inline"&gt;Add Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            2 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;




</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>pwa</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Bulls &amp; Cows Goes Offline — How I Turned My Game Into a PWA (and Skipped the $100 App Store Fee)</title>
      <dc:creator>Cristina Rodriguez</dc:creator>
      <pubDate>Tue, 16 Dec 2025 19:50:09 +0000</pubDate>
      <link>https://dev.to/yosolita1978/bulls-cows-goes-offline-how-i-turned-my-game-into-a-pwa-and-skipped-the-100-app-store-fee-3h4o</link>
      <guid>https://dev.to/yosolita1978/bulls-cows-goes-offline-how-i-turned-my-game-into-a-pwa-and-skipped-the-100-app-store-fee-3h4o</guid>
      <description>&lt;p&gt;Remember my &lt;a href="https://dev.to/yosolita1978/bulls-cows-bringing-a-pen-and-paper-classic-to-the-browser-no-ai-no-frameworks-just-js-iop"&gt;Bulls &amp;amp; Cows&lt;/a&gt; game? The one with no frameworks, no AI, just vanilla JS? Well, I made it installable. Like, "put it on your home screen and play offline" installable.&lt;/p&gt;

&lt;p&gt;No App Store. No Google Play. No $99/year Apple Developer fee. No $25 Google registration. Just... a web app that acts like a native app.&lt;/p&gt;

&lt;p&gt;And apparently, people like it? &lt;strong&gt;1.3K unique players, 1.4K sessions, and 1.8K pageviews&lt;/strong&gt; — with people spending almost 2 minutes per visit just thinking through a logic puzzle. That's enough traction that I gave it its own home.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🎮 Play it:&lt;/strong&gt; &lt;a href="https://www.picasyfijas.com/" rel="noopener noreferrer"&gt;picasyfijas.com&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  ✨ What's a PWA, Really?
&lt;/h2&gt;

&lt;p&gt;A Progressive Web App is just a regular website with two extra files that tell the browser: "Hey, I can work offline. Let me live on the home screen."&lt;/p&gt;

&lt;p&gt;That's it. Two files. Let me show you.&lt;/p&gt;

&lt;h2&gt;
  
  
  1️⃣ The Manifest (Your App's ID Card)
&lt;/h2&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;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bulls &amp;amp; Cows - Picas y Fijas"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"short_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bulls &amp;amp; Cows"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"start_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"display"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"standalone"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"background_color"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"#EBEBD3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"theme_color"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"#4281A4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"icons"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"src"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"icon-192.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"sizes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"192x192"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"image/png"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"src"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"icon-512.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"sizes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"512x512"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"image/png"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="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;This tells the browser what to call your app, what icon to use, and that &lt;code&gt;standalone&lt;/code&gt; means "hide the browser chrome and look like a real app."&lt;/p&gt;

&lt;p&gt;Link it in your HTML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;serviceWorker&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;serviceWorker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/sw.js&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;h2&gt;
  
  
  2️⃣ The Service Worker (Your Offline Brain)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CACHE_NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bulls-and-Cows-v4&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;FILES_TO_CACHE&lt;/span&gt; &lt;span class="o"&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;/&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;/index.html&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;/styles.css&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;/script.js&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;/manifest.json&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;/icon-192.png&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;/icon-512.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;install&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;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;caches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;CACHE_NAME&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;cache&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;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;FILES_TO_CACHE&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="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fetch&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;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;respondWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;caches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;response&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;response&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&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;That's the entire service worker. It caches your files on install, then serves them from cache when offline. No npm packages. No build step.&lt;/p&gt;

&lt;p&gt;Register it in your HTML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;serviceWorker&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;serviceWorker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/sw.js&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;h2&gt;
  
  
  3️⃣ The Install Prompt
&lt;/h2&gt;

&lt;p&gt;Browsers show an install button automatically, but I added my own banner because I wanted control over the UX:&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;let&lt;/span&gt; &lt;span class="nx"&gt;deferredPrompt&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="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;beforeinstallprompt&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;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;deferredPrompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;install-banner&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;display&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;flex&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="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;install-btn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;deferredPrompt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prompt&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;h2&gt;
  
  
  💰 Why Not the App Stores?
&lt;/h2&gt;

&lt;p&gt;Apple charges $99/year. Google charges $25 once. Both require review processes, screenshots, privacy policies, and jumping through hoops.&lt;/p&gt;

&lt;p&gt;My game collects zero data. No accounts. No ads. Just privacy-friendly Plausible for basic stats. It's literally just a logic puzzle.&lt;/p&gt;

&lt;p&gt;PWAs let me skip all of that. Users get an installable app. I keep my $124. Everyone wins.&lt;/p&gt;

&lt;h2&gt;
  
  
  🎯 The Result
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;✅ Works offline&lt;/li&gt;
&lt;li&gt;✅ Installable on Android, iOS, and desktop&lt;/li&gt;
&lt;li&gt;✅ No app store fees&lt;/li&gt;
&lt;li&gt;✅ Updates instantly (no waiting for review)&lt;/li&gt;
&lt;li&gt;✅ Still just HTML, CSS, and vanilla JS&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Total new code:&lt;/strong&gt; ~50 lines.&lt;/p&gt;




&lt;p&gt;👉 &lt;strong&gt;Play it:&lt;/strong&gt; &lt;a href="https://www.picasyfijas.com/" rel="noopener noreferrer"&gt;picasyfijas.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;Source:&lt;/strong&gt; &lt;a href="https://github.com/Yosolita1978/bullsandcows" rel="noopener noreferrer"&gt;github.com/Yosolita1978/bullsandcows&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Have you turned a side project into a PWA? I'd love to hear about it 👇&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>pwa</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Ever had your API calls fire way too often in a Next.js app? 😅
I wrote about how a simple useDebounce hook can smooth things out, boost performance, and keep your UI calm.

Read more 👇</title>
      <dc:creator>Cristina Rodriguez</dc:creator>
      <pubDate>Tue, 25 Nov 2025 20:01:43 +0000</pubDate>
      <link>https://dev.to/yosolita1978/ever-had-your-api-calls-fire-way-too-often-in-a-nextjs-app-i-wrote-about-how-a-simple-5b51</link>
      <guid>https://dev.to/yosolita1978/ever-had-your-api-calls-fire-way-too-often-in-a-nextjs-app-i-wrote-about-how-a-simple-5b51</guid>
      <description>&lt;p&gt;

&lt;/p&gt;
&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/yosolita1978/why-i-built-a-usedebounce-custom-hook-in-nextjs-and-why-you-should-too-54d0" class="crayons-story__hidden-navigation-link"&gt;Why I Built a useDebounce Custom Hook in Next.js (and Why You Should Too)&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/yosolita1978" class="crayons-avatar  crayons-avatar--l  "&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%2Fuser%2Fprofile_image%2F569284%2F4cb7d3ec-e243-4a1f-9624-335dfab6ee6a.jpeg" alt="yosolita1978 profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/yosolita1978" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Cristina Rodriguez
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Cristina Rodriguez
                
              
              &lt;div id="story-author-preview-content-3058844" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/yosolita1978" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&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%2Fuser%2Fprofile_image%2F569284%2F4cb7d3ec-e243-4a1f-9624-335dfab6ee6a.jpeg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Cristina Rodriguez&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/yosolita1978/why-i-built-a-usedebounce-custom-hook-in-nextjs-and-why-you-should-too-54d0" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Nov 25 '25&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/yosolita1978/why-i-built-a-usedebounce-custom-hook-in-nextjs-and-why-you-should-too-54d0" id="article-link-3058844"&gt;
          Why I Built a useDebounce Custom Hook in Next.js (and Why You Should Too)
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/reactjsdevelopment"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;reactjsdevelopment&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/webdev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;webdev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/react"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;react&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/programming"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;programming&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/yosolita1978/why-i-built-a-usedebounce-custom-hook-in-nextjs-and-why-you-should-too-54d0" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;5&lt;span class="hidden s:inline"&gt; reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/yosolita1978/why-i-built-a-usedebounce-custom-hook-in-nextjs-and-why-you-should-too-54d0#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              Comments


              1&lt;span class="hidden s:inline"&gt; comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            3 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;




</description>
      <category>nextjs</category>
      <category>react</category>
      <category>javascript</category>
      <category>performance</category>
    </item>
    <item>
      <title>Why I Built a useDebounce Custom Hook in Next.js (and Why You Should Too)</title>
      <dc:creator>Cristina Rodriguez</dc:creator>
      <pubDate>Tue, 25 Nov 2025 19:59:13 +0000</pubDate>
      <link>https://dev.to/yosolita1978/why-i-built-a-usedebounce-custom-hook-in-nextjs-and-why-you-should-too-54d0</link>
      <guid>https://dev.to/yosolita1978/why-i-built-a-usedebounce-custom-hook-in-nextjs-and-why-you-should-too-54d0</guid>
      <description>&lt;p&gt;If you've ever wired up a search bar in &lt;strong&gt;Next.js&lt;/strong&gt;, you’ve probably hit this problem: every keystroke fires a new &lt;strong&gt;API call&lt;/strong&gt;, triggers a &lt;strong&gt;re-render&lt;/strong&gt;, or runs an &lt;strong&gt;expensive calculation&lt;/strong&gt;. As your app grows, those “harmless little updates” start stacking up—fast.&lt;/p&gt;

&lt;p&gt;I’ve been there. I remember watching my terminal fill with logs like a slot machine every time I typed a word. And because I was calling an API, sometimes I even hit &lt;strong&gt;rate limits&lt;/strong&gt; just by typing too quickly.&lt;/p&gt;

&lt;p&gt;That’s when I realized: &lt;em&gt;I didn’t need results instantly.&lt;/em&gt; I needed results after the user paused typing.&lt;/p&gt;

&lt;p&gt;That’s exactly what &lt;strong&gt;debouncing&lt;/strong&gt; solves—and why building a small, reusable &lt;code&gt;useDebounce&lt;/code&gt; hook changed the flow of my whole project.&lt;/p&gt;




&lt;h2&gt;
  
  
  🚨 The Problem: Too Many Updates
&lt;/h2&gt;

&lt;p&gt;React is responsive—which is great—until it isn’t.&lt;/p&gt;

&lt;h3&gt;
  
  
  Common places this becomes a problem:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Search bars&lt;/strong&gt; that fire API calls on every letter&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Window resize&lt;/strong&gt; listeners&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Live form validation&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Text fields tied to heavy filtering logic&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The consequences:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Wasted &lt;strong&gt;API calls&lt;/strong&gt; (and money)&lt;/li&gt;
&lt;li&gt;Janky &lt;strong&gt;UI performance&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Constant &lt;strong&gt;re-renders&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Hitting provider &lt;strong&gt;rate limits&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Sluggish, frustrating &lt;strong&gt;user experience&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here’s a simple example of the problem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nf"&gt;useEffect&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="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/search?q=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;query&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="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a user types “nextjs,” this runs &lt;strong&gt;6 separate times&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;n → ne → nex → next → nextj → nextjs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your server cries.&lt;br&gt;
Your UI stutters.&lt;br&gt;
You quietly wonder if you picked the wrong career.&lt;/p&gt;


&lt;h2&gt;
  
  
  😅 Naive Solutions (And Why They Fail)
&lt;/h2&gt;
&lt;h3&gt;
  
  
  ❌ 1. “Let me just add a timeout…”
&lt;/h3&gt;

&lt;p&gt;This is the classic first attempt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nf"&gt;setTimeout&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="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It &lt;em&gt;seems&lt;/em&gt; fine… until you realize:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You never cancelled the previous timeout&lt;/li&gt;
&lt;li&gt;Race conditions appear&lt;/li&gt;
&lt;li&gt;Multiple calls still fire, just delayed&lt;/li&gt;
&lt;li&gt;Your component becomes a messy spaghetti bowl&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  ❌ 2. “I’ll track the last value manually”
&lt;/h3&gt;

&lt;p&gt;Trust me: you &lt;em&gt;can&lt;/em&gt; do this. You &lt;em&gt;shouldn’t&lt;/em&gt; do this.&lt;/p&gt;

&lt;p&gt;It leads to bugs, stale values, and callback nightmares.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Debouncing deserves its own tidy, reusable hook.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  ✅ Enter &lt;code&gt;useDebounce&lt;/code&gt;: The Custom Hook That Fixes Everything
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Debouncing&lt;/strong&gt; means:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Only run this action after the value has stopped changing for X milliseconds.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That tiny delay (300ms, for example) is enough to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prevent API spamming&lt;/li&gt;
&lt;li&gt;Smooth out re-renders&lt;/li&gt;
&lt;li&gt;Improve UX&lt;/li&gt;
&lt;li&gt;Save money and bandwidth&lt;/li&gt;
&lt;li&gt;Make your search bar feel intentional instead of chaotic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is exactly the kind of logic that belongs inside a &lt;strong&gt;Custom Hook&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧩 How &lt;code&gt;useDebounce&lt;/code&gt; Works (with Clear Code + Comments)
&lt;/h2&gt;

&lt;p&gt;Here’s the entire hook, ready to drop into your project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&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;useState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useEffect&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="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * useDebounce — delays updating a value until
 * `delay` milliseconds have passed without changes.
 */&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;useDebounce&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;value&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;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;300&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;debouncedValue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setDebouncedValue&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&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="nf"&gt;useEffect&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="c1"&gt;// Start a timer that updates debouncedValue when delay elapses&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&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="nf"&gt;setDebouncedValue&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="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Cancel the timer if value changes before delay ends&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timer&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="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;delay&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;debouncedValue&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;h3&gt;
  
  
  Why this works:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;timer resets&lt;/strong&gt; on every keystroke&lt;/li&gt;
&lt;li&gt;The update only happens &lt;strong&gt;after the pause&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Cleanup prevents race conditions&lt;/li&gt;
&lt;li&gt;The hook is fully &lt;strong&gt;reusable&lt;/strong&gt; across components&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🔍 Using &lt;code&gt;useDebounce&lt;/code&gt; in a Next.js Component
&lt;/h2&gt;

&lt;p&gt;Let’s apply it to a real-world search bar.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useEffect&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="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useDebounce&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="s2"&gt;@/hooks/useDebounce&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;SearchBar&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setQuery&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&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="c1"&gt;// Debounce raw user input by 300ms&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;debouncedQuery&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useDebounce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&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="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;debouncedQuery&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;search&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;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/search?q=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;debouncedQuery&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Results:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;search&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="nx"&gt;debouncedQuery&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt;
      &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&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="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;placeholder&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Search..."&lt;/span&gt;
      &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"border p-2"&lt;/span&gt;
    &lt;span class="p"&gt;/&amp;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;h3&gt;
  
  
  What’s happening behind the scenes?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;query&lt;/code&gt; updates instantly → keeps the UI responsive&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;debouncedQuery&lt;/code&gt; updates after 300ms → triggers &lt;strong&gt;one&lt;/strong&gt; clean API call&lt;/li&gt;
&lt;li&gt;No API spam&lt;/li&gt;
&lt;li&gt;No jittery UI&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🌟 Benefits of Using &lt;code&gt;useDebounce&lt;/code&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;1. Better Performance&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;No more re-render storms.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;2. Fewer API Calls&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Your backend (and your wallet) will thank you.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;3. Smoother UX&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Search results feel stable and intentional.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;4. Cleaner Code&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Centralized, reusable logic.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;5. Reusable Everywhere&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Use it for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Search bars&lt;/li&gt;
&lt;li&gt;Form validation&lt;/li&gt;
&lt;li&gt;Filtering tables&lt;/li&gt;
&lt;li&gt;Window resize&lt;/li&gt;
&lt;li&gt;Drag events&lt;/li&gt;
&lt;li&gt;Autosaving&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🧭 Conclusion
&lt;/h2&gt;

&lt;p&gt;Debouncing is one of those small optimizations that makes a &lt;strong&gt;huge difference&lt;/strong&gt;.&lt;br&gt;
The moment you wrap it in a custom React hook like &lt;strong&gt;useDebounce&lt;/strong&gt;, your app becomes faster, smoother, and easier to maintain.&lt;/p&gt;

&lt;p&gt;Once you use it in one component, you’ll find yourself adding it everywhere.&lt;/p&gt;

&lt;p&gt;If you’ve built your own version—or used &lt;code&gt;useDebounce&lt;/code&gt; in interesting ways—I’d love to hear about it. Drop your thoughts in the comments!&lt;/p&gt;

&lt;p&gt;Happy coding! 🚀&lt;/p&gt;

</description>
      <category>reactjsdevelopment</category>
      <category>webdev</category>
      <category>react</category>
      <category>programming</category>
    </item>
    <item>
      <title>🤓 Bringing back the old pen-and-paper logic game Bulls &amp; Cows!
No AI, no frameworks — just pure HTML, CSS &amp; vanilla JS.
Can you guess the secret number? 💭
👉 https://bulls.yosola.co/
 #javascript #webdev #gamedev #nostalgia</title>
      <dc:creator>Cristina Rodriguez</dc:creator>
      <pubDate>Wed, 15 Oct 2025 01:15:45 +0000</pubDate>
      <link>https://dev.to/yosolita1978/bringing-back-the-old-pen-and-paper-logic-game-bulls-cows-no-ai-no-frameworks-just-pure-1464</link>
      <guid>https://dev.to/yosolita1978/bringing-back-the-old-pen-and-paper-logic-game-bulls-cows-no-ai-no-frameworks-just-pure-1464</guid>
      <description>&lt;p&gt;

&lt;/p&gt;
&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/yosolita1978/bulls-cows-bringing-a-pen-and-paper-classic-to-the-browser-no-ai-no-frameworks-just-js-iop" class="crayons-story__hidden-navigation-link"&gt;Bulls &amp;amp; Cows — Bringing a Pen-and-Paper Classic to the Browser (No AI, No Frameworks, Just JS)&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/yosolita1978" class="crayons-avatar  crayons-avatar--l  "&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%2Fuser%2Fprofile_image%2F569284%2F4cb7d3ec-e243-4a1f-9624-335dfab6ee6a.jpeg" alt="yosolita1978 profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/yosolita1978" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Cristina Rodriguez
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Cristina Rodriguez
                
              
              &lt;div id="story-author-preview-content-2925119" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/yosolita1978" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&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%2Fuser%2Fprofile_image%2F569284%2F4cb7d3ec-e243-4a1f-9624-335dfab6ee6a.jpeg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Cristina Rodriguez&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/yosolita1978/bulls-cows-bringing-a-pen-and-paper-classic-to-the-browser-no-ai-no-frameworks-just-js-iop" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Oct 15 '25&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/yosolita1978/bulls-cows-bringing-a-pen-and-paper-classic-to-the-browser-no-ai-no-frameworks-just-js-iop" id="article-link-2925119"&gt;
          Bulls &amp;amp; Cows — Bringing a Pen-and-Paper Classic to the Browser (No AI, No Frameworks, Just JS)
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/javascript"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;javascript&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/webdev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;webdev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/gamedev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;gamedev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/programming"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;programming&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/yosolita1978/bulls-cows-bringing-a-pen-and-paper-classic-to-the-browser-no-ai-no-frameworks-just-js-iop" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;3&lt;span class="hidden s:inline"&gt; reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/yosolita1978/bulls-cows-bringing-a-pen-and-paper-classic-to-the-browser-no-ai-no-frameworks-just-js-iop#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              Comments


              &lt;span class="hidden s:inline"&gt;Add Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            2 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;




&lt;p&gt;

&lt;/p&gt;
&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://bulls.yosola.co/" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.picasyfijas.com%2Fog-image.png" height="auto" class="m-0"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://bulls.yosola.co/" rel="noopener noreferrer" class="c-link"&gt;
            Picas y Fijas - Bulls &amp;amp; Cows | Free Online Number Guessing Game
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Guess the secret 4-digit number in 10 attempts. Play free online in English or Spanish!
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbulls.yosola.co%2Ffavicon.ico"&gt;
          bulls.yosola.co
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;




</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>gamedev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Bulls &amp; Cows — Bringing a Pen-and-Paper Classic to the Browser (No AI, No Frameworks, Just JS)</title>
      <dc:creator>Cristina Rodriguez</dc:creator>
      <pubDate>Wed, 15 Oct 2025 01:14:21 +0000</pubDate>
      <link>https://dev.to/yosolita1978/bulls-cows-bringing-a-pen-and-paper-classic-to-the-browser-no-ai-no-frameworks-just-js-iop</link>
      <guid>https://dev.to/yosolita1978/bulls-cows-bringing-a-pen-and-paper-classic-to-the-browser-no-ai-no-frameworks-just-js-iop</guid>
      <description>&lt;p&gt;Lately, I’ve been feeling nostalgic 🤓.&lt;/p&gt;

&lt;p&gt;When I was a kid, I used to play a pen-and-paper logic game called &lt;em&gt;Picas y Fijas&lt;/em&gt; (also known as &lt;em&gt;Bulls and Cows&lt;/em&gt;). So, I decided to build my own digital version — no AI, no frameworks, just pure HTML, CSS, and vanilla JavaScript.&lt;/p&gt;

&lt;p&gt;🎮 &lt;strong&gt;Try it here:&lt;/strong&gt; &lt;a href="https://bulls.yosola.co" rel="noopener noreferrer"&gt;bulls.yosola.co&lt;/a&gt;&lt;br&gt;
💻 &lt;strong&gt;Source code:&lt;/strong&gt; &lt;a href="https://github.com/Yosolita1978/bullsandcows" rel="noopener noreferrer"&gt;github.com/Yosolita1978/bullsandcows&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It’s a quick game (around 3 minutes per round), bilingual (English/Spanish), and requires &lt;strong&gt;no registration&lt;/strong&gt;. Just open it and start guessing!&lt;/p&gt;




&lt;h2&gt;
  
  
  ✨ How It Works (and Why It’s So Simple)
&lt;/h2&gt;

&lt;p&gt;The entire game runs on basic JavaScript — no frameworks, no libraries, and no AI logic under the hood.&lt;br&gt;
It’s the kind of code you can actually read, understand, and modify in one sitting.&lt;/p&gt;

&lt;p&gt;Here are the main functions that power the logic 👇&lt;/p&gt;

&lt;h3&gt;
  
  
  1️⃣ Generating the Secret Number
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateSecretNumber&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;digits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;digits&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;lt;&lt;/span&gt; &lt;span class="mi"&gt;4&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;randomDigit&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;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&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;digits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;randomDigit&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;digits&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;randomDigit&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;digits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Creates a &lt;strong&gt;4-digit number&lt;/strong&gt; with no repeated digits.&lt;/li&gt;
&lt;li&gt;Simple &lt;code&gt;Math.random()&lt;/code&gt; and array checking — no fancy imports.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2️⃣ Comparing the Guess
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getBullsAndCows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;guess&lt;/span&gt;&lt;span class="p"&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;bulls&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;cows&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="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;secret&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&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="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;guess&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="nx"&gt;bulls&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;else&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;secret&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;guess&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt; &lt;span class="nx"&gt;cows&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;bulls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cows&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;ul&gt;
&lt;li&gt;Checks &lt;strong&gt;exact matches&lt;/strong&gt; (bulls) and &lt;strong&gt;misplaced matches&lt;/strong&gt; (cows).&lt;/li&gt;
&lt;li&gt;Uses a classic double-loop logic that every coder has probably written once in their life.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3️⃣ Updating the UI
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;updateResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;guess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bulls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cows&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;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tr&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`
    &amp;lt;td&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;guess&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/td&amp;gt;
    &amp;lt;td&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;bulls&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/td&amp;gt;
    &amp;lt;td&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;cows&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/td&amp;gt;
  `&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#results tbody&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Minimal DOM manipulation — just creating and appending rows to show your guesses.&lt;/li&gt;
&lt;li&gt;Keeps the focus on logic, not on libraries.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🧬 Why I Built It This Way
&lt;/h2&gt;

&lt;p&gt;With so many projects relying on AI and frameworks today, I wanted to build something that reminds us of the &lt;strong&gt;joy of pure logic and simplicity&lt;/strong&gt;.&lt;br&gt;
No dependencies, no build tools — just a small web app that works anywhere.&lt;/p&gt;

&lt;p&gt;You can even &lt;strong&gt;view source&lt;/strong&gt;, copy the code, and tweak it for your own version (like changing the number of digits or styling your own board).&lt;/p&gt;




&lt;h2&gt;
  
  
  🎯 Challenge for You
&lt;/h2&gt;

&lt;p&gt;Try to guess the secret number in &lt;strong&gt;fewer than 7 attempts&lt;/strong&gt;.&lt;br&gt;
I’d love to hear how you did — comment your best score below 👇&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://bulls.yosola.co" rel="noopener noreferrer"&gt;Play Bulls &amp;amp; Cows&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  🧩 Tech Summary
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Details&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Stack&lt;/td&gt;
&lt;td&gt;HTML, CSS, Vanilla JS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Languages&lt;/td&gt;
&lt;td&gt;English / Spanish toggle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Repo&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/Yosolita1978/bullsandcows" rel="noopener noreferrer"&gt;Yosolita1978/bullsandcows&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gameplay&lt;/td&gt;
&lt;td&gt;4-digit logic puzzle, “bulls” = right place, “cows” = right number wrong place&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;p&gt;💬 Have you ever built a small nostalgic game just for fun?&lt;br&gt;
I’d love to see your “no-framework” projects in the comments!&lt;/p&gt;




</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>gamedev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Building on LangChain, FAISS, and Next.js, I turned my mother‑in‑law’s Mexican recipes into SazónBot—a bilingual AI chef that scales dishes, offers substitutions, and even embeds cooking videos. Check out the journey!</title>
      <dc:creator>Cristina Rodriguez</dc:creator>
      <pubDate>Sat, 04 Oct 2025 00:37:43 +0000</pubDate>
      <link>https://dev.to/yosolita1978/building-on-langchain-faiss-and-nextjs-i-turned-my-mother-in-laws-mexican-recipes-into-pmg</link>
      <guid>https://dev.to/yosolita1978/building-on-langchain-faiss-and-nextjs-i-turned-my-mother-in-laws-mexican-recipes-into-pmg</guid>
      <description>&lt;p&gt;

&lt;/p&gt;
&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/yosolita1978/building-sazonbot-a-journey-from-family-recipes-to-ai-chatbot-3096" class="crayons-story__hidden-navigation-link"&gt;Building SazónBot: A Journey from Family Recipes to AI Chatbot&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/yosolita1978" class="crayons-avatar  crayons-avatar--l  "&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%2Fuser%2Fprofile_image%2F569284%2F4cb7d3ec-e243-4a1f-9624-335dfab6ee6a.jpeg" alt="yosolita1978 profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/yosolita1978" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Cristina Rodriguez
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Cristina Rodriguez
                
              
              &lt;div id="story-author-preview-content-2890289" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/yosolita1978" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&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%2Fuser%2Fprofile_image%2F569284%2F4cb7d3ec-e243-4a1f-9624-335dfab6ee6a.jpeg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Cristina Rodriguez&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/yosolita1978/building-sazonbot-a-journey-from-family-recipes-to-ai-chatbot-3096" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Oct 4 '25&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/yosolita1978/building-sazonbot-a-journey-from-family-recipes-to-ai-chatbot-3096" id="article-link-2890289"&gt;
          Building SazónBot: A Journey from Family Recipes to AI Chatbot
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/nextjs"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;nextjs&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ai&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/agentai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;agentai&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/opensource"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;opensource&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
            &lt;a href="https://dev.to/yosolita1978/building-sazonbot-a-journey-from-family-recipes-to-ai-chatbot-3096#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              Comments


              1&lt;span class="hidden s:inline"&gt; comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            3 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;




</description>
      <category>nextjs</category>
      <category>ai</category>
      <category>agentai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Building SazónBot: A Journey from Family Recipes to AI Chatbot</title>
      <dc:creator>Cristina Rodriguez</dc:creator>
      <pubDate>Sat, 04 Oct 2025 00:35:08 +0000</pubDate>
      <link>https://dev.to/yosolita1978/building-sazonbot-a-journey-from-family-recipes-to-ai-chatbot-3096</link>
      <guid>https://dev.to/yosolita1978/building-sazonbot-a-journey-from-family-recipes-to-ai-chatbot-3096</guid>
      <description>&lt;p&gt;I was sitting at my kitchen table, squinting at a Word document full of my mother‑in‑law’s recipes on my phone. Everything was there: pozole, chilaquiles, mole—but the file was cumbersome, the formatting inconsistent, and there was no extra context. I couldn’t ask it how to scale a recipe or swap an ingredient. That frustration sparked an idea: what if anyone could talk to this archive? What if I could share the warmth of my Mexican in‑laws’ kitchen with the world?&lt;/p&gt;

&lt;p&gt;That spark became &lt;a href="https://sazonbot.vercel.app/" rel="noopener noreferrer"&gt;SazónBot&lt;/a&gt;, a bilingual AI chatbot I built to honor the García family heritage that welcomed me while embracing modern technology. This is the story of how I, not Mexican myself but lucky enough to marry into a family that shares its culinary wisdom, wove together memory and machine to create a chef you can talk to.&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%2Fhtm9fbn6kag4t089bx1b.jpeg" 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%2Fhtm9fbn6kag4t089bx1b.jpeg" alt="Screenshot of the initial page of SazonBot" width="718" height="1600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  From Digital Files to Rich Data
&lt;/h3&gt;

&lt;p&gt;It all began with a simple question: how could I transform a set of clunky Word documents into something living and useful without losing their soul? The answer lay in blending tradition with technology. Using Python and FastAPI, I developed a backend that can parse and host data extracted from digital recipes. I built a vector store with FAISS, enabling semantic search so you can ask “¿Cómo hago enchiladas?” and get a nuanced answer.&lt;/p&gt;

&lt;p&gt;But feeding the bot isn’t enough—I needed it to be safe and reliable. I wrote custom tools in LangChain to handle recipe scaling, ingredient substitutions, and even web searches for cooking techniques. Along the way, I added a safety module to filter out harmful prompts and session memory to make conversations feel personal.&lt;/p&gt;

&lt;h3&gt;
  
  
  Teaching a Bot to Speak Two Languages
&lt;/h3&gt;

&lt;p&gt;One of the biggest challenges was giving the bot a bilingual voice. In my adopted community, we often switch between English and Spanish, so SazónBot had to do the same. By integrating OpenAI GPT-4o-mini and building a multilingual vector store, I achieved fluid code‑switching. You can start in Spanish—“¿Cómo preparo tacos al pastor?”—and seamlessly transition to English. This wasn’t just a feature; it was a promise that everyone, regardless of language, could feel at home in SazónBot’s kitchen.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bringing the Experience to Life
&lt;/h3&gt;

&lt;p&gt;With the backend humming, it was time to create a beautiful front end. Using Next.js and Tailwind CSS, I built a responsive interface that feels like flipping through a recipe book. Embedded YouTube videos and images bring dishes to life, while &lt;strong&gt;an intelligent agent behind the scenes coordinates 11 tools—from scaling recipes to finding cooking videos&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Deploying the site on Vercel came with a trade‑off: the free tier means the first request may take a few seconds. But once the bot warms up, the experience is seamless. It even remembers your session so it can follow up on conversations: “¿Te gustó el mole? Would you like to scale it for six people?”&lt;/p&gt;

&lt;h3&gt;
  
  
  Overcoming Challenges and Making It Real
&lt;/h3&gt;

&lt;p&gt;As with any passion project, I hit bumps along the road. Setting up the development environment required juggling multiple technologies: Node.js for the front end, Python for the back end, and environment variables for API keys. And like that tricky “Add to Calendar” button I sweated over in another project, each feature—be it recipe scaling or ingredient substitution—was its own mini‑challenge.&lt;/p&gt;

&lt;p&gt;But with perseverance, I deployed a living, bilingual cookbook. The result? &lt;a href="https://sazonbot.vercel.app/" rel="noopener noreferrer"&gt;A chatbot that feels like chatting with your mother‑in‑law, powered by AI&lt;/a&gt;. I achieved my mission: bridging a heritage I married into with technology to bring authentic Mexican recipes to anyone with an internet connection.&lt;/p&gt;

&lt;p&gt;So next time you’re craving chilaquiles or need a substitute for epazote, open SazónBot. You’ll find more than a recipe—you’ll find a piece of my extended family’s story.&lt;/p&gt;

&lt;p&gt;Are you interested in the code? &lt;a href="https://github.com/Yosolita1978/mexican-ai-chatbot" rel="noopener noreferrer"&gt;Visit my GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>ai</category>
      <category>agentai</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
