<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Alex Kates's Blog]]></title><description><![CDATA[👋 Hi, I'm Alex. With over 15 years of experience, I've specialized in application development, with a primary focus on fintech startups. My expertise lies in React, TypeScript, and AWS.]]></description><link>https://blog.alexkates.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1643573702114/qwtuhUsMl.png</url><title>Alex Kates&apos;s Blog</title><link>https://blog.alexkates.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Tue, 12 May 2026 06:45:34 GMT</lastBuildDate><atom:link href="https://blog.alexkates.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Introducing DynamoDB Extended - Query History, Favorites, and Editor Defaults]]></title><description><![CDATA[DynamoDB Extended is a Chrome extension that adds quality-of-life improvements to the AWS DynamoDB web console, including persistent query history, auto-unmarshalled JSON, and query replay.
Every query you run is automatically saved, so you don't hav...]]></description><link>https://blog.alexkates.dev/introducing-dynamodb-extended-query-history-favorites-and-editor-defaults</link><guid isPermaLink="true">https://blog.alexkates.dev/introducing-dynamodb-extended-query-history-favorites-and-editor-defaults</guid><category><![CDATA[AWS]]></category><category><![CDATA[DynamoDB]]></category><category><![CDATA[chrome extension]]></category><dc:creator><![CDATA[Alex Kates]]></dc:creator><pubDate>Mon, 09 Jun 2025 10:56:59 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1749465586757/d9c0a2f5-b1c1-4d1c-ab4d-c14d754d0d86.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><a target="_blank" href="https://chromewebstore.google.com/detail/dynamodb-extended-query-h/chdahhohgeddblidnmphgndkcbofpbaa?authuser=1&amp;hl=en">DynamoDB Extended</a> is a Chrome extension that adds quality-of-life improvements to the AWS DynamoDB web console, including persistent query history, auto-unmarshalled JSON, and query replay.</p>
<p>Every query you run is automatically saved, so you don't have to rebuild from memory or search through browser history. You can rename and favorite queries for quick access, and replay them with all parameters preserved, including indexes, keys, attributes, and filters.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1749430763696/6b0d7520-f0c9-48b3-af05-cb6b8b3e0985.png" alt="DynamoDB Extended with side panel open and the AWS DynamoDB console with a query executed." class="image--center mx-auto" /></p>
<p>This first release includes the following MVP features. If you have an idea for another feature, please let me know!</p>
<ul>
<li><p>Auto-saves query history as you run them</p>
</li>
<li><p>Favorites &amp; rename support to keep things organized</p>
</li>
<li><p>Replay queries with indexes, keys, attributes, and filters included</p>
</li>
<li><p>Editor defaults like auto-unmarshalling JSON and height adjustment</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1749430645540/8f55a24a-9525-4512-bc6e-57e656271981.png" alt="DynamoDB Extended with the side panel opened showing the favorites tab activated and the settings page open." class="image--center mx-auto" /></p>
<p>Install <a target="_blank" href="https://chromewebstore.google.com/detail/dynamodb-extended-query-h/chdahhohgeddblidnmphgndkcbofpbaa?authuser=1&amp;hl=en">DynamoDB Extended</a> from the Chrome Web Store and open the side panel to get started. Your queries will begin logging automatically.</p>
<p>Source code available on <a target="_blank" href="https://github.com/alexkates/dynamodb-extended">GitHub</a>. Feel free to open feature requests!</p>
<p>— Alex</p>
]]></content:encoded></item><item><title><![CDATA[Let's Talk About Rejection]]></title><description><![CDATA[On March 4th, 2025, I woke up thinking it was just another Tuesday morning. Like many people, I started my day by making a cup of coffee and scrolling through social media. But this day was different because it was the AWS Community Builder acceptanc...]]></description><link>https://blog.alexkates.dev/lets-talk-about-rejection</link><guid isPermaLink="true">https://blog.alexkates.dev/lets-talk-about-rejection</guid><category><![CDATA[AWS]]></category><category><![CDATA[stoicism]]></category><dc:creator><![CDATA[Alex Kates]]></dc:creator><pubDate>Thu, 06 Mar 2025 15:38:52 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1741283969841/05dcc7ab-4e20-4257-a97d-58a3b6607a49.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>On March 4th, 2025, I woke up thinking it was just another Tuesday morning. Like many people, I started my day by making a cup of coffee and scrolling through social media. But this day was different because it was the AWS Community Builder acceptance day, a big deal for many in the tech world. As I scrolled through my feed, I saw tons of excited posts from people celebrating their acceptance into the program. There were newcomers thrilled to join for the first time and returning builders eager to continue their journey. Even AWS Heroes, who are highly respected in the community, were joining in to congratulate everyone. The excitement and sense of community were really strong. With my own anticipation growing, I checked my email, and there it was…</p>
<blockquote>
<p>Unfortunately, our team did not select you to join the program this cycle. Evaluations were primarily based on the links to the content you submitted for consideration — and alignment to your chosen topic area — along with your AWS journey, background, and other information in the application.</p>
</blockquote>
<p>It felt like a punch in the gut. I've been part of the AWS Community Builder's program since 2020. I've written <a target="_blank" href="https://www.alexkates.dev/blog?sort=date&amp;tags=aws">over 10 tutorials on getting started with AWS Serverless</a> that have more than 12,000 views. I maintain <a target="_blank" href="https://github.com/alexkates">over 10 GitHub repositories</a> that have helped new AWS developers learn common Serverless patterns. I'm a Director of Engineering at a startup that builds exclusively with AWS Serverless technology. <a target="_blank" href="https://aws.amazon.com/blogs/mobile/fintech-startup-creditgenie-ultimate-speed-from-mvp-to-growth/">I was even interviewed by AWS</a> and am still featured in the <a target="_blank" href="https://aws.amazon.com/blogs/mobile/fintech-startup-creditgenie-ultimate-speed-from-mvp-to-growth/">AWS Amplify customer section</a>. As I sat there staring at the rejection email, I couldn’t help but think, “What went wrong?”</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741268220140/8ede6679-8c4e-4a6a-a516-78a037a1e5f9.png" alt="Alex Kates Serverless Builder since 2020" class="image--center mx-auto" /></p>
<p>I'm not sharing this experiences to brag about accomplishments or complain about the selection process, but to start a conversation about normalizing rejection. Rejection is tough. It can feel discouraging, but it's something we all experience, both personally and professionally. By discussing it openly, my goal is to remove the stigma. Rejection doesn't define your abilities or worth. It should be seen as an opportunity to grow. It's a chance to reflect on ourselves and consider how we can improve.</p>
<p>So, what does this moment mean for me? How can I turn this rejection—<a target="_blank" href="https://www.alexkates.dev/blog/turning-obstacles-into-opportunities">this obstacle—into an opportunity</a>? I haven't written any new AWS content in over a year, and during that time, our industry has changed a lot. AI has become a major focus, and I haven't shared any opinions, guides, or insights on it. I use AI and AWS Serverless technology every day, both at work and in side projects. I've been building a lot, but writing and sharing very little. Community building is all about just that—building community! So that’s my commitment for the next year. I plan to write and share more about my experiences with AWS and Serverless. These hands have some dirt on them, and I want to show it.</p>
<p>Rejection sucks, especially in our field, but it's part of the journey. In fact, one could argue that achievements aren’t meaningful without rejection. Here are a few ways we can turn these moments into growth opportunities:</p>
<ol>
<li><p><strong>Reflect and Learn</strong>: Take a moment to understand what happened. Was there feedback? Use it. Every piece of criticism is a chance to improve.</p>
</li>
<li><p><strong>Stay Connected</strong>: Lean on your community. Share your experiences; you'll find you're not alone. Engaging with others can offer new perspectives and support.</p>
</li>
<li><p><strong>Keep Building</strong>: Don't let rejection stop your progress. Dive into new projects, contribute to open-source, or write about your experiences. Continuous learning and sharing keep you moving forward.</p>
</li>
<li><p><strong>Take Care of Yourself</strong>: It's okay to feel down. Acknowledge it, but don't dwell on it. Engage in activities you enjoy, and remember to balance work with rest.</p>
</li>
</ol>
<p>Remember, rejection doesn't define us. It's just a detour, not a dead-end. Keep pushing, keep coding, and keep sharing. I know I plan to. To all the Community Builders, congratulations! I'll see you all next year.</p>
]]></content:encoded></item><item><title><![CDATA[Introducing CongressGPT]]></title><description><![CDATA[TL;DR
For the Hashnode AI For Tomorrow Hackathon, I launched CongressGPT to help us finally understand what Congress is actually doing.
CongressGPT lets users have natural language conversations with the latest bills from the United States House of R...]]></description><link>https://blog.alexkates.dev/introducing-congressgpt</link><guid isPermaLink="true">https://blog.alexkates.dev/introducing-congressgpt</guid><category><![CDATA[AIForTomorrow]]></category><category><![CDATA[Hashnode]]></category><category><![CDATA[Next.js]]></category><dc:creator><![CDATA[Alex Kates]]></dc:creator><pubDate>Sun, 28 Jul 2024 21:27:32 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1722191677831/3abaafb7-bee1-4816-9d85-d61ad09cbf9c.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-tldr">TL;DR</h2>
<p>For the <a target="_blank" href="https://hashnode.com/hackathons/ai-for-tomorrow">Hashnode AI For Tomorrow Hackathon</a>, I launched <a target="_blank" href="https://congressgpt.app">CongressGPT</a> to help us finally understand what Congress is actually doing.</p>
<p><a target="_blank" href="https://congressgpt.app">CongressGPT</a> lets users have natural language conversations with the latest bills from the United States House of Representatives and Senate. It uses Vercel's Cron and Supabase's RAG capabilities to continuously update a vector database with new bills.</p>
<p>Keeping up with what Congress is doing is hard. The language used in the bills can be challenging to follow. CongressGPT makes it easy to ask questions and understand congressional bills.</p>
<p>It's more important than ever to stay informed about what our representatives are doing, and I hope CongressGPT helps you do just that.</p>
<h2 id="heading-the-tech">The Tech</h2>
<h3 id="heading-nextjs-14">Next.js 14</h3>
<p>This project uses several new features from Next.js and React, including the <a target="_blank" href="https://nextjs.org/docs/app">app router</a>, <a target="_blank" href="https://nextjs.org/docs/app/building-your-application/optimizing/metadata#dynamic-metadata">dynamic metadata</a>, <a target="_blank" href="https://nextjs.org/docs/app/building-your-application/rendering/server-components">server components</a>, and <a target="_blank" href="https://nextjs.org/docs/app/building-your-application/routing/loading-ui-and-streaming">suspense</a>.</p>
<h3 id="heading-supabase">Supabase</h3>
<p>Supabase is powering a majority of CongressGPT features including authentication, chat and vector storage and similarity searching. I've been using Supabase for several projects and really enjoying it.</p>
<h3 id="heading-govinfo-api">Govinfo API</h3>
<p>The <a target="_blank" href="https://www.govinfo.gov/">Govinfo API</a> provides an RSS feed that is updated daily with the latest bills introduced in the U.S. House of Representatives and Senate.</p>
<h3 id="heading-vercel">Vercel</h3>
<p>Vercel hosts the Next.js application and runs a nightly <a target="_blank" href="https://vercel.com/docs/cron-jobs">cron job</a> to pull the latest Govinfo RSS entries, converting any new bills into a vector database.</p>
<h3 id="heading-shadcnui">Shadcn/ui</h3>
<p>The entire website is built using <a target="_blank" href="https://ui.shadcn.com/">shadcn/ui</a> and <a target="_blank" href="https://tailwindcss.com/">TailwindCSS</a>.</p>
]]></content:encoded></item><item><title><![CDATA[How I Built a Guestbook Page using Supabase and Next.js]]></title><description><![CDATA[I've come across a fun website feature that has been appearing mainly on developers' personal sites: a Guestbook page where users can sign in and leave a message. This somewhat reminds me of the old Facebook wall from the 2000s which I admit I miss v...]]></description><link>https://blog.alexkates.dev/how-i-built-a-guestbook-page-using-supabase-and-nextjs</link><guid isPermaLink="true">https://blog.alexkates.dev/how-i-built-a-guestbook-page-using-supabase-and-nextjs</guid><category><![CDATA[supabase]]></category><category><![CDATA[Next.js]]></category><category><![CDATA[TypeScript]]></category><dc:creator><![CDATA[Alex Kates]]></dc:creator><pubDate>Thu, 22 Feb 2024 11:58:36 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1708602930828/b5dc4d13-ae1c-49f2-a751-db97e17660f7.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I've come across a fun website feature that has been appearing mainly on developers' personal sites: a Guestbook page where users can sign in and leave a message. This somewhat reminds me of the old Facebook wall from the 2000s which I admit I miss very much (remember <a target="_blank" href="https://www.facebook.com/help/219967728031249">pokes</a>?). In this post, I'd like to discuss how I implemented a Guestbook page on my personal site <a target="_blank" href="https://alexkates.dev/guestbook">https://alexkates.dev/guestbook</a> using Supabase and Next.js.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">You can find the complete source code for the Guestbook feature in my personal site's <a target="_blank" href="https://github.com/alexkates/alexkates.dev/blob/main/src/app/guestbook/page.tsx">GitHub repository</a></div>
</div>

<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1708430303216/2d9ad1af-9c09-4e76-ac2a-83d2f1f06126.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-supabase">Supabase</h2>
<p>I've been a fan of Supabase for about a year now. This is my second project, the first being an open-source journaling app, <a target="_blank" href="https://supajournal.app">Supajournal</a>. This guestbook feature requires users to sign in and leave messages, making Supabase the perfect choice. In this section, we'll walk through setting up GitHub OAuth, a table with proper Row-Level Security (RLS), and generating TypeScript types.</p>
<h3 id="heading-github-oauth">GitHub OAuth</h3>
<p>Supabase makes setting up OAuth quite simple, offering a variety of provider options. As I primarily write tech content, I decided to implement the guestbook feature using GitHub OAuth.</p>
<p>This setup will require you to have both your GitHub OAuth app and Supabase authentication settings open simultaneously. Essentially, we need to connect these two systems by configuring each within the other. Don't worry, we'll guide you through this process step by step.</p>
<p>First, navigate to <a target="_blank" href="https://database.new">https://database.new</a> and create a new Supabase project. In my case, I named it <code>alexkates-dev</code>. Allow a few seconds for the project setup to complete.</p>
<p>Next, go to GitHub and log in. Click your avatar in the top-right corner, then click <code>Settings -&gt; Developer Settings -&gt; OAuth Apps -&gt; New OAuth App</code>. Keep this tab open, as we'll need it shortly.</p>
<p>Now, return to Supabase and navigate to your new project's dashboard. From the left-hand menu, click <code>Authentication -&gt; Providers -&gt; GitHub</code>. Excellent! Keep this tab open as well. The upcoming steps will involve switching between Supabase and GitHub to set up both sides of the OAuth exchange.</p>
<p>Copy the Callback URL (for OAuth) value from Supabase. It should resemble something like <code>https://yvcsamdilcryjsvbsct.supabase.co/auth/v1/callback.</code> Return to GitHub and paste this value into the Authorization callback URL field. Additionally, while you're here, enter your Application Name and Homepage URL.</p>
<p>Now, click <code>Generate a New Client Secret</code>. Take note of your Client ID and Client Secret, then return to Supabase. Enter those values into the GitHub provider configuration located above the Callback URL from earlier, and click save.</p>
<p>The final step in Supabase is to navigate to URL Configuration and add a couple of Redirect URLs. I added two: <code>http://localhost:3000/auth/callback</code> and <code>https://alexkates.dev/auth/callback</code>. You should definitely add the localhost, but your production callback will vary depending on your domain.</p>
<p>Excellent! By now, your GitHub and Supabase Authentication systems should be set up and prepared for coding later on.</p>
<h3 id="heading-guestbook-table">Guestbook Table</h3>
<p>Once a user can sign in, they will need the ability to leave a message. I kept this part relatively simple by creating a single table called <code>Guestbook</code> with columns for the id, created_at, message, user_id, username, and avatar.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1708372091836/53dc3901-c931-4f88-8026-50fe7f06de88.png" alt class="image--center mx-auto" /></p>
<p>Next, we need to configure Row Level Security, or RLS. This is an impressive feature in Supabase that enables you to implement access control policies directly in your database. Head back to <code>Authentication -&gt; Policies</code> where we will create two policies.</p>
<p>For a Guestbook feature, we have two access patterns. First, anonymous users need to be able to read messages left by others. Click <code>New Policy -&gt; Get started quickly</code>, and we'll use the template called "Enable read access to everyone."</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1708372699742/9858ea06-c25e-4921-981a-041b932d3591.png" alt class="image--center mx-auto" /></p>
<p>Next, an authenticated user needs to be able to leave a message. Once again, we'll use a pre-built template for this. Click <code>New Policy -&gt; Get started quickly</code>, and this time, select the template called "Enable insert access for authenticated users only".</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1708372850791/4d6f7238-7ed9-4efc-aa6a-be4aff79cb6f.png" alt class="image--center mx-auto" /></p>
<p>Excellent! By now, you should have Supabase authentication set up and a table called Guestbook with RLS configured for public read access and authenticated write access.</p>
<h2 id="heading-nextjs">Next.js</h2>
<p>I've been increasingly using Next.js 14, React Server Components, and Server Actions, and this Guestbook feature presented another perfect opportunity to use these newer features.</p>
<p>In this section, we'll walk through all the code changes you'll need to make to allow a user to view previous Guestbook messages, sign in with GitHub, and leave a new message.</p>
<p>I'm going to assume you already have a Next.js app. If you haven't created one yet, it's as simple as running <code>pnpm dlx create-next-app@latest</code>.</p>
<h3 id="heading-project-setup">Project Setup</h3>
<p>There are several initial setup steps required to make our Next.js app and Supabase work together. Let's begin with our .env.local file. We will need four environment variables to enable the Guestbook feature. These variables can be found in various locations within the Supabase dashboard.</p>
<pre><code class="lang-bash">NEXT_PUBLIC_SIGNIN_REDIRECT_URL=<span class="hljs-string">"http://localhost:3000/auth/callback"</span>
NEXT_PUBLIC_SUPABASE_ANON_KEY=<span class="hljs-string">"eyJhbGci1iKIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl2ZHNhbWRpbGNyeWpoc3Zic2N0Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3MDgyNzkwMzIsImV4cCI6MjAyMzg1NTAzMn0.vHUHdPZ-npFD9zhnUIexdTw9rlYByDQQa1HW5RsKtZM"</span>
NEXT_PUBLIC_SUPABASE_URL=<span class="hljs-string">"https://yvdsamdilcryjhsvbsct.supabase.co"</span>
SUPABASE_ACCESS_TOKEN=<span class="hljs-string">"sbp_c83522caea12924355eb1270e4521850ac4d307a"</span>
</code></pre>
<p><strong>NEXT_PUBLIC_SIGNIN_REDIRECT_URL</strong> - We'll use this environment variable within our "Login with GitHub" call to Supabase, instructing GitHub where to redirect after successful authentication.</p>
<p><strong>NEXT_PUBLIC_SUPABASE_ANON_KEY</strong> and <strong>NEXT_PUBLIC_SUPABASE_URL</strong> - these two values can be found in the Supabase dashboard. Note that they are marked as NEXT_PUBLIC and are perfectly safe to be exposed to client components.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1708425835094/10fae35d-be0c-4876-8ce6-8ea3db0f4bf4.png" alt class="image--center mx-auto" /></p>
<p><strong>SUPABASE_ACCESS_TOKEN</strong> - Unlike the ANON key, this access token is a secret and should not be exposed to client components. In fact, we will only use it as part of our build scripts to generate TypeScript files in the next step. To create one, go back to your Supabase project's main dashboard and, in the left navigation, click <code>Account -&gt; Access Tokens</code>. Don't worry, I've already rotated my secret, so the one shared above is invalid.</p>
<h3 id="heading-typescript-generation">TypeScript Generation</h3>
<p>In this section, we'll discuss generating TypeScript files based on your Supabase schema. This is beneficial because it ensures your code always reflects the latest state of your database design, while allowing you to build these artifacts and add them to your .gitignore file.</p>
<p>There are three new package.json scripts we need to add, specifically <code>predev</code>, <code>prebuild</code>, and <code>supabase-codegen</code>.</p>
<pre><code class="lang-bash"><span class="hljs-string">"scripts"</span>: {
    <span class="hljs-string">"predev"</span>: <span class="hljs-string">"pnpm install &amp;&amp; pnpm supabase-codegen"</span>,
    <span class="hljs-string">"dev"</span>: <span class="hljs-string">"next dev"</span>,
    <span class="hljs-string">"prebuild"</span>: <span class="hljs-string">"pnpm supabase-codegen"</span>,
    <span class="hljs-string">"build"</span>: <span class="hljs-string">"next build"</span>,
    <span class="hljs-string">"start"</span>: <span class="hljs-string">"next start"</span>,
    <span class="hljs-string">"lint"</span>: <span class="hljs-string">"next lint"</span>,
    <span class="hljs-string">"supabase-codegen"</span>: <span class="hljs-string">"supabase gen types typescript --project-id yvdsamdilcryjhsvbsct --schema public &gt; src/supabase/types.ts"</span>
  },
