<?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: Patricio Salazar</title>
    <description>The latest articles on DEV Community by Patricio Salazar (@patriciosalazar).</description>
    <link>https://dev.to/patriciosalazar</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%2F583216%2Fea905c0b-1af2-4313-8a71-ae6856c9ff39.jpg</url>
      <title>DEV Community: Patricio Salazar</title>
      <link>https://dev.to/patriciosalazar</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/patriciosalazar"/>
    <language>en</language>
    <item>
      <title>You Don't Need A Framework - Build A Dynamic Blog with HTML &amp; Vanilla JavaScript</title>
      <dc:creator>Patricio Salazar</dc:creator>
      <pubDate>Wed, 28 May 2025 18:37:57 +0000</pubDate>
      <link>https://dev.to/patriciosalazar/you-dont-need-a-framework-build-a-dynamic-blog-with-html-vanilla-javascript-15e3</link>
      <guid>https://dev.to/patriciosalazar/you-dont-need-a-framework-build-a-dynamic-blog-with-html-vanilla-javascript-15e3</guid>
      <description>&lt;p&gt;Months ago, when I was reworking parts of my website, I began questioning every part of the tech stack. During that process, I thought about how I would build a dynamic blog with vanilla JavaScript. Eventually, I realized I was still going to end up using a framework. So, I worked through the main concepts in my head to scratch the itch, then shelved the idea...until, I couldn't ignore the fact that the itch was still there.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This article is simply about the JavaScript side of building a dynamic blog. It is not a claim on the best approach for such a system, nor an argument for or against using vanilla JS or a framework. We're just looking at how this could get done.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Let's get into it
&lt;/h2&gt;

&lt;p&gt;First things first, we need a few HTML and JS files, so let's get a new project going in your favorite editor (feel free to code along).&lt;/p&gt;

&lt;p&gt;Create a new folder, name it whatever you want, and open it in your editor. Create the following files and folders:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;index.html&lt;/li&gt;
&lt;li&gt;blog.html&lt;/li&gt;
&lt;li&gt;blog/post.html&lt;/li&gt;
&lt;li&gt;js/blogCards.js&lt;/li&gt;
&lt;li&gt;js/blogPost.js&lt;/li&gt;
&lt;li&gt;js/blogPostsData.js&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;No Webpack, no Vite, no package.json... and no Tailwind 🤯. Ahhh, how refreshing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Boilerplate code
&lt;/h2&gt;

&lt;p&gt;Here is the relevant HTML&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;blog.html&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;Include the script tag for blogCards.js in the head&lt;br&gt;
&lt;code&gt;&amp;lt;script type="module" src="/js/blogCards.js" async&amp;gt;&amp;lt;/script&amp;gt;&lt;/code&gt;&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%2Frm8yydqsaj1bndjtlbdp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frm8yydqsaj1bndjtlbdp.png" alt="screenshot of code in blog.html" width="800" height="543"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;blog-post.html&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;Include the script tag for blogPost.js in the head&lt;br&gt;
&lt;code&gt;&amp;lt;script type="module" src="/js/blogPost.js" async&amp;gt;&amp;lt;/script&amp;gt;&lt;/code&gt;&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%2Frsr6j1ba9xgfa47talxs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frsr6j1ba9xgfa47talxs.png" alt="screenshot of code in blog-post.html" width="800" height="680"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Also, the blog post data&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;blogPosts.js&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;Add as many blog post objects as you want. I had ChatGPT generate some dummy data here.&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%2Foqvfu6mqad9uo71mjja2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foqvfu6mqad9uo71mjja2.png" alt="screenshot of code in blogPosts.js" width="800" height="402"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Start and preview
&lt;/h2&gt;

&lt;p&gt;This is a good time to start the project to see what we have so far. If you're using something like VS Code, view the project using &lt;a href="https://marketplace.visualstudio.com/items?itemName=ritwickdey.LiveServer" rel="noopener noreferrer"&gt;Live Server&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dynamically populate the blog listing page with blog-post cards
&lt;/h2&gt;

