<?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: Billy Le</title>
    <description>The latest articles on DEV Community by Billy Le (@billyle).</description>
    <link>https://dev.to/billyle</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%2F145741%2Fa04413de-c371-42ba-b0c7-f85bbc9d46a0.png</url>
      <title>DEV Community: Billy Le</title>
      <link>https://dev.to/billyle</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/billyle"/>
    <language>en</language>
    <item>
      <title>Common Errors for New Flutter Developers: Tips and Fixes</title>
      <dc:creator>Billy Le</dc:creator>
      <pubDate>Thu, 16 May 2024 00:00:00 +0000</pubDate>
      <link>https://dev.to/billyle/common-errors-for-new-flutter-developers-tips-and-fixes-4ahi</link>
      <guid>https://dev.to/billyle/common-errors-for-new-flutter-developers-tips-and-fixes-4ahi</guid>
      <description>&lt;p&gt;I started learning Flutter this year to expand into different areas of development.&lt;/p&gt;

&lt;p&gt;Because I'm new, I don't know my way around the ecosystem yet. Some minor setbacks are not unusual whenever I'm coming to a new technology or programming language.&lt;/p&gt;

&lt;p&gt;Flutter can be frustrating in the beginning, but once you're around the bend, it becomes easier to work with.&lt;/p&gt;

&lt;p&gt;I'm here to share with you some common errors that kept popping up and how I was able to fix them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installation problems and fixes
&lt;/h2&gt;

&lt;p&gt;I found that installing manually by downloading a zip file gave me fewer errors than doing the installation via VSCode.&lt;/p&gt;

&lt;p&gt;I don't remember all the details but somehow the PATH didn't register correctly. Whenever I ran, &lt;code&gt;dart run build_runner build -d&lt;/code&gt;, to generate code, I received errors and I couldn't figure it out even though &lt;code&gt;flutter doctor -v&lt;/code&gt; gave me all green check marks.&lt;/p&gt;

&lt;p&gt;Somewhere along the way, the &lt;code&gt;dart&lt;/code&gt; executable wasn't in my path and unsure where it exists.&lt;/p&gt;

&lt;p&gt;So if you're starting your journey with Flutter, I suggest installing by downloading the zip file and carefully following the instructions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Clean and repair cache
&lt;/h2&gt;

&lt;p&gt;When I find that something goes wrong with Flutter, this is usually my first approach. I run these commands in order before I start researching for a solution.&lt;/p&gt;

&lt;p&gt;Usually, it's a hit or miss but it gives me peace-of-mind that Flutter might not be the culprit and it could be something else like Xcode or a misconfigured setting.&lt;/p&gt;

&lt;p&gt;If you don't have a build_runner, you can skip steps 3 &amp;amp; 4.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;code&gt;flutter clean&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;flutter pub cache repair&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dart run build_runner clean&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dart run build_runner build -d&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;flutter pub get&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;flutter doctor&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Cocoapods errors
&lt;/h2&gt;

&lt;p&gt;Cocoapods is a dependency manager used by Xcode and it's a requirement if you plan to build any apps for Apple.&lt;/p&gt;

&lt;p&gt;This section is dedicated to some issues I ran into when working with Flutter.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cocoapods not installed error
&lt;/h3&gt;

&lt;p&gt;This one was quite frustrating and recurring.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Launching lib/main.dart on iPhone 15 Pro Max in debug mode...

Warning: CocoaPods not installed. Skipping pod install.

CocoaPods is used to retrieve the iOS and macOS platform side's plugin code that responds to your plugin usage on the Dart side.

Without CocoaPods, plugins will not work on iOS or macOS.

For more info, see https://flutter.dev/platform-plugins

To install see https://guides.cocoapods.org/using/getting-started.html#installation for instructions.

CocoaPods not installed or not in valid state.

Error launching application on iPhone 15 Pro Max.

Exited (1).

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can check if Cocoapods is installed by running &lt;code&gt;gem list | grep cocoapods&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cocoapods (1.15.2)
cocoapods-core (1.15.2)
cocoapods-deintegrate (1.0.5)
cocoapods-downloader (2.1)
cocoapods-plugins (1.0.0)
cocoapods-search (1.0.1)
cocoapods-trunk (1.6.0)
cocoapods-try (1.2.0)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you see Cocoapods listed there then the issue is not with the installation.&lt;/p&gt;

&lt;p&gt;Here are some possible fixes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Restart VSCode. You need to completely close all VSCode instances before opening the text editor again.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Reinstall Cocoapods:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;run &lt;code&gt;sudo gem uninstall cocoapods&lt;/code&gt;. Select all versions.&lt;/li&gt;
&lt;li&gt;after uninstall is complete, run &lt;code&gt;sudo gem install -n /usr/local/bin cocoapods&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;If using a Ruby Version Manager like &lt;code&gt;rbenv&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;list all available Ruby version &lt;code&gt;rbenv install -l&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;install the latest Ruby version: &lt;code&gt;rbenv install 3.3.0&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;set global to 3.3.0 with &lt;code&gt;rbenv global 3.3.0&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;make sure the &lt;code&gt;.rbenv/bin&lt;/code&gt; and &lt;code&gt;.rbenv/shims&lt;/code&gt; are available to your PATH&lt;/li&gt;
&lt;li&gt;run &lt;code&gt;gem install bundler&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;perform option 2 again&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Cocoapods base configuration
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[!] CocoaPods did not set the base configuration of your project because your project already has a custom config set. In order for CocoaPods integration to work at all, please either set the base configurations of the target `Runner` to `Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig` or include the `Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig` in your build configuration (`Flutter/Release.xcconfig`).

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To fix this issue, you'll need to modify two files where you'll remove the &lt;code&gt;?&lt;/code&gt; from &lt;code&gt;#include?&lt;/code&gt; statement.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In &lt;code&gt;ios/Flutter/Debug.xcconfig&lt;/code&gt;:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;In &lt;code&gt;ios/Flutter/Release.xcconfig&lt;/code&gt;:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You're going to add an extra &lt;code&gt;#include&lt;/code&gt; here for the &lt;code&gt;profile.xcconfig&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"
#include "Generated.xcconfig"

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Target versions not supported
&lt;/h2&gt;

&lt;p&gt;You might run into this issue when the version of your target platform isn't supported by a 3rd-party library that you've installed.&lt;/p&gt;

&lt;p&gt;Here are some examples of what that might look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;uses-sdk:minSdkVersion 16 cannot be smaller than version 23 declared in library [:audioplayers]

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;warning: The iOS Simulator deployment target 'IPHONEOS_DEPLOYMENT_TARGET' is set to 11.0, but the range of supported deployment target versions is 16.0 to 17.0. (in target 'firebase_core' from project 'Pods')

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What you need to do is modify your targets for each platform you're deploying to. In my case, for Android and iOS, I edited these two files to set the &lt;code&gt;minSdkVersion&lt;/code&gt; and the &lt;code&gt;platform :ios&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;android/local.properties&lt;/code&gt; file, add the &lt;code&gt;flutter.minSdkVersion&lt;/code&gt; on a new line and set it to the version you need.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flutter.minSdkVersion=21

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For iOS, go to &lt;code&gt;ios/Podfile&lt;/code&gt; file and uncomment where it says, &lt;code&gt;platform :ios, '&amp;lt;version&amp;gt;'&lt;/code&gt; and specify the version.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Uncomment this line to define a global platform for your project
platform :ios, '16.0'

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're deploying to other platforms, say Windows or macOS, you will need to search the internet on how to update those values.&lt;/p&gt;

&lt;h2&gt;
  
  
  Launch the simulator from VS Code
&lt;/h2&gt;

&lt;p&gt;This one was a bit tricky. When developing for Flutter in VSCode, you'll be using the debugger tool. When you start the debugging tool to launch your Flutter app, the device will often change.&lt;/p&gt;

&lt;p&gt;Sometimes it will launch on my physical iPhone. Sometimes it will launch for my macOS. Neither of which I want since I only want to run an iOS simulator.&lt;/p&gt;

&lt;p&gt;My approach to this is to use the &lt;code&gt;Flutter: Launch Emulator&lt;/code&gt; from the Command Palette to launch the simulator separately, and then run the debug tool.&lt;/p&gt;

&lt;p&gt;To do this, open the Command Palette by using the shortcut, (⌘ + Shift + P).&lt;/p&gt;

&lt;p&gt;Begin typing Flutter and you'll see a list of options you can choose from. When you select &lt;code&gt;Flutter: Launch Emulator&lt;/code&gt;, it will show you all the available simulators that you've installed on your machine.&lt;/p&gt;

&lt;p&gt;Select one and it should open.&lt;/p&gt;

&lt;p&gt;If not, then you may have these next set of issues.&lt;/p&gt;

&lt;h2&gt;
  
  
  Unable to boot the simulator
&lt;/h2&gt;

&lt;p&gt;This pops up for whatever reason. Maybe I tried to do something with Xcode and broke my simulator.&lt;/p&gt;

&lt;p&gt;But no matter what I did, restarting VSCode, uninstalling/reinstalling the simulator, nothing would work.&lt;/p&gt;

&lt;p&gt;That is until I found a solution by deleting the Xcode caches.&lt;/p&gt;

&lt;p&gt;You need to go to your "System Settings &amp;gt; General &amp;gt; Storage &amp;gt; Developer" and click on the "i" icon button. You should see the following page.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--wjVwYL0g--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/xcode-cache-delete.DGPy7Ixw_1au2dX.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--wjVwYL0g--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/xcode-cache-delete.DGPy7Ixw_1au2dX.webp" alt="Locating Xcode Caches" width="800" height="713"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Select "Xcode Caches", click "Delete..." and continue with the prompt.&lt;/p&gt;

&lt;p&gt;From there you should be able to boot your simulator again.&lt;/p&gt;

&lt;p&gt;But you might encounter another error when the simulator has booted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Springboard quit unexpectedly
&lt;/h2&gt;

&lt;p&gt;Another popup that might show up that something went wrong. This time it has something to do with Springboard.&lt;/p&gt;

&lt;p&gt;Maybe this issue stemmed from deleting the Xcode cache but I would never know.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--vMAQ7Bia--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/xcode-springboard.DJJmCIBG_Z1550z.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--vMAQ7Bia--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/xcode-springboard.DJJmCIBG_Z1550z.webp" alt="Springboard quit unexpectedly modal" width="372" height="412"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A quick internet search tells me that Springboard is the application that manages the home screen for iOS devices.&lt;/p&gt;

&lt;p&gt;Not liking the sound of that, I searched for a fix and the only thing that worked for me was to delete Xcode entirely and install it fresh.&lt;/p&gt;

&lt;p&gt;It was a hassle but it worked and was something I could do easily since every conversation about the error was way above my technical knowledge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rename App ID / Bundle ID
&lt;/h2&gt;

&lt;p&gt;Every app deployed to a platform has a unique App ID. In iOS terms, it's called the Bundle ID. These unique names are usually reversed in the manner of a domain name.&lt;/p&gt;

&lt;p&gt;These App IDs must be unique because this is how the platform identifies your app in their App Store. Whether that be the App Store, Google Play Store, etc. you'll need to keep them unique and that's why reversed domain names are standard practice.&lt;/p&gt;

&lt;p&gt;For example, &lt;code&gt;billyle.dev&lt;/code&gt; becomes &lt;code&gt;dev.billyle&lt;/code&gt; as my App ID.&lt;/p&gt;

&lt;p&gt;When you create a new Flutter project either by the command line or by VSCode, it will default the name of your App ID to be &lt;code&gt;com.example.&amp;lt;YOUR_PROJECT_NAME&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I don't like seeing the "example" in there and it's not a real representation of what I'm creating.&lt;/p&gt;

&lt;p&gt;Instead, when starting a new Flutter project use this command and replace &lt;code&gt;com.yourdomain&lt;/code&gt; and &lt;code&gt;app_name&lt;/code&gt; to your liking.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flutter create --org com.yourdomain app_name

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will then create the App ID for you with &lt;code&gt;com.yourdomain.app_name&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A thing I am starting to do is replace &lt;code&gt;app_name&lt;/code&gt; with specific platform names like &lt;code&gt;ios&lt;/code&gt; and &lt;code&gt;android&lt;/code&gt; to keep things organized but you don't need to do this.&lt;/p&gt;

&lt;p&gt;If you find that you're knee-deep in an existing project but your App ID still contains the &lt;code&gt;com.example&lt;/code&gt; name, you can use this &lt;a href="https://pub.dev/packages/rename"&gt;rename-cli&lt;/a&gt; to rename your App ID.&lt;/p&gt;

&lt;p&gt;The instructions are straightforward to use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping this updated
&lt;/h2&gt;

&lt;p&gt;There you have it. This is my current list of common errors and tips when developing in Flutter.&lt;/p&gt;

&lt;p&gt;I hope it helps you on your journey to becoming a Flutter Developer.&lt;/p&gt;

&lt;p&gt;With that, thanks for reading and I hope you have a good one.&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>ios</category>
      <category>android</category>
      <category>mobiledevelopment</category>
    </item>
    <item>
      <title>Adding RSS Feed Content and Fixing Markdown Image Paths in Astro</title>
      <dc:creator>Billy Le</dc:creator>
      <pubDate>Sat, 04 May 2024 00:00:00 +0000</pubDate>
      <link>https://dev.to/billyle/adding-rss-feed-content-and-fixing-markdown-image-paths-in-astro-2o11</link>
      <guid>https://dev.to/billyle/adding-rss-feed-content-and-fixing-markdown-image-paths-in-astro-2o11</guid>
      <description>&lt;p&gt;Having an RSS feed is a nice way to notify your readers when your new content has been published.&lt;/p&gt;

&lt;p&gt;I have a yellow "Subscribe" button at the end of every blog post which points to my &lt;code&gt;rss.xml&lt;/code&gt; file where anyone could subscribe using an RSS reader.&lt;/p&gt;

&lt;p&gt;To do this, AstroJS makes it easy for you to &lt;a href="https://docs.astro.build/en/guides/rss/" rel="noopener noreferrer"&gt;create an RSS feed&lt;/a&gt; by using their official plugin &lt;a href="https://github.com/withastro/astro/tree/main/packages/astro-rss" rel="noopener noreferrer"&gt;@astrojs/rss&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;However, when it comes to adding feed content, there was one little problem...&lt;/p&gt;

&lt;h2&gt;
  
  
  The RSS content image issue
&lt;/h2&gt;

&lt;p&gt;When I first integrated it, I tried adding the full contents of my post to the RSS feed.&lt;/p&gt;

&lt;p&gt;I realized the output image src were all using a relative path that didn't exist because of how Astro works with local images.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbillyle.dev%2F_astro%2Fastro-incorrect-image-path.dFMoIUDk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbillyle.dev%2F_astro%2Fastro-incorrect-image-path.dFMoIUDk.png" alt="Incorrect image path in RSS feed"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I couldn't find a direct fix for this initially so I put it off.&lt;/p&gt;

&lt;p&gt;With that said, I excluded using the post content and simply used the title and description for my RSS feed.&lt;/p&gt;

&lt;p&gt;Now, when I visit an RSS reader like &lt;a href="https://feedly.com" rel="noopener noreferrer"&gt;Feedly&lt;/a&gt; and look for my website, I get a list of feeds.&lt;/p&gt;

&lt;p&gt;It's exactly what I want, but it looks pretty bare-bones.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbillyle.dev%2F_astro%2Ffeedly-rss-feed-bare.BNK8_YAD.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbillyle.dev%2F_astro%2Ffeedly-rss-feed-bare.BNK8_YAD.png" alt="Bare Feedly RSS feed item"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;All a user can do at this point is to follow the link to visit the blog to read it.&lt;/p&gt;

&lt;p&gt;But what if I want users to read from their preferred source?&lt;/p&gt;

&lt;p&gt;Wouldn't it be nice if someone wanted to read on Medium, dev.to, or their own RSS reader?&lt;/p&gt;

&lt;p&gt;That's why I found the want to add the post content and begin cross-posting to other platforms so that my blog has more reach.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cross-posting on dev.to
&lt;/h2&gt;

&lt;p&gt;More recently, I wanted to cross-post over to dev.to to get more visibility on my writings.&lt;/p&gt;

&lt;p&gt;You can connect your RSS feed to dev.to under "Settings -&amp;gt; Extensions".&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbillyle.dev%2F_astro%2Fdev-to-add-rss.BbFBvvAf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbillyle.dev%2F_astro%2Fdev-to-add-rss.BbFBvvAf.png" alt="Add RSS feed in dev.to settings"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you want, you can specify to dev.to that you want your RSS feed to be the canonical URL. Be sure to check the box if this is your desire.&lt;/p&gt;