</code></pre>
<p><strong>predev</strong> - This script runs before each <code>pnpm dev</code> command, ensuring that our packages and generated TypeScript are always up to date before starting the development server.</p>
<p><strong>prebuild</strong> - Functions similarly to <code>predev</code>, but it ensures that the most recent TypeScript files are generated during the build process on Vercel.</p>
<p><strong>supabase-codegen</strong> - This is where the magic happens. This command instructs Supabase to generate TypeScript types based on a specific project-id. You can find this project id in the Supabase dashboard for your project. In the left navigation, click <code>Project Settings -&gt; General.</code></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1708427013913/a365d585-1b46-4d46-b268-f1e4a6ef77fd.png" alt class="image--center mx-auto" /></p>
<p>Great! Our Next.js environment variables are now set up. Next, we'll dive into some coding.</p>
<h3 id="heading-supabase-init">Supabase init</h3>
<p>We need to run a one-time command to initialize Supabase. This command simply connects the Next.js app to the Supabase instance. Just run <code>supabase init</code> from the root of your Next.js app and follow the prompts. For more information, you can find the Supabase init documentation <a target="_blank" href="https://supabase.com/docs/reference/cli/supabase-init">here</a>.</p>
<p>This command will create a supabase directory at the root of your project. Personally, I prefer to have it inside the <code>src</code> directory, so I moved it. However, feel free to leave it where it is if you prefer. Once finished, your Supabase folder should resemble the following.</p>
<p>The final step is to add a line to the <code>.gitignore</code> file in the <code>supabase</code> directory. Include <code>types.ts</code>, as this is where we will store all of our generated types. In fact, you can now run the script from earlier, <code>pnpm supabase-codegen</code>, and your types.ts file will be created in your supabase directory.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1708598191457/aa1966bb-2882-4d33-8c50-1a429f7430af.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-supabase-client-component">Supabase Client Component</h3>
<p>We'll be interacting with Supabase from both server and client components. The <a target="_blank" href="https://www.npmjs.com/package/@supabase/ssr">Supabase SSR library</a> needs to be configured slightly differently depending on where we are using it. In this section, we'll create a couple of client factories to make this process easier moving forward.</p>
<p>Starting with the <code>createBrowserClient</code> factory which is a bit more straightforward as there are no cookies to manage.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1708598551656/ef6367f6-b4c0-4660-a74a-4673e313f846.png" alt class="image--center mx-auto" /></p>
<p>This code ensures that we always obtain a version of the Supabase client compatible with client components. It utilizes the two NEXT_PUBLIC environment variables we set earlier: the public URL and the anonymous key. The client is also typed to the <code>Database</code> type generated from our codegen, providing autocomplete functionality when attempting to query later.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { createBrowserClient } <span class="hljs-keyword">from</span> <span class="hljs-string">"@supabase/ssr"</span>;
<span class="hljs-keyword">import</span> { Database } <span class="hljs-keyword">from</span> <span class="hljs-string">"./types"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">createClient</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> { NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY } = process.env;

  <span class="hljs-keyword">return</span> createBrowserClient&lt;Database&gt;(NEXT_PUBLIC_SUPABASE_URL!, NEXT_PUBLIC_SUPABASE_ANON_KEY!);
}
</code></pre>
<h2 id="heading-supabase-server-component">Supabase Server Component</h2>
<p>Our server client, which will be used in React Server Components, Server Actions, and middleware, is slightly more complex because it needs to manage the user's cookie state.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1708598933987/400e8bd0-657b-44fd-95c3-cf0c64f3fd60.png" alt class="image--center mx-auto" /></p>
<p>Similar to the client component, we are using the <code>createServerClient</code> factory function from <code>@supabase/ssr</code>. We also require the same environment variables as before. Finally, we need to configure "cookie methods." These methods determine how to get, set, and remove a cookie from the next/headers cookie store.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { createServerClient, <span class="hljs-keyword">type</span> CookieOptions } <span class="hljs-keyword">from</span> <span class="hljs-string">"@supabase/ssr"</span>;
<span class="hljs-keyword">import</span> { <span class="hljs-keyword">type</span> cookies } <span class="hljs-keyword">from</span> <span class="hljs-string">"next/headers"</span>;
<span class="hljs-keyword">import</span> { Database } <span class="hljs-keyword">from</span> <span class="hljs-string">"./types"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">createClient</span>(<span class="hljs-params">cookieStore: ReturnType&lt;<span class="hljs-keyword">typeof</span> cookies&gt;</span>) </span>{
  <span class="hljs-keyword">const</span> { NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY } = process.env;

  <span class="hljs-keyword">return</span> createServerClient&lt;Database&gt;(NEXT_PUBLIC_SUPABASE_URL!, NEXT_PUBLIC_SUPABASE_ANON_KEY!, {
    cookies: {
      get(name: <span class="hljs-built_in">string</span>) {
        <span class="hljs-keyword">return</span> cookieStore.get(name)?.value;
      },
      set(name: <span class="hljs-built_in">string</span>, value: <span class="hljs-built_in">string</span>, options: CookieOptions) {
        <span class="hljs-keyword">try</span> {
          cookieStore.set({ name, value, ...options });
        } <span class="hljs-keyword">catch</span> (error) {}
      },
      remove(name: <span class="hljs-built_in">string</span>, options: CookieOptions) {
        <span class="hljs-keyword">try</span> {
          cookieStore.set({ name, value: <span class="hljs-string">""</span>, ...options });
        } <span class="hljs-keyword">catch</span> (error) {}
      },
    },
  });
}
</code></pre>
<h3 id="heading-middleware">Middleware</h3>
<p>Next, we need to add a <code>middleware.ts</code> file to the root of the Next.js project (or the <code>src</code> directory if you're using that, like me). This middleware performs several important tasks, but its primary responsibility is to intercept every request and ensure that the Supabase user cookie is up-to-date in the cookie store. It also calls <code>supabase.auth.getUser()</code>, which refreshes the user's access token if it has become stale. Finally, it includes a matcher that only applies when the route is <code>/guestbook</code>, as most of my site is public.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { createServerClient, <span class="hljs-keyword">type</span> CookieOptions } <span class="hljs-keyword">from</span> <span class="hljs-string">"@supabase/ssr"</span>;
<span class="hljs-keyword">import</span> { NextResponse, <span class="hljs-keyword">type</span> NextRequest } <span class="hljs-keyword">from</span> <span class="hljs-string">"next/server"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">middleware</span>(<span class="hljs-params">request: NextRequest</span>) </span>{
  <span class="hljs-keyword">const</span> { NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY } = process.env;

  <span class="hljs-keyword">let</span> response = NextResponse.next({
    request: {
      headers: request.headers,
    },
  });

  <span class="hljs-keyword">const</span> supabase = createServerClient(NEXT_PUBLIC_SUPABASE_URL!, NEXT_PUBLIC_SUPABASE_ANON_KEY!, {
    cookies: {
      get(name: <span class="hljs-built_in">string</span>) {
        <span class="hljs-keyword">return</span> request.cookies.get(name)?.value;
      },
      set(name: <span class="hljs-built_in">string</span>, value: <span class="hljs-built_in">string</span>, options: CookieOptions) {
        request.cookies.set({
          name,
          value,
          ...options,
        });
        response = NextResponse.next({
          request: {
            headers: request.headers,
          },
        });
        response.cookies.set({
          name,
          value,
          ...options,
        });
      },
      remove(name: <span class="hljs-built_in">string</span>, options: CookieOptions) {
        request.cookies.set({
          name,
          value: <span class="hljs-string">""</span>,
          ...options,
        });
        response = NextResponse.next({
          request: {
            headers: request.headers,
          },
        });
        response.cookies.set({
          name,
          value: <span class="hljs-string">""</span>,
          ...options,
        });
      },
    },
  });

  <span class="hljs-keyword">await</span> supabase.auth.getUser();

  <span class="hljs-keyword">return</span> response;
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> config = {
  matcher: [<span class="hljs-string">"/guestbook"</span>],
};
</code></pre>
<h3 id="heading-appguestbookpagetsx">app/guestbook/page.tsx</h3>
<p>Let's start building the Guestbook page and components! First, we'll create the Next.js page.tsx file that serves as our Guestbook page. This React Server Component fetches the current authenticated user from Supabase and conditionally renders a page based on whether a user is signed in or not. This page also displays the <code>&lt;GuestbookList /&gt;</code> component and uses <code>&lt;Suspense /&gt;</code> to stream the list of guestbook entries to the client while the rest of the page loads instantly. If the user is not signed in yet, this page will display a client component called <code>&lt;SignInWithGithub /&gt;</code> that controls the OAuth flow with GitHub for us. Finally, if a user is signed in, this page will display a form, <code>&lt;GuestbookForm /&gt;</code>, for the user to submit their message.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> GuestbookForm <span class="hljs-keyword">from</span> <span class="hljs-string">"@/components/guestbook-form"</span>;
<span class="hljs-keyword">import</span> GuestbookList <span class="hljs-keyword">from</span> <span class="hljs-string">"@/components/guestbook-list"</span>;
<span class="hljs-keyword">import</span> ParagraphSkeleton <span class="hljs-keyword">from</span> <span class="hljs-string">"@/components/paragraph-skeleton"</span>;
<span class="hljs-keyword">import</span> SignInWithGitHub <span class="hljs-keyword">from</span> <span class="hljs-string">"@/components/sign-in-with-github"</span>;
<span class="hljs-keyword">import</span> SignOut <span class="hljs-keyword">from</span> <span class="hljs-string">"@/components/sign-out"</span>;
<span class="hljs-keyword">import</span> { cn, fadeIn } <span class="hljs-keyword">from</span> <span class="hljs-string">"@/lib/utils"</span>;
<span class="hljs-keyword">import</span> { createClient } <span class="hljs-keyword">from</span> <span class="hljs-string">"@/supabase/server"</span>;
<span class="hljs-keyword">import</span> { cookies } <span class="hljs-keyword">from</span> <span class="hljs-string">"next/headers"</span>;
<span class="hljs-keyword">import</span> { Suspense } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Page</span>(<span class="hljs-params">{
  searchParams,
}: {
  searchParams?: {
    submitted?: <span class="hljs-built_in">boolean</span>;
  };
}</span>) </span>{
  <span class="hljs-keyword">const</span> supabaseClient = createClient(cookies());
  <span class="hljs-keyword">const</span> {
    data: { user },
  } = <span class="hljs-keyword">await</span> supabaseClient.auth.getUser();

  <span class="hljs-keyword">if</span> (!user)
    <span class="hljs-keyword">return</span> (
      &lt;main className=<span class="hljs-string">"flex flex-col gap-8"</span>&gt;
        &lt;section className={cn(fadeIn, <span class="hljs-string">"animation-delay-200 flex flex-col gap-2"</span>)}&gt;
          Welcome to my guestbook!
          &lt;div&gt;
            &lt;SignInWithGitHub /&gt;
          &lt;/div&gt;
        &lt;/section&gt;
        &lt;section className={cn(fadeIn, <span class="hljs-string">"animation-delay-600"</span>)}&gt;
          &lt;Suspense fallback={&lt;ParagraphSkeleton /&gt;}&gt;
            &lt;GuestbookList /&gt;
          &lt;/Suspense&gt;
        &lt;/section&gt;
      &lt;/main&gt;
    );

  <span class="hljs-keyword">return</span> (
    &lt;main className=<span class="hljs-string">"flex flex-col gap-8"</span>&gt;
      &lt;section className={cn(fadeIn, <span class="hljs-string">"animation-delay-200"</span>)}&gt;
        &lt;div className=<span class="hljs-string">"flex items-center gap-2"</span>&gt;
          Hi, {user.user_metadata.user_name}!&lt;div className=<span class="hljs-string">"animate animate-wave animation-delay-1000"</span>&gt;👋&lt;/div&gt;
          &lt;SignOut /&gt;
        &lt;/div&gt;
        {searchParams?.submitted ? &lt;span&gt;Your message has been submitted! Thanks <span class="hljs-keyword">for</span> signing my guestbook.&lt;<span class="hljs-regexp">/span&gt; : &lt;GuestbookForm /</span>&gt;}
      &lt;/section&gt;
      &lt;section className={cn(fadeIn, <span class="hljs-string">"animation-delay-600"</span>)}&gt;
        &lt;Suspense fallback={&lt;ParagraphSkeleton /&gt;}&gt;
          &lt;GuestbookList /&gt;
        &lt;/Suspense&gt;
      &lt;/section&gt;
    &lt;/main&gt;
  );
}
</code></pre>
<p>Observe how we import the <code>createServer</code> factory method from earlier and pass the <code>cookies()</code> from <code>next/headers</code> to it. This enables Supabase to maintain the user's state across pages, layouts, RSCs, and server actions.</p>
<h3 id="heading-ltsigninwithgithub-gt">&lt;SignInWithGithub /&gt;</h3>
<p>Supabase makes the actual code for OAuth flows quite straightforward. Remember that we already configured the OAuth exchanges earlier using the GitHub OAuth app we created and the Supabase Authentication section of our project.</p>
<p>Now, all that remains is to add this client component that will handle signing in with GitHub for us! You'll also notice the <code>NEXT_PUBLIC_SIGNIN_REDIRECT_URL</code>, which is used to inform GitHub where to redirect within our app once the user has completed the OAuth flow.</p>
<p>Unlike other instances, this time we are using the <code>createClient</code> factory for our browser client. This is because the OAuth flow is primarily a client-side process and must be initiated from the browser rather than on the server.</p>
<pre><code class="lang-typescript"><span class="hljs-string">"use client"</span>;

<span class="hljs-keyword">import</span> { createClient } <span class="hljs-keyword">from</span> <span class="hljs-string">"@/supabase/client"</span>;
<span class="hljs-keyword">import</span> { GitHubLogoIcon } <span class="hljs-keyword">from</span> <span class="hljs-string">"@radix-ui/react-icons"</span>;
<span class="hljs-keyword">import</span> { Button } <span class="hljs-keyword">from</span> <span class="hljs-string">"./ui/button"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">SignInWithGitHub</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">signInWithGitHub</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> supabase = createClient();
    supabase.auth.signInWithOAuth({
      provider: <span class="hljs-string">"github"</span>,
      options: {
        redirectTo: process.env.NEXT_PUBLIC_SIGNIN_REDIRECT_URL,
      },
    });
  }

  <span class="hljs-keyword">return</span> (
    &lt;Button onClick={signInWithGitHub}&gt;
      &lt;GitHubLogoIcon className=<span class="hljs-string">"mr-2"</span> /&gt;
      Sign <span class="hljs-keyword">in</span> <span class="hljs-keyword">with</span> GitHub
    &lt;/Button&gt;
  );
}
</code></pre>
<h3 id="heading-ltguestbooklist-gt">&lt;GuestbookList /&gt;</h3>
<p>Next up is our GuestbookList component, which is also a server component. It's responsible for fetching the complete list of guestbook entries from our Supabase table and mapping each to the JSX that will be used to display the message. Once again, we use the Supabase server client, but this time we use it to actually run a SQL query, simply selecting all from the guestbook table.</p>
<p>After obtaining the guestbook entries, we sort them in descending order by <code>created_at</code> and map each to some JSX that resembles a text message component. If it wasn't already clear, I'm using shadcn/ui for the majority of the UI, which is also where the Avatar components come from.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { createClient } <span class="hljs-keyword">from</span> <span class="hljs-string">"@/supabase/server"</span>;
<span class="hljs-keyword">import</span> { cookies } <span class="hljs-keyword">from</span> <span class="hljs-string">"next/headers"</span>;
<span class="hljs-keyword">import</span> { Avatar, AvatarFallback, AvatarImage } <span class="hljs-keyword">from</span> <span class="hljs-string">"./ui/avatar"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">GuestbookList</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> supabaseClient = createClient(cookies());
  <span class="hljs-keyword">const</span> { data } = <span class="hljs-keyword">await</span> supabaseClient.from(<span class="hljs-string">"guestbook"</span>).select(<span class="hljs-string">"*"</span>);

  <span class="hljs-keyword">return</span> (
    &lt;ul className=<span class="hljs-string">"flex flex-col gap-6"</span>&gt;
      {data
        ?.sort(<span class="hljs-function">(<span class="hljs-params">a, b</span>) =&gt;</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(b.created_at).getTime() - <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(a.created_at).getTime())
        ?.map(<span class="hljs-function">(<span class="hljs-params">message</span>) =&gt;</span> (
          &lt;li key={message.id} className=<span class="hljs-string">"flex items-center gap-2"</span>&gt;
            &lt;Avatar&gt;
              &lt;AvatarImage src={message.avatar} alt={message.username} /&gt;
              &lt;AvatarFallback&gt;{message.username.substring(<span class="hljs-number">0</span>, <span class="hljs-number">2</span>)}&lt;/AvatarFallback&gt;
            &lt;/Avatar&gt;
            {message.username}: {message.message}
          &lt;/li&gt;
        ))}
    &lt;/ul&gt;
  );
}
</code></pre>
<h3 id="heading-ltguestbookform-gt">&lt;GuestbookForm /&gt;</h3>
<p>In my opinion, Next.js server actions have greatly simplified the process of submitting data to a server. There's no longer a need to create an API route and manage client-side state for submission. The GuestbookForm component consists of a form, text input, and a button to submit the form. The form submission action is a Server Action that writes directly to our guestbook table.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> submitGuestbookMessage <span class="hljs-keyword">from</span> <span class="hljs-string">"@/server/submit-guestbook-message"</span>;
<span class="hljs-keyword">import</span> SubmitGuestbookMessageButton <span class="hljs-keyword">from</span> <span class="hljs-string">"./submit-guestbook-message-button"</span>;
<span class="hljs-keyword">import</span> { Input } <span class="hljs-keyword">from</span> <span class="hljs-string">"./ui/input"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">GuestbookForm</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> (
    &lt;form action={submitGuestbookMessage} className=<span class="hljs-string">"flex flex-col gap-4"</span>&gt;
      &lt;div className=<span class="hljs-string">"flex gap-2 items-center"</span>&gt;
        &lt;Input className=<span class="hljs-string">"max-w-sm"</span> id=<span class="hljs-string">"message"</span> name=<span class="hljs-string">"message"</span> required placeholder=<span class="hljs-string">"Press enter to submit."</span> /&gt;
        &lt;SubmitGuestbookMessageButton /&gt;
      &lt;/div&gt;
    &lt;/form&gt;
  );
}
</code></pre>
<p>There are some interesting features within the <code>&lt;SubmitGuestbookMessageButton /&gt;</code> component. This client component utilizes the new <code>useFormStatus</code> hook from the latest <code>react-dom</code> release. This enables us to conditionally render content based on whether the form is pending submission or not.</p>
<pre><code class="lang-typescript"><span class="hljs-string">"use client"</span>;

<span class="hljs-keyword">import</span> { PaperPlaneIcon, ReloadIcon } <span class="hljs-keyword">from</span> <span class="hljs-string">"@radix-ui/react-icons"</span>;
<span class="hljs-keyword">import</span> { useFormStatus } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-dom"</span>;
<span class="hljs-keyword">import</span> { Button } <span class="hljs-keyword">from</span> <span class="hljs-string">"./ui/button"</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">SubmitGuestbookMessageButton</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> { pending } = useFormStatus();

  <span class="hljs-keyword">return</span> (
    &lt;Button variant=<span class="hljs-string">"default"</span> disabled={pending} size=<span class="hljs-string">"icon"</span>&gt;
      {!pending ? &lt;PaperPlaneIcon className=<span class="hljs-string">"pl-1 h-5 w-5"</span> /&gt; : &lt;ReloadIcon className=<span class="hljs-string">"pl-1 h-5 w-5 animate-spin"</span> /&gt;}
    &lt;/Button&gt;
  );
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> SubmitGuestbookMessageButton;
</code></pre>
<h3 id="heading-ltsignout-gt">&lt;SignOut /&gt;</h3>
<p>Finally, the <code>&lt;SignOut /&gt;</code> component enables users to sign out of our app if they wish. This client component utilizes the Supabase browser client to invoke the <code>auth.signOut()</code> function.</p>
<pre><code class="lang-typescript"><span class="hljs-string">"use client"</span>;

