<?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>Forem: Piotr Kulpinski</title>
    <description>The latest articles on Forem by Piotr Kulpinski (@piotrkulpinski).</description>
    <link>https://forem.com/piotrkulpinski</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%2F1221643%2F2d45d8ac-488b-4883-988d-b6166b561fb6.png</url>
      <title>Forem: Piotr Kulpinski</title>
      <link>https://forem.com/piotrkulpinski</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/piotrkulpinski"/>
    <language>en</language>
    <item>
      <title>7 Open-Source Tools for Better Website Analytics</title>
      <dc:creator>Piotr Kulpinski</dc:creator>
      <pubDate>Tue, 14 Jan 2025 14:22:33 +0000</pubDate>
      <link>https://forem.com/piotrkulpinski/7-open-source-tools-for-better-website-analytics-1lhl</link>
      <guid>https://forem.com/piotrkulpinski/7-open-source-tools-for-better-website-analytics-1lhl</guid>
      <description>&lt;p&gt;If you are running a website, SaaS, or online business, having a good analytics tool is important for understanding your website's performance and growth. Analytics tools can give you valuable information about user behavior, where your traffic comes from, and how people engage with your site.&lt;/p&gt;

&lt;p&gt;In 2025, open-source analytics tools have become more powerful and affordable, giving businesses of all sizes a way to track and analyze their data without breaking the bank. &lt;/p&gt;

&lt;p&gt;Let's dive into the list:&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Umami
&lt;/h2&gt;

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

&lt;p&gt;If you're looking for a modern and easy-to-use open-source analytics tool that helps you analyze and manage your website traffic, Umami is the perfect one. Umami shows you who visits your site, where they come from, and what they do. It offers tools to track custom events, spot trends, and analyze user behavior, making it great for growing your website.&lt;/p&gt;

&lt;p&gt;You can get started in just minutes with no complicated setup. It's also privacy-friendly and works well for businesses of any size. You can use it for free until you reach a limit of 100,000 events per month.&lt;/p&gt;

&lt;p&gt;If you want clear, real-time insights without the clutter, Umami is the solution you need. It's the best alternative for Fathom Analytics, Kissmetrics, and Google Analytics.&lt;/p&gt;

&lt;p&gt;It's the best alternative for &lt;a href="https://openalternative.co/alternatives/fathom-analytics" rel="noopener noreferrer"&gt;Fathom Analytics&lt;/a&gt;, &lt;a href="https://openalternative.co/alternatives/google-analytics" rel="noopener noreferrer"&gt;Google Analytics&lt;/a&gt;, &lt;a href="https://openalternative.co/alternatives/kissmetrics" rel="noopener noreferrer"&gt;Kissmetrics&lt;/a&gt;, and &lt;a href="https://openalternative.co/alternatives/mixpanel" rel="noopener noreferrer"&gt;MixPanel&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Project Link: &lt;a href="https://openalternative.co/umami" rel="noopener noreferrer"&gt;&lt;strong&gt;Umami&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  2. OpenPanel
&lt;/h2&gt;

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

&lt;p&gt;OpenPanel is an easy-to-use, open-source analytics tool that helps you monitor and understand how people interact with your website or app. It offers a strong alternative to Mixpanel &amp;amp; Plausible without the high costs.&lt;/p&gt;

&lt;p&gt;With real-time analytics and clear dashboards, you can quickly see what works and what doesn't. It includes features like user tracking, event analysis, and detailed reports to help you make better decisions. This tool respects your privacy while providing key information in an easy-to-use format. Easily check visitor details, referrals, popular pages, entry and exit points, device usage, sessions, bounce rate, duration, and geographical data. Use these insights to make informed decisions effortlessly.&lt;/p&gt;

&lt;p&gt;It's perfect for developers and businesses that want control and flexibility.&lt;/p&gt;

&lt;p&gt;It's the best alternative for &lt;a href="https://openalternative.co/alternatives/fathom-analytics" rel="noopener noreferrer"&gt;Fathom Analytics&lt;/a&gt;, &lt;a href="https://openalternative.co/alternatives/segment" rel="noopener noreferrer"&gt;Segment&lt;/a&gt;, &lt;a href="https://openalternative.co/alternatives/google-analytics" rel="noopener noreferrer"&gt;Google Analytics&lt;/a&gt;, &lt;a href="https://openalternative.co/alternatives/amplitude" rel="noopener noreferrer"&gt;Amplitude&lt;/a&gt;, &lt;a href="https://openalternative.co/alternatives/kissmetrics" rel="noopener noreferrer"&gt;Kissmetrics&lt;/a&gt;, and &lt;a href="https://openalternative.co/alternatives/mixpanel" rel="noopener noreferrer"&gt;MixPanel&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Project Link: &lt;a href="https://openalternative.co/openpanel" rel="noopener noreferrer"&gt;&lt;strong&gt;OpenPanel&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Countly
&lt;/h2&gt;

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

&lt;p&gt;This tool is a little bit different from others, as it provides insights based on how your users interact with your products. It's an All-in-One analytic tool for mobile, website, and desktop apps.&lt;/p&gt;

&lt;p&gt;Countly helps you understand how people use your apps and websites. It gathers data from all your platforms and shows you what works well and what doesn't. With Countly, you can make smart decisions to improve your business without needing many different tools. It protects your privacy, so your data stays safe.&lt;/p&gt;

&lt;p&gt;You can customize Countly to fit your needs by creating special dashboards or adding extra features. Countly makes analytics simple and powerful for businesses of all sizes.&lt;/p&gt;

&lt;p&gt;It's the best alternative for &lt;a href="https://openalternative.co/alternatives/june" rel="noopener noreferrer"&gt;June&lt;/a&gt;,  &lt;a href="https://openalternative.co/alternatives/fathom-analytics" rel="noopener noreferrer"&gt;Fathom Analytics&lt;/a&gt;, &lt;a href="https://openalternative.co/alternatives/google-analytics" rel="noopener noreferrer"&gt;Google Analytics&lt;/a&gt;, &lt;a href="https://openalternative.co/alternatives/amplitude" rel="noopener noreferrer"&gt;Amplitude&lt;/a&gt;, &lt;a href="https://openalternative.co/alternatives/kissmetrics" rel="noopener noreferrer"&gt;Kissmetrics&lt;/a&gt;, and &lt;a href="https://openalternative.co/alternatives/mixpanel" rel="noopener noreferrer"&gt;MixPanel&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Project Link: &lt;a href="https://openalternative.co/countly" rel="noopener noreferrer"&gt;&lt;strong&gt;Countly&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Swetrix
&lt;/h2&gt;

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

&lt;p&gt;This analytics tool is special because, unlike other tools, it provides additional information such as performance, sessions, errors, uptime, funnels, and alerts for your website.&lt;/p&gt;

&lt;p&gt;Swetrix keeps your data private and doesn't use cookies, unlike other tools. It is open source, so you can customize it to meet your needs. Whether you have a small website or a large online store, Swetrix makes analytics simple and clear.&lt;/p&gt;