&lt;p&gt;From there, you can click the "Fetch feed now" button and it will pull all your feed items into your dashboard as draft posts.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbillyle.dev%2F_astro%2Fdev-to-dashboard.orD1xDAV.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbillyle.dev%2F_astro%2Fdev-to-dashboard.orD1xDAV.png" alt="Dev.to dashboard showing draft posts"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Everything works as expected but the contents of the blog weren't included because the RSS feed item didn't have any content that dev.to can use.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbillyle.dev%2F_astro%2Fdev-to-no-content.BDDWTRbP.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbillyle.dev%2F_astro%2Fdev-to-no-content.BDDWTRbP.png" alt="Dev.to draft post with no content"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Before I get to the solution, I want to talk about how AstroJS outputs the image files.&lt;/p&gt;

&lt;h2&gt;
  
  
  AstroJS image optimization
&lt;/h2&gt;

&lt;p&gt;Let's understand a bit what is happening with the images in your markdown.&lt;/p&gt;

&lt;p&gt;In this photo, you can see I have an &lt;code&gt;_images&lt;/code&gt; folder alongside my markdowns that map to a relative path in a blog post.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbillyle.dev%2F_astro%2Fastro-markdown-image-path.Bne8jYNa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbillyle.dev%2F_astro%2Fastro-markdown-image-path.Bne8jYNa.png" alt="Markdown file showing image relative path"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you're using an import alias or relative path that is not the public directory in your markdowns, Astro will copy and optimize the images and place them in a static folder called &lt;code&gt;_astro/&lt;/code&gt; at build time.&lt;/p&gt;

&lt;p&gt;When AstroJS is transforming your markdowns to HTML, it will then replace all the image paths with the one found in the &lt;code&gt;_astro&lt;/code&gt; folder.&lt;/p&gt;

&lt;p&gt;Here is an example of what that folder looks like.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbillyle.dev%2F_astro%2Fastro-image-output.i-PoXrdc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbillyle.dev%2F_astro%2Fastro-image-output.i-PoXrdc.png" alt="Output directory _astro from image optimization"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;By default, AstroJS will give the file names a hash and also convert it to &lt;code&gt;webp&lt;/code&gt; for a smaller footprint.&lt;/p&gt;

&lt;p&gt;Pretty simple, right?&lt;/p&gt;

&lt;p&gt;Okay, it's time to revisit adding the RSS feed content.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the RSS feed content
&lt;/h2&gt;

&lt;p&gt;Using the AstroJS RSS tutorial as a base, we'll add onto it to make sure our images correctly point to a URL.&lt;/p&gt;

&lt;p&gt;This is the basic code to compile your RSS feed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import rss from '@astrojs/rss';
import { getCollection } from 'astro:content';
import sanitizeHtml from 'sanitize-html';
import MarkdownIt from 'markdown-it';
const parser = new MarkdownIt();