<span class="hljs-keyword">import</span> { createClient } <span class="hljs-keyword">from</span> <span class="hljs-string">"@/supabase/client"</span>;
<span class="hljs-keyword">import</span> { useRouter } <span class="hljs-keyword">from</span> <span class="hljs-string">"next/navigation"</span>;
<span class="hljs-keyword">import</span> { Button } <span class="hljs-keyword">from</span> <span class="hljs-string">"./ui/button"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">SignOut</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> router = useRouter();

  <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">signOut</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> supabase = createClient();
    <span class="hljs-keyword">await</span> supabase.auth.signOut();

    router.refresh();
  }

  <span class="hljs-keyword">return</span> (
    &lt;Button onClick={signOut} size=<span class="hljs-string">"sm"</span> variant=<span class="hljs-string">"link"</span> className=<span class="hljs-string">"p-0 text-muted-foreground"</span>&gt;
      Sign Out
    &lt;/Button&gt;
  );
}
</code></pre>
<h2 id="heading-outro"><strong>Outro</strong></h2>
<p>I truly enjoyed building the Guestbook page on my personal site using Next.js and Supabase. The new tools in Next.js 14, such as React Server Components, Server Actions, and the app router, combined with the power of Supabase auth and the <code>@supabase/ssr</code> library, made this a fun project to work through.</p>
<p>If you found this helpful, I have a couple other free to use tools over at <a target="_blank" href="http://alexkates.dev/projects">alexkates.dev/projects</a> and also consider connecting on <a target="_blank" href="https://twitter.com/thealexkates">Twitter</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Introducing Hashnode Next]]></title><description><![CDATA[TL;DR
For the Hashnode APIs Hackathon, I launched hashnode-next, a beautifully simple Hashnode starter kit powered by Next.js and shadcn/ui. Hashnode-next is the fastest way to go headless with Hashnode, enabling you to migrate your blog with just on...]]></description><link>https://blog.alexkates.dev/introducing-hashnode-next</link><guid isPermaLink="true">https://blog.alexkates.dev/introducing-hashnode-next</guid><category><![CDATA[APIHackathon]]></category><category><![CDATA[Hashnode]]></category><category><![CDATA[Next.js]]></category><dc:creator><![CDATA[Alex Kates]]></dc:creator><pubDate>Fri, 02 Feb 2024 10:50:09 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1706750008167/dfcffe29-fa6a-4fef-b009-6f1cb7bd11a7.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-tldr">TL;DR</h2>
<p>For the <a target="_blank" href="https://hashnode.com/hackathons/apihackathon">Hashnode APIs Hackathon</a>, I launched <a target="_blank" href="https://hashnode-next.dev">hashnode-next</a>, a beautifully simple Hashnode starter kit powered by <a target="_blank" href="https://nextjs.org/">Next.js</a> and <a target="_blank" href="https://ui.shadcn.com/">shadcn/ui</a>. Hashnode-next is the fastest way to go headless with Hashnode, enabling you to migrate your blog with just one click.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1706831178343/143ce827-8073-4a75-a4eb-fc2aed31359b.png" alt="Image of hashnode-next.dev landing page." class="image--center mx-auto" /></p>
<p>Use the <a target="_blank" href="https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Falexkates%2Fhashnode-next&amp;env=HASHNODE_HOST&amp;envLink=https%3A%2F%2Fapidocs.hashnode.com&amp;project-name=blog&amp;repository-name=blog&amp;demo-title=hashnode-next&amp;demo-description=The%20fastest%20way%20to%20go%20headless%20with%20Hashnode&amp;demo-url=https%3A%2F%2Fhashnode-next.dev%2Fblog&amp;demo-image=https%3A%2F%2Fhashnode-next.dev%2Fdemo.png">Vercel deploy button</a> to deploy your own copy directly to your Vercel account, or start locally using the command below.</p>
<pre><code class="lang-bash">npm create-next-app -e https://github.com/alexkates/hashnode-next
</code></pre>
<h2 id="heading-some-background">Some Background</h2>
<blockquote>
<p>"Either write something worth reading or do something worth writing."<br />- Benjamin Franklin</p>
</blockquote>
<p>I've been a writer and fan of Hashnode since January 2022, when I published my first article. Since then, I have published over 25 articles with more than 50,000 total views.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1706698580429/5ecb0e0d-2aea-483f-bc20-bb7bf9b8caa9.png" alt="Image of my first Hashnode article." class="image--center mx-auto" /></p>
<p>When <a target="_blank" href="https://hashnode.com/headless">Hashnode Headless</a> became generally available, I was really excited to migrate <a target="_blank" href="https://alexkates.dev/blog">my blog</a>. This was right around the time that Next 14 was launching, and I knew this would be a fun project.</p>
<p>Then Hashnode announced the <a target="_blank" href="https://hashnode.com/hackathons/apihackathon">Hashnode APIs Hackathon</a> and I knew it was the perfect time to open-source my blog template. I believed that creating a modern yet simple template that developers and bloggers could launch with just one click would be valuable.</p>
<h2 id="heading-getting-started">Getting Started</h2>
<p>Migrating your blog to headless with hashnode-next is as easy as <a target="_blank" href="https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Falexkates%2Fhashnode-next&amp;env=HASHNODE_API_KEY,HASHNODE_API_URL,HASHNODE_HOST&amp;envDescription=HASHNODE_API_URL%20is%20almost%20always%20https%3A%2F%2Fgql.hashnode.com.&amp;envLink=https%3A%2F%2Fapidocs.hashnode.com&amp;project-name=blog&amp;repository-name=blog&amp;demo-title=hashnode-next&amp;demo-description=The%20fastest%20way%20to%20go%20headless%20with%20Hashnode&amp;demo-url=https%3A%2F%2Fhashnode-next.dev%2Fblog&amp;demo-image=https%3A%2F%2Fhashnode-next.dev%2Fdemo.png">deploying on Vercel</a>, adding a single environment variable, and clicking Create.</p>
<p>If you'd prefer to start locally, you can use the following command and setting your <code>.env.local</code>.</p>
<pre><code class="lang-bash">npx create-next-app -e https://github.com/alexkates/hashnode-next
</code></pre>
<p>Lastly, the <a target="_blank" href="https://github.com/alexkates/hashnode-next/blob/main/README.md">README</a> is a great resource for Github ticket templates, contributing, and more.</p>
<h2 id="heading-the-tech">The Tech</h2>
<h3 id="heading-nextjs-14">Next.js 14</h3>
<p>Uses several of the newest capabilities from Next.js and React including <a target="_blank" href="https://nextjs.org/docs/app">app router</a>, <a target="_blank" href="https://nextjs.org/docs/app/building-your-application/optimizing/metadata#dynamic-metadata">dynamic metadata</a>, <a target="_blank" href="https://nextjs.org/docs/app/building-your-application/rendering/server-components">server components</a>, and <a target="_blank" href="https://nextjs.org/docs/app/building-your-application/routing/loading-ui-and-streaming">suspense</a>.</p>
<h3 id="heading-hashnode-api">Hashnode API</h3>
<p>The <a target="_blank" href="https://apidocs.hashnode.com/#definition-Post">blog</a>, <a target="_blank" href="https://apidocs.hashnode.com/#definition-Tag">tag</a>, and <a target="_blank" href="https://apidocs.hashnode.com/#definition-MyUser">personal data</a> GQL queries were primarily used to drive the <code>/blog</code> and <code>/about</code> pages. Additionally, the <a target="_blank" href="https://apidocs.hashnode.com/#introduction-item-1">GQL Playground</a> was utilized to develop the GQL documents.</p>
<p>Recording analytics and pageviews was by far the trickiest part. I ended up using <a target="_blank" href="https://github.com/alexkates/hashnode-next/blob/main/next.config.js">Next.js redirects</a> and Hashnode's <a target="_blank" href="https://github.com/alexkates/hashnode-next/blob/main/src/components/analytics.tsx">Google Analytics.</a></p>
<p>Lastly, <a target="_blank" href="https://github.com/dotansimha/graphql-code-generator">graphql-code-generator</a> was employed to create TypeScript types from the schema.</p>
<h3 id="heading-vercel">Vercel</h3>
<p>Nothing crazy here... just your typical Vercel hosting and the awesome <a target="_blank" href="https://vercel.com/docs/deployments/deploy-button">Vercel Deploy Button</a>. This was my first time wiring up the Vercel deploy button, and I loved it.</p>
<h3 id="heading-shadcnui">Shadcn/ui</h3>
<p>The entire template is built using <a target="_blank" href="https://ui.shadcn.com/">shadcn/ui</a> and <a target="_blank" href="https://tailwindcss.com/">TailwindCSS</a>, especially using the <a target="_blank" href="https://ui.shadcn.com/docs/components/card">Card</a> component for each post and badge tile.</p>
<h2 id="heading-outro">Outro</h2>
<p>Building <a target="_blank" href="https://www.hashnode-next.dev/">hashnode-next</a> was my first attempt at creating an open-source project. I'm truly proud of the outcome, and I hope that some Hashnode bloggers looking to go headless and learn Next.js 14 will find this helpful.</p>
<p>If you found this helpful, I have a couple other free to use tools over at <a target="_blank" href="http://alexkates.dev/projects">alexkates.dev/projects</a> and also consider connecting on <a target="_blank" href="https://twitter.com/thealexkates">Twitter</a>.</p>
]]></content:encoded></item><item><title><![CDATA[7 Essential Meta Tags for Enhancing Your SEO and Social Media Approach]]></title><description><![CDATA[As a web developer, it's important to understand and effectively use meta tags to enhance your website's SEO, social media presence, and overall user experience. Below are the top 7 SEO and social media-related meta tags that you should ensure are pa...]]></description><link>https://blog.alexkates.dev/7-essential-meta-tags-for-enhancing-your-seo-and-social-media-approach</link><guid isPermaLink="true">https://blog.alexkates.dev/7-essential-meta-tags-for-enhancing-your-seo-and-social-media-approach</guid><category><![CDATA[HTML]]></category><category><![CDATA[SEO]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[Alex Kates]]></dc:creator><pubDate>Sat, 06 Jan 2024 22:07:33 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1704578786597/f0181561-5674-426f-aba7-b680af8cfa66.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>As a web developer, it's important to understand and effectively use meta tags to enhance your website's SEO, social media presence, and overall user experience. Below are the top 7 SEO and social media-related meta tags that you should ensure are part of your website.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Preview your meta and opengraph tags using <a target="_blank" href="https://ogtester.app">https://ogtester.app</a></div>
</div>

<h2 id="heading-title-tag">Title Tag</h2>
<p>The <code>&lt;title&gt;</code> tag is one of the most important parts of your SEO strategy and user experience. It's like your website's first impression: search engines use it to grasp what a page is about, affecting how it ranks in search results. For users, it's the first part of a page's content, appearing as a clickable headline in search results and on browser tabs. A catchy and relevant title tag can boost a page's visibility and click-through rates.</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>Alex Kates | Welcome to my Blog!<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
</code></pre>
<h2 id="heading-description-tag">Description Tag</h2>
<p>The meta description tag is equally as important for your website's SEO and user engagement. Think of it as your website's elevator pitch: it doesn't directly affect rankings, but it's essential for drawing users in from search results. It's kind of like your website's hook. This tag provides a brief preview of your page's content in search engine listings. A well-crafted meta description can help someone feel confident that your website has what they are looking for.</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"description"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"Join Alex Kates on a journey through innovative tech trends, personal development tips, and insightful commentary on modern culture. Dive into the blog now!"</span>/&gt;</span>
</code></pre>
<h2 id="heading-robots-tag">Robots Tag</h2>
<p>The meta robots tag is like a map for search engines navigating your website, telling them what they should and shouldn't pay attention to on your site. This tag doesn't interact directly with your visitors, but it plays a role in defining your website's SEO strategy. By specifying whether to index a page or follow its links, the meta robots tag helps you manage how your content is discovered and displayed in search results. It's a powerful tool for controlling the accessibility of your website's content to search engines.</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"robots"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"index, follow"</span>/&gt;</span>
</code></pre>
<p>This tells search engines to index the page and follow the links on it, increasing its visibility and reach.</p>
<h2 id="heading-open-graph-tags">Open Graph Tags</h2>
<p>Open Graph (og:*) tags are like your website's social media business card. They're import for defining how your content appears when shared on many social media platforms. These tags ensure your content is not just visible, but also engaging and clickable in social feeds.</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">property</span>=<span class="hljs-string">"og:title"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"Exploring Tech Trends with Alex Kates"</span>/&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">property</span>=<span class="hljs-string">"og:description"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"Join the conversation on the latest in tech on Alex's blog."</span>/&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">property</span>=<span class="hljs-string">"og:image"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"http://example.com/image.png"</span>/&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">property</span>=<span class="hljs-string">"og:url"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"http://example.com/blogpost"</span>/&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">property</span>=<span class="hljs-string">"og:type"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"article"</span>/&gt;</span>
</code></pre>
<h3 id="heading-ogtitle">og:title</h3>
<p>The title of your content as it should appear in the share. It's the first thing people notice, like a headline.</p>
<h3 id="heading-ogdescription">og:description</h3>
<p>Provides a concise and compelling summary of your content, helping to get a user to click.</p>
<h3 id="heading-ogimage">og:image</h3>
<p>This is the visual hook of your share, the image that appears in the post.</p>
<h3 id="heading-ogurl">og:url</h3>
<p>The URL of your page. This tag ensures the link shared is consistent and directs users exactly where you want them to go.</p>
<h3 id="heading-ogtype">og:type</h3>
<p>This tag tells the social media platform what type of content you're sharing like an article, video, or image gallery.</p>
<h2 id="heading-twitter-card-tags">Twitter Card Tags</h2>
<p>Similar to Open Graph, but for Twitter. These tags ensure that your content is displayed attractively when shared on Twitter.</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"twitter:card"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"summary_large_image"</span>/&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"twitter:title"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"Latest Tech Insights by Alex Kates"</span>/&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"twitter:description"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"Dive deep into the world of technology with Alex's expert insights and discussions."</span>/&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"twitter:image"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"http://example.com/image.jpg"</span>/&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"twitter:site"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"@AlexKatesTech"</span>/&gt;</span>
</code></pre>
<h3 id="heading-twittercard">twitter:card</h3>
<p>This tag sets the type of content you're sharing, like a summary or a large image, shaping the layout of your tweet.</p>
<h3 id="heading-twittertitle">twitter:title</h3>
<p>Much like a headline, it grabs the reader's attention, summarizing your content in a tweet.</p>
<h3 id="heading-twitterdescription">twitter:description</h3>
<p>Offers a sneak peek into your content, enticing users to click through.</p>
<h3 id="heading-twitterimage">twitter:image</h3>
<p>This visual element can be the deciding factor for engagement, making your tweet stand out.</p>
<h3 id="heading-twittersite">twitter:site</h3>
<p>Represents your handle or site's identity, connecting your content with your brand.</p>
<h2 id="heading-canonical-tag">Canonical Tag</h2>
<p>The rel="canonical" link tag acts like a detour sign for search engines, guiding them to the original or most relevant version of your content. It's particularly useful when similar content appears across multiple URLs. This tag helps prevent confusion over which page to rank, ensuring that all the SEO juice flows to the URL you deem most important.</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">link</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">"canonical"</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"http://www.example.com/preferred-page.html"</span>/&gt;</span>
</code></pre>
<p>With this tag, you're effectively telling search engines, "This is the page that matters most," ensuring your intended page gets the recognition and ranking it deserves.</p>
<h2 id="heading-viewport-tag">Viewport Tag</h2>
<p>The viewport meta tag controls how your website scales and renders on different screen sizes, from desktop monitors to mobile devices. This tag is fundamental in responsive web design, allowing your content to adapt across devices, enhancing user experience and accessibility.</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"viewport"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"width=device-width, initial-scale=1"</span>/&gt;</span>
</code></pre>
<p>This tag tells the browser to match the screen's width in device-independent pixels and start with a zoom level of 1. It's an important part of making your website accessible to all users across devices.</p>
<h2 id="heading-thanks-for-reading">Thanks For Reading</h2>
<p>Thanks for joining in on this journey through key meta tags. Mastering these can really elevate your site's SEO and user experience. Stay tuned for more tips and tricks in the tech world – feel free to follow along on Twitter @<a target="_blank" href="https://twitter.com/thealexkates">thealexkates</a>. Happy coding!</p>
]]></content:encoded></item><item><title><![CDATA[Introducing Supajournal]]></title><description><![CDATA[TL;DR 📚
For the Supabase Launchweek X Hackathon, I launched supajournal.app, an AI-powered journal that features statistics tracking and AI-generated writing prompts.

Intro 👋

“Write hard and clear about what hurts.”
– Ernest Hemingway

Journaling...]]></description><link>https://blog.alexkates.dev/introducing-supajournal</link><guid isPermaLink="true">https://blog.alexkates.dev/introducing-supajournal</guid><category><![CDATA[Next.js]]></category><category><![CDATA[side project]]></category><category><![CDATA[AI]]></category><dc:creator><![CDATA[Alex Kates]]></dc:creator><pubDate>Thu, 21 Dec 2023 14:20:51 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1704129563067/ecaaf8a2-71f8-4f78-8186-40ac9e252c21.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-tldr">TL;DR 📚</h1>
<p>For the <a target="_blank" href="https://www.madewithsupabase.com/hackathons/launch-week-x">Supabase Launchweek X Hackathon</a>, I launched <a target="_blank" href="https://supajournal.app">supajournal.app</a>, an AI-powered journal that features statistics tracking and AI-generated writing prompts.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1703166817791/f2aa42c5-aa8b-44cd-8a21-9cdc354ff5ed.png" alt="supajournal.app home page" class="image--center mx-auto" /></p>
<h1 id="heading-intro">Intro 👋</h1>
<blockquote>
<p>“Write hard and clear about what hurts.”</p>
<p><cite>– Ernest Hemingway</cite></p>
</blockquote>
<p>Journaling is one of the best things you can do for your mind, your heart, and your soul. it's a profound gift you give yourself—a space to untangle your thoughts, celebrate your victories, and find solace in your challenges.</p>
<p>But building a habit of writing can be challenging at first. It takes commitment, consistency, and patience.</p>
<p>Introducing <a target="_blank" href="https://supajournal.app">Supajournal!</a> Build that journaling habit once and for all.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1702898141260/79d7d7df-dcff-4949-947e-41a5f5a8c570.png" alt="An image of supajournal.app writing page" class="image--center mx-auto" /></p>
<h1 id="heading-features"><strong>Features 🔍</strong></h1>
<p><a target="_blank" href="https://supajournal.app">supajournal.app</a> just launched for the <a target="_blank" href="https://supabase.com/blog/supabase-hackathon-lwx">Supabase Launch Week X Hackathon</a> and already has several important features to help you build that journaling habit.</p>
<h2 id="heading-ai-generated-writing-prompts">AI Generated Writing Prompts 🤖</h2>
<p>For many, the hardest part of journaling everyday is finding inspiration. With <a target="_blank" href="http://supajournal.app">supajournal.app</a>, you no longer have to worry about running out of ideas or struggling to come up with topics.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1702899152720/357eecfb-6ab2-4cad-b841-dfd402ab513d.png" alt="An image of a new supajournal entry with an AI generated prompt." class="image--center mx-auto" /></p>
<p>Our AI-generated writing prompts are specifically designed to ignite your creativity and offer a starting point for your journal entries. The AI is optimized to encourage deep, meaningful, and insightful writing, helping you establish a journaling habit.</p>
<h2 id="heading-statistics">Statistics 📈</h2>
<p>You can't improve what you don't measure. It's crucial to track progress over time when developing any new habit. That's why Supajournal includes six key statistics as part of the MVP:</p>
<ol>
<li><p>Journal Entries</p>
</li>
<li><p>Streak</p>
</li>
<li><p>Most Popular Word</p>
</li>
<li><p>Word Count</p>
</li>
<li><p>Words per Journal Entry</p>
</li>
<li><p>Most Recent Entry</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1702924293965/31a45020-61de-4396-8c0e-8d33012838d5.png" alt="Image of Supajournal's statistics" class="image--center mx-auto" /></p>
<h2 id="heading-block-editor">Block Editor ✍🏻</h2>
<p>Supajournal offers a <a target="_blank" href="https://www.notion.so/">Notion</a>-like editor experience. Powered by <a target="_blank" href="http://novel.sh">novel.sh</a> and <a target="_blank" href="https://tiptap.dev/">TipTap</a>, this editor feels like a next-generation writing experience. The editor is composed of blocks, each of which can be modified independently from the others. You can drag, delete, edit, and customize each of these blocks to fit the mood you are in.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1702983181650/537d4251-d998-434d-a5f9-ed10e3cbc0c7.png" alt="Image of Supajournal's editor" class="image--center mx-auto" /></p>
<h1 id="heading-technology">Technology 🤖</h1>
<h2 id="heading-supabasehttpssupabasecom"><a target="_blank" href="https://supabase.com">Supabase</a></h2>
<p>This project was submitted as part of the <a target="_blank" href="https://www.madewithsupabase.com/p/supajournal">Supabase Launchweek X hackathon</a>. Supabase powers the Postgres database, authentication, and row-level security that form the core of the Supajournal app.</p>
<h2 id="heading-open-aihttpsplatformopenaicomdocsintroduction"><a target="_blank" href="https://platform.openai.com/docs/introduction">Open AI</a></h2>
<p>At the core of Supajournal is its integration with OpenAI. Powered by the new <a target="_blank" href="https://platform.openai.com/docs/models/gpt-3-5">gpt-3.5-turbo model</a>, Supajournal generates thought-provoking journal prompts to help you overcome writer's block and engage in self-reflection.</p>
<h2 id="heading-nextjshttpsnextjsorg"><a target="_blank" href="https://nextjs.org/">Next.js</a></h2>
<p>I absolutely love Next.js. I think it's the best web development framework I've ever seen. Supajournal is using <a target="_blank" href="https://nextjs.org/blog/next-14">Next.js 14</a> and utilizes several new features including React Server Components, App Router, and Server Actions.</p>
<h2 id="heading-typescripthttpswwwtypescriptlangorg"><a target="_blank" href="https://www.typescriptlang.org/">TypeScript</a></h2>
<p>Not much to say here other than it's 2023 and I've been using TypeScript by default for about 2 years.</p>
<h2 id="heading-shadcnuihttpsuishadcncom"><a target="_blank" href="https://ui.shadcn.com/">Shadcn/ui</a></h2>
<p>This was my first project using shadcn, and I must admit I was impressed. I genuinely appreciate the paradigm of installing a component without using NPM. It feels as if I own the components and can customize them however I need. Additionally, the components themselves feel incredibly clean and pleasant to use.</p>
<h1 id="heading-outro"><strong>Outro</strong> 👋</h1>
<p>Building a consistent journaling habit is hard. It requires dedication and persistence. Many people try but struggle with inconsistency or writer's block. However, journaling offers benefits such as improved mental clarity, self-awareness, and personal growth. Committing to journaling can lead to a more fulfilling and meaningful life.</p>
<p>I hope you try <a target="_blank" href="https://supajournal.app">Supajournal</a> and finally build that journaling habit once and for all!</p>
]]></content:encoded></item><item><title><![CDATA[Hosting Custom Fonts in AWS]]></title><description><![CDATA[Alright, here's the scenario: You're a front-end developer who was just given a stunning custom font from your designers, and they're incredibly excited about it. They want to use it as soon as possible. However, you're only familiar with using fonts...]]></description><link>https://blog.alexkates.dev/hosting-custom-fonts-in-aws</link><guid isPermaLink="true">https://blog.alexkates.dev/hosting-custom-fonts-in-aws</guid><category><![CDATA[AWS]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[Alex Kates]]></dc:creator><pubDate>Mon, 27 Nov 2023 10:22:06 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1701022945666/f3fbe1eb-70c1-4cbf-844a-198fffb1a2cf.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Alright, here's the scenario: You're a front-end developer who was just given a stunning custom font from your designers, and they're incredibly excited about it. They want to use it as soon as possible. However, you're only familiar with using fonts from Google Fonts and are unsure how to handle these ttf/woff2 files. You find yourself asking questions like, "Where should I place these files?" and "How do I write the appropriate CSS to utilize them?". Fear not; I recently faced a similar situation and would like to share with you exactly how I handled this situation.</p>
<p>In this post, we will guide you step by step on how to take custom font files and host them on a Content Delivery Network (CDN) in AWS. We'll be using the AWS CDK, TypeScript, and Bun to manage various AWS resources, including S3 and CloudFront, to deliver your stunning custom fonts to the edge, close to your users and apps.</p>
<p>We will then build a new Next.js app in a Bun monorepo. This Next.js app will use our new CDN to pull in our new fonts. We'll write the CSS and font-face code to properly use our new font.</p>
<h1 id="heading-project-setup">Project Setup</h1>
<div data-node-type="callout">
<div data-node-type="callout-emoji">🖥</div>
<div data-node-type="callout-text">You can find all the code used in this walkthrough at <a target="_blank" href="https://github.com/alexkates/how-to-host-custom-fonts-in-s3">https://github.com/alexkates/how-to-host-custom-fonts-in-s3</a></div>
</div>