&lt;p&gt;Let's start by importing the data at the top of the &lt;code&gt;blogCards.js&lt;/code&gt; file:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;import { blogPosts } from "./blogsPosts.js"&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We're going to have to place each blog card inside of a the container element we created in the blog listing page, so let's declare a &lt;code&gt;blogCardsContainer&lt;/code&gt; variable and store a reference to the container in it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;const blogCardsContainer = document.querySelector("#blog-cards-container");&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now, we can loop over each blog post in our data, create the cards, and then finally append them to &lt;code&gt;blogCardsContainer&lt;/code&gt;:&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%2F08zv5qjyj7yfjiwx287b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F08zv5qjyj7yfjiwx287b.png" alt="screenshot of code in blogCards.js" width="800" height="485"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The first three lines inside of the forEach loop:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Creates a div element to be styled as the card&lt;/li&gt;
&lt;li&gt;Adds a "blog-card" class to the div&lt;/li&gt;
&lt;li&gt;Sets a "data-id" attribute on the div with the value of current post id to uniquely identify each blog post card (in case you want control for individual cards).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's one of the most important parts of the &lt;code&gt;blogCardHTML&lt;/code&gt; content we created above: We set the href attribute to the individual post.html page, but, and &lt;strong&gt;here it is&lt;/strong&gt;, we add in a &lt;code&gt;slug&lt;/code&gt; parameter which is the value of the blog post slug. This is how the individual post.html page knows what blog post it's supposed to be loading when navigated to.&lt;/p&gt;

&lt;p&gt;Now that we know which post is supposed to be showing, all we have to do is create the HTML structure for our blog post and load in the post data, similar to how we did for the blogCards - except we're not doing a loop this time. Let's apply the logic...&lt;/p&gt;

&lt;h2&gt;
  
  
  Dynamically building the individual blog post page
&lt;/h2&gt;

&lt;p&gt;When our post.html file loads, we already know that we're going to have a post slug as a parameter. &lt;strong&gt;This is how we know which post to work with, so let's set it up&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;At the top of blogPost.js, import &lt;code&gt;blogPosts&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You may know about the &lt;code&gt;window.location&lt;/code&gt; object. But, to isolate the query parameters from the URL, we can access the &lt;code&gt;search&lt;/code&gt; property on window.location, and we'll pass that into the &lt;code&gt;URLSearchParams&lt;/code&gt; interface:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;`const params = new URLSearchParams(window.location.search);&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;`The URLSearchParams interface exposes a 'get' utility method allowing us to retrieve the exact parameter we're looking for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;const slug = params.get("slug");&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Perfect. Now, let's find the exact post:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;const blogPost = blogPosts.find((post) =&amp;gt; post.slug === slug);&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Like with the blogCards, let's grab the blog post container on our post.html file so that we know where to inject the post after we build it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;const blogPostContainer = document.querySelector("#blogPost");&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Okay, now let's build the blog post HTML structure, and inject into the blog post container, but only on the condition that the post was actually found. For now, we handle the situation where the post isn't found by displaying some simple text.&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%2F16rlr6rsi8wfjxe4smfj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F16rlr6rsi8wfjxe4smfj.png" alt="screenshot of code in blogPost.js" width="800" height="412"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;Go back to the blog listing page. Navigate to different posts, and watch them be displayed onto the page.&lt;/p&gt;

&lt;p&gt;🎉 Congrats! You now know some essential concepts needed to build a dynamic blog system in vanilla JavaScript 🎉&lt;/p&gt;

&lt;p&gt;If all this has made you curious to find out how much more it would take to get this blog to a complete, production-ready level, I encourage you to scratch that itch - It'll be worth it.&lt;/p&gt;

&lt;p&gt;Here's a hint to some modifications and optimizations that would make this project better:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SPA experience

&lt;ul&gt;
&lt;li&gt;load all content and pages using a single div in the index.html file, just like frameworks do&lt;/li&gt;
&lt;li&gt;this will offer better URLs and faster load times since you won't be loading full pages&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Store blog posts in an external db&lt;/li&gt;

&lt;li&gt;Use a mouseover event listener on blog post cards to prefetch html content&lt;/li&gt;

&lt;/ul&gt;

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