export async function GET(context) {
  const blog = await getCollection('blog');
  return rss({
    title: 'Buzz’s Blog',
    description: 'A humble Astronaut’s guide to the stars',
    site: context.site,
    items: blog.map((post) =&amp;gt; ({
      link: `/blog/${post.slug}/`,
      // Note: this will not process components or JSX expressions in MDX files.
      content: sanitizeHtml(parser.render(post.body), {
        allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img'])
      }),
      ...post.data,
    })),
  });
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the &lt;code&gt;parser.render(post.body)&lt;/code&gt;, we are passing in the contents of our markdown into render which includes the relative image paths.&lt;/p&gt;

&lt;p&gt;That is why in our final output for our &lt;code&gt;rss.xml&lt;/code&gt;, we get those incorrect URLs.&lt;/p&gt;

&lt;p&gt;At this point, I had an idea to fix this. What if before I pass the HTML string into &lt;code&gt;sanitizeHtml()&lt;/code&gt;, I modify the markdown or HTML image paths myself, would that work?&lt;/p&gt;

&lt;p&gt;Let's see...&lt;/p&gt;

&lt;h3&gt;
  
  
  Add dependency node-html-parser
&lt;/h3&gt;

&lt;p&gt;We're going to need an HTML parser so we can easily manipulate objects instead of strings.&lt;/p&gt;

&lt;p&gt;Install &lt;a href="https://www.npmjs.com/package/node-html-parser" rel="noopener noreferrer"&gt;node-html-parser&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The Node HTML parser will convert the string output from the rendered markdown from &lt;code&gt;markdown-it&lt;/code&gt; and create a DOM-like HTML structure.&lt;/p&gt;

&lt;p&gt;We can query against this structure like how we would use the DOM API for browsers.&lt;/p&gt;

&lt;p&gt;Install it with your preferred package manager.&lt;/p&gt;

&lt;p&gt;I use pnpm so that command would be &lt;code&gt;pnpm add node-html-parser&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The image relative path fix
&lt;/h3&gt;

&lt;p&gt;Here is my solution to this problem.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import rss from "@astrojs/rss";
import sanitizeHtml from "sanitize-html";
import MarkdownIt from "markdown-it";
import { allPosts } from "@utils/getCollection";
import { parse as htmlParser } from "node-html-parser";
import { getImage } from "astro:assets";

import type { AstroGlobal } from "astro";
import type { RSSFeedItem } from "@astrojs/rss";
const markdownParser = new MarkdownIt();

// get dynamic import of images as a map collection
const imagesGlob = import.meta.glob&amp;lt;{ default: ImageMetadata }&amp;gt;(
  "/src/content/posts/_images/**/*.{jpeg,jpg,png,gif}", // add more image formats if needed
);

export async function GET(context: AstroGlobal) {
  if (!context.site) {
    throw Error("site not set");
  }

  const feed: RSSFeedItem[] = [];

  for (const post of allPosts) {
    // convert markdown to html string
    const body = markdownParser.render(post.body);
    // convert html string to DOM-like structure
    const html = htmlParser.parse(body);
    // hold all img tags in variable images
    const images = html.querySelectorAll("img");

    for (const img of images) {
      const src = img.getAttribute("src")!;

      // Relative paths that are optimized by Astro build
      if (src.startsWith("./")) {
        // remove prefix of `./`
        const prefixRemoved = src.replace("./", "");
        // create prefix absolute path from root dir
        const imagePathPrefix = `/src/content/posts/${prefixRemoved}`;

        // call the dynamic import and return the module
        const imagePath = await imagesGlob[imagePathPrefix]?.()?.then(
          (res) =&amp;gt; res.default,
        );

        if (imagePath) {
          const optimizedImg = await getImage({ src: imagePath });
          // set the correct path
          img.setAttribute(
            "src",
            context.site + optimizedImg.src.replace("/", ""),
          );
        }
      } else if (src.startsWith("/images")) {
        // images starting with `/images/` is the public dir
        img.setAttribute("src", context.site + src.replace("/", ""));
      } else {
        throw Error("src unknown");
      }
    }

    feed.push({
      title: post.data.title,
      description: post.data.description,
      author: `${post.data.author.email} (${post.data.author.name})`,
      pubDate: post.data.pubDate,
      categories: post.data.tags,
      link: `/posts/${post.slug}`,
      // sanitize the new html string with corrected image paths
      content: sanitizeHtml(html.toString(), {
        allowedTags: sanitizeHtml.defaults.allowedTags.concat(["img"]),
      }),
    });
  }

  return rss({
    title: "Billy Le | Blog",
    description:
      "My creative outlet is a reflection of the experiences I've encountered—whether in learning, facing setbacks, or achieving success—as a software developer.",
    site: context.site,
    items: feed,
    stylesheet: "/pretty-feed-v3.xsl",
    xmlns: {
      atom: "http://www.w3.org/2005/Atom",
    },
    customData: [
      "&amp;lt;language&amp;gt;en-us&amp;lt;/language&amp;gt;",
      `&amp;lt;atom:link href="${new URL("rss.xml", context.site)}" rel="self" type="application/rss+xml" /&amp;gt;`,
    ].join(""),
    trailingSlash: false,
  });
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I littered the code with comments but I'll try to break this down to make more sense of it.&lt;/p&gt;

&lt;p&gt;The most important thing part is the &lt;code&gt;import.meta.glob()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Thanks to Henri Fournier, from the Astro Lounge Discord Support Channel, for this tip.&lt;/p&gt;

&lt;p&gt;You can read more about &lt;a href="https://docs.astro.build/en/recipes/dynamically-importing-images/" rel="noopener noreferrer"&gt;dynamically importing your images&lt;/a&gt; and the &lt;code&gt;import.meta.glob()&lt;/code&gt; in more detail.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;imagesGlob&lt;/code&gt; variable holds an object that holds the key as the path, and the value as a dynamic import function.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const imagesGlob = {
  '/src/content/posts/_images/content-collection-in-sync/types-to-any.png': [Function: /src/content/posts/_images/content-collection-in-sync/types-to-any.png],
  '/src/content/posts/_images/creating-toc/remark-toc-md.png': [Function: /src/content/posts/_images/creating-toc/remark-toc-md.png],
  '/src/content/posts/_images/creating-toc/remark-toc-static.png': [Function: /src/content/posts/_images/creating-toc/remark-toc-static.png],
  ...
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each dynamic import contains the &lt;code&gt;ImageMetadata&lt;/code&gt; object which looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export interface ImageMetadata {
    src: string;
    width: number;
    height: number;
    format: ImageInputFormat;
    orientation?: number;
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With that ready to go, I loop over all my blog posts and convert them to HTML using the &lt;code&gt;node-html-parser&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const body = markdownParser.render(post.body);
const html = htmlParser.parse(body);
const images = html.querySelectorAll("img");

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From there, I loop through the images and use the dynamic imports from &lt;code&gt;imagesGlob&lt;/code&gt; to get the correct path from the &lt;code&gt;ImageMetadata&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;And that looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const src = img.getAttribute("src")!;

if (src.startsWith("./")) {
  // remove prefix of `./`
  const prefixRemoved = src.replace("./", "");
  // create prefix absolute path from root dir
  const imagePathPrefix = `/src/content/posts/${prefixRemoved}`;

  // call the dynamic import and return the module
  const imagePath = await imagesGlob[imagePathPrefix]?.()?.then(
    (res) =&amp;gt; res.default,
  );

  if (imagePath) {
    const optimizedImg = await getImage({ src: imagePath });
    // set the correct path
    img.setAttribute(
      "src",
      context.site + optimizedImg.src.replace("/", ""),
    );
  }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rest of the &lt;code&gt;else if/else&lt;/code&gt; statements check if the image is from my public/images directory or if it's an unknown source, I throw an error.&lt;/p&gt;

&lt;p&gt;Finally, I push the feed item into an array and when we sanitize our HTML, we call &lt;code&gt;html.toString()&lt;/code&gt; which is passed into &lt;code&gt;sanitizeHtml()&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Check RSS output fix
&lt;/h3&gt;

&lt;p&gt;Now it's time to see if everything works.&lt;/p&gt;

&lt;p&gt;Run the build command, I'm using &lt;code&gt;pnpm build&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Check the contents of your &lt;code&gt;_dist&lt;/code&gt; folder and look for your RSS XML file.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbillyle.dev%2F_astro%2Fastro-correct-image-path.UU0iNtGI.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbillyle.dev%2F_astro%2Fastro-correct-image-path.UU0iNtGI.png" alt="Corrected image path in RSS feed"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;🥳 Yes! It's looking good and seems to be pointing the the path correctly.&lt;/p&gt;

&lt;p&gt;Okay, time to push it live and test Feedly and dev.to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verifying fix on supported platforms
&lt;/h2&gt;

&lt;p&gt;If you had an RSS feed on Feedly before adding content, they won't update as the &lt;a href="https://groups.google.com/g/feedly-cloud/c/3evZeYOnS2I" rel="noopener noreferrer"&gt;date is encoded on their servers&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Only new content will be updated with the new RSS changes or you change your feed URL.&lt;/p&gt;

&lt;p&gt;Viewing from Feedly, I only have one item that correctly shows the contents of my blog post.&lt;/p&gt;

&lt;p&gt;And the images are working!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbillyle.dev%2F_astro%2Ffeedly-rss-feed-content.u-krpTEn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbillyle.dev%2F_astro%2Ffeedly-rss-feed-content.u-krpTEn.png" alt="Corrected Feedly RSS feed"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's awesome!&lt;/p&gt;

&lt;p&gt;Over on dev.to, I deleted all my drafts with no content.&lt;/p&gt;

&lt;p&gt;I kind of wish they added a multi-select action on this part but it is what it is.&lt;/p&gt;

&lt;p&gt;Once I removed all my old draft posts, I fetched my updated RSS feed using the "Fetch feed now" button.&lt;/p&gt;

&lt;p&gt;And now, I see that all the contents are there ready to be published!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbillyle.dev%2F_astro%2Fdev-to-content.Dol3PGpp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbillyle.dev%2F_astro%2Fdev-to-content.Dol3PGpp.png" alt="Dev.to draft posts with content"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion and improvements
&lt;/h2&gt;

&lt;p&gt;With the RSS feed content in place, we can now grab our blog posts into platforms that support fetching RSS feeds.&lt;/p&gt;

&lt;p&gt;This is great because we can benefit by sharing our blog with platforms and give our readers a choice where they get updates and read new content.&lt;/p&gt;

&lt;p&gt;In this post, I've gone over the issue of the incorrect image path when creating an RSS feed from markdown to XML with AstroJS.&lt;/p&gt;

&lt;p&gt;To fix this, we had to convert the markdown to HTML and modify the image src to use the correct path. That's all thanks to the &lt;code&gt;import.meta.glob()&lt;/code&gt;, a useful Vite utility function.&lt;/p&gt;

&lt;p&gt;After verifying our fix, we can view places like Feedly and dev.to, to continue sharing our posts.&lt;/p&gt;

&lt;p&gt;If you want to see how you can add a featured image per blog post for your RSS. Check out &lt;a href="(https://webreaper.dev/posts/astro-rss-feed-blog-post-images/)"&gt;Web Reaper's blog post&lt;/a&gt; on how you can do that.&lt;/p&gt;

&lt;p&gt;Well, that's all I can think of. Let me know what you think and if there is anything I can add.&lt;/p&gt;

&lt;p&gt;Thanks for reading, and as always, have a good one! 😊&lt;/p&gt;

</description>
      <category>blogging</category>
      <category>contentcreation</category>
      <category>astro</category>
    </item>
    <item>
      <title>How Docker Breathes New Life into My Workflow</title>
      <dc:creator>Billy Le</dc:creator>
      <pubDate>Tue, 30 Apr 2024 00:00:00 +0000</pubDate>
      <link>https://dev.to/billyle/how-docker-breathes-new-life-into-my-workflow-3oe3</link>
      <guid>https://dev.to/billyle/how-docker-breathes-new-life-into-my-workflow-3oe3</guid>
      <description>&lt;p&gt;Have you ever come across a tool and were so impressed by it that you tried to integrate it into your daily workflow? First was TailwindCSS and now I can say that about Docker.&lt;/p&gt;

&lt;p&gt;As a frontend developer first, I haven't considered learning Docker at all since there wasn't a reason for me to do so.&lt;/p&gt;

&lt;p&gt;That is until I launched my self-hosted Coolify which uses Docker itself to deploy my applications.&lt;/p&gt;

&lt;p&gt;Ever since then, I've been practicing and learning Docker on all my new projects.&lt;/p&gt;

&lt;p&gt;And I wished I started sooner.&lt;/p&gt;

&lt;p&gt;Understanding how to use Docker benefits me greatly as it is being used everywhere. Especially in the cloud.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Docker 🐳?
&lt;/h2&gt;

&lt;p&gt;From what I understand, &lt;a href="https://www.docker.com"&gt;Docker&lt;/a&gt; offers containerization products that allow you to package your application as images with all its dependencies and run them as containers.&lt;/p&gt;

&lt;p&gt;Each container is an instance of that image and you can run nearly infinite amounts if you had the resources.&lt;/p&gt;

&lt;p&gt;That's why companies choose to build their infrastructure with containers; they scale well and meet the demands of their business.&lt;/p&gt;

&lt;p&gt;Docker has many different products from CLI tools to desktop applications that help manage your images and containers.&lt;/p&gt;

&lt;p&gt;There is also a Docker Hub, a cloud repository for images that can be hosted publicly or privately and where you can find official images.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do I use Docker?
&lt;/h2&gt;

&lt;p&gt;There are several ways I'm using Docker at the moment. These include:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Using &lt;a href="https://www.docker.com/products/docker-desktop/"&gt;Docker Desktop&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Interacting with the &lt;a href="https://docs.docker.com/reference/cli/docker/"&gt;Docker Client CLI&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Docker as a Version Manager&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Of these three listed, I primarily use #2. Usually, I run &lt;code&gt;docker compose&lt;/code&gt; commands to build and run my images in one go.&lt;/p&gt;

&lt;p&gt;I'll briefly describe how I use them below.&lt;/p&gt;

&lt;h3&gt;
  
  
  Docker Desktop - a must
&lt;/h3&gt;

&lt;p&gt;Docker Desktop is super neat. It does all the Docker things in one single beautiful UI.&lt;/p&gt;

&lt;p&gt;And it's easy to get around and inspect every aspect of it too.&lt;/p&gt;

&lt;p&gt;With Docker Desktop, you can view your images, containers, and volumes. You can also pull in new images or push your own to Docker Hub.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--fYdpOP_M--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/docker-desktop.gSWj-6wm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--fYdpOP_M--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/docker-desktop.gSWj-6wm.png" alt="Docker Desktop" width="800" height="482"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once you have your containers running, you can inspect the container's contents, the logs, and some analytics like CPU and RAM usage.&lt;/p&gt;

&lt;p&gt;You can launch VS Code from here by accessing the dropdown menu of a container.&lt;/p&gt;

&lt;p&gt;I like using Docker Desktop to monitor all my images, containers, volumes, and build history. I also use it to pull one-off images to play with by using the search tools.&lt;/p&gt;

&lt;p&gt;You can easily access this by ⌘K if you're coming from a MacOS device.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--xLf_G86r--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/docker-vs-code.RA-rhZIR.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--xLf_G86r--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/docker-vs-code.RA-rhZIR.png" alt="Docker Extension via VS Code" width="800" height="509"&gt;&lt;/a&gt;In VS Code, I can download the &lt;a href="https://code.visualstudio.com/docs/containers/overview"&gt;Docker extension&lt;/a&gt; that features running docker commands by the right-click menu and inspect container files.&lt;/p&gt;

&lt;p&gt;This is super helpful when you're troubleshooting why the Docker build is missing some dependencies.&lt;/p&gt;

&lt;p&gt;There's &lt;strong&gt;so much&lt;/strong&gt; you can do and I have yet to touch Docker Scout, Docker Swarm, and extensions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dockering through Docker CLI
&lt;/h3&gt;

&lt;p&gt;I like using the CLI since I don't have to leave VS Code to interact with my containers.&lt;/p&gt;

&lt;p&gt;Normally, I run &lt;code&gt;docker compose up --watch&lt;/code&gt; and &lt;code&gt;docker compose down&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;--watch&lt;/code&gt; flag watches for any changes you've specified in the &lt;code&gt;docker-compose.yaml&lt;/code&gt; file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;services:
  app:
      container_name: my_app
      build: .
      develop:
        watch:
          - path: .
            action: sync
            target: /app
            ignore:
              - node_modules/
          - path: package.json
            action: rebuild

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This configuration will watch any changes in my project using the watch fields.&lt;/p&gt;

&lt;p&gt;There are three actions, &lt;code&gt;sync&lt;/code&gt;, &lt;code&gt;rebuild&lt;/code&gt;, and &lt;code&gt;restart&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I'm using &lt;code&gt;sync&lt;/code&gt; to sync any changes done on the host machine, to also reflect in the container. This makes a seamless experience like you're developing locally.&lt;/p&gt;

&lt;p&gt;And if I ever need to install new dependencies, the action &lt;code&gt;rebuild&lt;/code&gt; watches the &lt;code&gt;package.json&lt;/code&gt; file, rebuilds the image, and launches new containers.&lt;/p&gt;

&lt;p&gt;Learning the CLI commands to start, stop, list, and remove images and containers is pretty helpful too.&lt;/p&gt;

&lt;p&gt;When I start using the CLI more frequently, I find myself not having to access the documentation as much anymore.&lt;/p&gt;

&lt;p&gt;Like if I want to run a container with an exposed port, I know that's &lt;code&gt;-p&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If there are environment variables that I want to use, that would be &lt;code&gt;-e&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Once I got the hang of it all, I was spinning up and down containers fairly easily.&lt;/p&gt;

&lt;h3&gt;
  
  
  My new version manager
&lt;/h3&gt;

&lt;p&gt;I use Docker as a way to manage different versions of technologies like PostgreSQL, Redis, etc.&lt;/p&gt;

&lt;p&gt;The reason for this is that I don't have to worry about anything breaking if I'm working on an old project.&lt;/p&gt;

&lt;p&gt;For example, there were some breaking changes with Node when I was using the latest version and working on an old project.&lt;/p&gt;

&lt;p&gt;I would try to run the server and it would spit out some cryptic error.&lt;/p&gt;

&lt;p&gt;A few minutes later, I realized that the project was using an earlier version of Node. 😓&lt;/p&gt;

&lt;p&gt;So I would switch to the correct version of Node to get it working.&lt;/p&gt;

&lt;p&gt;While I can add some assistance for Node by updating the &lt;code&gt;package.json&lt;/code&gt; with an &lt;code&gt;engines&lt;/code&gt; field, it doesn't entirely solve the issue.&lt;/p&gt;

&lt;p&gt;I had to &lt;strong&gt;manually&lt;/strong&gt; switch my Node version and some time was wasted. I don't like that.&lt;/p&gt;

&lt;p&gt;This is why having Docker as a version manager is advantageous. All I needed to do was build the image and run a container. That's all!&lt;/p&gt;

&lt;p&gt;I haven't been using Docker for long so I don't know what all the pros and cons are but I think one downside of this approach is that you're using up more disk space by doing it this way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using Docker in Development
&lt;/h2&gt;

&lt;p&gt;I initially started using &lt;code&gt;docker init&lt;/code&gt;. It's a command that quickly initializes your project with some Docker files.&lt;/p&gt;

&lt;p&gt;However, after a while, I stopped using it since I didn't quite fully understand the generated output.&lt;/p&gt;

&lt;p&gt;That's why I find myself writing the &lt;code&gt;Dockerfile&lt;/code&gt; or &lt;code&gt;docker-compose.yaml&lt;/code&gt; by hand because it gives me a lot of practice and get a grasp of how Docker works.&lt;/p&gt;

&lt;p&gt;I make sure my &lt;code&gt;Dockerfile&lt;/code&gt; can correctly build an image before I start working inside the compose file.&lt;/p&gt;

&lt;p&gt;Here is a simple Dockerfile which uses Node:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Run as a non-privileged user
FROM node:18-alpine
RUN useradd -ms /bin/sh -u 1001 app
USER app

# Install dependencies
WORKDIR /app
COPY package.json package.lock .
RUN npm install

# Copy source files into application directory
COPY --chown=app:app . /app

CMD ["node", "server.js"]

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the &lt;code&gt;docker-compose.yaml&lt;/code&gt; I list out all my apps and services in my project.&lt;/p&gt;

&lt;p&gt;So if I'm on Redis v6 and PostgreSQL v14, I can list those out and add a volume to each.&lt;/p&gt;

&lt;p&gt;Then in my main service app, I add a watch field, which syncs the changes between the host and the container environment.&lt;/p&gt;

&lt;p&gt;This is how it would look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;services:
  redis:
    image: redis:6-alpine
    port:
      - 6379:6379
    volumes:
      - node_app:/data

  db:
    image: postgres:14-alpine
    environment:
      - POSTGRES_USER=${POSTGRES_USER}
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
      - POSTGRES_DB=${POSTGRES_DB}
    port:
      - 5432:5432
    volumes:
      - node_app:/var/lib/postgresql/data

  app:
    container_name: node_app
    build:
      context: .
    environment:
      - POSTGRES_URL=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
      - REDIS_URL=redis:6379
    develop:
      watch:
        - action: sync
          path: .
          target: /app
          ignore:
            - node_modules/
    ports:
      - 8000:8000
volumes:
  node_app:
    external: false

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From here, I would run the &lt;code&gt;docker compose --watch&lt;/code&gt; command and start coding on my app.&lt;/p&gt;

&lt;p&gt;The different containers will all be on one network, all talking to each other by the service name reference.&lt;/p&gt;

&lt;p&gt;All the data is stored in a separate volume which I can easily dispose of once I'm finished.&lt;/p&gt;

&lt;p&gt;That's how easy it is once you've grown accustomed to working with Docker.&lt;/p&gt;

&lt;p&gt;If you want to start learning now, you can view the &lt;a href="https://docs.docker.com/develop/"&gt;Develop with Docker&lt;/a&gt; documentation which covers some basic information.&lt;/p&gt;

&lt;p&gt;But to get further ahead, you have to play around with it to get the hang of it like I did.&lt;/p&gt;

&lt;h2&gt;
  
  
  My plans ahead
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Smooth seas do not make skillful sailors - African Proverb&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Even though Docker made it easy to build images and run containers, in no way did that make me a master.&lt;/p&gt;

&lt;p&gt;I am still learning and dialing down my workflow.&lt;/p&gt;

&lt;p&gt;To improve my skills, I plan to dive deeper when I start deploying to the cloud, working with a container orchestrator, and more.&lt;/p&gt;

&lt;p&gt;Only then could I call myself an expert.&lt;/p&gt;

&lt;p&gt;So far it's been a pleasing experience working with Docker and all their products.&lt;/p&gt;

&lt;p&gt;I hope reading this convinces you to work with Docker if you have yet to do so.&lt;/p&gt;

&lt;p&gt;I promise you'll gain so much from it.&lt;/p&gt;

&lt;p&gt;Well, that's all I've got! Thank you for reading. As always, have a good one!&lt;/p&gt;

</description>
      <category>docker</category>
      <category>devops</category>
    </item>
    <item>
      <title>Registering for Apple and Google Developer Accounts</title>
      <dc:creator>Billy Le</dc:creator>
      <pubDate>Tue, 23 Apr 2024 00:00:00 +0000</pubDate>
      <link>https://dev.to/billyle/registering-for-apple-and-google-developer-accounts-521c</link>
      <guid>https://dev.to/billyle/registering-for-apple-and-google-developer-accounts-521c</guid>
      <description>&lt;p&gt;If you read my previous blog, I said I was going to form an LLC. I decided not to because I felt it was too early in my journey.&lt;/p&gt;

&lt;p&gt;My goal right now is to build mobile apps. The problem is that I can't publish my projects.&lt;/p&gt;

&lt;p&gt;So I signed up for Apple and Google Developer Accounts and wanted to share my research and experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Platform pros and cons
&lt;/h2&gt;

&lt;p&gt;When it comes to comparing Apple and Android for development, I found these metrics important for me as a developer.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Apple&lt;/th&gt;
&lt;th&gt;Google&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;🪧 Developer Account Registration&lt;/td&gt;
&lt;td&gt;$99 yearly&lt;/td&gt;
&lt;td&gt;$25 one-time fee&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;📊 OS Market Share&lt;/td&gt;
&lt;td&gt;27.58% &lt;strong&gt;(Q3 '23)&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;71.72% &lt;strong&gt;(Q3 '23)&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;📋 App Reviewal Process&lt;/td&gt;
&lt;td&gt;~ 48 hours&lt;/td&gt;
&lt;td&gt;~ 2 hours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;💰 Revenue Share Model&lt;/td&gt;
&lt;td&gt;70% to dev / 30% to platform&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;💻 Programming Language&lt;/td&gt;
&lt;td&gt;Swift&lt;/td&gt;
&lt;td&gt;Java, Kotlin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;📱 Emulator Support&lt;/td&gt;
&lt;td&gt;iOS devices, Andriod&lt;/td&gt;
&lt;td&gt;Android&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Apple costs a lot more than Google year-over-year and the strict review guidelines can slow iteration cycles but offer consumers higher-quality products.&lt;/p&gt;

&lt;p&gt;For Google, you can iterate and validate ideas much more quickly.&lt;/p&gt;

&lt;p&gt;You can also see that Apple owns less of the market share than Google but Apple generates more revenue because people are willing to pay more.&lt;/p&gt;

&lt;p&gt;I didn't want to limit myself to one platform so I chose to target both platforms using Flutter as my tool of choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Registration Experience
&lt;/h2&gt;

&lt;p&gt;On both platforms, the registration was pretty straightforward.&lt;/p&gt;

&lt;p&gt;There are a few screens where they will ask you what kind of app you're building.&lt;/p&gt;

&lt;p&gt;The following screenshots are redacted and incomplete but it should give you an idea of what the process would look like.&lt;/p&gt;

&lt;h3&gt;
  
  
  Apple Developer Program
&lt;/h3&gt;

&lt;p&gt;To get started with the &lt;a href="https://developer.apple.com/programs/enroll/"&gt;Apple Developer Program&lt;/a&gt;, you will start on their enrollment page.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--aETMPprw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/apple-enrollment-page.CL1BPcRr.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--aETMPprw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/apple-enrollment-page.CL1BPcRr.jpeg" alt="Apple enrollment page" width="800" height="464"&gt;&lt;/a&gt;Click "Enroll" and you'll be asked to sign in. After signing in, you'll see these next few pages.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--xLAEpIiA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/apple-enrollment-web.Uc0cGH1p.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--xLAEpIiA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/apple-enrollment-web.Uc0cGH1p.jpeg" alt="Apple enrollment - continue to web" width="800" height="464"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--VSTUzSIr--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/apple-enrollment-type.CmuTGKDR.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--VSTUzSIr--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/apple-enrollment-type.CmuTGKDR.jpeg" alt="Apple account type" width="800" height="464"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I chose the Sole Proprietor account since I am not at the point where I need to form an LLC. &lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--sPB-4cdj--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/apple-enrollment-purchase.CH9EFx1n.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--sPB-4cdj--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/apple-enrollment-purchase.CH9EFx1n.jpeg" alt="Apple enrollment purchase page" width="800" height="464"&gt;&lt;/a&gt;This should be the last step but if not, you'll have to enter your payment details.&lt;/p&gt;

&lt;h3&gt;
  
  
  Google Play Console
&lt;/h3&gt;

&lt;p&gt;The same goes for &lt;a href="https://developer.android.com/distribute/console"&gt;Google Play Console&lt;/a&gt;. Sign in and follow the flow.&lt;/p&gt;

&lt;p&gt;Here are some screenshots from the Google Play Console's registration.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--i5cQLmRn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/google-play-get-started.CW5apB4p.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--i5cQLmRn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/google-play-get-started.CW5apB4p.jpeg" alt="Google Play Console - Get Started page" width="800" height="464"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ao08WvUL--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/google-play-requirements.CXrNMDHZ.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ao08WvUL--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/google-play-requirements.CXrNMDHZ.jpeg" alt="Requirements to create a Google Play Developer account" width="800" height="464"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--RW0fYsQa--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/google-play-payment-profile.CFZvVaVi.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--RW0fYsQa--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/google-play-payment-profile.CFZvVaVi.jpeg" alt="Creating a new Payment Profile" width="800" height="464"&gt;&lt;/a&gt;You would create a new Payment Profile if you never had one.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Zh4bA72p--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/google-play-terms.BL9Xu4w4.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Zh4bA72p--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/google-play-terms.BL9Xu4w4.jpeg" alt="Google Play Console - Terms page" width="800" height="464"&gt;&lt;/a&gt;And lastly, you'll come to the Terms page. You can see all the steps on the left-hand side that you'll have to complete.&lt;/p&gt;

&lt;h2&gt;
  
  
  Approval process time
&lt;/h2&gt;

&lt;p&gt;Google &lt;strong&gt;immediately&lt;/strong&gt; activated my account. After activation, you still need to verify with them before you can publish any apps.&lt;/p&gt;

&lt;p&gt;Google will ask you to verify by uploading a valid ID or using a physical Android device.&lt;/p&gt;

&lt;p&gt;For Apple, I thought the approval would take longer. I've read that it may take up to 5 weeks for them to approve them but that wasn't the case for me.&lt;/p&gt;

&lt;p&gt;It only took Apple &lt;strong&gt;one day&lt;/strong&gt; to approve my account.&lt;/p&gt;

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

&lt;p&gt;I know this post wasn't technical or anything but I hope it gives you an idea of what the registration process is for both platforms.&lt;/p&gt;

&lt;p&gt;I'll be doing some mobile development in the coming weeks, so I hope I can share with you those experiences as well.&lt;/p&gt;

&lt;p&gt;Thanks for reading and have a good one!&lt;/p&gt;

</description>
      <category>blogging</category>
      <category>mobiledevelopment</category>
      <category>ios</category>
      <category>android</category>
    </item>
    <item>
      <title>Diving Head First into the Startup Unknowns</title>
      <dc:creator>Billy Le</dc:creator>
      <pubDate>Thu, 18 Apr 2024 00:00:00 +0000</pubDate>
      <link>https://dev.to/billyle/diving-head-first-into-the-startup-unknowns-a44</link>
      <guid>https://dev.to/billyle/diving-head-first-into-the-startup-unknowns-a44</guid>
      <description>&lt;p&gt;After I'd been forced to quit my last employment, I've been idling by since I felt burnt out from that role.&lt;/p&gt;

&lt;p&gt;I didn't have any drive to do anything for about a year but I have been keeping up-to-date on tech matters.&lt;/p&gt;

&lt;p&gt;Watching the tech market plummet with massive layoffs and the rise of AI, I knew I had to do something or risk being left behind.&lt;/p&gt;

&lt;p&gt;I started to pick up Golang as my backend language of choice for its simplicity and speed while also picking up PostgreSQL.&lt;/p&gt;

&lt;p&gt;And then after a conversation with a friend, he recommended using Flutter for mobile development.&lt;/p&gt;

&lt;p&gt;With this new stack, I rolled up my sleeves to work on some projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Project #1: Furfil
&lt;/h2&gt;

&lt;p&gt;Furfil has been spinning in my mind for quite a while.&lt;/p&gt;

&lt;p&gt;It's about creating a platform where pet services can meet customers. It's like Rover but will offer plenty of more services.&lt;/p&gt;

&lt;p&gt;It will be released as a web and mobile app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Project #2: Dotted
&lt;/h2&gt;

&lt;p&gt;Dotted is a project that a friend and I are working on.&lt;/p&gt;

&lt;p&gt;It's an AI mobile app that helps generate itineraries for traveling.&lt;/p&gt;

&lt;p&gt;We're using Flutter to release on mobile, and potentially releasing a web app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Learning how to launch startups
&lt;/h2&gt;

&lt;p&gt;I listened to hours of podcasts about startups but I'm constantly thinking about how I'm supposed to launch my own.&lt;/p&gt;

&lt;p&gt;The majority of the content I've been consuming is very broad and high-level. It doesn't necessarily cover the nitty-gritty details I need to take.&lt;/p&gt;

&lt;p&gt;Like should I start an LLC first or use a sole proprietorship?&lt;/p&gt;

&lt;p&gt;Should I use the beginning of a tax year to form the LLC?&lt;/p&gt;

&lt;p&gt;Where should I set one up? Wyoming? Delaware? Do I need a lawyer from that state?&lt;/p&gt;

&lt;p&gt;Should I have a holding company and hold ownership in another LLC for Dotted since that will be a partnership?&lt;/p&gt;

&lt;p&gt;Can my partner qualify for working on Dotted since they're on an H1-B visa?&lt;/p&gt;

&lt;p&gt;I have so many questions and they are coming in slowly. It's been paralyzing to take action because there is so much to do.&lt;/p&gt;

&lt;h2&gt;
  
  
  The struggles of app development
&lt;/h2&gt;

&lt;p&gt;Development will be always slow on new languages and technologies being learned. As we push forward, there are always an area of unknowns and it takes time and research to figure out the best approach.&lt;/p&gt;

&lt;p&gt;I get mentally exhausted from learning and hitting obstacles that I end up working on my website like this blog post as a way to reclaim my peace.&lt;/p&gt;

&lt;p&gt;And every minute taken away from the projects means time to market is prolonged.&lt;/p&gt;

&lt;p&gt;I'm thinking about starting Furfil from scratch using what I know to keep things simple and fast. But I know Furfil won't scale well with Node and MongoDB in the long run.&lt;/p&gt;

&lt;p&gt;There's a tradeoff to everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next course of action
&lt;/h2&gt;

&lt;p&gt;Until I do something to push the needle forward, I will be in this constant state.&lt;/p&gt;

&lt;p&gt;So I'm starting my new LLC next month. I have a domain ready and I'll announce it when it's finalized.&lt;/p&gt;

&lt;p&gt;I have to learn by doing and learn by failing. If this is not the right decision, I'll learn to live with it and adapt accordingly.&lt;/p&gt;

&lt;p&gt;That's how anyone can find success and every success story is different from one another.&lt;/p&gt;

&lt;p&gt;It's time to write my own story.&lt;/p&gt;

&lt;p&gt;Thanks for reading my rant. I hope you have a good one.&lt;/p&gt;

</description>
      <category>blogging</category>
      <category>startup</category>
    </item>
    <item>
      <title>Keep Astro Content Collection Types in Sync on Git Commit</title>
      <dc:creator>Billy Le</dc:creator>
      <pubDate>Wed, 17 Apr 2024 00:00:00 +0000</pubDate>
      <link>https://dev.to/billyle/keep-astro-content-collection-types-in-sync-on-git-commit-1b2k</link>
      <guid>https://dev.to/billyle/keep-astro-content-collection-types-in-sync-on-git-commit-1b2k</guid>
      <description>&lt;p&gt;I'm not sure when but sometimes the type definitions in the .astro folder keep going out of sync once in a while. Instead of adding the schema definitions from my collections, it replaces them with the &lt;code&gt;any&lt;/code&gt; type.&lt;/p&gt;

&lt;p&gt;Here is a preview of the issue from my Git history:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--yx7u_NoG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/types-to-any.BgAE5NCz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--yx7u_NoG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/types-to-any.BgAE5NCz.png" alt="schema types replaced by any type" width="800" height="459"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is an issue for me since I love working with Typescript and having that safety matters when I'm developing my site.&lt;/p&gt;

&lt;p&gt;According to &lt;a href="https://docs.astro.build/en/guides/content-collections/#the-astro-directory"&gt;Astro's docs&lt;/a&gt;, it runs &lt;code&gt;astro sync&lt;/code&gt; whenever you run &lt;code&gt;astro dev&lt;/code&gt; or &lt;code&gt;astro build&lt;/code&gt; so somehow during development, the types become &lt;code&gt;any&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;We're going to re-sync our types and make sure that we get the results we want by running the &lt;code&gt;astro sync&lt;/code&gt; command again on a Git commit.&lt;/p&gt;

&lt;p&gt;Add a script to your &lt;code&gt;package.json&lt;/code&gt; called &lt;code&gt;sync&lt;/code&gt; or whatever you like and give it the value &lt;code&gt;astro sync&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "scripts": {
    "sync": "astro sync"
  }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll need to have Husky installed in your project for this to work. It's relatively simple to set up and I talked about it &lt;a href="https://dev.to/posts/use-husky-and-node-to-unstage-draft-posts-from-git#what-is-husky"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Inside your &lt;code&gt;.husky/pre-commit&lt;/code&gt; file, add these lines anywhere in the file. I'm using pnpm. Remember to replace "pnpm" with your package manager CLI command to run scripts.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pnpm sync
git add .astro/types.d.ts

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that should do it! Whenever you make a new commit, the pre-commit will fire and it will sync your content collections' schemas perfectly.&lt;/p&gt;

&lt;p&gt;Thanks for reading and have a good one! 😄&lt;/p&gt;

</description>
      <category>blogging</category>
      <category>contentcreation</category>
      <category>javascript</category>
      <category>node</category>
    </item>
    <item>
      <title>Fix Missing 404 Pages for Coolify Static Site Deployments</title>
      <dc:creator>Billy Le</dc:creator>
      <pubDate>Mon, 15 Apr 2024 00:00:00 +0000</pubDate>
      <link>https://dev.to/billyle/fix-missing-404-pages-for-coolify-static-site-deployments-57ap</link>
      <guid>https://dev.to/billyle/fix-missing-404-pages-for-coolify-static-site-deployments-57ap</guid>
      <description>&lt;p&gt;When I first started using Coolify, I came across a bug where my 404 page was not being served.&lt;/p&gt;

&lt;p&gt;In development, it was all smooth sailing. But in production, going to a non-existent URL would always redirect back to the home page.&lt;/p&gt;

&lt;p&gt;This was a bit frustrating since I didn't know where to start to begin debugging.&lt;/p&gt;

&lt;p&gt;As of Coolify v4.0.0-beta.258, I suspect it was how Nixpacks did not correctly configure the server.&lt;/p&gt;

&lt;p&gt;But luckily, I was able to come up with a solution and am here to share that with you if you're in this situation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The solution
&lt;/h2&gt;

&lt;p&gt;Instead of having Nixpacks magically take care of everything, we need to have two things to replace the build pipeline.&lt;/p&gt;

&lt;p&gt;That is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;containerizing our site&lt;/li&gt;
&lt;li&gt;creating a reverse proxy&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The two technologies I chose are Docker and Nginx for this fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install Docker Desktop
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.docker.com/products/docker-desktop/"&gt;Download and install Docker Desktop&lt;/a&gt;, which will contain a graphical user interface for Docker. It comes with all the bells and whistles like the CLI, the background daemon, and Docker Compose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using Docker Init
&lt;/h2&gt;

&lt;p&gt;To get started, you can use the command &lt;code&gt;docker init&lt;/code&gt; which will run an interactive terminal where it can detect what you're using for your project.&lt;/p&gt;

&lt;p&gt;This is what you'll see when the command is entered into the terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Welcome to the Docker Init CLI!

This utility will walk you through creating the following files with sensible defaults for your project:
  - .dockerignore
  - Dockerfile
  - compose.yaml
  - README.Docker.md

Let's get started!

? What application platform does your project use? [Use arrows to move, type to filter]
&amp;gt; Node - (detected) suitable for a Node server application
  Go - suitable for a Go server application
  Python - suitable for a Python server application
  Rust - suitable for a Rust server application
  ASP.NET Core - suitable for an ASP.NET Core application
  PHP with Apache - suitable for a PHP web application
  Java - suitable for a Java application that uses Maven and packages as an uber jar
  Other - general purpose starting point for containerizing your application
  Don't see something you need? Let us know!
  Quit

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Follow the prompts and it will generate a template for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Modifying the Dockerfile
&lt;/h2&gt;

&lt;p&gt;If you look at the Dockerfile, it has some sensible stages like building the dependencies and building the app.&lt;/p&gt;

&lt;p&gt;I'm removing most of the generated comments so it's easier to follow.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# syntax=docker/dockerfile:1

ARG NODE_VERSION=21.4.0
ARG PNPM_VERSION=8.12.0

FROM node:${NODE_VERSION}-alpine as base
WORKDIR /usr/src/app
RUN --mount=type=cache,target=/root/.npm \
    npm install -g pnpm@${PNPM_VERSION}

FROM base as build
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm run build

FROM nginx:stable-alpine3.17 as final
COPY nginx.conf /etc/nginx/nginx.conf
COPY --from=build /usr/src/app/dist /usr/share/nginx/html
EXPOSE 4321

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the last step, I am pulling the latest Nginx image from DockerHub with the 'alpine' tag.&lt;/p&gt;

&lt;p&gt;Then I'm copying over my &lt;code&gt;nginx.conf&lt;/code&gt; file, which we'll create later, into the Nginx directory where it will be used as configuration.&lt;/p&gt;

&lt;p&gt;Finally, from my build stage, I'm copying over my static files into the &lt;code&gt;/usr/share/nginx/html&lt;/code&gt; directory.&lt;/p&gt;

&lt;p&gt;Nginx will serve our static files from this default directory.&lt;/p&gt;

&lt;p&gt;And finally exposing Port 4321, because that's what the local AstroJS development server is running on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create the nginx.conf file
&lt;/h2&gt;

&lt;p&gt;At the root of your project directory, create a &lt;code&gt;nginx.conf&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;The contents of this file will be something similar below for a static site.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;worker_processes 1;

events {
  worker_connections 1024;
}

http {
  server {
    listen 4321;
    server_name _;

    root /usr/share/nginx/html;
    index index.html index.htm;
    include /etc/nginx/mime.types;

    gzip on;
    gzip_min_length 1000;
    gzip_proxied expired no-cache no-store private auth;
    gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript;

    error_page 404 /404.html;
    location = /404.html {
            root /usr/share/nginx/html;
            internal;
    }

    location / {
            try_files $uri $uri/index.html =404;
    }
  }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I won't explain all the details of the file but I want to note four parts.&lt;/p&gt;

&lt;p&gt;Our &lt;code&gt;listen 4321;&lt;/code&gt; matches what we exposed in the Dockerfile.&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;error_page 404 /404.html;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This line is going to serve my custom 404 page built by AstroJS.&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;location = /404.html {
          root /usr/share/nginx/html;
          internal;
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The location of this 404 page is found in &lt;code&gt;/usr/share/nginx/html&lt;/code&gt;&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;location / {
          try_files $uri $uri/index.html =404;
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, at the root path, we'll use a special &lt;code&gt;$uri&lt;/code&gt; variable for a lookup for that specific URL that was entered. The &lt;code&gt;try_files&lt;/code&gt; will continue to look for the file and if it's not found, we'll respond with a 404.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing locally with Docker
&lt;/h2&gt;

&lt;p&gt;Everything is put in place, now it's time to test if this is going to work when we build an image with Docker.&lt;/p&gt;

&lt;p&gt;Make sure that Docker Desktop is opened and in a terminal run the command:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;docker build -t my-static-site .&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Wait for the build to complete and if all goes well, you should see your built image with the tag name &lt;code&gt;my-static-site&lt;/code&gt; in Docker Desktop.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s---0XSQ9Fo--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/my-static-site-image.DKmGDdCE.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s---0XSQ9Fo--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/my-static-site-image.DKmGDdCE.png" alt="The completed my-static-site image in Docker Desktop" width="800" height="482"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Or if you're a terminal type of person, &lt;code&gt;docker images -a&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;REPOSITORY TAG IMAGE ID CREATED SIZE
my-static-site latest fa5fdc1f7169 2 minutes ago 219MB
server latest 18ee5c19b642 14 hours ago 234MB

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we're going to launch and start up a container by running the command:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;docker run -p 4321:4321 my-static-site&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The docker container should be running in your terminal now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
/docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
10-listen-on-ipv6-by-default.sh: info: Getting the checksum of /etc/nginx/conf.d/default.conf
10-listen-on-ipv6-by-default.sh: info: Enabled listen on IPv6 in /etc/nginx/conf.d/default.conf
/docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh
/docker-entrypoint.sh: Launching /docker-entrypoint.d/30-tune-worker-processes.sh
/docker-entrypoint.sh: Configuration complete; ready for start up

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a browser window, go to &lt;code&gt;localhost:4321&lt;/code&gt; and you should see your website. If not, then your nginx might be misconfigured or you are missing something in the Dockerfile.&lt;/p&gt;

&lt;p&gt;If your page loads, try going to a non-existent URL like &lt;code&gt;localhost:4321/this-is-not-a-real-page&lt;/code&gt; and you should be greeted with your custom 404 page!&lt;/p&gt;

&lt;p&gt;If it's a generic Nginx 404 page, you will have to fix that in your &lt;code&gt;nginx.conf&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;Commit your files to Git and then push them to a remote repository like GitHub.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploying on Coolify
&lt;/h2&gt;

&lt;p&gt;All we need to do now is deploy on Coolify.&lt;/p&gt;

&lt;p&gt;I'm assuming you already have a project running on Coolify because you are here.&lt;/p&gt;

&lt;p&gt;On the Configuration page, under "Build Pack", select "Dockerfile".&lt;/p&gt;

&lt;p&gt;Then make sure the "Dockerfile Location" is set to &lt;code&gt;/Dockerfile&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Pb3ieOlG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/coolify-dockerfile-setting.B7wd3Mle.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Pb3ieOlG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/coolify-dockerfile-setting.B7wd3Mle.png" alt="Coolify's Dockerfile configuration" width="800" height="306"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Under the "Network" configuration, enter &lt;code&gt;4321&lt;/code&gt; for "Ports Exposes" and click on "Save".&lt;/p&gt;

&lt;p&gt;When you hit save, make sure to look at the Traefik configuration is also pointing to PORT 4321.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--EE4anYDC--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/coolify-network-setting.BxaAxfTT.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--EE4anYDC--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/coolify-network-setting.BxaAxfTT.png" alt="Coolify's network port configuration" width="800" height="272"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When that's done, click "Redeploy" and Coolify will start building from the Dockerfile we've created.&lt;/p&gt;

&lt;p&gt;If it succeeds, you can go to your live site and start testing.&lt;/p&gt;

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

&lt;p&gt;Hopefully, that fixes your issue! If all this is a little intimidating to you, don't worry, I've been there and you can reach out and get some help.&lt;/p&gt;

&lt;p&gt;Now we know that all we need to do to fix the missing 404-page configuration is to write up our own Dockerfile and an Nginx config.&lt;/p&gt;

&lt;p&gt;Well, that's all for now. Thanks for reading and have a good one!&lt;/p&gt;

</description>
      <category>paas</category>
      <category>selfhosting</category>
      <category>devops</category>
    </item>
    <item>
      <title>Configure a Contact Form Email Server with Resend for Your Website</title>
      <dc:creator>Billy Le</dc:creator>
      <pubDate>Sat, 13 Apr 2024 00:00:00 +0000</pubDate>
      <link>https://dev.to/billyle/configure-a-contact-form-email-server-with-resend-for-your-website-1o9j</link>
      <guid>https://dev.to/billyle/configure-a-contact-form-email-server-with-resend-for-your-website-1o9j</guid>
      <description>&lt;p&gt;Sending an email from a contact form is a necessary task for any front-end developer. Results from a search engine provide a few solutions for implementing this feature.&lt;/p&gt;

&lt;p&gt;They usually are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Use a form provider where you send the data to their API and they will forward the email to your inbox&lt;/li&gt;
&lt;li&gt;Use an email framework like Nodemailer&lt;/li&gt;
&lt;li&gt;Create an SMTP Server to relay emails&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The technical difficulty of implementing these features increases linearly.&lt;/p&gt;

&lt;p&gt;But there is one other solution I found that is least talked about but it's quickly emerging and that is using &lt;a href="https://resend.com/home"&gt;Resend&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Resend?
&lt;/h2&gt;

&lt;p&gt;Resend is a startup aimed at making sending emails easier for developers.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--N6f8q5dF--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/resend-homepage.i4UMGmhO.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--N6f8q5dF--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/resend-homepage.i4UMGmhO.png" alt="Resend homepage - hero section" width="800" height="395"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can send transactional emails, start a marketing campaign, and create beautiful emails using React. And this all comes with a generous free tier.&lt;/p&gt;

&lt;p&gt;You can also add your custom domain where the "from" field will be sent from your domain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resend Domain Config and API Key
&lt;/h2&gt;

&lt;p&gt;Sign up for Resend and verify your email. There are two steps we're going to complete here. You can skip the Custom Domain instructions if that doesn't apply to you.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add your Custom Domain
&lt;/h3&gt;

&lt;p&gt;Once you're verified and logged into Resend, head over to the Domains page in the left panel. You should see a similar page to the one below.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--8yo0VhwZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/resend-domains-page.BV49rHQx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--8yo0VhwZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/resend-domains-page.BV49rHQx.png" alt="Resend Domains page" width="800" height="205"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I already have mine set up but click on "+ Add Domain" and you'll enter your Custom domain and a region.&lt;/p&gt;

&lt;p&gt;After that, you need to go to your DNS provider and enter all the DNS records that Resend provides you on the following page. I have Cloudflare as my DNS provider.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--RxeH6D-C--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/resend-dns-records.CUIgkBaa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--RxeH6D-C--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/resend-dns-records.CUIgkBaa.png" alt="Resend Domains DNS records" width="800" height="394"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There will be a button for you to verify your DNS records. My recommendation is to wait for an hour and then try to verify that your DNS records are correctly configured with your provider.&lt;/p&gt;

&lt;p&gt;You should see all green "Verified" badges next to all the records you need to add.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create a Resend API Key
&lt;/h3&gt;

&lt;p&gt;Once that's done, head over to the API Keys page for Resend. Here I'm redacting my API Key for security purposes but I'll run you through setting up a new API Key with your custom domain.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--7CK2XWeZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/resend-api-keys-page.DM2d5H9c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--7CK2XWeZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/resend-api-keys-page.DM2d5H9c.png" alt="Resend API Keys page" width="800" height="205"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click on "+ Create API Key" and a dialog will pop up.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Enter a name for your API key.&lt;/li&gt;
&lt;li&gt;Select "Sending Access" under Permission since we're only using this for sending emails.&lt;/li&gt;
&lt;li&gt;In the Domain dropdown, select your verified custom domain.&lt;/li&gt;
&lt;li&gt;Finish by clicking "Add"&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--UoMTL5Ld--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/resend-create-api-key.BDSxKzjk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--UoMTL5Ld--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/resend-create-api-key.BDSxKzjk.png" alt="Resend Create API Key dialog" width="600" height="463"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And that's all! Now onto the coding part.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up the Front-end
&lt;/h2&gt;

&lt;p&gt;I'm using AstroJS and Tailwind for styling, but you can use vanilla or another framework if you like. Either works since we just need an HTML and some frontend JavaScript to make POST requests.&lt;/p&gt;



&lt;h3&gt;
  
  
  Create a simple Form element
&lt;/h3&gt;

&lt;p&gt;You'll need a basic contact form markup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;form class="contact-form flex flex-col space-y-4 max-w-md"&amp;gt;
  &amp;lt;input
    type="text"
    name="name"
    class="h-10 px-3 rounded ring-2 ring-neutral-300"
    placeholder="Name"
    required
  /&amp;gt;
  &amp;lt;input
    type="email"
    name="email"
    class="h-10 px-3 rounded ring-2 ring-neutral-300"
    placeholder="Email"
    required
  /&amp;gt;
  &amp;lt;textarea
    name="message"
    class="px-3 py-2 rounded ring-2 ring-neutral-300"
    rows={5}
    placeholder="What would you like to say?"
    required&amp;gt;&amp;lt;/textarea&amp;gt;
  &amp;lt;div class="flex justify-end space-x-2"&amp;gt;
    &amp;lt;button
      type="submit"
      class="px-3 py-1 rounded text-xl text-slate-50 bg-slate-900 dark:bg-slate-700 dark:ring-2 dark:ring-slate-50"
    &amp;gt;
      Submit
    &amp;lt;/button&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/form&amp;gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Basic Form Event Handler
&lt;/h3&gt;

&lt;p&gt;In a script tag, we'll need to target the form and listen for events.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;script&amp;gt;
  const contactForm =
    contactsContainer.querySelector&amp;lt;HTMLFormElement&amp;gt;(".contact-form");

  if (contactForm) {
    contactForm.addEventListener("submit", async (e) =&amp;gt; {
      e.preventDefault();

      const formEl = e.target as HTMLFormElement;
      const formData = new FormData(formEl);

      const requestBody = {
        name: formData.get("name"),
        email: formData.get("email"),
        message: formData.get("message"),
      };

      console.log(requestBody);
    });
  }
&amp;lt;/script&amp;gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You might be using React or some other framework but the idea here is to prepare our front-end code to start sending requests.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test form submission
&lt;/h3&gt;

&lt;p&gt;This is all we're going to need for the time being. Fire up your frontend server then fill out your contact form and hit submit. You should see a log in the dev console with your input.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--V2iL-qaz--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/contact-form-filled.DZ3g6szJ.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--V2iL-qaz--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/contact-form-filled.DZ3g6szJ.png" alt="Contact form filled out" width="635" height="582"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "name": "billy",
  "email": "billy@example.com",
  "message": "hi from contact form"
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Prepare the Back-end
&lt;/h2&gt;

&lt;p&gt;I'm using Bun, which is a JavaScript run-time like Node. You should get very similar results if you're using Node and a web app framework like Express. If you need in that regard, feel free to reach out.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create a &lt;code&gt;.env&lt;/code&gt; file
&lt;/h3&gt;

&lt;p&gt;Create a &lt;code&gt;.env&lt;/code&gt; file. Use the template below since it is what I'm using these properties that will be used by our server.&lt;/p&gt;

&lt;p&gt;You'll need to install the &lt;a href="https://www.npmjs.com/package/dot-env"&gt;dot-env&lt;/a&gt; package if you're using Node. The link will show you how to set it up.&lt;/p&gt;

&lt;p&gt;Be sure to enter your Resend API key here.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SERVER_PORT=3000
RESEND_API_KEY=&amp;lt;API Key from Resend&amp;gt;
EMAIL_TO=hi@billyle.dev
ALLOWED_ORIGIN=billyle.dev

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Install the Resend package
&lt;/h3&gt;

&lt;p&gt;We also need to install the &lt;a href="https://www.npmjs.com/package/resend"&gt;resend&lt;/a&gt; package. We'll instantiate a new Resend object and pass in our API key which will allow us to send our emails.&lt;/p&gt;

&lt;p&gt;This is what a basic setup would look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const resend = new Resend(Bun.env.RESEND_API_KEY);

const { error } = await resend.emails.send({
  from: `${name} &amp;lt;${Bun.env.EMAIL_TO}&amp;gt;`,
  to: [Bun.env.EMAIL_TO],
  subject: `New Message Received From ${name}`,
  react: EmailTemplate({ name, email, message }),
});

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Creating our Bun server
&lt;/h3&gt;

&lt;p&gt;We'll be creating our server next.&lt;/p&gt;

&lt;p&gt;If you're unfamiliar with Bun, there is a web app framework called &lt;a href="https://elysiajs.com/"&gt;ElysiaJS&lt;/a&gt; which is similar to ExpressJS. It comes with some cool features like validation which I'm using below.&lt;/p&gt;

&lt;p&gt;I've added some comments to explain a bit of what's going on.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { Elysia, t } from "elysia";
import { cors } from "@elysiajs/cors";
import { Resend } from "resend";
import { EmailTemplate } from "./email-templates/message";
import winston, { format } from "winston";

const { errors, printf, combine, colorize, timestamp } = format;

// logger for server logs
export const logger = winston.createLogger({
  level: "info",
});

logger.add(
  new winston.transports.Console({
    format: combine(
      errors({ stack: true }),
      colorize({ all: true }),
      timestamp(),
      printf(
        ({ level, message, timestamp, stack }) =&amp;gt;
          `[${timestamp}]:${level}: ${message}${stack ? `\n\n${stack}` : ""}`,
      ),
    ),
  }),
);

const resend = new Resend(Bun.env.RESEND_API_KEY);
const serverPort = Bun.env.SERVER_PORT || 3000;

// creating Elysia instance
new Elysia()
  .use(
    cors({
      methods: ["POST"],
      origin: [
        Bun.env.NODE_ENV === "production"
          ? Bun.env.ALLOWED_ORIGIN
          : "localhost:4321",
      ],
    }),
  )
  .post(
    "/send-email",
    async (context) =&amp;gt; {
      const { name, email, message } = context.body;

      try {
        // the part where we are sending emails
        const { error } = await resend.emails.send({
          from: `${name} &amp;lt;${Bun.env.EMAIL_TO}&amp;gt;`,
          to: [Bun.env.EMAIL_TO],
          subject: `New Message Received From ${name}`,
          react: EmailTemplate({ name, email, message }),
        });

        // error sending email, send back error status and message
        if (error) {
          return context.error((error as any).statusCode, error);
        }

        logger.info("New email received");
        return new Response(JSON.stringify({ message: "Success" }), {
          headers: {
            "content-type": "application/json",
          },
        });
      } catch (error) {
        // catch error and log it, respond to user
        if (error instanceof Error) {
          logger.error(error.message, error);
        }
        return context.error("Internal Server Error", error);
      }
    },
    {
      // body must be of three properties: name, email, and message
      // validation happens as a middleware
      body: t.Object({
        name: t.String({
          minLength: 3,
          error: "Name must be at least 3 characters",
        }),
        email: t.String({
          format: "email",
          error: "Invalid email format",
        }),
        message: t.String({
          minLength: 1,
          maxLength: 1024,
          error:
            "Message should be at least 10 characters and max of 1024 characters",
        }),
      }),
      // any errors that happens during validation will be logged
      error: ({ path, body, request: { method, headers }, error, code }) =&amp;gt; {
        const errorMessage = `method=${method} path=${path} error=${
          error.message
        } body=${JSON.stringify(body)} userAgent=${headers.get("user-agent")}`;
        logger.error(errorMessage);
        return error;
      },
    },
  )
  // if any errors happen on the server itself, log the errors
  .onError(({ path, request: { method, headers }, error }) =&amp;gt; {
    const errorMessage = `method=${method} path=${path} userAgent=${headers.get(
      "user-agent",
    )}`;
    logger.error(errorMessage, error);
    return error;
  })
  .listen(serverPort, () =&amp;gt; {
    logger.info(`Server starting on PORT: ${serverPort}`);
  });

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;p&gt;In a nutshell, we created a server that is listening on Port 3000.&lt;/p&gt;

&lt;p&gt;Whenever a POST request is received to &lt;code&gt;/send-email&lt;/code&gt;, our middleware will validate the incoming body.&lt;/p&gt;

&lt;p&gt;If any validation errors occur, we log it on our server and also return it as a response object.&lt;/p&gt;

&lt;p&gt;Now, if everything seems okay, we're going to attempt to send an email to Resend which in turn, will forward the email in our &lt;code&gt;to:&lt;/code&gt; field.&lt;/p&gt;

&lt;p&gt;Pretty simple right?&lt;/p&gt;

&lt;h2&gt;
  
  
  Front-end meets Back-end
&lt;/h2&gt;

&lt;p&gt;Back on our front end, we're going to make a fetch request to our server listening on Port 3000. My AstroJS server is running on PORT 4321 so make sure you distinguish the two.&lt;/p&gt;

&lt;p&gt;In the event listener on form submission, we'll update our code to use &lt;code&gt;fetch()&lt;/code&gt; as a POST request to our email server.&lt;/p&gt;

&lt;p&gt;Please not the comments in the following code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;script&amp;gt;
  const contactForm =
    contactsContainer.querySelector&amp;lt;HTMLFormElement&amp;gt;(".contact-form");

  if (contactForm) {
    contactForm.addEventListener("submit", async (e) =&amp;gt; {
      e.preventDefault();

      const formEl = e.target as HTMLFormElement;
      const formData = new FormData(formEl);

      const requestBody = {
        name: formData.get("name"),
        email: formData.get("email"),
        message: formData.get("message"),
      };

      try {
        const { message } = await fetch(
          import.meta.env.PROD
            ? "https://api.mycustomdomain.com/send-email" // host a production server with your custom domain
            : "http://localhost:3000/send-email",
          {
            method: "POST",
            body: JSON.stringify(requestBody),
            headers: {
              "Content-Type": "application/json",
            },
          },
        ).then((res) =&amp;gt; res.json());

        // store input elements in a variable for later use
        const nameInput =
          formEl.querySelector&amp;lt;HTMLInputElement&amp;gt;('input[name="name"]');
        const emailInput = formEl.querySelector&amp;lt;HTMLInputElement&amp;gt;(
          'input[name="email"]',
        );
        const messageInput = formEl.querySelector&amp;lt;HTMLTextAreaElement&amp;gt;(
          'textarea[name="message"]',
        );

        // remove all error classes from the inputs
        nameInput?.classList?.remove("ring-red-500");
        emailInput?.classList?.remove("ring-red-500");
        messageInput?.classList?.remove("ring-red-500");

        // store adjacent siblings to see if they are error divs
        const errorName = nameInput?.nextElementSibling as HTMLDivElement;
        const errorEmail = emailInput?.nextElementSibling as HTMLDivElement;
        const messageError = messageInput?.nextElementSibling as HTMLDivElement;

        // each if statement checks the input adjacent sibling and removes them from the DOM
        // if they have the `data-error="true"` attribute
        if (errorName?.dataset?.["error"]) {
          errorName.remove();
        }
        if (errorEmail?.dataset?.["error"]) {
          errorEmail.remove();
        }
        if (messageError?.dataset?.["error"]) {
          messageError.remove();
        }

        // create the error div with the data-error="true" attribute
        function createErrorMessage(message: string) {
          const p = document.createElement("p");
          p.className = "text-sm text-red-400";
          p.dataset["error"] = "true";
          p.textContent = message;
          return p;
        }

        // insert the error divs after to the input element
        function insertErrorMessage(el: HTMLElement) {
          const error = el?.nextSibling as HTMLDivElement;
          if (!error?.dataset?.["error"]) {
            const div = createErrorMessage(message);
            el?.insertAdjacentElement("afterend", div);
          }
        }

        // create a toaster message upon success
        async function createSuccessToaster() {
          const div = document.createElement("div");
          const p = document.createElement("p");
          div.className =
            "fixed bottom-8 right-8 p-4 bg-slate-200 dark:bg-slate-700 shadoow-lg rounded-md opacity-0 transition-opacity duration-300 border-t-4 border-emerald-400 border-solid";
          p.className = "text-xl text-slate-800 dark:text-slate-100";
          p.textContent = "Your message has been sent!";
          div.appendChild(p);
          document.body.appendChild(div);

          // using sleep to animate the toaster
          await sleep(100);
          div.classList.remove("opacity-0");
          await sleep(3000);
          div.classList.add("opacity-0");
          await sleep(200);
          div.remove();
        }

        // message could be success or errors
        switch (message) {
          case "Success": {
            // reset the form on success and show a toaster
            formEl.reset();
            createSuccessToaster();
            break;
          }
          case "Name must be at least 3 characters": {
            if (nameInput) {
              insertErrorMessage(nameInput);
            }
            break;
          }
          case "Invalid email format": {
            if (emailInput) {
              insertErrorMessage(emailInput);
            }
            break;
          }
          case "Message should be at least 10 characters and max of 1024 characters": {
            if (messageInput) {
              insertErrorMessage(messageInput);
            }
            break;
          }
          default: {
            // if message is received, log it out in the dev console
            if (import.meta.env.DEV) {
              console.log("Unknown message: ", message);
            }
          }
        }
      } catch (err) {
        // if anything else happens during sending, log it out in the dev console
        if (import.meta.env.DEV) {
          console.log(err);
        }
      }
    });
  }
&amp;lt;/script&amp;gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alright, a lot going on here. Since I'm not using a front-end framework, I'm using vanilla JavaScript to manually manage the form state and show UI elements based on the server response.&lt;/p&gt;

&lt;p&gt;And now if you try to submit a form, you should see it hitting your server and show up in the server logs.&lt;/p&gt;

&lt;p&gt;Try submitting different values and see what kind of messages are generated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploying your Email Server
&lt;/h2&gt;

&lt;p&gt;Now it's time to deploy the email server.&lt;/p&gt;

&lt;p&gt;First, you're going to need a server. You can purchase a Virtual Private Server (VPS) or go through a managed service like Digital Ocean Droplets or Heroku.&lt;/p&gt;

&lt;p&gt;With the former services, it should be fairly easy since you'll connect your GitHub account, point the server to your GitHub repository, and let it build/run for you. There are plenty of tutorials on this topic which I will not cover here.&lt;/p&gt;

&lt;p&gt;I have a Hetzner VPS with Coolify installed so I'll be using this configuration. If you're interested in this setup, please check out two of my other blog posts where I show you how to &lt;a href="https://dev.to/posts/self-hosting-your-website-with-coolify-v4-a-step-by-step-guide"&gt;self-host with Coolify&lt;/a&gt; and &lt;a href="https://dev.toposts/adding-github-pull-request-preview-deployments-with-coolify"&gt;add Github preview deployments to Coolify&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In the Coolify admin page, create a New Project as usual.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--alaAky0a--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/coolify-new-project.CkcO8M0F.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--alaAky0a--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/coolify-new-project.CkcO8M0F.png" alt="Creating a new project in Coolify" width="700" height="426"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then select the "Production" environment on the following page.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adding a Resource
&lt;/h3&gt;

&lt;p&gt;This next step will depend if you have Coolify integrated with GitHub or not. With a GitHub integration, Coolify can automatically deploy a new build whenever you make changes to your main branch.&lt;/p&gt;

&lt;p&gt;If you don't have this configured yet, don't fret. You can just copy the GitHub URL to the email server repository but you have to manually redeploy anytime you make a change to see an effect.&lt;/p&gt;

&lt;p&gt;Go ahead and add a new Resource and you will see this next page.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--P66V1CsQ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/coolify-new-resource.DATstmFB.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--P66V1CsQ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/coolify-new-resource.DATstmFB.png" alt="Creating a new resource in Coolify" width="800" height="281"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;From there keep selecting the server you want to host it from, and then the destination.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuring the app settings
&lt;/h3&gt;

&lt;p&gt;When you get to the Configuration page, you would want to choose the &lt;code&gt;Nixpacks&lt;/code&gt; build pack. It will automatically detect your stack and deploy a container for you.&lt;/p&gt;

&lt;p&gt;Before we deploy, we want to add environment variables.&lt;/p&gt;

&lt;p&gt;Navigate to the environment variables page and enter the environment variables we used in the &lt;code&gt;.env&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;There should be 4 variables to create:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;SERVER_PORT&lt;/code&gt;: the PORT you want to use&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;RESEND_API_KEY&lt;/code&gt;: the key from Resend&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;EMAIL_TO&lt;/code&gt;: the email address where you will receive the messages&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ALLOWED_ORIGIN&lt;/code&gt;: the domain that is only allowed to make requests&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you're running a server different from the front-end server, you'll need to use the &lt;code&gt;ALLOWED_ORIGIN&lt;/code&gt; and enter your front-end domain value. This will help with CORS and allow your server to accept requests from your custom domain.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--oFY1YxVW--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/coolify-environment-variables.BBrnxQEq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--oFY1YxVW--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/coolify-environment-variables.BBrnxQEq.png" alt="Coolify environment variables page" width="800" height="319"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then back on the main configuration page, we want to add a Start command to run after our build finishes.&lt;/p&gt;

&lt;p&gt;Add &lt;code&gt;NODE_ENV=production bun run start&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--1nz_CTkA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/coolify-start-command.BhkT2QK2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--1nz_CTkA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/coolify-start-command.BhkT2QK2.png" alt="Coolify start command input" width="549" height="135"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then in the Domain field, add a custom domain name. I have Coolify set up to use subdomains so here I'm using &lt;code&gt;https://test-email.billyle.dev&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So my front-end application will now make POST requests to &lt;code&gt;https://test-email.billyle.dev/send-email&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--AU1vZ452--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/coolify-custom-subdomain.CIeBIIz1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--AU1vZ452--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/coolify-custom-subdomain.CIeBIIz1.png" alt="Coolify custom subdomain" width="412" height="96"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click save and deploy your application.&lt;/p&gt;

&lt;p&gt;If all is good, you should see a green status dot saying "Healthy".&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the integration
&lt;/h2&gt;

&lt;p&gt;Update your front-end code where you're making the POST request.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const { message } = await fetch(
  import.meta.env.PROD
    ? "https://test-email.billyle.dev/send-email" // change this part here
    : "http://localhost:3000/send-email",
  {
    method: "POST",
    body: JSON.stringify(requestBody),
    headers: {
      "Content-Type": "application/json",
    },
  },
).then((res) =&amp;gt; res.json());

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From the Contact Form, enter all the details and click Submit.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--mGnMH5re--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/contact-form-test.ClST0q34.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--mGnMH5re--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/contact-form-test.ClST0q34.png" alt="contact form filled to test integration" width="577" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If it's successfully submitted, hopefully, you have a way of displaying to your users that it was successful. Here is my toast message that shows that.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s---aRTVsXO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/toaster-message.C5o7MYt3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s---aRTVsXO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/toaster-message.C5o7MYt3.png" alt="Success Toast message" width="536" height="261"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;From your Resend dashboard, you can all the emails that flowed through by checking the "Emails" page.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--YkaxhTJX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/resend-emails.fo8l2wOK.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--YkaxhTJX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/resend-emails.fo8l2wOK.png" alt="Resend Emails page" width="800" height="148"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I can see the "Jane Doe" email I sent earlier which is a good thing.&lt;/p&gt;

&lt;p&gt;And then in your inbox, you should see your test email as below.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--lZoSRG5I--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/email-inbox.BJLWB0hQ.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--lZoSRG5I--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/email-inbox.BJLWB0hQ.png" alt="Email inbox showing test message" width="800" height="114"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Congratulations! It's all working! Now you can deploy your front-end and do the same testing to see if it works in production.&lt;/p&gt;

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

&lt;p&gt;That was quite a long post but I hope it gave you some ideas on how you too could implement an email server using Resend and deploying it somewhere live.&lt;/p&gt;

&lt;p&gt;The Resend documentation is well-written as they have nearly every language support. You can easily copy and paste the code and test it from there.&lt;/p&gt;

&lt;p&gt;If you need help with any of this setup, please use my &lt;a href="https://dev.to/#contact"&gt;Contact Form&lt;/a&gt;!&lt;/p&gt;

&lt;p&gt;There are some things I didn't include like fighting spam using honeypot techniques but that's above my current knowledge.&lt;/p&gt;

&lt;p&gt;Well, until then, thank you for reading, and have a good one.&lt;/p&gt;

</description>
      <category>blogging</category>
      <category>astro</category>
      <category>bunjs</category>
      <category>node</category>
    </item>
    <item>
      <title>Highlight Table of Content Items Using Intersection Observer</title>
      <dc:creator>Billy Le</dc:creator>
      <pubDate>Fri, 05 Apr 2024 00:00:00 +0000</pubDate>
      <link>https://dev.to/billyle/highlight-table-of-content-items-using-intersection-observer-33h7</link>
      <guid>https://dev.to/billyle/highlight-table-of-content-items-using-intersection-observer-33h7</guid>
      <description>&lt;p&gt;Giving your readers a way to navigate through the Table of Contents (ToC) is a nice feature but I was &lt;em&gt;still&lt;/em&gt; missing a &lt;strong&gt;critical&lt;/strong&gt; detail that would make the experience much more pleasant.&lt;/p&gt;

&lt;p&gt;The missing feature was a way to highlight or give some sort of visual indicator of which part of the ToC the reader was viewing.&lt;/p&gt;

&lt;p&gt;I've adapted the &lt;a href="https://rezahedi.dev/blog/create-table-of-contents-in-astro-and-sectionize-the-markdown-content#separating-markdown-content-into-sections"&gt;work of Reza Zahedi&lt;/a&gt; as I did before in my other blog post so all credit goes to him.&lt;/p&gt;

&lt;p&gt;You'll more or less likely find the same information in his blog and this post and I'm creating this post as an entry for my own record.&lt;/p&gt;

&lt;h2&gt;
  
  
  A bit about AstroJS remark support
&lt;/h2&gt;

&lt;p&gt;AstroJS ships with &lt;a href="https://github.com/remarkjs/remark"&gt;remark&lt;/a&gt;, a markdown processor with many community-built plugins.&lt;/p&gt;

&lt;p&gt;You can add things like linters, MDX support, or compile your markdown to PDFs. &lt;a href="https://github.com/remarkjs/remark/blob/main/doc/plugins.md#list-of-plugins"&gt;The list goes on and on&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I am going to use the &lt;a href="https://github.com/jake-low/remark-sectionize"&gt;remark-sectionize plugin&lt;/a&gt;. This plugin will parse through my markdowns and for every article heading greater than &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt;, it will wrap a surrounding &lt;code&gt;&amp;lt;section&amp;gt;&lt;/code&gt; element.&lt;/p&gt;

&lt;p&gt;To see what I mean, here is an example:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before&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;&amp;lt;article&amp;gt;
  &amp;lt;h1&amp;gt;My Article Title&amp;lt;/h1&amp;gt;
  &amp;lt;h2 id="heading-1"&amp;gt;This is the first heading&amp;lt;/h2&amp;gt;
  &amp;lt;p&amp;gt;This paragraph is about whatever heading-1 is about.&amp;lt;/p&amp;gt;
  &amp;lt;h2 id="heading-2"&amp;gt;This is the second heading&amp;lt;/h2&amp;gt;
  &amp;lt;p&amp;gt;This paragraph is about whatever heading-2 is about.&amp;lt;/p&amp;gt;
&amp;lt;/article&amp;gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;After&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;&amp;lt;article&amp;gt;
  &amp;lt;h1&amp;gt;My Article Title&amp;lt;/h1&amp;gt;
  &amp;lt;section&amp;gt;
    &amp;lt;h2 id="heading-1"&amp;gt;This is the first heading&amp;lt;/h2&amp;gt;
    &amp;lt;p&amp;gt;This paragraph is about whatever heading-1 is about.&amp;lt;/p&amp;gt;
  &amp;lt;/section&amp;gt;
    &amp;lt;h2 id="heading-2"&amp;gt;This is the second heading&amp;lt;/h2&amp;gt;
    &amp;lt;p&amp;gt;This paragraph is about whatever heading-2 is about.&amp;lt;/p&amp;gt;
  &amp;lt;/section&amp;gt;
&amp;lt;/article&amp;gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It seems simple enough, right?&lt;/p&gt;

&lt;h2&gt;
  
  
  Highlight Table of Contents
&lt;/h2&gt;

&lt;p&gt;For now, I simply want to change the text color in the ToC whenever a reader is viewing that section.&lt;/p&gt;

&lt;p&gt;To do so, I have to use the Intersection Observer API which will allow me to manipulate the DOM elements as they enter or leave the viewport.&lt;/p&gt;

&lt;p&gt;Here are the steps I'll need to complete it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Give my ToC a class name, &lt;code&gt;.toc-links&lt;/code&gt; for selecting the DOM element.&lt;/li&gt;
&lt;li&gt;Select all &lt;code&gt;&amp;lt;section&amp;gt;&lt;/code&gt; elements within the &lt;code&gt;&amp;lt;article&amp;gt;&lt;/code&gt; tag.&lt;/li&gt;
&lt;li&gt;Create an Intersection Observer and write a callback function to process the event and data.&lt;/li&gt;
&lt;li&gt;Inside the callback, find the heading element of that section, map it to the ToC, and toggle on/off the class &lt;code&gt;active&lt;/code&gt; as they enter or leave.&lt;/li&gt;
&lt;li&gt;Loop over the &lt;code&gt;&amp;lt;section&amp;gt;&lt;/code&gt; tags from Step 1 and use the Intersection Observer we created to observe each section.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The Intersection Observer API
&lt;/h3&gt;

&lt;p&gt;If you have never used or heard of &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API"&gt;Intersection Observer&lt;/a&gt;, it's a Web API that allows us to listen to events and trigger functions when an element is entering or leaving the viewport.&lt;/p&gt;

&lt;p&gt;This is the perfect use case for using the Intersection Observer API because we want to manipulate DOM elements whenever the events fire.&lt;/p&gt;

&lt;p&gt;We will create an Intersection Observer that will change the class name of the &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt; tags inside our Table of Contents whenever we are viewing the corresponding section.&lt;/p&gt;

&lt;h3&gt;
  
  
  The implementation code
&lt;/h3&gt;

&lt;p&gt;In my &lt;code&gt;BlogLayout.astro&lt;/code&gt; file that is responsible for rendering the very HTML page you're reading, I'm going to write a &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag.&lt;/p&gt;

&lt;p&gt;Here is the code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;script&amp;gt;
  const articleSections = document.querySelectorAll&amp;lt;HTMLDivElement&amp;gt;("article section");

  const observer = new IntersectionObserver((entries) =&amp;gt; {
    entries.map((entry) =&amp;gt; {
      const heading =
        entry.target.querySelector&amp;lt;HTMLHeadingElement&amp;gt;("h2,h3,h4,h5");
      if (!heading) return;
      const id = heading.getAttribute("id");
      if (!id) return;
      const link = document.querySelector&amp;lt;HTMLAnchorElement&amp;gt;(
        `.toc-links a[href="#${id}"]`,
      );
      if (!link) return;

      const addRemove = entry.intersectionRatio &amp;gt; 0 ? "add" : "remove";
      link.classList[addRemove]("text-blue-500", "dark:text-blue-400");
    });
  });

  for (const section of articleSections) {
    observer.observe(section);
  }

  window.document.addEventListener("beforeunload", () =&amp;gt; {
    observer.disconnect();
  });
&amp;lt;/script&amp;gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Breaking the code down
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const articleSections =
  document.querySelectorAll&amp;lt;HTMLDivElement&amp;gt;("article section");

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'm collecting all the article sections using the &lt;code&gt;document.querySelectAll()&lt;/code&gt; function.&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const observer = new IntersectionObserver((entries) =&amp;gt; {});

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'm creating a &lt;code&gt;new IntersectionObserver()&lt;/code&gt; that takes a callback function whenever it is fired.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;entries&lt;/code&gt; parameter is coming from the intersection event triggered and is an array of &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry"&gt;IntersectionObserverEntry&lt;/a&gt;.&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;entries.forEach((entry) =&amp;gt; {});

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using the &lt;code&gt;entries&lt;/code&gt; parameter from the callback, we loop over it in a &lt;code&gt;.forEach()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;From each entry, there is a target. That target is the HTML element that fired the event. In my case, it will be a &lt;code&gt;&amp;lt;section&amp;gt;&lt;/code&gt; tag.&lt;/p&gt;

&lt;p&gt;How does it know that it's a section element? Well, I'll explain later in the code.&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const heading = entry.target.querySelector&amp;lt;HTMLHeadingElement&amp;gt;("h2,h3");
if (!heading) return;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To get the heading of the section, we use &lt;code&gt;entry.target.querySelector&amp;lt;HTMLHeadingElement&amp;gt;("h2,h3")&lt;/code&gt; and store it in a variable called &lt;code&gt;heading&lt;/code&gt;. There is a guard clause to return if nothing is found.&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const id = heading.getAttribute("id");
if (!id) return;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, I find the id attribute by calling &lt;code&gt;heading.getAttribute("id")&lt;/code&gt; and store that in another variable called &lt;code&gt;id&lt;/code&gt;.&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const link = document.querySelector&amp;lt;HTMLAnchorElement&amp;gt;(
  `.toc-links a[href="#${id}"]`,
);
if (!link) return;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next up, find the associated &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt; tag by using string interpolation and storing that into a &lt;code&gt;link&lt;/code&gt; variable.&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const addRemove = entry.intersectionRatio &amp;gt; 0 ? "add" : "remove";
link.classList[addRemove]("text-blue-500", "dark:text-blue-400");

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using the &lt;code&gt;entry&lt;/code&gt; variable from before, we can detect when a section is entering or leaving by using the &lt;code&gt;intersectionRatio&lt;/code&gt;. If the &lt;code&gt;intersectionRatio&lt;/code&gt; is greater than 0, the element is entering, and when it's below 0, it is leaving.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;addRemove&lt;/code&gt; variable stores the &lt;code&gt;key&lt;/code&gt; of the &lt;code&gt;classList&lt;/code&gt; API so we can easily toggle on and off the class names.&lt;/p&gt;

&lt;p&gt;If the section is being viewed, I changed the ToC item to a blue text and off when the section is no longer in view.&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;for (const section of articleSections) {
  observer.observe(section);
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now that the Intersection Observer is created with a callback function, we can observe elements in our DOM to invoke the callback as they enter or leave.&lt;/p&gt;

&lt;p&gt;In this case, I loop over all the sections from my &lt;code&gt;articleSections&lt;/code&gt; variable and observe them.&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;window.document.addEventListener("beforeunload", () =&amp;gt; {
  observer.disconnect();
});

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You may not need this but I added this part anyway. Before a user navigates away from the page, I want to disconnect the observer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;As you can probably see, the text turns blue when you go from one section to the other.&lt;/p&gt;

&lt;p&gt;If there is a child section within a parent section, such as an h3 within an h2 section, it still keeps the parent heading highlighted.&lt;/p&gt;

&lt;p&gt;This is great since most articles have a hierarchy.&lt;/p&gt;

&lt;p&gt;Congratulations! You learned to add this simple feature in your blog or anywhere you need to highlight different parts of your site by using the Intersection Observer API.&lt;/p&gt;

&lt;p&gt;You can do some more fancy stuff with the Intersection Observer like this &lt;a href="https://lab.hakim.se/progress-nav/"&gt;Progress Navigation by Hakim El Hattab&lt;/a&gt; that I found via &lt;a href="https://kld.dev/toc-animation/#lets-start-with-the-markup"&gt;Kevin Drum&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you end up implementing this, let me know! I would love to see your work.&lt;/p&gt;

&lt;p&gt;Until next time, have a good one, and happy coding!&lt;/p&gt;

</description>
      <category>blogging</category>
      <category>contentcreation</category>
      <category>astro</category>
    </item>
    <item>
      <title>Use Husky and Node to Unstage Draft Blog Posts From Git</title>
      <dc:creator>Billy Le</dc:creator>
      <pubDate>Thu, 21 Mar 2024 00:00:00 +0000</pubDate>
      <link>https://dev.to/billyle/use-husky-and-node-to-unstage-draft-blog-posts-from-git-ao7</link>
      <guid>https://dev.to/billyle/use-husky-and-node-to-unstage-draft-blog-posts-from-git-ao7</guid>
      <description>&lt;p&gt;Astro allows us to only create static files during the build step for published work by filtering through the collection and filtering out draft posts. But it doesn't prevent Git from knowing the context of your collection.&lt;/p&gt;

&lt;p&gt;So what ends up happening is you might check in your drafts in your Git history. For me, I didn't want my draft blog posts to be viewable until they were ready to be published.&lt;/p&gt;

&lt;p&gt;I thought about adding my draft files into another folder and using gitignore, but it wasn't an elegant solution since I have to manually move files from the drafts folder and into the proper folder each time I want to publish a blog.&lt;/p&gt;

&lt;p&gt;A solution I came up with was to use Husky to trigger pre-commit scripts and unstaged all my markdown drafts. I'll show you exactly how I did that below.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Husky?
&lt;/h2&gt;

&lt;p&gt;If you don't know what Husky is, it's a program that will run during different life cycles of your Git workflow. This is particularly useful for doing a bunch of things before and after committing files in your Git history.&lt;/p&gt;

&lt;p&gt;Usually, you will see the &lt;code&gt;pre-commit&lt;/code&gt; hook often used for performing linting, prettifying, or running tests on your project.&lt;/p&gt;

&lt;h3&gt;
  
  
  List of Git Hooks Husky supports
&lt;/h3&gt;

&lt;p&gt;Husky supports all client-side Git hooks. There are 13 of them in total.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;applypatch-msg&lt;/li&gt;
&lt;li&gt;commit-msg&lt;/li&gt;
&lt;li&gt;post-applypatch&lt;/li&gt;
&lt;li&gt;post-checkout&lt;/li&gt;
&lt;li&gt;post-commit&lt;/li&gt;
&lt;li&gt;post-merge&lt;/li&gt;
&lt;li&gt;post-rewrite&lt;/li&gt;
&lt;li&gt;pre-applypatch&lt;/li&gt;
&lt;li&gt;pre-auto-gc&lt;/li&gt;
&lt;li&gt;pre-commit&lt;/li&gt;
&lt;li&gt;pre-push&lt;/li&gt;
&lt;li&gt;pre-rebase&lt;/li&gt;
&lt;li&gt;prepare-commit-msg&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're interested in what Git hooks are, here is a list of all the different &lt;a href="https://git-scm.com/docs/githooks"&gt;Git hooks&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install Husky
&lt;/h2&gt;

&lt;p&gt;To &lt;a href="https://typicode.github.io/husky/"&gt;install Husky&lt;/a&gt;, you need to have &lt;code&gt;Node&lt;/code&gt; installed since you will use &lt;code&gt;npm&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I use &lt;code&gt;pnpm&lt;/code&gt;, but you can use any package manager supported in the link above.&lt;/p&gt;

&lt;p&gt;Run the command &lt;code&gt;pnpm add husky -D&lt;/code&gt;. This will install Husky as a devDependency.&lt;/p&gt;

&lt;p&gt;Then run &lt;code&gt;pnpm exec husky init&lt;/code&gt;, so Husky can take care of the setup for you.&lt;/p&gt;

&lt;p&gt;If you look in your project now, you should see a &lt;code&gt;.husky&lt;/code&gt; folder. If you look inside, you will see a &lt;code&gt;pre-commit&lt;/code&gt; file with the command &lt;code&gt;pnpm test&lt;/code&gt; inside.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--tmFSLfvN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/husky-post-setup.CTHY09dc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--tmFSLfvN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/husky-post-setup.CTHY09dc.png" alt="husky post setup files" width="800" height="207"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As a concept, if you tried to run &lt;code&gt;git add .&lt;/code&gt; and &lt;code&gt;git commit -m "my message"&lt;/code&gt; the pre-commit hook will trigger and run &lt;code&gt;pnpm test&lt;/code&gt;. If your test happens to fail, then it won't commit anything and your Git history remains untouched otherwise you're golden.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retrieving a list of your staged files
&lt;/h2&gt;

&lt;p&gt;Now we need a way to list out all the files that are staged in Git. Luckily for us, we can do that with this command, &lt;code&gt;git diff --name-status --staged&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This command grabs all staged files that were changed and returns the status mode and file names. This is how it would look in your terminal:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--rldUqS1G--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/list-staged-files.D9k0RTMb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--rldUqS1G--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/list-staged-files.D9k0RTMb.png" alt="list of files staged" width="800" height="379"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Press q to kill the process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preparing the Node script
&lt;/h2&gt;

&lt;p&gt;Our next step requires us to write a Node script. Since my project is written in Typescript and I want to take advantage of the type system, so I am using &lt;code&gt;ts-node&lt;/code&gt; to run the script.&lt;/p&gt;

&lt;p&gt;However, if you're just using Node, then you can change the file extension to &lt;code&gt;.js&lt;/code&gt;, and remove the types, and it should work the same.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Install &lt;code&gt;front-matter&lt;/code&gt; and &lt;code&gt;ts-node&lt;/code&gt; packages&lt;/li&gt;
&lt;li&gt;Ensure you have a Post schema type&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We need a way to read from our front matter in our &lt;code&gt;.md&lt;/code&gt; files. I found this package, &lt;a href="https://www.npmjs.com/package/front-matter"&gt;front-matter&lt;/a&gt;, that easily allows us to get key-value pairs of our markdown.&lt;/p&gt;

&lt;h3&gt;
  
  
  The meat of the script
&lt;/h3&gt;

&lt;p&gt;I created a file in my root directory called &lt;code&gt;unstage-drafts.ts&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We'll need to change who can access the file with &lt;code&gt;chmod&lt;/code&gt;, so in your terminal run the command &lt;code&gt;chmod 777 unstage-drafts.ts&lt;/code&gt; to change the permissions.&lt;/p&gt;

&lt;p&gt;Then inside the file, I wrote this script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import fs from "node:fs/promises";
import childProcess from "node:child_process";
import util from "node:util";
import fm from "front-matter";
import type { Post } from "src/content/config.ts";

const execPromise = util.promisify(childProcess.exec);

let data = "";

process.stdin.on("readable", () =&amp;gt; {
  let chunk;

  while (null !== (chunk = process.stdin.read())) {
    data += chunk;
  }
});

process.stdin.on("end", async () =&amp;gt; {
  // process all markdown files and unstage any draft posts
  const stagedFiles: string[] = [];
  const markdownFiles: string[] = [];

  data
    .split("\n")
    .filter((x) =&amp;gt; x)
    .forEach((line) =&amp;gt; {
      if (line.endsWith(".md")) {
        if (!line.startsWith("D")) {
          const markdownFile = line.split("\t")[1] as string;
          markdownFiles.push(markdownFile);
        }
      } else {
        stagedFiles.push(line);
      }
    });

  let draftCount = 0;

  for (const file of markdownFiles) {
    const content = await fs
      .readFile(file, { encoding: "utf-8" })
      .then((f) =&amp;gt; fm&amp;lt;Post&amp;gt;(f));

    if (content.attributes.draft) {
      draftCount++;
      await execPromise(`git reset ${file}`);
    }
  }

  if (draftCount === markdownFiles.length &amp;amp;&amp;amp; !stagedFiles.length) {
    throw Error("only draft posts were staged.");
  }
});

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Script breakdown
&lt;/h3&gt;

&lt;p&gt;Let's do a quick break of what's going on.&lt;/p&gt;

&lt;p&gt;The necessary imports of modules I'm using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import fs from "node:fs/promises";
import childProcess from "node:child_process";
import util from "node:util";
import fm from "front-matter";
import type { Post } from "src/content/config.ts";

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command &lt;code&gt;git diff --name-status --staged&lt;/code&gt; writes to out &lt;code&gt;stdout&lt;/code&gt;, so we can read from it in Node by using &lt;code&gt;process.stdin&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here we're listening to the event "readable" which is a stream of bytes. Then each chunk is appended in our &lt;code&gt;data&lt;/code&gt; variable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;let data = "";

process.stdin.on("readable", () =&amp;gt; {
  let chunk;

  while (null !== (chunk = process.stdin.read())) {
    data += chunk;
  }
});

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We're going to &lt;code&gt;promisify()&lt;/code&gt; our &lt;code&gt;childProcess.exec&lt;/code&gt;, so we can await it later.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const execPromise = util.promisify(childProcess.exec);

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When our readable stream ends, we will listen to the event &lt;code&gt;end&lt;/code&gt; and run an async callback function that uses the &lt;code&gt;data&lt;/code&gt; variable to process all the staged files.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;process.stdin.on("end", async () =&amp;gt; {});

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I have two variables - one that will keep track of non-&lt;code&gt;.md&lt;/code&gt; files and another to store markdown files, respectfully called &lt;code&gt;stagedFiles&lt;/code&gt; and &lt;code&gt;markdownFiles&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I split the data by new lines and then filtered valid entries. Then for each item in the array, I test some conditions to check whether the file is &lt;code&gt;.md&lt;/code&gt; or not.&lt;/p&gt;

&lt;p&gt;If it is a markdown file, we want to check if it's not a &lt;code&gt;D&lt;/code&gt; status. The "D" status here means that I've untracked the file that was previously tracked by Git and I don't want to unstage those changes.&lt;/p&gt;

&lt;p&gt;If it is not "D" then we push it to the &lt;code&gt;markdownFiles&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const stagedFiles: string[] = [];
const markdownFiles: string[] = [];

data
  .split("\n")
  .filter((x) =&amp;gt; x)
  .forEach((line) =&amp;gt; {
    if (line.endsWith(".md")) {
      if (!line.startsWith("D")) {
        const markdownFile = line.split("\t")[1] as string;
        markdownFiles.push(markdownFile);
      }
    } else {
      stagedFiles.push(line);
    }
  });

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After we have a list of our staged markdown files, we're going to process each file in a for loop and read from the front matter. If the post is a draft, we update a counter and call &lt;code&gt;await execPromise()&lt;/code&gt; to unstage it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;let draftCount = 0;

for (const file of markdownFiles) {
  const content = await fs
    .readFile(file, { encoding: "utf-8" })
    .then((f) =&amp;gt; fm&amp;lt;Post&amp;gt;(f));

  if (content.attributes.draft) {
    draftCount++;
    await execPromise(`git reset ${file}`);
  }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This next condition is a guard to check if there are staged files to commit while also checking if all staged markdown files were drafts. If this ends up being true, it must mean we probably did a &lt;code&gt;git commit&lt;/code&gt; on staged files that were only markdown drafts. I threw an error so that there isn't an empty commit history if this does happen.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if (draftCount === markdownFiles.length &amp;amp;&amp;amp; !stagedFiles.length) {
  throw Error("only draft posts were staged.");
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the entirety of the script. As you can see, you can do a lot more in this script if there are extra requirements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Piping it all together
&lt;/h2&gt;

&lt;p&gt;Back in our &lt;code&gt;.husky/pre-commit&lt;/code&gt; file where we had a single line of &lt;code&gt;pnpm test&lt;/code&gt;, we're going to replace that by combining both the &lt;code&gt;git diff&lt;/code&gt; and the Node script by using the piping method.&lt;/p&gt;

&lt;p&gt;Replace &lt;code&gt;pnpm test&lt;/code&gt; with &lt;code&gt;git diff --name-status --staged | node --import 'data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; register("ts-node/esm", pathToFileURL("./"));' unstage-drafts.ts&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--5piUD7x2--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/pre-commit-piping-script.D2phtuxd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--5piUD7x2--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/pre-commit-piping-script.D2phtuxd.png" alt="content of the pre-commit to use node script" width="800" height="323"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The verboseness of the command above is important to Typescript and Node.&lt;/p&gt;

&lt;p&gt;If you're not using Typescript, it will look a lot simpler:&lt;code&gt;git diff --name-status --staged | node unstage-drafts.ts&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Untracked files committed to history
&lt;/h2&gt;

&lt;p&gt;There's one other important thing that I want to do -- removing my existing drafts in my Git history, so they are no longer available in my public repo.&lt;/p&gt;

&lt;p&gt;To do that, run the command &lt;code&gt;git rm --cached &amp;lt;path/to/file&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I have one file, &lt;code&gt;learning-golang-for-javascript-developers.md&lt;/code&gt;, that is a draft and is already in my Git history. I want to remove it so I run &lt;code&gt;git rm --cached src/content/posts/learning-golang-for-javascript-developers.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Now the file is labeled as Untracked by Git.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Xhp0cGfU--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/untrack-files.DzTIIX-D.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Xhp0cGfU--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/untrack-files.DzTIIX-D.png" alt="showing files that were untracked" width="800" height="301"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Seeing the pre-commit hook in action
&lt;/h2&gt;

&lt;p&gt;Time to put it to the test and make sure our drafts are no longer being committed. Running the commands in order to make sure I have everything staged correctly.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;code&gt;git add .&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;git rm --cached src/content/posts/learning-golang-for-javascript-developers.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;then followed by &lt;code&gt;git status&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is the output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;➜ billyle.dev git:(ft/husky-precommit) ✗ git status
On branch ft/husky-precommit
Changes to be committed:
  (use "git restore --staged &amp;lt;file&amp;gt;..." to unstage)
        modified: .astro/types.d.ts
        new file: .husky/pre-commit
        modified: package.json
        modified: pnpm-lock.yaml
        new file: public/images/blog/husky-node-unstage/husky-post-setup.png
        new file: public/images/blog/husky-node-unstage/list-staged-files.png
        new file: public/images/blog/husky-node-unstage/pre-commit-piping-script.png
        new file: public/images/blog/husky-node-unstage/untrack-files.png
        modified: src/content/config.ts
        deleted: src/content/posts/learning-golang-for-javascript-developers.md
        new file: src/content/posts/use-husky-and-node-to-unstage-draft-posts-from-git.md
        modified: src/layouts/BlogLayout.astro
        new file: unstage-drafts.ts

Untracked files:
  (use "git add &amp;lt;file&amp;gt;..." to include in what will be committed)
        src/content/posts/learning-golang-for-javascript-developers.md

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now I will make a &lt;code&gt;git commit -m "remove existing drafts from git history and unstage drafts"&lt;/code&gt; and see that this blog post is removed from the staging area.&lt;/p&gt;

&lt;p&gt;Checking &lt;code&gt;git status&lt;/code&gt; shows me:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;➜ billyle.dev git:(ft/husky-precommit) ✗ git status
On branch ft/husky-precommit
Untracked files:
  (use "git add &amp;lt;file&amp;gt;..." to include in what will be committed)
        src/content/posts/learning-golang-for-javascript-developers.md
        src/content/posts/use-husky-and-node-to-unstage-draft-posts-from-git.md

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Awesome! It worked! I can freely work on all my draft blog posts without ever checking them into Git again.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;So if you're like me and want some bit of automation in your creative process, you can use this solution. I'm pretty sure there are better ones out there but if you like this approach, feel free to take it and use it as your own.&lt;/p&gt;

&lt;p&gt;In summary, we learned how to use Husky, piping the &lt;code&gt;git diff --name-status --staged&lt;/code&gt; command output into Node, and letting our script unstaged draft blog posts.&lt;/p&gt;

&lt;p&gt;I hope you learned something today and if not, that's alright! I'm glad you took the time to read it anyway.&lt;/p&gt;

&lt;p&gt;Well, until next time, happy coding!&lt;/p&gt;

</description>
      <category>blogging</category>
      <category>javascript</category>
      <category>node</category>
      <category>astro</category>
    </item>
    <item>
      <title>Creating Custom Table of Contents for Astro Content Collections</title>
      <dc:creator>Billy Le</dc:creator>
      <pubDate>Wed, 13 Mar 2024 00:00:00 +0000</pubDate>
      <link>https://dev.to/billyle/creating-custom-table-of-contents-for-astro-content-collections-4jme</link>
      <guid>https://dev.to/billyle/creating-custom-table-of-contents-for-astro-content-collections-4jme</guid>
      <description>&lt;p&gt;Having a Table of Contents (ToC) for your blog is nice because it allows users to see an overview of your content and provides a quick way to navigate between sections. To do this in Astro with the Content Collections API, there's a bit of legwork, but the results are satisfying. But first, let's talk about the issues I ran into.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;I tried to do this with the plugin, &lt;strong&gt;'remark-toc'&lt;/strong&gt; , which is mentioned in the Astro documentation. What I didn't like about it is that if I wanted to include a ToC, I would have to manually add it to the top of all my &lt;code&gt;.md&lt;/code&gt; files.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--njGaoOwy--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/remark-toc-static.DIfgRDwN.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--njGaoOwy--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/remark-toc-static.DIfgRDwN.png" alt="remark-toc display output on the blog post" width="800" height="628"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Another note is that wherever I included it in the markdown, the ToC sits statically and to style it, I would have to target the &lt;code&gt;id&lt;/code&gt; and fight my existing blog layout.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--vdynoxjr--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/remark-toc-md.CsRR9MxN.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--vdynoxjr--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/remark-toc-md.CsRR9MxN.png" alt="using remark-toc in a markdown file" width="800" height="491"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is not ideal for me. I had to figure out a better way to do this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retrieving all headers of a markdown
&lt;/h2&gt;

&lt;p&gt;In the official documentation, there are &lt;a href="https://docs.astro.build/en/guides/markdown-content/#exported-properties"&gt;two ways you can get all the headings&lt;/a&gt; of your blog posts. The two ways are when you're importing a &lt;code&gt;.md&lt;/code&gt; into a &lt;code&gt;.astro&lt;/code&gt; file or using the &lt;code&gt;Astro.glob()&lt;/code&gt; function. Neither of those was valid in my case because I'm using the Content Collections API.&lt;/p&gt;

&lt;p&gt;Buried in the documentation, I found that you can get the &lt;a href="https://docs.astro.build/en/reference/api-reference/#collection-entry-type"&gt;headings from a RenderedEntry&lt;/a&gt; if you're using the Content Collections API.&lt;/p&gt;

&lt;p&gt;Inside of my &lt;code&gt;/src/pages/posts/[...slug].astro&lt;/code&gt; where I am pre-rendering my blog posts using &lt;code&gt;getStaticPaths()&lt;/code&gt;, I have a utility function that pulls all my published blog posts into a collection. From there, I extract the headings using a &lt;code&gt;Promise.all()&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
import BlogLayout from "../../layouts/BlogLayout.astro";
import { allPosts } from "@utils/getCollection";
import type { GetStaticPaths } from "astro";

export const getStaticPaths = (async () =&amp;gt; {
  const headings = await Promise.all(
    allPosts.map((entry) =&amp;gt; entry.render().then((data) =&amp;gt; data.headings)),
  );

  const posts = allPosts.map((entry, index) =&amp;gt; {
    return {
      params: { slug: entry.slug },
      props: { entry, headings: headings[index] },
    };
  });

  return posts;
}) satisfies GetStaticPaths;

const { entry, headings } = Astro.props;
const { Content } = await entry.render();
---

&amp;lt;BlogLayout {...entry.data} {headings}&amp;gt;
  &amp;lt;Content /&amp;gt;
&amp;lt;/BlogLayout&amp;gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For reference, inside an Astro Collection, you have a list of Entries. These Entries have a &lt;code&gt;render()&lt;/code&gt; method that compiles the &lt;code&gt;.md&lt;/code&gt; file for rendering. It also returns a property called &lt;code&gt;headings&lt;/code&gt; which I used here to collect all the headings in a given markdown.&lt;/p&gt;

&lt;p&gt;Here is the shape of that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// headings shape
const headings: {
  depth: number;
  text: string;
  slug: string;
}[];

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With that, I returned it inside the props object which can be extracted from &lt;code&gt;Astro.props&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating the heading hierarchy
&lt;/h2&gt;

&lt;p&gt;I have all the headings passed down to my &lt;code&gt;BlogLayout&lt;/code&gt; component, and now I can use it. The first thing I need to do is make sure that there is a hierarchy of headings so that the ToC properly indents the headings.&lt;/p&gt;

&lt;p&gt;I tried doing this on my own with a recursive function but didn't have too much success. Luckily, I came across this &lt;a href="https://rezahedi.dev/blog/create-table-of-contents-in-astro-and-sectionize-the-markdown-content#retrieving-the-headings-prop-in-astro-layouts-or-components"&gt;blog by Reza Zahedi&lt;/a&gt; that showed me a good foundation to start with.&lt;/p&gt;

&lt;p&gt;With the &lt;del&gt;stolen&lt;/del&gt; copied code, I noticed that the nesting only allowed one list of subheadings. So if a heading has a depth of 2, and two headings succeeding that is of depth of 3 and 4 respectively, then it outputs something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const nestedHeadings = [
  {
    depth: 2,
    text: "My Heading",
    slug: "my-heading",
    subheadings: [
      {
        depth: 3,
        text: "My Subheading 1",
        slug: "my-subheading-1",
      },
      {
        depth: 4,
        text: "My Subheading 2",
        slug: "my-subheading-2",
      },
    ],
  },
];

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I was okay with this since I do not want the ToC to get carried away with indentations. I wanted to prevent from writing any headings greater than 3, so I added a guard to throw an error if I did include one by accident.&lt;/p&gt;

&lt;p&gt;Inside my &lt;code&gt;TOCHeading.astro&lt;/code&gt; component, I'm exporting an interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import type { MarkdownHeading } from "astro";
export interface HeadingHierarchy extends MarkdownHeading {
  subheadings: HeadingHierarchy[];
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside my &lt;code&gt;BlogLayout.astro&lt;/code&gt; component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import type { HeadingHierarchy } from "@ui/components/TOCHeading.astro";
import type { MarkdownHeading } from "astro";

const { headings } = Astro.props;

function createHeadingHierarchy(headings: MarkdownHeading[]) {
  const topLevelHeadings: HeadingHierarchy[] = [];

  headings.forEach((heading) =&amp;gt; {
    if (heading.depth &amp;gt; 3) {
      throw Error(
        `Depths greater than 3 not allowed:\n${JSON.stringify(heading, null, 2)}`,
      );
    }
    const h = {
      ...heading,
      subheadings: [],
    };

    if (h.depth === 2) {
      topLevelHeadings.push(h);
    } else {
      let parent = topLevelHeadings[topLevelHeadings.length - 1];
      if (parent) {
        parent.subheadings.push(h);
      }
    }
  });

  return topLevelHeadings;
}

const toc: HeadingHierarchy[] = createHeadingHierarchy(headings ?? []);
const hasToC = toc.length &amp;gt; 1;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'm using a variable called &lt;code&gt;hasToC&lt;/code&gt; since in some cases I have a short blog post with only one heading, and it doesn't make sense to show the ToC. I use this variable to conditionally render the ToC and the appropriate layout.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rendering the ToC
&lt;/h2&gt;

&lt;p&gt;Rendering is fairly straightforward in Astro. I have a &lt;code&gt;TOCHeading.astro&lt;/code&gt; component that I found in the other blog post and made minor adjustments like giving it types and such.&lt;/p&gt;

&lt;p&gt;If you're going to use a sticky ToC, be sure that the parent component has a &lt;code&gt;position: relative&lt;/code&gt; and that there is no &lt;code&gt;overflow&lt;/code&gt; property on it. If your parent is a &lt;code&gt;flex&lt;/code&gt; or &lt;code&gt;grid&lt;/code&gt; parent, you need to wrap your &lt;code&gt;position: sticky&lt;/code&gt; ToC with a container so that it will properly work.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;section class={`${hasToC ? "max-w-7xl mx-auto lg:grid lg:grid-cols-4" : ""}`}&amp;gt;
  {hasToC &amp;amp;&amp;amp; (
    &amp;lt;div class="relative mx-auto px-4 prose dark:prose-invert xl:pt-10 2xl:px-0"&amp;gt;
      &amp;lt;nav class="xl:sticky xl:top-20"&amp;gt;
        &amp;lt;h2 class="text-emerald-400"&amp;gt;Table of Contents&amp;lt;/h2&amp;gt;
        &amp;lt;ul&amp;gt;
          {toc.map((heading) =&amp;gt; (
            &amp;lt;TOCHeading heading={heading} /&amp;gt;
          ))}
        &amp;lt;/ul&amp;gt;
      &amp;lt;/nav&amp;gt;
    &amp;lt;/div&amp;gt;
  )}

  &amp;lt;article
    class={`py-10 sm:py-20 px-4 mx-auto prose prose-h1:font-vidaloka dark:prose-invert
            prose-code:before:hidden prose-code:after:hidden
            sm:prose-lg lg:prose-xl
            ${hasToC ? "lg:col-span-3" : ""}
        `}
  &amp;gt;
    &amp;lt;h1&amp;gt;{title}&amp;lt;/h1&amp;gt;
    &amp;lt;slot /&amp;gt;
  &amp;lt;/article&amp;gt;
&amp;lt;/section&amp;gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The results
&lt;/h2&gt;

&lt;p&gt;As you can see, my Table of Contents appears on the left-hand side. Now you can easily move between sections as you read!&lt;/p&gt;

&lt;p&gt;For now, I'm only supporting the sticky ToC for desktops as I haven't found a good UI for tablets and mobile devices yet.&lt;/p&gt;

&lt;p&gt;I guess all that's left to do is highlight the ToC heading that is currently being viewed, but I'll do that some other time.&lt;/p&gt;

&lt;p&gt;I hope that was a bit helpful if you're trying to add a ToC for your Astro website if you're using the Content Collections API.&lt;/p&gt;

&lt;p&gt;Well, thanks for reading and I hope you have a good one.&lt;/p&gt;

</description>
      <category>blogging</category>
      <category>contentcreation</category>
      <category>astro</category>
    </item>
    <item>
      <title>Enabling Developer Mode on iOS 17.3.1</title>
      <dc:creator>Billy Le</dc:creator>
      <pubDate>Thu, 07 Mar 2024 00:00:00 +0000</pubDate>
      <link>https://dev.to/billyle/enabling-developer-mode-on-ios-1731-56d4</link>
      <guid>https://dev.to/billyle/enabling-developer-mode-on-ios-1731-56d4</guid>
      <description>&lt;p&gt;I had trouble recently trying to find the "Developer Mode" settings on my iPhone. I tried viewing the "Privacy &amp;amp; Security" settings and scrolling down to the bottom only to find that it wasn't there.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--GFdIT3yb--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/developer-mode-invisible.IIDmyaWJ.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--GFdIT3yb--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/developer-mode-invisible.IIDmyaWJ.jpg" alt="screen of iphone where developer mode is not showing" width="295" height="640"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Even the official documentation was not helpful and users around the internet repeated the same instructions.&lt;/p&gt;

&lt;p&gt;It wasn't until I found an obscure comment that helped me unlock "Developer Mode". So now I'm going to share how I got "Developer Mode" to show.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to enable "Developer Mode" for iPhone and Xcode
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://developer.apple.com/xcode/"&gt;Download Xcode&lt;/a&gt; on your macOS device. &lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--WMu9PwUb--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/xcode-logo.BxjIYh9C.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--WMu9PwUb--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/xcode-logo.BxjIYh9C.png" alt="xcode logo" width="192" height="192"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Connect your iPhone to your macOS device and make sure to trust your device on your iPhone. &lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--6WliNPCi--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/trust-computer-prompt.BAndkChZ.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--6WliNPCi--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/trust-computer-prompt.BAndkChZ.jpeg" alt="trust computer prompt on iOS" width="295" height="640"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;And enter your passcode on your iPhone &lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--V2J-oLtS--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/enter-device-passcode.9pBouVU-.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--V2J-oLtS--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/enter-device-passcode.9pBouVU-.jpeg" alt="enter passcode on iOS" width="295" height="640"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Open Xcode and navigate to the settings "Product" &amp;gt; "Destination" &amp;gt; "Manage Run Destinations" &lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--3HhJruoX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/xcode-manage-run-destinations.BiM_e87p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--3HhJruoX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/xcode-manage-run-destinations.BiM_e87p.png" alt='menu navigation to "manage run destinations"' width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You should see your device now but there's a warning banner that "Developer Mode" is not enabled. &lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--AReb3ki1--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/xcode-destinations-no-dev-mode.Dqb8xddl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--AReb3ki1--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/xcode-destinations-no-dev-mode.Dqb8xddl.png" alt="destinations management showing a banner that dev mode is not enabled" width="800" height="542"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Head over to "Privacy &amp;amp; Security" in "Settings" on your iPhone and you should now see "Developer Mode". &lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--6rsnb8be--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/privacy-and-security.6Eab4oTO.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--6rsnb8be--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/privacy-and-security.6Eab4oTO.jpeg" alt="privacy and security menu" width="295" height="640"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Turn it on and it will ask you to "Restart" which is required. &lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s---WuA7qq8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/dev-mode-on.DnnNogaN.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s---WuA7qq8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/dev-mode-on.DnnNogaN.jpeg" alt="turning on dev mode" width="295" height="640"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Upon logging back into your iPhone, it will confirm you want to turn on "Developer Mode". Press "Turn On". &lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--gMP6_D95--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/dev-mode-reduced-security.XbKr0X9m.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--gMP6_D95--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/dev-mode-reduced-security.XbKr0X9m.jpeg" alt="dev mode reduced security prompt" width="295" height="640"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;One last time you'll be asked to enter your passcode. &lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--T8KZvNuD--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/dev-mode-passcode.C58zwFZK.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--T8KZvNuD--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/dev-mode-passcode.C58zwFZK.jpeg" alt="dev mode enter the passcode" width="295" height="640"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If you look at Xcode, you should see that your iPhone will try to pair. If not, you may need to wait or click on the warning banner. &lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--QX8xSQXn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/xcode-device-pairing.BvbZSd1D.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--QX8xSQXn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://billyle.dev/_astro/xcode-device-pairing.BvbZSd1D.png" alt="iphone and xcode paring" width="800" height="542"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's it!&lt;/p&gt;

&lt;p&gt;You now have "Developer Mode" set on your iPhone. Turn off "Developer Mode" by going back to your iPhone "Privacy &amp;amp; Security" settings if you no longer need it.&lt;/p&gt;

&lt;p&gt;Good luck, happy coding and see you next time.&lt;/p&gt;

</description>
      <category>mobiledevelopment</category>
      <category>ios</category>
    </item>
  </channel>
</rss>