<p>To start we need to set up our new project. Were going to be using <a target="_blank" href="http://bun.sh">Bun</a>, <a target="_blank" href="https://www.typescriptlang.org/">TypeScript</a>, and the <a target="_blank" href="https://docs.aws.amazon.com/cdk/v2/guide/home.html">AWS CDK</a>. Utilizing these tools together allows us to use a single language, TypeScript, across our entire repository. It will also make it incredibly easy to transition to a monorepo powered by Bun later on in this article.</p>
<h2 id="heading-cdk-init">cdk init</h2>
<p>Begin by opening your terminal and running <code>mkdir how-to-host-custom-fonts-in-s3</code> to create a new directory specifically for this project.</p>
<p>Next, navigate to this new directory with cd how-to-host-custom-fonts-in-s3, and run <code>bunx cdk init app --language typescript</code>. Bunx is an alias for `bun x` and functions similarly to `npx`.</p>
<pre><code class="lang-bash">mkdir how-to-host-custom-fonts-in-s3
<span class="hljs-built_in">cd</span> how-to-host-custom-fonts-in-s3
bunx cdk init app --language typescript
</code></pre>
<h2 id="heading-nvm-use">nvm use</h2>
<p>Next, we'll need to run a couple more commands to properly set up Node and install our dependencies.</p>
<p>I usually have multiple work and personal-related projects on my laptop, so managing Node.js versions across all of them is crucial. I prefer using a .nvmrc file at the root of my projects. For this project, we will utilize Node.js version 20.</p>
<p>Let's create a <code>.nvmrc</code> file at the root of our project and set its value to <code>v20</code>, which is the latest version of Node.js.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700829303733/bfeb83d2-a4ab-482b-bfd0-61cb4b1c13b7.png" alt="VSCode with a .nvmrc file created with value v20" class="image--center mx-auto" /></p>
<p>Great! Now just a few more commands to finish setting up our development environment.</p>
<pre><code class="lang-bash">nvm install v20
nvm use
</code></pre>
<ol>
<li><p><strong>nvm install v20</strong>: Installs Node.js version 20 using Node Version Manager (nvm).</p>
</li>
<li><p><strong>nvm use</strong>: Activates the installed Node.js version for the session.</p>
</li>
</ol>
<h2 id="heading-bun-install">bun install</h2>
<p>Now we are going to switch from npm to <a target="_blank" href="https://bun.sh/">Bun</a> as our package manager. I'm personally a big fan of <a target="_blank" href="https://bun.sh/">Bun</a> and have been slowly migrating all of my existing projects to use it. For any new projects, <a target="_blank" href="https://bun.sh/">Bun</a> is a default for me.</p>
<p>The AWS CDK scaffolds new projects using NPM as the package manager. To switch to Bun, we need to delete the package-lock.json and run <code>bun install</code>. Simply run the following commands to do so.</p>
<pre><code class="lang-bash">rm package-lock.json
bun install
</code></pre>
<ol>
<li><p><strong>rm package-lock.json</strong>: Removes the <code>package-lock.json</code> file that was created by our <code>cdk init</code> command earlier.</p>
</li>
<li><p><strong>bun install</strong>: Installs project dependencies using Bun.</p>
</li>
</ol>
<p>If everything goes smoothly, your folder structure should resemble the image below. The most crucial aspect is that you now have a bun.lock file. This lockfile functions differently from npm or yarn, as it is a binary file rather than plaintext. The primary reason for this is to enhance performance. <a target="_blank" href="https://bun.sh/docs/install/lockfile">Bun’s lockfile</a> saves &amp; loads quickly and saves a lot more data than what is typically inside lockfiles.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700828333945/437cfc2d-56b1-49b0-b9da-89867a42aeb3.png" alt="Image of VSCode after running the cdk init command and re-installing packages using bun" class="image--center mx-auto" /></p>
<h2 id="heading-prettierrcjson">.prettierrc.json</h2>
<p>The following configuration is based on personal preference. Since I have a widescreen monitor, I prefer to set my Prettier <code>printWidth</code> wider than the default. In this case, I'm using a value of 120.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700829484114/42ffbe9b-7eb7-4149-bccd-dbb047c3941d.png" alt="VSCode with a .prettierrc.json file created with printWidth of 120" class="image--center mx-auto" /></p>
<h1 id="heading-building-the-stack">Building the stack</h1>
<p>For our font hosting and delivery setup, we will be implementing several key AWS services:</p>
<ul>
<li><p><strong>S3 Bucket</strong>: Used for reliable and organized storage of font files. It's the main storage component.</p>
</li>
<li><p><strong>CloudFront Distribution</strong>: Acts as the delivery mechanism, ensuring rapid distribution of fonts to users globally.</p>
</li>
<li><p><strong>CloudFront Origin Access Identity (OAI)</strong>: A security feature that allows CloudFront to securely access S3 fonts without public access.</p>
</li>
<li><p><strong>CloudFront Cache Policy</strong>: Dictates caching behavior to optimize performance and costs, affecting load times and bandwidth.</p>
</li>
<li><p><strong>CORS Configuration</strong>: Essential for cross-origin resource sharing, it enables secure usage of fonts across different domains.</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700830153728/b8a9c6f3-15a3-4a41-97c7-0c54229e993a.png" alt="CDK stack code with an S3 bucket, Cloudfront Distribution, Origin Access Identity, and Cache policy" class="image--center mx-auto" /></p>
<p>Let's break down exactly what is happening here, line by line.</p>
<ul>
<li><p><strong>Imports</strong>: Bringing in necessary CDK modules for S3, CloudFront, and CloudFront origins.</p>
</li>
<li><p><strong>Stack Definition</strong>: Creating a custom stack class <code>HowToHostCustomFontsInS3Stack</code> extending the CDK <code>Stack</code>.</p>
</li>
<li><p><strong>S3 Bucket Setup</strong>: Initializing an S3 bucket with a removal policy and CORS configuration, allowing <code>GET</code> requests from any origin.</p>
</li>
<li><p><strong>Origin Access Identity</strong>: Creating a CloudFront Origin Access Identity (OAI) for secure access to the S3 bucket contents.</p>
</li>
<li><p><strong>Bucket Permissions</strong>: Granting read permissions to the OAI for the S3 bucket.</p>
</li>
<li><p><strong>Cache Policy</strong>: Defining a CloudFront cache policy to control header behavior, particularly for the 'Origin' header.</p>
</li>
<li><p><strong>CloudFront Distribution</strong>: Setting up a CloudFront distribution with the cache policy, an S3 origin linked to the OAI, and enforcing HTTPS.</p>
</li>
<li><p><strong>CDK Output</strong>: Outputting the domain name of the CloudFront distribution for easy access post-deployment.</p>
</li>
</ul>
<p>It's time to deploy this stack to AWS. Simply run <code>bun cdk deploy --require-approval never</code>. If all goes well, you will see an output similar to the one below. Pay particular attention to the distribution domain name output, as we will use it later to construct the URL for fetching the font files.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700913995959/c7e0c409-dc13-4639-bc9e-a3e169ffdaed.png" alt="Image of terminal output after running bun cdk deploy" class="image--center mx-auto" /></p>
<p>That's it! In just about 45 lines of TypeScript and AWS CDK code, you can create a CDN using CloudFront, backed by S3, to host your custom fonts. These fonts will be readily available at the edge, wherever your users access your web application.</p>
<h1 id="heading-upload-fonts-to-s3">Upload Fonts to S3</h1>
<p>For simplicity, we will use the <a target="_blank" href="https://fonts.google.com/specimen/Montserrat">Montserrat font from Google</a>, but the specific font is not important. If you have a completely custom font, this same solution will work just as well! This also works for any type of font file, including TTF, OTF, and WOFF.</p>
<ol>
<li><p><strong>Log in to AWS Management Console</strong>: Open your AWS account and navigate to the S3 service.</p>
</li>
<li><p><strong>Select Your S3 Bucket</strong>: Locate and access the S3 bucket designated for the 'Montserrat' font files.</p>
</li>
<li><p><strong>Upload 'Montserrat' Folder</strong>: Click on 'Upload', then 'Add folder', and select the 'Montserrat/static' folder which includes various font files like 'Montserrat-Regular.ttf'. Ensure the MIME type for each file is set to 'font/truetype'.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700830888848/e8038af9-d84e-4890-ad20-53a50ea61ece.png" alt="Image of Mac finder about to upload our font files" /></p>
</li>
<li><p><strong>Review and Upload</strong>: Double-check that the MIME types are correct and proceed with the upload. If the MIME type is incorrect, you may encounter a CORS error later, which can be a nightmare to debug.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700830984577/3b0c74dd-0ab4-49c3-854b-15084318fe2d.png" alt="Image of AWS S3 with all of our font files ready to be uploaded" class="image--center mx-auto" /></p>
</li>
<li><p><strong>Verify Upload</strong>: Refresh the bucket post-upload to confirm the Montserrat folder and its contents are uploaded. To test the 'Montserrat-Regular.ttf', use this URL format in your browser: <a target="_blank" href="https://dkeeepdefzien.cloudfront.net/Montserrat/static/Montserrat-Regular.ttf">https://dkeeepdefzien.cloudfront.net/Montserrat/static/Montserrat-Regular.ttf</a>. This URL combines your CloudFront domain with the file path, allowing you to verify the successful hosting of the font file.</p>
</li>
<li><p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700831252775/c44249e1-618d-4203-94fc-759f70612f02.png" alt="Image of a browser with the address highlighted showing the full url to one of our font files" class="image--center mx-auto" /></p>
<p> <strong>Test with cURL:</strong> The last thing to verify is to make sure the proper Content-Type header is set. This is important for browsers to handle the font file correctly.</p>
</li>
</ol>
<pre><code class="lang-bash">curl -I https://dkeeepdefzien.cloudfront.net/Montserrat/static/Montserrat-Regular.ttf
</code></pre>
<p>If everything is working, you should be able to download the file by navigating directly to <a target="_blank" href="https://dkeeepdefzien.cloudfront.net/Montserrat/static/Montserrat-Regular.ttf">https://dkeeepdefzien.cloudfront.net/Montserrat/static/Montserrat-Regular.ttf</a>. Also, the cURL command should produce output like the following.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700849780925/9fdf3961-c835-406b-b6af-4735cdd2bbae.png" alt="Image of a terminal output from a curl command targeting our CDN" class="image--center mx-auto" /></p>
<p>Congratulations! You now have a functioning CDN hosting your custom font, which you can download directly from your web app.</p>
<h1 id="heading-extra-credit">Extra Credit</h1>
<p>Now let's have some fun! In this section, we are going to ...</p>
<ol>
<li><p>Convert our project to a <a target="_blank" href="https://bun.sh/docs/install/workspaces">Bun monorepo</a></p>
</li>
<li><p>Add a Next.js app</p>
</li>
<li><p>Fetch and use our custom fonts</p>
</li>
</ol>
<h2 id="heading-convert-to-a-monorepo">Convert to a Monorepo</h2>
<p>Monorepos with Bun are straightforward and have the added benefit of hoisting common dependencies to the root of the repository.</p>
<ol>
<li><p>From the root, create the folder <code>/packages/fonts-cdn</code></p>
</li>
<li><p>Move everything from the root to /packages/fonts-cdn</p>
</li>
<li><p>Delete /packages/fonts-cdn/node_modules</p>
</li>
<li><p>Update the name of /packages/fonts-cdn/package.json to be "fonts-cdn"</p>
</li>
<li><p>Create package.json in the root and set it to the following</p>
<pre><code class="lang-json"> {
   <span class="hljs-attr">"name"</span>: <span class="hljs-string">"how-to-host-custom-fonts-in-s3"</span>,
   <span class="hljs-attr">"version"</span>: <span class="hljs-string">"1.0.0"</span>,
   <span class="hljs-attr">"workspaces"</span>: [
     <span class="hljs-string">"packages/*"</span>
   ]
 }