&lt;p&gt;&lt;em&gt;Although there are many changes you could make to this project, the point is you don't always need every framework feature. Often, all you need is simplicity.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If this article helped you learn something, I'm so glad. But there are three important takeaways I hope you’ll leave with:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Remember to question things from time to time.&lt;/li&gt;
&lt;li&gt;Go beyond the question, and let your curiosity scratch the itch (think about the solution)&lt;/li&gt;
&lt;li&gt;You don't always need a framework 😏️&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://patriciosalazar.hashnode.dev/you-dont-need-a-framework" rel="noopener noreferrer"&gt;https://patriciosalazar.hashnode.dev/you-dont-need-a-framework&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>html</category>
      <category>programming</category>
    </item>
    <item>
      <title>How to Use Laravel Sail with Docker for PHP 8.2 (No Global PHP Upgrade Needed)</title>
      <dc:creator>Patricio Salazar</dc:creator>
      <pubDate>Tue, 06 May 2025 18:21:18 +0000</pubDate>
      <link>https://dev.to/patriciosalazar/how-to-use-laravel-sail-with-docker-for-php-82-no-global-php-upgrade-needed-1c28</link>
      <guid>https://dev.to/patriciosalazar/how-to-use-laravel-sail-with-docker-for-php-82-no-global-php-upgrade-needed-1c28</guid>
      <description>&lt;p&gt;How to Use Laravel Sail with Docker for PHP 8.2 (No Global PHP Upgrade Needed)&lt;/p&gt;

&lt;p&gt;Working on a Laravel project that requires a newer PHP version can be tricky if your system’s PHP is older. My personal computer is a Mac, but my work computer is a Linux machine where I sometimes run small projects for exploring and learning new technologies. In that case, upgrading my global PHP installation is not an option since I’d risk messing with work projects.&lt;/p&gt;

&lt;p&gt;This is exactly the kind of situation Laravel Sail is for. Laravel Sail lets you use Docker to run your Laravel app with the required dependencies, all while keeping your global system versions untouched.&lt;/p&gt;

&lt;p&gt;Here is how I set up new Laravel projects on a Linux computer (works on macOS, Linux, or Windows via WSL2) with Sail and the latest versions of PHP (without touching the global version).&lt;/p&gt;

&lt;h2&gt;
  
  
  First, get your project and terminal ready
&lt;/h2&gt;

&lt;p&gt;Once you have your Laravel project files ready it’s time to run some commands.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If you’re using a Laravel starter kit with Inertia and React or Vue, the installation process might have already run &lt;code&gt;npm install&lt;/code&gt; for you. If not, go ahead and run that. Otherwise, skip this command.&lt;/li&gt;
&lt;li&gt;Now, try running &lt;code&gt;composer install&lt;/code&gt;. If you get an error because you have an older version of PHP than required…good! We’re on the same page.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next, we need to run a temporary Docker container to get the latest versions of the required dependencies. Run this command:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;&lt;code&gt;docker run --rm \ -u "$(id -u):$(id -g)" \ -v "$PWD":/var/www/html \ -w /var/www/html \ laravelsail/php82-composer:latest \ composer install&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What the above command does&lt;/strong&gt;: It spins up a temporary Docker container using the Laravel Sail PHP 8.2 image, executes composer install inside the project folder, and then stops and removes the container.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;docker run --rm&lt;/code&gt;: Start a new container, and remove it (–rm) when it’s done (so it won’t hang around on your system).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-u "$(id -u):$(id -g)&lt;/code&gt;: Run the container with your user’s UID:GID. This ensures any files Composer creates (like the vendor folder) will have the correct permissions matching your user, not root.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-v "$PWD":/var/www/html&lt;/code&gt;: This command mounts your current host directory into the container at &lt;code&gt;/var/www/html&lt;/code&gt;, allowing the container to access and modify files in your project.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-w /var/www/html&lt;/code&gt;: This sets the working directory inside the container to &lt;code&gt;/var/www/html&lt;/code&gt;, ensuring that subsequent commands run in the correct context.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;laravelsail/php82-composer:latest&lt;/code&gt;: This specifies the Docker image to use. It’s an image with PHP 8.2 and Composer installed (provided by Laravel Sail).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;composer install&lt;/code&gt;: This is the command we want to run inside the container. It will install all the dependencies as if you ran it on a machine with PHP 8.2.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Configure Sail and Docker
&lt;/h2&gt;