&lt;p&gt;Swetrix is the best choice for monitoring and growing your business if you want to avoid using complicated tools.&lt;/p&gt;

&lt;p&gt;It's the best alternative for &lt;a href="https://openalternative.co/alternatives/fathom-analytics" rel="noopener noreferrer"&gt;Fathom Analytics&lt;/a&gt;, &lt;a href="https://openalternative.co/alternatives/google-analytics" rel="noopener noreferrer"&gt;Google Analytics&lt;/a&gt;, &lt;a href="https://openalternative.co/alternatives/mixpanel" rel="noopener noreferrer"&gt;MixPanel&lt;/a&gt;, and &lt;a href="https://openalternative.co/alternatives/kissmetrics" rel="noopener noreferrer"&gt;Kissmetrics&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Project Link: &lt;a href="https://openalternative.co/swetrix" rel="noopener noreferrer"&gt;&lt;strong&gt;Swetrix&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Matomo
&lt;/h2&gt;

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

&lt;p&gt;Matomo prioritizes the safety and confidentiality of user information while helping you understand how visitors interact with your website. Unlike other analytics platforms, Matomo gives you full ownership of your data, reducing any privacy concerns.&lt;/p&gt;

&lt;p&gt;It's user-friendly and doesn't rely on tracking cookies, making it easier to comply with privacy regulations. You can use Matomo to monitor visitor activity, gain insights into their interests, and make informed decisions to improve your website.&lt;/p&gt;

&lt;p&gt;If you want to grow your website without compromising privacy, Matomo is an excellent choice, trusted by over a million users. &lt;/p&gt;

&lt;p&gt;It's the best alternative for &lt;a href="https://openalternative.co/alternatives/fathom-analytics" rel="noopener noreferrer"&gt;Fathom Analytics&lt;/a&gt;, &lt;a href="https://openalternative.co/alternatives/google-analytics" rel="noopener noreferrer"&gt;Google Analytics&lt;/a&gt;, &lt;a href="https://openalternative.co/alternatives/google-tag-manager" rel="noopener noreferrer"&gt;Google Tag Manager&lt;/a&gt;, and &lt;a href="https://openalternative.co/alternatives/kissmetrics" rel="noopener noreferrer"&gt;Kissmetrics&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Project Link: &lt;a href="https://openalternative.co/matomo" rel="noopener noreferrer"&gt;&lt;strong&gt;Matomo&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Plausible
&lt;/h2&gt;

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

&lt;p&gt;A straightforward and private solution for monitoring website traffic is Plausible Analytics. It doesn't use cookies, so there won't be any prominent banners, and it's quite light, so it won't slow down your website.&lt;/p&gt;

&lt;p&gt;While adhering to privacy regulations like the GDPR, you can know precisely where your traffic is coming from and how users are interacting with your website. It's simple to set up, open source, and even makes it easy to move away from Google Analytics.&lt;/p&gt;

&lt;p&gt;Plausible is the best option if you value privacy and want lucid insights.&lt;/p&gt;

&lt;p&gt;It's the best alternative for &lt;a href="https://openalternative.co/alternatives/fathom-analytics" rel="noopener noreferrer"&gt;Fathom Analytics&lt;/a&gt;, &lt;a href="https://openalternative.co/alternatives/google-analytics" rel="noopener noreferrer"&gt;Google Analytics&lt;/a&gt;, &lt;a href="https://openalternative.co/alternatives/google-workspace" rel="noopener noreferrer"&gt;Google Workspace&lt;/a&gt;, and &lt;a href="https://openalternative.co/alternatives/kissmetrics" rel="noopener noreferrer"&gt;Kissmetrics&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Project Link: &lt;a href="https://openalternative.co/plausible" rel="noopener noreferrer"&gt;&lt;strong&gt;Plausible&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Trench
&lt;/h2&gt;

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

&lt;p&gt;Trench is an open-source tool that makes it super easy to analyze and manage big data in real-time. This tool is only 3 months old and is supported by Y-Combinator.&lt;/p&gt;

&lt;p&gt;It is fast, scalable, and works well for tracking events and user actions on websites or apps. Built with strong technologies like Kafka and Clickhouse, it handles large data volumes effectively. Trench is GDPR-compliant, so you can trust it to protect your data privacy. You can choose to self-host it or use their cloud service, making it flexible for your needs.&lt;/p&gt;

&lt;p&gt;It is perfect for developers who want full control and powerful analytics.&lt;/p&gt;

&lt;p&gt;It's the best alternative for &lt;a href="https://openalternative.co/alternatives/june" rel="noopener noreferrer"&gt;June&lt;/a&gt;, &lt;a href="https://openalternative.co/alternatives/segment" rel="noopener noreferrer"&gt;Segment&lt;/a&gt;, &lt;a href="https://openalternative.co/alternatives/amplitude" rel="noopener noreferrer"&gt;Amplitude&lt;/a&gt;, &lt;a href="https://openalternative.co/alternatives/kissmetrics" rel="noopener noreferrer"&gt;Kissmetrics&lt;/a&gt;, &lt;a href="https://openalternative.co/alternatives/fullstory" rel="noopener noreferrer"&gt;Full Story&lt;/a&gt;, and &lt;a href="https://openalternative.co/alternatives/mixpanel" rel="noopener noreferrer"&gt;MixPanel&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Project Link: &lt;a href="https://openalternative.co/trench" rel="noopener noreferrer"&gt;&lt;strong&gt;Trench&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;




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

&lt;p&gt;I hope these open-source projects are useful to you. You can look into &lt;a href="https://openalternative.co" rel="noopener noreferrer"&gt;OpenAlternative&lt;/a&gt; if they don't meet your demands or if you're searching for other projects. It offers a wide range of free and open-source tools as substitutes for paid and expensive software out there.&lt;/p&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;

&lt;p&gt;Written by &lt;a href="https://x.com/hii_mohit" rel="noopener noreferrer"&gt;Mohit Vaswani&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>software</category>
      <category>analytics</category>
      <category>productivity</category>
    </item>
    <item>
      <title>How to post a link with embed card on Bluesky with JavaScript</title>
      <dc:creator>Piotr Kulpinski</dc:creator>
      <pubDate>Wed, 11 Dec 2024 16:19:47 +0000</pubDate>
      <link>https://forem.com/piotrkulpinski/how-to-post-a-link-with-embed-card-on-bluesky-with-javascript-4bll</link>
      <guid>https://forem.com/piotrkulpinski/how-to-post-a-link-with-embed-card-on-bluesky-with-javascript-4bll</guid>
      <description>&lt;p&gt;As &lt;a href="https://bsky.app" rel="noopener noreferrer"&gt;Bluesky&lt;/a&gt; continues to gain popularity, more tools are being developed around it. One of the most popular applications is post scheduling and automation.&lt;/p&gt;

&lt;p&gt;However, Bluesky's API currently doesn't offer a direct way to post links with OpenGraph cards. This can be a challenge for users who want to share links with attractive previews.&lt;/p&gt;