</code></pre>
</li>
<li><p>From the root, run <code>bun install</code></p>
</li>
</ol>
<p>Your project should now resemble the structure below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700916551249/e834b2d9-1c07-4b3a-add1-aac43568f497.png" alt="Image of VSCode after migrating to a bun monorepo" class="image--center mx-auto" /></p>
<h2 id="heading-create-a-nextjs-app">Create a Next.js app</h2>
<p>Now let's scaffold a Next.js using create-next-app and Bun.</p>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> packages
bun create-next-app
</code></pre>
<p>Step through the normal create-next-app prompts and you should see an output that looks like this.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700917179990/e6237f4d-a88a-49ad-b28c-fb4f90ca29df.png" alt="Image of output from running bun create-next-app" class="image--center mx-auto" /></p>
<p>After completing the create-next-app prompts, your project should resemble the following structure. You will notice that there are now two packages. The first one is "fonts-cdn", which was our AWS CDK app from part 1 of this walkthrough. The second one is our newly created Next.js app called "fonts-app".</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1701002065571/a28ba347-9a65-4d5f-a259-3ef190b6cc78.png" alt class="image--center mx-auto" /></p>
<p>Great! Let's run the Next.js app in development mode and ensure everything works properly. From what I understand, with Bun, you need to change directories (cd) into the package you want to run and then execute the development command. This is slightly different from Yarn, where you can stay at the root and run the 'yarn workspace' command followed by the desired command.</p>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> packages/fonts-app
bun dev
open http://localhost:3000
</code></pre>
<p>You should see the iconic Next.js default page!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700917456760/e1c535db-4978-4b9b-b78c-5981d22f6896.png" alt="Image of the default Next.js page layout after running bun dev" class="image--center mx-auto" /></p>
<h2 id="heading-use-the-font-cdn">Use the Font CDN</h2>
<p>Fantastic, we now have all the components necessary to utilize our custom fonts hosted on our CDN. When it comes to using a custom font with Next.js and Tailwind, there are several options for customizing Tailwind.</p>
<p>I recommend <a target="_blank" href="https://www.tailwindtoolbox.com/guides/adding-fonts-to-tailwind-css">these docs from Tailwind Toolbox</a>, which explain the differences between various Tailwind font customizations. Essentially, you can either overwrite or extend the default Tailwind theme, and your approach will affect the appearance of your generated Tailwind utility classes.</p>
<p>For the sake of brevity, let's host our CSS locally and overwrite the existing Tailwind utility classes. However, this decision ultimately depends on your specific use cases.</p>
<h3 id="heading-create-montserratcss">Create montserrat.css</h3>
<p>First, we must create a CSS file that defines our font faces. The crucial aspect is to establish a font face for each font file, taking into account font weight and italics. Call this CSS file 'montserrat.css' and place it in <code>fonts-app/src/app/montserrat.css</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1701003642316/e9e1a7a6-52de-4d3a-8fc8-396870628ca9.png" alt="Image of motserrat.css with all our custom font faces" class="image--center mx-auto" /></p>
<h3 id="heading-update-globalscss">Update globals.css</h3>
<p>Next, make a one-line change to 'fonts-app/src/app/globals.css' to include 'montserrat.css'. Simply use the @import statement in 'globals.css' to incorporate the new CSS file. After making this change, your 'globals.css' should resemble the following.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1701009214848/a1cb4481-45c5-4f6e-8913-8b9b38badca1.png" alt="Default Next.js globals.css with the mtserrat.css file included" class="image--center mx-auto" /></p>
<h3 id="heading-delete-nextfontgoogle">Delete next/font/google</h3>
<p>The latest version of Next.js comes with an excellent font optimization system. However, it doesn't directly align with our use case since we want to host our font on a CDN for multiple apps to utilize. Therefore, we will remove the font optimization code for now.</p>
<p>Navigate to fonts-app/src/app/layout.tsx and eliminate any usage of next/font/google and Inter. Once completed, your layout.tsx should resemble the following.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1701008864166/ff296080-35dc-4913-8a0b-ca8c0d547277.png" alt="Default Next.js layout.tsx with any font optimizations removed" class="image--center mx-auto" /></p>
<h3 id="heading-update-tailwindconfigts">Update tailwind.config.ts</h3>
<p>Since we are using Tailwind, we need to inform it that we want to use Montserrat as our sans-serif font family. Tailwind makes this process quite straightforward. There are several ways to customize your font family with Tailwind, depending on your use case and objectives. For this walkthrough, we will override the default sans-serif font so that the out-of-the-box utility classes seamlessly use our Montserrat font. To accomplish this, update your tailwind.config.ts file, specifically the config.theme.fontFamily.sans value. Once updated, it should look like the following.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1701019860278/54a1ada6-cb3a-49d6-85f1-579d077bd1e7.png" alt="Image of tailwind.config.ts after adding the Montserrat as the default fontFamily sans font." class="image--center mx-auto" /></p>
<p>Notice that we are setting the sans font family to an array, with the primary value as Montserrat and using the spread operator to include the default settings as backups.</p>
<h3 id="heading-testing-it-all-out">Testing it all out</h3>
<p>Awesome! Now let's test our new fonts and setup. Restart your dev server by running the following commands.</p>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> packages/fonts-app
bun run dev
open http://localhost:3000
</code></pre>
<p>If everything is working, the default Next.js page should now be using the Montserrat font and should look like the following image.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1701020516135/6ca63a9d-ebe5-4f0e-918e-315682b7e860.png" alt="Image of the browser with dev tools open showing that the default Next.js font is now Montserrat" class="image--center mx-auto" /></p>
<h1 id="heading-thanks-for-reading">Thanks for reading!</h1>
<p>Congratulations! If you made it this far, you successfully hosted custom font files on a Content Delivery Network (CDN) in AWS using AWS CDK, TypeScript, and Bun. We covered setting up the project, building and deploying the stack, uploading fonts to S3, and utilizing the CDN with a Next.js app.</p>
<p>If you found this content helpful, please consider liking, sharing, and following me here on Hashnode and at <a target="_blank" href="https://twitter.com/thealexkates">https://twitter.com/thealexkates</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Introducing Guaranteed Buybacks with the Croissant Chrome Extension]]></title><description><![CDATA[About 18 months ago I joined Croissant as Director of Engineering to help change the way we think about shopping. The stuff you buy has value beyond the point of purchase and Croissant's Guaranteed Buybacks ensure that your purchases retain their wor...]]></description><link>https://blog.alexkates.dev/introducing-guaranteed-buybacks-with-the-croissant-chrome-extension</link><guid isPermaLink="true">https://blog.alexkates.dev/introducing-guaranteed-buybacks-with-the-croissant-chrome-extension</guid><category><![CDATA[Web Development]]></category><category><![CDATA[Startups]]></category><dc:creator><![CDATA[Alex Kates]]></dc:creator><pubDate>Sun, 12 Nov 2023 21:11:38 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1699822825071/3fea782d-ed79-4025-9aed-6c2d747ba25c.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>About 18 months ago I joined <a target="_blank" href="https://croissant.com/chrome-extension?utm_source=team_magenta&amp;utm_medium=outreach&amp;utm_campaign=chromex_competition&amp;utm_content=alexkates-dot-dev">Croissant</a> as Director of Engineering to help change the way we think about shopping. The stuff you buy has value beyond the point of purchase and Croissant's Guaranteed Buybacks ensure that your purchases retain their worth.</p>
<p>Today we launched the <a target="_blank" href="https://croissant.com/chrome-extension?utm_source=team_magenta&amp;utm_medium=outreach&amp;utm_campaign=chromex_competition&amp;utm_content=alexkates-dot-dev">Croissant Chrome Extension</a> that offers Guaranteed Buybacks of up to 75% on over 1 million items at your <a target="_blank" href="https://croissant.com/chrome-extension?utm_source=team_magenta&amp;utm_medium=outreach&amp;utm_campaign=chromex_competition&amp;utm_content=alexkates-dot-dev">favorite retailers</a>.</p>
<p><a target="_blank" href="https://croissant.com/chrome-extension?utm_source=team_magenta&amp;utm_medium=outreach&amp;utm_campaign=chromex_competition"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1699547998813/021b6b4d-0364-4d1b-91c0-b83e1289d97d.png" alt="Image of the Croissant Chrome Extension with two Guaranteed Buybacks" /></a></p>
<p>Croissant’s Guaranteed Buybacks are exactly what they sound like —<br />you buy an item at retail, and we’ll buy it back from you at a baked-in price for up to one year. No fees. No pressure. Just money back for your things.</p>
<p>After you install it from the Chrome Web Store, you can use it to see the Guaranteed Buyback value of items as you shop. Any eligible items you buy, while the extension is activated, will be added to your <a target="_blank" href="https://app.croissant.com">Croissant Collection</a>. If you choose to redeem your buybacks, simply send them to Croissant and get paid.</p>
<h2 id="heading-how-it-works">How It Works</h2>
<p>Getting started with the <a target="_blank" href="https://croissant.com/chrome-extension?utm_source=team_magenta&amp;utm_medium=outreach&amp;utm_campaign=chromex_competition&amp;utm_content=alexkates-dot-dev">Croissant Chrome Extension</a> is a simple and straightforward process. All you need to do is follow these steps:</p>
<ol>
<li><p><strong>Quick Installation</strong>: Add the <a target="_blank" href="https://croissant.com/chrome-extension?utm_source=team_magenta&amp;utm_medium=outreach&amp;utm_campaign=chromex_competition&amp;utm_content=alexkates-dot-dev">Croissant Chrome Extension</a> to your browser from the Chrome Web Store, and you're all set. It integrates quietly and quickly, ready to work behind the scenes.</p>
</li>
<li><p><strong>Know Your Item's Worth</strong>: As you shop, Croissant shows you the guaranteed buyback value for items you're interested in. This helps you shop with an awareness of an item’s future value.</p>
</li>
<li><p><strong>Track Your Purchases</strong>: Upon purchase, the item enters your <a target="_blank" href="https://app.croissant.com">Croissant Collection,</a> which acts as a personal inventory, tracking the value of everything you buy.</p>
</li>
<li><p><strong>Easy Resell Option</strong>: When you're ready for a change, Croissant’s buyback process is at your service. Sell your items, and Croissant ensures you receive the guaranteed value back in return.</p>
</li>
</ol>
<p>With Croissant, you're not just buying—you're investing in items that hold onto value, giving you a smarter way to shop.</p>
<h3 id="heading-know-value">Know Value</h3>
<p>At its core, Croissant's Chrome Extension is all about learning and understanding the value of things while you shop. For instance, consider the <a target="_blank" href="https://www.aloyoga.com/products/m1133r-the-triumph-crew-neck-tee-mars-clay?variant=42062629109940">Triumph Crew Neck Tee from Alo Yoga</a>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1699816979821/448a1792-e7e0-4b36-a829-fff76362f3b6.png" alt class="image--center mx-auto" /></p>
<p>It retails for $54, but Croissant informs me that I can resell it within a year and receive a guaranteed value of $24 back in cash. This realization has a psychological effect at the point of purchase. I now think, "Oh, I'm only spending $30 on this full-priced item because it holds value beyond the initial purchase." When I'm ready to move on from it, I can resell it back to Croissant for $24!</p>
<h3 id="heading-activate-guaranteed-buybacks">Activate Guaranteed Buybacks</h3>
<p>With just a click, you activate the buyback feature for your new purchases. This locks in your guaranteed buyback, ensuring you can choose to sell any item back to Croissant at a predetermined price.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1699817621261/6841acdd-6b32-446f-9f7c-148307b16d80.gif" alt /></p>
<p>Upon activating the Guaranteed Buybacks feature, you can rest assured that your purchases will be smoothly integrated into your Croissant account, providing you with the opportunity to resell them at a later date. This not only secures the predetermined buyback price for each item but also offers you the flexibility and convenience to manage your purchases with ease. By activating, you proactively maximize the value of your purchases while ensuring a hassle-free experience when you decide to sell them later.</p>
<h3 id="heading-hassle-free-reselling">Hassle-Free Reselling</h3>
<p>As you activate the <a target="_blank" href="https://croissant.com/chrome-extension?utm_source=team_magenta&amp;utm_medium=outreach&amp;utm_campaign=chromex_competition&amp;utm_content=alexkates-dot-dev">Croissant Chrome Extension</a> and lock in your Guaranteed Buybacks, your purchases will flow into your Croissant collection. You can access your Croissant collection via our <a target="_blank" href="https://app.croissant.com">web app</a> or <a target="_blank" href="https://apps.apple.com/us/app/croissant/id1662287582">IOS app</a>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1699820723947/18862883-e183-4ee5-9371-6a365a26780c.png" alt class="image--center mx-auto" /></p>
<p>When you're ready, you can access your collection and locate your Guaranteed Buybacks. Simply click or tap the "Sell" button, package and ship your item, and your proceeds will be deposited into your Croissant wallet, where you can withdraw them via Venmo or PayPal.</p>
<h2 id="heading-thanks-for-reading">Thanks For Reading</h2>
<p>Launching <a target="_blank" href="https://croissant.com/chrome-extension?utm_source=team_magenta&amp;utm_medium=outreach&amp;utm_campaign=chromex_competition&amp;utm_content=alexkates-dot-dev">Croissant Chrome Extension</a> has been a highlight of my career. It's been an incredible ride, and I couldn't be prouder of the product we've built, the team and the way we've all pulled together to make it happen. It feels amazing to put this out into the world!</p>
]]></content:encoded></item><item><title><![CDATA[Mastering AWS S3: 7 Essential Tips for Using the aws s3 ls Command]]></title><description><![CDATA[In this article, we explore 7 useful AWS CLI commands for managing S3 buckets, including checking bucket size, listing objects with specific extensions, finding the largest files, listing objects from a specific date, listing objects with a specific ...]]></description><link>https://blog.alexkates.dev/mastering-aws-s3-7-essential-tips-for-using-the-aws-s3-ls-command</link><guid isPermaLink="true">https://blog.alexkates.dev/mastering-aws-s3-7-essential-tips-for-using-the-aws-s3-ls-command</guid><category><![CDATA[AWS]]></category><category><![CDATA[Cloud]]></category><category><![CDATA[Developer]]></category><dc:creator><![CDATA[Alex Kates]]></dc:creator><pubDate>Mon, 30 Oct 2023 11:14:50 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1698664446245/3bae05b4-2aad-46b5-a891-e61fb8e465de.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>In this article, we explore 7 useful AWS CLI commands for managing S3 buckets, including checking bucket size, listing objects with specific extensions, finding the largest files, listing objects from a specific date, listing objects with a specific prefix, listing recently modified objects, and identifying empty directories. These commands will help you manage and optimize your S3 storage more effectively.</p>
</blockquote>
<p>Whether you're experienced with AWS or new to cloud storage, mastering the AWS CLI, particularly the <code>aws s3 ls</code> command, can help you manage your S3 buckets. I've collected 7 of my most common and useful <code>aws s3 ls</code> commands that I've been recently using that have helped make s3 interactions so straightforward.</p>
<h3 id="heading-1-check-the-size-of-an-s3-bucket"><strong>1. Check the Size of an S3 Bucket</strong></h3>
<p>Ever wondered how much data you've stored in a specific bucket? This command helps you calculate the total size of an S3 bucket, giving you insights into your storage utilization.</p>
<pre><code class="lang-bash">aws s3 ls s3://YOUR_BUCKET_NAME --recursive | awk <span class="hljs-string">'{total += $3} END {print "Total:", total/1024/1024 " MB"}'</span>
</code></pre>
<h3 id="heading-2-list-all-objects-with-a-specific-extension"><strong>2. List All Objects with a Specific Extension</strong></h3>
<p>Filtering by file extension is useful when you're trying to get an overview of specific file types stored in your bucket, like PDFs.</p>
<pre><code class="lang-bash">aws s3 ls s3://YOUR_BUCKET_NAME --recursive | grep <span class="hljs-string">".pdf"</span>
</code></pre>
<h3 id="heading-3-list-top-10-largest-files-in-a-bucket"><strong>3. List Top 10 Largest Files in a Bucket</strong></h3>
<p>This command helps you spot the 10 largest files in your bucket, which might assist in clean-up or storage optimization.</p>
<pre><code class="lang-bash">aws s3 ls s3://YOUR_BUCKET_NAME --recursive | sort -k 3 -n -r | head -n 10
</code></pre>
<h3 id="heading-4-list-objects-from-a-specific-date"><strong>4. List Objects from a Specific Date</strong></h3>
<p>If you've uploaded or modified files on a particular date and want to view them, this command is your go-to. Just replace the date accordingly.</p>
<pre><code class="lang-bash">aws s3 ls s3://YOUR_BUCKET_NAME --recursive | grep <span class="hljs-string">"2023-08-21"</span>
</code></pre>
<h3 id="heading-5-list-objects-with-a-specific-prefix"><strong>5. List Objects with a Specific Prefix</strong></h3>
<p>Organizing your S3 bucket with prefixes helps in data management. This command lets you view objects under a specific prefix.</p>
<pre><code class="lang-bash">aws s3 ls s3://YOUR_BUCKET_NAME/PREFIX/
</code></pre>
<h3 id="heading-6-list-all-objects-modified-within-the-last-10-days"><strong>6. List All Objects Modified Within the Last 10 Days</strong></h3>
<p>Want to see what's been updated recently? This command lists all the objects modified in the last 10 days, helping you keep track of recent changes.</p>
<pre><code class="lang-bash">aws s3 ls s3://YOUR_BUCKET_NAME --recursive | grep <span class="hljs-string">"<span class="hljs-subst">$(date +%Y-%m-%d -d '10 days ago')</span>"</span>
</code></pre>
<h3 id="heading-7-find-empty-directories-in-a-bucket"><strong>7. Find Empty Directories in a Bucket</strong></h3>
<p>Empty directories can clutter your bucket and might be remnants of older structures. Use this command to identify and possibly clean them up.</p>
<pre><code class="lang-bash"><span class="hljs-keyword">for</span> prefix <span class="hljs-keyword">in</span> $(aws s3 ls s3://YOUR_BUCKET_NAME/ | awk <span class="hljs-string">'{print $2}'</span>); <span class="hljs-keyword">do</span> 
    count=$(aws s3 ls s3://YOUR_BUCKET_NAME/<span class="hljs-variable">$prefix</span> --recursive | wc -l); 
    <span class="hljs-keyword">if</span> [ <span class="hljs-variable">$count</span> -eq 0 ]; <span class="hljs-keyword">then</span> 
        <span class="hljs-built_in">echo</span> <span class="hljs-string">"Empty directory: <span class="hljs-variable">$prefix</span>"</span>; 
    <span class="hljs-keyword">fi</span>; 
<span class="hljs-keyword">done</span>
</code></pre>
<p>Alright, folks, that's a wrap! Whether you're an AWS pro or just getting started, I hope these <code>aws s3 ls</code> tricks make your life a tad easier. The more you play around with these commands, the more you'll get the hang of it. Did I miss any that you regularly use?</p>
<p>Like this type of content? Follow me on <a target="_blank" href="https://twitter.com/thealexkates">Twitter</a> for more!</p>
]]></content:encoded></item><item><title><![CDATA[Upgrade Your Vercel Hosted Astro App to Bun 1.0]]></title><description><![CDATA[Vercel rolled out native support for Bun 1.0 just 72 hours after its launch. That's the kind of speed that got me excited to switch my new Astro project, BullyBarks, over to Bun 1.0.
In this blog post, we're going to walk through a step-by-step guide...]]></description><link>https://blog.alexkates.dev/upgrade-your-vercel-hosted-astro-app-to-bun-10</link><guid isPermaLink="true">https://blog.alexkates.dev/upgrade-your-vercel-hosted-astro-app-to-bun-10</guid><category><![CDATA[Astro]]></category><category><![CDATA[Bun]]></category><category><![CDATA[Vercel]]></category><dc:creator><![CDATA[Alex Kates]]></dc:creator><pubDate>Tue, 12 Sep 2023 11:43:48 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1694518957815/ca7adc50-f8d2-41fe-9e8e-15d41ad52dc6.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Vercel rolled out native support for Bun 1.0 just 72 hours after its launch. That's the kind of speed that got me excited to switch my new Astro project, <a target="_blank" href="https://bullybarks.com/"><strong>BullyBarks</strong></a>, over to Bun 1.0.</p>
<p>In this blog post, we're going to walk through a step-by-step guide to upgrading an Astro project to take advantage of all the features and optimizations Bun 1.0 has to offer.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://twitter.com/vercel/status/1701378595623415934">https://twitter.com/vercel/status/1701378595623415934</a></div>
<p> </p>
<h2 id="heading-astro-project-structure">Astro Project Structure</h2>
<p>Before diving into the migration steps, it's essential to understand the existing structure of the BullyBarks project. In this Astro-based project, you'll notice a few specific files like <code>.nvmrc</code> (NVM), <code>.npmrc</code>, and <code>pnpm-lock.yaml</code>. These indicate that I've been using Node Version Manager (NVM), npm, and PNPM for package management. These are precisely the tools we'll be migrating away from as we transition to Bun 1.0. Now, let's break down the folder structure to better understand how the project is organized.</p>
<ul>
<li><p><code>.prettierrc</code>: The configuration file for the Prettier code formatter.</p>
</li>
<li><p><code>.vscode</code>: This directory holds Visual Studio Code-specific settings.</p>
</li>
<li><p><code>src/</code>: Project's source code including components, layouts, and pages.</p>
</li>
<li><p><code>public/</code>: static files</p>
</li>
<li><p><code>astro.config.mjs</code>: The Astro configuration file.</p>
</li>
<li><p><code>tsconfig.json</code>: The TypeScript configuration file.</p>
</li>
<li><p><code>tailwind.config.cjs</code>: This holds the configuration for Tailwind CSS.</p>
</li>
<li><p><code>node_modules/</code>: This directory contains all the dependencies for the project.</p>
</li>
<li><p><code>dist/</code>: The output directory generated after running Astro's build script.</p>
</li>
<li><p><code>.npmrc</code> and <code>.nvmrc</code>: These are configuration files for npm and NVM, respectively.</p>
</li>
<li><p><code>package.json</code>: Like any Node.js-based project, this file lists dependencies and scripts.</p>
</li>
<li><p><code>pnpm-lock.yaml</code>: Since we're using PNPM for package management, this file locks down the versions of the dependencies.</p>
</li>
<li><p><code>.astro</code>: Astro types directory</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1694333508670/1e18da07-3cb6-45f8-b1ec-a6ead6914a10.png" alt="Astro project before migrating to Bun" class="image--center mx-auto" /></p>
<h2 id="heading-install-bun">Install Bun</h2>
<p>Bun ships as a single executable that can be installed in a few different ways. I chose to use the bun.sh install script below.</p>
<pre><code class="lang-bash">curl -fsSL https://bun.sh/install | bash
</code></pre>
<h2 id="heading-verifying-bun-installation"><strong>Verifying Bun Installation</strong></h2>
<p>After installing Bun, it's a good practice to verify that the installation was successful. This ensures that Bun and its accompanying executables are correctly set up in your system.</p>
<p>To confirm that Bun is properly installed, run the following commands:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">which</span> bun
<span class="hljs-built_in">which</span> bunx
</code></pre>
<p>If both commands return valid file paths, that means Bun and BunX are successfully installed and are ready for action.</p>
<h2 id="heading-eliminating-previous-package-managers"><strong>Eliminating Previous Package Managers</strong></h2>
<p>Time to roll up your sleeves! We're going to start by eliminating any traces of the previous package managers from the project. Delete the following files and directories: <code>.npmrc</code>, <code>.nvmrc</code>, <code>pnpm-lock.yaml</code>, and <code>node_modules</code>.</p>
<p>To do this, run the following command:</p>
<pre><code class="lang-bash">rm -fr node_modules .npmrc .nvmrc pnpm-lock.yaml
</code></pre>
<h2 id="heading-install-dependencies-with-bun">Install Dependencies with Bun</h2>
<p>With the old package manager files out of the way, it's time to bring in the new star of the show—Bun. To install all of your project's dependencies using Bun, simply run the following command:</p>
<pre><code class="lang-bash">bun install
</code></pre>
<h2 id="heading-run-astro-dev-with-bun">Run Astro Dev with Bun</h2>
<p>Now that Bun is installed and verified, you can use it to run your Astro development server. <code>bunx</code>, an accompanying tool of Bun, enables you to run package scripts without requiring system-wide installs.</p>
<p>To fire up the Astro development server using <code>bunx</code>, execute the following command:</p>
<pre><code class="lang-bash">bunx --bun astro dev
</code></pre>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">The <code>--bun</code> flag forces the executable to run using Bun's runtime, irrespective of the shebang specified in the script. By default, Bun respects shebangs such as <code>#!/usr/bin/env node</code>, launching a Node.js process to execute the file. The <code>--bun</code> flag ensures the executable runs using Bun's runtime, overriding any shebang in the script.</div>
</div>

<p>If you're following along with the steps, your console output should closely resemble the screenshot below:</p>
<h2 id="heading-update-packagejson-scripts">Update package.json Scripts</h2>
<p>Now that you're running Astro with Bun, you'll want to update your <code>package.json</code> scripts to make this change permanent. Update the <code>scripts</code> section in your <code>package.json</code> to look like the following:</p>
<pre><code class="lang-json"><span class="hljs-string">"scripts"</span>: {
  <span class="hljs-attr">"dev"</span>: <span class="hljs-string">"bunx --bun astro dev"</span>,
  <span class="hljs-attr">"build"</span>: <span class="hljs-string">"astro build"</span>,
  <span class="hljs-attr">"preview"</span>: <span class="hljs-string">"bunx --bun astro preview"</span>,
  <span class="hljs-attr">"astro"</span>: <span class="hljs-string">"bunx --bun astro"</span>
},
</code></pre>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">You may notice that the <code>build</code> script doesn't use Bun. This is intentional. If you're deploying on Vercel, they handle Bun execution for you during the build process. You can read more about how Vercel manages this in their <a target="_new" href="https://vercel.com/docs/deployments/configure-a-build#install-command"><strong>documentation</strong></a>.</div>
</div>

<h2 id="heading-push-to-vercel">Push to Vercel</h2>
<p>After you've made your changes and updated your <code>package.json</code>, commit and push your updates to GitHub. This action will trigger a new build on Vercel.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1694515681745/42eea296-18b1-42ca-816f-79e5b8182037.png" alt="Vercel output for Bun install and build commands" class="image--center mx-auto" /></p>
<p>The screenshot confirms that Vercel recognizes we're using Bun and handles the installation and build process accordingly.</p>
<h2 id="heading-wrapping-it-up">Wrapping It Up</h2>
<p>So, there you have it—switching your Astro project over to Bun is pretty much a breeze. And let's be honest, who wouldn't want the extra speed that Bun brings? Just a few tweaks to your <code>package.json</code>, and you're pretty much set.</p>
<p>The best part? Vercel's added Bun support within 72 hours of launch. Vercel detects that your repository is using Bun and handles the install and build steps for you.</p>
<p>I'm still blown away by Bun and all that it promises, and props to Vercel for seeing it as well.</p>
<p>If you found this helpful, give it a like and follow me here on Hashnode and Twitter at <a target="_blank" href="https://twitter.com/thealexkates"><strong>@thealexkates</strong></a>. Cheers, and happy coding! 🚀</p>
]]></content:encoded></item><item><title><![CDATA[Server-Side Rendering (SSR) with Bun and React]]></title><description><![CDATA[The much-awaited JavaScript Swiss Army knife, Bun, has finally released its 1.0 version, and it's a game-changer. If you're new to the scene, Bun serves as an all-in-one JavaScript runtime and toolkit, engineered for blazing speed. It comes complete ...]]></description><link>https://blog.alexkates.dev/server-side-rendering-ssr-with-bun-and-react</link><guid isPermaLink="true">https://blog.alexkates.dev/server-side-rendering-ssr-with-bun-and-react</guid><category><![CDATA[Bun]]></category><category><![CDATA[React]]></category><category><![CDATA[TypeScript]]></category><dc:creator><![CDATA[Alex Kates]]></dc:creator><pubDate>Sat, 09 Sep 2023 20:05:24 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1694289642931/50e15258-b655-40f1-a4ca-86a8d5cc852c.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The much-awaited JavaScript Swiss Army knife, <a target="_blank" href="https://bun.sh">Bun</a>, has finally released its 1.0 version, and it's a game-changer. If you're new to the scene, Bun serves as an all-in-one JavaScript runtime and toolkit, engineered for blazing speed. It comes complete with a bundler, test runner, native TypeScript and JSX support, and even a Node.js-compatible package manager.</p>
<p>In this guide, we're diving into the world of <a target="_blank" href="http://Bun.sh">Bun</a> 1.0 to unlock its full potential. We'll cover:</p>
<ul>
<li><p>🛠️ Installation process for Bun</p>
</li>
<li><p>🌱 Your first Bun project</p>
</li>
<li><p>🖥️ Creating your inaugural Bun server</p>
</li>
<li><p>🎭 Server-side rendering with Bun streams and React</p>
</li>
<li><p>📦 Fetching 3rd party data and rendering server-side</p>
</li>
</ul>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">You can find all of the code used in this guide at <a target="_blank" href="https://github.com/alexkates/ssr-bun-react">https://github.com/alexkates/ssr-bun-react</a></div>
</div>

<h2 id="heading-project-setup">Project Setup</h2>
<h3 id="heading-install-bun">Install Bun</h3>
<p>You can install Bun alongside your current node installation without messing up any of your other repositories.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Install Bun</span>
curl -fsSL https://bun.sh/install | bash
</code></pre>
<h3 id="heading-initialize-bun-project">Initialize Bun Project</h3>
<p>Next, let's initialize a new Bun project.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Project setup</span>
mkdir bun-httpserver
<span class="hljs-built_in">cd</span> bun-httpserver
bun init
</code></pre>
<p>Using <code>bun init</code> will scaffold out a project, as you can see in the following screenshot. You'll notice a new file, <code>bun.lockb</code>, which takes the place of yarn, npm, or pnpm lock files. Also, <code>index.ts</code> and <code>tsconfig.json</code> are both scaffolded by default, which means TypeScript support is baked right in, and no extra setup is required.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1694276625049/64a3ea05-69b4-4567-bd63-7acdc0fcaa5a.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-your-first-bun-server">Your First Bun Server</h2>
<p>Believe it or not, setting up your first Bun server is remarkably simple. You can get up and running with just a couple of lines of code.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> server = Bun.serve({
  port: <span class="hljs-number">3000</span>,
  fetch(req) {
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Response(<span class="hljs-string">`Bun!`</span>);
  },
});