&lt;p&gt;Run the Sail installation (Artisan) command inside Docker. This command publishes the &lt;code&gt;docker-compose.yml&lt;/code&gt; file and updates your &lt;code&gt;.env&lt;/code&gt; with environment variables for Docker.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: If you want to use a different database in your project rather than the default MySQL, add the &lt;code&gt;--with=&lt;/code&gt; flag to the command. To keep the default MySQL db, omit the additional flag.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;p&gt;&lt;code&gt;docker run --rm \ -u "$(id -u):$(id -g)" \ -v "$(pwd)":/var/www/html \ -w /var/www/html \ laravelsail/php82-composer:latest \ php artisan sail:install --with=pgsql&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;This docker command is similar to the previous one, except now we’re executing the artisan sail:install setting up Docker/Sail along with the optional PostgreSQL db configuration.&lt;/p&gt;

&lt;p&gt;You should see a &lt;code&gt;docker-compose.yml&lt;/code&gt; file in the root of your project now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: The values added to your &lt;code&gt;.env&lt;/code&gt; (and the ones in &lt;code&gt;docker-compose.yml&lt;/code&gt;) are mostly default/local settings. For example, Sail might set &lt;code&gt;DB_PASSWORD=password&lt;/code&gt; and &lt;code&gt;DB_USERNAME=sail&lt;/code&gt; (or &lt;code&gt;postgres&lt;/code&gt;) for your database, which are local credentials for the Docker containers. If you’re anything like me and have multiple Laravel projects running at the same time, you might need to adjust port numbers. For instance, you probably need to add &lt;code&gt;APP_PORT&lt;/code&gt; in the &lt;code&gt;.env&lt;/code&gt; file and customize the value with something like &lt;code&gt;APP_PORT=7070&lt;/code&gt; to have your app run on port &lt;code&gt;7070&lt;/code&gt; instead of the default port &lt;code&gt;80&lt;/code&gt;, where &lt;code&gt;80&lt;/code&gt; is already in use. Just make sure you update your &lt;code&gt;docker-compose.yml&lt;/code&gt; file to match this. The same is true for &lt;code&gt;SERVER_PORT&lt;/code&gt;, and &lt;code&gt;SERVER_HOST&lt;/code&gt; (and &lt;code&gt;VITE_PORT&lt;/code&gt;, if you have an inertia frontend).&lt;/p&gt;

&lt;h2&gt;
  
  
  It’s time to launch Sail
&lt;/h2&gt;

&lt;p&gt;Run:&lt;br&gt;
&lt;code&gt;./vendor/bin/sail up -d&lt;/code&gt;. OR&lt;br&gt;
&lt;code&gt;sail up -d&lt;/code&gt;, if you already created the alias for sail. The &lt;code&gt;-d&lt;/code&gt; flag runs containers in the background and frees up your terminal shell.&lt;/p&gt;

&lt;p&gt;At this point, the above command should run successfully.&lt;/p&gt;

&lt;p&gt;If you cloned a repo your &lt;code&gt;.env&lt;/code&gt; file won’t have an app key value. If this is true for you, run this command while Sail is running: &lt;code&gt;sail artisan key:generate&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Now, run migrations: &lt;code&gt;sail artisan migrate&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you’re using Inertia, this is where you want to start the frontend application: &lt;code&gt;sail composer run dev&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Congrats, now you can happily code to your programmer hearts content. Go build, but don’t forget to ship!&lt;/p&gt;

</description>
      <category>php</category>
      <category>laravel</category>
      <category>docker</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Getting My First Users From Building Something Unexpected</title>
      <dc:creator>Patricio Salazar</dc:creator>
      <pubDate>Tue, 17 Dec 2024 07:38:23 +0000</pubDate>
      <link>https://dev.to/patriciosalazar/getting-my-first-users-from-building-something-unexpected-20kp</link>
      <guid>https://dev.to/patriciosalazar/getting-my-first-users-from-building-something-unexpected-20kp</guid>
      <description>&lt;p&gt;
This one has to start with the story. I’ll try to keep it short.
&lt;/p&gt;



&lt;h2&gt;A Random Memory&lt;/h2&gt;