&lt;p&gt;In this tutorial, I'll show you how to use JavaScript to post a link on Bluesky with a embed card. This method works around the API limitation, allowing you to share links more effectively.&lt;/p&gt;

&lt;p&gt;Let's get started!&lt;/p&gt;

&lt;h2&gt;
  
  
  Posting on Bluesky using JavaScript API
&lt;/h2&gt;

&lt;p&gt;Working with Bluesky API is pretty simple. The &lt;a href="https://docs.bsky.app/docs/get-started" rel="noopener noreferrer"&gt;docs&lt;/a&gt; are pretty good. First up, we need to install the &lt;code&gt;@atproto/api&lt;/code&gt; package from NPM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @atproto/api
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, we create an instance of the Bluesky Agent and login with your Bluesky credencials.&lt;/p&gt;

&lt;p&gt;I recommend creating a new &lt;a href="https://bsky.app/settings/app-passwords" rel="noopener noreferrer"&gt;App Password&lt;/a&gt; for your Bluesky account, rather than using your main password. This will make it easier to revoke access if needed and keep your main account secure. Also make sure to set the &lt;code&gt;BLUESKY_USERNAME&lt;/code&gt; and &lt;code&gt;BLUESKY_PASSWORD&lt;/code&gt; environment variables in your project.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AtpAgent&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@atproto/api&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getBlueskyAgent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AtpAgent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://bsky.social&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;login&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;identifier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;BLUESKY_USERNAME&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;BLUESKY_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once you have the agent, you can use it to post to Bluesky which is pretty straightforward.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/**
 * Send a post to Bluesky
 * @param text - The text of the post
 */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sendBlueskyPost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getBlueskyAgent&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And there you have it, you just sent a post to Bluesky. Unfortunately, even if you include a link in the text of your post, it isn't automatically converted into an anchor link. We'll fix that shortly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Detect Faceted Links on Bluesky Automatically
&lt;/h2&gt;

&lt;p&gt;When you include a link in your post text on Bluesky, it isn't automatically turned into an anchor link. Instead, it appears as plain text.&lt;/p&gt;

&lt;p&gt;To fix this, you need to detect the links and convert them into faceted links.&lt;/p&gt;

&lt;p&gt;While there are manual methods to achieve this, fortunately, ATProto provides a &lt;code&gt;RichText&lt;/code&gt; class that can automatically detect links and convert them into faceted links.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;RichText&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@atproto/api&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * Send a post to Bluesky
 * @param text - The text of the post
 */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sendBlueskyPost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getBlueskyAgent&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RichText&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;rt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;detectFacets&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;facets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;facets&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's great, but we still need to add the embed card to the post. Let's do that next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating an Embed Card on Bluesky
&lt;/h2&gt;

&lt;p&gt;Including a link in your post is great, but it's even better if you can add a embed card.&lt;/p&gt;

&lt;p&gt;To achieve this, we need to use the &lt;a href="https://docs.bsky.app/docs/advanced-guides/posts#website-card-embeds" rel="noopener noreferrer"&gt;Website card embed&lt;/a&gt; feature of Bluesky. Essentially, you add an embed key to your post that includes, at a minimum, a URL, title, and description.&lt;/p&gt;

&lt;p&gt;There are several ways to obtain the required data. If you know it at the time of posting, you can simply hardcode it. Otherwise, you can scrape the URL to gather the title, description, and image.&lt;/p&gt;

&lt;p&gt;However, I find the easiest way is to use the &lt;a href="https://dub.co/tools/metatags" rel="noopener noreferrer"&gt;Dub.co Metatags API&lt;/a&gt; to fetch the URL metadata and then create the embed card from that. Let's see how that works.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Metadata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * Get the URL metadata
 * @param url - The URL to get the metadata for
 * @returns The metadata
 */&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getUrlMetadata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`https://api.dub.co/metatags?url=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Metadata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;metadata&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We created a simple function that fetches the URL metadata and then returns the data in a clear format.&lt;/p&gt;

&lt;p&gt;Next, let's create a function that uses the metadata to upload the image to Bluesky and then create the embed card.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getBlueskyEmbedCard&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AtpAgent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;metadata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getUrlMetadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uploadBlob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;encoding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image/jpeg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;$type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;app.bsky.embed.external&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;external&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;thumb&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Error fetching embed card:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once we have the embed card, we can add it to the post.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sendBlueskyPost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getBlueskyAgent&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RichText&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;rt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;detectFacets&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;facets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;facets&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getBlueskyEmbedCard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we have a function that sends a post to Bluesky with an embed card.&lt;/p&gt;

&lt;h2&gt;
  
  
  Complete Example
&lt;/h2&gt;

&lt;p&gt;Hopefully, if you have followed along, you should have a complete code by now. If not, here is the complete code that you can copy and paste into your project. It:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Creates a Bluesky agent&lt;/li&gt;
&lt;li&gt;Fetches the URL metadata&lt;/li&gt;
&lt;li&gt;Creates an embed card&lt;/li&gt;
&lt;li&gt;Sends a post to Bluesky with the embed card and automatically detects faceted links
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AtpAgent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;RichText&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@atproto/api&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Metadata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * Get the URL metadata
 * @param url - The URL to get the metadata for
 * @returns The metadata
 */&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getUrlMetadata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`https://api.dub.co/metatags?url=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Metadata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;metadata&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * Get the Bluesky embed card
 * @param url - The URL to get the embed card for
 * @param agent - The Bluesky agent
 * @returns The embed card
 */&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getBlueskyEmbedCard&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AtpAgent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;metadata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getUrlMetadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uploadBlob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;encoding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image/jpeg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;$type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;app.bsky.embed.external&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;external&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;thumb&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Error fetching embed card:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * Get the Bluesky agent
 * @returns The Bluesky agent
 */&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getBlueskyAgent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AtpAgent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://bsky.social&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;login&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;identifier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;BLUESKY_USERNAME&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;BLUESKY_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * Send a post to Bluesky
 * @param text - The text of the post
 * @param url - The URL to include in the post
 */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sendBlueskyPost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getBlueskyAgent&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RichText&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;rt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;detectFacets&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;facets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;facets&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getBlueskyEmbedCard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I hope you found this tutorial helpful and that you will consider using it in your own projects.&lt;/p&gt;

&lt;p&gt;Happy posting!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>atprotocol</category>
      <category>bluesky</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Top 5 Open-Source Projects for Developers</title>
      <dc:creator>Piotr Kulpinski</dc:creator>
      <pubDate>Wed, 04 Dec 2024 14:19:29 +0000</pubDate>
      <link>https://forem.com/piotrkulpinski/top-5-open-source-projects-for-developers-477n</link>
      <guid>https://forem.com/piotrkulpinski/top-5-open-source-projects-for-developers-477n</guid>
      <description>&lt;p&gt;If you’re a developer looking to improve your productivity, check out some outstanding open-source projects I’ve found. They can enhance your skills and streamline your workflow effectively.&lt;/p&gt;

&lt;p&gt;These projects are made by skilled developers for the entire community. Whether you’re just starting or have years of experience, there’s something for you.&lt;/p&gt;