<span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Listening on http://localhost:<span class="hljs-subst">${server.port}</span> ...`</span>);
</code></pre>
<p>Look a bit closer at each line ...</p>
<ul>
<li><p><code>const server = Bun.serve({ ... });</code>: This line initializes the server using <code>Bun.serve()</code> and sets it to listen on port <code>3000</code>.</p>
</li>
<li><p><code>port: 3000,</code>: Specifies that the server should listen on port <code>3000</code>.</p>
</li>
<li><p><code>fetch(req) { ... }</code>: Defines a function that will handle all incoming HTTP requests. When a request comes in, it returns a new HTTP response with the text "Bun!".</p>
</li>
<li><p><code>return new Response(</code>Bun!<code>);</code>: Creates a new HTTP response object with the text "Bun!".</p>
</li>
<li><p><code>console.log(Listening on http://localhost:${server.port} ...);</code>: Logs a message to the console, indicating that the server is listening. It uses template literals to insert the port number dynamically.</p>
</li>
</ul>
<p>Your entire project should now look like the following screenshot.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1694276719253/a93f80ae-9772-4ab2-a39c-5cd19b9403ee.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-implementing-server-side-rendering-ssr-with-react-and-bun"><strong>Implementing Server-Side Rendering (SSR) with React and Bun</strong></h2>
<p>Now the Real Fun Starts: Implementing Server-Side Rendering (SSR) with React and Bun. In this section, we'll dive into the intricacies of Server-Side Rendering, or SSR as it's often abbreviated, using both React and Bun.</p>
<h3 id="heading-adding-packages-the-bun-way"><strong>Adding Packages the Bun Way</strong></h3>
<p>If you're familiar with yarn, you'll feel right at home here. To add packages in Bun, simply use the <code>add</code> command. Want it as a dev dependency? Just throw in the <code>-d</code> flag.</p>
<pre><code class="lang-bash">bun add react react-dom
bun add @types/react-dom -d
</code></pre>
<h3 id="heading-switching-gears-to-jsx"><strong>Switching Gears to JSX</strong></h3>
<p>Next up, we're going to transition our existing <code>index.ts</code> server file to <code>index.tsx</code>. This allows us to return JSX elements directly.</p>
<pre><code class="lang-bash">mv index.ts index.tsx
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1694277753506/616a679b-3ee9-4ad8-9caf-10b8ab2fdd63.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-diving-into-our-new-indextsx"><strong>Diving into Our New index.tsx</strong></h3>
<p>In this revamped <code>index.tsx</code> file, we're using <code>renderToReadableStream</code> from <code>react-dom/server</code> to render our <code>Pokemon</code> component. We then wrap this stream in a <code>Response</code> object, ensuring the content type is set to "text/html".</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { renderToReadableStream } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-dom/server"</span>;
<span class="hljs-keyword">import</span> Pokemon <span class="hljs-keyword">from</span> <span class="hljs-string">"./components/Pokemon"</span>;

Bun.serve({
  <span class="hljs-keyword">async</span> fetch(request) {

    <span class="hljs-keyword">const</span> stream = <span class="hljs-keyword">await</span> renderToReadableStream(&lt;Pokemon /&gt;);

    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Response(stream, {
      headers: { <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"text/html"</span> },
    });
  },
});

<span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Listening ..."</span>);
</code></pre>
<p>Ok, more is going on here. Let's take a look.</p>
<ul>
<li><p><code>import { renderToReadableStream } from "react-dom/server";</code>: Imports the function <code>renderToReadableStream</code> from the <code>react-dom/server</code> package for server-side React rendering.</p>
</li>
<li><p><code>import Pokemon from "./components/Pokemon";</code>: Imports a React component named <code>Pokemon</code> from a relative file path.</p>
</li>
<li><p><code>Bun.serve({ ... });</code>: Uses the <code>Bun.serve()</code> method to set up an HTTP server. It includes an asynchronous <code>fetch</code> function to handle incoming HTTP requests.</p>
</li>
<li><p><code>async fetch(request) { ... }</code>: An asynchronous function that will be triggered for each HTTP request coming to the server.</p>
</li>
<li><p><code>const stream = await renderToReadableStream(&lt;Pokemon /&gt;);</code>: Asynchronously renders the <code>Pokemon</code> React component to a readable stream.</p>
</li>
<li><p><code>return new Response(stream, { ... });</code>: Returns a new HTTP Response object with the readable stream and sets the "Content-Type" header to "text/html".</p>
</li>
<li><p><code>console.log("Listening ...");</code>: Outputs a message to the console indicating that the server is listening for incoming requests.</p>
</li>
</ul>
<h3 id="heading-crafting-a-streamable-react-component"><strong>Crafting a Streamable React Component</strong></h3>
<p>Finally, we're going to build a straightforward React component. This component will be server-side rendered (SSR) and streamed right back to the client.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1694277764246/9361cd70-919b-451f-b330-dfbcce3fc84c.png" alt class="image--center mx-auto" /></p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> React <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;

<span class="hljs-keyword">type</span> PokemonProps = {
  name?: <span class="hljs-built_in">string</span>;
};

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Pokemon</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> &lt;div&gt;Bun Forrest, Bun!&lt;/div&gt;;
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> Pokemon;
</code></pre>
<h3 id="heading-firing-up-the-bun-serve"><strong>Firing Up the Bun Serve</strong></h3>
<p>Next is the exciting part—let's run our Bun server and see everything come together!</p>
<pre><code class="lang-bash">bun index.tsx
</code></pre>
<p>Navigate to <code>http://localhost:3000</code> and you should see our SSR Pokemon component!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1694277805991/07e1b3c6-1762-4646-963a-53f4ce603f35.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-building-dynamic-routes-with-a-pokemon-twist"><strong>Building Dynamic Routes with a Pokémon Twist</strong></h2>
<p>Ready for something more advanced? In this section, we'll be creating two distinct routes: <code>/pokemon</code> and <code>/pokemon/[pokemonName]</code>.</p>
<ul>
<li><p>Navigating to <code>/pokemon</code> will trigger a fetch request to the Pokémon API, rendering the results as a clickable list of anchor tags.</p>
</li>
<li><p>Clicking any of these anchors takes you to <code>/pokemon/[pokemonName]</code>, where a specific Pokémon is fetched, server-side rendered (SSR), and then streamed back to your client.</p>
</li>
</ul>
<h3 id="heading-a-closer-look-at-our-enhanced-indextsx"><strong>A Closer Look at Our Enhanced index.tsx</strong></h3>
<p>In this updated version, our <code>index.tsx</code> is doing some heavy lifting. It now includes dynamic routing to either show a list of Pokémon fetched from the Pokémon API or to display a specific Pokémon based on the URL. Whether it's the list or an individual Pokémon, the component is server-side rendered and then streamed back to the client.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { PokemonResponse } <span class="hljs-keyword">from</span> <span class="hljs-string">"./types/PokemonResponse"</span>;
<span class="hljs-keyword">import</span> { PokemonsResponse } <span class="hljs-keyword">from</span> <span class="hljs-string">"./types/PokemonsResponse"</span>;
<span class="hljs-keyword">import</span> { renderToReadableStream } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-dom/server"</span>;
<span class="hljs-keyword">import</span> Pokemon <span class="hljs-keyword">from</span> <span class="hljs-string">"./components/Pokemon"</span>;
<span class="hljs-keyword">import</span> PokemonList <span class="hljs-keyword">from</span> <span class="hljs-string">"./components/PokemonList"</span>;

Bun.serve({
  <span class="hljs-keyword">async</span> fetch(request) {
    <span class="hljs-keyword">const</span> url = <span class="hljs-keyword">new</span> URL(request.url);

    <span class="hljs-keyword">if</span> (url.pathname === <span class="hljs-string">"/pokemon"</span>) {
      <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">"https://pokeapi.co/api/v2/pokemon"</span>);

      <span class="hljs-keyword">const</span> { results } = (<span class="hljs-keyword">await</span> response.json()) <span class="hljs-keyword">as</span> PokemonsResponse;

      <span class="hljs-keyword">const</span> stream = <span class="hljs-keyword">await</span> renderToReadableStream(&lt;PokemonList pokemon={results} /&gt;);

      <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Response(stream, {
        headers: { <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"text/html"</span> },
      });
    }

    <span class="hljs-keyword">const</span> pokemonNameRegex = <span class="hljs-regexp">/^\/pokemon\/([a-zA-Z0-9_-]+)$/</span>;
    <span class="hljs-keyword">const</span> match = url.pathname.match(pokemonNameRegex);

    <span class="hljs-keyword">if</span> (match) {
      <span class="hljs-keyword">const</span> pokemonName = match[<span class="hljs-number">1</span>];

      <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">`https://pokeapi.co/api/v2/pokemon/<span class="hljs-subst">${pokemonName}</span>`</span>);

      <span class="hljs-keyword">if</span> (response.status === <span class="hljs-number">404</span>) {
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Response(<span class="hljs-string">"Not Found"</span>, { status: <span class="hljs-number">404</span> });
      }

      <span class="hljs-keyword">const</span> {
        height,
        name,
        weight,
        sprites: { front_default },
      } = (<span class="hljs-keyword">await</span> response.json()) <span class="hljs-keyword">as</span> PokemonResponse;

      <span class="hljs-keyword">const</span> stream = <span class="hljs-keyword">await</span> renderToReadableStream(&lt;Pokemon name={name} height={height} weight={weight} img={front_default} /&gt;);

      <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Response(stream, {
        headers: { <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"text/html"</span> },
      });
    }

    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Response(<span class="hljs-string">"Not Found"</span>, { status: <span class="hljs-number">404</span> });
  },
});

<span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Listening ..."</span>);
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1694280919065/0336a41c-d913-4ce3-8e77-5d79a7b4873c.png" alt class="image--center mx-auto" /></p>
<p>A lot is going on here. Let's dive deeper into the interesting pieces.</p>
<ul>
<li><p><strong>Initialize HTTP Server with Bun</strong>: The <code>Bun.serve()</code> method sets up an HTTP server and specifies an asynchronous <code>fetch</code> function to handle incoming requests, effectively acting as the entry point for all HTTP traffic.</p>
</li>
<li><p><strong>Route for All Pokémon</strong>: When the URL path is <code>/pokemon</code>, the server fetches a list of Pokémon from an external API and renders a <code>PokemonList</code> React component to HTML. This HTML is then sent back to the client.</p>
</li>
<li><p><strong>Route for Specific Pokémon</strong>: The code uses a Regular Expression to match URL paths that specify a particular Pokémon's name (e.g., <code>/pokemon/pikachu</code>). If such a path is detected, the server fetches details for that specific Pokémon and renders it using the <code>Pokemon</code> React component.</p>
</li>
<li><p><strong>Server-Side React Rendering</strong>: For both the general and specific Pokémon routes, the <code>renderToReadableStream</code> function converts React components to a readable stream, which is then returned as an HTML response.</p>
</li>
<li><p><strong>Error Handling</strong>: The code includes specific handling for 404 errors. If a Pokémon is not found in the API or if the URL doesn't match any expected routes, a "Not Found" message is returned with a 404 status code.</p>
</li>
</ul>
<h3 id="heading-the-pokemonlist-component"><strong>The PokemonList</strong> Component</h3>
<p>This component is taking the list of Pokemon and turning them into clickable list items. Each list item is an anchor tag that routes the user to <code>/pokemon/[name]</code> when clicked, rendering individual Pokémon details.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> React <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">PokemonList</span>(<span class="hljs-params">{ pokemon }: { pokemon: { name: <span class="hljs-built_in">string</span>; url: <span class="hljs-built_in">string</span> }[] }</span>) </span>{
  <span class="hljs-keyword">return</span> (
    &lt;ul&gt;
      {pokemon.map(<span class="hljs-function">(<span class="hljs-params">{ name }</span>) =&gt;</span> (
        &lt;li key={name}&gt;
          &lt;a href={<span class="hljs-string">`/pokemon/<span class="hljs-subst">${name}</span>`</span>}&gt;{name}&lt;/a&gt;
        &lt;/li&gt;
      ))}
    &lt;/ul&gt;
  );
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> PokemonList;
</code></pre>
<h3 id="heading-the-pokemon-component">The Pokemon Component</h3>
<p>The Pokemon component is responsible for taking an individual Pokemon's height, weight, name, and image URL, and returning exactly how we want to display a single Pokemon.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> React <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Pokemon</span>(<span class="hljs-params">{ height, weight, name, img }: { height: <span class="hljs-built_in">number</span>; weight: <span class="hljs-built_in">number</span>; name: <span class="hljs-built_in">string</span>; img: <span class="hljs-built_in">string</span> }</span>) </span>{
  <span class="hljs-keyword">return</span> (
    &lt;div&gt;
      &lt;h1&gt;{name}&lt;/h1&gt;
      &lt;img src={img} alt={name} /&gt;
      &lt;p&gt;Height: {height}&lt;/p&gt;
      &lt;p&gt;Weight: {weight}&lt;/p&gt;
    &lt;/div&gt;
  );
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> Pokemon;
</code></pre>
<h3 id="heading-re-running-the-server-with-hmr">Re-running the Server with HMR</h3>
<p>Time to restart our server, but this time let's add the <code>--watch</code> flag for Hot Module Reloading (HMR). Good news—Bun has us covered, so you can say goodbye to <code>nodemon</code>.</p>
<pre><code class="lang-bash">bun --watch index.tsx
</code></pre>
<h3 id="heading-dynamic-routes-in-action">Dynamic Routes in Action</h3>
<p>The first screenshot shows you what happens when you navigate to <code>/pokemon</code>. As you can see, a list of Pokémon appears, each one being a clickable link. All this is happening thanks to our <code>PokemonList</code> component, which fetches and displays clickable names.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1694281355726/55446f14-0c17-45b9-a8b6-12291fbb5a66.png" alt class="image--center mx-auto" /></p>
<p>The second screenshot takes us to <code>/pokemon/charmander</code>. This time, our <code>Pokemon</code> component takes center stage, showing Charmander's height, weight, and an image—everything beautifully server-side rendered, of course.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1694281364469/2f6ebe73-2655-4a73-88d7-f66731580d56.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-thats-all-folks">That's All, Folks!</h2>
<p>If you've been coding along, give yourself a pat on the back! You've just:</p>
<ul>
<li><p>🛠️ Installed and initialized a shiny new Bun project</p>
</li>
<li><p>🌐 Created your very own HTTP server</p>
</li>
<li><p>🖼️ Utilized Server-Side Rendering (SSR) to stream a simple React component</p>
</li>
<li><p>🗺️ Constructed two distinct routes that fetch data and SSR different React components</p>
</li>
</ul>
<p>Feel free to leave a comment or hit that like button if you found this useful or entertaining and follow me on <a target="_blank" href="https://twitter.com/thealexkates">Twitter</a>!</p>
<p>Until next time, happy coding! 🚀</p>
]]></content:encoded></item><item><title><![CDATA[Yes, You Should Use TypeScript]]></title><description><![CDATA[The developer community has been set abuzz by DHH's recent Twitter announcement: Turbo 8 will not be using TypeScript. This has reignited a classic debate among developers—TypeScript or JavaScript?
https://twitter.com/dhh/status/1699427078586716327
 ...]]></description><link>https://blog.alexkates.dev/yes-you-should-use-typescript</link><guid isPermaLink="true">https://blog.alexkates.dev/yes-you-should-use-typescript</guid><category><![CDATA[TypeScript]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[software development]]></category><dc:creator><![CDATA[Alex Kates]]></dc:creator><pubDate>Thu, 07 Sep 2023 19:45:50 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1694115811692/3ec5169a-3636-41ca-be22-2ae48a440d0c.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The developer community has been set abuzz by DHH's recent Twitter announcement: Turbo 8 will not be using TypeScript. This has reignited a classic debate among developers—TypeScript or JavaScript?</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://twitter.com/dhh/status/1699427078586716327">https://twitter.com/dhh/status/1699427078586716327</a></div>
<p> </p>
<p>While the question may seem simple—"Are types really necessary?"—it's a multi-faceted issue that deserves deeper exploration. With that said, let me make a case for TypeScript, a technology that many, including myself, believe brings undeniable advantages to the development experience.</p>
<h2 id="heading-types-exist-the-unavoidable-truth"><strong>Types Exist: The Unavoidable Truth</strong></h2>
<p>Firstly, whether you're an ardent TypeScript supporter or a dyed-in-the-wool JavaScript developer, the undeniable truth is types exist. They form the framework of our coding logic, governing how we interact with variables, invoke functions, and even structure entire programs. Now, the question isn't really about the existence of types, but rather, when do you want to know about type errors?</p>
<h2 id="heading-catch-errors-early-with-typescript"><strong>Catch Errors Early with TypeScript</strong></h2>
<p>TypeScript provides the benefit of compile-time type checking. Imagine knowing you have spinach in your teeth before walking into a meeting rather than finding out afterwards. That's what TypeScript offers—a chance to catch and rectify errors early, during development, saving you from the embarrassment of runtime crashes or even worse, production failures.</p>
<p>Sure, you can write unit tests in JavaScript to catch type issues. But then you find yourself asking: why not just use TypeScript in the first place and catch these issues even earlier?</p>
<h2 id="heading-the-rich-developer-experience"><strong>The Rich Developer Experience</strong></h2>
<p>Ever had your GPS guide you smoothly around traffic, and thought, "Wow, what did we do before this?" That's what working with TypeScript feels like. The rich IDE support with features like Intellisense and autocomplete not only speeds up the development process but also makes it more accurate and efficient.</p>
<p>Matt Pocock sums this up beautifully in this tweet. The DX of TypeScript is so good that 70% of respondents in the State of JS 2022 said that they use TypeScript over JavaScript.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://twitter.com/mattpocockuk/status/1699507864895738143">https://twitter.com/mattpocockuk/status/1699507864895738143</a></div>
<p> </p>
<h2 id="heading-one-language-to-rule-them-all"><strong>One Language to Rule Them All</strong></h2>
<p>The power of TypeScript shines exceptionally bright when you're dealing with monorepos that include web apps, mobile platforms, APIs, data layers, and even infrastructure as code. Why? Because TypeScript allows you to use one language across your entire stack, and in a type-safe way. This is nothing short of a superpower in the realm of development. Think about it: consistency, reduced context switching, and increased productivity—TypeScript offers all these benefits and more.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://twitter.com/thealexkates/status/1699569174551417187">https://twitter.com/thealexkates/status/1699569174551417187</a></div>
<p> </p>
<p>I can't imagine, in 2023, building software outside of the TypeScript/monorepo ecosystem. Once you've experienced being able to build a feature, from client to server to infrastructure to database, all in the same repository, in a fully type-safe way, there's no going back.</p>
<h2 id="heading-community-reactions">Community Reactions</h2>
<p>The tech community's response to this development has been nothing short of enthusiastic, with many individuals sharing their thoughts and experiences. Here are a few of the most compelling and insightful reactions that I found to be particularly interesting:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://www.youtube.com/watch?v=FnmZhXWohP0">https://www.youtube.com/watch?v=FnmZhXWohP0</a></div>
<p> </p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://twitter.com/giuseppelt/status/1699500387466633307">https://twitter.com/giuseppelt/status/1699500387466633307</a></div>
<p> </p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://twitter.com/ThePrimeagen/status/1699815789497319668">https://twitter.com/ThePrimeagen/status/1699815789497319668</a></div>
<p> </p>
<h2 id="heading-conclusion"><strong>Conclusion</strong></h2>
<p>While DHH's announcement about dropping TypeScript from Turbo 8 has sparked renewed debate, it also provides an opportunity for reflection. TypeScript isn't just a trend or a buzzword; it's a tool that offers tangible advantages such as early error detection, a more robust IDE experience, and a unified, type-safe language for monorepos.</p>
<p>So, coming back to our original question—do we really need types? With TypeScript, the answer seems to be a resounding "Yes, and they offer so much more!" Whether you view it as a safety net or an enabling superpower, TypeScript stands out as an incredibly valuable tool for modern development.</p>
]]></content:encoded></item><item><title><![CDATA[VTL Quote Escaping for AWS API Gateway and Kinesis Integration]]></title><description><![CDATA[Introduction
AWS provides a wide range of services that, when combined, can produce powerful results. One such combination is the direct integration of API Gateway with Kinesis, although it comes with its own set of challenges. In this post, we will ...]]></description><link>https://blog.alexkates.dev/vtl-quote-escaping-for-aws-api-gateway-and-kinesis-integration</link><guid isPermaLink="true">https://blog.alexkates.dev/vtl-quote-escaping-for-aws-api-gateway-and-kinesis-integration</guid><category><![CDATA[AWS]]></category><dc:creator><![CDATA[Alex Kates]]></dc:creator><pubDate>Fri, 01 Sep 2023 11:42:36 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1693567643138/b883d3cf-11f3-48ce-983e-94992a20dd1b.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction">Introduction</h2>
<p>AWS provides a wide range of services that, when combined, can produce powerful results. One such combination is the direct integration of API Gateway with Kinesis, although it comes with its own set of challenges. In this post, we will discuss a common issue developers encounter when working with VTL transformations for this integration: escaping VTL quotes.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://twitter.com/thealexkates/status/1696879825653293468">https://twitter.com/thealexkates/status/1696879825653293468</a></div>
<p> </p>
<p>Utilizing API Gateway to direct payloads into a Kinesis stream is a valuable configuration in event-driven architectures. By removing intermediaries like Lambda, you achieve a more efficient workflow. However, the trade-off is that you must use VTL in API Gateway to ensure the payload meets Kinesis's requirements.</p>
<h2 id="heading-our-cdk-stack">Our CDK Stack</h2>
<p>The Cloud Development Kit (CDK) provides a high-level, object-oriented abstraction over AWS CloudFormation. For direct integration, the CDK stack establishes an API Gateway endpoint that is directly integrated with a Kinesis stream:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Stack, App } <span class="hljs-keyword">from</span> <span class="hljs-string">'@aws-cdk/core'</span>;
<span class="hljs-keyword">import</span> { RestApi, Integration, PassthroughBehavior } <span class="hljs-keyword">from</span> <span class="hljs-string">'@aws-cdk/aws-apigateway'</span>;
<span class="hljs-keyword">import</span> { Stream } <span class="hljs-keyword">from</span> <span class="hljs-string">'@aws-cdk/aws-kinesis'</span>;

<span class="hljs-keyword">class</span> ApiGatewayKinesisIntegrationStack <span class="hljs-keyword">extends</span> Stack {
  <span class="hljs-keyword">constructor</span>(<span class="hljs-params">scope: App, id: <span class="hljs-built_in">string</span></span>) {
    <span class="hljs-built_in">super</span>(scope, id);

    <span class="hljs-keyword">const</span> kinesisStream = <span class="hljs-keyword">new</span> Stream(<span class="hljs-built_in">this</span>, <span class="hljs-string">'MyKinesisStream'</span>);

    <span class="hljs-keyword">const</span> api = <span class="hljs-keyword">new</span> RestApi(<span class="hljs-built_in">this</span>, <span class="hljs-string">'MyApi'</span>, {
      restApiName: <span class="hljs-string">'Kinesis Integration Service'</span>,
    });

    <span class="hljs-keyword">const</span> kinesisIntegration = <span class="hljs-keyword">new</span> Integration({
      <span class="hljs-keyword">type</span>: IntegrationType.AWS,
      options: {
        passthroughBehavior: PassthroughBehavior.NEVER,
        requestTemplates: {
          <span class="hljs-string">'application/json'</span>: <span class="hljs-string">`{
            "StreamName": "YourKinesisStreamName",
            "Data": "WHAT SHOULD I DO HERE???", 
            "PartitionKey": "yourPartitionKey"
          }`</span>,
        },
        integrationResponses: [{
          statusCode: <span class="hljs-string">'200'</span>,
        }],
      },
      integrationHttpMethod: <span class="hljs-string">'POST'</span>,
      uri: <span class="hljs-string">`arn:aws:apigateway:your-region:kinesis:action/PutRecord`</span>,
    });

    api.root.addMethod(<span class="hljs-string">'ANY'</span>, kinesisIntegration, { apiKeyRequired: <span class="hljs-literal">true</span> });
  }
}
</code></pre>
<p>This stack creates a Kinesis stream and an API Gateway endpoint. The endpoint employs direct integration with Kinesis. The <code>requestTemplates</code> section is where the VTL transformation occurs. Pay attention to the Data property in <code>requestTemplates</code>, as this is our primary focus.</p>
<h2 id="heading-wrapping-the-request-body-and-the-vtl-quote-escaping-challenge"><strong>Wrapping the Request Body and the VTL Quote Escaping Challenge</strong></h2>
<p>When directly integrating API Gateway with Kinesis using VTL, a frequent requirement is to wrap the request body in a new object structure. This may be to add metadata, contextual information, or simply reformat the payload for consistent processing downstream. But this nesting of JSON brings its own challenges due to VTL's unique way of handling quotes.</p>
<p>Imagine our first naive attempt to construct the payload:</p>
<pre><code class="lang-plaintext">#set($data = $input.json('$'))
{
  "StreamName": "YourKinesisStreamName",
  "Data": "$util.base64Encode('{type: $context.resourcePath, data: $data}')",
  "PartitionKey": "yourPartitionKey"
}
</code></pre>
<p>It's evident from the above that while we're attempting to wrap the payload, the mishandling of quotes will throw us into issues.</p>
<p>A slightly improved, but still flawed, attempt could look something like:</p>
<pre><code class="lang-plaintext">#set($data = $input.json('$'))
{
  "StreamName": "YourKinesisStreamName",
  "Data": "$util.base64Encode('{"type": "$context.resourcePath", "data": "$data"}')",
  "PartitionKey": "yourPartitionKey"
}
</code></pre>
<p>Here, we are trying to nest the JSON correctly. But again, this will fail due to how quotes are mishandled.</p>
<h2 id="heading-a-solution-emerges">A Solution Emerges</h2>
<p>Those familiar with other languages might expect a backslash (<code>\</code>) to be the go-to for escaping, but in VTL, we escape double quotes with another double quote (<code>""</code>).</p>
<pre><code class="lang-plaintext">#set($data = $input.json('$'))
#set($type = $context.resourcePath)