&lt;p&gt;
When I was younger, say in my teens, I would do this thing in my head where I would double numbers. Sometimes random numbers, but most of the time from the number 2. So for example, 2, 4, 8, 16, etc. And I put a mental timer on it, so my goal was to see how fast I could do it without thinking much and see how far I could take it.
&lt;/p&gt;



&lt;p&gt;
Side-note: I don’t know why I did that. I was never big on math. It was like a mental fidget during random in-between moments.
&lt;/p&gt;



&lt;p&gt;
I had absolutely forgotten that I did that. Three months ago, I suddenly remembered while I was having lunch with my wife. I asked if she’d ever done anything like that, and yup, you guessed it—she hadn’t. Now, I’ve never been interested in building a web game, but one of the next thoughts I had was “I have to build this thing”.
&lt;/p&gt;



&lt;p&gt;
Okay stop. Story over. There you have it, that’s the unexpected thing. I built a web game. BUT, please don’t leave just yet. I also want to tell you about what happened after I built the game.
&lt;/p&gt;



&lt;h2&gt;Building and Sharing&lt;/h2&gt;



&lt;p&gt;
Building the game took me about a couple days, it was done in a weekend (this post isn’t about the code, so I won’t get into that). I didn’t focus on making it look fancy or anything like that. I made it for myself, as an ode to the memory of an old mental fidget. I did however, know I would take a chance on sharing it. After all, if people like it, they’ll like it for the experience, not a shiny UI. By the way, it’s called &lt;a href="https://playdoubles.org" rel="noopener noreferrer"&gt;Doubles&lt;/a&gt;.
&lt;/p&gt;



&lt;p&gt;
Enter Reddit. I went looking for the right subreddits and ended up posting in these:
&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;r/math&lt;/li&gt;
&lt;li&gt;r/gamedev&lt;/li&gt;
&lt;li&gt;r/IndieDev&lt;/li&gt;
&lt;li&gt;r/learnmath&lt;/li&gt;
&lt;li&gt;r/WebGames&lt;/li&gt;
&lt;li&gt;r/IndieGaming&lt;/li&gt;
&lt;/ul&gt;



&lt;p&gt;
Sharing on Reddit taught me that the right audience will amplify visibility.
&lt;/p&gt;



&lt;p&gt;
Right away, and to my complete surprise, strangers on the internet loved it. I got responses like this:
&lt;/p&gt;
&lt;br&gt;

&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fnsdysbxlabtmtxscxqvw.supabase.co%2Fstorage%2Fv1%2Fobject%2Fpublic%2Fpersonal-website%2FDoubles%2FDoubles_Reddit_1.png" alt="Reddit user comment challenging and exciting nature of the timer aspect of Doubles game" width="748" height="142"&gt;
&lt;br&gt;&lt;br&gt;

&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fnsdysbxlabtmtxscxqvw.supabase.co%2Fstorage%2Fv1%2Fobject%2Fpublic%2Fpersonal-website%2FDoubles%2FDoubles_Reddit_2.png" alt="Reddit user comment stating he shared the Doubles game in a group chat with friends" width="585" height="114"&gt;
&lt;br&gt;&lt;br&gt;

&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fnsdysbxlabtmtxscxqvw.supabase.co%2Fstorage%2Fv1%2Fobject%2Fpublic%2Fpersonal-website%2FDoubles%2FDoubles_Reddit_3.png" alt="Reddit user comment posted his share-able score and saying how funny it is for addicts to calculation" width="380" height="228"&gt;
&lt;br&gt;&lt;br&gt;

&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fnsdysbxlabtmtxscxqvw.supabase.co%2Fstorage%2Fv1%2Fobject%2Fpublic%2Fpersonal-website%2FDoubles%2FDoubles_Reddit_4.png" alt="Reddit user comment about Doubles stating how fun it is to see how high you can go" width="440" height="124"&gt;
&lt;br&gt;&lt;br&gt;&lt;br&gt;

&lt;p&gt;
🤯🤯🤯 As the title of this post suggests, these are my first-ever users. A.K.A. you’re watching me create my first public side project, learn how to get and interact with users, and iterate on a project based on those interactions.
&lt;/p&gt;