&lt;p&gt;They will help you with a variety of tasks like —&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Track, improve, and build better apps.&lt;/li&gt;
&lt;li&gt;  Find and Fix code errors faster than ever.&lt;/li&gt;
&lt;li&gt;  Connect and organize your data for smarter decisions.&lt;/li&gt;
&lt;li&gt;  See and fix issues in your app while keeping your data secure.&lt;/li&gt;
&lt;li&gt;  Make your website analytics much easier and shareable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let’s take a look at the top five projects in detail.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. PostHog
&lt;/h2&gt;

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

&lt;p&gt;This project is a secret weapon for developers who want to build awesome websites and apps that users love to use.&lt;/p&gt;

&lt;p&gt;It helps you track what people are doing on your website, find ways to make it better and fix problems quickly. All of your data is in one place, and you have complete control over it.&lt;/p&gt;

&lt;p&gt;It’s super powerful, easy to use, and makes building successful &amp;amp; scalable apps feel easy.&lt;/p&gt;

&lt;p&gt;It’s the best alternative for &lt;a href="https://openalternative.co/alternatives/fathom-analytics" rel="noopener noreferrer"&gt;&lt;strong&gt;Fathom Analytics&lt;/strong&gt;&lt;/a&gt;, &lt;a href="https://openalternative.co/alternatives/segment" rel="noopener noreferrer"&gt;&lt;strong&gt;Segment&lt;/strong&gt;&lt;/a&gt;, &lt;a href="https://openalternative.co/alternatives/amplitude" rel="noopener noreferrer"&gt;&lt;strong&gt;Amplitude&lt;/strong&gt;&lt;/a&gt;, &lt;a href="https://openalternative.co/alternatives/optimizely" rel="noopener noreferrer"&gt;&lt;strong&gt;Optimizely&lt;/strong&gt;&lt;/a&gt;, &lt;a href="https://openalternative.co/alternatives/june" rel="noopener noreferrer"&gt;&lt;strong&gt;June&lt;/strong&gt;&lt;/a&gt;, and &lt;a href="https://openalternative.co/alternatives/mixpanel" rel="noopener noreferrer"&gt;&lt;strong&gt;MixPanel&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Many big companies are satisfied customers of Posthog.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Project Link:&lt;/strong&gt; &lt;a href="https://openalternative.co/posthog" rel="noopener noreferrer"&gt;Posthog&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Sentry
&lt;/h2&gt;

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

&lt;p&gt;When your app has bugs, it can be frustrating and time-consuming to fix them. But with Sentry, you can resolve errors quickly and easily.&lt;/p&gt;

&lt;p&gt;It helps you find what went wrong in your codebase and shows you how to fix it. You can track issues, replay user sessions, and even see what your users are experiencing — all in one place.&lt;/p&gt;

&lt;p&gt;It’s like having an expert programmer for your coding. You won’t need to guess or waste time anymore. You can create clean and effective websites faster than ever.&lt;/p&gt;

&lt;p&gt;It’s the best alternative for &lt;a href="https://openalternative.co/alternatives/datadog" rel="noopener noreferrer"&gt;&lt;strong&gt;Datadog&lt;/strong&gt;&lt;/a&gt;, &lt;a href="https://openalternative.co/alternatives/betterstack" rel="noopener noreferrer"&gt;&lt;strong&gt;BetterStack&lt;/strong&gt;&lt;/a&gt; and &lt;a href="https://openalternative.co/alternatives/logrocket" rel="noopener noreferrer"&gt;&lt;strong&gt;LogRocket&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Project Link&lt;/strong&gt;: &lt;a href="https://openalternative.co/sentry" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Airbyte
&lt;/h2&gt;

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

&lt;p&gt;This project helps you make your data more useful, no matter where it is stored. It connects data from different apps and platforms, allowing you to use it in smart ways, such as enhancing AI or improving your business.&lt;/p&gt;

&lt;p&gt;It’s open-source, so anyone can use it. Also, it’s easy to set up, secure and works with tons of tools.&lt;/p&gt;

&lt;p&gt;Whether you’re a data wizard or just starting out, this makes managing and using your data a breeze.&lt;/p&gt;

&lt;p&gt;It’s the best alternative for &lt;a href="https://openalternative.co/alternatives/fivetran" rel="noopener noreferrer"&gt;&lt;strong&gt;Fivetran&lt;/strong&gt;&lt;/a&gt;, &lt;a href="https://openalternative.co/alternatives/matillion" rel="noopener noreferrer"&gt;&lt;strong&gt;Matillion&lt;/strong&gt;&lt;/a&gt;, and &lt;a href="https://openalternative.co/alternatives/supermetrics" rel="noopener noreferrer"&gt;&lt;strong&gt;Supermetrics&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Project Link:&lt;/strong&gt; &lt;a href="https://openalternative.co/airbyte" rel="noopener noreferrer"&gt;Airbyte&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  4. OpenReplay
&lt;/h2&gt;

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

&lt;p&gt;OpenReplay is like a time machine for your apps. It lets you watch exactly what your users are doing on your website or app — like a replay of their sessions.&lt;/p&gt;

&lt;p&gt;You can find and fix bugs to make your website run better. The best part is that you can host it yourself, keeping all your data safe and private. It has great features like product analytics, DevTools, and co-browsing to help users in real time.&lt;/p&gt;

&lt;p&gt;Honestly, it’s like having a perfect tool to make your app the best.&lt;/p&gt;

&lt;p&gt;It’s the best alternative for &lt;a href="https://openalternative.co/alternatives/mixpanel" rel="noopener noreferrer"&gt;&lt;strong&gt;Mixpanel&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;,&lt;/strong&gt; &lt;a href="https://openalternative.co/alternatives/new-relic" rel="noopener noreferrer"&gt;&lt;strong&gt;New Relic&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;,&lt;/strong&gt; &lt;a href="https://openalternative.co/alternatives/betterstack" rel="noopener noreferrer"&gt;&lt;strong&gt;BetterStack&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;,&lt;/strong&gt; &lt;a href="https://openalternative.co/alternatives/kissmetrics" rel="noopener noreferrer"&gt;&lt;strong&gt;Kissmetrics&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;,&lt;/strong&gt; &lt;a href="https://openalternative.co/alternatives/amplitude" rel="noopener noreferrer"&gt;&lt;strong&gt;Amplitude&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;,&lt;/strong&gt; &lt;a href="https://openalternative.co/alternatives/june" rel="noopener noreferrer"&gt;&lt;strong&gt;June&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;,&lt;/strong&gt; &lt;a href="https://openalternative.co/alternatives/logrocket" rel="noopener noreferrer"&gt;&lt;strong&gt;LogRocket&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;,&lt;/strong&gt; and &lt;a href="https://openalternative.co/alternatives/fullstory" rel="noopener noreferrer"&gt;&lt;strong&gt;FullStory&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Project Link:&lt;/strong&gt; &lt;a href="https://openalternative.co/openreplay" rel="noopener noreferrer"&gt;OpenReplay&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Plausible
&lt;/h2&gt;

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