{
  "StreamName": "YourKinesisStreamName",
  "Data": "$util.base64Encode("{""type"": ""$type"", ""data"": $data}")",
  "PartitionKey": "yourPartitionKey"
}
</code></pre>
<p>By properly managing our quotes, the JSON is correctly structured, and we avoid transformation errors.</p>
<h3 id="heading-conclusion"><strong>Conclusion</strong></h3>
<p>The combination of API Gateway with Kinesis offers a fast, direct method of ingesting events into your stream. Yet, the intricacies of VTL can sometimes be a stumbling block. By understanding the subtleties of quote handling and the need for nested JSON structures, you can ensure smooth data flow into Kinesis. Happy streaming!</p>
]]></content:encoded></item><item><title><![CDATA[Turning Obstacles Into Opportunities]]></title><description><![CDATA[Introduction
For many years, I had heard about the ancient philosophy of Stoicism, but I never took the time to dive deeper into its teachings. That is, until the Summer of 2022, when I attended Michael McGill's Practical Stoicism online course. From...]]></description><link>https://blog.alexkates.dev/turning-obstacles-into-opportunities</link><guid isPermaLink="true">https://blog.alexkates.dev/turning-obstacles-into-opportunities</guid><category><![CDATA[stoicism]]></category><category><![CDATA[crisis]]></category><category><![CDATA[learning]]></category><dc:creator><![CDATA[Alex Kates]]></dc:creator><pubDate>Thu, 16 Mar 2023 21:16:40 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1679001336819/e4ba1abc-e294-42ad-a743-0504f81339cd.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction">Introduction</h2>
<p>For many years, I had heard about the ancient philosophy of Stoicism, but I never took the time to dive deeper into its teachings. That is, until the Summer of 2022, when I attended <a target="_blank" href="https://twitter.com/mcgillmd921">Michael McGill's</a> Practical Stoicism online course. From that moment on, I was captivated by the philosophy's practical approach to life's challenges. I was completely hooked on the Stoic ideas of the dichotomy of control, Amor Fati, and Memento Mori.</p>
<p>As I continued my exploration of Stoicism, I stumbled upon several books by Ryan Holiday, including my favorite so far, <a target="_blank" href="https://www.goodreads.com/book/show/18668059-the-obstacle-is-the-way">The Obstacle Is The Way</a>. This book spoke to me on a deep level and provided me with valuable insights on how to navigate life's challenges.</p>
<p>In this post, I want to share with you my three main takeaways from the book and how each one helped me during a recent crisis.</p>
<h2 id="heading-the-crisis">The Crisis</h2>
<p>Recently, during a heavy rainstorm, I experienced a real-life crisis that put my Stoic training to the test. My basement flooded due to a clogged main drain, and I found myself in crisis management mode for hours on end. I had to continuously shop vacuum water into buckets and carry them upstairs and outside to prevent my belongings from being damaged. To make matters worse, I was dealing with a nasty head cold at the same time.</p>
<p>Despite the overwhelming nature of the situation, I tried to remain calm and focused, thinking "how would a Stoic handle this?". I reminded myself that I couldn't control the rain or the drain blockage, but I could control my response to the situation. I also reflected on the Stoic idea of Memento Mori, or the acceptance of our mortality, and realized that this crisis was a reminder that life is fragile and unpredictable.</p>
<p>After calling several emergency plumbing services, I finally found one that could come out to fix the blockage. Although I was taken aback by the cost of the emergency repair, I reminded myself of the Stoic principle of accepting what we cannot change and focusing on what we can control. After several hours, hundreds of dollars, and a lot of hard work, the crisis was finally over.</p>
<p>In the following sections, I will share my three main takeaways from The Obstacle Is The Way and how each one helped me during this crisis.</p>
<h2 id="heading-develop-a-different-perspective-on-obstacles">Develop a different perspective on obstacles</h2>
<p>It's very easy to panic during a crisis. We are kind of hard-wired to either fight or flight. It takes a ton of practice and presence to create a bit of space between a thing happening and your reaction to it. I've learned that cultivating a mindset of resilience, optimism, and focus is key to overcoming obstacles. By taking a step back and looking at the situation objectively, we can avoid being consumed by emotions and make more effective decisions.</p>
<p>When my basement flooded, I found myself struggling to keep a positive outlook. But then I remembered that perspective is everything, especially when dealing with obstacles. That's when I had a lightbulb moment: my house is over 100 years old and the basement walls were crumbling with dust and rubble all around. So, instead of feeling defeated, I decided to shift my perspective and see the flood as an opportunity to finally give my basement the cleaning it desperately needed. It's amazing how a small change in perspective can turn a seemingly negative situation into a positive one</p>
<h2 id="heading-take-action-and-persevere">Take action and persevere</h2>
<p>Ryan Holiday encourages us to set clear and specific goals and make a plan to achieve them. It's important to break down big goals into smaller, manageable steps, and to stay focused on what needs to be done right now instead of getting overwhelmed by the bigger picture. By doing this, we can take action and persist through any challenge. Looking at my situation through this lens presented some interesting things.</p>
<p>First, this was a big obstacle, requiring attention to water removal, finding an expert, and researching the root cause all at the same time. Chunkifying these problems made thinking about them easier. For example, I spent 30 minutes only dealing with the water flowing in. By focusing on something I could immediately control, I was able to get a small win and a little momentum, which helped my outlook on the situation.</p>
<p>Next, after some time to think, it occurred to me that this problem was beyond what I can do and I would need an expert. Now that I had a system in place for water removal, my next problem was finding an emergency plumber late on a Friday night in a rain storm.</p>
<h2 id="heading-embrace-obstacles-as-opportunities-for-growth"><strong>Embrace obstacles as opportunities for growth</strong></h2>
<p>The Stoics believed that obstacles were not something to be avoided or ignored, but rather embraced as opportunities for growth and development. Ryan Holiday echoes this sentiment in his book, encouraging readers to view obstacles as a means of learning and becoming stronger.</p>
<p>During my basement flood crisis, I realized that this was a chance for me to practice Stoicism in real life. The emergency plumber quoted me $500 to fix the issue. My initial reaction was that this was expensive. But then I tried a different perspective. I looked at this as an opportunity to pay for a real-life lesson. I agreed, and I watched every single thing this plumber did and asked questions along the way. I learned how to snake the main line leaving the house in case this ever were to happen again.</p>
<p>In addition, I learned more about how my house functions and what I can do to prevent similar situations from happening in the future. I also gained a new sense of confidence and self-reliance in dealing with unexpected events. I think changing perspective is exactly what the Stoics meant when they talked about turning obstacles into opportunities.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Ryan Holiday's book, The Obstacle Is The Way, offers practical advice and inspiration for dealing with life's challenges. Through the lens of Stoicism, Holiday encourages readers to develop a different perspective on obstacles, take action and persevere, and embrace obstacles as opportunities for growth.</p>
<p>My experience with the basement flood crisis was a perfect example of how these three takeaways can be applied in real life. By shifting my perspective, taking action, and embracing the obstacle, I was able to turn a negative situation into a positive one and learn valuable lessons along the way.</p>
]]></content:encoded></item><item><title><![CDATA[Introducing FitGPT]]></title><description><![CDATA[TL;DR 📚
I've launched https://fitgpt.xyz, which offers an AI-powered Fitness Coach capable of creating specialized workout routines and personalized meal plans tailored to your current fitness level and future goals.
Intro 👋
Are you feeling bored w...]]></description><link>https://blog.alexkates.dev/introducing-fitgpt</link><guid isPermaLink="true">https://blog.alexkates.dev/introducing-fitgpt</guid><category><![CDATA[Next.js]]></category><category><![CDATA[side project]]></category><category><![CDATA[AI]]></category><dc:creator><![CDATA[Alex Kates]]></dc:creator><pubDate>Mon, 06 Mar 2023 14:06:14 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1678110030931/2ddaf504-d40f-4fa1-ad38-1b5092fcbdac.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-tldr">TL;DR 📚</h1>
<p>I've launched <a target="_blank" href="https://fitgpt.xyz"><strong>https://fitgpt.xyz</strong></a>, which offers an AI-powered Fitness Coach capable of creating specialized workout routines and personalized meal plans tailored to your current fitness level and future goals.</p>
<h1 id="heading-intro">Intro 👋</h1>
<p>Are you feeling bored with your fitness and nutrition routine? If your workout regimen has lost its excitement and your meals have become repetitive, it may be time to switch things up.</p>
<p>Introducing <a target="_blank" href="https://fitgpt.xyz">FitGPT</a>! Your AI-Powered Fitness Coach.</p>
<p><a target="_blank" href="https://fitgpt.xyz">FitGPT</a> is a fitness companion that tailors to your unique needs, no matter where you are on your fitness journey. By leveraging the power of AI, <a target="_blank" href="https://fitgpt.xyz">FitGPT</a> creates personalized workout routines and meal plans that take into account your current fitness level and goals, ensuring that you get the most out of your fitness routine.</p>
<p>I'm building <a target="_blank" href="https://fitgpt.xyz">FitGPT</a> to solve two specific issues I saw I was personally having with my Fitness journey.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://twitter.com/thealexkates/status/1632378237690032136">https://twitter.com/thealexkates/status/1632378237690032136</a></div>
<p> </p>
<p>To start, I have over 10 years of experience building my own fitness routines and have experimented with numerous workout programs over time. However, I found that my routine became stagnant and I wanted a simple way to create fresh routines on demand.</p>
<p>Additionally, I am passionate about nutrition and think it's super important to be consistent. I aim to eat whole, nutritious foods for at least 6 days out of the week. However, I have become increasingly bored with my current meal options.</p>
<p>Whenever I sought out new recipes on the internet, I found myself sifting through excessive blog posts and advertisements before finally arriving at the recipe. It was simply frustrating.</p>
<p>This is why I'm building <a target="_blank" href="https://fitgpt.xyz">FitGPT</a>.</p>
<h1 id="heading-features">Features 🔍</h1>
<p><a target="_blank" href="https://fitgpt.xyz">FitGPT</a> is just launching and already has several important features to help you on your fitness journey including ...</p>
<h2 id="heading-routines">Routines 💪</h2>
<p>The "Create Routines" feature in <a target="_blank" href="https://fitgpt.xyz">FitGPT</a> makes it easier than ever for users to create personalized workout routines tailored to their specific needs and goals. This feature uses the same AI technology powering ChatGPT, which considers the user's fitness experience, equipment availability, time constraints, and workout goals. <a target="_blank" href="https://fitgpt.xyz">FitGPT</a> combines details about your current fitness level with AI to generate a custom workout plan that is both effective and enjoyable.</p>
<p>Not only does the "Create Routines" feature generate a fresh workout routine every time, but it also provides the user with a beautifully formatted plan that is easy to follow. The workout plan includes a calorie estimate along with clear instructions on each exercise and the number of sets and reps to complete.</p>
<p>Users can save their workout routines and access them at any time, making it easy to stay on track with their fitness goals. With this feature, users can take the guesswork out of creating a workout routine and focus on getting the most out of their workouts.</p>
<h2 id="heading-meals">Meals 🍜</h2>
<p><a target="_blank" href="https://fitgpt.xyz">FitGPT</a>'s "Generate Meals" feature is an awesome tool for anyone looking to make healthy eating a breeze. It considers your dietary preferences, nutritional requirements, and cooking abilities to develop a custom meal plan just for you.</p>
<p>When you use the "Generate Meals" feature, it asks you a few questions like "What ingredients do you have?" and "What are your dietary restrictions?" It uses your answers to create meals that are not only delicious but also personalized to your needs. It takes into account factors like food allergies, dietary preferences, and calorie goals to develop dishes that are both satisfying and healthy.</p>
<p>One of the greatest benefits of the "Generate Meals" feature is its ability to eliminate the stress of meal planning. With this tool, you can access on-demand meals that are tailored to your dietary requirements and preferences. You'll get step-by-step instructions, an ingredient list, and calorie and macro estimates. Whether you're looking for breakfast, lunch, or dinner ideas, <a target="_blank" href="https://fitgpt.xyz">FitGPT</a> has got you covered with healthy, personalized meals at your fingertips.</p>
<h1 id="heading-technology">Technology 🤖</h1>
<p><a target="_blank" href="https://fitgpt.xyz">FitGPT</a> is powered by some of my favorite web development technologies. It's been so fun diving deep into all of these different technologies.</p>
<h2 id="heading-open-aihttpsplatformopenaicomdocsintroduction"><a target="_blank" href="https://platform.openai.com/docs/introduction">Open AI</a></h2>
<p>At the heart of <a target="_blank" href="https://fitgpt.xyz">FitGPT</a> is the integration with Open AI. Powered by the new <a target="_blank" href="https://openai.com/blog/introducing-chatgpt-and-whisper-apis">gpt-3.5-turbo model</a>, <a target="_blank" href="https://fitgpt.xyz">FitGPT</a> is able to create personalized routines and meals nearly instantaneously.</p>
<h2 id="heading-vercelhttpvercelcom"><a target="_blank" href="http://vercel.com">Vercel</a></h2>
<p>Vercel is the platform of choice for <a target="_blank" href="https://fitgpt.xyz">FitGPT</a>. It provides build, deployment, hosting, environment management, API management, and so much more. I even upgraded to the <a target="_blank" href="https://vercel.com/pricing">Pro Plan</a> for the extra bandwidth and function requests.</p>
<h2 id="heading-nextjshttpsnextjsorg"><a target="_blank" href="https://nextjs.org/">Next.js</a></h2>
<p>I absolutely love Next.js. I think it's the best web development framework I've ever seen. <a target="_blank" href="https://fitgpt.xyz">FitGPT</a> is using <a target="_blank" href="https://nextjs.org/blog/next-13">Next.js 13</a> and utilizes several new features including the new next/image and next/link.</p>
<h2 id="heading-nextauthjshttpsnext-authjsorg"><a target="_blank" href="https://next-auth.js.org/">NextAuth.js</a></h2>
<p>Authentication can be a real bear when spinning up a new project. NextAuth.js makes it super easy to get going, and it lets me own my Auth data. I'm using the DynamoDB provider along with the Google adapter, with plans to introduce other ways to login in the future.</p>
<h2 id="heading-typescripthttpswwwtypescriptlangorg"><a target="_blank" href="https://www.typescriptlang.org/">TypeScript</a></h2>
<p>Not much to say here other than it's 2023 and I've been using TypeScript by default for about 2 years.</p>
<h2 id="heading-dynamodbhttpsawsamazoncomdynamodb"><a target="_blank" href="https://aws.amazon.com/dynamodb/">DynamoDB</a></h2>
<p>I love, love, love DynamoDB, especially single-table design. I can't overstate how much simpler it is to store data for a transactional-based web application.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://twitter.com/thealexkates/status/1630634315909398528">https://twitter.com/thealexkates/status/1630634315909398528</a></div>
<p> </p>
<h2 id="heading-tailwind-csshttpstailwindcsscom"><a target="_blank" href="https://tailwindcss.com/">Tailwind CSS</a></h2>
<p>Tailwind CSS is a must-have for me when it comes to UI development.</p>
<h2 id="heading-daisyuihttpsdaisyuicom"><a target="_blank" href="https://daisyui.com/">DaisyUI</a></h2>
<p>I just discovered DaisyUI and fell in love with it. Simply put, it's delightful to work with. With DaisyUI, I feel like I have all of the power with Tailwind CSS plus some really awesome pre-built components.</p>
<h1 id="heading-outro">Outro</h1>
<p>So, to sum it up, <a target="_blank" href="https://fitgpt.xyz">FitGPT</a> is the ultimate fitness companion that is perfect for anyone at any point in their fitness journey. Using AI, <a target="_blank" href="https://fitgpt.xyz">FitGPT</a> creates personalized workout plans and meal ideas based on your fitness level and goals, making sure you get the most out of your fitness journey.</p>
<p>I hope you try <a target="_blank" href="https://fitgpt.xyz">FitGPT</a> and find it as useful as it has been for me!</p>
]]></content:encoded></item><item><title><![CDATA[MacBook Pro M1 Developer Setup 2022]]></title><description><![CDATA[I just picked up a new MacBook Pro M1 and decided to take this opportunity to document all of the various applications that I use on a daily basis.
Brave
My go to browser for the past year has been Brave. As a web developer, it offers me everything t...]]></description><link>https://blog.alexkates.dev/macbook-pro-m1-developer-setup-2022</link><guid isPermaLink="true">https://blog.alexkates.dev/macbook-pro-m1-developer-setup-2022</guid><category><![CDATA[software development]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[Alex Kates]]></dc:creator><pubDate>Sat, 14 May 2022 14:28:34 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1652538395478/JD-kc8QKJ.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I just picked up a new MacBook Pro M1 and decided to take this opportunity to document all of the various applications that I use on a daily basis.</p>
<h1 id="heading-brave">Brave</h1>
<p>My go to browser for the past year has been <a target="_blank" href="https://brave.com/">Brave</a>. As a web developer, it offers me everything that Chrome offers, but seems to run more efficiently and I <a target="_blank" href="https://brave.com/brave-rewards/">earn rewards simply by browsing</a></p>
<h1 id="heading-notion">Notion</h1>
<p><a target="_blank" href="https://www.notion.so/">Notion</a> is my second brain. This is where I organize all of my thoughts, ideas, personal projects, todo lists, work, finances, etc.</p>
<h1 id="heading-alfred">Alfred</h1>
<p><a target="_blank" href="https://www.alfredapp.com/">Alfred</a> is a better Spotlight. I don't even use the dock anymore. Everything I do on my MacBook starts with CMD+SPACE. You can configure Alfred to index your entire hard-drive, which makes searching and opening individual files super easy.</p>
<h1 id="heading-slack">Slack</h1>
<p>All work comms go through <a target="_blank" href="https://slack.com/">Slack</a>. It's basically replaced email for me.</p>
<h1 id="heading-spectacle">Spectacle</h1>
<p>I used to be a Windows dev and one thing I immediately missed when switching to a MacBook was window snapping. <a target="_blank" href="https://www.spectacleapp.com/">Spectacle</a> is the missing window snapping tool for MacBook. I use CMD + OPTION + ← OR → to snap windows to the left or right of my screen.</p>
<h1 id="heading-visual-studio-code">Visual Studio Code</h1>
<p><a target="_blank" href="https://code.visualstudio.com/">Visual Studio Code</a> has everything I need to work full-stack. I do all of my coding from this single IDE.</p>
<h2 id="heading-extensions">Extensions</h2>
<ul>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=vscode-icons-team.vscode-icons">vscode-icons</a></li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode">prettier-vscode</a></li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens">gitlens</a></li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=dsznajder.es7-react-js-snippets">es7-snippets</a></li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=MaoSantaella.night-wolf">night wolf theme</a></li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=GitHub.copilot">github-copilot</a></li>
</ul>
<h1 id="heading-canva">Canva</h1>
<p>I have literally zero artistic ability. <a target="_blank" href="https://www.canva.com/download/mac/">Canva</a> makes it super simple to create graphics for blog posts, Twitter, etc.</p>
<h1 id="heading-spotify">Spotify</h1>
<p>Let's be real, if you aren't jamming out <a target="_blank" href="https://www.spotify.com/us/download/mac/">on Spotify</a> while coding, are you even coding? Personally, I love LOFI. Haters may hate.</p>
<h1 id="heading-iterm2">ITerm2</h1>
<p><a target="_blank" href="https://iterm2.com/">ITerm2</a> is the must-have terminal upgrade. I've been using it for years. It includes features such as autocomplete, search, and many other configuration settings.</p>
<h1 id="heading-oh-my-zsh">Oh My Zsh</h1>
<p><a target="_blank" href="https://ohmyz.sh/">Oh My Zsh</a> is an open source, community-driven framework for managing your Zsh configuration. It did for the terminal what React did for JavaScript. It basically gives your terminal a framework within which you can customize everything.</p>
<h1 id="heading-powerlevel10k">Powerlevel10k</h1>
<p>Ever wonder how content creators make their terminal look almost futuristic? Check out <a target="_blank" href="https://github.com/romkatv/powerlevel10k#oh-my-zsh">Powerlevel10k</a>. It has a great configuration wizard, performance, and customizations to make your terminal awesome.</p>
<h1 id="heading-figio">Fig.io</h1>
<p><a target="_blank" href="https://fig.io/">Fig.io</a> adds IDE-style autocomplete to your existing terminal. I use tab-completion all the time, and Fig brings this to a whole other level.</p>
<h1 id="heading-aws-cli">AWS CLI</h1>
<p>I do a ton of work in AWS, and the majority of my interactions are done via <a target="_blank" href="https://aws.amazon.com/cli/">the AWS CLI</a>.</p>
<h1 id="heading-xcode">XCode</h1>
<p>Get it from the App Store. I need it for mobile app development.</p>
<h1 id="heading-nvm">NVM</h1>
<p>Full-stack TypeScript developer here, and <a target="_blank" href="https://github.com/nvm-sh/nvm#installing-and-updating">NVM is a must have</a>.</p>
<ul>
<li><code>nvm install --lts</code></li>
<li><code>npm install -g npm</code> # Upgrades to latest NPM for LTS.</li>
<li><code>which node</code></li>
<li><code>which npm</code></li>
</ul>
<h1 id="heading-global-npm-packages">Global NPM Packages</h1>
<p>I like to install the following packages globally rather than using <code>npx</code> because I find that fig.io works better for these when installed.</p>
<ul>
<li><code>npm install -g cdk</code></li>
<li><code>npm install -g expo-cli</code></li>
<li><code>npm install -g yarn</code></li>
</ul>
<h1 id="heading-zoom">Zoom</h1>
<p>What can I say? <a target="_blank" href="https://zoom.us/download#client_4meeting">Zoom</a> seems to have won the video conference battle. It works great.</p>
<p>That's it as of now. I'll update this if any new software comes along that I can't live without!</p>
<h1 id="heading-loom">Loom</h1>
<p>Like Zoom, but with an L! <a target="_blank" href="https://www.loom.com/">Loom</a> is fantastic for shooting demo videos of new features and sharing them with the team.</p>
<h1 id="heading-discord">Discord</h1>
<p>Can you ever have too many chat programs? Honestly, if I had my way, I'd exclusively use <a target="_blank" href="https://discord.com/">Discord</a> for everything.</p>
<h1 id="heading-docker">Docker</h1>
<p>I'm big into Serverless technologies, so I thought I was going to get away with not needing <a target="_blank" href="https://www.docker.com/products/docker-desktop/">Docker</a>. However, I've recently started to use <a target="_blank" href="https://supabase.com">Supabase</a>, which requires docker to run locally.</p>
<h1 id="heading-supabase-cli">Supabase CLI</h1>
<p><a target="_blank" href="https://supabase.com/docs/guides/local-development">Supabase</a> makes me feel like I have superhero powers. I click 2 buttons and get a Postgres database, authentication, row-level security, APIs, serverless functions, and blob storage. Seriously Supabase, y'all outdid yourselves.</p>
<p>If you like this content please consider following me at https://twitter.com/thealexkates</p>
]]></content:encoded></item><item><title><![CDATA[How to Trigger an AWS CloudWatch Alarm from a Lambda Function]]></title><description><![CDATA[In this post, we are going to use the AWS CDK and TypeScript to build a Lambda Function that triggers a CloudWatch Alarm that sends an email when an invocation error occurs.
All of the code can be found in this repository.
Setup
We need to run a few ...]]></description><link>https://blog.alexkates.dev/how-to-trigger-an-aws-cloudwatch-alarm-from-a-lambda-function</link><guid isPermaLink="true">https://blog.alexkates.dev/how-to-trigger-an-aws-cloudwatch-alarm-from-a-lambda-function</guid><category><![CDATA[AWS]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[aws-cdk]]></category><dc:creator><![CDATA[Alex Kates]]></dc:creator><pubDate>Tue, 10 May 2022 15:18:47 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1652195897603/fD_KAaGBU.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In this post, we are going to use the AWS CDK and TypeScript to build a Lambda Function that triggers a CloudWatch Alarm that sends an email when an invocation error occurs.</p>
<p>All of the code <a target="_blank" href="https://github.com/alexkates/how-to-trigger-cloudwatch-alarm-from-lambda">can be found in this repository</a>.</p>
<h2 id="heading-setup">Setup</h2>
<p>We need to run a few commands to set up our CDK app.</p>
<pre><code class="lang-shell">mkdir how-to-trigger-cloudwatch-alarm-from-lambda
cd how-to-trigger-cloudwatch-alarm-from-lambda
npx cdk init app --language typescript
</code></pre>
<p>This should build the following directory structure.
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1652103295347/uvsaRyYw9.png" alt="Directory structure after running the CDK init command" /></p>
<p>Also, make sure to have the AWS CLI configured. For more information follow <a target="_blank" href="https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html">the AWS CLI quickstart guide</a>.</p>
<h2 id="heading-create-the-lambda-function">Create the Lambda Function</h2>
<p>Deploying a Lambda function requires bootstrapping the CDK app which builds an S3 bucket where the Lambda's source code will live. This is a one-time operation.</p>
<pre><code class="lang-shell">npx cdk bootstrap
</code></pre>
<p>Install esbuild so the TypeScript can be efficiently compiled and minified into JavaScript. This isn't a requirement, but having <a target="_blank" href="https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_lambda_nodejs-readme.html#local-bundling">esbuild installed will bypass the need for docker</a>.</p>
<pre><code class="lang-shell">npm install -D esbuild
</code></pre>
<p>Create src/index.ts and paste the following code which will throw an error on invocation.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">handler</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">"error"</span>);
}
</code></pre>
<p>Open <code>lib/how-to-trigger-cloudwatch-alarm-from-lambda.ts</code> and define the new Lambda function.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> nodejsFunction = <span class="hljs-keyword">new</span> NodejsFunction(
  <span class="hljs-built_in">this</span>,
  <span class="hljs-string">"how-to-trigger-cloudwatch-alarm-from-lambda"</span>,
  {
    handler: <span class="hljs-string">"handler"</span>, <span class="hljs-comment">// The name of the TypeScript function to invoke.</span>
    runtime: Runtime.NODEJS_14_X, <span class="hljs-comment">// The Node.js runtime to use.</span>
    entry: <span class="hljs-string">"src/index.ts"</span>, <span class="hljs-comment">// The TypeScript file that contains the handler function.</span>
  }
);
</code></pre>
<p>Now let's deploy the Stack to AWS.</p>
<pre><code class="lang-shell">npx cdk deploy --require-approval never
</code></pre>
<h2 id="heading-create-the-cloudwatch-alarm">Create the CloudWatch Alarm</h2>
<p>Open lib/how-to-trigger-cloudwatch-alarm-from-lambda.ts and add the CloudWatch Metric and Alarm.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> numberOfFunctionErrors = nodejsFunction.metricErrors( <span class="hljs-comment">// Capture invocation errors.</span>
  {
    period: Duration.minutes(<span class="hljs-number">1</span>), <span class="hljs-comment">// Sum invocation errors each minute.</span>
  }
);