&lt;p&gt;
I have to say, getting users on something you built, and having positive feedback has to be one of the best feelings ever. Not in an ego way, in a purely shocking way. It’s gotta be the “indie dev’s” version of a musician’s first time hearing themselves on the radio. Alright, chill—I’m dating myself 🤣.
&lt;/p&gt;



&lt;p&gt;
The first few days my Cloudflare analytics showed somewhere around &lt;strong&gt;200—300 users&lt;/strong&gt; had visited the site. That was incredible already. Then, days after not having posted anything, I checked the analytics and there was a spike of &lt;strong&gt;1K+ visitors overnight!&lt;/strong&gt; Whaat?? Excuse mee?! I started investigating.
&lt;/p&gt;



&lt;p&gt;
It turns out they were all referred by &lt;a href="https://B3ta.com" rel="noopener noreferrer"&gt;B3ta.com&lt;/a&gt;. A UK based newsletter. I’m not sure how long they’ve been around, but they claim it “currently has nearly 80,000 subscribers”. Someone on their team must have seen my game, liked it enough, and shared it to their subscriber base. They also share a version of their newsletter on their website, and guess what? I FOUND IT. Here’s what they shared:
&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%2Fnsdysbxlabtmtxscxqvw.supabase.co%2Fstorage%2Fv1%2Fobject%2Fpublic%2Fpersonal-website%2FDoubles%2FB3ta_write_up_on_Doubles.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fnsdysbxlabtmtxscxqvw.supabase.co%2Fstorage%2Fv1%2Fobject%2Fpublic%2Fpersonal-website%2FDoubles%2FB3ta_write_up_on_Doubles.png" alt="Comedic summary of Doubles game by B3ta newsletter" width="609" height="161"&gt;&lt;/a&gt;&lt;br&gt;
&lt;br&gt;&lt;br&gt;&lt;/p&gt;

&lt;p&gt;
Hahaha. Good by me.
&lt;/p&gt;



&lt;p&gt;
Fast forwarding a bit, I saw traffic from them trickle in for a long time. Nothing as big as the first couple days after they shared, but it was amazing enough. I’ll never forget that.
&lt;/p&gt;



&lt;p&gt;
Since then, I still haven’t shared anything new, but I saw Doubles get picked up by another couple sites. One was &lt;a href="https://www.mattrutherford.co.uk/" rel="noopener noreferrer"&gt;this guy’s website&lt;/a&gt;. Shoutout to him. I saw a small amount come from there, but he never had to do that in the first place. Another is a site called &lt;a href="https://cloudhiker.net/" rel="noopener noreferrer"&gt;CloudHiker&lt;/a&gt;. I see a couple visitors from that site every day. I’m not sure how it ended up there.
&lt;/p&gt;



&lt;p&gt;
Traction continued for a couple months every single day all on its own. I saw anywhere from 20-80 regular users a day. I know because they were navigating directly to the site, not from any referrer.
&lt;/p&gt;



&lt;h3&gt;Monetization&lt;/h3&gt;



&lt;p&gt;
Nothing to report here, lol!
&lt;/p&gt;

&lt;p&gt;
I tried to figure out a way, but this kind of site is not easy to monetize. And as far as ads go, no worthwhile ad service wants to serve ads on my little game website. Oh well. This is something I will consider more deeply ahead of future projects, but not everything needs to make money.
&lt;/p&gt;



&lt;h2&gt;
How’s It Doing Today?
&lt;/h2&gt;



&lt;p&gt;
Truthfully, it’s died down hahaha. I never talked about it again, and obviously I think that was a mistake, but you can consider this the start of me talking about it again. You can still play it here &lt;a href="https://playdoubles.org" rel="noopener noreferrer"&gt;Doubles&lt;/a&gt;.
&lt;/p&gt;



&lt;p&gt;
&lt;strong&gt;Here’s what I learned&lt;/strong&gt;:
&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
Picking the right project matters.
&lt;/li&gt;
&lt;li&gt;
As a solo creator, I can make decisions, build, iterate, fail, and learn fast.
&lt;/li&gt;
&lt;li&gt;
Caring about user feedback was highly valuable. Talking to the users and responding to them as soon as I implemented a feature request or bug fix is major and made them feel closer to the project. For example, after working with one of my users requests, he replied like this: &lt;span&gt;“Love to see a dev actively responding and excited for feedback :)”&lt;/span&gt;
&lt;/li&gt;
&lt;li&gt;
You spent time and effort building something, you should do the same for communicating and marketing it.
&lt;/li&gt;
&lt;li&gt;
Some users will give you the best ideas to make your project better. Build relationships with them.
&lt;/li&gt;
&lt;/ul&gt;