&lt;p&gt;Plausible is the cool, privacy-friendly analytics your website needs. It’s a perfect alternative to Google Analytics that shows you all the necessary stats about your site — like visitors, clicks, and where people are coming from.&lt;/p&gt;

&lt;p&gt;It’s lightweight, so it won’t slow down your site, and it doesn’t use cookies or need those unnecessary consent banners. Also, it’s open-source, so you can trust it’s safe and transparent.&lt;/p&gt;

&lt;p&gt;If you care about privacy and want clean, no-fuss data, Plausible is your perfect match!&lt;/p&gt;

&lt;p&gt;It’s the best alternative for &lt;a href="https://openalternative.co/alternatives/mixpanel" rel="noopener noreferrer"&gt;&lt;strong&gt;Mixpanel&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;,&lt;/strong&gt; &lt;a href="https://openalternative.co/alternatives/google-analytics" rel="noopener noreferrer"&gt;&lt;strong&gt;Google Analytics&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;,&lt;/strong&gt; &lt;a href="https://openalternative.co/alternatives/kissmetrics" rel="noopener noreferrer"&gt;&lt;strong&gt;Kissmetrics&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;,&lt;/strong&gt; &lt;a href="https://openalternative.co/alternatives/fathom-analytics" rel="noopener noreferrer"&gt;&lt;strong&gt;Fathom Analytics&lt;/strong&gt;&lt;/a&gt; and &lt;a href="https://openalternative.co/alternatives/google-workspace" rel="noopener noreferrer"&gt;&lt;strong&gt;Google Workspace&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Project Link:&lt;/strong&gt; &lt;a href="https://openalternative.co/plausible" rel="noopener noreferrer"&gt;Plausible&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;I hope you find these open-source projects helpful. If they don’t fit your needs or if you’re looking for more projects, you can explore &lt;a href="https://openalternative.co/" rel="noopener noreferrer"&gt;&lt;strong&gt;OpenAlternative&lt;/strong&gt;&lt;/a&gt;. It includes a variety of open-source alternative tools to paid software.&lt;/p&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;




&lt;p&gt;Written by &lt;a href="https://x.com/hii_mohit" rel="noopener noreferrer"&gt;Mohit Vaswani&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>programming</category>
      <category>software</category>
      <category>github</category>
    </item>
    <item>
      <title>Supercharge Your Number Inputs with Custom Step Sizes</title>
      <dc:creator>Piotr Kulpinski</dc:creator>
      <pubDate>Fri, 26 Apr 2024 21:33:41 +0000</pubDate>
      <link>https://forem.com/piotrkulpinski/supercharge-your-number-inputs-with-custom-step-sizes-mlh</link>
      <guid>https://forem.com/piotrkulpinski/supercharge-your-number-inputs-with-custom-step-sizes-mlh</guid>
      <description>&lt;p&gt;The &lt;code&gt;&amp;lt;input type="number"&amp;gt;&lt;/code&gt; element provides a convenient way to handle numeric input in web forms. You can set bounds with the &lt;code&gt;min&lt;/code&gt; and &lt;code&gt;max&lt;/code&gt; attributes, and users can press the up and down arrow keys to increment or decrement the value by the specified &lt;code&gt;step&lt;/code&gt; size. However, what if you want to allow users to adjust the value using different step sizes?&lt;/p&gt;

&lt;p&gt;By default, the &lt;code&gt;step&lt;/code&gt; attribute not only determines the increment/decrement amount but also clips the value to the nearest multiple of the step. For example, if you have an input with a value of 5 and a step of 10, pressing the up arrow key will set the value to 10 (the nearest multiple of the step) instead of 15 (5 + 10).&lt;/p&gt;

&lt;h2&gt;
  
  
  How to build a better number input
&lt;/h2&gt;

&lt;p&gt;In this article, we'll explore how to enhance the functionality of number inputs to support custom step sizes. We'll implement the following features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;↑/↓&lt;/code&gt; – adjusts the value by the specified &lt;code&gt;step&lt;/code&gt; size (1 if step is not specified).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Shift&lt;/code&gt;+&lt;code&gt;↑/↓&lt;/code&gt; – adjusts the value by step &lt;strong&gt;multiplied by 10&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Alt&lt;/code&gt;+&lt;code&gt;↑/↓&lt;/code&gt; – adjusts the value by step &lt;strong&gt;divided by 10&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Ctrl/Cmd&lt;/code&gt;+&lt;code&gt;↑/↓&lt;/code&gt; – adjusts the value by step &lt;strong&gt;multiplied by 100&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;If the input is empty, the value is calculated based on the &lt;code&gt;min&lt;/code&gt; attribute (0 if &lt;code&gt;min&lt;/code&gt; is not specified).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's dive into the implementation!&lt;/p&gt;

&lt;h2&gt;
  
  
  Helper Functions
&lt;/h2&gt;

&lt;p&gt;First, let's define some utility functions that we'll use throughout the implementation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parseNumericValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parseFloat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isNaN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;parseNumericValue&lt;/code&gt; function takes a value (which can be a string, number, or null) and parses it as a floating-point number. If the value is &lt;code&gt;undefined&lt;/code&gt; or &lt;code&gt;null&lt;/code&gt;, it returns &lt;code&gt;undefined&lt;/code&gt;. If the parsing fails, it also returns &lt;code&gt;undefined&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This function will be useful for parsing the &lt;code&gt;min&lt;/code&gt;, &lt;code&gt;max&lt;/code&gt;, &lt;code&gt;step&lt;/code&gt;, and current value of the input elements.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;preciseRound&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;decimals&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;factor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;decimals&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;EPSILON&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;factor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;factor&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;preciseRound&lt;/code&gt; function rounds a number to a specified number of decimal places. It uses the &lt;code&gt;Number.EPSILON&lt;/code&gt; constant to handle &lt;a href="https://blog.demofox.org/2017/11/21/floating-point-precision/"&gt;floating-point precision issues&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;keepNumberInRange&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;min&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;max&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;min&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;max&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;min&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;max&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;min&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;min&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;max&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;max&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;keepNumberInRange&lt;/code&gt; function ensures that a given number stays within the specified &lt;code&gt;min&lt;/code&gt; and &lt;code&gt;max&lt;/code&gt; bounds. I like to use reusable utility functions like this to keep the code clean and maintainable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementing the Key Up Handler
&lt;/h2&gt;