<span class="hljs-keyword">const</span> alarm = <span class="hljs-keyword">new</span> Alarm(
  <span class="hljs-built_in">this</span>,
  <span class="hljs-string">"how-to-trigger-cloudwatch-alarm-from-lambda-alarm"</span>,
  {
    metric: numberOfFunctionErrors, <span class="hljs-comment">// Watch the number of invocations.</span>
    threshold: <span class="hljs-number">1</span>, <span class="hljs-comment">// Compare to the threshold of 1.</span>
    evaluationPeriods: <span class="hljs-number">1</span>, <span class="hljs-comment">// Look within the context of 1 evaluation period.</span>
    comparisonOperator:
      ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD, <span class="hljs-comment">// Trigger if greater than or equal to 1.</span>
  }
);

## Create the SNS Email Subscription

Open <span class="hljs-string">`lib/how-to-trigger-cloudwatch-alarm-from-lambda.ts`</span> and add the SNS Topic, Email Subscription and SNS Action.

<span class="hljs-string">``</span><span class="hljs-string">`typescript
const topic = new Topic(
  this,
  "how-to-trigger-cloudwatch-alarm-from-lambda-topic"
);
topic.addSubscription(new EmailSubscription("your@email.com")); // Add a new email subscription to the SNS topic.
alarm.addAlarmAction(new SnsAction(topic)); // Add a new SNS Action to the CloudWatch Alarm.</span>
</code></pre>
<h2 id="heading-stack-summary">Stack Summary</h2>
<p>After piecing everything together, your <code>lib/how-to-trigger-cloudwatch-alarm-from-lambda.ts</code> should look like this.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Duration, Stack, StackProps } <span class="hljs-keyword">from</span> <span class="hljs-string">"aws-cdk-lib"</span>;
<span class="hljs-keyword">import</span> { Construct } <span class="hljs-keyword">from</span> <span class="hljs-string">"constructs"</span>;
<span class="hljs-keyword">import</span> { NodejsFunction } <span class="hljs-keyword">from</span> <span class="hljs-string">"aws-cdk-lib/aws-lambda-nodejs"</span>;
<span class="hljs-keyword">import</span> { Runtime } <span class="hljs-keyword">from</span> <span class="hljs-string">"aws-cdk-lib/aws-lambda"</span>;
<span class="hljs-keyword">import</span> { Alarm, ComparisonOperator } <span class="hljs-keyword">from</span> <span class="hljs-string">"aws-cdk-lib/aws-cloudwatch"</span>;
<span class="hljs-keyword">import</span> { Topic } <span class="hljs-keyword">from</span> <span class="hljs-string">"aws-cdk-lib/aws-sns"</span>;
<span class="hljs-keyword">import</span> { EmailSubscription } <span class="hljs-keyword">from</span> <span class="hljs-string">"aws-cdk-lib/aws-sns-subscriptions"</span>;
<span class="hljs-keyword">import</span> { SnsAction } <span class="hljs-keyword">from</span> <span class="hljs-string">"aws-cdk-lib/aws-cloudwatch-actions"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> HowToTriggerCloudwatchAlarmFromLambdaStack <span class="hljs-keyword">extends</span> Stack {
  <span class="hljs-keyword">constructor</span>(<span class="hljs-params">scope: Construct, id: <span class="hljs-built_in">string</span>, props?: StackProps</span>) {
    <span class="hljs-built_in">super</span>(scope, id, props);

    <span class="hljs-keyword">const</span> nodejsFunction = <span class="hljs-keyword">new</span> NodejsFunction(
      <span class="hljs-built_in">this</span>,
      <span class="hljs-string">"how-to-trigger-cloudwatch-alarm-from-lambda"</span>,
      {
        handler: <span class="hljs-string">"handler"</span>,
        runtime: Runtime.NODEJS_14_X,
        entry: <span class="hljs-string">"src/index.ts"</span>,
      }
    );

    <span class="hljs-keyword">const</span> numberOfFunctionErrors = nodejsFunction.metricErrors({
      period: Duration.minutes(<span class="hljs-number">1</span>),
    });

    <span class="hljs-keyword">const</span> alarm = <span class="hljs-keyword">new</span> Alarm(
      <span class="hljs-built_in">this</span>,
      <span class="hljs-string">"how-to-trigger-cloudwatch-alarm-from-lambda-alarm"</span>,
      {
        metric: numberOfFunctionErrors,
        threshold: <span class="hljs-number">1</span>,
        evaluationPeriods: <span class="hljs-number">1</span>,
        comparisonOperator:
          ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
      }
    );

    <span class="hljs-keyword">const</span> topic = <span class="hljs-keyword">new</span> Topic(
      <span class="hljs-built_in">this</span>,
      <span class="hljs-string">"how-to-trigger-cloudwatch-alarm-from-lambda-topic"</span>
    );

    topic.addSubscription(<span class="hljs-keyword">new</span> EmailSubscription(<span class="hljs-string">"your@email.com"</span>)); <span class="hljs-comment">// Update with your email!</span>

    alarm.addAlarmAction(<span class="hljs-keyword">new</span> SnsAction(topic));
  }
}
</code></pre>
<p>Let's do one final deployment.</p>
<pre><code class="lang-shell">npx cdk deploy --require-approval never
</code></pre>
<h2 id="heading-testing">Testing</h2>
<p>Let's use the AWS CLI to test everything.</p>
<p>First, let's get the Lambda Function name, which can be found using the following command</p>
<pre><code class="lang-shell">aws lambda list-functions
</code></pre>
<p>Next, using the Lambda Function name from the previous command, use the AWS CLI to invoke the Lambda Function.</p>
<pre><code class="lang-shell"> aws lambda invoke \
  --function-name "HowToTriggerCloudwatchAla-howtotriggercloudwatchal-aYncvjc4a5RT" \
  response.json
</code></pre>
<p>Wait about a minute and check the email address that was configured for the SNS Email subscription. If all went well, an email should have been sent with a subject similar to <code>ALARM: "HowToTriggerCloudwatchAlarmFromLambdaStack-howtotriggercloudwat..." in US East (N. Virginia)</code></p>
<h2 id="heading-clean-up">Clean Up</h2>
<p>Don't forget to delete the stack when finished!</p>
<pre><code class="lang-shell">npx cdk destroy
</code></pre>
<p>Thanks for reading! If you found this useful, please follow me here 
https://dev.to/thealexkates
https://twitter.com/thealexkates</p>
]]></content:encoded></item><item><title><![CDATA[Introducing Petsura]]></title><description><![CDATA[TL;DR
My submission for the Hasura X Hashnode Hackathon is Petsura. 
Help pets find their forever homes! Quickly scroll your feed of adoptable fluffs and share   on Twitter, send an email to the foster, or open the pet directly in Petfinder

Petsura ...]]></description><link>https://blog.alexkates.dev/introducing-petsura</link><guid isPermaLink="true">https://blog.alexkates.dev/introducing-petsura</guid><category><![CDATA[Hasura Hackathon]]></category><category><![CDATA[Hasura]]></category><dc:creator><![CDATA[Alex Kates]]></dc:creator><pubDate>Mon, 28 Mar 2022 15:09:01 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1648474926816/m6C-yfuoE.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-tldr">TL;DR</h1>
<p>My submission for the <a target="_blank" href="https://townhall.hashnode.com/hasura-hackathon">Hasura X Hashnode Hackathon</a> is <a target="_blank" href="https://petsura.vercel.app/feed">Petsura</a>. </p>
<p>Help pets find their forever homes! Quickly scroll your feed of adoptable fluffs and share   on Twitter, send an email to the foster, or open the pet directly in <a target="_blank" href="https://www.petfinder.com/">Petfinder</a></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1648474984243/XNS-wzO8Z.png" alt="Petsura Screenshot" /></p>
<p>Petsura is powered by the <a target="_blank" href="https://hasura.io/">Hasura</a> platform, built using <a target="_blank" href="https://nextjs.org">NextJS</a> on <a target="_blank" href="https://vercel.com">Vercel</a>, and leverages the <a target="_blank" href="https://www.petfinder.com/developers/v2/docs/">Petfinder API</a>.</p>
<h1 id="heading-intro">Intro</h1>
<p><a target="_blank" href="https://petsura.vercel.app/feed">Petsura</a> is an Instagram-like web application that allows you to quickly browse through the adoptable pets found on <a target="_blank" href="https://www.petfinder.com/">Petfinder</a>. When you find a fluff that sparks joy, you can share it on Twitter, contact the foster directly, or open the pet's full biography on Petfinder.</p>
<h1 id="heading-motivation">Motivation</h1>
<p>I have three rescue animals that live with me, and I think the world would be a better place if more animals were adopted. I wanted to make an app that was basically Instagram for adoptable pets. Also, I've been itching to play with Hasura and this hackathon was a perfect time.</p>
<h1 id="heading-how-to-use">How to use</h1>
<h2 id="heading-feed">Feed</h2>
<p>The main page is called the <a target="_blank" href="https://petsura.vercel.app/feed">feed page</a>. It displays 25 random, adoptable pets from the Petfinder API. Scroll the feed until you find a pet that sparks joy. You can load more pets when you arrive at the end of the feed.</p>
<p>When you find that special pet, you can do three different actions.</p>
<p>🐦 Share the pet on Twitter.</p>
<p>📧 Email the foster to express interest in adopting.</p>
<p>🌐 Open the pet's full biography on Petfinder.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1648475021574/qIeKKe2Nw.png" alt="Petsura actions including Tweet, Emails, and opening externally." /></p>
<h1 id="heading-tech-stack">Tech stack</h1>
<h2 id="heading-hosting">Hosting</h2>
<p><a target="_blank" href="https://vercel.com/">Vercel</a> is a platform for hosting full-stack web applications. Petsura uses Vercel to host the NextJS web application.</p>
<h2 id="heading-web-framework">Web Framework</h2>
<p><a target="_blank" href="https://nextjs.org">NextJS</a> is the React framework for building modern, production-ready applications. Petsura is built on NextJS.</p>
<h2 id="heading-styles">Styles</h2>
<p><a target="_blank" href="https://tailwindcss.com/">Tailwind CSS</a> CSS framework that lets you rapidly build beautiful web pages without leaving your HTML files. </p>
<h2 id="heading-api">API</h2>
<p><a target="_blank" href="https://hasura.io/">Hasura</a> is a fully managed GraphQL platform. Petsura uses Hasura for its GraphQL API and also leverages the Action feature to integrate with an AWS APIGW.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1648479988298/jq6oTmo69.png" alt="Hasura Action Screen Shot" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1648480064775/rx-IzfIIK.png" alt="Hasura GraphQL Query Screen Shot" /></p>
<p><a target="_blank" href="https://www.serverless.com/">Serverless</a> allows you to rapidly develop serverless applications in AWS. Petsura uses Serverless to build the APIGW and Lambda that integrates the Hasura GraphQL API with the Petfinder Rest API.</p>
<p><a target="_blank" href="https://www.apollographql.com/docs/react/">Apollo GraphQL Client</a> is a great library to use in your React application to fetch data using GraphQL. Petsura uses the useQuery hook to interact with the <a target="_blank" href="https://hasura.io/">Hasura</a>.</p>
<h1 id="heading-outro">Outro</h1>
<p>I hope you enjoy using Petsura as much as I did building it! 
Let's connect on Twitter -&gt; https://twitter.com/thealexkates</p>
]]></content:encoded></item><item><title><![CDATA[Introducing Hashnode Roulette]]></title><description><![CDATA[TL;DR
My submission for the Hashnode/Netlify hackathon is Hashnode Roulette. 
Make your hashnode feed wonderful. Quickly find new stories and authors to follow using simple mobile gestures.
Hashnode Roulette is powered by the Netlify platform, built ...]]></description><link>https://blog.alexkates.dev/introducing-hashnode-roulette</link><guid isPermaLink="true">https://blog.alexkates.dev/introducing-hashnode-roulette</guid><category><![CDATA[Netlify]]></category><category><![CDATA[NetlifyHackathon]]></category><category><![CDATA[Hashnode]]></category><dc:creator><![CDATA[Alex Kates]]></dc:creator><pubDate>Sun, 27 Feb 2022 01:49:13 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1645886348714/TgWb341pr.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-tldr">TL;DR</h1>
<p>My submission for the Hashnode/Netlify hackathon is <a target="_blank" href="https://hashnode-roulette.netlify.app/">Hashnode Roulette</a>. </p>
<p>Make your hashnode feed wonderful. Quickly find new stories and authors to follow using simple mobile gestures.</p>
<p>Hashnode Roulette is powered by the <a target="_blank" href="https://www.netlify.com/">Netlify</a> platform, built using <a target="_blank" href="https://nextjs.org">NextJS</a>, and leverages the <a target="_blank" href="https://api.hashnode.com/">Hashnode API</a>.</p>
<h1 id="heading-intro">Intro</h1>
<p><a target="_blank" href="https://hashnode-roulette.netlify.app/">Hashnode Roulette</a> is a Tinder-like web application that allows you to quickly browse through the top Hashnode articles and swipe them to react, follow, or open the article.</p>
<p>☝️ Swipe up to open the article in a new window.</p>
<p>👉 Swipe right to like the current article.</p>
<p>👇 Swipe down to follow the author.</p>
<p>👈 Swipe left to skip the article.</p>
<h1 id="heading-motivation">Motivation</h1>
<p>I personally think Hashnode is the best developer blogging platform in the market right now. I wanted to spend some time learning their GraphQL API. Also, I love working with NextJS and wanted to learn next-auth. Also, it's a lot of fun refreshing the page and getting random articles.</p>
<h1 id="heading-how-to-use">How to use</h1>
<h2 id="heading-login">Login</h2>
<p>Unfortunately, there isn't a way to sign in to Hashnode via OAuth yet. For now, use your Twitter credentials to sign in.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1645925401738/NTTvqp4YS.png" alt="Hashnode Roulette landing page" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1645925454088/4DoxHmqPb.png" alt="Sign in with Twitter" /></p>
<h2 id="heading-settings">Settings</h2>
<p>Certain gestures, like swipe-right and swipe-down, use GraphQL mutations via the Hashnode API. These mutations require you to use your API key. </p>
<p>You'll need to grab your Hashnode API key. Head over to your Account Settings -&gt; Developer -&gt; Generate New Token.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1645925855365/6WvKHOfuf.png" alt="Generate new Hashnode API key" /></p>
<p>Enter your Hashnode API key and click Submit.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1645925904160/IRfCScbal.png" alt="Save your Hashnode API key" /></p>
<h2 id="heading-deck">Deck</h2>
<p>The main page is called the Deck page. It has a random set of Hashnode articles as cards. These cards can be swiped in different directions based on how you want to react to the article.</p>
<p>☝️ Swipe up to open the article in a new window.</p>
<p>👉 Swipe right to like the current article.</p>
<p>👇 Swipe down to follow the author.</p>
<p>👈 Swipe left to skip the article.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1645926120862/8wfmIccqH.png" alt="Hashnode Roulette deck page" /></p>
<h1 id="heading-tech-stack">Tech stack</h1>
<h2 id="heading-hosting">Hosting</h2>
<p><a target="_blank" href="https://www.netlify.com/">Netlify</a> is a platform for hosting full-stack web applications. Hashnode Roulette uses Netlify to host the NextJS web application.</p>
<h2 id="heading-web-framework">Web Framework</h2>
<p><a target="_blank" href="https://nextjs.org">NextJS</a> is the React framework for building modern, production-ready applications. Hashnode Roulette is built on NextJS.</p>
<h2 id="heading-authentication">Authentication</h2>
<p><a target="_blank" href="https://next-auth.js.org/">next-auth</a> is the go-to authentication framework when working with NextJS. Hashnode Roulette uses the twitter-oauth module to allow users to authenticate using their Twitter credentials.</p>
<h2 id="heading-ui-controls">UI Controls</h2>
<p><a target="_blank" href="https://www.npmjs.com/package/react-tinder-card">react-tinder-card</a> is an NPM module that makes it easy to implement the swipe-card UI control. Hashnode Roulette uses this package to present the user with Hashnode articles that can be swiped.</p>
<h2 id="heading-data">Data</h2>
<p><a target="_blank" href="https://www.apollographql.com/docs/react/">Apollo GraphQL Client</a> is a great library to use in your React application to fetch data using GraphQL. Hashnode Roulette uses the useQuery and useMutation hooks to interact with the <a target="_blank" href="https://api.hashnode.com/">Hashnode API</a>.</p>
<h1 id="heading-outro">Outro</h1>
<p>I hope you enjoy using Hashnode Roulette as much as I did building it! 
Let's connect on Twitter -&gt; https://twitter.com/thealexkates</p>
]]></content:encoded></item></channel></rss>