&lt;p&gt;
There you have it. That’s my story of getting my very first users from a game.
&lt;/p&gt;

&lt;p&gt;
Thanks for sticking around until the end.
&lt;/p&gt;



&lt;p&gt;
If you enjoyed this content, stay tuned for more updates on the projects I build, along with more stories and lessons, by subscribing below (no spam). And if you haven't already, click on one of the emoji’s on this page!
&lt;/p&gt;



&lt;p&gt;Until next time!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>gamedev</category>
      <category>sideprojects</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>How I Added CSV Importing In My React-Node.js Project</title>
      <dc:creator>Patricio Salazar</dc:creator>
      <pubDate>Wed, 11 Dec 2024 21:20:36 +0000</pubDate>
      <link>https://dev.to/patriciosalazar/how-i-added-csv-importing-in-my-react-nodejs-project-2mij</link>
      <guid>https://dev.to/patriciosalazar/how-i-added-csv-importing-in-my-react-nodejs-project-2mij</guid>
      <description>&lt;p&gt;&lt;a href="https://touchbaseapp.co/" rel="noopener noreferrer"&gt;Touch Base&lt;/a&gt; was fine. It was a cool project. It worked. But, let's face it—was it usable? (Touch Base is a full stack React contact management app that I made).&lt;/p&gt;

&lt;p&gt;I was thinking about this and realized something obvious. When a user starts using Touch Base they have to add contacts manually. Which might be fine if you have 5 contacts. If you have 1,000 contacts you want to add, this sucks… and you probably won't want to use this system. So of course, I knew I had to add the ability to import contacts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Researching Options&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My first Google search was “csv importers”, or something like that. I looked through some of the options available and found FlatFile. Their main heading read “The fastest way to collect, onboard and migrate data.” Perfect… except, it wasn't all that for me. Now, this is probably my fault (they seem like an amazing service) but the process of implementing their importer was taking more effort than I was willing to put in for this. This is the perfect time for a little sidebar context:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Lately, I've been really valuing scrappiness. I want to get things done, fast. This isn't about cutting corners, I just don't want to have any excuses or unnecessary delays. After all, I'm just one guy working on side projects. So my current attitude is fail, learn, and iterate fast. All while doing good work.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Back to FlatFile. As much as I wanted to use their promising software I asked myself if I really needed all their bells and whistles and if fighting their docs was worth it. Definitely not. So I went back to my search and landed on Papa Parse. I recalled seeing it in my previous search. Their main heading read “The powerful, in-browser CSV parser for big boys and girls.” 😆 I was in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Implementation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;First things first, I added a POST route to my API.&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%2Fw64z8cd1tqj30thqbxpj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw64z8cd1tqj30thqbxpj.png" alt="'import-contacts API POST route'" width="800" height="108"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;verifyToken&lt;/code&gt; is a function I use in all my routes that does exactly that—verifies the users id token. I use multer in my app which is a node.js middleware for handling file uploads. &lt;code&gt;upload.single('file')&lt;/code&gt; is a multer function that helps me upload files to my s3 bucket.&lt;/p&gt;

&lt;p&gt;Inside the route, I grab the user id and file through destructuring.&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%2Fe4giv8u7dsgs66y5tybz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe4giv8u7dsgs66y5tybz.png" alt="code displaying the extraction of a user id and file properties from the request through the use of destructuring" width="800" height="177"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Like the npm package docs for Papa Parse state, “Papa Parse can parse a Readable Stream instead of a File when used in Node.js environments (in addition to plain strings).”&lt;/p&gt;