&lt;p&gt;Here's the main event handler function that will be called when the user presses the up or down arrow keys on a number input:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;KeyboardEvent&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleKeyUp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;KeyboardEvent&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLInputElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ArrowUp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ArrowDown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentTarget&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;direction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ArrowUp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;min&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseNumericValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;min&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;max&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseNumericValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;max&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseNumericValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;step&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseNumericValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;min&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;increment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;metaKey&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;shiftKey&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;altKey&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;newValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;preciseRound&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;direction&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;increment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newValue&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;keepNumberInRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newValue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;min&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;max&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Usage: &amp;lt;input type="number" onKeyUp={handleKeyUp} /&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's break down the code:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We check if the pressed key is either "ArrowUp" or "ArrowDown". If not, we return early.&lt;/li&gt;
&lt;li&gt;We get the current target input element and determine the direction based on the pressed key (+1 for "ArrowUp", -1 for "ArrowDown").&lt;/li&gt;
&lt;li&gt;We parse the &lt;code&gt;min&lt;/code&gt;, &lt;code&gt;max&lt;/code&gt;, and &lt;code&gt;step&lt;/code&gt; attributes of the input element using the &lt;code&gt;parseNumericValue&lt;/code&gt; utility function.&lt;/li&gt;
&lt;li&gt;We parse the current value of the input using &lt;code&gt;parseNumericValue&lt;/code&gt; or fallback to the &lt;code&gt;min&lt;/code&gt; value or 0 if the input is empty.&lt;/li&gt;
&lt;li&gt;We calculate the increment based on the modifier keys: Ctrl/Cmd multiplies the step by 100, Shift multiplies it by 10, Alt divides it by 10, and the default is 1.&lt;/li&gt;
&lt;li&gt;We calculate the new value by adding the product of the direction and the increment to the current value, using the &lt;code&gt;preciseRound&lt;/code&gt; utility function to handle floating-point precision.&lt;/li&gt;
&lt;li&gt;If the new value is different from the current value, we update the input value using the &lt;code&gt;setInputValue&lt;/code&gt; function, ensuring it stays within the specified &lt;code&gt;min&lt;/code&gt; and &lt;code&gt;max&lt;/code&gt; range.&lt;/li&gt;
&lt;li&gt;Finally, we call &lt;code&gt;e.preventDefault()&lt;/code&gt; to prevent the default browser behavior.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With this implementation, your number inputs will have the desired supercharged behavior, allowing users to quickly adjust values using different step sizes based on the modifier keys they press.&lt;/p&gt;

&lt;p&gt;You can easily integrate this code into your project by attaching the &lt;code&gt;handleKeyUp&lt;/code&gt; event handler to your number input elements.&lt;/p&gt;

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

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>ux</category>
      <category>beginners</category>
    </item>
    <item>
      <title>How to Build Dynamic Breadcrumbs in Remix</title>
      <dc:creator>Piotr Kulpinski</dc:creator>
      <pubDate>Fri, 29 Mar 2024 20:30:46 +0000</pubDate>
      <link>https://forem.com/piotrkulpinski/how-to-build-dynamic-breadcrumbs-in-remix-1nid</link>
      <guid>https://forem.com/piotrkulpinski/how-to-build-dynamic-breadcrumbs-in-remix-1nid</guid>
      <description>&lt;p&gt;In Remix, building dynamic breadcrumbs that reflect your route hierarchy is straightforward. This tutorial will guide you through leveraging the &lt;code&gt;useMatches&lt;/code&gt; and &lt;code&gt;handle&lt;/code&gt; capabilities to achieve this.&lt;/p&gt;

&lt;p&gt;We'll also cover how to add schema metadata to your breadcrumbs for better SEO and social sharing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Basics
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Breadcrumbs&lt;/strong&gt; are a navigation aid that helps users understand their current location within a website. They typically appear horizontally at the top of a page and show the hierarchy of the current page in relation to the site structure.&lt;/p&gt;

&lt;p&gt;Here's an example of what breadcrumbs code could look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;ol&lt;/span&gt; &lt;span class="na"&gt;itemscope&lt;/span&gt; &lt;span class="na"&gt;itemtype=&lt;/span&gt;&lt;span class="s"&gt;"https://schema.org/BreadcrumbList"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;li&lt;/span&gt; &lt;span class="na"&gt;itemprop=&lt;/span&gt;&lt;span class="s"&gt;"itemListElement"&lt;/span&gt; &lt;span class="na"&gt;itemscope&lt;/span&gt; &lt;span class="na"&gt;itemtype=&lt;/span&gt;&lt;span class="s"&gt;"https://schema.org/ListItem"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://example.com/"&lt;/span&gt; &lt;span class="na"&gt;itemprop=&lt;/span&gt;&lt;span class="s"&gt;"item"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;itemprop=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Home&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;itemprop=&lt;/span&gt;&lt;span class="s"&gt;"position"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;li&lt;/span&gt; &lt;span class="na"&gt;itemprop=&lt;/span&gt;&lt;span class="s"&gt;"itemListElement"&lt;/span&gt; &lt;span class="na"&gt;itemscope&lt;/span&gt; &lt;span class="na"&gt;itemtype=&lt;/span&gt;&lt;span class="s"&gt;"https://schema.org/ListItem"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://example.com/posts"&lt;/span&gt; &lt;span class="na"&gt;itemprop=&lt;/span&gt;&lt;span class="s"&gt;"item"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;itemprop=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Posts&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;itemprop=&lt;/span&gt;&lt;span class="s"&gt;"position"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"2"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;li&lt;/span&gt; &lt;span class="na"&gt;itemprop=&lt;/span&gt;&lt;span class="s"&gt;"itemListElement"&lt;/span&gt; &lt;span class="na"&gt;itemscope&lt;/span&gt; &lt;span class="na"&gt;itemtype=&lt;/span&gt;&lt;span class="s"&gt;"https://schema.org/ListItem"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://example.com/posts/slug"&lt;/span&gt; &lt;span class="na"&gt;itemprop=&lt;/span&gt;&lt;span class="s"&gt;"item"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;itemprop=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Post Title&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;itemprop=&lt;/span&gt;&lt;span class="s"&gt;"position"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"3"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/ol&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, let's see how to build dynamic breadcrumbs in Remix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Defining the Breadcrumbs Components
&lt;/h2&gt;

&lt;p&gt;Let's start by creating the necessary component that we'll later use to render the breadcrumbs.&lt;/p&gt;

&lt;p&gt;We'll need a &lt;code&gt;Breadcrumbs&lt;/code&gt; component to render the list of breadcrumbs and a &lt;code&gt;BreadcrumbsItem&lt;/code&gt; component to render each breadcrumb item.&lt;/p&gt;

&lt;p&gt;Start by defining a wrapper component that will list all of the breadcrumbs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/components/Breadcrumbs.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;UIMatch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useMatches&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@remix-run/react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Fragment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;HTMLAttributes&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;BreadcrumbMatch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;UIMatch&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;
  &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;breadcrumb&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;JSX&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Element&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Breadcrumbs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;HTMLAttributes&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;matches&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;useMatches&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;BreadcrumbMatch&lt;/span&gt;&lt;span class="p"&gt;[]).&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;handle&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;breadcrumb&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;ol&lt;/span&gt;
      &lt;span class="na"&gt;itemScope&lt;/span&gt;
      &lt;span class="na"&gt;itemType&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"https://schema.org/BreadcrumbList"&lt;/span&gt;
      &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"flex flex-wrap items-center gap-2.5"&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Fragment&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;
            &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"contents"&lt;/span&gt;
            &lt;span class="na"&gt;itemProp&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"itemListElement"&lt;/span&gt;
            &lt;span class="na"&gt;itemScope&lt;/span&gt;
            &lt;span class="na"&gt;itemType&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"https://schema.org/ListItem"&lt;/span&gt;
          &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text-sm"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;/&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
            &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;breadcrumb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;meta&lt;/span&gt; &lt;span class="na"&gt;itemProp&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"position"&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Fragment&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;ol&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the example above, we're using the &lt;code&gt;useMatches&lt;/code&gt; hook to get the current route matches. We then filter the matches to only include those that have a &lt;code&gt;breadcrumb&lt;/code&gt; handle. We will define this handle in the routes where we want to include a breadcrumb. We then iterate over the matches and render the breadcrumb component for each match.&lt;/p&gt;