&lt;p&gt;So I prepared to stream the file directly to Papa Parse by creating said stream from my s3 bucket as well as an empty array to hold the results data. Can't forget about handling potential errors.&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%2Fv2byqjkj7v6o3ux2odpg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv2byqjkj7v6o3ux2odpg.png" alt="create a read stream of a file from an s3 bucket" width="800" height="407"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then I finally pass the stream to Papa Parse, set my config options and handle any errors coming from the results.&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%2F04s93550zlkpq8kzsmuo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F04s93550zlkpq8kzsmuo.png" alt="passing the stream directly to Papa Parse" width="800" height="340"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the code above, &lt;code&gt;complete&lt;/code&gt; is a Papa Parse property that takes a callback function. It executes once the parsing is complete. I then grab a hold of the data provided by &lt;code&gt;results&lt;/code&gt; as &lt;code&gt;parsedData&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;After this, it's time to run some queries on the database and process the contacts. But, I need to store a connection the the db to run the queries on first.&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%2F83x693rrh6rak3x8rz10.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F83x693rrh6rak3x8rz10.png" alt="code displaying the storing of a database connection" width="800" height="225"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This next part is a lot of code in a try catch statement, so I'll just give it to you straight with some comments on it.&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%2Fen78ud9wqgaimi3odqin.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fen78ud9wqgaimi3odqin.png" alt="processing and conditionally inserting contacts data into a PostgresQL database table" width="800" height="1256"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As you can see, I&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fetch existing contacts&lt;/li&gt;
&lt;li&gt;filter out duplicate contacts using emails, since no two emails can be the same&lt;/li&gt;
&lt;li&gt;bulk insert the non-duplicate contacts into the table&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Frontend&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The frontend will be largely specific to my approach of the app, but let's connect the dots here.&lt;/p&gt;

&lt;p&gt;The Import Contacts page does one thing so it's very simple. I use the native file upload button which is really an input.&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%2Fknfdfe64dd0ljzikjerk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fknfdfe64dd0ljzikjerk.png" alt="native input element that accepts .csv files and triggers a handling function" width="800" height="102"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When the input detects a change I trigger a &lt;code&gt;handleFileUpload&lt;/code&gt; function.&lt;/p&gt;

&lt;p&gt;Inside of the &lt;code&gt;handleFileUpload&lt;/code&gt; function, I first set the loading state to true so that I can display my little loading spinner to the user while this process takes place.&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%2Fwg4t9ckx7jo6torr0hqv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwg4t9ckx7jo6torr0hqv.png" alt="code displaying a loading state being set to true at the beginning of a function" width="780" height="314"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When a user successfully uploads their .csv file, I append it to a new &lt;code&gt;formData&lt;/code&gt; object and send it to my backend route above to process it. I directly use a fetch request here since it's the only place in my app that's going to hit the &lt;code&gt;/import-contacts&lt;/code&gt; endpoint. If another part of my app needed to hit the route, I would store the request using context and use that to avoid repeating code. Also, you can see the error handling I have set up…&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%2Fv0l0nvylwp1w0lixoc2n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv0l0nvylwp1w0lixoc2n.png" alt="code displaying the sending of a file and handling of the response from an api endpoint" width="800" height="1042"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Upon successful handling of the file or if it errors out, the loading state gets set back to false, and I trigger an appropriate toast alert to let the user know exactly what happened in a nice way.&lt;/p&gt;

&lt;p&gt;It feels so nice to log in, upload a .csv file of contacts, get a successful toast alert, and then see all of the new contacts populated in your account. And it's so quick. You might see the loading spinner for just a second. The bulk insert query also helps a lot there.&lt;/p&gt;

&lt;p&gt;From Maybe Usable to Usable&lt;/p&gt;

&lt;p&gt;Before adding this feature I wondered how usable the app truly was. Now, there's no question about that. Although it wasn't super complex, it's a feature you would expect to see in this type of application so I found it a requirement to implement. I think it makes it a little more serious of a project. Aside from that, I've never done anything with .csv files which made this super fun to work on. Papa Parse integrated so well with all the tools I was already using which made it super easy. I definitely recommend using it.&lt;/p&gt;

&lt;p&gt;If you made it this far, cheers to you for reading this 🥂...&lt;br&gt;
and cheers to software that doesn't suck 🥂&lt;/p&gt;

&lt;p&gt;p.s I'm still wondering if my project sucks 😂&lt;br&gt;
If you want to check it out &lt;a href="https://touchbaseapp.co/" rel="noopener noreferrer"&gt;here's the link again&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Til next time!&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>react</category>
      <category>node</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