&lt;p&gt;You may have noticed that we're also passing a &lt;code&gt;data&lt;/code&gt; prop to the &lt;code&gt;handle.breadcrumb&lt;/code&gt; function. This is because the &lt;code&gt;breadcrumb&lt;/code&gt; handle can accept data from the route loader. We'll see how to pass data to the breadcrumb handle in the next section.&lt;/p&gt;

&lt;p&gt;Next, let's define the &lt;code&gt;BreadcrumbsItem&lt;/code&gt; component that we'll use to render each breadcrumb item.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/components/BreadcrumbsItem.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Link&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@remix-run/react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;HTMLAttributes&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;BreadcrumbsItem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;HTMLAttributes&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Link&lt;/span&gt; &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;itemProp&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"item"&lt;/span&gt; &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt; &lt;span class="na"&gt;itemProp&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Link&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now that we have our components, let's define the breadcrumbs in our routes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Defining the Breadcrumbs handles in the Routes
&lt;/h2&gt;

&lt;p&gt;In the routes where you want to include breadcrumbs, you need to define a &lt;code&gt;breadcrumb&lt;/code&gt; handle that will render the breadcrumb item.&lt;/p&gt;

&lt;p&gt;For example, let's define the following routes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/&lt;/code&gt; - Home&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/posts&lt;/code&gt; - Posts&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/posts/$slug&lt;/code&gt; - Post Details
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/routes/index.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;BreadcrumbsItem&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;~/components/BreadcrumbsItem&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;breadcrumb&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;BreadcrumbsItem&lt;/span&gt; &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Home&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;BreadcrumbsItem&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Index&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Home&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/routes/posts/index.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;BreadcrumbsItem&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;~/components/BreadcrumbsItem&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;breadcrumb&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;BreadcrumbsItem&lt;/span&gt; &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/posts"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Posts&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;BreadcrumbsItem&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Posts&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Posts&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/routes/posts.$slug.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;LoaderFunctionArgs&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@remix-run/node&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useLoaderData&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@remix-run/react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;BreadcrumbsItem&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;~/components/BreadcrumbsItem&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;breadcrumb&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;BreadcrumbsItem&lt;/span&gt; &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`/posts/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;BreadcrumbsItem&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;loader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;LoaderFunctionArgs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Example Post&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;example-post&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useLoaderData&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;loader&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the last route, we're using the &lt;code&gt;loader&lt;/code&gt; function to fetch the post data. We then pass this data to the &lt;code&gt;breadcrumb&lt;/code&gt; handle to render the breadcrumb item. This allows us to dynamically generate the breadcrumb item based on the post data.&lt;/p&gt;

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

&lt;p&gt;Hopefully, this guide has helped you understand how to implement breadcrumbs in your Remix application.&lt;/p&gt;

&lt;p&gt;If you're interested to see how breadcrumbs can be implemented in a real-world application, you can check out the &lt;a href="https://openalternative.co"&gt;OpenAlternative&lt;/a&gt; project, or check out the &lt;a href="https://github.com/piotrkulpinski/openalternative"&gt;source code&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you have any questions or need further clarification, feel free to reach out to me on &lt;a href="https://twitter.com/piotrkulpinski"&gt;Twitter&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>breadcrumbs</category>
      <category>webdev</category>
      <category>tutorial</category>
      <category>remix</category>
    </item>
    <item>
      <title>Growing a side-project to 100k Unique Visitors in one week</title>
      <dc:creator>Piotr Kulpinski</dc:creator>
      <pubDate>Tue, 19 Mar 2024 21:43:24 +0000</pubDate>
      <link>https://forem.com/piotrkulpinski/growing-a-side-project-to-100k-unique-visitors-in-one-week-29mf</link>
      <guid>https://forem.com/piotrkulpinski/growing-a-side-project-to-100k-unique-visitors-in-one-week-29mf</guid>
      <description>&lt;p&gt;I'm a software engineer by trade, but I've always been interested in marketing and growth. I've been working on a various side projects for a while now, and I recently hit a major success: &lt;strong&gt;100,000 unique visitors in one week&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here's how I did it 👇&lt;/p&gt;

&lt;p&gt;&lt;a href="https://twitter.com/piotrkulpinski/status/1767108966603980866" rel="noopener noreferrer"&gt;https://twitter.com/piotrkulpinski/status/1767108966603980866&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Idea
&lt;/h2&gt;

&lt;p&gt;I was collecting a list of open-source software for a while. Mostly for my own use (to learn and reuse parts of the code), but I thought it would be cool to share it with others. I was also interested in learning more about SEO and growth, so I thought this would be a good project to experiment with.&lt;/p&gt;

&lt;p&gt;I didn't want to spend a lot of time on it, so I decided to build it within &lt;strong&gt;48 hours&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;With such a short time frame, I had to be very selective about the features I wanted to include. I decided to build a simple listing with no search or filtering.&lt;/p&gt;

&lt;h2&gt;
  
  
  Curating the list
&lt;/h2&gt;

&lt;p&gt;I started by expanding the list of open-source software. I used a combination of Google, Reddit, and GitHub to find the most popular open-source projects. I also used my own knowledge and experience to find lesser-known projects that I thought were worth including.&lt;/p&gt;

&lt;p&gt;I didn't want to include every open-source project I could find. I wanted to keep the list high-quality and focused on the most popular, useful software and actively maintained projects. I &lt;strong&gt;collected about 70 projects&lt;/strong&gt; in total.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the site
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://astro.build" rel="noopener noreferrer"&gt;Astro&lt;/a&gt; was always on my list of things to learn. I've been using Remix and NextJS for a while, and I was interested in trying out a new framework. I decided it would be a good opportunity to build the site with it. This decision turned out to be a great one, as it saved me a lot of money on hosting costs later on.&lt;/p&gt;

&lt;p&gt;While browsing the Astro documentation, I found out they have a pretty easy way to implement a &lt;a href="https://developer.chrome.com/docs/web-platform/view-transitions" rel="noopener noreferrer"&gt;View Transitions&lt;/a&gt; so I tought it would be a nice touch to the site.&lt;/p&gt;

&lt;p&gt;For the backend, I opted for &lt;a href="https://airtable.com" rel="noopener noreferrer"&gt;Airtable&lt;/a&gt; as a database. It's a simple, no-code solution that I've used before. It's not the most powerful database, but it's perfect for a project like this. I could easily add, edit, and delete records, and it has an embeddable form functionality that I used for user submissions.&lt;/p&gt;

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

&lt;p&gt;Unfortunately, Airtable's API isn't the best for querying data, so I used a &lt;a href="https://baseql.com" rel="noopener noreferrer"&gt;middleware service&lt;/a&gt; to turn the Airtable API into a GraphQL API. This allowed me to query the data in a more flexible way.&lt;/p&gt;

&lt;p&gt;With the data and the site structure in place, I started building the site. I used Tailwind CSS for the styling, and I was able to build the site in about &lt;strong&gt;12 hours&lt;/strong&gt;. The good thing about having multiple projects under my belt is that I can reuse a lot of the code I've written before.&lt;/p&gt;

&lt;p&gt;Building an Open Source listing, it was pretty obvious that I should open-source &lt;a href="https://github.com/piotrkulpinski/openalternative" rel="noopener noreferrer"&gt;the code&lt;/a&gt; as well.&lt;/p&gt;

&lt;h2&gt;
  
  
  GitHub Data and Programmatic SEO
&lt;/h2&gt;

&lt;p&gt;To make the site more useful, I decided to include some data from GitHub. I've built a scheduled Cloudflare Worker that's pulling the GitHub API data to fetch the number of stars, forks, and issues for each project.&lt;/p&gt;

&lt;p&gt;I also pulled some related tags from each repo, like what programming language it's written in, and what it's related to (e.g. CMS, CRM, etc.). I then put this data into Airtable and used it to generate lots of pages for &lt;strong&gt;Programmatic SEO&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It's too early to tell if this will have a big impact on SEO, but I think it's a good idea to have a lot of pages for Google to index. It's already ranking for some very competitive keywords like "open source alternatives" so I think it may work pretty well.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Hosting
&lt;/h2&gt;

&lt;p&gt;Since Astro is a static site generator, I could host the site for free on &lt;a href="https://cloudflare.com" rel="noopener noreferrer"&gt;Cloudflare&lt;/a&gt;. I've never used Cloudflare before, but they've been pretty popular lately due to their free hosting and CDN. I was impressed with how easy it was to set up, and the performance was great.&lt;/p&gt;

&lt;p&gt;I then picked the simplest name I could find and registered &lt;a href="https://openalternative.co" rel="noopener noreferrer"&gt;openalternative.co&lt;/a&gt; for $6.99.&lt;/p&gt;

&lt;h2&gt;
  
  
  Launch(es)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Twitter
&lt;/h3&gt;

&lt;p&gt;Like always, I posted about it on &lt;a href="https://twitter.com/piotrkulpinski/status/1764561508028326128" rel="noopener noreferrer"&gt;Twitter&lt;/a&gt;. Given that I only had 900 followers and I usually tweet to the void, I didn't expect much. But to my surprise, the tweet got some attention. Nothing crazy, but it was a good start.&lt;/p&gt;

&lt;p&gt;For the next few days I let it sit and see what happens. I was engaging with people on Twitter to get some feedback. One of such conversations was with &lt;a href="https://twitter.com/steventey" rel="noopener noreferrer"&gt;Steven Tey&lt;/a&gt; who seemed to like the idea and decided to share it with his audience.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://twitter.com/steventey/status/1765841867017437599" rel="noopener noreferrer"&gt;https://twitter.com/steventey/status/1765841867017437599&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This was a huge boost for the site, as it got a lot of traffic from his tweet.&lt;/p&gt;

&lt;h3&gt;
  
  
  Product Hunt
&lt;/h3&gt;

&lt;p&gt;I knew I had to keep the momentum going, so I decided to post it on &lt;a href="https://www.producthunt.com/posts/openalternative" rel="noopener noreferrer"&gt;Product Hunt&lt;/a&gt; the next day.&lt;/p&gt;

&lt;p&gt;I still had some nice traffic to the site, so I've added a &lt;strong&gt;custom PH banner&lt;/strong&gt; to the top of the site to let people know about the launch. I also posted about it on Twitter and LinkedIn.&lt;/p&gt;

&lt;p&gt;With the help of the community, we were able to get &lt;strong&gt;3rd place&lt;/strong&gt; on the day of the launch with over 500 upvotes. This was one of the most successful PH launches I've ever had.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Hacker News
&lt;/h3&gt;

&lt;p&gt;Knowing that the site was getting some attention, I decided to post it on &lt;a href="https://news.ycombinator.com/item?id=39639386" rel="noopener noreferrer"&gt;Hacker News&lt;/a&gt; as well.&lt;/p&gt;

&lt;p&gt;I had a feeling that it would do well on HN, as I thought it would be a good fit for the community, but I didn't expect it to get this good.&lt;/p&gt;

&lt;p&gt;The post immediately got featured on to the front page and quickly climbed to claim the &lt;strong&gt;#1 spot on the site&lt;/strong&gt;. It got over 150 upvotes and stayed on the front page for a few hours.&lt;/p&gt;

&lt;p&gt;Being featured on Product Hunt is great, but #1 on Hacker News is a whole different level. The traffic was insane. I was getting thousands of visitors every hour.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Reddit
&lt;/h3&gt;

&lt;p&gt;In the same time, I was also posting about it on Reddit. I've posted it on a few subreddits, but the most successful one was on &lt;a href="https://www.reddit.com/r/selfhosted/comments/1b9kfwn/a_collection_of_selfhostable_opensource_software/" rel="noopener noreferrer"&gt;r/SelfHosted&lt;/a&gt;. It quicky got over &lt;strong&gt;250 upvotes&lt;/strong&gt; and a lot of comments.&lt;/p&gt;

&lt;p&gt;Unfortunately, I made a mistake by trying to monetize some of the traffic by adding a Stripe Pay link to the site. For $97, a project could be featured on the top of the list. I thought it would be a good way to monetize the site, but it was a mistake. I got a lot of negative feedback and I quickly removed it.&lt;/p&gt;

&lt;p&gt;Due to that, reddit users quickly turned on me and the site. I was getting a lot of negative comments and downvotes, and the post was &lt;strong&gt;removed from the subreddit&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Still, the traffic was great and I was able to get a lot of feedback from the community.&lt;/p&gt;

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

&lt;p&gt;The site was a huge success. It got over &lt;strong&gt;100,000 visitors in the first week&lt;/strong&gt;, and it's still getting a few thousand visitors every day. It was a great experiment in launching a side project and growth, and I learned a lot from it.&lt;/p&gt;

&lt;p&gt;Hopefully, you'll be able to take something away from this story and apply it to your own projects. If you have any questions or feedback, feel free to reach out to me on &lt;a href="https://twitter.com/piotrkulpinski" rel="noopener noreferrer"&gt;Twitter&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you want to check out the site, you can find it at &lt;a href="https://openalternative.co" rel="noopener noreferrer"&gt;openalternative.co&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>learning</category>
      <category>writing</category>
      <category>startup</category>
    </item>
  </channel>
</rss>
