<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/rss-blog.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>CodeTV Blog RSS Feed</title><description>Articles and tutorials about web dev, career growth, and more.</description><link>https://codetv.dev/</link><item><title>Web Dev Challenge Hackathon S2.E11: Twilio Hotline Challenge
</title><link>https://codetv.dev/blog/web-dev-challenge-hackathon-s2e11-code-powered-phone-hotline/</link><guid isPermaLink="true">https://codetv.dev/blog/web-dev-challenge-hackathon-s2e11-code-powered-phone-hotline/</guid><description>In this challenge, build a custom AI voice hotline using ConversationRelay. Hotlines due Friday, November 21, 2025.
</description><pubDate>Mon, 03 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/f_auto/q_auto/v1762190557/wdc-hackathon-s2e11-build-a-hotline-social.jpg&quot; alt=&quot;Web Dev Challenge Hackathon S2.E11: Twilio Hotline Challenge
&quot; /&gt;&lt;/p&gt;&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/f_auto/q_auto/v1762190557/wdc-hackathon-s2e11-build-a-hotline-social.jpg&quot; alt=&quot;Over the shoulder view of code on a monitor. The text reads, &amp;quot;Build a web app, learn something new, earn rewards. Brought to you by Twilio and CodeTV. Web Dev Challenge&amp;quot;&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;h2&gt;Press 1 for an awesome AI-Powered Hotline&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Ever thought: gee, I wish there was a hotline for that? Well now’s your chance to build it!&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Think: an office-hours hotline, community resources line, pizza-ordering line, event status line, bug triage, wake‑up calls, volunteer coordination, campus shuttle tracker — if it rings it fits.&lt;/p&gt;
&lt;h2&gt;The Tool: ConversationRelay&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Your hotline must use &lt;a href=&quot;https://twil.io/cr&quot;&gt;ConversationRelay&lt;/a&gt; as part of the build.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Twilio’s ConversationRelay lets you place a relay layer between callers and your logic so you can route messages, swap in bots or humans, and keep a clean separation of concerns.&lt;/p&gt;
&lt;p&gt;Use it with Twilio Programmable Voice and/or Messaging (SMS/WhatsApp), webhooks, and your runtime of choice (Twilio Functions/Studio/Serverless or your own stack).&lt;/p&gt;
&lt;p&gt;Ready to get started? Check out the &lt;a href=&quot;https://www.twilio.com/docs/voice/conversationrelay&quot;&gt;ConversationRelay docs&lt;/a&gt;, dive into one of the &lt;a href=&quot;https://www.twilio.com/en-us/blog/developers/tutorials/product/integrate-openai-twilio-voice-using-conversationrelay&quot;&gt;tutorials on the Twilio blog&lt;/a&gt;, or check out the &lt;a href=&quot;https://github.com/robinske/cr-demo&quot;&gt;starter repo on Github&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Make new connections and get expert guidance&lt;/h2&gt;
&lt;p&gt;The Twilio team has set up a &lt;a href=&quot;https://discord.gg/6ERg5HDkXG&quot;&gt;dedicated channel&lt;/a&gt; on the Twilio Discord for brainstorming, sharing ideas, and keeping each other accountable.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;A note from Jason:&lt;/strong&gt; don&apos;t miss out on the chance to make connections with industry pros and get hands-on feedback on your code! Opportunities like these are a chance to &lt;a href=&quot;https://codetv.link/manufacture-luck&quot;&gt;manufacture your own luck&lt;/a&gt; — don&apos;t pass it up, especially if you&apos;re looking for new opportunities!&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;Multiple Ways to Win!&lt;/h2&gt;
&lt;p&gt;The Twilio team will choose three built hotline submissions to win some great Twilio swag from our &lt;a href=&quot;https://store.twilio.com/&quot;&gt;merch store&lt;/a&gt; and Twilio credits.&lt;/p&gt;
&lt;p&gt;The first five qualifying hotlines submitted will also receive an item of their choice (up to $150) from Ugmonk, Drop, or Cocoon.&lt;/p&gt;
&lt;p&gt;Finally, whether you’ve built a hotline or just have a great idea, share it in &lt;a href=&quot;https://x.com/twilio/status/1987971002643710442&quot;&gt;the challenge thread on Twilio&apos;s X account&lt;/a&gt; or &lt;a href=&quot;https://www.linkedin.com/posts/twilio-inc-_twilio-hotline-challenge-activity-7393694154926559232-nbx6&quot;&gt;LinkedIn post&lt;/a&gt;. The top five ideas by total likes, comments, and reshares at the deadline will each receive a gift card valued at $25. As a bonus, we will be building one standout idea live on Twitch with Alex and Jason.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Shipping is limited to countries where Ugmonk, Drop, and Cocoon are able to deliver. See their websites for specifics.&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;Watch the episode for inspiration&lt;/h2&gt;
&lt;p&gt;See how Jack, Sherm, Brian, Pedro, Avi, and Dibyanshu tackled the challenge in the latest episode of &lt;a href=&quot;https://codetv.link/wdc/s2e11&quot;&gt;&lt;em&gt;Web Dev Challenge&lt;/em&gt;&lt;/a&gt; to jumpstart your own creativity.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/98vk7sXa7p0&quot;&gt;Watch on YouTube&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;The rules and how to submit&lt;/h2&gt;
&lt;p&gt;If you want to play along, here’s how:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Team up with a friend or work on your onw&lt;/li&gt;
&lt;li&gt;Build a web app that meets the challenge&lt;/li&gt;
&lt;li&gt;Spend 30 minutes¹ planning your app&lt;/li&gt;
&lt;li&gt;Spend 4 hours¹ building your app&lt;/li&gt;
&lt;li&gt;Use &lt;a href=&quot;https://twil.io/cr&quot;&gt;ConversationRelay&lt;/a&gt; to build your web app&lt;/li&gt;
&lt;li&gt;Publish the source code as a public GitHub repo&lt;/li&gt;
&lt;li&gt;Publish the web app to a public URL&lt;/li&gt;
&lt;li&gt;11:59 pm Pacific on Friday, Nov 21, 2025 by &lt;a href=&quot;https://x.com/twilio/status/1987971002643710442&quot;&gt;replying to Twilio&apos;s post on X&lt;/a&gt; or &lt;a href=&quot;https://www.linkedin.com/posts/twilio-inc-_twilio-hotline-challenge-activity-7393694154926559232-nbx6&quot;&gt;on LinkedIn&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Bonus: you can also win for a great idea!&lt;/h3&gt;
&lt;p&gt;If you don’t have time to build a hotline, you can &lt;em&gt;also&lt;/em&gt; win by submitting our favorite hotline idea. &lt;a href=&quot;https://x.com/twilio/status/1987971002643710442&quot;&gt;Reply to the post&lt;/a&gt; with your idea, and if we decide to build it, you’ll win a prize!&lt;/p&gt;
&lt;h3&gt;&lt;a href=&quot;https://x.com/twilio/status/1987971002643710442&quot;&gt;Submit your web app&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Happy building! Let&apos;s have some fun. &lt;/p&gt;
&lt;aside&gt;&lt;p&gt;¹ The time limit is intended to make this a quick project that can fit most schedules. That being said, if you decide to spend more time on this, we&apos;ll never let a silly thing like rules stand in the way of a good time.&lt;/p&gt;&lt;/aside&gt;</content:encoded></item><item><title>Web Dev Challenge Hackathon S2.E9: Breakfast Apps
</title><link>https://codetv.dev/blog/web-dev-challenge-hackathon-s2e9-breakfast-apps/</link><guid isPermaLink="true">https://codetv.dev/blog/web-dev-challenge-hackathon-s2e9-breakfast-apps/</guid><description>In this challenge, build a breakfast-related app using Hashbrown. Apps due October 6, 2025.
</description><pubDate>Tue, 23 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/f_auto/q_auto/v1758597761/wdc-hackathon-s2e9-breakfast-apps-hashbrown-social.jpg&quot; alt=&quot;Web Dev Challenge Hackathon S2.E9: Breakfast Apps
&quot; /&gt;&lt;/p&gt;&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/f_auto/q_auto/v1758597761/wdc-hackathon-s2e9-breakfast-apps-hashbrown-social.jpg&quot; alt=&quot;James Q. Quick wearing a Hashbrown apron. The text reads, &amp;quot;Web Dev Challenge community hackathon, S2.E9 Breakfast Apps by Hashbrown and CodeTV&amp;quot;&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;h2&gt;Build an app that’s about breakfast&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;It’s the most important ~~meal~~ app of the day! Build an app that is somehow related to breakfast.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Manage your favorite recipes, track the best breakfast burritos, build a digital shrine to pancakes — we don’t care what you build as long as it has something to do with breakfast.&lt;/p&gt;
&lt;h2&gt;The Tool: Hashbrown&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Your app must use Hashbrown as part of the build.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://codetv.link/hashbrown&quot;&gt;Hashbrown&lt;/a&gt; allows developers to create generative UIs using your own components. It’s fully free and open source, released under the MIT license.&lt;/p&gt;
&lt;p&gt;With Hashbrown, developers can &lt;a href=&quot;https://hashbrown.dev/docs/react/concept/components&quot;&gt;provide a set of custom components&lt;/a&gt; with a clear schema, and use that to give your LLM guardrails for outputting generative — but quality controlled! — user interfaces that adapt to meet the needs of your users.&lt;/p&gt;
&lt;p&gt;You’ve also got access to &lt;a href=&quot;https://hashbrown.dev/docs/react/concept/functions&quot;&gt;safe function calling&lt;/a&gt;, &lt;a href=&quot;https://hashbrown.dev/docs/react/concept/structured-output&quot;&gt;structured output&lt;/a&gt;s, and a &lt;a href=&quot;https://hashbrown.dev/docs/angular/concept/runtime&quot;&gt;JavaScript Runtime&lt;/a&gt;, which open up some pretty interesting opportunities within apps.&lt;/p&gt;
&lt;h2&gt;Make new connections and get expert guidance&lt;/h2&gt;
&lt;p&gt;We&apos;ve set up a dedicated channel in the &lt;a href=&quot;https://codetv.link/discord&quot;&gt;CodeTV Discord&lt;/a&gt; called &lt;code&gt;#builder-chat&lt;/code&gt; for brainstorming, sharing ideas, and keeping each other accountable.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;A note from Jason:&lt;/strong&gt; don&apos;t miss out on the chance to make connections with industry pros and get hands-on feedback on your code! Opportunities like these are a chance to &lt;a href=&quot;https://codetv.link/manufacture-luck&quot;&gt;manufacture your own luck&lt;/a&gt; — don&apos;t pass it up, especially if you&apos;re looking for new opportunities!&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;The first 5 devs to submit a qualifying app get their choice of gear&lt;/h2&gt;
&lt;p&gt;The first 5 qualifying apps submitted will receive an item of their choice, up to $150 value, from one of the following sites:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://ugmonk.com/&quot;&gt;Ugmonk&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://drop.com/home&quot;&gt;Drop&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cocooninnovations.com/&quot;&gt;Cocoon&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Shipping is limited to countries where Ugmonk, Drop, and Cocoon are able to deliver. See their websites for specifics.&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;Watch the episode for inspiration&lt;/h2&gt;
&lt;p&gt;See how Shaundai, Scott, James, Brian, Jane, and David tackled the challenge in the latest episode of &lt;a href=&quot;https://codetv.link/wdc/s2e9&quot;&gt;&lt;em&gt;Web Dev Challenge&lt;/em&gt;&lt;/a&gt; to jumpstart your own creativity.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/nUSbmGQRsv4&quot;&gt;Watch on YouTube&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;The rules and how to submit&lt;/h2&gt;
&lt;p&gt;If you want to play along, here’s how:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Team up with a friend or work on your onw&lt;/li&gt;
&lt;li&gt;Build a web app that meets the challenge&lt;/li&gt;
&lt;li&gt;Spend 30 minutes¹ planning your app&lt;/li&gt;
&lt;li&gt;Spend 4 hours¹ building your app&lt;/li&gt;
&lt;li&gt;Use &lt;a href=&quot;https://codetv.link/hashbrown&quot;&gt;Hashbrown&lt;/a&gt; to build your web app&lt;/li&gt;
&lt;li&gt;Publish the source code as a public GitHub repo&lt;/li&gt;
&lt;li&gt;Publish the web app to a public URL&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://codetv.link/wdc-hackathon-s2e9-submit&quot;&gt;Submit your web app&lt;/a&gt; by 11:59 pm Pacific on Monday, Sep 8, 2025&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;&lt;a href=&quot;https://codetv.link/wdc-hackathon-s2e9-submit&quot;&gt;Submit your web app&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Happy building! Let&apos;s have some fun. &lt;/p&gt;
&lt;aside&gt;&lt;p&gt;¹ The time limit is intended to make this a quick project that can fit most schedules. That being said, if you decide to spend more time on this, we&apos;ll never let a silly thing like rules stand in the way of a good time.&lt;/p&gt;&lt;/aside&gt;</content:encoded></item><item><title>CSS overrides without important using layers in Astro components
</title><link>https://codetv.dev/blog/astro-css-overrides-layers/</link><guid isPermaLink="true">https://codetv.dev/blog/astro-css-overrides-layers/</guid><description>We used to need `!important` to override styles, but it’s not 2021 anymore and there’s a better way: CSS layers.
</description><pubDate>Sat, 30 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Recently I was working on a shared Astro component and found an edge case that required me to override the CSS of the Astro component from within the page that imported it.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://bsky.app/profile/did:plc:ga3wlji66r5mxqch6wh7nq4v/post/3lweptrp4dc2b&quot;&gt;I thought the fix was neat&lt;/a&gt;, so I figured I&apos;d write it up in case it helps anyone else.&lt;/p&gt;
&lt;h2&gt;Minimal reproduction of CSS override challenges is Astro&lt;/h2&gt;
&lt;p&gt;To see the problem in action, let&apos;s imagine a simple Astro site that has a layout, a heading component, and a page that imports that heading component.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
const { title } = Astro.props;
---

&amp;lt;html lang=&quot;en&quot;&amp;gt;
	&amp;lt;head&amp;gt;
		&amp;lt;meta charset=&quot;utf-8&quot; /&amp;gt;
		&amp;lt;link rel=&quot;icon&quot; type=&quot;image/svg+xml&quot; href=&quot;/favicon.svg&quot; /&amp;gt;
		&amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width&quot; /&amp;gt;
		&amp;lt;meta name=&quot;generator&quot; content={Astro.generator} /&amp;gt;
		&amp;lt;title&amp;gt;{title}&amp;lt;/title&amp;gt;
	&amp;lt;/head&amp;gt;
	&amp;lt;body&amp;gt;
		&amp;lt;slot /&amp;gt;
	&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;

&amp;lt;style&amp;gt;
	html,
	body {
		font-family: system-ui, sans-serif;
		margin: 20px;
		text-align: center;
	}
&amp;lt;/style&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;---
const { title } = Astro.props;
---

&amp;lt;h1 class=&quot;heading&quot;&amp;gt;{title}&amp;lt;/h1&amp;gt;

&amp;lt;style&amp;gt;
	.heading {
		color: red;
	}
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;---
import Layout from &apos;../components/layout.astro&apos;;
import Heading from &apos;../components/heading.astro&apos;;

const title = &apos;Style overrides in Astro with CSS layers&apos;;
---

&amp;lt;Layout title={title}&amp;gt;
	&amp;lt;Heading title={title} /&amp;gt;
&amp;lt;/Layout&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These three files result in a simple Astro site.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/q_auto/f_auto/c_crop,w_1480,h_885,g_north,y_94/v1756534595/astro-css-layers-before.png&quot; alt=&quot;an Astro site showing a red heading&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;h2&gt;Why standard overrides don&apos;t work in Astro components&lt;/h2&gt;
&lt;p&gt;One of Astro&apos;s strengths is that, by default, all CSS is scoped to the component. This solves one of the biggest sources of frustration that many devs feel with CSS: the cascade and inheritance, which can become really hard to keep track of in large code bases.&lt;/p&gt;
&lt;p&gt;The way Astro does this is by applying a unique generated &lt;code&gt;data&lt;/code&gt; attribute to the built HTML, which is then appended to every CSS selector.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/f_auto/q_auto/v1756538162/Screenshot_2025-08-30_at_00.14.07.png&quot; alt=&quot;Chromium devtools open to highlight the generated data attribute on an Astro component&apos;s rendered HTML&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;The problem comes in when we &lt;em&gt;do&lt;/em&gt; want to use the cascade. For example, if an edge case arises where the headline needs to be blue, we may try something like adding a global override to the &lt;code&gt;.heading&lt;/code&gt; class in the page using the &lt;code&gt;Heading&lt;/code&gt; component:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
import Layout from &apos;../components/layout.astro&apos;;
import Heading from &apos;../components/heading.astro&apos;;

const title = &apos;Style overrides in Astro with CSS layers&apos;;
---

&amp;lt;Layout title={title}&amp;gt;
	&amp;lt;Heading title={title} /&amp;gt;
&amp;lt;/Layout&amp;gt;

&amp;lt;style is:global&amp;gt;
	.heading {
		color: blue;
	}
&amp;lt;/style&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This won&apos;t work. If we inspect the element, we can see that the scoped component style has higher specificity, so we can&apos;t override it this way.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/f_auto/q_auto/v1756537668/astro-css-layers-scoped-styles.png&quot; alt=&quot;devtools output showing the override crossed out because the component CSS specificity is higher&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;If we want to override the CSS in an Astro component, we&apos;re going to need to try something else.&lt;/p&gt;
&lt;h2&gt;CSS overrides in Astro the old way: &lt;code&gt;!important&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Previously, I would have reluctantly slapped an &lt;code&gt;!important&lt;/code&gt; at the end of the override. It&apos;s heavy-handed and feels kinda gross, but it works. Them&apos;s the breaks.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
import Layout from &apos;../components/layout.astro&apos;;
import Heading from &apos;../components/heading.astro&apos;;

const title = &apos;Style overrides in Astro with CSS layers&apos;;
---

&amp;lt;Layout title={title}&amp;gt;
	&amp;lt;Heading title={title} /&amp;gt;
&amp;lt;/Layout&amp;gt;

&amp;lt;style is:global&amp;gt;
	.heading {
		/* not ideal */
		color: blue !important;
	}
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But it&apos;s not 2021 anymore! We have a better option: enter &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/@layer&quot;&gt;CSS layers&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;CSS overrides in Astro the new way: CSS layers&lt;/h2&gt;
&lt;p&gt;Since March 2022, &lt;code&gt;@layer&lt;/code&gt; is part of &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Glossary/Baseline/Compatibility&quot;&gt;Baseline&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This means we can assign a given rule or rules to a named layer. For example, we can define a layer called &lt;code&gt;component&lt;/code&gt; for our heading component styles:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
const { title } = Astro.props;
---

&amp;lt;h1 class=&quot;heading&quot;&amp;gt;{title}&amp;lt;/h1&amp;gt;

&amp;lt;style&amp;gt;
	@layer component {
		.heading {
			color: red;
		}
	}
&amp;lt;/style&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, we can define an additional layer called &lt;code&gt;overrides&lt;/code&gt; in the page that imports the component:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
import Layout from &apos;../components/layout.astro&apos;;
import Heading from &apos;../components/heading.astro&apos;;

const title = &apos;Style overrides in Astro with CSS layers&apos;;
---

&amp;lt;Layout title={title}&amp;gt;
	&amp;lt;Heading title={title} /&amp;gt;
&amp;lt;/Layout&amp;gt;

&amp;lt;style is:global&amp;gt;
	@layer overrides {
		.heading {
			color: blue !important;
			color: blue;
		}
	}
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Because these layers are arbitrarily named, there&apos;s no magic involved — we can either rely on the layers to be applied in the order they were defined (which will get &lt;em&gt;very&lt;/em&gt; confusing with components), or we can manually define the layer order.&lt;/p&gt;
&lt;p&gt;To manually define the layer order, add a &lt;code&gt;@layer&lt;/code&gt; declaration to the top of the layout component that tells the browser to render the component styles first, then apply the overrides:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
const { title } = Astro.props;
---

&amp;lt;html lang=&quot;en&quot;&amp;gt;
	&amp;lt;head&amp;gt;
		&amp;lt;meta charset=&quot;utf-8&quot; /&amp;gt;
		&amp;lt;link rel=&quot;icon&quot; type=&quot;image/svg+xml&quot; href=&quot;/favicon.svg&quot; /&amp;gt;
		&amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width&quot; /&amp;gt;
		&amp;lt;meta name=&quot;generator&quot; content={Astro.generator} /&amp;gt;
		&amp;lt;title&amp;gt;{title}&amp;lt;/title&amp;gt;
	&amp;lt;/head&amp;gt;
	&amp;lt;body&amp;gt;
		&amp;lt;slot /&amp;gt;
	&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;

&amp;lt;style&amp;gt;
	@layer component, overrides;

	html,
	body {
		font-family: system-ui, sans-serif;
		margin: 20px;
		text-align: center;
	}
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;A note on where to declare the layer stack.&lt;/strong&gt; My current understanding is that the &lt;code&gt;@layer&lt;/code&gt; stack needs to appear before any of the CSS that references layers in order to work properly, so declaring layer order at the top of your layout is probably safest. If someone knows for sure how this works, please let me know and I&apos;ll update this section.&lt;/p&gt;&lt;/aside&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/q_auto/f_auto/c_crop,w_1480,h_885,g_north,y_94/v1756534591/astro-css-layers-after.png&quot; alt=&quot;an Astro site showing a blue heading&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;Because of how cascade layers work, we don&apos;t need the &lt;code&gt;!important&lt;/code&gt; hack to override the component styles because our layer order tells the browser that the overrides take precedence.&lt;/p&gt;
&lt;h2&gt;Modern CSS overrides, no hacks&lt;/h2&gt;
&lt;p&gt;CSS layers are great because they give all the control of things like &lt;code&gt;!important&lt;/code&gt;, but without the compounding frustration of ever-increasing specificity wars. As developers, we can choose what takes precedence by placing our styles into layers — and we choose the order of importance for the layers.&lt;/p&gt;
&lt;p&gt;I was really happy to see this feature land in all modern browsers, and even happier that Astro is built in such a way that we can use the platform to accomplish edge case goals like these.&lt;/p&gt;
&lt;h2&gt;Resources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.astro.build/en/guides/styling/&quot;&gt;Astro&apos;s approach to styling&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/@layer&quot;&gt;CSS layers&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Web Dev Challenge Hackathon S2.E7: Touch Grass
</title><link>https://codetv.dev/blog/web-dev-challenge-hackathon-s2e7-touch-grass/</link><guid isPermaLink="true">https://codetv.dev/blog/web-dev-challenge-hackathon-s2e7-touch-grass/</guid><description>In this challenge, help people reconnect with the world outside their screen and you could win a ticket to GraphQL Summit 2025. Apps due September 8, 2025.
</description><pubDate>Tue, 26 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/f_auto/q_auto/v1756257171/wdc-hackathon-s2e7-touch-grass-apollo-lg-v1.jpg&quot; alt=&quot;Web Dev Challenge Hackathon S2.E7: Touch Grass
&quot; /&gt;&lt;/p&gt;&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/f_auto/q_auto/v1756257171/wdc-hackathon-s2e7-touch-grass-apollo-lg-v1.jpg&quot; alt=&quot;Israa, Hosna, Nicole, Ryan, Mark, and Joe on the set of Web Dev Challenge. The text reads, &amp;quot;Web Dev Challenge community hackathon, S2.E7 Touch Grass by Apollo and CodeTV&amp;quot;&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;h2&gt;Build an app that connects us to the physical world&lt;/h2&gt;
&lt;p&gt;So much of the software out there today seems to be reducing our connections — we talk to fewer people, have fewer reasons to go outside, etc. — so let’s build apps that help connect us to the physical world.&lt;/p&gt;
&lt;p&gt;Face-to-face. IRL. Meatspace. Between no-contact deliveries and every AI app trying to replace another human relationship — from pair programming to therapy to romantic partners — it feels like so much of the technology being produced right now is designed to minimize, or even eliminate, our need to interact with other humans.&lt;/p&gt;
&lt;p&gt;Let’s do our small part to change that. Your challenge is to build an app that encourages people to connect with the physical world and/or each other.&lt;/p&gt;
&lt;p&gt;What helps you disconnect from the digital firehose and spend time making offline connections? Go in whatever direction inspires you, whether that’s a hobby, different ways to spend time together with other people, connecting to nature, or something completely different.&lt;/p&gt;
&lt;p&gt;Your app should help ground your users and reconnect them to the world beyond their screens.&lt;/p&gt;
&lt;h2&gt;The Tool: Apollo Connectors&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Your app must use &lt;a href=&quot;https://www.apollographql.com/graphos/apollo-connectors&quot;&gt;Apollo Connectors&lt;/a&gt; as part of the build.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://codetv.link/apollo&quot;&gt;Apollo&lt;/a&gt; has been helping developers build flexible APIs via GraphQL since the early days of the technology, and Connectors make it possible to bring any REST API into GraphQL quickly and painlessly. This is exciting because it means you can extend your own data with third-party API data (or vice versa) to build out anything you can imagine without a lot of data plumbing hassle.&lt;/p&gt;
&lt;p&gt;For this challenge, you’ll have access to a large collection of &lt;a href=&quot;https://www.apollographql.com/graphos/apollo-connectors#explore&quot;&gt;pre-built Connectors&lt;/a&gt; for everything from Pokémon to Stripe data to LLM providers like Anthropic and OpenAI. You can also quickly hook up your own REST APIs to a custom Connector.&lt;/p&gt;
&lt;h2&gt;Make new connections and get expert guidance&lt;/h2&gt;
&lt;p&gt;We&apos;ve set up a dedicated channel in the &lt;a href=&quot;https://codetv.link/discord&quot;&gt;CodeTV Discord&lt;/a&gt; called &lt;code&gt;#builder-chat&lt;/code&gt; for brainstorming, sharing ideas, and keeping each other accountable.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;A note from Jason:&lt;/strong&gt; don&apos;t miss out on the chance to make connections with industry pros and get hands-on feedback on your code! Opportunities like these are a chance to &lt;a href=&quot;https://codetv.link/manufacture-luck&quot;&gt;manufacture your own luck&lt;/a&gt; — don&apos;t pass it up, especially if you&apos;re looking for new opportunities!&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;Win a ticket to GraphQL Summit!&lt;/h2&gt;
&lt;p&gt;The Apollo team will choose 3 hackathon submissions to win a free ticket to the &lt;a href=&quot;https://summit.graphql.com/&quot;&gt;GraphQL Summit&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;In addition, the first 5 qualifying apps submitted will receive an item of their choice, up to $150 value, from one of the following sites:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://ugmonk.com/&quot;&gt;Ugmonk&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://drop.com/home&quot;&gt;Drop&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cocooninnovations.com/&quot;&gt;Cocoon&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Shipping is limited to countries where Ugmonk, Drop, and Cocoon are able to deliver. See their websites for specifics.&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;Watch the episode for inspiration&lt;/h2&gt;
&lt;p&gt;See how Israa, Hosna, Nicole, Ryan, Mark, and Joe tackled the challenge in the latest episode of &lt;a href=&quot;https://codetv.link/wdc/s2e7&quot;&gt;&lt;em&gt;Web Dev Challenge&lt;/em&gt;&lt;/a&gt; to jumpstart your own creativity.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/rvr-xOwrY70&quot;&gt;Watch on YouTube&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;The rules and how to submit&lt;/h2&gt;
&lt;p&gt;If you want to play along, here’s how:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Team up with a friend or work on your onw&lt;/li&gt;
&lt;li&gt;Build a web app that meets the challenge&lt;/li&gt;
&lt;li&gt;Spend 30 minutes¹ planning your app&lt;/li&gt;
&lt;li&gt;Spend 4 hours¹ building your app&lt;/li&gt;
&lt;li&gt;Use &lt;a href=&quot;https://www.apollographql.com/graphos/apollo-connectors&quot;&gt;Apollo Connectors&lt;/a&gt; to build your web app&lt;/li&gt;
&lt;li&gt;Publish the source code as a public GitHub repo&lt;/li&gt;
&lt;li&gt;Publish the web app to a public URL&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://codetv.link/wdc-hackathon-s2e7-submit&quot;&gt;Submit your web app&lt;/a&gt; by 11:59 pm Pacific on Monday, Sep 8, 2025&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;&lt;a href=&quot;https://codetv.link/wdc-hackathon-s2e7-submit&quot;&gt;Submit your web app&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Happy building! Let&apos;s have some fun. &lt;/p&gt;
&lt;aside&gt;&lt;p&gt;¹ The time limit is intended to make this a quick project that can fit most schedules. That being said, if you decide to spend more time on this, we&apos;ll never let a silly thing like rules stand in the way of a good time.&lt;/p&gt;&lt;/aside&gt;</content:encoded></item><item><title>Web Dev Challenge Hackathon S2.E5: Give in to your worst developer urges
</title><link>https://codetv.dev/blog/web-dev-challenge-hackathon-s2e5-worst-dev-urges/</link><guid isPermaLink="true">https://codetv.dev/blog/web-dev-challenge-hackathon-s2e5-worst-dev-urges/</guid><description>In this challenge, we want you to give in to that urge and automate something in your life that you find mildly inconvenient. Apps due July 14, 2025.
</description><pubDate>Tue, 01 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/f_auto/q_auto/v1751386931/wdc-hackathon-s2e5-worst-dev-tendencies-intuit-lg-v1.jpg&quot; alt=&quot;Web Dev Challenge Hackathon S2.E5: Give in to your worst developer urges
&quot; /&gt;&lt;/p&gt;&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/f_auto/q_auto/v1751386931/wdc-hackathon-s2e5-worst-dev-tendencies-intuit-lg-v1.jpg&quot; alt=&quot;Bryn, Jacob, Lyn, Ebrima, Ben, and Seve on the set of Web Dev Challenge. The text reads, &amp;quot;Web Dev Challenge community hackathon, S2.E5 Worst Developer Urges by Intuit and CodeTV&amp;quot;&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;h2&gt;Give in to your worst developer urges and automate something unnecessary&lt;/h2&gt;
&lt;p&gt;As developers, we all know the urge to spend hours automating a task that would have taken us minutes is overpowering. In this challenge, we want you to give in to that urge and automate something in your life that you find mildly inconvenient.&lt;/p&gt;
&lt;p&gt;Why &lt;em&gt;do&lt;/em&gt; a thing when you could spend an inordinate amount of time building an app that does the thing &lt;em&gt;for you&lt;/em&gt;? No task is too small. No inconvenience too minor. We want to see you create apps that automate your life in extremely specific, personal ways.&lt;/p&gt;
&lt;p&gt;Do you have to send that one friend a reminder text to make sure they show up on time for your plans? Build an app for that! Sick of doing tech support for your family? Create an AI agent that reads the manual and responds on your behalf. You can keep it simple and practical, or go big and channel your inner Rube Goldberg — anything goes!&lt;/p&gt;
&lt;h2&gt;The Sponsor: Intuit&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://codetv.link/intuit&quot;&gt;Intuit&lt;/a&gt; wants to empower the builders in our community. There’s no required tool this time around — &lt;em&gt;you&lt;/em&gt; are the secret ingredient here.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Combine your skills in web development, problem solving, creative thinking, and leveraging new technologies like AI agents or MCP to build your personal problem-solving web app.&lt;/p&gt;
&lt;h2&gt;Make new connections and get expert guidance&lt;/h2&gt;
&lt;p&gt;We&apos;ve set up a dedicated channel in the &lt;a href=&quot;https://codetv.link/discord&quot;&gt;CodeTV Discord&lt;/a&gt; called &lt;code&gt;#builder-chat&lt;/code&gt; for brainstorming, sharing ideas, and keeping each other accountable.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;A note from Jason:&lt;/strong&gt; don&apos;t miss out on the chance to make connections with industry pros and get hands-on feedback on your code! Opportunities like these are a chance to &lt;a href=&quot;https://codetv.link/manufacture-luck&quot;&gt;manufacture your own luck&lt;/a&gt; — don&apos;t pass it up, especially if you&apos;re looking for new opportunities!&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;The first 5 devs to submit a qualifying app get their choice of gear&lt;/h2&gt;
&lt;p&gt;The first 5 qualifying apps submitted will receive an item of their choice, up to $150 value, from one of the following sites:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://ugmonk.com/&quot;&gt;Ugmonk&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://drop.com/home&quot;&gt;Drop&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cocooninnovations.com/&quot;&gt;Cocoon&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Shipping is limited to countries where Ugmonk, Drop, and Cocoon are able to deliver. See their websites for specifics.&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;Watch the episode for inspiration&lt;/h2&gt;
&lt;p&gt;See how Lyn and Ebrima, Bryn and Jacob, and Ben and Seve tackled the challenge in the latest episode of &lt;a href=&quot;https://codetv.link/wdc/s2e5&quot;&gt;&lt;em&gt;Web Dev Challenge&lt;/em&gt;&lt;/a&gt; to jumpstart your own creativity.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/YQ41RpkuhRA&quot;&gt;Watch on YouTube&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;The rules and how to submit&lt;/h2&gt;
&lt;p&gt;If you want to play along, here’s how:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Team up with a friend or work on your onw&lt;/li&gt;
&lt;li&gt;Build a web app that meets the challenge&lt;/li&gt;
&lt;li&gt;Spend 30 minutes¹ planning your app&lt;/li&gt;
&lt;li&gt;Spend 4 hours¹ building your app&lt;/li&gt;
&lt;li&gt;Use &lt;a href=&quot;https://codetv.link/intuit&quot;&gt;Intuit&lt;/a&gt; to build your web app&lt;/li&gt;
&lt;li&gt;Publish the source code as a public GitHub repo&lt;/li&gt;
&lt;li&gt;Publish the web app to a public URL&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://codetv.link/wdc-hackathon-s2e5-submit&quot;&gt;Submit your web app&lt;/a&gt; by 11:59 pm Pacific on Monday, July 14, 2025&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;&lt;a href=&quot;https://codetv.link/wdc-hackathon-s2e5-submit&quot;&gt;Submit your web app&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Happy building! Let&apos;s have some fun. &lt;/p&gt;
&lt;aside&gt;&lt;p&gt;¹ The time limit is intended to make this a quick project that can fit most schedules. That being said, if you decide to spend more time on this, we&apos;ll never let a silly thing like rules stand in the way of a good time.&lt;/p&gt;&lt;/aside&gt;</content:encoded></item><item><title>Web Dev Challenge Hackathon S2.E3: Create a devious web-based video player
</title><link>https://codetv.dev/blog/web-dev-challenge-hackathon-s2e3-devious-video-player-mux/</link><guid isPermaLink="true">https://codetv.dev/blog/web-dev-challenge-hackathon-s2e3-devious-video-player-mux/</guid><description>Build the most unhinged, Byzantine, devious, unusual video player UX you can imagine. Apps due June 15, 2025.
</description><pubDate>Tue, 13 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/f_auto/q_auto/v1747103620/wdc/wdc-hackathon-s2e3-video-player-mux-lg-v1.jpg&quot; alt=&quot;Web Dev Challenge Hackathon S2.E3: Create a devious web-based video player
&quot; /&gt;&lt;/p&gt;&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/f_auto/q_auto/v1747103620/wdc/wdc-hackathon-s2e3-video-player-mux-lg-v1.jpg&quot; alt=&quot;Chan and Chance, Eve and Roxy, and Braydon and Devyn on the set of Web Dev Challenge. the text reads, &amp;quot;Web Dev Challenge community hackathon, S2.E3 Worst Video Player by Mux and CodeTV&amp;quot;&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;h2&gt;Build the most unhinged, Byzantine, devious, unusual video player UX you can imagine.&lt;/h2&gt;
&lt;p&gt;Programmer social media passes around intentionally bad UI designs every once in a while, and it’s both very fun and also deeply painful to see how creatively a developer can make something like a simple button hurt so much to use.&lt;/p&gt;
&lt;p&gt;Your challenge is to embody this kind of mischievous energy and build out a truly heinous video player UX built on top of the Mux video player base. The video &lt;em&gt;must&lt;/em&gt; be actually playable (though we’ll let you get creative with what “playable” means).&lt;/p&gt;
&lt;p&gt;Apps must use &lt;a href=&quot;https://codetv.link/mux&quot;&gt;Mux&lt;/a&gt; as part of the build.&lt;/p&gt;
&lt;h3&gt;Introduction to Mux&lt;/h3&gt;
&lt;p&gt;In the kickoff call for this hackathon, Dave Kiss from Mux gives an overview of how Mux and Media Chrome works.&lt;/p&gt;
&lt;figure&gt;&lt;/figure&gt;
&lt;h2&gt;Big prizes for this hackathon!&lt;/h2&gt;
&lt;p&gt;We always have goodies for the hackathon, but Mux is stepping it up this time:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Winning submission gets a trip to Vercel Ship&lt;/strong&gt; — fly to New York City and attend the conference. Mux covers your ticket, flight, and hotel.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Runner up gets an Elgato streaming bundle&lt;/strong&gt; — Prompter, 2x Key Light MK.2, Stream Deck, and Wave:3 mic.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;First 50 entrants get a Media Chrome Thrift-tee&lt;/strong&gt; — Retro vibes guaranteed to help you learn to write COBOL&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;First 100 entrants get $100 Mux credit&lt;/strong&gt; — Get Mux bucks, on us.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href=&quot;https://worst.player.style/&quot;&gt;Get more details on the Mux site&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Make new connections and get expert guidance&lt;/h2&gt;
&lt;p&gt;We&apos;ve set up a dedicated channel in the &lt;a href=&quot;https://codetv.link/discord&quot;&gt;CodeTV Discord&lt;/a&gt; called &lt;code&gt;#builder-chat&lt;/code&gt; for brainstorming, sharing ideas, and keeping each other accountable.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;A note from Jason:&lt;/strong&gt; don&apos;t miss out on the chance to make connections with industry pros and get hands-on feedback on your code! Opportunities like these are a chance to &lt;a href=&quot;https://codetv.link/manufacture-luck&quot;&gt;manufacture your own luck&lt;/a&gt; — don&apos;t pass it up, especially if you&apos;re looking for new opportunities!&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;Find an accountabilibuddy, get expert guidance from the industry professionals, run your idea past Jason and other community members, and listen in to what the rest of the community is cooking up in this open call to kick off the hackathon.&lt;/p&gt;
&lt;p&gt;RSVP to add it to your calendar:&lt;/p&gt;
&lt;figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/figure&gt;
&lt;h2&gt;Watch the episode for inspiration&lt;/h2&gt;
&lt;p&gt;See how Chan and Chance, Eve and Roxy, and Braydon and Devyn tackled the challenge in the latest episode of &lt;a href=&quot;https://codetv.link/wdc/s2e3&quot;&gt;&lt;em&gt;Web Dev Challenge&lt;/em&gt;&lt;/a&gt; to jumpstart your own creativity.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/bF32laUxoK0&quot;&gt;Watch on YouTube&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;The rules and how to submit&lt;/h2&gt;
&lt;p&gt;If you want to play along, here’s how:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Team up with a friend or work on your onw&lt;/li&gt;
&lt;li&gt;Build a web app that meets the challenge&lt;/li&gt;
&lt;li&gt;Spend 30 minutes¹ planning your app&lt;/li&gt;
&lt;li&gt;Spend 4 hours¹ building your app&lt;/li&gt;
&lt;li&gt;Use &lt;a href=&quot;https://codetv.link/mux&quot;&gt;Mux&lt;/a&gt; to build your web app&lt;/li&gt;
&lt;li&gt;Publish the source code as a public GitHub repo&lt;/li&gt;
&lt;li&gt;Publish the web app to a public URL&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://codetv.link/wdc-hackathon-s2e3-submit&quot;&gt;Submit your web app&lt;/a&gt; by 11:59 pm Pacific on Monday, June 15, 2025&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;&lt;a href=&quot;https://codetv.link/wdc-hackathon-s2e3-submit&quot;&gt;Submit your web app&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Happy building! Let&apos;s have some fun. &lt;/p&gt;
&lt;aside&gt;&lt;p&gt;¹ The time limit is intended to make this a quick project that can fit most schedules. That being said, if you decide to spend more time on this, we&apos;ll never let a silly thing like rules stand in the way of a good time.&lt;/p&gt;&lt;/aside&gt;</content:encoded></item><item><title>Web Dev Challenge Hackathon S2.E2: Build a game played on at least 2 devices
</title><link>https://codetv.dev/blog/web-dev-challenge-hackathon-s2e2-multi-device-game-temporal/</link><guid isPermaLink="true">https://codetv.dev/blog/web-dev-challenge-hackathon-s2e2-multi-device-game-temporal/</guid><description>Build a game that requires at least 2 devices to play. Apps due May 12, 2025.
</description><pubDate>Tue, 29 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/f_auto/q_auto/v1745961852/wdc-hackathon-s2e2-game-temporal-lg-v1.jpg&quot; alt=&quot;Web Dev Challenge Hackathon S2.E2: Build a game played on at least 2 devices
&quot; /&gt;&lt;/p&gt;&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/f_auto/q_auto/v1745961852/wdc-hackathon-s2e2-game-temporal-lg-v1.jpg&quot; alt=&quot;Nikki, Sarah, Adam, Lane, Shashi, and Nick on the set of Web Dev Challenge. the text reads, &amp;quot;Web Dev Challenge community hackathon, S2.E2 Multi-Device Game, presented by Temporal and CodeTV&amp;quot;&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;h2&gt;The prompt: Build a game that’s played on at least 2 devices.&lt;/h2&gt;
&lt;p&gt;Single player, multiplayer, cooperative, competitive, or something totally different — your challenge is to come up with something fun that is played across at least two devices. Temporal’s workflow tools will allow you to manage sending information between devices dependably.&lt;/p&gt;
&lt;p&gt;Your game could be something like Jackbox, where a tv is the “game board” and each player’s phone is how they interact with it on their turn. You could make a game that uses phone APIs like the camera or gyroscope. Or you can implement a simple word game like the New York Times connection puzzles, but with the twist that it&apos;s designed to be solved collaboratively, and a session can persist beyond the players closing the web app.&lt;/p&gt;
&lt;p&gt;Get creative and have fun with it!&lt;/p&gt;
&lt;p&gt;Apps must use &lt;a href=&quot;https://codetv.link/temporal&quot;&gt;Temporal&lt;/a&gt; as part of the build.&lt;/p&gt;
&lt;h2&gt;Introduction to Temporal + devs discuss their ideas&lt;/h2&gt;
&lt;p&gt;In the kickoff call for this hackathon, Alex Garnett from Temporal gives an overview of how Temporal works, including a live coded demo, and answers questions from developers.&lt;/p&gt;
&lt;figure&gt;&lt;/figure&gt;
&lt;h2&gt;Make new connections and get expert guidance&lt;/h2&gt;
&lt;p&gt;We&apos;ve set up a dedicated channel in the &lt;a href=&quot;https://codetv.link/discord&quot;&gt;CodeTV Discord&lt;/a&gt; called &lt;code&gt;#builder-chat&lt;/code&gt; for brainstorming, sharing ideas, and keeping each other accountable.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;A note from Jason:&lt;/strong&gt; don&apos;t miss out on the chance to make connections with industry pros and get hands-on feedback on your code! Opportunities like these are a chance to &lt;a href=&quot;https://codetv.link/manufacture-luck&quot;&gt;manufacture your own luck&lt;/a&gt; — don&apos;t pass it up, especially if you&apos;re looking for new opportunities!&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;The first 5 devs to submit a qualifying app get their choice of gear&lt;/h2&gt;
&lt;p&gt;The first 5 qualifying apps submitted will receive an item of their choice, up to $150 value, from one of the following sites:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://ugmonk.com/&quot;&gt;Ugmonk&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://drop.com/home&quot;&gt;Drop&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cocooninnovations.com/&quot;&gt;Cocoon&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Shipping is limited to countries where Ugmonk, Drop, and Cocoon are able to deliver. See their websites for specifics.&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;Watch the episode for inspiration&lt;/h2&gt;
&lt;p&gt;See how Sarah and Nikki, Shashi and Nick, and Adam and Lane tackled the challenge in the latest episode of &lt;a href=&quot;https://codetv.link/wdc/s2e2&quot;&gt;&lt;em&gt;Web Dev Challenge&lt;/em&gt;&lt;/a&gt; to jumpstart your own creativity.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/ftYmXoH0V5I&quot;&gt;Watch on YouTube&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;The rules and how to submit&lt;/h2&gt;
&lt;p&gt;If you want to play along, here’s how:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Team up with a friend or work on your onw&lt;/li&gt;
&lt;li&gt;Build a web app that meets the challenge&lt;/li&gt;
&lt;li&gt;Spend 30 minutes¹ planning your app&lt;/li&gt;
&lt;li&gt;Spend 4 hours¹ building your app&lt;/li&gt;
&lt;li&gt;Use &lt;a href=&quot;https://codetv.link/temporal&quot;&gt;Temporal&lt;/a&gt; to build your web app&lt;/li&gt;
&lt;li&gt;Publish the source code as a public GitHub repo&lt;/li&gt;
&lt;li&gt;Publish the web app to a public URL&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://codetv.link/wdc-hackathon-s2e2-submit&quot;&gt;Submit your web app&lt;/a&gt; by 11:59 pm Pacific on Monday, May 12, 2025&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;&lt;a href=&quot;https://codetv.link/wdc-hackathon-s2e2-submit&quot;&gt;Submit your web app&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Happy building! Let&apos;s have some fun. &lt;/p&gt;
&lt;aside&gt;&lt;p&gt;¹ The time limit is intended to make this a quick project that can fit most schedules. That being said, if you decide to spend more time on this, we&apos;ll never let a silly thing like rules stand in the way of a good time.&lt;/p&gt;&lt;/aside&gt;</content:encoded></item><item><title>Web Dev Challenge Hackathon S2.E1: Build a Custom API + App
</title><link>https://codetv.dev/blog/web-dev-challenge-hackathon-s2e1-custom-api-postman/</link><guid isPermaLink="true">https://codetv.dev/blog/web-dev-challenge-hackathon-s2e1-custom-api-postman/</guid><description>Build a custom API — and an app that consumes it — that makes something in your life a little more convenient. Apps due Apr 28.
</description><pubDate>Tue, 15 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/f_auto/q_auto/v1744694771/wdc/wdc-hackathon-s2e1-custom-api-postman-lg-v1.jpg&quot; alt=&quot;Web Dev Challenge Hackathon S2.E1: Build a Custom API + App
&quot; /&gt;&lt;/p&gt;&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/f_auto/q_auto/v1744694771/wdc/wdc-hackathon-s2e1-custom-api-postman-lg-v1.jpg&quot; alt=&quot;Michael, Brittany, and Dave all staring intently at their code. the text reads, &amp;quot;Web Dev Challenge community hackathon, S2.E1 Custom API + App, presented by Postmand and CodeTV&amp;quot;&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;h2&gt;The prompt: Build a custom API — and an app that consumes it — that makes something in your life a little more convenient.&lt;/h2&gt;
&lt;p&gt;Not every app needs to be the Next Big Thing. Sometimes you have a specific problem, challenge, or even just a pet peeve that can be solved with software. And these days, building an app that’s just for personal use is faster than ever.&lt;/p&gt;
&lt;p&gt;For this challenge, your team needs to create a custom API that uses at least one third-party source and extends it in some way. You’ll also need to build a web app that interacts with your API. The end result should be a system that adds convenience to your life.&lt;/p&gt;
&lt;p&gt;This could be something larger and practical, like automating something tedious you have to do over and over again (e.g. turn important incoming emails into calendar invites and/or Discord notifications), or it could be something fun and silly (e.g. use data from your wearable fitness device to calculate how many mini Snickers bars you’ve earned today).&lt;/p&gt;
&lt;p&gt;Apps must use &lt;a href=&quot;https://codetv.link/postman&quot;&gt;Postman&lt;/a&gt; as part of the build.&lt;/p&gt;
&lt;h2&gt;Make new connections and get expert guidance&lt;/h2&gt;
&lt;p&gt;We&apos;ve set up a dedicated channel in the &lt;a href=&quot;https://codetv.link/discord&quot;&gt;CodeTV Discord&lt;/a&gt; called &lt;code&gt;#builder-chat&lt;/code&gt; for brainstorming, sharing ideas, and keeping each other accountable.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;A note from Jason:&lt;/strong&gt; don&apos;t miss out on the chance to make connections with industry pros and get hands-on feedback on your code! Opportunities like these are a chance to &lt;a href=&quot;https://codetv.link/manufacture-luck&quot;&gt;manufacture your own luck&lt;/a&gt; — don&apos;t pass it up, especially if you&apos;re looking for new opportunities!&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;Find an accountabilibuddy, get expert guidance from the industry professionals, run your idea past Jason and other community members, and listen in to what the rest of the community is cooking up in this open call to kick off the hackathon.&lt;/p&gt;
&lt;p&gt;RSVP to add it to your calendar:&lt;/p&gt;
&lt;figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/figure&gt;
&lt;h2&gt;The first 5 devs to submit a qualifying app get their choice of gear&lt;/h2&gt;
&lt;p&gt;The first 5 qualifying apps submitted will receive an item of their choice, up to $150 value, from one of the following sites:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://ugmonk.com/&quot;&gt;Ugmonk&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://drop.com/home&quot;&gt;Drop&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cocooninnovations.com/&quot;&gt;Cocoon&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Shipping is limited to countries where Ugmonk, Drop, and Cocoon are able to deliver. See their websites for specifics.&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;Watch the episode for inspiration&lt;/h2&gt;
&lt;p&gt;See how Michael and Will, Brittany and Dave, and Abbey and Alex tackled the challenge in the latest episode of &lt;a href=&quot;https://codetv.link/wdc/s2e1&quot;&gt;&lt;em&gt;Web Dev Challenge&lt;/em&gt;&lt;/a&gt; to jumpstart your own creativity.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/X2sEoZG8EIw&quot;&gt;Watch on YouTube&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;The rules and how to submit&lt;/h2&gt;
&lt;p&gt;If you want to play along, here’s how:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Build a web app that meets the challenge&lt;/li&gt;
&lt;li&gt;Spend 30 minutes¹ planning your app&lt;/li&gt;
&lt;li&gt;Spend 4 hours¹ building your app&lt;/li&gt;
&lt;li&gt;Use &lt;a href=&quot;https://codetv.link/postman&quot;&gt;Postman&lt;/a&gt; to build your web app&lt;/li&gt;
&lt;li&gt;Publish the source code as a public GitHub repo&lt;/li&gt;
&lt;li&gt;Publish the web app to a public URL&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://codetv.link/wdc-hackathon-s2e1-submit&quot;&gt;Submit your web app&lt;/a&gt; by 11:59 pm Pacific on Wednesday, April 28, 2025&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;&lt;a href=&quot;https://codetv.link/wdc-hackathon-s2e1-submit&quot;&gt;Submit your web app&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Happy building! Let&apos;s have some fun. &lt;/p&gt;
&lt;aside&gt;&lt;p&gt;¹ The time limit is intended to make this a quick project that can fit most schedules. That being said, if you decide to spend more time on this, we&apos;ll never let a silly thing like rules stand in the way of a good time.&lt;/p&gt;&lt;/aside&gt;</content:encoded></item><item><title>Web Dev Challenge Hackathon 10: Workshop Woes
</title><link>https://codetv.dev/blog/web-dev-challenge-hackathon-s1e10-workshop-woes/</link><guid isPermaLink="true">https://codetv.dev/blog/web-dev-challenge-hackathon-s1e10-workshop-woes/</guid><description>Build an app to help Santa get his workshop in order using Sanity. Apps due Jan 1.
</description><pubDate>Sat, 14 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/f_auto/q_auto/v1734241998/wdc/wdc-hackathon-s1e10-workshop-woes-lg-v1.jpg&quot; alt=&quot;Web Dev Challenge Hackathon 10: Workshop Woes
&quot; /&gt;&lt;/p&gt;&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/v1734242030/wdc/wdc-s1e10-workshop-woes-poster.png&quot; alt=&quot;a movie poster with Santa above a pile of presents. it says, Trouble In Toyland, along with the names of the sponsors, the devs, and the advisors&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;h2&gt;The prompt: The holidays are upon us and Santa’s elves are behind schedule! Build an app to get the holiday back on track.&lt;/h2&gt;
&lt;p&gt;Santa’s workshop is in chaos. The drop on the 25th is getting closer by every hour, every day  You’ve been brought in as a consultant to help get things back in order. Your job: decide what the cause of the chaos is — then plan and build an app to solve the problem.&lt;/p&gt;
&lt;p&gt;The team at Sanity will provide us with a starter dataset and schema giving us some information about Santa’s workshop and content for all the kids in the world (the good, bad, young and old ones) — things like the staff, the inventory, services, etc. — and you can extend or modify that schema as you see fit (or, if you prefer, ignore it entirely and do something else).&lt;/p&gt;
&lt;p&gt;Apps must use &lt;a href=&quot;https://lwj.dev/sanity&quot;&gt;Sanity&lt;/a&gt; as part of the build.&lt;/p&gt;
&lt;h2&gt;Make new connections and get expert guidance&lt;/h2&gt;
&lt;p&gt;We&apos;ve set up a dedicated channel in the &lt;a href=&quot;https://lwj.dev/discord&quot;&gt;Learn With Jason Discord&lt;/a&gt; called &lt;code&gt;#builder-chat&lt;/code&gt; for brainstorming, sharing ideas, and keeping each other accountable.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;A note from Jason:&lt;/strong&gt; don&apos;t miss out on the chance to make connections with industry pros and get hands-on feedback on your code! Opportunities like these are a chance to &lt;a href=&quot;https://lwj.dev/manufacture-luck&quot;&gt;manufacture your own luck&lt;/a&gt; — don&apos;t pass it up, especially if you&apos;re looking for new opportunities!&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;Find an accountabilibuddy, get expert guidance from the industry professionals, run your idea past Jason and other community members, and listen in to what the rest of the community is cooking up in this open call to kick off the hackathon.&lt;/p&gt;
&lt;p&gt;RSVP to add it to your calendar:&lt;/p&gt;
&lt;figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/figure&gt;
&lt;h2&gt;The first 5 devs to submit a qualifying app get their choice of gear&lt;/h2&gt;
&lt;p&gt;The first 5 qualifying apps submitted will receive an item of their choice, up to $150 value, from one of the following sites:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://ugmonk.com/&quot;&gt;Ugmonk&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://drop.com/home&quot;&gt;Drop&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cocooninnovations.com/&quot;&gt;Cocoon&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Shipping is limited to countries where Ugmonk, Drop, and Cocoon are able to deliver. See their websites for specifics.&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;Watch the episode for inspiration&lt;/h2&gt;
&lt;p&gt;See how Aaron, Charlie, Kent, and Dom tackled the challenge in the latest episode of &lt;a href=&quot;https://lwj.dev/wdc/s1e10&quot;&gt;&lt;em&gt;Web Dev Challenge&lt;/em&gt;&lt;/a&gt; to jumpstart your own creativity.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/_H5pnNeCJWQ&quot;&gt;Watch on YouTube&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;The rules and how to submit&lt;/h2&gt;
&lt;p&gt;If you want to play along, here’s how:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Build a web app that meets the challenge&lt;/li&gt;
&lt;li&gt;Spend 30 minutes¹ planning your app&lt;/li&gt;
&lt;li&gt;Spend 4 hours¹ building your app&lt;/li&gt;
&lt;li&gt;Use &lt;a href=&quot;https://lwj.dev/sanity&quot;&gt;Sanity&lt;/a&gt; to build your web app&lt;/li&gt;
&lt;li&gt;Publish the source code as a public GitHub repo&lt;/li&gt;
&lt;li&gt;Publish the web app to a public URL&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://lwj.dev/wdc-hackathon-10-submit&quot;&gt;Submit your web app&lt;/a&gt; by 11:59 pm Pacific on Wednesday, January 1, 2025&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;&lt;a href=&quot;https://lwj.dev/wdc-hackathon-10-submit&quot;&gt;Submit your web app&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Happy building! Let&apos;s have some fun. &lt;/p&gt;
&lt;aside&gt;&lt;p&gt;¹ The time limit is intended to make this a quick project that can fit most schedules. That being said, if you decide to spend more time on this, we&apos;ll never let a silly thing like rules stand in the way of a good time.&lt;/p&gt;&lt;/aside&gt;</content:encoded></item><item><title>Web Dev Challenge Hackathon 9: Blvck Spades
</title><link>https://codetv.dev/blog/web-dev-challenge-hackathon-s1e9-blvck-spades/</link><guid isPermaLink="true">https://codetv.dev/blog/web-dev-challenge-hackathon-s1e9-blvck-spades/</guid><description>Build an app to connect people to the Blvck Spades brand through other experiences. Apps due Dec 16.
</description><pubDate>Tue, 03 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/f_auto/q_auto/v1733286735/wdc/wdc-hackathon-s1e9-blvck-spades-lg-v1.jpg&quot; alt=&quot;Web Dev Challenge Hackathon 9: Blvck Spades
&quot; /&gt;&lt;/p&gt;&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/v1733283149/wdc/Blvck_Sands_Final.jpg&quot; alt=&quot;a movie poster with an Egyptian themed profile of Anubis, with hieroglyphics-inspired details inside the profile. it says, Web Dev Challenge, along with the names of the sponsors, the devs, and the advisors&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Heads up!&lt;/strong&gt; This episode was a special collaboration with RenderATL. Thanks to Justin and team for lining up the venue and in-person hackathon! &lt;a href=&quot;https://lwj.dev/renderatl&quot;&gt;Get tickets to RenderATL 2025&lt;/a&gt; and come hang out with me and a ton of other great folks!&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;The prompt: Come up with ways to connect people’s behavior to e-commerce incentives&lt;/h2&gt;
&lt;p&gt;Creating brand loyalty is everything to an e-commerce store. Our friends at &lt;a href=&quot;https://blvckspades.com/&quot;&gt;Blvck Spades&lt;/a&gt; have a great product, and they need help coming up with ways to incentivize users to interact with their brand.&lt;/p&gt;
&lt;p&gt;Your challenge is to build software that connects a user’s activity to incentives. Beyond an e-commerce store, many businesses might have an app, partnerships, or physical locations or events — our job is to connect a user’s activity in these other places to incentives in the e-commerce store using the Mailchimp users as the source of truth.&lt;/p&gt;
&lt;p&gt;For example, something like “play 10 rounds of our game and get 10% off your order in the store”, or “show up to a local meetup and earn an exclusive, limited edition t-shirt”.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;IMPORTANT:&lt;/strong&gt; You &lt;em&gt;do not&lt;/em&gt; have to build the full e-commerce store part of this! In fact, it’s probably not possible given the time constraint. Instead, focus on building the fun interaction thing and making the note in Mailchimp that the user has done the thing that earns the incentive.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;Apps must use &lt;a href=&quot;https://lwj.dev/mailchimp&quot;&gt;Mailchimp&lt;/a&gt; as part of the build.&lt;/p&gt;
&lt;h2&gt;Make new connections and get expert guidance&lt;/h2&gt;
&lt;p&gt;We&apos;ve set up a dedicated channel in the &lt;a href=&quot;https://lwj.dev/discord&quot;&gt;Learn With Jason Discord&lt;/a&gt; called &lt;code&gt;#builder-chat&lt;/code&gt; for brainstorming, sharing ideas, and keeping each other accountable.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;A note from Jason:&lt;/strong&gt; don&apos;t miss out on the chance to make connections with industry pros and get hands-on feedback on your code! Opportunities like these are a chance to &lt;a href=&quot;https://lwj.dev/manufacture-luck&quot;&gt;manufacture your own luck&lt;/a&gt; — don&apos;t pass it up, especially if you&apos;re looking for new opportunities!&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;Find an accountabilibuddy, get expert guidance from the industry professionals, run your idea past Jason and other community members, and listen in to what the rest of the community is cooking up in this open call to kick off the hackathon.&lt;/p&gt;
&lt;p&gt;RSVP to add it to your calendar:&lt;/p&gt;
&lt;figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/figure&gt;
&lt;h2&gt;The first 5 devs to submit a qualifying app get their choice of gear&lt;/h2&gt;
&lt;p&gt;The first 5 qualifying apps submitted will receive an item of their choice, up to $150 value, from one of the following sites:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://ugmonk.com/&quot;&gt;Ugmonk&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://drop.com/home&quot;&gt;Drop&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cocooninnovations.com/&quot;&gt;Cocoon&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Shipping is limited to countries where Ugmonk, Drop, and Cocoon are able to deliver. See their websites for specifics.&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;Watch the episode for inspiration&lt;/h2&gt;
&lt;p&gt;See how Bree, Robbie, Ximena, and Anthony tackled the challenge in the latest episode of &lt;a href=&quot;https://lwj.dev/wdc/s1e9&quot;&gt;&lt;em&gt;Web Dev Challenge&lt;/em&gt;&lt;/a&gt; to jumpstart your own creativity.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/ICpQQxD8vc4&quot;&gt;Watch on YouTube&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;The rules and how to submit&lt;/h2&gt;
&lt;p&gt;If you want to play along, here’s how:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Build a web app that meets the challenge&lt;/li&gt;
&lt;li&gt;Spend 30 minutes¹ planning your app&lt;/li&gt;
&lt;li&gt;Spend 4 hours¹ building your app&lt;/li&gt;
&lt;li&gt;Use &lt;a href=&quot;https://lwj.dev/mailchimp&quot;&gt;Mailchimp&lt;/a&gt; to build your web app&lt;/li&gt;
&lt;li&gt;Publish the source code as a public GitHub repo&lt;/li&gt;
&lt;li&gt;Publish the web app to a public URL&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://lwj.dev/wdc-hackathon-9-submit&quot;&gt;Submit your web app&lt;/a&gt; by 11:59 pm Pacific on Monday, Dec 16, 2024&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;&lt;a href=&quot;https://lwj.dev/wdc-hackathon-9-submit&quot;&gt;Submit your web app&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Happy building! Let&apos;s have some fun. &lt;/p&gt;
&lt;aside&gt;&lt;p&gt;¹ The time limit is intended to make this a quick project that can fit most schedules. That being said, if you decide to spend more time on this, we&apos;ll never let a silly thing like rules stand in the way of a good time.&lt;/p&gt;&lt;/aside&gt;</content:encoded></item><item><title>Web Dev Challenge Hackathon 7: Spooky Apps
</title><link>https://codetv.dev/blog/web-dev-challenge-s1e7-spooky-hackathon/</link><guid isPermaLink="true">https://codetv.dev/blog/web-dev-challenge-s1e7-spooky-hackathon/</guid><description>Build an app to help people capture memories. Apps due Nov 11.
</description><pubDate>Wed, 30 Oct 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/f_auto/q_auto/v1730262119/wdc/wdc-hackathon-s1e7-spooky-lg-v1.jpg&quot; alt=&quot;Web Dev Challenge Hackathon 7: Spooky Apps
&quot; /&gt;&lt;/p&gt;&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1200/f_auto/q_auto/v1730261819/wdc/WDC_Poster_18X24_ORANGE.jpg&quot; alt=&quot;a movie poster that shows a snarling wolf on an orange background, with a moon and bats behind it. the text reads &amp;quot;Byte Night&amp;quot; and lists the names of the developers and advisors&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;h2&gt;The prompt: build an app that’s haunted. Make it spooky, make it creepy — let’s have a little Halloween fun.&lt;/h2&gt;
&lt;p&gt;Your app should get into the Halloween spirit in its functionality, user experience, or design. The specifics are up to you: it can be fun or funny, like a Scooby-Doo episode; it can be scary like a horror movie; it can be something totally unexpected.&lt;/p&gt;
&lt;p&gt;Let’s build some apps to put a few “bytes” into Fright Night!&lt;/p&gt;
&lt;p&gt;Apps must use &lt;a href=&quot;https://lwj.dev/convex&quot;&gt;Convex&lt;/a&gt; as part of the build.&lt;/p&gt;
&lt;h2&gt;Make new connections and get expert guidance&lt;/h2&gt;
&lt;p&gt;We&apos;ve set up a dedicated channel in the &lt;a href=&quot;https://lwj.dev/discord&quot;&gt;Learn With Jason Discord&lt;/a&gt; called &lt;code&gt;#builder-chat&lt;/code&gt; for brainstorming, sharing ideas, and keeping each other accountable.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;A note from Jason:&lt;/strong&gt; don&apos;t miss out on the chance to make connections with industry pros and get hands-on feedback on your code! Opportunities like these are a chance to &lt;a href=&quot;https://lwj.dev/manufacture-luck&quot;&gt;manufacture your own luck&lt;/a&gt; — don&apos;t pass it up, especially if you&apos;re looking for new opportunities!&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;Find an accountabilibuddy, get expert guidance from the industry professionals, run your idea past Jason and other community members, and listen in to what the rest of the community is cooking up in this open call to kick off the hackathon.&lt;/p&gt;
&lt;p&gt;RSVP to add it to your calendar:&lt;/p&gt;
&lt;figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/figure&gt;
&lt;h2&gt;The first 5 devs to submit a qualifying app get their choice of gear&lt;/h2&gt;
&lt;p&gt;The first 5 qualifying apps submitted will receive an item of their choice, up to $150 value, from one of the following sites:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://ugmonk.com/&quot;&gt;Ugmonk&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://drop.com/home&quot;&gt;Drop&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cocooninnovations.com/&quot;&gt;Cocoon&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Shipping is limited to countries where Ugmonk, Drop, and Cocoon are able to deliver. See their websites for specifics.&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;Watch the episode for inspiration&lt;/h2&gt;
&lt;p&gt;See how Joel, Doug, Danny, and Jason tackled the challenge in the latest episode of &lt;a href=&quot;https://lwj.dev/wdc&quot;&gt;&lt;em&gt;Web Dev Challenge&lt;/em&gt;&lt;/a&gt; to jumpstart your own creativity.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/fNDSDWJaj2M&quot;&gt;Watch on YouTube&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;The rules and how to submit&lt;/h2&gt;
&lt;p&gt;If you want to play along, here’s how:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Build a web app that meets the challenge&lt;/li&gt;
&lt;li&gt;Spend 30 minutes¹ planning your app&lt;/li&gt;
&lt;li&gt;Spend 4 hours¹ building your app&lt;/li&gt;
&lt;li&gt;Use &lt;a href=&quot;https://lwj.dev/convex&quot;&gt;Convex&lt;/a&gt; to build your web app&lt;/li&gt;
&lt;li&gt;Publish the source code as a public GitHub repo&lt;/li&gt;
&lt;li&gt;Publish the web app to a public URL&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://lwj.dev/wdc-hackathon-7-submit&quot;&gt;Submit your web app&lt;/a&gt; by 11:59 pm Pacific on Monday, Nov 11, 2024&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;&lt;a href=&quot;https://lwj.dev/wdc-hackathon-7-submit&quot;&gt;Submit your web app&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Happy building! Let&apos;s have some fun. &lt;/p&gt;
&lt;aside&gt;&lt;p&gt;¹ The time limit is intended to make this a quick project that can fit most schedules. That being said, if you decide to spend more time on this, we&apos;ll never let a silly thing like rules stand in the way of a good time.&lt;/p&gt;&lt;/aside&gt;</content:encoded></item><item><title>Web Dev Challenge Hackathon 6: Capture Memories
</title><link>https://codetv.dev/blog/web-dev-challenge-s1e6-memories-hackathon/</link><guid isPermaLink="true">https://codetv.dev/blog/web-dev-challenge-s1e6-memories-hackathon/</guid><description>Build an app to help people capture memories. Apps due Oct 21.
</description><pubDate>Wed, 09 Oct 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/f_auto/q_auto/v1728514706/wdc/wdc-hackathon-s1e6-capture-memories-lg-v1.jpg&quot; alt=&quot;Web Dev Challenge Hackathon 6: Capture Memories
&quot; /&gt;&lt;/p&gt;&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1200/f_auto/q_auto/v1728513332/wdc/dana-ulama-wdc-s1e6-memories.png&quot; alt=&quot;a movie poster that shows an illustrated photo wall with a web browser, photos of friends, a speaker at a podium, some code, a cat, and a landscape. the credits for the episode are printed at the bottom&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;h2&gt;The prompt: build an app that helps people capture memories.&lt;/h2&gt;
&lt;p&gt;There are so many things we experience that might seem small in the moment, but that we’d see as a treasured memory looking back on it years from now. A special event, a dinner with loved ones, a small adventure — or a big one.&lt;/p&gt;
&lt;p&gt;Your challenge is to create an app to help people capture a special kind of memory. You get to choose what kind of memories your app will capture, but it should allow users to upload images and/or videos.&lt;/p&gt;
&lt;p&gt;Apps must use &lt;a href=&quot;https://lwj.dev/tigris&quot;&gt;Tigris Data&lt;/a&gt; as part of the build, and use of &lt;a href=&quot;https://lwj.dev/fly&quot;&gt;Fly.io&lt;/a&gt; is encouraged.&lt;/p&gt;
&lt;h2&gt;Make new connections and get expert guidance&lt;/h2&gt;
&lt;p&gt;We&apos;ve set up a dedicated channel in the &lt;a href=&quot;https://lwj.dev/discord&quot;&gt;Learn With Jason Discord&lt;/a&gt; called &lt;code&gt;#builder-chat&lt;/code&gt; for brainstorming, sharing ideas, and keeping each other accountable.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;A note from Jason:&lt;/strong&gt; don&apos;t miss out on the chance to make connections with industry pros and get hands-on feedback on your code! Opportunities like these are a chance to &lt;a href=&quot;https://lwj.dev/manufacture-luck&quot;&gt;manufacture your own luck&lt;/a&gt; — don&apos;t pass it up, especially if you&apos;re looking for new opportunities!&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;Find an accountabilibuddy, get expert guidance from the industry professionals, run your idea past Jason and other community members, and listen in to what the rest of the community is cooking up in this open call to kick off the hackathon.&lt;/p&gt;
&lt;p&gt;RSVP to add it to your calendar:&lt;/p&gt;
&lt;figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/figure&gt;
&lt;h2&gt;The first 5 devs to submit a qualifying app get their choice of gear&lt;/h2&gt;
&lt;p&gt;The first 5 qualifying apps submitted will receive an item of their choice, up to $150 value, from one of the following sites:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://ugmonk.com/&quot;&gt;Ugmonk&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://drop.com/home&quot;&gt;Drop&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cocooninnovations.com/&quot;&gt;Cocoon&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Shipping is limited to countries where Ugmonk, Drop, and Cocoon are able to deliver. See their websites for specifics.&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;Watch the episode for inspiration&lt;/h2&gt;
&lt;p&gt;See how Lawrence, Amy, Josh, and Jason tackled the challenge in the latest episode of &lt;a href=&quot;https://lwj.dev/wdc&quot;&gt;&lt;em&gt;Web Dev Challenge&lt;/em&gt;&lt;/a&gt; to jumpstart your own creativity.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/V96_3fBgvPA&quot;&gt;Watch on YouTube&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;The rules and how to submit&lt;/h2&gt;
&lt;p&gt;If you want to play along, here’s how:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Build a web app that meets the challenge&lt;/li&gt;
&lt;li&gt;Spend 30 minutes¹ planning your app&lt;/li&gt;
&lt;li&gt;Spend 4 hours¹ building your app&lt;/li&gt;
&lt;li&gt;Use &lt;a href=&quot;https://lwj.dev/tigris&quot;&gt;Tigris Data&lt;/a&gt; and, optionally, &lt;a href=&quot;https://lwj.dev/fly&quot;&gt;Fly.io&lt;/a&gt; to build your web app&lt;/li&gt;
&lt;li&gt;Publish the source code as a public GitHub repo&lt;/li&gt;
&lt;li&gt;Publish the web app to a public URL&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://lwj.dev/wdc-hackathon-6-submit&quot;&gt;Submit your web app&lt;/a&gt; by 11:59 pm Pacific on Monday, Oct 21, 2024&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;&lt;a href=&quot;https://lwj.dev/wdc-hackathon-6-submit&quot;&gt;Submit your web app&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Happy building! Let&apos;s have some fun. &lt;/p&gt;
&lt;aside&gt;&lt;p&gt;¹ The time limit is intended to make this a quick project that can fit most schedules. That being said, if you decide to spend more time on this, we&apos;ll never let a silly thing like rules stand in the way of a good time.&lt;/p&gt;&lt;/aside&gt;</content:encoded></item><item><title>Web Dev Challenge Hackathon 5: The Local Food Scene
</title><link>https://codetv.dev/blog/web-dev-challenge-s1e5-food-scene-hackathon/</link><guid isPermaLink="true">https://codetv.dev/blog/web-dev-challenge-s1e5-food-scene-hackathon/</guid><description>Build an app to help out your local food scene in any way you can. Apps due Sep 30.
</description><pubDate>Tue, 17 Sep 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/f_auto/q_auto/v1726641430/wdc/wdc-hackathon-s1e5-food-scene-lg-v1.jpg&quot; alt=&quot;Web Dev Challenge Hackathon 5: The Local Food Scene
&quot; /&gt;&lt;/p&gt;&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1200/f_auto/q_auto/v1726641399/wdc/wdc-s1e5-food-scene-dan-stiles-web.jpg&quot; alt=&quot;An illustration of a computer monitor with fire raging out of it. An arm is holding out a hot dog on a stick to roast over it. The combination looks like a smiley face. There are sticky notes on the computer that say &amp;quot;Web Dev Challenge&amp;quot;.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;h2&gt;The prompt: make something that gets people excited about their local food scene.&lt;/h2&gt;
&lt;p&gt;The way restaurants run changed in a big way during the pandemic. The big food delivery apps control which restaurants get seen, and social media algorithm changes make it harder than ever for these businesses to get noticed. Your challenge is to come up with some way of getting more people to notice — and get involved in — their local food scene.&lt;/p&gt;
&lt;p&gt;This can be anything web-based, whether you want to create some way for people to discover new restaurants and chefs, create an event or a social game, or even tackle the notoriously awful restaurant website design aesthetic.&lt;/p&gt;
&lt;p&gt;Apps must use &lt;a href=&quot;https://lwj.dev/convex&quot;&gt;Convex&lt;/a&gt;, &lt;a href=&quot;https://go.clerk.com/5ABtiiV&quot;&gt;Clerk&lt;/a&gt;, and/or &lt;a href=&quot;https://ray.so/lwj&quot;&gt;Raycast&lt;/a&gt; as part of the build.&lt;/p&gt;
&lt;h2&gt;Make new connections and get expert guidance&lt;/h2&gt;
&lt;p&gt;We&apos;ve set up a dedicated channel in the &lt;a href=&quot;https://lwj.dev/discord&quot;&gt;Learn With Jason Discord&lt;/a&gt; called &lt;code&gt;#builder-chat&lt;/code&gt; for brainstorming, sharing ideas, and keeping each other accountable.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;A note from Jason:&lt;/strong&gt; don&apos;t miss out on the chance to make connections with industry pros and get hands-on feedback on your code! Opportunities like these are a chance to &lt;a href=&quot;https://lwj.dev/manufacture-luck&quot;&gt;manufacture your own luck&lt;/a&gt; — don&apos;t pass it up, especially if you&apos;re looking for new opportunities!&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;Find an accountabilibuddy, get expert guidance from the Clerk, Convex, and Raycast teams, run your idea past Jason and other community members, and listen in to what the rest of the community is cooking up in this open call to kick off the hackathon.&lt;/p&gt;
&lt;p&gt;RSVP to add it to your calendar:&lt;/p&gt;
&lt;figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/figure&gt;
&lt;h2&gt;The first 5 devs to submit a qualifying app get their choice of gear&lt;/h2&gt;
&lt;p&gt;The first 5 qualifying apps submitted will receive an item of their choice, up to $150 value, from one of the following sites:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://ugmonk.com/&quot;&gt;Ugmonk&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://drop.com/home&quot;&gt;Drop&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cocooninnovations.com/&quot;&gt;Cocoon&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Shipping is limited to countries where Ugmonk, Drop, and Cocoon are able to deliver. See their websites for specifics.&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;Watch the episode for inspiration&lt;/h2&gt;
&lt;p&gt;See how Maxi, Natalie, John, and Jason tackled the challenge in the latest episode of &lt;a href=&quot;https://lwj.dev/wdc&quot;&gt;&lt;em&gt;Web Dev Challenge&lt;/em&gt;&lt;/a&gt; to jumpstart your own creativity.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/8Oxy6WV7zag&quot;&gt;Watch on YouTube&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;The rules and how to submit&lt;/h2&gt;
&lt;p&gt;If you want to play along, here’s how:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Build a web app that meets the challenge&lt;/li&gt;
&lt;li&gt;Spend 30 minutes¹ planning your app&lt;/li&gt;
&lt;li&gt;Spend 4 hours¹ building your app&lt;/li&gt;
&lt;li&gt;Use &lt;a href=&quot;https://lwj.dev/convex&quot;&gt;Convex&lt;/a&gt;, &lt;a href=&quot;https://go.clerk.com/5ABtiiV&quot;&gt;Clerk&lt;/a&gt;, and/or &lt;a href=&quot;https://ray.so/lwj&quot;&gt;Raycast&lt;/a&gt; to build your web app&lt;/li&gt;
&lt;li&gt;Publish the source code as a public GitHub repo&lt;/li&gt;
&lt;li&gt;Publish the web app to a public URL&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://lwj.dev/wdc-hackathon-5-submit&quot;&gt;Submit your web app&lt;/a&gt; by 11:59 pm Pacific on Monday, September 30, 2024&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;&lt;a href=&quot;https://lwj.dev/wdc-hackathon-5-submit&quot;&gt;Submit your web app&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Happy building! Let&apos;s have some fun. &lt;/p&gt;
&lt;aside&gt;&lt;p&gt;¹ The time limit is intended to make this a quick project that can fit most schedules. That being said, if you decide to spend more time on this, we&apos;ll never let a silly thing like rules stand in the way of a good time.&lt;/p&gt;&lt;/aside&gt;</content:encoded></item><item><title>Web Dev Challenge Hackathon 4: Monsters!
</title><link>https://codetv.dev/blog/web-dev-challenge-hackathon-monsters/</link><guid isPermaLink="true">https://codetv.dev/blog/web-dev-challenge-hackathon-monsters/</guid><description>Monsters have arrived, and your challenge is to build an app that helps out in this new world. Apps due Sep 9.
</description><pubDate>Tue, 27 Aug 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/f_auto/q_auto/v1724822763/wdc-hackathon-s1e4-monsters-lg-v2.jpg&quot; alt=&quot;Web Dev Challenge Hackathon 4: Monsters!
&quot; /&gt;&lt;/p&gt;&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1200/f_auto/q_auto/v1724821873/MONSTERS__web.png&quot; alt=&quot;An illustration of a monster looming over a computer control room. There are people sitting in front of monitors that are glowing green. The monster is holding a fake nose and mustache disguise, a camera, a map, and a kettlebell. It&apos;s wearing a bowtie.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;h2&gt;The prompt: monsters have arrived — build an app to help out in this new world!&lt;/h2&gt;
&lt;p&gt;What kind of monster is up to you! Maybe kaiju have emerged from the ocean. Maybe Pokémon are actually real. Maybe gremlins were discovered. Maybe cryptids finally got caught on camera. Make ‘em cute, make ‘em scary, make ‘em fun — your call!&lt;/p&gt;
&lt;p&gt;And “help” is also up for interpretation. Maybe this is an exciting scientific opportunity and your app supports that exploration. Maybe you’re helping people survive monster attacks. Or maybe you’re building an app to remote operate a monster-hunting robot to save humanity (if you can build that in 4 hours, you’re my hero).&lt;/p&gt;
&lt;p&gt;Apps must use &lt;a href=&quot;https://lwj.dev/convex&quot;&gt;Convex&lt;/a&gt;, &lt;a href=&quot;https://go.clerk.com/O2xl2oi&quot;&gt;Clerk&lt;/a&gt;, and/or &lt;a href=&quot;https://ray.so/lwj&quot;&gt;Raycast&lt;/a&gt; as part of the build.&lt;/p&gt;
&lt;h2&gt;Make new connections and get expert guidance&lt;/h2&gt;
&lt;p&gt;We&apos;ve set up a dedicated channel in the &lt;a href=&quot;https://lwj.dev/discord&quot;&gt;Learn With Jason Discord&lt;/a&gt; called &lt;code&gt;#builder-chat&lt;/code&gt; for brainstorming, sharing ideas, and keeping each other accountable.&lt;/p&gt;
&lt;p&gt;We&apos;ll have devs from the Algolia team available to provide technical guidance as you&apos;re building.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;A note from Jason:&lt;/strong&gt; don&apos;t miss out on the chance to make connections with industry pros and get hands-on feedback on your code! Opportunities like these are a chance to &lt;a href=&quot;https://lwj.dev/manufacture-luck&quot;&gt;manufacture your own luck&lt;/a&gt; — don&apos;t pass it up, especially if you&apos;re looking for new opportunities!&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;Find an accountabilibuddy, get expert guidance from the Clerk, Convex, and Raycast teams, run your idea past Jason and other community members, and listen in to what the rest of the community is cooking up in this open call to kick off the hackathon.&lt;/p&gt;
&lt;p&gt;RSVP to add it to your calendar:&lt;/p&gt;
&lt;figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/figure&gt;
&lt;h2&gt;The first 5 devs to submit a qualifying app get their choice of gear&lt;/h2&gt;
&lt;p&gt;The first 5 qualifying apps submitted will receive an item of their choice, up to $150 value, from one of the following sites:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://ugmonk.com/&quot;&gt;Ugmonk&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://drop.com/home&quot;&gt;Drop&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cocooninnovations.com/&quot;&gt;Cocoon&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Shipping is limited to countries where Ugmonk, Drop, and Cocoon are able to deliver. See their websites for specifics.&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;Watch the episode for inspiration&lt;/h2&gt;
&lt;p&gt;See how Annie, Shruti, Zrybea, and Jason tackled the challenge in the latest episode of &lt;a href=&quot;https://lwj.dev/wdc&quot;&gt;&lt;em&gt;Web Dev Challenge&lt;/em&gt;&lt;/a&gt; to jumpstart your own creativity.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/D9lWrkpVUm0&quot;&gt;Watch on YouTube&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;The rules and how to submit&lt;/h2&gt;
&lt;p&gt;If you want to play along, here’s how:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Build a web app that meets the challenge&lt;/li&gt;
&lt;li&gt;Spend 30 minutes¹ planning your app&lt;/li&gt;
&lt;li&gt;Spend 4 hours¹ building your app&lt;/li&gt;
&lt;li&gt;Use &lt;a href=&quot;https://lwj.dev/convex&quot;&gt;Convex&lt;/a&gt;, &lt;a href=&quot;https://go.clerk.com/O2xl2oi&quot;&gt;Clerk&lt;/a&gt;, and/or &lt;a href=&quot;https://ray.so/lwj&quot;&gt;Raycast&lt;/a&gt; to build your web app&lt;/li&gt;
&lt;li&gt;Publish the source code as a public GitHub repo&lt;/li&gt;
&lt;li&gt;Publish the web app to a public URL&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://lwj.dev/wdc-hackathon-4-submit&quot;&gt;Submit your web app&lt;/a&gt; by 11:59 pm Pacific on Monday, September 9, 2024&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;&lt;a href=&quot;https://lwj.dev/wdc-hackathon-4-submit&quot;&gt;Submit your web app&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Happy building! Let&apos;s have some fun. &lt;/p&gt;
&lt;aside&gt;&lt;p&gt;¹ The time limit is intended to make this a quick project that can fit most schedules. That being said, if you decide to spend more time on this, we&apos;ll never let a silly thing like rules stand in the way of a good time.&lt;/p&gt;&lt;/aside&gt;</content:encoded></item><item><title>Join the Web Dev Challenge Hackathon (E-comm Edition)
</title><link>https://codetv.dev/blog/web-dev-challenge-hackathon-algolia/</link><guid isPermaLink="true">https://codetv.dev/blog/web-dev-challenge-hackathon-algolia/</guid><description>Create an e-commerce site with a twist using Algolia by August 19. Get feedback from pro developers, build your portfolio, and make new industry friends. 
</description><pubDate>Tue, 06 Aug 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/f_auto,q_auto/v1723007230/wdc/wdc-hackathon-s1e3-algolia-large.jpg&quot; alt=&quot;Join the Web Dev Challenge Hackathon (E-comm Edition)
&quot; /&gt;&lt;/p&gt;&lt;h2&gt;The prompt: build an e-commerce site... with a twist&lt;/h2&gt;
&lt;p&gt;E-commerce makes the world go round, and we want you to shake up the standard e-comm approaches with something a little different. The twist can be anything: make us laugh, add some M. Night Shyamalan flair — have fun with it!&lt;/p&gt;
&lt;p&gt;Apps must use &lt;a href=&quot;https://lwj.dev/algolia&quot;&gt;Algolia&lt;/a&gt; as part of the build. Algolia provides solutions for quickly adding search, recommendations, and more to web apps with pre-built components, fast responses, and painless integration.&lt;/p&gt;
&lt;h2&gt;Make new connections and get expert guidance&lt;/h2&gt;
&lt;p&gt;We&apos;ve set up a dedicated channel in the &lt;a href=&quot;https://lwj.dev/discord&quot;&gt;Learn With Jason Discord&lt;/a&gt; called &lt;code&gt;#builder-chat&lt;/code&gt; for brainstorming, sharing ideas, and keeping each other accountable.&lt;/p&gt;
&lt;p&gt;We&apos;ll have devs from the Algolia team available to provide technical guidance as you&apos;re building.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;A note from Jason:&lt;/strong&gt; don&apos;t miss out on the chance to make connections with industry pros and get hands-on feedback on your code! Opportunities like these are a chance to &lt;a href=&quot;https://lwj.dev/manufacture-luck&quot;&gt;manufacture your own luck&lt;/a&gt; — don&apos;t pass it up, especially if you&apos;re looking for new opportunities!&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;Find an accountabilibuddy, get expert guidance from the Algolia team, run your idea past Jason and other community members, and listen in to what the rest of the community is cooking up in this open call to kick off the hackathon.&lt;/p&gt;
&lt;p&gt;RSVP to add it to your calendar:&lt;/p&gt;
&lt;figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/figure&gt;
&lt;h2&gt;The first 5 devs to submit a qualifying app get their choice of gear&lt;/h2&gt;
&lt;p&gt;The first 5 qualifying apps submitted will receive an item of their choice, up to $150 value, from one of the following sites:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://ugmonk.com/&quot;&gt;Ugmonk&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://drop.com/home&quot;&gt;Drop&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cocooninnovations.com/&quot;&gt;Cocoon&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Shipping is limited to countries where Ugmonk, Drop, and Cocoon are able to deliver. See their websites for specifics.&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;Watch the episode for inspiration&lt;/h2&gt;
&lt;p&gt;See how Sidney, Alex, Shaundai, and Jason tackled the challenge in the latest episode of &lt;a href=&quot;https://lwj.dev/wdc&quot;&gt;&lt;em&gt;Web Dev Challenge&lt;/em&gt;&lt;/a&gt; to jumpstart your own creativity.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/wPL13VR88iY&quot;&gt;Watch on YouTube&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;The rules and how to submit&lt;/h2&gt;
&lt;p&gt;If you want to play along, here’s how:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Build a web app that meets the challenge&lt;/li&gt;
&lt;li&gt;Spend 30 minutes¹ planning your app&lt;/li&gt;
&lt;li&gt;Spend 4 hours¹ building your app&lt;/li&gt;
&lt;li&gt;Use &lt;a href=&quot;https://lwj.dev/algolia&quot;&gt;Algolia&lt;/a&gt; as part of your web app&lt;/li&gt;
&lt;li&gt;Publish the source code as a public GitHub repo&lt;/li&gt;
&lt;li&gt;Publish the web app to a public URL&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://lwj.dev/wdc-hackathon-3-submit&quot;&gt;Submit your web app&lt;/a&gt; by 11:59 pm Pacific on Monday, August 19, 2024&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;&lt;a href=&quot;https://lwj.dev/wdc-hackathon-3-submit&quot;&gt;Submit your web app&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Happy building! Let&apos;s have some fun. &lt;/p&gt;
&lt;aside&gt;&lt;p&gt;¹ The time limit is intended to make this a quick project that can fit most schedules. That being said, if you decide to spend more time on this, we&apos;ll never let a silly thing like rules stand in the way of a good time.&lt;/p&gt;&lt;/aside&gt;</content:encoded></item><item><title>&quot;Businesses hate fun&quot; and other lies, with Salma Alam-Naylor
</title><link>https://codetv.dev/blog/businesses-hate-fun-other-lies-salma-alam-naylor/</link><guid isPermaLink="true">https://codetv.dev/blog/businesses-hate-fun-other-lies-salma-alam-naylor/</guid><description>Contrary to popular belief, metrics are not preventing us from being creative. (Hot take: they might even make us MORE creative.)
</description><pubDate>Mon, 08 Jul 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/f_auto,q_auto,w_1280/v1720490994/web-lunch/web-lunch-salma-v2.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://youtu.be/mMnm2BzOk1w&quot;&gt;Watch on YouTube&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;What to expect in this episode of Web Lunch&lt;/h2&gt;
&lt;p&gt;I had the pleasure of joining &lt;a href=&quot;https://whitep4nth3r.com&quot;&gt;Salma Alam-Naylor&lt;/a&gt; for a meal and talking to her about being a person vs. being a persona, aligning creativity and self-expression with business goals, and inventing villains as an excuse for not doing our best work.&lt;/p&gt;
&lt;h3&gt;Coming to terms with being extremely online&lt;/h3&gt;
&lt;p&gt;How much of yourself is too much to put online? Should we &lt;em&gt;truly&lt;/em&gt; be ourselves, or a persona that we inhabit online? This is... complicated. We dig into it.&lt;/p&gt;
&lt;h3&gt;How to reconcile our creativity with business needs&lt;/h3&gt;
&lt;p&gt;Is it possible to be creative at a job that expects us to deliver business value? (Spoiler: yes.) Salma and I discuss how we find creative opportunities within the constraints of a full-time job, the power of learning the business side of things, and our strategies for connecting our ideas to monetary return to the company.&lt;/p&gt;
&lt;h3&gt;How to get buy-in for creative ideas&lt;/h3&gt;
&lt;p&gt;It&apos;s possible to pitch and work on extremely fun, creatively challenging work as part of your day job — if we can convince stakeholders that it&apos;s going to get them great results. We talk about how to make the business case for creative ideas.&lt;/p&gt;
&lt;h3&gt;What success actually means (maybe)&lt;/h3&gt;
&lt;p&gt;How do you know you&apos;re &quot;successful&quot;? Is it financial gain? Fame? Title? Salma talks about different measures of success that are more fulfilling than any of these for her.&lt;/p&gt;
&lt;p&gt;We talk about what kind of impact we care about — and which kinds we actively reject these days.&lt;/p&gt;
&lt;h2&gt;What is Web Lunch?&lt;/h2&gt;
&lt;p&gt;In &lt;a href=&quot;https://www.youtube.com/playlist?list=PLz8Iz-Fnk_eQKzDS0xO6FEA6ho8i3E3o2&quot;&gt;Web Lunch&lt;/a&gt;, host Jason Lengstorf joins talented, successful, and — most importantly — &lt;em&gt;happy&lt;/em&gt; web professionals for a meal and a conversation.&lt;/p&gt;
&lt;p&gt;These experts have:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Built careers that are resilient in the face of an ever-changing software industry&lt;/li&gt;
&lt;li&gt;Achieved proper compensation and recognition for their efforts&lt;/li&gt;
&lt;li&gt;Found joy and growth in their work — instead (or sometimes on the other side) of burnout&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In each conversation, Jason aims to learn the strategies, mental frameworks, and techniques that got them there. Watch to learn how you can build your own resilient, lucrative, joyful career.&lt;/p&gt;</content:encoded></item><item><title>Inside Figma’s HQ: talking creativity with Jake Albaugh
</title><link>https://codetv.dev/blog/inside-figma-hq-jake-albaugh-creative-communities/</link><guid isPermaLink="true">https://codetv.dev/blog/inside-figma-hq-jake-albaugh-creative-communities/</guid><description>I talked to Jake Albaugh inside Figma HQ’s secret library about creative communities in coding and design — and whether there are any left.
</description><pubDate>Mon, 01 Jul 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/f_auto,q_auto,w_1280/v1719881295/jake-albaugh-figma-v3.jpg&quot; alt=&quot;Inside Figma’s HQ: talking creativity with Jake Albaugh
&quot; /&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://youtu.be/FcQ1pBgfKvo&quot;&gt;Watch on YouTube&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Where are the communities for creative coders? For designers? Not just to show their work, but to actually connect, collaborate, and grow together? I got the chance to visit Figma&apos;s developer advocate, &lt;a href=&quot;https://jake.fun/&quot;&gt;Jake Albaugh&lt;/a&gt;, in their San Francisco office to talk about why creativity matters — and how to find people to share and grow with.&lt;/p&gt;
&lt;p&gt;Jake Albaugh wants you to build stuff simply for the joy of creation. The act of creating has intrinsic value; it doesn&apos;t &lt;em&gt;need&lt;/em&gt; to be monetized to matter. This mindset is crucial for fostering innovation and maintaining a sense of curiosity in an increasingly commercialized digital landscape.&lt;/p&gt;
&lt;p&gt;The web started out as an open playground, filled with limitless possibilities and mostly free of consequences. Creative experimentation was the norm, because no one had agreed on how to use the internet yet. But as the internet has become more of a vehicle for commerce, the spirit of free experimentation moved to the fringes.&lt;/p&gt;
&lt;p&gt;&quot;It feels like there are fewer and fewer places to do that goofy stuff,&quot; Jake said. There&apos;s pressure to make everything commercially viable — to always be &lt;em&gt;productive&lt;/em&gt; — that makes the idea of playful experimentation start to feel like a guilty pleasure.&lt;/p&gt;
&lt;p&gt;But play is a core part of how we learn. Making space to goof around with something new in a low-stakes environment is a proven path to innovation. It&apos;s counterintuitive, but actively choosing &lt;em&gt;not&lt;/em&gt; to be productive is a great way to discover new ideas and connections that might just end up as the catalyst for your next great career move.&lt;/p&gt;
&lt;p&gt;Jake tries to lead by example here, constantly using (and misusing) tools to make things that generate absolutely no shareholder value, and along the way he&apos;s been able to create a body of work that landed him a job as a developer advocate at &lt;a href=&quot;https://figma.com&quot;&gt;Figma&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The importance of community is something Jake believes in strongly. Surrounding yourself with others who celebrate exploration and playful uses of technology is the key to staying inspired.&lt;/p&gt;
&lt;p&gt;Everything is so serious — finding a way to stay playful, to stay curious, gives us a chance to find joy in a world that can otherwise try to force us to only value direct return on investment.&lt;/p&gt;
&lt;p&gt;The fun is the point. The community is the point. The growth is a bonus.&lt;/p&gt;
&lt;p&gt;If you want to keep goofing around on the web, do it in public! Be weird! Share it with Jake and me! Bring back fun on the web. &lt;/p&gt;</content:encoded></item><item><title>Join the Web Dev Challenge mini-hackathon (retro gaming edition)
</title><link>https://codetv.dev/blog/web-dev-challenge-giveaway-full-stack-amplify/</link><guid isPermaLink="true">https://codetv.dev/blog/web-dev-challenge-giveaway-full-stack-amplify/</guid><description>Build a web app using AWS Amplify to boost your portfolio, make professional connections, learn new skills — and even win prizes. Due July 29, 2024.
</description><pubDate>Mon, 24 Jun 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/f_auto,q_auto,w_1200/wdc/wdc-aws-retro-games-v3.jpg&quot; alt=&quot;Join the Web Dev Challenge mini-hackathon (retro gaming edition)
&quot; /&gt;&lt;/p&gt;&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Heads up!&lt;/strong&gt; Submissions for this hackathon are now closed. &lt;a href=&quot;/newsletter&quot;&gt;Join the newsletter&lt;/a&gt; to hear about future hackathons first!&lt;/p&gt;&lt;/aside&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/f_auto,q_auto,w_1200/wdc/wdc-aws-retro-games-v3.jpg&quot; alt=&quot;AWS + LWJ — build a retro app, win a prize!&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;h2&gt;Watch the episode for inspiration&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/jVHoffCdKa4&quot;&gt;Watch on YouTube&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;See how Dev, Ben, Lindsay, and Jason tackled the challenge to get your creative gears turning!&lt;/p&gt;
&lt;h2&gt;What is AWS Amplify?&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://aws.amazon.com/amplify?utm_source=learnwithjason&amp;amp;utm_medium=video&amp;amp;utm_campaign=wdc&quot;&gt;AWS Amplify&lt;/a&gt; has everything you need to create and deploy web and mobile apps in just a few hours! In this challenge, you&apos;ll have the opportunity to get started with AWS and level up your skill set.&lt;/p&gt;
&lt;p&gt;Amplify has a variety of features that you can use in this challenge.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.amplify.aws/react/how-amplify-works/concepts/?utm_source=learnwithjason&amp;amp;utm_medium=video&amp;amp;utm_campaign=wdc#data&quot;&gt;Data&lt;/a&gt; - Build secure, real-time APIs backed by AWS databases.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.amplify.aws/react/how-amplify-works/concepts/?utm_source=learnwithjason&amp;amp;utm_medium=video&amp;amp;utm_campaign=wdc#auth&quot;&gt;Auth&lt;/a&gt; - Enable secure authentication flows and control access to data, files, and more.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.amplify.aws/react/build-a-backend/storage/?utm_source=learnwithjason&amp;amp;utm_medium=video&amp;amp;utm_campaign=wdc&quot;&gt;Storage&lt;/a&gt; - Storage and manage app content and data.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.amplify.aws/react/build-a-backend/functions/?utm_source=learnwithjason&amp;amp;utm_medium=video&amp;amp;utm_campaign=wdc&quot;&gt;Fuctions&lt;/a&gt; - Add functions and configure environment variables.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://aws.amazon.com/amplify/hosting/?utm_source=learnwithjason&amp;amp;utm_medium=video&amp;amp;utm_campaign=wdc&quot;&gt;Hosting&lt;/a&gt; - Deploy your website globally.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To get started, use the &lt;a href=&quot;https://docs.amplify.aws/react/start/quickstart/?utm_source=learnwithjason&amp;amp;utm_medium=video&amp;amp;utm_campaign=wdc&quot;&gt;quickstart guide&lt;/a&gt;. Amplify is part of the &lt;a href=&quot;https://aws.amazon.com/amplify/pricing/?nc=sn&amp;amp;loc=4&amp;amp;utm_source=learnwithjason&amp;amp;utm_medium=video&amp;amp;utm_campaign=wdc&quot;&gt;AWS free tier&lt;/a&gt;!&lt;/p&gt;
&lt;h2&gt;You have the chance to win a dream desk setup!&lt;/h2&gt;
&lt;p&gt;In every episode of &lt;a href=&quot;https://lwj.dev/wdc&quot;&gt;&lt;em&gt;Web Dev Challenge&lt;/em&gt;&lt;/a&gt;, developers from around the community challenge themselves to build an app that meets a prompt and uses a given tool.&lt;/p&gt;
&lt;p&gt;Now you can be one of those devs! &lt;strong&gt;Build your own take on the app prompt and submit it to the showcase&lt;/strong&gt; to learn something new, show off your work, have some fun with the web dev community, and learn (or practice) practical skills.&lt;/p&gt;
&lt;p&gt;And, as a bonus, &lt;strong&gt;one dev will win a dream desk setup&lt;/strong&gt;: &lt;a href=&quot;https://ugmonk.com/collections/the-gather-collection/products/the-complete-gather-collection-black-maple?variant=42601772220566&quot;&gt;Ugmonk&apos;s complete Gather collection&lt;/a&gt;.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/f_auto,q_auto,w_1280/v1714343015/4d1a/complete-gather-collection.jpg&quot; alt=&quot;The complete Gather Collection from Ugmonk&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Please note:&lt;/strong&gt; while anyone can participate, only US residents are eligible to win the desk setup at this time. We&apos;re exploring the legal requirements of opening up the giveaway to more countries in future giveaways, but this stuff is &lt;em&gt;complicated&lt;/em&gt;. Thanks for your understanding.&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;The prompt: “build a retro games-themed web app using AWS Amplify”&lt;/h2&gt;
&lt;p&gt;For this episode, the devs are building a web app for any kind of retro gaming theme they can think of. A Dr. Mario tournament app? Pong leaderboard? Investment advice for Monopoly players? It&apos;s all fair game!&lt;/p&gt;
&lt;p&gt;Amazon Web Services is sponsoring this episode and giveaway, and the apps must be built using &lt;a href=&quot;https://lwj.dev/amplify&quot;&gt;AWS Amplify&lt;/a&gt;, which can be used for the frontend deployment and/or for the backend of your web app.&lt;/p&gt;
&lt;h2&gt;Make new connections and get expert guidance&lt;/h2&gt;
&lt;p&gt;We&apos;ve set up a dedicated channel in the &lt;a href=&quot;https://lwj.dev/discord&quot;&gt;Learn With Jason Discord&lt;/a&gt; called &lt;code&gt;#builder-chat&lt;/code&gt; for brainstorming, sharing ideas, and keeping each other accountable.&lt;/p&gt;
&lt;p&gt;We&apos;ve also got our friends from the AWS Amplify team available to provide technical guidance as you&apos;re building.&lt;/p&gt;
&lt;h2&gt;The rules and how to submit&lt;/h2&gt;
&lt;p&gt;If you want to play along, here’s how:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Build a web app that has a retro games theme&lt;/li&gt;
&lt;li&gt;Spend 30 minutes planning your app¹&lt;/li&gt;
&lt;li&gt;Spend 4 hours building your app¹&lt;/li&gt;
&lt;li&gt;Use &lt;a href=&quot;https://lwj.dev/amplify&quot;&gt;AWS Amplify&lt;/a&gt; as part of your web app&lt;/li&gt;
&lt;li&gt;Release the source code as a public GitHub repo&lt;/li&gt;
&lt;li&gt;Publish the web app to a public URL&lt;/li&gt;
&lt;li&gt;Submit your web app by 11:59 pm Pacific on Monday, July 29, 2024&lt;/li&gt;
&lt;/ol&gt;
&lt;aside&gt;&lt;p&gt;¹ The time limit is intended to make this a quick project that can fit most schedules. That being said, if you decide to spend more time on this, we&apos;ll never let a silly thing like rules stand in the way of a good time.&lt;/p&gt;&lt;/aside&gt;
&lt;h3&gt;Submit your web app&lt;/h3&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Heads up!&lt;/strong&gt; Submissions for this hackathon are now closed. &lt;a href=&quot;/newsletter&quot;&gt;Join the newsletter&lt;/a&gt; to hear about future hackathons first!&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;Happy building! Let&apos;s have some fun. &lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Didn&apos;t this challenge already happen?&lt;/strong&gt; You may have noticed that this challenge was previously announced with a different deadline — we&apos;re learning as we go here and realized that we didn&apos;t do a great job of telling people about this challenge the first time, so we&apos;ve adjusted our approach and opened it again so more people have the chance to participate!&lt;/p&gt;&lt;p&gt;The original participants all got some bonus goodies to say thank you, and we carried all original entries forward for this iteration of the giveaway.&lt;/p&gt;&lt;/aside&gt;</content:encoded></item><item><title>Not another f%*#in&apos; chatbot — Web Dev Challenge S1E1
</title><link>https://codetv.dev/blog/web-dev-challenge-not-another-chatbot/</link><guid isPermaLink="true">https://codetv.dev/blog/web-dev-challenge-not-another-chatbot/</guid><description>What could you create if you had 30 minutes to plan and 4 hours to build? Lizzie Siegle, Chance Strickland, Jack Herrington, and Jason Lengstorf took on the Web Dev Challenge to find out.
</description><pubDate>Sun, 23 Jun 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/f_auto,q_auto,w_1200/v1719209813/wdc/web-dev-challenge-ai-app-not-another-chatbot-v5.jpg&quot; alt=&quot;Not another f%*#in&apos; chatbot — Web Dev Challenge S1E1
&quot; /&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://youtu.be/8RCL5neas_M&quot;&gt;Watch on YouTube&lt;/a&gt;&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;This episode is part of the &lt;a href=&quot;https://lwj.dev/wdc&quot;&gt;&lt;em&gt;Web Dev Challenge&lt;/em&gt; series&lt;/a&gt;, which was previously referred to as &lt;em&gt;4 Web Devs, 1 App Idea&lt;/em&gt;.&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;The Challenge&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Build an AI-powered app that&apos;s not another f%*#in&apos; chatbot.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So much of what we see in the AI space are variations on the chatbot interface. Carter at DataStax challenged the devs to build something different.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;This episode is sponsored by &lt;a href=&quot;https://lwj.dev/astra-db&quot;&gt;DataStax Astra DB&lt;/a&gt;. Click the link and show them some love on your favorite!&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;The Tool&lt;/h2&gt;
&lt;p&gt;All apps must use &lt;a href=&quot;https://lwj.dev/astra-db&quot;&gt;Astra DB&lt;/a&gt; as part of the final build. Astra DB provides a vector store and tools for generating vector embeddings from any data you choose.&lt;/p&gt;
&lt;h2&gt;The Rules&lt;/h2&gt;
&lt;p&gt;In the &lt;em&gt;Web Dev Challenge&lt;/em&gt;, the rules are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;4 web developers each build a web app&lt;/li&gt;
&lt;li&gt;The web app is built to complete a specific challenge&lt;/li&gt;
&lt;li&gt;The web app must include a given tool or technology, which changes each episode&lt;/li&gt;
&lt;li&gt;Devs are given 30 minutes to plan&lt;/li&gt;
&lt;li&gt;Devs are given 4 hours to build&lt;/li&gt;
&lt;li&gt;At the end of 4 hours, the devs demo the web app they&apos;ve built&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;Web Dev Challenge&lt;/em&gt; is not a competition. Instead, it&apos;s a chance to build something with low stakes, learn something new, and have fun with friends.&lt;/p&gt;
&lt;h2&gt;The Web Devs&lt;/h2&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/f_auto,q_auto,w_1200/v1719209800/wdc/web-dev-challenge-ai-app-not-another-chatbot-dev-lineup.jpg&quot; alt=&quot;candid headshots of Lizzie, Chance, Jack, and Jason lined up left to right, with their names underneath their photos&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;&lt;strong&gt;Lizzie Siegle&lt;/strong&gt; — https://www.lizziesiegle.xyz&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Chance Strickland&lt;/strong&gt; — https://chance.dev&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Jack Herrington&lt;/strong&gt; — https://youtube.com/@jherr&lt;/p&gt;
&lt;p&gt;And, of course, &lt;a href=&quot;https://jason.energy/links&quot;&gt;your friend Jason&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;The Advisor&lt;/h2&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/v1719210266/wdc/carter.jpg&quot; alt=&quot;Carter standing outside and smiling&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/crtr0&quot;&gt;&lt;strong&gt;Carter Rabasa&lt;/strong&gt;&lt;/a&gt;, Head of Developer Relations at DataStax, was in studio to support the devs, teach us about vector databases, and answer questions.&lt;/p&gt;
&lt;h2&gt;The Apps&lt;/h2&gt;
&lt;p&gt;Jack&apos;s DnD Encounter Generator: &lt;a href=&quot;https://github.com/jherr/astradb-dnd&quot;&gt;source code&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Jason&apos;s related videos PoC: &lt;a href=&quot;https://not-another-chatbot.netlify.app/&quot;&gt;live app&lt;/a&gt; · &lt;a href=&quot;https://github.com/learnwithjason/wdc-not-another-chatbot&quot;&gt;source code&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Lizzie&apos;s Star Wars fanfic generator: &lt;a href=&quot;https://github.com/elizabethsiegle/star-wars-fanfic-generator-streamlit-astra-cf&quot;&gt;source code&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Chance&apos;s burger generator: &lt;a href=&quot;https://github.com/chaance/carters-burgers&quot;&gt;source code&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;The Giveaway&lt;/h2&gt;
&lt;p&gt;We opened up this challenge to the whole community to see what you would build, and &lt;a href=&quot;/blog/4d1a-not-another-chatbot-giveaway&quot;&gt;you could win a dream desk setup by Ugmonk&lt;/a&gt; by building your own app.&lt;/p&gt;
&lt;p&gt;Huge congrats to &lt;a href=&quot;https://www.lindakat.com/&quot;&gt;Linda Thompson&lt;/a&gt;, who won the dream desk setup!&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Don&apos;t forget!&lt;/strong&gt; If you want to hear about future giveaways first, make sure you &lt;a href=&quot;/newsletter&quot;&gt;get on the newsletter list&lt;/a&gt;.&lt;/p&gt;&lt;/aside&gt;</content:encoded></item><item><title>Understand your work archetype: explorers, villagers, and town planners
</title><link>https://codetv.dev/blog/work-archetype-explorer-villager-town-planner/</link><guid isPermaLink="true">https://codetv.dev/blog/work-archetype-explorer-villager-town-planner/</guid><description>What kind of work is the best fit FOR YOU? A quick assessment to help you optimize for a job that you&apos;ll actually enjoy.
</description><pubDate>Fri, 31 May 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/q_auto,f_auto/v1717352184/lwj/blog/job-archetype-explorers-villagers-city-planners.jpg&quot; alt=&quot;Understand your work archetype: explorers, villagers, and town planners
&quot; /&gt;&lt;/p&gt;&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; A previous version of this article referred to &quot;pioneers, settlers, and town planners&quot;, which was Simon Wardley&apos;s original naming. He later &lt;a href=&quot;https://swardley.medium.com/how-to-organise-yourself-f36f084a611b&quot;&gt;renamed to &quot;explorers, villagers, and town planners&quot;&lt;/a&gt; by Wardley to remove colonial overtones.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;In 2018, I saw &lt;a href=&quot;https://craft-conf.com/2018/speaker/SimonWardley&quot;&gt;Simon Wardley speak at Craft Conf&lt;/a&gt;. In his talk, he mentioned in passing a model for types of work and the people best suited for each.&lt;/p&gt;
&lt;p&gt;His casual aside was a &quot;holy crap&quot; moment for me because &lt;strong&gt;this framing unlocked a new perspective on my working history. I suddenly understood why I&apos;d succeeded in certain jobs I&apos;d had — and why I&apos;d burned out quickly in others.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Let me explain.&lt;/p&gt;
&lt;h2&gt;What we do every day matters&lt;/h2&gt;
&lt;p&gt;Imagine two people with the job title &quot;software engineer&quot;.&lt;/p&gt;
&lt;p&gt;What do they &lt;em&gt;actually do&lt;/em&gt; every day?&lt;/p&gt;
&lt;p&gt;The first might handle Jira tickets for their team&apos;s section (billing) of an enterprise product&apos;s dashboard. The tickets are mostly minor tweaks and bug fixes. Over the course of an average quarter, there aren&apos;t many large changes to the codebase. Instead, there are long-running initiatives to modernize outdated code, fix performance issues, and handle edge cases as they&apos;re reported by customers.&lt;/p&gt;
&lt;p&gt;The second might work at a startup and every day is a new adventure: sometimes the CEO codes up a new proof of concept over the weekend and now their task is to turn it into a production-ready feature; other times they&apos;re handling priority tickets from important customers; other &lt;em&gt;other&lt;/em&gt; times they get handed a partially completed set of requirements and a tight deadline.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;These people have the same job title, but these are two completely different roles.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;Our working archetype should match our job duties&lt;/h2&gt;
&lt;p&gt;In the two hypothetical software engineering roles described above, I feel very different about one vs. the other. I could see myself thriving in one of the roles, and in the other I&apos;m relatively confident I&apos;d be back on the job market pretty quickly.&lt;/p&gt;
&lt;p&gt;I would wager that you, dear reader, probably feel the same way. But whether you and I feel that way about &lt;em&gt;the same roles&lt;/em&gt; is a coin toss. &lt;strong&gt;That&apos;s the critical point here: each of us gets our energy from certain types of work, and feels drained and/or stressed by other types of work. It&apos;s up to each of us to figure out what our individual strengths are — and then work toward landing roles that let us spend most of our time leveraging them.&lt;/strong&gt;&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/q_auto,f_auto/v1717352184/lwj/blog/job-archetype-explorers-villagers-city-planners.jpg&quot; alt=&quot;a three-panel composite image with a covered wagon on a rural landscape in the left panel, a construction site with a house framed with wood in the center, and a complicated network of pipes and valves on the right&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;Simon Wardley describes &lt;a href=&quot;https://blog.gardeviance.org/search?q=PST&quot;&gt;&quot;explorers, villagers, and town planners&quot;&lt;/a&gt; as the three core archetypes that make a company function.&lt;/p&gt;
&lt;p&gt;People are complex, so I don&apos;t think we all fit tidily into one of these three boxes, but I do think there&apos;s a spectrum, and each of us can observe our own behavior to get a pretty good idea of where we fall on it.&lt;/p&gt;
&lt;h2&gt;Explorers thrive in the unknown&lt;/h2&gt;
&lt;p&gt;The explorer archetype is at their best when they have a problem to solve and very little structure around how to solve it. Explorers are comfortable with ambiguity and excited to try something new — in fact, they&apos;re often bored by things that &lt;em&gt;aren&apos;t&lt;/em&gt; new.&lt;/p&gt;
&lt;h3&gt;Explorers thrive when:&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Working with uncertainty&lt;/strong&gt; — they thrive on chaos and get energy from creating new ideas&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Trusting their instincts&lt;/strong&gt; — they have a good gut feel for what will work and have enough strength in their convictions to act on them&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Experimenting and &quot;failing fast&quot;&lt;/strong&gt; — they see failure as progress and roll the lessons of unsuccessful experiments into new hypotheses&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Explorers will struggle with:&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Highly structured environments&lt;/li&gt;
&lt;li&gt;Prescriptive direction (dictating &lt;em&gt;how&lt;/em&gt; to do things)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Villagers turn promise into product&lt;/h2&gt;
&lt;p&gt;The villager archetype shines in roles where there&apos;s strong evidence that an idea will work, if only someone would sit down and do the work properly. Villagers find satisfaction in turning potential into reality, combining customer feedback and iterative testing to smooth out the rough edges and build something commercially viable.&lt;/p&gt;
&lt;h3&gt;Villagers thrive when:&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Turning good ideas into actual products&lt;/strong&gt; — they turn a cool demo into actual revenue&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Getting customer feedback&lt;/strong&gt; — they take pride in user feedback and act quickly to respond to problems&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Getting the angles right&lt;/strong&gt; — they&apos;re iterating toward success and energized by steady progress&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Villagers struggle with:&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Losing progress due to upstream changes&lt;/li&gt;
&lt;li&gt;Isolation and lack of feedback&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Town planners build resilience and scale&lt;/h2&gt;
&lt;p&gt;The town planner archetype is driven by the pursuit of a flawless system. They&apos;re process-driven, meticulously organized, and patient — willing to put in the hours to hunt down the most elusive edge cases and mysterious imperfections.&lt;/p&gt;
&lt;h3&gt;Town planners thrive when:&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Standardizing and operationalizing&lt;/strong&gt; — they work toward consistency and repeatability in a detail-oriented, very thorough manner&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Responding to analytics and data&lt;/strong&gt; — they care about clearly defined goals and verifiable measures of success&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Steadily working toward perfection&lt;/strong&gt; — they aim to perfect systems through small, incremental improvements&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Town planners struggle with:&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Lack of clarity and/or consistency&lt;/li&gt;
&lt;li&gt;Rapidly changing priorities&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;It takes all three to run a successful company&lt;/h2&gt;
&lt;p&gt;In case it&apos;s not clear: none of these archetypes are better or worse than the others. All three have unique strengths and weaknesses, and any successful company will need all three archetypes in roles that match their strengths.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Knowing your archetype has nothing to do with your intrinsic value.&lt;/strong&gt; Instead, having a clear idea of your archetype and the types of work that energize you will help you optimize for roles that take advantage of your strengths and don&apos;t force you into work that drains you.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;While many jobs have the same job title, what actually matters is the day-to-day duties.&lt;/strong&gt; It&apos;s important that your duties match up with your archetype if you want to remain happy in your role for a long time.&lt;/p&gt;
&lt;h2&gt;Which archetype best matches you?&lt;/h2&gt;
&lt;p&gt;As you read the descriptions of the archetypes above, did one feel the most &quot;you&quot;?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;I believe these archetypes are more of a spectrum than a clear delineation&lt;/strong&gt;, and I know people who feel like they&apos;re a blend of two of the archetypes. (In fact, I&apos;d wager &lt;em&gt;most&lt;/em&gt; of us will see parts of ourselves in two of the archetypes.)&lt;/p&gt;
&lt;p&gt;I don&apos;t think it&apos;s important to force ourselves into one of these archetypes. &lt;strong&gt;The point isn&apos;t to shrink ourselves down to fit into a box; it&apos;s to understand what gives us energy so we can optimize our career efforts&lt;/strong&gt; toward landing roles that make us feel happy and fulfilled by lining them up with our preferred working styles.&lt;/p&gt;</content:encoded></item><item><title>4 Web Devs 1 App Not Another Chatbot Dream Desk Giveaway
</title><link>https://codetv.dev/blog/4d1a-not-another-chatbot-giveaway/</link><guid isPermaLink="true">https://codetv.dev/blog/4d1a-not-another-chatbot-giveaway/</guid><description>Build an app, learn something new, have some fun, win a prize! Enter by May 13, 2024 to win.
</description><pubDate>Mon, 29 Apr 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/f_auto,q_auto/v1714525074/4d1a-datastax-giveaway.jpg&quot; alt=&quot;4 Web Devs 1 App Not Another Chatbot Dream Desk Giveaway
&quot; /&gt;&lt;/p&gt;&lt;aside&gt;&lt;p&gt;&lt;strong&gt;UPDATE!&lt;/strong&gt; This giveaway has ended, but there will be more soon. &lt;a href=&quot;https://lwj.dev/newsletter&quot;&gt;Sign up for newsletter updates to hear about them first!&lt;/a&gt;&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;A new season of &lt;a href=&quot;https://lwj.dev/4d1a&quot;&gt;4 Web Devs 1 App&lt;/a&gt; is coming, and we want everyone — especially you! — to join in on the fun. And to make things even more exciting, we&apos;ve added a new feature: a community challenge and giveaway!&lt;/p&gt;
&lt;h2&gt;We’re giving away a dream desk setup!&lt;/h2&gt;
&lt;p&gt;In every episode of &lt;a href=&quot;https://lwj.dev/4d1a&quot;&gt;4 Web Devs 1 App&lt;/a&gt;, developers from around the community challenge themselves to build an app that meets a prompt and uses a given tool.&lt;/p&gt;
&lt;p&gt;Now you can be one of those devs! &lt;strong&gt;Build your own take on the app prompt and submit it to the showcase&lt;/strong&gt; to learn something new, show off your work, have some fun with the web dev community, and learn (or practice) practical skills.&lt;/p&gt;
&lt;p&gt;And, as a bonus, &lt;strong&gt;one dev will win a dream desk setup&lt;/strong&gt;: &lt;a href=&quot;https://ugmonk.com/collections/the-gather-collection/products/the-complete-gather-collection-black-maple?variant=42601772220566&quot;&gt;Ugmonk&apos;s complete Gather collection&lt;/a&gt;.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/f_auto,q_auto,w_1280/v1714343015/4d1a/complete-gather-collection.jpg&quot; alt=&quot;The complete Gather Collection from Ugmonk&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Please note:&lt;/strong&gt; while anyone can participate, only US residents are eligible to win the desk setup at this time. We&apos;re exploring the legal requirements of opening up the giveaway to more countries in future giveaways, but this stuff is &lt;em&gt;complicated&lt;/em&gt;. Thanks for your understanding.&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;The prompt: “build an app using AI that isn’t another chatbot”&lt;/h2&gt;
&lt;p&gt;For this episode, the devs are building a web app that uses AI — but there&apos;s a catch: it can&apos;t just be another chatbot.&lt;/p&gt;
&lt;p&gt;The team over at &lt;a href=&quot;https://lwj.dev/datastax&quot;&gt;DataStax&lt;/a&gt; are sponsoring this episode, and the apps must be built using &lt;a href=&quot;https://lwj.dev/astra-db&quot;&gt;Astra DB&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;How to enter&lt;/h2&gt;
&lt;p&gt;If you want to play along, here’s how to enter:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Build a web app that includes AI, but is not a chatbot&lt;/li&gt;
&lt;li&gt;Use &lt;a href=&quot;https://lwj.dev/astra-db&quot;&gt;Astra DB&lt;/a&gt; as part of the web app&lt;/li&gt;
&lt;li&gt;Release the source code as a public GitHub repo&lt;/li&gt;
&lt;li&gt;Publish the web app to a hosting provider of your choice&lt;/li&gt;
&lt;li&gt;Submit your web app using the form below by 11:59pm on Monday, May 13&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Submissions are now closed&lt;/h2&gt;
&lt;p&gt;This giveaway has ended, but there will be more soon. &lt;a href=&quot;https://lwj.dev/newsletter&quot;&gt;Sign up for newsletter updates to hear about them first!&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Play along with friends!&lt;/h2&gt;
&lt;p&gt;If you&apos;re looking for others who are getting involved, we&apos;ve set up a dedicated channel in the &lt;a href=&quot;https://lwj.dev/discord&quot;&gt;Learn With Jason Discord&lt;/a&gt; (called &lt;code&gt;#builder-chat&lt;/code&gt;) for people to brainstorm, share ideas, and keep each other accountable.&lt;/p&gt;
&lt;p&gt;We&apos;ve also got &lt;a href=&quot;https://twitter.com/crtr0&quot;&gt;Carter&lt;/a&gt; and friends from the DataStax team available to answer your Astra DB questions as you&apos;re building.&lt;/p&gt;
&lt;p&gt;Happy building! Let&apos;s have some fun. &lt;/p&gt;</content:encoded></item><item><title>Fix Drizzle &quot;SqliteError: no such table&quot; error — how to create tables
</title><link>https://codetv.dev/blog/drizzle-orm-sqlite-create-tables/</link><guid isPermaLink="true">https://codetv.dev/blog/drizzle-orm-sqlite-create-tables/</guid><description>I tried Drizzle ORM with SQLite but got stuck on &quot;SqliteError: no such table&quot;. Here&apos;s how I solved the error and created SQLite tables from a Drizzle schema.
</description><pubDate>Sat, 17 Feb 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I was trying to build something with &lt;a href=&quot;https://orm.drizzle.team/&quot;&gt;Drizzle&lt;/a&gt; but I couldn&apos;t figure out how to actually create tables in my SQLite database. Here are the docs I wish I&apos;d had that would have saved me an hour of Googling things.&lt;/p&gt;
&lt;h2&gt;Create a new Node project&lt;/h2&gt;
&lt;p&gt;If you don&apos;t already have one, create a new Node project.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# create a folder 
mkdir my-drizzle-project
cd my-drizzle-project/

# initialize
npm init -y
git init

# install the required deps
npm i drizzle-orm better-sqlite3
npm i -D @types/better-sqlite3 @types/node drizzle-kit ts-node typescript
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Update &lt;code&gt;package.json&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;In &lt;code&gt;package.json&lt;/code&gt;, make the highlighted changes. More on the command prefixed with &lt;code&gt;db:&lt;/code&gt;  later in this tutorial.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
	&quot;engines&quot;: {
		&quot;node&quot;: &quot;&amp;gt;=20.6.0&quot;
	},
	&quot;type&quot;: &quot;module&quot;,
	&quot;name&quot;: &quot;drizzle-sqlite-basic-example&quot;,
	&quot;version&quot;: &quot;1.0.0&quot;,
	&quot;description&quot;: &quot;&quot;,
	&quot;main&quot;: &quot;index.js&quot;,
	&quot;scripts&quot;: {
		&quot;db:studio&quot;: &quot;drizzle-kit studio&quot;,
		&quot;build&quot;: &quot;npx tsc&quot;,
		&quot;dev&quot;: &quot;node --env-file=.env --watch --loader ts-node/esm index.ts&quot;,
		&quot;test&quot;: &quot;echo \&quot;Error: no test specified\&quot; &amp;amp;&amp;amp; exit 1&quot;
	},
	&quot;author&quot;: &quot;Jason Lengstorf &amp;lt;jason@learnwithjason.dev&amp;gt;&quot;,
	&quot;license&quot;: &quot;ISC&quot;,
	&quot;dependencies&quot;: {
		&quot;better-sqlite3&quot;: &quot;^9.4.1&quot;,
		&quot;drizzle-orm&quot;: &quot;^0.29.3&quot;
	},
	&quot;devDependencies&quot;: {
		&quot;@types/better-sqlite3&quot;: &quot;^7.6.9&quot;,
		&quot;@types/node&quot;: &quot;^20.11.19&quot;,
		&quot;drizzle-kit&quot;: &quot;^0.20.14&quot;,
		&quot;ts-node&quot;: &quot;^10.9.2&quot;,
		&quot;typescript&quot;: &quot;^5.3.3&quot;
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; These settings are my preferences for new Node projects. You can use whatever you like. Be aware that if you use CommonJS some of the code that follows in this tutorial may need to be tweaked a bit — I didn&apos;t test for CJS compatibility.&lt;/p&gt;&lt;/aside&gt;
&lt;h3&gt;Add &lt;code&gt;tsconfig.json&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;You can use any TypeScript setup you prefer, but here&apos;s the one I tested with.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
	&quot;compilerOptions&quot;: {
		&quot;strict&quot;: true,
		&quot;target&quot;: &quot;ESNext&quot;,
		&quot;module&quot;: &quot;NodeNext&quot;,
		&quot;esModuleInterop&quot;: true,
		&quot;forceConsistentCasingInFileNames&quot;: true,
		&quot;skipLibCheck&quot;: true
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Create a Drizzle config file&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import type { Config } from &apos;drizzle-kit&apos;;

export default {
	schema: &apos;./db/schema.ts&apos;,
	out: &apos;./db/migrations&apos;,
	driver: &apos;better-sqlite&apos;,
	dbCredentials: {
		url: &apos;./db/demo.db&apos;,
	},
} satisfies Config;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Create a SQLite database schema&lt;/h2&gt;
&lt;p&gt;Your schema can define any tables you need for your app. Here&apos;s what mine looks like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { integer, text, sqliteTable } from &apos;drizzle-orm/sqlite-core&apos;;

export const users = sqliteTable(&apos;users&apos;, {
	id: integer(&apos;id&apos;, { mode: &apos;number&apos; }).primaryKey({ autoIncrement: true }),
	name: text(&apos;name&apos;),
});

export const ideas = sqliteTable(&apos;ideas&apos;, {
	id: integer(&apos;id&apos;, { mode: &apos;number&apos; }).primaryKey({ autoIncrement: true }),
	text: text(&apos;text&apos;),
	status: text(&apos;status&apos;, { enum: [&apos;approved&apos;, &apos;rejected&apos;, &apos;pending&apos;] }),
	creator: integer(&apos;creator_id&apos;).references(() =&amp;gt; users.id),
});

export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;

export type Idea = typeof ideas.$inferSelect;
export type NewIdea = typeof ideas.$inferInsert;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Create the SQLite database tables from your Drizzle schema&lt;/h2&gt;
&lt;p&gt;Right now, the schema is defined but the tables haven&apos;t been created in the SQLite database yet.&lt;/p&gt;
&lt;p&gt;This is where I got stuck. I tried to run my app, but I kept getting a &lt;code&gt;SqliteError: no such table: users&lt;/code&gt; error. I couldn&apos;t find any reference to how to actually create the database tables in the Drizzle docs.&lt;/p&gt;
&lt;p&gt;After searching around, I figured out two ways to get the SQLite tables created using Drizzle:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://orm.drizzle.team/kit-docs/commands#generate-migrations&quot;&gt;Generate a migration&lt;/a&gt; (the SQL query to create the tables as defined in your schema), which can be applied manually, using Drizzle&apos;s &lt;code&gt;migrate&lt;/code&gt; helper, or with third-party tools — this is a production-friendly approach&lt;/li&gt;
&lt;li&gt;Push the schema changes directly, which is exactly what I was looking for to set up my local database and is what I&apos;ll cover in this tutorial.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Both generating migrations and pushing schemas can be done using Drizzle&apos;s CLI helper, &lt;a href=&quot;https://orm.drizzle.team/kit-docs/overview&quot;&gt;Drizzle Kit&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Push the SQLite database changes using Drizzle Kit&lt;/h3&gt;
&lt;p&gt;For prototyping, local dev, or initializing a new database, the &lt;a href=&quot;https://orm.drizzle.team/kit-docs/commands#prototype--push&quot;&gt;&lt;code&gt;push&lt;/code&gt; command&lt;/a&gt; will use the defined schema to update the database — which, in our case, will create the missing tables.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npx drizzle-kit push:sqlite
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;View the SQLite database in Drizzle Studio&lt;/h2&gt;
&lt;p&gt;To verify that the tables were created, you can use &lt;a href=&quot;https://orm.drizzle.team/drizzle-studio/overview&quot;&gt;Drizzle Studio&lt;/a&gt;, which gives you a browser-based interface for viewing and working with your SQLite data.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm run db:studio
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This runs &lt;code&gt;drizzle-kit studio&lt;/code&gt; under the hood and will start up Drizzle Studio at &lt;code&gt;https://local.drizzle.studio/&lt;/code&gt;.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/f_auto,q_auto,w_1270,b_rgb:a8fffb/v1708212151/lwj/blog/drizzle-studio.png&quot; alt=&quot;Drizzle Studio screenshot showing the tables created in this tutorial&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;h2&gt;Test CRUD in SQLite using Drizzle ORM&lt;/h2&gt;
&lt;p&gt;Now the tables exist, you can actually insert and select data from the database.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; At this point, if you already know how to use Drizzle and are ready to build, you can stop reading here. I&apos;m including this section for anyone who&apos;s brand new to Drizzle and wants to go from zero to writing and reading data from a SQLite database.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;To try this out, create &lt;code&gt;index.ts&lt;/code&gt; at the root of the project and add the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { drizzle } from &apos;drizzle-orm/better-sqlite3&apos;;
import Database from &apos;better-sqlite3&apos;;
import {
	users,
	ideas,
	type NewIdea,
	type NewUser,
	type User,
} from &apos;./db/schema.js&apos;;
import { eq } from &apos;drizzle-orm&apos;;

const sqlite = new Database(&apos;./db/demo.db&apos;);
const db = drizzle(sqlite);

// inserts a new user and returns the newly created entry
async function addUser(user: NewUser): Promise&amp;lt;User&amp;gt; {
	return (await db.insert(users).values(user).returning()).at(0)!;
}

async function addIdea(idea: NewIdea): void {
	await db.insert(ideas).values(idea);
}

// joins the ideas and users tables to select ideas with creator name
async function getIdeas() {
	return await db
		.select({
			id: ideas.id,
			text: ideas.text,
			status: ideas.status,
			creator: users.name, // &amp;lt;= use the creator&apos;s name instead of ID
		})
		.from(ideas)
		.leftJoin(users, eq(ideas.creator, users.id));
}

const user = await addUser({ name: &apos;Jason Lengstorf&apos; });

await addIdea({
	text: &apos;Learn how ORMs work&apos;,
	status: &apos;pending&apos;,
	creator: user.id, // &amp;lt;= use the new user&apos;s ID as the creator
});

const allIdeas = await getIdeas();

console.log(allIdeas);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After saving this, start the dev server by running &lt;code&gt;npm run dev&lt;/code&gt; and you&apos;ll see output similar to the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;❯ npm run dev

&amp;gt; drizzle-sqlite-basic-example@1.0.0 dev
&amp;gt; node --env-file=.env --watch --loader ts-node/esm index.ts

(node:82803) ExperimentalWarning: Watch mode is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
(node:82808) ExperimentalWarning: `--experimental-loader` may be removed in the future; instead use `register()`:
--import &apos;data:text/javascript,import { register } from &quot;node:module&quot;; import { pathToFileURL } from &quot;node:url&quot;; register(&quot;ts-node/esm&quot;, pathToFileURL(&quot;./&quot;));&apos;
(Use `node --trace-warnings ...` to show where the warning was created)
[
  {
    id: 1,
    text: &apos;Learn how ORMs work&apos;,
    status: &apos;pending&apos;,
    creator: &apos;Jason Lengstorf&apos;
  },
]
Completed running &apos;index.ts&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This creates both a user and an idea, then does a &lt;a href=&quot;https://orm.drizzle.team/docs/joins#partial-select&quot;&gt;partial select&lt;/a&gt; to create a combined view of the idea and user.&lt;/p&gt;
&lt;p&gt;If you&apos;re like me and this is your first time using Drizzle and SQLite: congratulations! You just created and interacted with a database in a Node and TypeScript project, and you&apos;re ready to build your data-powered app.&lt;/p&gt;
&lt;p&gt;Happy building!&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; A previous version of this article incorrectly stated that it was necessary to use both generate and push together. The &lt;a href=&quot;https://twitter.com/DrizzleORM/status/1759037941182042527&quot;&gt;Drizzle team clarified how generate and push work&lt;/a&gt; and this post has been updated accordingly.&lt;/p&gt;&lt;/aside&gt;</content:encoded></item><item><title>How to set up a Node server with TypeScript in 2024</title><link>https://codetv.dev/blog/modern-node-server-typescript-2024/</link><guid isPermaLink="true">https://codetv.dev/blog/modern-node-server-typescript-2024/</guid><description>A quick tutorial on how to set up a Node project in 2024. Includes TypeScript, live reload, and environment variable support.
</description><pubDate>Wed, 14 Feb 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I&apos;ve had to look up how to create a new Node server enough times that I&apos;m writing it down to make it easier for me to find in the future. This is a quick and dirty post without much context or explanation. Feel free to &lt;a href=&quot;https://lwj.dev/discord&quot;&gt;ask me for details&lt;/a&gt; if something I said isn&apos;t clear.&lt;/p&gt;
&lt;p&gt;This is the fastest way I know of to get a Node server running with TypeScript as of 2024. If you know a better way, please do let me know!&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Heads up!&lt;/strong&gt; Make sure you&apos;re using Node &amp;gt;=20.6 — it&apos;s required for some of the flags used in this setup.&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;Set up the project&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# create a new project
mkdir my-node-app
cd my-node-app/

# initialize the project
git init
npm init -y

# install dev dependencies
npm i -D typescript ts-node @types/node

# initialize TypeScript
npx tsc --init
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, open &lt;code&gt;package.json&lt;/code&gt; and add the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
	&quot;engines&quot;: {
		&quot;node&quot;: &quot;&amp;gt;=20.6.0&quot;
	},
	&quot;name&quot;: &quot;my-node-app&quot;,
	&quot;version&quot;: &quot;1.0.0&quot;,
	&quot;description&quot;: &quot;&quot;,
	&quot;main&quot;: &quot;index.js&quot;,
	&quot;scripts&quot;: {
		&quot;build&quot;: &quot;tsc&quot;,
		&quot;dev&quot;: &quot;node --env-file=.env --watch -r ts-node/register src/index.ts&quot;,
		&quot;test&quot;: &quot;echo \&quot;Error: no test specified\&quot; &amp;amp;&amp;amp; exit 1&quot;
	},
	&quot;author&quot;: &quot;Jason Lengstorf &amp;lt;jason@learnwithjason.dev&amp;gt;&quot;,
	&quot;license&quot;: &quot;ISC&quot;,
	&quot;devDependencies&quot;: {
		&quot;@types/node&quot;: &quot;^20.11.17&quot;,
		&quot;ts-node&quot;: &quot;^10.9.2&quot;,
		&quot;typescript&quot;: &quot;^5.3.3&quot;
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Heads up!&lt;/strong&gt; The &lt;a href=&quot;https://nodejs.org/docs/latest/api/cli.html#--watch&quot;&gt;&lt;code&gt;--watch&lt;/code&gt; flag&lt;/a&gt; was added in Node v18.11.0. The &lt;a href=&quot;https://nodejs.org/docs/latest/api/cli.html#--env-fileconfig&quot;&gt;&lt;code&gt;--env-file=config&lt;/code&gt; flag&lt;/a&gt; was added in Node v20.6.0.&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;Add environment variables, if needed&lt;/h2&gt;
&lt;p&gt;Create &lt;code&gt;.env&lt;/code&gt; and put any secrets inside:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;TEST_VALUE=hello
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Create the main Node app file&lt;/h2&gt;
&lt;p&gt;Create &lt;code&gt;src/index.ts&lt;/code&gt; and put something inside:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function test(): void {
	console.log(process.env.TEST_VALUE);
}

test();
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Start the Node server and test live reload&lt;/h2&gt;
&lt;p&gt;Start the server with &lt;code&gt;npm run dev&lt;/code&gt; and you&apos;ll see the following output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;❯ npm run dev

&amp;gt; my-node-app@1.0.0 dev
&amp;gt; node --env-file=.env --watch -r ts-node/register src/index.ts

(node:29702) ExperimentalWarning: Watch mode is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
hello
Completed running &apos;src/index.ts&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Make a change to &lt;code&gt;src/index.ts&lt;/code&gt; or &lt;code&gt;.env&lt;/code&gt; and the server will automatically restart and show your changes in the console.&lt;/p&gt;
&lt;p&gt;And that&apos;s it — now you&apos;ve got a Node app with TypeScript running with live reloading and environment variables, using as few dependencies as possible, modernized for building apps in 2024.&lt;/p&gt;
&lt;p&gt;Happy building!&lt;/p&gt;</content:encoded></item><item><title>I miss RSS</title><link>https://codetv.dev/blog/i-miss-rss/</link><guid isPermaLink="true">https://codetv.dev/blog/i-miss-rss/</guid><description>There was something special about hand-selecting XML and running out of stuff to read that I’d like to get back.
</description><pubDate>Tue, 16 Jan 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/q_auto,f_auto/v1705434364/lwj/blog/rss.jpg&quot; alt=&quot;I miss RSS&quot; /&gt;&lt;/p&gt;
&lt;p&gt;There was this incredible moment in the mid-2000s where it felt like everyone had their own blog, and those blogs all had RSS feeds. I&apos;d start my day by scrolling through the latest posts (RIP &lt;a href=&quot;https://en.wikipedia.org/wiki/Google_Reader&quot;&gt;Google Reader&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;It was &lt;em&gt;sort of&lt;/em&gt; like social media today: &lt;strong&gt;ideas and insights from my industry peers were available to me in a handy little scrollable list, all in one place.&lt;/strong&gt;&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/q_auto,f_auto/v1705434364/lwj/blog/rss.jpg&quot; alt=&quot;Jason looking old and grumpy with the caption, &amp;quot;in my day social media was Google Reader&amp;quot;&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;h2&gt;RSS was missing features... and I think that was good, actually?&lt;/h2&gt;
&lt;p&gt;RSS feeds didn&apos;t have some of the things social media would introduce later:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;there was no &quot;like&quot; or &quot;retweet&quot; option&lt;/li&gt;
&lt;li&gt;discussion was broken up into individual comment sections on the blogs (if the blog had comments at all)&lt;/li&gt;
&lt;li&gt;discovery was harder because there was no Algorithm™ to speak of — you curated your own list (because the feed reader had no opinions or business goals)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But in retrospect, &lt;strong&gt;what RSS was missing might have been what made it great:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;no likes or retweets meant that the point was to &lt;em&gt;share&lt;/em&gt;, not to farm engagement&lt;/li&gt;
&lt;li&gt;localized discussion meant the issue of context collapse was much more contained&lt;/li&gt;
&lt;li&gt;having a small list meant I actually read things instead of skimming to try and finish reading the whole internet&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It was far from perfect, but I can&apos;t help but feel like we lost something when we gave up on RSS.&lt;/p&gt;
&lt;h2&gt;We lost control of what we see&lt;/h2&gt;
&lt;p&gt;The biggest loss of the last 15-ish years, internet-wise, is that &lt;strong&gt;at some point we lost our ability to choose what we see online.&lt;/strong&gt; Every social site has some kind of recommendation algorithm, and while I understand why this starts, it ultimately turns into &quot;show people the most inflammatory stuff because engagement makes investors and advertisers open their wallets&quot;.&lt;/p&gt;
&lt;p&gt;Cassidy Williams put this really well in a recent post:&lt;/p&gt;
&lt;figure&gt;&lt;blockquote&gt;
&lt;p&gt;&quot;In the earlier internet days, you went to a fun website or read the latest thing because you decided to go do it. Now, all of this content is &lt;a href=&quot;https://www.nbcnews.com/health/health-news/teens-inundated-phone-prompts-day-night-research-finds-rcna108044&quot;&gt;pushed in your face&lt;/a&gt;, designed to be as addicting as possible, so you keep coming back. You can curate it to a point, but companies design these systems this way on purpose.&quot;&lt;/p&gt;
&lt;/blockquote&gt;&lt;/figure&gt;
&lt;p&gt;That&apos;s fine, I guess, but the side effect is that now I see all sorts of junk on social media that I didn&apos;t choose to see. Suggested posts and promoted posts often make up more of what I see than the people I actually, y&apos;know &lt;em&gt;subscribe&lt;/em&gt; to.&lt;/p&gt;
&lt;p&gt;And for every post that gets shoved into my feed that I enjoy, there are a hundred or so that either make me feel sad, angry, or annoyed — when did we decide this was the right default?&lt;/p&gt;
&lt;p&gt;I&apos;m not sure what happened, but it feels... bad.&lt;/p&gt;
&lt;h2&gt;You could run out of stuff to read&lt;/h2&gt;
&lt;p&gt;This may come as a surprise to folks who haven&apos;t been on the internet as long as I have, but there used to be a point where you could &lt;em&gt;run out of things to read online&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;It could be frustrating sometimes, sure, but &lt;strong&gt;there was a sense of satisfaction that came with knowing that you&apos;d read everything you intended to read.&lt;/strong&gt; That last entry would get marked as read and you could just, like, do other stuff and not worry that you were being left out of the conversation.&lt;/p&gt;
&lt;h2&gt;Can RSS make a comeback?&lt;/h2&gt;
&lt;p&gt;I don&apos;t know. I hope so.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://lwj.dev/blog/rss.xml&quot;&gt;This blog has an RSS feed.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I know some of my friends never stopped publishing RSS (examples: &lt;a href=&quot;https://chriscoyier.net/feed/&quot;&gt;Chris Coyier&lt;/a&gt; and &lt;a href=&quot;https://blog.cassidoo.co/rss.xml&quot;&gt;Cassidy Williams&lt;/a&gt;). Sites like &lt;a href=&quot;https://www.youtube.com/feeds/videos.xml?channel_id=UCnty0z0pNRDgnuoirYXnC5A&quot;&gt;YouTube&lt;/a&gt; still publish RSS feeds. Even &lt;a href=&quot;https://hachyderm.io/@jlengstorf.rss&quot;&gt;some of our social media&lt;/a&gt; provides an RSS feed, with options to convert other feeds like &lt;a href=&quot;https://nitter.1d4.us/jlengstorf/rss&quot;&gt;Twitter to RSS&lt;/a&gt; if you want!&lt;/p&gt;
&lt;p&gt;And there are still RSS readers out there like &lt;a href=&quot;https://feedly.com/&quot;&gt;Feedly&lt;/a&gt; and &lt;a href=&quot;https://feeder.co/&quot;&gt;Feeder&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It feels like there&apos;s a moment happening where this idea of building our own little spaces on the internet and curating our own little feeds of what our friends and colleagues are up to is maybe — just maybe — something that would work.&lt;/p&gt;
&lt;p&gt;So I don&apos;t now. Maybe add an RSS feed to your site and let people know about it.&lt;/p&gt;</content:encoded></item><item><title>Add user management to a Next.js site with React server components, server actions, and AuthKit</title><link>https://codetv.dev/blog/authkit-next/</link><guid isPermaLink="true">https://codetv.dev/blog/authkit-next/</guid><description>The most tedious and difficult part of building a web app is authentication. But a new tool just entered the discussion that’s hoping to change that.
</description><pubDate>Thu, 30 Nov 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/f_auto,q_auto/lwj/blog/authkit_next_react_server_components.jpg&quot; alt=&quot;Add user management to a Next.js site with React server components, server actions, and AuthKit&quot; /&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://youtu.be/lEEoUa_mfHc&quot;&gt;Watch on YouTube&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;There are lots of options to get auth set up quickly, but they often aren&apos;t built to handle the more complex, enterprise features that you hope to grow into over time. In the other direction, user management that&apos;s powerful enough for a more mature company is usually so cumbersome and expensive to set up that it&apos;s borderline foolish to start with it.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.authkit.com/&quot;&gt;AuthKit&lt;/a&gt; caught my attention because its stated intention is to fit the needs of early-stage companies as well as more mature, complex use cases. They have a hosted UI for getting full-featured user management set up in a few lines of code, plus they have a full SDK for building custom user management flows and adding enterprise features like SSO. I love this, because it lines up with my views on &lt;a href=&quot;https://jason.energy/progressive-disclosure-of-complexity&quot;&gt;progressive disclosure of complexity&lt;/a&gt; in software.&lt;/p&gt;
&lt;p&gt;In this tutorial, you&apos;ll use AuthKit to &lt;strong&gt;add user management to a Next.js project using the app directory, React server components, and server actions&lt;/strong&gt;.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;When the &lt;a href=&quot;https://lwj.dev/workos&quot;&gt;WorkOS&lt;/a&gt; team hit me up to give AuthKit a try, I was excited to dive in. Huge thanks to them for sponsoring this tutorial.&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;Enable authentication in your WorkOS dashboard&lt;/h2&gt;
&lt;p&gt;To enable authentication, head to dashboard.workos.com, then click the Authentication tab and make sure that at least one of the methods is enabled.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:a8fffb/lwj/blog/authkit-00-workos-dashboard.jpg&quot; alt=&quot;the authentication tab of the WorkOS daashboard with Google and Microsoft OAuth enabled&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;While you&apos;re in here, also make sure to visit the Redirects tab and make sure &lt;code&gt;http://localhost:3000/callback&lt;/code&gt; is added as a sign-in callback.&lt;/p&gt;
&lt;h2&gt;Get the project cloned and running&lt;/h2&gt;
&lt;p&gt;We&apos;ll be starting from a modified version of the official AuthKit example app, which is built using the app directory in Next.js and &lt;a href=&quot;https://www.radix-ui.com/&quot;&gt;Radix&lt;/a&gt; (the same UI library AuthKit uses to create the hosted UI). We&apos;re not going to focus on the UI — instead, we&apos;ll only be looking at the specific code that makes user login and session management work in the app.&lt;/p&gt;
&lt;p&gt;To get started, clone the app:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# clone the repo using the start branch
gh repo clone jlengstorf/next-authkit-example -- -b start

# move into the directory
cd workos-authkit/

# install dependencies
npm i
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, copy &lt;code&gt;.env.local.example&lt;/code&gt; to &lt;code&gt;.env.local&lt;/code&gt; and add the required environment variables:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Available in your WorkOS dashboard
WORKOS_CLIENT_ID=&quot;...&quot;
WORKOS_API_KEY=&quot;...&quot;
WORKOS_REDIRECT_URI=&quot;http://localhost:3000/callback&quot;

# Your JWT secret key
JWT_SECRET_KEY=&quot;...&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To get these values:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Head to &lt;a href=&quot;https://dashboard.workos.com/&quot;&gt;dashboard.workos.com&lt;/a&gt; and head to the API Keys tab&lt;/li&gt;
&lt;li&gt;Copy the Client ID from the top as &lt;code&gt;WORKOS_CLIENT_ID&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Click &quot;+ Create Key&quot; to generate a new API key to use as &lt;code&gt;WORKOS_API_KEY&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Leave the &lt;code&gt;WORKOS_REDIRECT_URI&lt;/code&gt; set to &lt;code&gt;http://localhost:3000/callback&lt;/code&gt; — this must match one of the callbacks in your &lt;a href=&quot;https://dashboard.workos.com/&quot;&gt;WorkOS dashboard&lt;/a&gt; Redirects tab&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;JWT_SECRET_KEY&lt;/code&gt; to any value — I used https://www.uuidgenerator.net/&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Finally, start up local development to see the app.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Open &lt;code&gt;http://localhost:3000&lt;/code&gt; to see the running site.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:a8fffb/lwj/blog/authkit-01-local-dev.jpg&quot; alt=&quot;the running dev site after adding env vars and installing dependencies&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;h2&gt;Get the URL for signing in or signing up via AuthKit&lt;/h2&gt;
&lt;p&gt;Our first step is to get the URL that sends a user to AuthKit&apos;s hosted sign in/sign up flow. To do that, make the following changes in &lt;code&gt;src/auth.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { cookies } from &quot;next/headers&quot;;
import { redirect } from &quot;next/navigation&quot;;
import WorkOS, { User } from &quot;@workos-inc/node&quot;;
import { jwtVerify } from &quot;jose&quot;;

// TODO Initialize the WorkOS client
// Initialize the WorkOS client
export const workos = new WorkOS(process.env.WORKOS_API_KEY);

export function getClientId() {
  const clientId = process.env.WORKOS_CLIENT_ID;

  if (!clientId) {
    throw new Error(&quot;WORKOS_CLIENT_ID is not set&quot;);
  }

  return clientId;
}

// TODO get the authorization URL for logging in and signing up with AuthKit
export async function getAuthorizationUrl() {
  const redirectUri = process.env.WORKOS_REDIRECT_URI;

  if (!redirectUri) {
    throw new Error(&quot;WORKOS_REDIRECT_URI is not set&quot;);
  }

  const authorizationUrl = workos.userManagement.getAuthorizationUrl({
    provider: &quot;authkit&quot;,
    clientId: getClientId(),
    // The endpoint that WorkOS will redirect to after a user authenticates
    redirectUri,
  });

  return authorizationUrl;
}

export function getJwtSecretKey() {
  const secret = process.env.JWT_SECRET_KEY;

  if (!secret) {
    throw new Error(&quot;JWT_SECRET_KEY is not set&quot;);
  }

  return new Uint8Array(Buffer.from(secret, &quot;base64&quot;));
}

// TODO verify that the JWT is valid

// TODO determine whether a user is authenticated and return user details if so

// TODO log the user out by deleting their token

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After initializing a new instance of the WorkOS Node SDK, this file also exports a new function called &lt;code&gt;getAuthorizationUrl()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This function uses the &lt;a href=&quot;https://workos.com/docs/reference/user-management&quot;&gt;user management API&lt;/a&gt; — WorkOS&apos;s collection of features that make up AuthKit&apos;s core functionality. The &lt;code&gt;getAuthorizationUrl()&lt;/code&gt; method takes in the client ID and redirect URI (which were set via &lt;code&gt;.env.local&lt;/code&gt; earlier) and the &lt;code&gt;provider&lt;/code&gt;, which is set to &lt;code&gt;&apos;authkit&apos;&lt;/code&gt; to use the hosted UI.&lt;/p&gt;
&lt;p&gt;Put this authorization URL to work by making the following changes in &lt;code&gt;src/app/components/sign-in-button.tsx&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { Button, Flex } from &quot;@radix-ui/themes&quot;;
import { getAuthorizationUrl } from &apos;../../auth&apos;;

export async function SignInButton({ large }: { large?: boolean }) {
  // TODO determine login status from AuthKit
  const isAuthenticated = false;

  // TODO get AuthKit authorization URL
  const authorizationUrl = &quot;#login&quot;;
  const authorizationUrl = await getAuthorizationUrl();

  if (isAuthenticated) {
    return (
      &amp;lt;Flex gap=&quot;3&quot;&amp;gt;
        &amp;lt;form
          action={async () =&amp;gt; {
            &quot;use server&quot;;
            // TODO log the user out
          }}
        &amp;gt;
          &amp;lt;Button type=&quot;submit&quot; size={large ? &quot;3&quot; : &quot;2&quot;}&amp;gt;
            Sign Out
          &amp;lt;/Button&amp;gt;
        &amp;lt;/form&amp;gt;
      &amp;lt;/Flex&amp;gt;
    );
  }

  return (
    &amp;lt;Button asChild size={large ? &quot;3&quot; : &quot;2&quot;}&amp;gt;
      &amp;lt;a href={authorizationUrl}&amp;gt;Sign In {large &amp;amp;&amp;amp; &quot;with AuthKit&quot;}&amp;lt;/a&amp;gt;
    &amp;lt;/Button&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Save these changes, then click the &quot;Sign In with AuthKit&quot; button on the home page. You&apos;ll be redirected to the hosted sign in/sign up flow&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:a8fffb/lwj/blog/authkit-02-auth-url-set.jpg&quot; alt=&quot;JSON error output that reads, &amp;quot;TODO: implement AuthKit callback flow&amp;quot;&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;The user is authorizing the app, but there&apos;s no handler in place to turn the authorization code into a user token. In the next step, we&apos;ll implement the callback handler to exchange the code for a user token.&lt;/p&gt;
&lt;h2&gt;Create a callback handler for user logins&lt;/h2&gt;
&lt;p&gt;Exchange the code for user details by replacing the contents of &lt;code&gt;src/app/callback/route.tsx&lt;/code&gt; with the following code:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { SignJWT } from &quot;jose&quot;;
import { NextRequest, NextResponse } from &quot;next/server&quot;;
import { getJwtSecretKey, workos, getClientId } from &quot;../../auth&quot;;

export async function GET(request: NextRequest) {
  const code = request.nextUrl.searchParams.get(&quot;code&quot;);

  if (code) {
    try {
      // Use the code returned to us by AuthKit and authenticate the user with WorkOS
      const { user } = await workos.userManagement.authenticateWithCode({
        clientId: getClientId(),
        code,
      });

      // Create a JWT token with the user&apos;s information
      const token = await new SignJWT({
        // Here you might lookup and retrieve user details from your database
        user,
      })
        .setProtectedHeader({ alg: &quot;HS256&quot;, typ: &quot;JWT&quot; })
        .setIssuedAt()
        .setExpirationTime(&quot;1h&quot;)
        .sign(getJwtSecretKey());

      const url = request.nextUrl.clone();

      // Cleanup params
      url.searchParams.delete(&quot;code&quot;);

      // Redirect to the requested path and store the session
      url.pathname = &quot;/&quot;;
      const response = NextResponse.redirect(url);

      response.cookies.set({
        name: &quot;token&quot;,
        value: token,
        path: &quot;/&quot;,
        httpOnly: true,
      });

      return response;
    } catch (error) {
      return NextResponse.json(error);
    }
  }

  return NextResponse.json({
    error: &quot;No authorization code was received from AuthKit&quot;,
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To exchange the authorization code for user information, this code uses the &lt;code&gt;authenticateWithCode()&lt;/code&gt; method.&lt;/p&gt;
&lt;p&gt;Next, the user details are encoded into a JSON web token (JWT) using the &lt;code&gt;jose&lt;/code&gt; package.&lt;/p&gt;
&lt;p&gt;Once a token is created, the current URL is cloned, updated to point to the home page, and the code is deleted. This updated URL is where the user will be redirected after a successful authorization.&lt;/p&gt;
&lt;p&gt;Finally, the token is added as an HTTP-only cookie before returning the response.&lt;/p&gt;
&lt;p&gt;After saving, click the &quot;Sign In with AuthKit&quot; button again. You&apos;ll end up back on the home page, which still needs changes to show a logged in user, but if you check the &quot;application&quot; tab in your devtools and look at cookies, you&apos;ll see the &lt;code&gt;token&lt;/code&gt; cookie is now present.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:a8fffb/lwj/blog/authkit-03-cookie-set.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;With the user token set, the next step is to read it and update the UI based on whether a user is logged in.&lt;/p&gt;
&lt;h2&gt;Add logic to verify and load a logged in user + log out&lt;/h2&gt;
&lt;p&gt;Now that a token is set when a user is logged in, add the logic to check and verify that token by making the following changes in &lt;code&gt;src/auth.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { cookies } from &quot;next/headers&quot;;
import { redirect } from &quot;next/navigation&quot;;
import WorkOS, { User } from &quot;@workos-inc/node&quot;;
import { jwtVerify } from &quot;jose&quot;;

// Initialize the WorkOS client
export const workos = new WorkOS(process.env.WORKOS_API_KEY);

export function getClientId() { /* unchanged */ }

export async function getAuthorizationUrl() { /* unchanged */ }

export function getJwtSecretKey() { /* unchanged */ }

// TODO verify that the JWT is valid
export async function verifyJwtToken(token: string) {
  try {
    const { payload } = await jwtVerify(token, getJwtSecretKey());
    return payload;
  } catch (error) {
    return null;
  }
}

// TODO determine whether a user is authenticated and return user details if so
export async function getUser(): Promise&amp;lt;{
  isAuthenticated: boolean;
  user?: User | null;
}&amp;gt; {
  const token = cookies().get(&quot;token&quot;)?.value;
  const verifiedToken = token &amp;amp;&amp;amp; (await verifyJwtToken(token));

  if (verifiedToken) {
    return {
      isAuthenticated: true,
      user: verifiedToken.user as User | null,
    };
  }

  return { isAuthenticated: false };
}

// TODO log the user out by deleting their token
export async function clearCookie() {
  cookies().delete(&quot;token&quot;);
  redirect(&quot;/&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;verifyJwtToken()&lt;/code&gt; function uses the &lt;code&gt;jose&lt;/code&gt; package to make sure the token was created using the secret key set in &lt;code&gt;.env.local&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;getUser()&lt;/code&gt; function loads the &lt;code&gt;token&lt;/code&gt; cookie, verifies it using &lt;code&gt;verifyJwtToken()&lt;/code&gt;, then returns an object with the current authentication status (&lt;code&gt;isAuthenticated&lt;/code&gt;) and, when a user is logged in, the user&apos;s details such as name and email.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;clearCookie()&lt;/code&gt; function deletes the cookie and redirects the user to the home page — this will be used to handle the logout action.&lt;/p&gt;
&lt;h2&gt;Only show the account details page to logged in users&lt;/h2&gt;
&lt;p&gt;For protected routes, such as the account details page, use Next.js middleware to check for a valid token and, if none is found, redirect the user to the authorization URL. Replace the contents of &lt;code&gt;src/middleware.ts&lt;/code&gt; with the following code:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { NextRequest, NextResponse } from &quot;next/server&quot;;
import { getAuthorizationUrl, verifyJwtToken } from &quot;./auth&quot;;

export async function middleware(request: NextRequest) {
  const { cookies } = request;
  const { value: token } = cookies.get(&quot;token&quot;) ?? { value: null };

  const hasVerifiedToken = token &amp;amp;&amp;amp; (await verifyJwtToken(token));

  // Redirect unauthenticated users to the AuthKit flow
  if (!hasVerifiedToken) {
    const authorizationUrl = await getAuthorizationUrl();
    const response = NextResponse.redirect(authorizationUrl);

    response.cookies.delete(&quot;token&quot;);

    return response;
  }

  return NextResponse.next();
}

// Match against the account page
export const config = { matcher: [&quot;/account/:path*&quot;] };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After saving, delete the &lt;code&gt;token&lt;/code&gt; cookie in the Application tab of your devtools and try to visit the account tab — you&apos;ll be redirected to the AuthKit authorization flow.&lt;/p&gt;
&lt;h2&gt;Update the UI to use authentication information&lt;/h2&gt;
&lt;p&gt;The last step is to make the UI react to the presence of a logged in user.&lt;/p&gt;
&lt;h3&gt;Add user details to the account page&lt;/h3&gt;
&lt;p&gt;First, make the following changes to &lt;code&gt;src/app/account/page.tsx&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { Text, Heading, TextFieldInput, Flex, Box } from &quot;@radix-ui/themes&quot;;
import { getUser } from &quot;../../auth&quot;;

export default async function AccountPage() {
  const user: any = {};
  const { user } = await getUser();

  const userFields = user &amp;amp;&amp;amp; [
    [&quot;First name&quot;, user.firstName],
    [&quot;Last name&quot;, user.lastName],
    [&quot;Email&quot;, user.email],
    [&quot;Id&quot;, user.id],
  ];

  return (
    &amp;lt;&amp;gt;
      &amp;lt;Flex direction=&quot;column&quot; gap=&quot;2&quot; mb=&quot;7&quot;&amp;gt;
        &amp;lt;Heading size=&quot;8&quot; align=&quot;center&quot;&amp;gt;
          Account details
        &amp;lt;/Heading&amp;gt;
        &amp;lt;Text size=&quot;5&quot; align=&quot;center&quot; color=&quot;gray&quot;&amp;gt;
          Below are your account details
        &amp;lt;/Text&amp;gt;
      &amp;lt;/Flex&amp;gt;

      {userFields &amp;amp;&amp;amp; (
        &amp;lt;Flex
          direction=&quot;column&quot;
          gap=&quot;3&quot;
          style={{ width: 400 }}
          justify=&quot;center&quot;
        &amp;gt;
          {userFields.map(([label, value]) =&amp;gt; (
            &amp;lt;Flex asChild align=&quot;center&quot; gap=&quot;6&quot; key={value}&amp;gt;
              &amp;lt;label&amp;gt;
                &amp;lt;Text weight=&quot;bold&quot; size=&quot;3&quot; style={{ width: 100 }}&amp;gt;
                  {label}
                &amp;lt;/Text&amp;gt;

                &amp;lt;Box grow=&quot;1&quot;&amp;gt;
                  &amp;lt;TextFieldInput value={value || &quot;&quot;} readOnly /&amp;gt;
                &amp;lt;/Box&amp;gt;
              &amp;lt;/label&amp;gt;
            &amp;lt;/Flex&amp;gt;
          ))}
        &amp;lt;/Flex&amp;gt;
      )}
    &amp;lt;/&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The account page is only available to logged in accounts, so all it needs is the &lt;code&gt;user&lt;/code&gt; details returned by &lt;code&gt;getUser()&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Turn the &quot;sign in&quot; button to a &quot;sign out&quot; button when logged in&lt;/h3&gt;
&lt;p&gt;Next, make the following changes to &lt;code&gt;src/app/components/sign-in-button.tsx&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { getAuthorizationUrl } from &quot;../../auth&quot;;
import { clearCookie, getAuthorizationUrl, getUser } from &quot;../../auth&quot;;
import { Button, Flex } from &quot;@radix-ui/themes&quot;;

export async function SignInButton({ large }: { large?: boolean }) {
  // TODO determine login status from AuthKit
  const isAuthenticated = false;
  const { isAuthenticated } = await getUser();
  const authorizationUrl = await getAuthorizationUrl();

  if (isAuthenticated) {
    return (
      &amp;lt;Flex gap=&quot;3&quot;&amp;gt;
        &amp;lt;form
          action={async () =&amp;gt; {
            &quot;use server&quot;;
            // TODO log the user out
            await clearCookie();
          }}
        &amp;gt;
          &amp;lt;Button type=&quot;submit&quot; size={large ? &quot;3&quot; : &quot;2&quot;}&amp;gt;
            Sign Out
          &amp;lt;/Button&amp;gt;
        &amp;lt;/form&amp;gt;
      &amp;lt;/Flex&amp;gt;
    );
  }

  return (
    &amp;lt;Button asChild size={large ? &quot;3&quot; : &quot;2&quot;}&amp;gt;
      &amp;lt;a href={authorizationUrl}&amp;gt;Sign In {large &amp;amp;&amp;amp; &quot;with AuthKit&quot;}&amp;lt;/a&amp;gt;
    &amp;lt;/Button&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This change uses the actual authenticated status to show a &quot;sign out&quot; button when a user is logged in. It also uses a server action to delete the token cookie when clicked, allowing the user to log out and be redirected to the home page.&lt;/p&gt;
&lt;h3&gt;Update the home page for logged in users&lt;/h3&gt;
&lt;p&gt;Finally, make the following changes to &lt;code&gt;src/app/page.tsx&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { Button, Flex, Heading, Text } from &quot;@radix-ui/themes&quot;;
import NextLink from &quot;next/link&quot;;
import { SignInButton } from &quot;./components/sign-in-button&quot;;
import { getUser } from &quot;../auth&quot;;

export default async function HomePage() {
  // TODO determine login status from AuthKit
  const isAuthenticated = false;

  // TODO load user details from AuthKit when logged in
  const user: any = {};
  const { isAuthenticated, user } = await getUser();

  return (
    &amp;lt;Flex direction=&quot;column&quot; align=&quot;center&quot; gap=&quot;2&quot;&amp;gt;
      {isAuthenticated ? (
        &amp;lt;&amp;gt;
          &amp;lt;Heading size=&quot;8&quot;&amp;gt;
            Welcome back{user?.firstName &amp;amp;&amp;amp; `, ${user?.firstName}`}
          &amp;lt;/Heading&amp;gt;
          &amp;lt;Text size=&quot;5&quot; color=&quot;gray&quot;&amp;gt;
            You are now authenticated into the application
          &amp;lt;/Text&amp;gt;
          &amp;lt;Flex align=&quot;center&quot; gap=&quot;3&quot; mt=&quot;4&quot;&amp;gt;
            &amp;lt;Button asChild size=&quot;3&quot; variant=&quot;soft&quot;&amp;gt;
              &amp;lt;NextLink href=&quot;/account&quot;&amp;gt;View account&amp;lt;/NextLink&amp;gt;
            &amp;lt;/Button&amp;gt;
            &amp;lt;SignInButton large /&amp;gt;
          &amp;lt;/Flex&amp;gt;
        &amp;lt;/&amp;gt;
      ) : (
        &amp;lt;&amp;gt;
          &amp;lt;Heading size=&quot;8&quot;&amp;gt;AuthKit authentication example&amp;lt;/Heading&amp;gt;
          &amp;lt;Text size=&quot;5&quot; color=&quot;gray&quot; mb=&quot;4&quot;&amp;gt;
            Sign in to view your account details
          &amp;lt;/Text&amp;gt;
          &amp;lt;SignInButton large /&amp;gt;
        &amp;lt;/&amp;gt;
      )}
    &amp;lt;/Flex&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This swaps out the hard-coded values for real login status and user details. Save to see the user details displayed on the home page when a user is logged in.&lt;/p&gt;
&lt;h2&gt;Going further with user management&lt;/h2&gt;
&lt;p&gt;As your app grows, you may want to add new features or customize how things look. This is where AuthKit is uniquely valuable, as far as I can tell.&lt;/p&gt;
&lt;h3&gt;Bring your own UI&lt;/h3&gt;
&lt;p&gt;If you want to customize the UI instead of using a hosted workflow, the user management APIs are available in the WorkOS Node SDK to do anything AuthKit can do within your own UI.&lt;/p&gt;
&lt;p&gt;I won&apos;t go into how, but they have a ton of &lt;a href=&quot;https://github.com/workos/authkit/tree/main/src/app/using-your-own-ui&quot;&gt;examples up on GitHub&lt;/a&gt; to get you started.&lt;/p&gt;
&lt;h3&gt;Add enterprise features&lt;/h3&gt;
&lt;p&gt;You might have spotted it when you enabled authentication in your WorkOS dashboard, but adding enterprise user management features like single sign-on is a checkbox away. For more details, check out my tutorial on &lt;a href=&quot;https://www.codetv.dev/blog/workos-sso-okta-idp&quot;&gt;SSO with WorkOS in a Node app&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Resources and further reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.authkit.com/&quot;&gt;AuthKit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://workos.com/docs/reference/user-management&quot;&gt;WorkOS User Management&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.radix-ui.com/&quot;&gt;Radix&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.npmjs.com/package/jose&quot;&gt;&lt;code&gt;jose&lt;/code&gt; on npm&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Add audit logging and log streams to a Node Express app</title><link>https://codetv.dev/blog/audit-logging-node-express/</link><guid isPermaLink="true">https://codetv.dev/blog/audit-logging-node-express/</guid><description>Building a SaaS product is no small feat. And when you start selling to large customers, the list of requirements gets even longer — but if you want to land those six-figure (and beyond) contracts, you&apos;ll need to land enterprise-level features.
</description><pubDate>Tue, 14 Nov 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/f_auto,q_auto/lwj/blog/audit_logs_node_express.jpg&quot; alt=&quot;Add audit logging and log streams to a Node Express app&quot; /&gt;&lt;/p&gt;&lt;p&gt;In this tutorial, we&apos;re going to learn how to add audit logging and log streams to an existing Node app built with Express using WorkOS.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; Thanks to WorkOS for sponsoring this tutorial! Get your app ready for enterprise-level customers by &lt;a href=&quot;https://lwj.dev/workos&quot;&gt;signing up for WorkOS today&lt;/a&gt;.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/7FHrfo3iHZo&quot;&gt;Watch on YouTube&lt;/a&gt;&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Want to jump to the end?&lt;/strong&gt; &lt;a href=&quot;https://github.com/learnwithjason/node-express-audit-log-event-stream-workos&quot;&gt;Check out the source code on
GitHub.&lt;/a&gt;&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;Get the project cloned and running&lt;/h2&gt;
&lt;p&gt;This demo adds audit logging to an existing Node app built using Express. We&apos;ll focus exclusively on how to configure and send audit logging events via Node middleware using the WorkOS audit logging functionality.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; This tutorial assumes you have a WorkOS account and have configured an organization for both SSO and SCIM. It’s not necessary to have these set up to use audit logging, but this app builds on previous tutorials for &lt;a href=&quot;/blog/workos-sso-okta-idp&quot;&gt;configuring SSO in a Node app&lt;/a&gt; and &lt;a href=&quot;/blog/scim-directory-sync&quot;&gt;SCIM provisioning in Express apps&lt;/a&gt;.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;To get started, clone the app:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# clone the repo
gh repo clone learnwithjason/node-express-audit-log-event-stream-workos

# move into the directory
cd node-express-audit-log-event-stream-workos/

# install dependencies
npm i
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, get the required environment variables:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;WORKOS_API_KEY=&quot;&quot;
WORKOS_CLIENT_ID=&quot;&quot;
WORKOS_REDIRECT_URI=&quot;http://localhost:3000/auth/callback&quot;
WORKOS_ORG_ID=&quot;&quot;
WORKOS_DIRECTORY_ID=&quot;&quot;
WORKOS_WEBHOOK_SECRET=&quot;&quot;

SESSION_SECRET=&quot;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These environment variables are all part of getting the SSO and SCIM integrations running. Once these are in place, there are no additional credentials required to add audit logging.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; For full instructions on getting the environment variable values, see the &lt;a href=&quot;/blog/scim-directory-sync#get-environment-variables&quot;&gt;“get environment variables” section of the SCIM tutorial&lt;/a&gt;.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;With the environment variables saved, start the app locally:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will open the app at &lt;code&gt;http://localhost:3000&lt;/code&gt;. Open it in your browser and log in with your SSO credentials to see the dashboard.&lt;/p&gt;
&lt;h2&gt;What is audit logging?&lt;/h2&gt;
&lt;p&gt;On bigger teams, knowing who did what inside the app is a big deal. It creates accountability and makes sure the company is confident that every action can be attributed to one of their team members.&lt;/p&gt;
&lt;p&gt;That&apos;s what &lt;a href=&quot;https://workos.com/docs/audit-logs&quot;&gt;audit logging&lt;/a&gt; is designed to solve: provide a uniform API for keeping track of who did what inside the app to ensure there&apos;s a paper trail.&lt;/p&gt;
&lt;p&gt;Practically speaking, an audit log is a JSON object with data about who did what to which parts of the app that gets sent as an event. These events are stored somewhere (in our case, in WorkOS), and can be further sent (via log streams) to destinations like Amazon S3, Datadog, or Splunk.&lt;/p&gt;
&lt;p&gt;In WorkOS, an &lt;a href=&quot;https://workos.com/docs/reference/audit-logs/create-event&quot;&gt;audit logging event&lt;/a&gt; looks something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;actor&quot;: {
    &quot;id&quot;: &quot;00ua...&quot;,
    &quot;type&quot;: &quot;user&quot;
  },
  &quot;action&quot;: &quot;user.login&quot;,
  &quot;context&quot;: {
    &quot;location&quot;: &quot;0.0.0.0&quot;,
    &quot;user_agent&quot;: &quot;Mozilla/5.0 ...&quot;
  },
  &quot;targets&quot;: [
    {
      &quot;id&quot;: &quot;00ua...&quot;,
      &quot;type&quot;: &quot;user&quot;
    },
    {
      &quot;id&quot;: &quot;directory_group_01...&quot;,
      &quot;type&quot;: &quot;team&quot;
    }
  ],
  &quot;occurred_at&quot;: &quot;2023-11-07T00:21:32.898Z&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There are 5 parts to it:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;actor&lt;/code&gt; — who is taking the action? Providing a type (e.g. &lt;code&gt;user&lt;/code&gt; or &lt;code&gt;system&lt;/code&gt;) along with an identifier provides auditors with the ability to identify who did the thing.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;action&lt;/code&gt; — what was done? This is a unique identifier chosen by whoever sets up audit logging for the team.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;context&lt;/code&gt; — location and browser information about the actor&lt;/li&gt;
&lt;li&gt;&lt;code&gt;targets&lt;/code&gt; — an array of what was acted upon. For example, an event might include the post that was affected, along with the user&apos;s team. These targets can be used to group events during audits (e.g. “show me everything that’s happened to this post”).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;occurred_at&lt;/code&gt; — a timestamp for when the action happened.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The good news is that after a bit of initial setup, audit logging is as straightforward as console logging in an Express app. Let&apos;s build it!&lt;/p&gt;
&lt;h2&gt;Create a new file for audit logging middleware&lt;/h2&gt;
&lt;p&gt;Usually, audit logging is being added to an already existing app. Because of that, it&apos;s likely that we already have most of the information we need configured: the user&apos;s information, the actions they can take, and so on.&lt;/p&gt;
&lt;p&gt;In Express, this information can be added to session storage, and since much of what we&apos;ll need is going to be the same for every event (e.g. the user&apos;s ID), we can use &lt;a href=&quot;https://expressjs.com/en/guide/using-middleware.html&quot;&gt;Express middleware&lt;/a&gt; to provide a helper function for logging that will have most of the work already done for the dev — it makes the right thing the easy thing, which (hopefully) means it will actually get used without someone needing to hound the dev team to implement it.&lt;/p&gt;
&lt;p&gt;Set up this middleware by creating a new file at &lt;code&gt;src/middleware/audit-logging.js&lt;/code&gt; and add the following code inside:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const { WorkOS } = require(&apos;@workos-inc/node&apos;);

const workos = new WorkOS(process.env.WORKOS_API_KEY);

exports.auditLoggingMiddleware = (req, res, next) =&amp;gt; {
	// get the IP and user agent of the request, if available
	const ip = req.headers[&apos;x-forwarded-for&apos;] ?? &apos;&apos;;
	const userAgent = req.headers[&apos;user-agent&apos;] ?? &apos;&apos;;

	req.log = async ({ action, targets = [] }) =&amp;gt; {
		if (!req.session.user || !req.session.user.idpId) {
			console.error(&apos;unable to send audit log events without a valid user&apos;);
			return;
		}

		const userId = req.session.user.idpId;
		const groups = req.session.user.groups ?? [&apos;missing&apos;];

		// all events get attached to the user and team(s) they’re part of
		const defaultTargets = [
			{ type: &apos;user&apos;, id: userId },
			...groups.map((groupId) =&amp;gt; ({ type: &apos;team&apos;, id: groupId })),
		];

		await workos.auditLogs.createEvent(process.env.WORKOS_ORG_ID, {
			action,
			actor: {
				type: &apos;user&apos;,
				id: userId,
			},
			occurredAt: new Date(),
			targets: defaultTargets.concat(targets),
			context: {
				location: ip,
				userAgent,
			},
		});
	};

	next();
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After getting a new instance of the &lt;a href=&quot;https://workos.com/docs/sdks/node&quot;&gt;WorkOS Node SDK&lt;/a&gt;, this file exports a new middleware function called &lt;code&gt;auditLoggingMiddleware&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Inside, it tries to grab the IP address and user agent from the current request, then attaches a new function called &lt;code&gt;log&lt;/code&gt; to the request before continuing the request with &lt;code&gt;next()&lt;/code&gt;. This middleware will run before all routes, which means every route will now have access to &lt;code&gt;req.log()&lt;/code&gt; for sending audit logging events.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;req.log&lt;/code&gt; function accepts an options argument with the name of the action and an optional array of targets. Everything else required will already be present in the session thanks to the auth middleware. (We&apos;ll look at specifically what&apos;s needed in the next section.)&lt;/p&gt;
&lt;p&gt;Inside &lt;code&gt;req.log&lt;/code&gt;, the function will:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Check for a logged in user and bail if none is found&lt;/li&gt;
&lt;li&gt;Grab the user&apos;s ID and group(s) from the session&lt;/li&gt;
&lt;li&gt;Create default targets for the user and each group the user belongs to&lt;/li&gt;
&lt;li&gt;Call the WorkOS SDK&apos;s &lt;code&gt;createEvent&lt;/code&gt; method to send an audit logging event&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;createEvent&lt;/code&gt; requires two arguments:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The organization ID to attach the event to&lt;/li&gt;
&lt;li&gt;The event to log&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The event itself is mostly populated from existing data. The only things that the developer needs to provide are the action type and an optional array of additional targets to attach the event to.&lt;/p&gt;
&lt;p&gt;This setup makes it far less cumbersome to send an audit logging event. In the minimum use case, all the developer needs to do is something like this in a route:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;req.log({ action: &apos;some.action&apos; });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will create an event that&apos;s properly associated with the user and their group(s), along with additional information to help auditors.&lt;/p&gt;
&lt;p&gt;To add additional targets, the developer only needs the ID to send along:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;req.log({
	action: &apos;some.action&apos;,
	targets: [
		{
			type: &apos;document&apos;,
			id: documentId,
		},
	],
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now that we&apos;ve got this set up, let&apos;s add it to the app.&lt;/p&gt;
&lt;h2&gt;Use the custom Express middleware for audit logging&lt;/h2&gt;
&lt;p&gt;In &lt;code&gt;src/index.js&lt;/code&gt;, add the following code:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	const { join } = require(&apos;node:path&apos;);
	const express = require(&apos;express&apos;);
	const session = require(&apos;express-session&apos;);
	const pgSimple = require(&apos;connect-pg-simple&apos;);

	const app = express();
	const sessionStore = pgSimple(session);
+	const { auditLoggingMiddleware } = require(&apos;./middleware/audit-logging&apos;);

	app.set(&apos;view engine&apos;, &apos;ejs&apos;);
	app.set(&apos;views&apos;, join(__dirname, &apos;views&apos;));

	app.use(
		session({
			secret: process.env.SESSION_SECRET,
			resave: false,
			saveUninitialized: false,
			store: new sessionStore({
				createTableIfMissing: true,
				conObject: {
					database: &apos;postgres&apos;,
				},
			}),
		}),
	);
	app.use(express.static(join(__dirname, &apos;static&apos;)));
	app.use(express.json());
	app.use(express.urlencoded({ extended: true }));

+	app.use(auditLoggingMiddleware);

	app.use(&apos;/&apos;, require(&apos;./routes/public&apos;));
	app.use(&apos;/auth&apos;, require(&apos;./routes/auth&apos;));
	app.use(&apos;/dashboard&apos;, require(&apos;./routes/dashboard&apos;));
	app.use(&apos;/api&apos;, require(&apos;./routes/api&apos;));

	const port = 3000;
	app.listen(port, () =&amp;gt; {
		console.log(`app listening at http://localhost:${port}`);
	});

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once this is added, every route in the app will have access to the &lt;code&gt;req.log&lt;/code&gt; method!&lt;/p&gt;
&lt;h2&gt;Make sure the required data is available in the session&lt;/h2&gt;
&lt;p&gt;Because we want to attach audit logging events to both the user &lt;em&gt;and&lt;/em&gt; the user&apos;s teams, we need to make sure the appropriate data is added to the user session.&lt;/p&gt;
&lt;p&gt;Inside &lt;code&gt;src/routes/auth.js&lt;/code&gt;, find the &lt;code&gt;req.get(&apos;/callback&apos;, ...)&lt;/code&gt; route and make the following changes:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	router.get(&apos;/callback&apos;, async (req, res) =&amp;gt; {
		const { code } = req.query;

		const { profile } = await workos.sso.getProfileAndToken({
			code,
			clientID: process.env.WORKOS_CLIENT_ID,
		});

		// store the user in the app database
		const user = await db.getUserByEmail(profile.email);
+		const dirUser = await workos.directorySync
+			.listUsers({
+				directory: process.env.WORKOS_DIRECTORY_ID,
+			})
+			.then(({ data }) =&amp;gt; data.find((u) =&amp;gt; u.idpId === profile.idpId));

		req.session.user = profile;
		req.session.user.id = user.id;
+		req.session.user.idpId = profile.idpId;
		req.session.user.roles = user.roles;
+		req.session.user.groups = dirUser.groups.map((g) =&amp;gt; g.id);

		res.redirect(&apos;/dashboard&apos;);
	});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Users in this app log in with SSO and are synced with a user directory using SCIM, so we use WorkOS&apos;s directory sync to look up all the users and match the current user to get access to all their current groups.&lt;/p&gt;
&lt;p&gt;Next, we attach the identity provider ID and an array of group IDs to the user&apos;s session object.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; The identity provider ID is used as a preference based on the assumption that the IT admin will be using that ID as their source of truth for linking data to users. You can use any unique identifier you prefer.&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;Define audit logging events in WorkOS&lt;/h2&gt;
&lt;p&gt;Next, head over to your WorkOS dashboard, navigate to Configuration &amp;gt; Audit Logs, and click &quot;+ Create an event&quot; to add all the events required for this app. This demo uses 5 event types:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;user.login&lt;/code&gt;, with target types of &lt;code&gt;user&lt;/code&gt; and &lt;code&gt;team&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;user.logout&lt;/code&gt;, with target types of &lt;code&gt;user&lt;/code&gt; and &lt;code&gt;team&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;user.invite&lt;/code&gt;, with target types of &lt;code&gt;user&lt;/code&gt; and &lt;code&gt;team&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;post.create&lt;/code&gt;, with target types of &lt;code&gt;post&lt;/code&gt;, &lt;code&gt;user&lt;/code&gt; and &lt;code&gt;team&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;post.delete&lt;/code&gt;, with target types of &lt;code&gt;post&lt;/code&gt;, &lt;code&gt;user&lt;/code&gt; and &lt;code&gt;team&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once these are created, we can add the audit log calls inside our app.&lt;/p&gt;
&lt;h2&gt;Add audit logging to an Express app&lt;/h2&gt;
&lt;p&gt;In any route that should be logged, add a new call to the &lt;code&gt;req.log&lt;/code&gt; method.&lt;/p&gt;
&lt;p&gt;In &lt;code&gt;src/routes/api.js&lt;/code&gt;, update the &lt;code&gt;/delete-post/:post_id&lt;/code&gt; route:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	router.get(&apos;/invite/:id&apos;, async (req, res) =&amp;gt; {
		// TODO implement invite system
		console.log(`Invited user: ${req.params.id}`);
+		req.log({
+			action: &apos;user.invite&apos;,
+			targets: [{ type: &apos;user&apos;, id: String(req.params.id) }],
+		});
+
		res.redirect(&apos;/dashboard/team&apos;);
	});

	 router.get(&apos;/delete-post/:post_id&apos;, async (req, res) =&amp;gt; {
+		req.log({
+			action: &apos;post.delete&apos;,
+			targets: [{ type: &apos;post&apos;, id: String(req.params.post_id) }],
+		});
+
		await db.query(
			db.sql`
				DELETE FROM posts
				WHERE id = $1
			`,
			[req.params.post_id],
		);

		res.redirect(&apos;/dashboard&apos;);
	});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In &lt;code&gt;src/routes/auth.js&lt;/code&gt;, update the &lt;code&gt;/callback&lt;/code&gt; and &lt;code&gt;/logout&lt;/code&gt; routes:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	router.get(&apos;/callback&apos;, async (req, res) =&amp;gt; {
		const { code } = req.query;

		const { profile } = await workos.sso.getProfileAndToken({
			code,
			clientID: process.env.WORKOS_CLIENT_ID,
		});

		// store the user in the app database
		const user = await db.getUserByEmail(profile.email);
		const dirUser = await workos.directorySync
			.listUsers({
				directory: process.env.WORKOS_DIRECTORY_ID,
			})
			.then(({ data }) =&amp;gt; data.find((u) =&amp;gt; u.idpId === profile.idpId));

		req.session.user = profile;
		req.session.user.id = user.id;
		req.session.user.idpId = profile.idpId;
		req.session.user.roles = user.roles;
		req.session.user.groups = dirUser.groups.map((g) =&amp;gt; g.id);

+		await req.log({ action: &apos;user.login&apos; });
+
		res.redirect(&apos;/dashboard&apos;);
	});

-	router.get(&apos;/logout&apos;, (req, res) =&amp;gt; {
+	router.get(&apos;/logout&apos;, async (req, res) =&amp;gt; {
+		await req.log({ action: &apos;user.logout&apos; });
+
		req.session.user = null;
		req.session.save();

		res.redirect(&apos;/&apos;);
	});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And finally, in &lt;code&gt;src/routes/dashboard.js&lt;/code&gt;, update the &lt;code&gt;/new&lt;/code&gt; route for &lt;code&gt;POST&lt;/code&gt; requests:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	router.post(&apos;/new&apos;, async (req, res) =&amp;gt; {
		const { title, content } = req.body;
		const user_id = req.session.user.id;

		try {
			const result = await db.query(
				db.sql`
					INSERT INTO posts (user_id, title, content)
						VALUES ($1, $2, $3)
+						RETURNING id;
				`,
				[user_id, title, content],
			);

			console.log(result);
+			await req.log({
+				action: &apos;post.create&apos;,
+				targets: [{ type: &apos;post&apos;, id: String(result.rows.at(0).id) }],
+			});
		} catch (err) {
			console.error(err);
		}

		res.redirect(&apos;/dashboard&apos;);
	});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Perform actions and check the log in WorkOS&lt;/h2&gt;
&lt;p&gt;After saving, try logging in, logging out, creating and deleting posts, or inviting a team member. After the actions have been performed, head to your WorkOS dashboard and view the log for your organization. You&apos;ll see audit logging events showing up in real time!&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:c10b84/lwj/blog/workos-audit-logging-01-log-view.jpg&quot; alt=&quot;the WorkOS audit logs dashboard showing recent events&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;h2&gt;Add log streams to integrate with your Security Incident and Event Management (SIEM) provider&lt;/h2&gt;
&lt;p&gt;No team wants to open a bunch of different dashboards to monitor the dozens of tools used by the company. &lt;a href=&quot;https://workos.com/docs/audit-logs/log-streams&quot;&gt;Setting up log streams&lt;/a&gt; allows them to bring all the logs together into a central SIEM provider, and it all happens without any code changes required.&lt;/p&gt;
&lt;p&gt;Inside WorkOS, go to your organization and click &quot;Configure manually&quot; under Log Streams, then choose your SIEM provider (currently supported: Amazon S3, Datadog, and Splunk), then add the configuration details on the next screen to get logs streaming to your provider.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:ffde38/lwj/blog/workos-audit-logging-02-log-streams.jpg&quot; alt=&quot;the WorkOS dashboard showing the log streams destination options of Amazon S3, Datadog, and Splunk&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;And that&apos;s it! Log streams are handled and your teams don&apos;t need to add another dashboard to their list.&lt;/p&gt;
&lt;h2&gt;Audit logging and log streaming are important to larger customers&lt;/h2&gt;
&lt;p&gt;Remember: as companies grow, features like audit logging and log streams start to become critical factors in their decision to adopt a new product.&lt;/p&gt;
&lt;p&gt;These kinds of features maybe don&apos;t feel exciting to implement, but you know what &lt;em&gt;is&lt;/em&gt; exciting? Closing a six-figure deal with a huge new customer.&lt;/p&gt;
&lt;p&gt;Thanks again to WorkOS for sponsoring this tutorial. Happy building, friends!&lt;/p&gt;
&lt;h2&gt;Resources &amp;amp; further reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://workos.com/docs/audit-logs&quot;&gt;WorkOS audit logging&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://expressjs.com/en/guide/using-middleware.html&quot;&gt;Express middleware&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://workos.com/docs/sdks/node&quot;&gt;WorkOS Node SDK&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://workos.com/docs/reference/audit-logs/create-event&quot;&gt;API reference for audit log events&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Animated CSS gradient borders (no JavaScript, no hacks)</title><link>https://codetv.dev/blog/animated-css-gradient-border/</link><guid isPermaLink="true">https://codetv.dev/blog/animated-css-gradient-border/</guid><description>Learn how to create beautiful, CSS-only gradient borders. Combine custom properties, OKLCH, and background-origin — and zero hacks.
</description><pubDate>Sat, 04 Nov 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://youtu.be/M4p29o3KveQ&quot;&gt;Watch on YouTube&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;So much cool stuff has been happening in CSS lately — if you haven’t looked at it in a few years, it might be worth giving things another shot. A lot has changed!&lt;/p&gt;
&lt;p&gt;Here’s what you’ll build by the end of this article.&lt;/p&gt;
&lt;p&gt;CodePen: &lt;a href=&quot;https://codepen.io/jlengstorf/pen/WNPGMJo&quot;&gt;Gorgeous animated gradient borders using only CSS&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;To follow along, &lt;a href=&quot;https://codepen.io/pen&quot;&gt;create your own CodePen&lt;/a&gt; or open an HTML document where you can modify the HTML and CSS.&lt;/p&gt;
&lt;h2&gt;Set up the HTML markup&lt;/h2&gt;
&lt;p&gt;Because of the way these borders are set up, we don’t need any extra container elements. We can use straightforward semantic markup.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;main&amp;gt;
  &amp;lt;article&amp;gt;
    &amp;lt;h1&amp;gt;Hey look, this is only CSS!&amp;lt;/h1&amp;gt;
    &amp;lt;p&amp;gt;
      I didn’t know you could do gradient borders like this. Hover over this
      element to see the gradient animate!
    &amp;lt;/p&amp;gt;
  &amp;lt;/article&amp;gt;
&amp;lt;/main&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;article&lt;/code&gt; will be the element that we style with the border.&lt;/p&gt;
&lt;h2&gt;Add base styles&lt;/h2&gt;
&lt;p&gt;This step is optional. To make our demo look a little nicer, let’s add some base styles:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a basic reset&lt;/li&gt;
&lt;li&gt;background colors&lt;/li&gt;
&lt;li&gt;centering and spacing&lt;/li&gt;
&lt;li&gt;miscellaneous touches to make it feel nice&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;* {
  box-sizing: border-box;
}

html {
  font-family: -apple-system, BlinkMacSystemFont, &apos;Segoe UI&apos;, Roboto, Helvetica,
    Arial, sans-serif, &apos;Apple Color Emoji&apos;, &apos;Segoe UI Emoji&apos;, &apos;Segoe UI Symbol&apos;;
  font-size: 18px;
  line-height: 1.45;
}

body {
  margin: 0;
}

main {
  background: radial-gradient(
      circle,
      oklch(0.15 0.2 330 / 0),
      oklch(0.15 0.2 330 / 1)
    ), linear-gradient(344deg in oklch, oklch(0.3 0.37 310), oklch(0.35 0.37 330), oklch(0.3
          0.37 310));
  display: grid;
  height: 100svh;
  place-items: center;
}

article {
  border-radius: 1rem;
  box-shadow: 0.125rem 0.25rem 0.25rem 0.5rem oklch(0.1 0.37 315 / 0.25);
  color: white;
  padding: 1rem;
  width: min(400px, 90vw);

  &amp;amp; h1 {
    line-height: 1.1;
    margin: 0;
  }

  &amp;amp; p {
    margin: 0.75rem 0 0;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Heads up!&lt;/strong&gt; &lt;a href=&quot;https://caniuse.com/css-nesting&quot;&gt;CSS nesting&lt;/a&gt; Just Works™ in
modern browsers now! No PostCSS, Sass, or other CSS preprocessor required.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;Things won’t quite look right yet, but you’ll be looking much better than the default styles.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:585760/lwj/blog/css-gradient-border-01-base-styles.jpg&quot; alt=&quot;CodePen screenshot showing the base styles applied to the
markup&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;h2&gt;Create a gradient border using &lt;code&gt;background-origin&lt;/code&gt; and &lt;code&gt;conic-gradient()&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;To add the gradient border, we need to add two backgrounds to the &lt;code&gt;article&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;one to be the solid, interior background (the dark color that makes the text visible)&lt;/li&gt;
&lt;li&gt;one to be the gradient border (a conical gradient)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To add these, add the following styles:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;article {
  background: linear-gradient(
        to bottom,
        oklch(0.1 0.2 240 / 0.95),
        oklch(0.1 0.2 240 / 0.95)
      ) padding-box, conic-gradient(
        from 0deg in oklch longer hue,
        oklch(1 0.37 0) 0 0
      ) border-box;

  border: 6px solid transparent;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After saving, the border will appear!&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:585760/lwj/blog/css-gradient-border-02-add-border.jpg&quot; alt=&quot;CodePen screenshot showing the gradient border applied to the
element&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;Let’s break down how this works.&lt;/p&gt;
&lt;h3&gt;Use multiple backgrounds&lt;/h3&gt;
&lt;p&gt;We add two backgrounds to our element:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/linear-gradient&quot;&gt;&lt;code&gt;linear-gradient()&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;A &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/conic-gradient&quot;&gt;&lt;code&gt;conic-gradient()&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The first background is the solid background inside the element that the text is placed on top of.&lt;/p&gt;
&lt;p&gt;The second background is the gradient that will be used for the border.&lt;/p&gt;
&lt;h3&gt;Use &lt;code&gt;background-origin&lt;/code&gt; to place the gradient in the border&lt;/h3&gt;
&lt;p&gt;A setting I’d never given much thought to before was &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/background-origin&quot;&gt;&lt;code&gt;background-origin&lt;/code&gt;&lt;/a&gt;. This tells the browser where the background should be contained (e.g. should it go to the border? the end of the padding? just the content area?).&lt;/p&gt;
&lt;p&gt;By setting our first background to &lt;code&gt;padding-box&lt;/code&gt; and the second to &lt;code&gt;border-box&lt;/code&gt;, the gradient background extends further than the inner background, allowing it to become a “border”.&lt;/p&gt;
&lt;p&gt;This means we control the border mostly in the usual way: we set a &lt;code&gt;border&lt;/code&gt; property and choose a width. The only difference is that we set the border’s color to &lt;code&gt;transparent&lt;/code&gt;, which allows the gradient background to shine through.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; The &lt;code&gt;background-origin&lt;/code&gt; property doesn’t affect background colors.
That’s why the first background uses &lt;code&gt;linear-gradient()&lt;/code&gt;, despite being a
solid color.&lt;/p&gt;&lt;/aside&gt;
&lt;h3&gt;Use OKLCH for better-looking gradients&lt;/h3&gt;
&lt;p&gt;I had so much to say about &lt;a href=&quot;/blog/oklch-better-color-css-browser&quot;&gt;why OKLCH is my new favorite way to do color in CSS&lt;/a&gt; that I had to split it out into its own post.&lt;/p&gt;
&lt;p&gt;The gist is this: gradients in CSS used to look bad, but HSL and LCH make them look good. This is partly due to how gradients are calculated in different color spaces, and partly due to &lt;a href=&quot;https://lea.verou.me/blog/2020/04/lch-colors-in-css-what-why-and-how/&quot;&gt;LCH’s ability to represent about 50% more colors&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;In our gradient code, we set a super bright pink using &lt;code&gt;oklch(1 0.37 0)&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Use a &lt;code&gt;conic-gradient()&lt;/code&gt; to have the gradient “wrap” the whole border&lt;/h3&gt;
&lt;p&gt;A conical gradient starts from a center point and goes around it in a circle (or, you know, a cone). By placing the gradient in the middle of our container, the parts that are visible at the border appear to follow the border itself, effectively “wrapping” the component in a gradient.&lt;/p&gt;
&lt;p&gt;Pretty nice, right?&lt;/p&gt;
&lt;p&gt;But the syntax looks a little wild at first, so let’s break down what’s happening.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    conic-gradient(
        from 0deg in oklch longer hue,
        oklch(1 0.37 0) 0 0
      )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There’s a surprising amount of information packed into these four lines of code, so let’s break it down piece by piece, starting with the first line:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;from 0deg&lt;/code&gt; — this is the starting angle of the gradient&lt;/li&gt;
&lt;li&gt;&lt;code&gt;in oklch&lt;/code&gt; — this tells CSS to use the OKLCH color space to calculate the gradient (since we also use &lt;code&gt;oklch()&lt;/code&gt; to set the color, this isn’t stricly necessary, but it doesn’t hurt to be explicit)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;longer hue&lt;/code&gt; — I learned this from &lt;a href=&quot;https://twitter.com/ChallengesCss/status/1720577031232987169&quot;&gt;Temani Afif, who pointed me&lt;/a&gt; to &lt;a href=&quot;https://developer.chrome.com/articles/high-definition-css-color-guide/#shorter-vs-longer-hue-interpolation&quot;&gt;Adam Argyle’s illustrated explanation of how hues are calculated in gradients&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;tl;dr: hues are a circle, and we can tell CSS to take the long way around the circle to calculate the gradient, resulting in additional colors&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Next, let’s take a look at the second line:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;oklch(1 0.37 0)&lt;/code&gt; — this is our starting color, which is a vivid pink&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0 0&lt;/code&gt; — this is shorthand for “use the same color” (another &lt;a href=&quot;https://twitter.com/ChallengesCss/status/1720577468673806660&quot;&gt;tip from Temani&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Because we’re using the &lt;code&gt;longer hue&lt;/code&gt; setting, creating a &lt;code&gt;conic-gradient()&lt;/code&gt; with the same start and end color gives us a full spin around the color wheel, resulting in a very colorful gradient with a single line of code!&lt;/p&gt;
&lt;h2&gt;Animate the gradient border&lt;/h2&gt;
&lt;p&gt;To animate the border, we need exactly one thing to change: the angle of the gradient. By animating the starting angle from &lt;code&gt;0deg&lt;/code&gt; to &lt;code&gt;360deg&lt;/code&gt;, we get an infinitely spinning animated gradient border.&lt;/p&gt;
&lt;h3&gt;This is harder than it looks — why doesn’t this work?&lt;/h3&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Heads up!&lt;/strong&gt; If you don’t care about &lt;em&gt;why&lt;/em&gt; the final code works, you can skip
this section.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;This is deceptively hard, though. My original thought was that I could do something with a CSS custom property:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* this doesn’t work? why?! */
:root {
  --bg-angle: 0deg;
}

@keyframes spin {
  to {
    --bg-angle: 360deg;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This approach results in an animation that “pops” from one state to the next, so it looks like nothing is happening. If you change the keyframes value to &lt;code&gt;180deg&lt;/code&gt;, though, you can see what’s actually happening more clearly.&lt;/p&gt;
&lt;figure&gt;&lt;/figure&gt;
&lt;p&gt;There was no way I knew of to solve this without JavaScript. At least, until very recently.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;@property&lt;/code&gt; makes angle interpolation work in CSS&lt;/h3&gt;
&lt;p&gt;However, &lt;code&gt;@property&lt;/code&gt; makes interpolation work by telling CSS what kind of unit is stored in the custom property — this means animations work smoothly! It’s in all modern browsers now with the exception of &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/121&quot;&gt;Firefox, which will release support on December 19, 2023&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The main difference is that we can give the custom property a type. Here’s how it works:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@property --bg-angle {
  inherits: false;
  initial-value: 0deg;
  syntax: &apos;&amp;lt;angle&amp;gt;&apos;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;syntax&lt;/code&gt; is the secret sauce that makes this work. By specifying &lt;code&gt;&quot;&amp;lt;angle&amp;gt;&quot;&lt;/code&gt;, CSS knows how to interpolate changed values in animation, which means the animations can be smooth now.&lt;/p&gt;
&lt;p&gt;Update the &lt;code&gt;conic-gradient()&lt;/code&gt; to use the custom property and add the animation — but let’s start out the animation as &lt;code&gt;paused&lt;/code&gt;. We only want to set the &lt;code&gt;animation-play-state&lt;/code&gt; to &lt;code&gt;running&lt;/code&gt; when the element is hovered.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;+	@property --bg-angle {
+		inherits: false;
+		initial-value: 0deg;
+		syntax: &quot;&amp;lt;angle&amp;gt;&quot;;
+	}
+
+	@keyframes spin {
+		to {
+			--bg-angle: 180deg;
+		}
+	}
+
	article {
+		animation: spin 1.5s linear infinite paused;
		background: linear-gradient(
					to bottom,
					oklch(0.1 0.2 240 / 0.95),
					oklch(0.1 0.2 240 / 0.95)
				)
				padding-box,
			conic-gradient(
-					from 0deg in oklch longer hue,
+					from var(--bg-angle) in oklch longer hue,
					oklch(1 0.37 0) 0 0
				)
				border-box;

		border: 6px solid transparent;
+
+		&amp;amp;:hover {
+			animation-play-state: running;
+		}
	}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once you’ve made these changes, you should see the gradient borders and they’ll animate when you hover!&lt;/p&gt;
&lt;p&gt;CodePen: &lt;a href=&quot;https://codepen.io/jlengstorf/pen/WNPGMJo&quot;&gt;Gorgeous animated gradient borders using only CSS&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;It blows my mind that this kind of stuff is not only possible in CSS these days, but that we can do it without complex hacks, pseudo-elements, hidden elements, or other messy implementations that always felt fragile to me.&lt;/p&gt;
&lt;h2&gt;Resources &amp;amp; Further Reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://caniuse.com/mdn-css_at-rules_property&quot;&gt;&lt;code&gt;@property&lt;/code&gt; browser support&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://caniuse.com/mdn-css_types_color_oklch&quot;&gt;OKLCH browser support&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://dev.to/afif/we-can-finally-animate-css-gradient-kdk&quot;&gt;&lt;em&gt;We can finally animate CSS gradient&lt;/em&gt; by Temani Afif&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.w3.org/TR/css-images-4/#conic-gradients&quot;&gt;&lt;code&gt;conic-gradient()&lt;/code&gt; in the W3C spec&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/background-origin&quot;&gt;&lt;code&gt;background-origin&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/oklch&quot;&gt;&lt;code&gt;oklch()&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>OKLCH for better color in the browser</title><link>https://codetv.dev/blog/oklch-better-color-css-browser/</link><guid isPermaLink="true">https://codetv.dev/blog/oklch-better-color-css-browser/</guid><description>Why I’ll (probably) never use hex colors in CSS again, and what I’m doing instead. With visual examples!
</description><pubDate>Sat, 04 Nov 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Something that has always bugged me about CSS is the dullness of most gradients.&lt;/p&gt;
&lt;p&gt;You’d have this beautiful idea for your design, set up the gradient in your CSS, and when it rendered in the browser it was just... &lt;em&gt;blech&lt;/em&gt;.&lt;/p&gt;
&lt;figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/figure&gt;
&lt;p&gt;The middle gets all muddy and the whole thing just feels kinda flat and lifeless.&lt;/p&gt;
&lt;p&gt;But watch what happens when you do the &lt;em&gt;exact same gradient&lt;/em&gt;, but in the OKLCH color space, which I learned was available to use from &lt;a href=&quot;https://chriscoyier.net/2023/01/22/ok-oklch-%f0%9f%91%91/&quot;&gt;Chris Coyier’s post crowning OKLCH as the “winner” for color in CSS&lt;/a&gt;:&lt;/p&gt;
&lt;figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/figure&gt;
&lt;p&gt;Now &lt;em&gt;that&lt;/em&gt; looks great!&lt;/p&gt;
&lt;p&gt;So... what exactly is happening here, and why does OKLCH make such a big difference?&lt;/p&gt;
&lt;h2&gt;I am not the expert here&lt;/h2&gt;
&lt;p&gt;Before I start explaining anything: I’m not an expert in this stuff. I read a bunch of material written by actual experts, and I’m collecting what I learned here in hopes that some of the knowledge gets caught in my brain wrinkles along the way.&lt;/p&gt;
&lt;p&gt;If you want to read the experts, check out the links and resources at the end of this post.&lt;/p&gt;
&lt;h2&gt;What the heck is a color space?&lt;/h2&gt;
&lt;p&gt;There’s an entire field of study behind color spaces, so let me gloss over most of it and say: color spaces are the math computers use to turn stuff like &lt;code&gt;rgb(100 200 0 / 0.5)&lt;/code&gt; into color on the screen.&lt;/p&gt;
&lt;p&gt;The default browser color space, sRGB, is called a &lt;em&gt;rectangular&lt;/em&gt; color space. In addition to &lt;code&gt;srgb&lt;/code&gt;, browsers also support rectangular color spaces &lt;code&gt;srgb-linear&lt;/code&gt;, &lt;code&gt;lab&lt;/code&gt;, &lt;code&gt;oklab&lt;/code&gt;, &lt;code&gt;xyz&lt;/code&gt;, &lt;code&gt;xyz-d50&lt;/code&gt;, and &lt;code&gt;xyz-d65&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;OKLCH, however, is a &lt;em&gt;polar&lt;/em&gt; color space. The browser can support several polar color spaces: &lt;code&gt;hsl&lt;/code&gt;, &lt;code&gt;hwb&lt;/code&gt;, &lt;code&gt;lch&lt;/code&gt;, and &lt;code&gt;oklch&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;What does color space change how gradients look?&lt;/h2&gt;
&lt;p&gt;From what I understand, the reason gradients look so much better in OKLCH is that rectangular color spaces draw a straight line through the color space between the gradient colors, where polar color spaces draw an arc through the hues to get there.&lt;/p&gt;
&lt;p&gt;That’s why the same gradient looks muddy in &lt;code&gt;srgb&lt;/code&gt; and looks great in &lt;code&gt;oklch&lt;/code&gt;. To (very roughly) visualize what’s happening with a gradient, take a look at the lines drawn between the two colors from our gradients above:&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:585760/lwj/blog/srgb-oklch-color-spaces-gradients.jpg&quot; alt=&quot;Two color wheels, with the first showing a straight line between two
selected colors and the second showing an arc drawn between the two selected
colors&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;So in a rectangular color space, the interpolation takes the middle colors from the muddy middle of the color wheel, which Adam Argyle lovingly calls “the dead zone” in this &lt;a href=&quot;https://developer.chrome.com/articles/high-definition-css-color-guide/&quot;&gt;article that has more information about color&lt;/a&gt; than anyone could ask for.&lt;/p&gt;
&lt;p&gt;In a polar color space the arc keeps the gradient along the hues at roughly the same distance from the “center” (again: lots of math happening here that I’m skimming past).&lt;/p&gt;
&lt;h2&gt;OKLCH is good for more than just gradients, though&lt;/h2&gt;
&lt;p&gt;On top of making gradients look actually good, OKLCH moves us into a color space that literally has more colors in it. sRGB is limited because when it was defined, monitors couldn’t display all the colors it defined. These days, though, we’re getting HDR monitors that support nearly double the colors, and OKLCH lets us start using them.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:585760/lwj/blog/evil-martians-oklch-color-picker.jpg&quot; alt=&quot;Screenshot of the Evil Martians OKLCH color
picker&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;The short version is that if we switch to OKLCH, we get a larger palette of brighter, more vivid colors. You can see the extra colors visualized in &lt;a href=&quot;https://oklch.com/#84.52,0.185,202.63,100&quot;&gt;Evil Martian’s OKLCH color picker&lt;/a&gt; (note the difference between the selected color and the fallback).&lt;/p&gt;
&lt;p&gt;The long version is far better explained in &lt;a href=&quot;https://lea.verou.me/blog/2020/04/lch-colors-in-css-what-why-and-how/&quot;&gt;Lea Verou’s post on LCH in CSS&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;See the OKLCH difference for yourself&lt;/h2&gt;
&lt;p&gt;I threw together a quick CodePen showing how a gradient looks in all the different color spaces. Edit this to change out the colors and see how each color space handles it.&lt;/p&gt;
&lt;p&gt;CodePen: &lt;a href=&quot;https://codepen.io/jlengstorf/pen/ZEwBdVq&quot;&gt;Visualizing all CSS color-interpolation-methods in gradients&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I’ll let you draw your own conclusions since this all comes down to preference, but for me, OKLCH will be my new default choice for colors in the browser.&lt;/p&gt;
&lt;h2&gt;Resources &amp;amp; Further Reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://evilmartians.com/chronicles/oklch-in-css-why-quit-rgb-hsl&quot;&gt;&lt;em&gt;OKLCH in CSS: why we moved from RGB and HSL&lt;/em&gt; by Evil Martians&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://chriscoyier.net/2023/01/22/ok-oklch-%f0%9f%91%91/&quot;&gt;&lt;em&gt;OK OKLCH 👑&lt;/em&gt; by Chris Coyier&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://lea.verou.me/blog/2020/04/lch-colors-in-css-what-why-and-how/&quot;&gt;&lt;em&gt;LCH colors in CSS: what, why, and how?&lt;/em&gt; by Lea Verou&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.chrome.com/articles/high-definition-css-color-guide/&quot;&gt;&lt;em&gt;High Definition CSS Color Guide&lt;/em&gt; by Adam Argyle&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>The killer tool for devs who don&apos;t want to touch design is... Wix?</title><link>https://codetv.dev/blog/wix-custom-code/</link><guid isPermaLink="true">https://codetv.dev/blog/wix-custom-code/</guid><description>If you&apos;re a developer who focuses on data &amp; APIs and doesn&apos;t want to touch the design of a site, Wix might be exactly what you need. Hear me out.
</description><pubDate>Mon, 23 Oct 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/f_auto,q_auto/lwj/blog/wix-studio.jpg&quot; alt=&quot;The killer tool for devs who don&apos;t want to touch design is... Wix?&quot; /&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://youtu.be/BUApR8vFW6k&quot;&gt;Watch on YouTube&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;A lot of developers I&apos;ve spoken to really enjoy working with the data layer of web apps, but wish they never had to touch UI code. If you&apos;re one of those developers, Wix might actually be an excellent fit for your dev stack.&lt;/p&gt;
&lt;h2&gt;Wait, you mean THAT Wix?&lt;/h2&gt;
&lt;p&gt;If you thought Wix was only for no-code sites, you&apos;re not alone. Up until the team at Wix hit me up and asked if I&apos;d be interested in trying out their developer tools, I thought the same thing.&lt;/p&gt;
&lt;p&gt;Wix Studio, which Wix describes as &quot;a new end-to-end web creation platform for freelancers and agencies&quot;, gives us access to a new tools and a library of developer APIs that let us handle the backend of our client sites while letting our clients continue managing their site&apos;s look and feel in a visual, no-code interface.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;Disclosure: Wix Studio sponsored this tutorial, but they don&apos;t have any
control over what I built or what I say about the tools.&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;What does Wix Studio offer developers?&lt;/h2&gt;
&lt;p&gt;When I started looking through the developer-focused tools Wix Studio offers, two things caught my eye:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;There&apos;s a &lt;a href=&quot;https://dev.wix.com/docs/develop-websites/articles/coding-with-wix-studio/wix-studio-about-the-wix-ide&quot;&gt;VS Code-based Wix IDE&lt;/a&gt; that lets you work on you Wix site code in a familiar interface (if you&apos;re a VS Code user).&lt;/li&gt;
&lt;li&gt;There&apos;s a set of APIs called &lt;a href=&quot;https://www.wix.com/velo/reference/api-overview&quot;&gt;Velo&lt;/a&gt; that provides JavaScript APIs for modifying Wix element data, handling animation, auth, and all sorts of other advanced controls I wasn&apos;t expecting to see.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I&apos;m not going to get into all the details during this build, so if you&apos;re interested go hit up their docs to see all the things you&apos;re able to do.&lt;/p&gt;
&lt;p&gt;What we&apos;ll cover in this tutorial is a first look at how a developer can manage the data layer of a client&apos;s Wix site using an IDE and APIs that are familiar and surprisingly nice to work with.&lt;/p&gt;
&lt;h2&gt;The setup: create a Wix site with a Pro Gallery element&lt;/h2&gt;
&lt;p&gt;To get started, we need a Wix site we can add custom code to. For my demo, I created a new site with the &quot;start from a blank canvas&quot; option, then added a Pro Gallery element to the page.&lt;/p&gt;
&lt;p&gt;That&apos;s really all we need. You can play with it a bit, but really the point here is that we don&apos;t need to worry about what it looks like. The client will do whatever they want to on the presentation side; we only need to worry about getting the right image data into the gallery.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:585760/lwj/blog/wix-studio-01-new-site.jpg&quot; alt=&quot;the Pro Gallery component displayed in Wix Studio&apos;s
editor&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;h2&gt;Enable the Wix Studio developer tools&lt;/h2&gt;
&lt;p&gt;To get started with coding, click the curly bois (&lt;code&gt;{}&lt;/code&gt;) in the left-hand nav and click the &quot;Start Coding&quot; button.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:585760/lwj/blog/wix-studio-02-enable-dev-tools.jpg&quot; alt=&quot;Code tab in Wix Studio on first use showing a confirmation for enabling dev
tools.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;This opens up the Wix Studio code tools, which are fine, but what I really want is that VS Code-based IDE — so click the &quot;Code in Wix IDE&quot; button at the top of the Page Code section.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:585760/lwj/blog/wix-studio-03-page-code-view.jpg&quot; alt=&quot;the code tab in Wix Studio with a link to &amp;quot;Code in Wix IDE&amp;quot; shown at the top
of the
pane&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;This opens the familiar VS Code UI right in the browser, which is pretty slick.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:585760/lwj/blog/wix-studio-04-wix-ide.jpg&quot; alt=&quot;Wix IDE&apos;s home screen in the
browser&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;h2&gt;Use code to set Wix gallery element images&lt;/h2&gt;
&lt;p&gt;Now that we&apos;ve got a site and we&apos;re in the Wix IDE, let&apos;s use the &lt;a href=&quot;https://www.wix.com/velo/reference/$w&quot;&gt;Wix Editor Elements Velo API&lt;/a&gt; to tell the page which images should be displayed in the gallery.&lt;/p&gt;
&lt;p&gt;In the explorer, go into the &lt;code&gt;src/pages/&lt;/code&gt; directory and look for the file called &lt;code&gt;Home&lt;/code&gt; with a random-looking suffix and a &lt;code&gt;.js&lt;/code&gt; extension. In my site, the file is &lt;code&gt;src/pages/Home.c1dmp.js&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Inside, replace the contents with the following code:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// docs: https://www.wix.com/velo/reference/$w/gallery
$w.onReady(function () {
  // @ts-expect-error known issue where IFrame type is inferred incorrectly
  $w(&apos;#gallery1&apos;).items = [
    {
      type: &apos;image&apos;,
      title: &apos;Yellow Tracksuit&apos;,
      description: &apos;Photo by Dom Hill&apos;,
      alt: &apos;woman in yellow tracksuit standing on basketball court side&apos;,
      src: &apos;https://images.unsplash.com/photo-1515886657613-9f3515b0c78f?ixlib=rb-4.0.3&amp;amp;ixid=M3wxMjA3fDB8MHxzZWFyY2h8Mnx8ZmFzaGlvbnxlbnwwfHwwfHx8MA%3D%3D&amp;amp;auto=format&amp;amp;fit=crop&amp;amp;w=1200&amp;amp;q=60&apos;,
      link: &apos;https://unsplash.com/photos/nimElTcTNyY&apos;,
    },
    {
      type: &apos;image&apos;,
      title: &apos;Striped V-Neck Tee&apos;,
      description: &apos;Photo by Ayo Ogunseinde&apos;,
      alt: &apos;black haired man making face&apos;,
      src: &apos;https://images.unsplash.com/photo-1463453091185-61582044d556?ixlib=rb-4.0.3&amp;amp;ixid=M3wxMjA3fDB8MHxzZWFyY2h8N3x8YmxhY2slMjBtYW58ZW58MHwwfDB8fHww&amp;amp;auto=format&amp;amp;fit=crop&amp;amp;w=1200&amp;amp;q=60&apos;,
      link: &apos;https://unsplash.com/photos/sibVwORYqs0&apos;,
    },
    {
      type: &apos;image&apos;,
      title: &apos;Coral Blouse&apos;,
      description: &apos;Photo by Eye for Ebony&apos;,
      alt: &apos;woman standing in front of red brick wall&apos;,
      src: &apos;https://images.unsplash.com/photo-1502764613149-7f1d229e230f?ixlib=rb-4.0.3&amp;amp;ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTF8fGJsYWNrJTIwd29tYW58ZW58MHwwfDB8fHww&amp;amp;auto=format&amp;amp;fit=crop&amp;amp;w=1200&amp;amp;q=60&apos;,
      link: &apos;https://unsplash.com/photos/OExQjtxbIpE&apos;,
    },
    {
      type: &apos;image&apos;,
      title: &apos;Pinstripe Parachute Pants&apos;,
      description: &apos;Photo by Ahmed Carter&apos;,
      alt: &apos;posing woman in white sleeveless top&apos;,
      src: &apos;https://images.unsplash.com/photo-1509631179647-0177331693ae?ixlib=rb-4.0.3&amp;amp;ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTR8fGZhc2hpb258ZW58MHx8MHx8fDA%3D&amp;amp;auto=format&amp;amp;fit=crop&amp;amp;w=1200&amp;amp;q=60&apos;,
      link: &apos;https://unsplash.com/photos/posing-woman-in-white-sleeveless-top-tiWcNvpQF4E&apos;,
    },
    {
      type: &apos;image&apos;,
      title: &apos;White Dress&apos;,
      description: &apos;Photo by Vladimir Yelizarov&apos;,
      alt: &apos;woman in white dress sitting on white textile&apos;,
      src: &apos;https://images.unsplash.com/photo-1617551307578-7f5160d6615e?ixlib=rb-4.0.3&amp;amp;ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTh8fGJsYWNrJTIwd29tYW58ZW58MHwwfDB8fHww&amp;amp;auto=format&amp;amp;fit=crop&amp;amp;w=1200&amp;amp;q=60&apos;,
      link: &apos;https://unsplash.com/photos/PfbItsR8Gqo&apos;,
    },
    {
      type: &apos;image&apos;,
      title: &apos;Wool Overcoat&apos;,
      description: &apos;Photo by Alexandru Zdrobău&apos;,
      alt: &apos;woman holding brown leather bag in bokeh photography&apos;,
      src: &apos;https://images.unsplash.com/photo-1485968579580-b6d095142e6e?ixlib=rb-4.0.3&amp;amp;ixid=M3wxMjA3fDB8MHxzZWFyY2h8MzF8fGZhc2hpb258ZW58MHx8MHx8fDA%3D&amp;amp;auto=format&amp;amp;fit=crop&amp;amp;w=1200&amp;amp;q=60&apos;,
      link: &apos;https://unsplash.com/photos/juESZxMhtXk&apos;,
    },
    {
      type: &apos;image&apos;,
      title: &apos;Gray Jumper&apos;,
      description: &apos;Photo by Judeus Samson&apos;,
      alt: &apos;woman standing in the middle of the road&apos;,
      src: &apos;https://images.unsplash.com/photo-1545291730-faff8ca1d4b0?ixlib=rb-4.0.3&amp;amp;ixid=M3wxMjA3fDB8MHxzZWFyY2h8MjR8fGZhc2hpb258ZW58MHx8MHx8fDA%3D&amp;amp;auto=format&amp;amp;fit=crop&amp;amp;w=1200&amp;amp;q=60&apos;,
      link: &apos;https://unsplash.com/photos/0UECcInuCR4&apos;,
    },
    {
      type: &apos;image&apos;,
      title: &apos;Leather Jacket&apos;,
      description: &apos;Photo by Dami Adebayo&apos;,
      alt: &apos;man in brown leather coat&apos;,
      src: &apos;https://images.unsplash.com/photo-1487222477894-8943e31ef7b2?ixlib=rb-4.0.3&amp;amp;ixid=M3wxMjA3fDB8MHxzZWFyY2h8Mjh8fGZhc2hpb258ZW58MHx8MHx8fDA%3D&amp;amp;auto=format&amp;amp;fit=crop&amp;amp;w=1200&amp;amp;q=60&apos;,
      link: &apos;https://unsplash.com/photos/k6aQzmIbR1s&apos;,
    },
    {
      type: &apos;image&apos;,
      title: &apos;Wool Sweater&apos;,
      description: &apos;Photo by Maia Habegger&apos;,
      alt: &apos;woman wearing brown sweater standing on forest&apos;,
      src: &apos;https://images.unsplash.com/photo-1520508358701-63027028d924?ixlib=rb-4.0.3&amp;amp;ixid=M3wxMjA3fDB8MHxzZWFyY2h8M3x8d29tYW4lMjBzd2VhdGVyfGVufDB8MHwwfHx8MA%3D%3D&amp;amp;auto=format&amp;amp;fit=crop&amp;amp;w=1200&amp;amp;q=60&apos;,
      link: &apos;https://unsplash.com/photos/mAg7Dz1IQQU&apos;,
    },
  ];
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you&apos;ve ever used jQuery or the browser&apos;s built-in &lt;code&gt;querySelector&lt;/code&gt; API, this might look familiar.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;$w()&lt;/code&gt; function accepts a CSS selector as an argument, and in the Wix IDE the available elements will autocomplete once you type the quotes. Every Wix element automatically gets an ID that matches its type, so if you create your first gallery element, the ID will be &lt;code&gt;#gallery1&lt;/code&gt;.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; It&apos;s possible to change this ID through the Wix Studio if you
prefer, but be aware that if the client deletes and re-adds the gallery, it
will revert to the default ID name.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;After selecting the gallery, we can set the items to be displayed by providing an array to the &lt;code&gt;.items&lt;/code&gt; property.&lt;/p&gt;
&lt;p&gt;Each item in the array is an object containing either image or video information. In this example we&apos;re only providing images.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; It&apos;s not listed in the docs, but you can provide an &lt;code&gt;alt&lt;/code&gt; property
and it will be used as the image&apos;s alt tag — make sure to add this for
accessibility!&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;The nice thing about this API is that there&apos;s barely any code required to insert our images — nearly all of this code is the image data itself.&lt;/p&gt;
&lt;h2&gt;See the custom-coded images in the site&lt;/h2&gt;
&lt;p&gt;To test that the images are loading as expected, go back to the Wix Studio and click the eye icon at the very top right of the UI, next to the &quot;Publish&quot; button.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:585760/lwj/blog/wix-studio-05-gallery-preview.jpg&quot; alt=&quot;preview of the Wix site showing custom-coded images in the
gallery&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;Assuming you used the same images from the code sample above, your gallery will look similar to the one in the screenshot above. (The specific layout will depend on options you selected for the gallery element.)&lt;/p&gt;
&lt;p&gt;With that, you&apos;ve successfully custom coded images into your client&apos;s Wix site. They can change all the settings, move the layout around, and whatever else they want to do, and you as the developer only need to worry about setting this one call to &lt;code&gt;$w()&lt;/code&gt; up.&lt;/p&gt;
&lt;h2&gt;Going beyond hard-coded images&lt;/h2&gt;
&lt;p&gt;You&apos;re not limited to hard-coded image data, either — you can grab images from any API and display them in your Wix gallery elements.&lt;/p&gt;
&lt;p&gt;For example, here&apos;s how you would display a list of &lt;a href=&quot;https://dog.ceo/dog-api/documentation/breed&quot;&gt;corgi images from the Dog API&lt;/a&gt; using the web standard &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API&quot;&gt;Fetch API&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// docs: https://www.wix.com/velo/reference/$w/gallery
$w.onReady(async function () {
  const res = await fetch(&apos;https://dog.ceo/api/breed/corgi/images&apos;);

  if (!res.ok) {
    console.error(&apos;something went wrong loading the images&apos;);
    return;
  }

  const data = await res.json();
  const images = data.message.map((image, i) =&amp;gt; {
    return {
      type: &apos;image&apos;,
      title: `Corgi ${i}`,
      description: &apos;Images from the Dog API&apos;,
      alt: &apos;a corgi&apos;,
      src: image,
      link: &apos;https://dog.ceo/dog-api/documentation/breed&apos;,
    };
  });

  // @ts-expect-error known issue where IFrame type is inferred incorrectly
  $w(&apos;#gallery1&apos;).items = images;
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After saving, preview the site to see the gallery images update.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:585760/lwj/blog/wix-studio-06-images-from-api.jpg&quot; alt=&quot;the image gallery showing photos of
corgis&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;h2&gt;No-code for clients doesn&apos;t mean no-code for devs&lt;/h2&gt;
&lt;p&gt;We only looked at a single workflow in this tutorial, but the fact that we can work in a VS Code-based IDE, with autocomplete and a familiar, selector-based API, is a welcome surprise to devs like me who thought choosing Wix mean it was impossible to make code changes to client sites.&lt;/p&gt;
&lt;p&gt;And, for those devs who never want to touch the design parts of web dev, Wix might actually be a welcome addition to the way they work with clients.&lt;/p&gt;
&lt;h2&gt;Resources &amp;amp; Further Reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.wix.com/studio/development&quot;&gt;Wix Studio&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.wix.com/velo/reference/api-overview&quot;&gt;Velo API Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://dev.wix.com/docs/develop-websites/articles/coding-with-wix-studio/wix-studio-about-the-wix-ide&quot;&gt;Wix IDE&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://dog.ceo/dog-api/&quot;&gt;Dog API&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>SCIM Provisioning in Node Express Using WorkOS and Okta</title><link>https://codetv.dev/blog/scim-directory-sync/</link><guid isPermaLink="true">https://codetv.dev/blog/scim-directory-sync/</guid><description>If you&apos;re building a SaaS app, landing the largest customers means supporting large-scale needs like provisioning user accounts and managing permissions based on their central directory. In this tutorial, you&apos;ll learn how to add SCIM support to your Node-based app using WorkOS.
</description><pubDate>Tue, 10 Oct 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/f_auto,q_auto/lwj/blog/workos-scim.jpg&quot; alt=&quot;SCIM Provisioning in Node Express Using WorkOS and Okta&quot; /&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://youtu.be/cFr7GfOng1o&quot;&gt;Watch on YouTube&lt;/a&gt;&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Want to jump to the end?&lt;/strong&gt; &lt;a href=&quot;https://github.com/learnwithjason/node-express-scim-workos-example&quot;&gt;Check out the source code on
GitHub.&lt;/a&gt;&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;Prerequisites&lt;/h2&gt;
&lt;p&gt;This tutorial extends an app that uses Single Sign-On (SSO) for user auth. If you&apos;re not familiar with SSO or how to implement it, you can see the implementation in the source code or learn how to &lt;a href=&quot;https://www.codetv.dev/blog/workos-sso-okta-idp/&quot;&gt;implement SSO in a Node app in this tutorial&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If you want to build along with this tutorial, you&apos;ll need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Node v20.6.0 or later&lt;/li&gt;
&lt;li&gt;PostgreSQL available in your development environment (I used v14.9)&lt;/li&gt;
&lt;li&gt;A &lt;a href=&quot;https://lwj.dev/workos&quot;&gt;WorkOS account&lt;/a&gt; (you can sign up without a credit card and build in dev mode for free)&lt;/li&gt;
&lt;li&gt;An &lt;a href=&quot;https://developer.okta.com/signup/&quot;&gt;Okta account&lt;/a&gt; to use as your identity provider (a dev account is free while you build)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ngrok.com/&quot;&gt;ngrok&lt;/a&gt; or a similar tool for exposing your dev environment via URL (for webhook testing)&lt;/li&gt;
&lt;/ul&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; this tutorial was sponsored by
&lt;a href=&quot;https://lwj.dev/workos&quot;&gt;WorkOS&lt;/a&gt;. Thanks, friends!&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;Add SCIM support to a Node Express app&lt;/h2&gt;
&lt;p&gt;WorkOS calls this &lt;a href=&quot;https://workos.com/docs/directory-sync&quot;&gt;Directory Sync&lt;/a&gt;. Quote:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;System for Cross-domain Identity Management (or SCIM) is an open standard for managing automated user and group provisioning.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Set up your development environment&lt;/h2&gt;
&lt;h3&gt;Clone the repo&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# clone the repo on the start branch (using the GitHub CLI: https://cli.github.com)
gh repo clone learnwithjason/node-express-scim-workos-example -- -b start

# move into it and install dependencies
cd node-express-scim-workos-example/
npm i
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Get environment variables&lt;/h3&gt;
&lt;p&gt;For WorkOS SSO to work, you&apos;ll need to have your identity provider linked to your WorkOS account for single sign-on. If you&apos;re not sure how to do this, I have a &lt;a href=&quot;https://www.codetv.dev/blog/workos-sso-okta-idp/&quot;&gt;Node SSO with WorkOS tutorial&lt;/a&gt; that walks though how to set up SSO using Okta as your identity provider.&lt;/p&gt;
&lt;p&gt;This app already has the SSO flow set up, so all you&apos;ll need are your WorkOS credentials and redirect URI.&lt;/p&gt;
&lt;p&gt;Rename &lt;code&gt;.env.EXAMPLE&lt;/code&gt; to &lt;code&gt;.env&lt;/code&gt;, then update the following values:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;+	WORKOS_API_KEY=&quot;sk_test_...&quot;
+	WORKOS_CLIENT_ID=&quot;client_...&quot;
+	WORKOS_REDIRECT_URI=&quot;http://localhost:3000/auth/callback&quot;
+	WORKOS_ORG_ID=&quot;org_...&quot;
	WORKOS_DIRECTORY_ID=&quot;&quot;
	WORKOS_WEBHOOK_SECRET=&quot;&quot;

+	SESSION_SECRET=&quot;secret sauce&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;WORKOS_API_KEY&lt;/code&gt; is found on your &lt;a href=&quot;https://dashboard.workos.com&quot;&gt;WorkOS Dashboard&lt;/a&gt; under the API keys section&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WORKOS_CLIENT_ID&lt;/code&gt; is found on the Configuration section of your dashboard&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WORKOS_REDIRECT_URI&lt;/code&gt; is also found in the Configuration section
&lt;ul&gt;
&lt;li&gt;Note that you can make this any path you prefer, but if you change it from the value shown above you&apos;ll need to update the route in &lt;code&gt;src/routes/auth.js&lt;/code&gt; to match&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WORKOS_ORG_ID&lt;/code&gt; can be found in the Organizations tab at the top of the org you set up for SSO&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SESSION_SECRET&lt;/code&gt; is any string value — this is used by the session middleware to protect user sessions from tampering&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We&apos;ll add both &lt;code&gt;WORKOS_DIRECTORY_ID&lt;/code&gt; and &lt;code&gt;WORKOS_WEBHOOK_SECRET&lt;/code&gt; as part of this tutorial, so leave them blank for now.&lt;/p&gt;
&lt;h3&gt;Start the app locally&lt;/h3&gt;
&lt;p&gt;With the environment variables updated, start the app:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; This app takes advantage of the &lt;a href=&quot;https://nodejs.org/api/cli.html#--env-fileconfig&quot;&gt;&lt;code&gt;--env-file&lt;/code&gt;
flag&lt;/a&gt;, which was added in
v20.6.0. If you get an error when starting the app, make sure you&apos;re on Node
&amp;gt;= v20.6.0! (Or, if you prefer, add the &lt;code&gt;dotenv&lt;/code&gt; package and set it up at the
top of &lt;code&gt;src/index.js&lt;/code&gt; and remove the flag to support older versions of Node.)&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;The app will start, and you&apos;ll be able to access it at &lt;code&gt;http://localhost:3000&lt;/code&gt;. Open it in your browser and log in via SSO.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:a8fffb/lwj/blog/workos-scim-01-app-dashboard.jpg&quot; alt=&quot;the dashboard of the local
app&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;h2&gt;Add Directory Sync using SCIM&lt;/h2&gt;
&lt;h3&gt;Add a new directory in WorkOS&lt;/h3&gt;
&lt;p&gt;In your WorkOS dashboard, find the organization you want to set up Directory Sync for, then open the Actions dropdown and choose &quot;Add Directory&quot;.&lt;/p&gt;
&lt;p&gt;Choose Okta as your provider, then choose a display name (I chose &quot;Okta&quot;).&lt;/p&gt;
&lt;p&gt;On the next screen, the &quot;Directory Details&quot; panel will show you the custom endpoint and bearer token that you&apos;ll need to configure Okta.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:a8fffb/lwj/blog/workos-scim-01.1-directory-set-up.jpg&quot; alt=&quot;WorkOS directory configuration page for
Okta&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;Grab the &quot;Directory ID&quot; from the top of this page — this is what you&apos;ll need in the &lt;code&gt;.env.&lt;/code&gt; as the value of &lt;code&gt;WORKOS_DIRECTORY_ID&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	WORKOS_API_KEY=&quot;sk_test_...&quot;
	WORKOS_CLIENT_ID=&quot;client_...&quot;
	WORKOS_REDIRECT_URI=&quot;http://localhost:3000/auth/callback&quot;
	WORKOS_ORG_ID=&quot;org_...&quot;
+	WORKOS_DIRECTORY_ID=&quot;directory_...&quot;
	WORKOS_WEBHOOK_SECRET=&quot;&quot;

	SESSION_SECRET=&quot;secret sauce&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Enable SCIM provisioning in Okta&lt;/h3&gt;
&lt;p&gt;Next, open your Okta dashboard and choose the application you&apos;ve configured for SSO. (Or, if you prefer, create a new application.)&lt;/p&gt;
&lt;p&gt;On the General tab of your app&apos;s home page, click the &quot;Edit&quot; option in App Settings, then check the box to &quot;Enable SCIM provisioning&quot;.&lt;/p&gt;
&lt;h3&gt;Configure SCIM provisioning in Okta&lt;/h3&gt;
&lt;p&gt;The app will now have a Provisioning tab. Click that, then click the Edit option for the SCIM connection. And set the following options:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;For the &quot;SCIM connector base URL&quot;, use the endpoint from the WorkOS directory details from earlier&lt;/li&gt;
&lt;li&gt;For &quot;Unique identifier field for users&quot;, use &quot;email&quot;&lt;/li&gt;
&lt;li&gt;Check the boxes for &quot;Push New Users&quot;, &quot;Push Profile Updates&quot;, and &quot;Push Groups&quot;&lt;/li&gt;
&lt;li&gt;Change the &quot;Authentication Mode&quot; to &quot;HTTP Header&quot;&lt;/li&gt;
&lt;li&gt;Add the Bearer Token from the WorkOS directory details to the &quot;Authorization&quot; field&lt;/li&gt;
&lt;/ul&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:a8fffb/lwj/blog/workos-scim-02-scim-connection.jpg&quot; alt=&quot;Okta configuration for
SCIM&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;On the next page, click the Edit option for &quot;Provisioning to App&quot; and check the boxes for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Create Users&lt;/li&gt;
&lt;li&gt;Update User Attributes&lt;/li&gt;
&lt;li&gt;Deactivate Users&lt;/li&gt;
&lt;/ul&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:a8fffb/lwj/blog/workos-scim-03-provisioning-to-app.jpg&quot; alt=&quot;Okta configuration for what should be
synced&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;h3&gt;Assign the app to people in your company&lt;/h3&gt;
&lt;p&gt;Next, navigate to your Okta app&apos;s Assignments tab. Open the &quot;Assign&quot; dropdown, then choose &quot;Assign to Groups&quot; (you can also manually assign to each person if you prefer). Since my Okta account is a test instance, I assigned the app to everyone, but you can do whatever makes the most sense for your testing.&lt;/p&gt;
&lt;p&gt;For this app specifically, create a new group called &quot;Authors&quot; and assign at least one person to that group for testing. Assign this group to the app as well.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:a8fffb/lwj/blog/workos-scim-04-assign-groups.jpg&quot; alt=&quot;assigning groups in
Okta&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;h3&gt;Push groups to WorkOS&lt;/h3&gt;
&lt;p&gt;With the groups assigned, it&apos;s time to try things out. Go to the &quot;Push Groups&quot; tab in your Okta app&apos;s dashboard, then open the &quot;Push Groups&quot; dropdown and choose &quot;Find groups by name&quot;. Find &quot;Authors&quot; in the dropdown, ensure the &quot;Push group memberships immediately&quot; box is checked, and click save.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:a8fffb/lwj/blog/workos-scim-05-push-groups.jpg&quot; alt=&quot;pushing the Authors group to WorkOS from
Okta&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;Once the push status changes to &quot;Active&quot;, check your WorkOS directory and you&apos;ll see that the group and its users have been synced to WorkOS. That means SCIM is set up properly and Directory Sync is working!&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:a8fffb/lwj/blog/workos-scim-06-group-synced.jpg&quot; alt=&quot;the group in WorkOS after being pushed from
Okta&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;Any changes in your Okta directory will now update WorkOS as well. In the next section we&apos;ll set up a webhook so our app is also updated to stay in sync with user permissions.&lt;/p&gt;
&lt;h2&gt;Set up a webhook to sync directory changes to a Node app&lt;/h2&gt;
&lt;p&gt;Now that Okta events are syncing to WorkOS, we need to ensure that our app updates whenever users or groups change.&lt;/p&gt;
&lt;p&gt;In our app, we rely on WorkOS for user login, so if a user&apos;s account is deactivated, we need our app to make sure they&apos;re logged out immediately (i.e. their next request will bounce them out to the login screen).&lt;/p&gt;
&lt;p&gt;We&apos;re also going to rely on groups for permissions in the app (i.e. authors can create and delete their own posts, but not delete the posts of others, but admins can delete anyone&apos;s posts). This means that we need to immediately sync group changes so that a user&apos;s membership in a permission group is up-to-date on every request.&lt;/p&gt;
&lt;p&gt;To do that, we need to set up a webhook endpoint in our app, then tell WorkOS to send all directory sync events to that endpoint.&lt;/p&gt;
&lt;h3&gt;Set up a webhook endpoint in the Node app&lt;/h3&gt;
&lt;p&gt;In the example app, a stubbed out endpoint already exists at &lt;code&gt;/api/directory-sync&lt;/code&gt; (defined in &lt;code&gt;src/routes/api.js&lt;/code&gt;). This is good enough for testing, so our only task at this point is to make sure it&apos;s accessible to WorkOS for testing.&lt;/p&gt;
&lt;p&gt;To do that, we&apos;ll use &lt;a href=&quot;https://ngrok.com/&quot;&gt;ngrok&lt;/a&gt; in this tutorial. This avoids the need to deploy the app to the web for testing webhooks by allowing us to expose our own localhost as a public URL for as long as we keep the ngrok command running.&lt;/p&gt;
&lt;p&gt;With your app still running, open a new terminal and run:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ngrok http 3000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will give you a &quot;Forwarding&quot; URL that looks similar to this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://56cd-2603-3004-6e3-8100-7429-8493-1862-fbb8.ngrok-free.app
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you visit the URL, you&apos;ll get a notice from ngrok, and if you click &quot;Visit Site&quot; you&apos;ll see your local app.&lt;/p&gt;
&lt;p&gt;Copy this URL and keep both your dev command and ngrok running — this is the URL we&apos;ll use to test the webhook.&lt;/p&gt;
&lt;h3&gt;Register a webhook with WorkOS&lt;/h3&gt;
&lt;p&gt;In the WorkOS dashboard, click the Webhooks section, then create a new webhook. In the &quot;Endpoint URL&quot; field, add your ngrok URL with the path &lt;code&gt;/api/directory-sync&lt;/code&gt; appended. It should look similar to this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://56cd-2603-3004-6e3-8100-7429-8493-1862-fbb8.ngrok-free.app/api/directory-sync
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Check the boxes for &quot;Directory Events&quot;, &quot;Directory Group Events&quot;, and &quot;Directory User Events&quot; (this will select all the boxes below each of these event categories), then save.&lt;/p&gt;
&lt;h3&gt;Store the webhook secret as an environment variable&lt;/h3&gt;
&lt;p&gt;After creating the webhook, copy the Signing Secret value from the top of the new webhook&apos;s dashboard and store it in &lt;code&gt;.env&lt;/code&gt; as &lt;code&gt;WORKOS_WEBHOOK_SECRET&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	WORKOS_API_KEY=&quot;sk_test_...&quot;
	WORKOS_CLIENT_ID=&quot;client_...&quot;
	WORKOS_REDIRECT_URI=&quot;http://localhost:3000/auth/callback&quot;
	WORKOS_ORG_ID=&quot;org_...&quot;
	WORKOS_DIRECTORY_ID=&quot;directory_...&quot;
+	WORKOS_WEBHOOK_SECRET=&quot;abc...&quot;

	SESSION_SECRET=&quot;secret sauce&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Validate that webhooks are sending&lt;/h3&gt;
&lt;p&gt;To make sure your webhook events are sending, make a change in your Okta directory: add a user to a group, create a new group, or otherwise modify your users and groups in a way that will trigger a sync with WorkOS.&lt;/p&gt;
&lt;p&gt;Once you&apos;ve made a change, the console of your dev process will show the incoming webhook. The data will look something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  id: &apos;event_01HBF492M21X1QYD1TKCQK7GSE&apos;,
  data: {
    id: &apos;directory_group_01HBF491X5ECAHXQ0J13KXGEH6&apos;,
    name: &apos;Authors&apos;,
    idp_id: &apos;Authors&apos;,
    object: &apos;directory_group&apos;,
    created_at: &apos;2023-09-29T00:09:07.748Z&apos;,
    updated_at: &apos;2023-09-29T00:09:08.479Z&apos;,
    directory_id: &apos;directory_01HBF1A4XRKNJDVSX1NJW5TGJ5&apos;,
    raw_attributes: {},
    organization_id: &apos;org_01HBF10W8ZVRY2VG96HZ6KHX72&apos;,
    previous_attributes: {}
  },
  event: &apos;dsync.group.updated&apos;,
  created_at: &apos;2023-09-29T00:09:08.482Z&apos;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Update the app in response to webhook events&lt;/h2&gt;
&lt;p&gt;Now that the app is receiving webhook events, we need to write the code to update our app in response to those events.&lt;/p&gt;
&lt;h3&gt;Validate that incoming webhook requests are valid&lt;/h3&gt;
&lt;p&gt;To ensure that only requests sent from WorkOS&apos;s Directory Sync updates are able to modify our app, we need to start by validating every request that&apos;s made to our endpoint.&lt;/p&gt;
&lt;p&gt;To do this, make the following changes to the &lt;code&gt;/directory-sync&lt;/code&gt; route in &lt;code&gt;src/routes/api.js&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	const Router = require(&apos;express-promise-router&apos;);
+	const { WorkOS } = require(&apos;@workos-inc/node&apos;);
	const db = require(&apos;../db&apos;);

	const router = new Router();
+	const workos = new WorkOS(process.env.WORKOS_API_KEY);

	router.post(&apos;/directory-sync&apos;, async (req, res) =&amp;gt; {
-		console.log(req.body);
-
-		// TODO implement directory sync
+		const payload = req.body;
+		const sigHeader = req.headers[&apos;workos-signature&apos;];
+
+		// validate the event and get the data
+		const webhook = workos.webhooks.constructEvent({
+			payload,
+			sigHeader,
+			secret: process.env.WORKOS_WEBHOOK_SECRET,
+		});
+
		res.send(&apos;ok&apos;);
	});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;WorkOS requests include a &lt;code&gt;workos-signature&lt;/code&gt; header, which is the result of hashing the request payload with the webhook secret. If the signature matches, it&apos;s a valid webhook and the event data is returned from the &lt;code&gt;constructEvent&lt;/code&gt; method.&lt;/p&gt;
&lt;h3&gt;Handle changes in group membership&lt;/h3&gt;
&lt;p&gt;The first group of events our app needs to handle are group membership updates. These are how our app knows which permissions each user has. In this example, we store the group names as part of their user data, but this could also map to roles, permissions, or any other approach you prefer.&lt;/p&gt;
&lt;p&gt;Set up a switch on the &lt;code&gt;event&lt;/code&gt; value of the webhook payload, then handle the &lt;code&gt;dsync.group.user_added&lt;/code&gt; and &lt;code&gt;dsync.group.user_removed&lt;/code&gt; events by adding the following code to the &lt;code&gt;/directory-sync&lt;/code&gt; route in &lt;code&gt;src/routes/api.js&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	router.post(&apos;/directory-sync&apos;, async (req, res) =&amp;gt; {
		const payload = req.body;
		const sigHeader = req.headers[&apos;workos-signature&apos;];

		// validate the event and get the data
		const webhook = workos.webhooks.constructEvent({
			payload,
			sigHeader,
			secret: process.env.WORKOS_WEBHOOK_SECRET,
		});
+
+		switch (webhook.event) {
+			case &apos;dsync.group.user_added&apos;:
+			case &apos;dsync.group.user_removed&apos;:
+				const { user, group } = webhook.data;
+				const roles = await db.getUserRolesByEmail(user.username);
+
+				if (webhook.event === &apos;dsync.group.user_added&apos;) {
+					roles.add(group.name.toUpperCase());
+				} else {
+					roles.delete(group.name.toUpperCase());
+				}
+
+				await db.createOrUpdateUser({
+					email: user.username,
+					firstName: user.firstName,
+					lastName: user.lastName,
+					roles: [...roles.values()],
+					active: user.state === &apos;active&apos;,
+				});
+				break;
+
+			default:
+				console.log(`TODO: handle ${webhook.event} events`);
+		}

		res.send(&apos;ok&apos;);
	});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When a user is added to or removed from a group, this code loads the affected user&apos;s profile, adds or removes the group according to the event type, then updates the user in the app&apos;s database with a new set of roles.&lt;/p&gt;
&lt;h3&gt;Add new users&lt;/h3&gt;
&lt;p&gt;Next, your app needs to create new users when they&apos;re added to the app. Update the &lt;code&gt;/directory-sync&lt;/code&gt; endpoint in &lt;code&gt;src/routes/api.js&lt;/code&gt; with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	router.post(&apos;/directory-sync&apos;, async (req, res) =&amp;gt; {
		const payload = req.body;
		const sigHeader = req.headers[&apos;workos-signature&apos;];

		// validate the event and get the data
		const webhook = workos.webhooks.constructEvent({
			payload,
			sigHeader,
			secret: process.env.WORKOS_WEBHOOK_SECRET,
		});

		switch (webhook.event) {
			case &apos;dsync.group.user_added&apos;:
			case &apos;dsync.group.user_removed&apos;:
				const { user, group } = webhook.data;
				const roles = await db.getUserRolesByEmail(user.username);

				if (webhook.event === &apos;dsync.group.user_added&apos;) {
					roles.add(group.name.toUpperCase());
				} else {
					roles.delete(group.name.toUpperCase());
				}

				await db.createOrUpdateUser({
					email: user.username,
					firstName: user.firstName,
					lastName: user.lastName,
					roles: [...roles.values()],
					active: user.state === &apos;active&apos;,
				});
				break;
+
+			case &apos;dsync.user.created&apos;:
+				await db.createOrUpdateUser({
+					email: webhook.data.username,
+					firstName: webhook.data.firstName,
+					lastName: webhook.data.lastName,
+					roles: [],
+					active: webhook.data.state === &apos;active&apos;,
+				});
+				break;

			default:
				console.log(`TODO: handle ${webhook.event} events`);
		}

		res.send(&apos;ok&apos;);
	});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To do this, we directly insert the necessary details into our user database.&lt;/p&gt;
&lt;h3&gt;Deactivate deleted users&lt;/h3&gt;
&lt;p&gt;Finally, if a user&apos;s account is disabled — whether that means it&apos;s set to inactive or suspended, or deleted altogether — we need to make sure that user is immediately logged out. The SSO auth flow will already make sure they&apos;re unable to log back in again.&lt;/p&gt;
&lt;p&gt;To do this, make one last set of changes in the &lt;code&gt;/directory-sync&lt;/code&gt; endpoint in &lt;code&gt;src/routes/api.js&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	router.post(&apos;/directory-sync&apos;, async (req, res) =&amp;gt; {
		const payload = req.body;
		const sigHeader = req.headers[&apos;workos-signature&apos;];

		// validate the event and get the data
		const webhook = workos.webhooks.constructEvent({
			payload,
			sigHeader,
			secret: process.env.WORKOS_WEBHOOK_SECRET,
		});

		switch (webhook.event) {
			case &apos;dsync.group.user_added&apos;:
			case &apos;dsync.group.user_removed&apos;:
				const { user, group } = webhook.data;
				const roles = await db.getUserRolesByEmail(user.username);

				if (webhook.event === &apos;dsync.group.user_added&apos;) {
					roles.add(group.name.toUpperCase());
				} else {
					roles.delete(group.name.toUpperCase());
				}

				await db.createOrUpdateUser({
					email: user.username,
					firstName: user.firstName,
					lastName: user.lastName,
					roles: [...roles.values()],
					active: user.state === &apos;active&apos;,
				});
				break;

			case &apos;dsync.user.created&apos;:
				await db.createOrUpdateUser({
					email: webhook.data.username,
					firstName: webhook.data.firstName,
					lastName: webhook.data.lastName,
					roles: [],
					active: webhook.data.state === &apos;active&apos;,
				});
				break;
+
+			case &apos;dsync.user.updated&apos;:
+				if (
+					webhook.data.state === &apos;inactive&apos; ||
+					webhook.data.state === &apos;suspended&apos;
+				) {
+					await db.deactivateUserByEmail(webhook.data.username);
+				}
+				break;
+
+			case &apos;dsync.user.deleted&apos;:
+				await db.deactivateUserByEmail(webhook.data.username);
+				break;

			default:
				console.log(`TODO: handle ${webhook.event} events`);
		}

		res.send(&apos;ok&apos;);
	});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This code listens for both updated and deleted users, and deactivates the user&apos;s account if any of the conditions are met. Once deactivated, any active sessions are destroyed for that user, meaning they&apos;ll be logged out on their next request.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; The reason users are &lt;em&gt;deactivated&lt;/em&gt; and not &lt;em&gt;deleted&lt;/em&gt; is to preserve
a record of authorship on posts. In a production app, an option to permanently
delete a user and all their posts will be necessary to meet some regulatory
compliance audits.&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;Test the sync&lt;/h2&gt;
&lt;p&gt;To see this in action, try any of the following actions:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create a group called &quot;Admins&quot; and add a user to it. Note that users in the Admins group will have a &quot;delete&quot; option for all posts, not just those they created.&lt;/li&gt;
&lt;li&gt;Change a user&apos;s groups and watch the permissions update on each page refresh.&lt;/li&gt;
&lt;li&gt;Remove a user from the app while logged in as that user. Note that a removed user is immediately moved back to the home page (logged out) on their next request.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The tutorial app is stripped down for the sake of clarity. In a production app, you can do much more with Directory Sync to keep your app synced with the customer&apos;s team as it grows and changes.&lt;/p&gt;
&lt;h2&gt;Bonus tip: use Directory Sync to expand your adoption&lt;/h2&gt;
&lt;p&gt;Many SaaS apps today charge based on user seats, so another interesting way to use SCIM and Directory Sync is to build an &quot;invite your teammates&quot; flow into the app.&lt;/p&gt;
&lt;p&gt;This is a win-win, because for your customer, their team has a fast path to find and invite the people they need to collaborate with, and for your company, there&apos;s an organic growth loop where each employee of your customer that starts using your app is another opportunity to add more users as they bring their team along.&lt;/p&gt;
&lt;p&gt;To see the simplest implementation, add the following code to &lt;code&gt;src/routes/dashboard.js&lt;/code&gt; at the top of the file and in the &lt;code&gt;/team&lt;/code&gt; route:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	const Router = require(&apos;express-promise-router&apos;);
+	const { WorkOS } = require(&apos;@workos-inc/node&apos;);
	const db = require(&apos;../db&apos;);

	const router = new Router();
+	const workos = new WorkOS(process.env.WORKOS_API_KEY);

	/* unchanged code omitted for brevity */

	router.get(&apos;/team&apos;, async (req, res) =&amp;gt; {
-		const teammates = [];
+		const users = await workos.directorySync.listUsers({
+			directory: process.env.WORKOS_DIRECTORY_ID,
+		});
+
+		const teammates = users.list.data
+			.filter((user) =&amp;gt; {
+				return !user.emails.some((e) =&amp;gt; e.value === req.session.user.email);
+			})
+			.map(({ id, emails, firstName, lastName, groups }) =&amp;gt; {
+				console.log(groups);
+				return {
+					firstName,
+					lastName,
+					email: emails.at(0).value,
+					groups: groups.map((g) =&amp;gt; g.name).join(&apos;, &apos;),
+					inviteLink: `/api/invite/${id}`,
+				};
+			});

		res.render(&apos;dashboard/team&apos;, { teammates, user: req.session.user });
	});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This code pulls a list of all the users that &lt;em&gt;could&lt;/em&gt; create an account in your app and shows them to the logged-in user with an invite link.&lt;/p&gt;
&lt;p&gt;Save, then visit &lt;code&gt;http://localhost:3000/dashboard/team&lt;/code&gt; to see the teammates listed.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:a8fffb/lwj/blog/workos-scim-07-invite-flow.jpg&quot; alt=&quot;teammates displayed in the app, pulled from the synced
directory&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;To take this further, you could get only the employees that are on the current user&apos;s team or who are in their department, depending on the metadata supplied by the customer.&lt;/p&gt;
&lt;h2&gt;SCIM means your biggest customers feel comfortable signing bigger contracts&lt;/h2&gt;
&lt;p&gt;From admin overhead to security risks, third-party SaaS products cause a lot of stress for large companies&apos; IT teams. By adding support for SCIM, your SaaS product leverages the customer&apos;s already-vetted directory and user management software, so the customer doesn&apos;t have to manually provision user accounts or worry that a terminated employee will still have access to things for a period after being removed from the central system.&lt;/p&gt;
&lt;p&gt;As the size of the companies you&apos;re pursuing for deals increases, these types of concerns become more and more of a blocker to finalizing a signed contract. Adding SCIM support alongside your SSO gives even the largest potential customer confidence that your app can handle their scale and pass their security and compliance audits.&lt;/p&gt;
&lt;h2&gt;Resources and further reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/learnwithjason/node-express-scim-workos-example&quot;&gt;Source code&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://workos.com/docs/directory-sync?utm_source=learnwithjason&amp;amp;utm_medium=tutorial-scim&amp;amp;utm_campaign=six-figure-saas&quot;&gt;WorkOS Directory Sync&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.okta.com/signup/&quot;&gt;Okta Developer Account&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.codetv.dev/blog/workos-sso-okta-idp/&quot;&gt;Add SSO to a Node app&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Add notifications to a Node app (app inbox, SMS, and email)</title><link>https://codetv.dev/blog/notifications-in-app-sms-email-courier/</link><guid isPermaLink="true">https://codetv.dev/blog/notifications-in-app-sms-email-courier/</guid><description>Notifications can be a huge value add to your app users, but if you get them wrong, they&apos;re hugely annoying. Learn how to add smart app notifications in this tutorial.
</description><pubDate>Tue, 19 Sep 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/f_auto,q_auto/lwj/blog/courier-notifications.jpg&quot; alt=&quot;Add notifications to a Node app (app inbox, SMS, and email)&quot; /&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://youtu.be/5qZCaK6s7Kw&quot;&gt;Watch on YouTube&lt;/a&gt;&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Want to jump to the end?&lt;/strong&gt; &lt;a href=&quot;https://github.com/learnwithjason/courier-notifications&quot;&gt;Check out the source code on
GitHub.&lt;/a&gt;&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;Configure Courier for your app&lt;/h2&gt;
&lt;p&gt;First, log in or create an account at &lt;a href=&quot;https://www.courier.com/&quot;&gt;courier.com&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;On the dashboard, at the bottom of the left-hand sidebar, there&apos;s a toggle that lets you choose either test or production data. For this tutorial, we&apos;ll work with test data.&lt;/p&gt;
&lt;p&gt;Our first step is to add the notification channels we want our app to support. &lt;a href=&quot;https://www.courier.com/integrations/&quot;&gt;Courier has built-in integrations&lt;/a&gt; with dozens of tools, plus ways to add your own with webhooks and SDKs.&lt;/p&gt;
&lt;p&gt;This tutorial will set up in-app notifications, email, and SMS.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;Thanks to Courier for making this tutorial possible. &lt;a href=&quot;https://lwj.dev/courier&quot;&gt;Create a free
account&lt;/a&gt; today and add notifications to your apps!&lt;/p&gt;&lt;/aside&gt;
&lt;h3&gt;Add support for in-app notifications&lt;/h3&gt;
&lt;p&gt;A notification inbox in an app is a great way to unobtrusively let users know what&apos;s happening. Courier provides everything you need to add one, including React components (which we&apos;ll look at later in this tutorial).&lt;/p&gt;
&lt;p&gt;To set it up, go to &lt;a href=&quot;https://app.courier.com/channels&quot;&gt;your channels&lt;/a&gt; and choose Courier under the Inbox section. On the next screen, scroll all the way down and click &quot;install provider&quot;.&lt;/p&gt;
&lt;p&gt;Head back to your channels and it&apos;ll show up under your configured providers as well as in your routing settings.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:211e2abf/lwj/blog/courier-notifications-01-channel-courier.jpg&quot; alt=&quot;The Courier channels dashboard in test
mode.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;h3&gt;Add support for SMS and email&lt;/h3&gt;
&lt;p&gt;To send notifications to users who aren&apos;t actively viewing your app dashboard, you&apos;ll need additional channels.&lt;/p&gt;
&lt;p&gt;First, add email by choosing Gmail from the Email providers and authorizing with any of your Gmail accounts. (If you don&apos;t have one, you can skip this or use a different provider.)&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Important!&lt;/strong&gt; Make sure to authorize the email for the test environment as
well!&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;Next, add SMS using Twilio. You&apos;ll need your account SID and auth token, which are available at &lt;a href=&quot;https://console.twilio.com&quot;&gt;on your Twilio console home page&lt;/a&gt;, and any active Twilio phone number on your account.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Important!&lt;/strong&gt; Again, make sure to add the details to the test configuration
so we can use our test keys.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;Once this is done, both email and SMS will be available under configured providers.&lt;/p&gt;
&lt;h3&gt;Add channels to your default routing&lt;/h3&gt;
&lt;p&gt;Courier allows you to choose whether a given channel should &lt;em&gt;always&lt;/em&gt; be notified, or if only the best available option out of a set of channels should be used.&lt;/p&gt;
&lt;p&gt;This is a great way to allow you to reach your users without being too overbearing about it. No one likes to get the same notification duplicated across every account and device they own.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:211e2abf/lwj/blog/courier-notifications-02-configured-channels.jpg&quot; alt=&quot;The Courier channels screen showing routing set up with Courier in the
&amp;quot;always send to&amp;quot; group and Twilio and Gmail in the &amp;quot;send to the best of&amp;quot;
group.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;How this works in practice is that Courier will &lt;em&gt;always&lt;/em&gt; deliver a notification to the in-app inbox, and will choose &lt;em&gt;one&lt;/em&gt; of either SMS or email, using SMS first if both are available. These are only defaults, though — you can also allow your users to choose which platforms they want to get notified on, and if they want an alert on every device and account they have, you can make that happen!&lt;/p&gt;
&lt;h2&gt;Set up the app for local development&lt;/h2&gt;
&lt;p&gt;For this tutorial, the app we&apos;re working with is a React-powered app dashboard. The dashboard itself doesn&apos;t do anything — it&apos;s only there so we have a plausible app to work with.&lt;/p&gt;
&lt;p&gt;Under the hood, this demo uses a couple things to make our lives easier with setup:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Notifications themselves are managed by Courier. We&apos;ll spend most of this tutorial on how to configure, display, and send notifications in a React app.&lt;/li&gt;
&lt;li&gt;User auth is handled by &lt;a href=&quot;https://clerk.com/&quot;&gt;Clerk&lt;/a&gt;. Having a logged-in user is important because it means we have a unique ID for each person viewing the dashboard, which allows Courier to route notifications to people properly.&lt;/li&gt;
&lt;li&gt;This app uses a webhook to sync users between Clerk and Courier. The webhook will be built on &lt;a href=&quot;https://www.netlify.com/products/functions/&quot;&gt;Netlify Functions&lt;/a&gt; both so we don&apos;t have to think about how to deploy a server and also because we get a way to expose our local dev environment as a live URL for testing.&lt;/li&gt;
&lt;li&gt;To send notifications, we&apos;ll use a Netlify Function to keep secret credentials secure.&lt;/li&gt;
&lt;/ol&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Heads up!&lt;/strong&gt; The steps to display notifications in a React app will be the
same for any React codebase. If you already have an auth solution or don&apos;t
care about the auth part, you can skip the demo app and add this directly to
any React app you have handy.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;To grab &lt;a href=&quot;https://github.com/learnwithjason/courier-notifications/tree/start&quot;&gt;the tutorial repo&lt;/a&gt; at the starting point, clone the &lt;code&gt;start&lt;/code&gt; branch:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# clone the start branch of the repo using the GitHub CLI (https://cli.github.com)
gh repo clone learnwithjason/courier-notifications -- -b start

# move into the folder
cd courier-notifications/

# install dependencies
npm i
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Get Clerk credentials&lt;/h3&gt;
&lt;p&gt;To use the auth, you&apos;ll need a free Clerk account. This will take less than five minutes to set up.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Log in or create an account at &lt;a href=&quot;https://clerk.com&quot;&gt;clerk.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Create a new app and choose any auth provider that you want to use to log in with — this tutorial uses GitHub&lt;/li&gt;
&lt;li&gt;Go to API Keys in the left-hand nav&lt;/li&gt;
&lt;li&gt;Copy your publishable key and your secret key&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;In your code, rename &lt;code&gt;.env.EXAMPLE&lt;/code&gt; to &lt;code&gt;.env&lt;/code&gt; and add your publishable key as &lt;code&gt;VITE_CLERK_PUBLISHABLE_KEY&lt;/code&gt; and your secret key as &lt;code&gt;CLERK_SECRET_KEY&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Start the dev server&lt;/h3&gt;
&lt;p&gt;With the credentials saved, you&apos;re able to start the app and log in.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# start the dev server
npm run dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Open &lt;code&gt;http://localhost:5173&lt;/code&gt; in your browser and you&apos;ll be redirected to the login page&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:211e2abf/lwj/blog/courier-notifications-03-app-login.jpg&quot; alt=&quot;The Clerk login screen showing GitHub as the login
option.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;Use your GitHub account to log in and you&apos;ll see the dashboard.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:211e2abf/lwj/blog/courier-notifications-04-app-logged-in.jpg&quot; alt=&quot;The example app dashboard, including the logged in user’s avatar shown at
the top
right.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;Now that we&apos;re able to log in, we want to add a notifications inbox up in the right side of the header next to our user info.&lt;/p&gt;
&lt;h2&gt;Display notifications in the app&lt;/h2&gt;
&lt;p&gt;To add a notifications inbox to our app, we&apos;ll take advantage of Courier&apos;s ready-made React components.&lt;/p&gt;
&lt;h3&gt;Get your Courier credentials&lt;/h3&gt;
&lt;p&gt;First, we need our client key from Courier. To find this, head to &lt;a href=&quot;https://app.courier.com/channels/courier&quot;&gt;the settings for your Courier inbox channel&lt;/a&gt;, scroll down to the &quot;Test Configuration&quot; section, and copy the &quot;Client Key (Public)&quot;. Add it to your &lt;code&gt;.env&lt;/code&gt; file as the value of &lt;code&gt;VITE_COURIER_CLIENT_KEY&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Install Courier npm packages&lt;/h3&gt;
&lt;p&gt;The demo repo already has dependencies installed. For posterity, you can install everything you need to run Courier notifications in a React app by installing these npm packages:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm i @trycourier/courier @trycourier/react-inbox @trycourier/react-provider
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Create a Notifications component&lt;/h3&gt;
&lt;p&gt;In your code, create a new file at &lt;code&gt;src/components/notifications.tsx&lt;/code&gt; and add the following code inside:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { CourierProvider } from &apos;@trycourier/react-provider&apos;;
import { Inbox } from &apos;@trycourier/react-inbox&apos;;
import { useUser } from &apos;@clerk/clerk-react&apos;;

export function Notifications() {
  const { user } = useUser();

  console.log(user?.id);

  return (
    &amp;lt;CourierProvider
      userId={user?.id}
      clientKey={import.meta.env.VITE_COURIER_CLIENT_KEY}
    &amp;gt;
      &amp;lt;div className=&quot;notifications-wrapper&quot;&amp;gt;
        &amp;lt;Inbox /&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/CourierProvider&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This component places the &lt;code&gt;Inbox&lt;/code&gt; component inside a &lt;code&gt;CourierProvider&lt;/code&gt; that receives the client key from Courier and the user ID from Clerk.&lt;/p&gt;
&lt;p&gt;We also log the user ID so it&apos;s easy to find for testing.&lt;/p&gt;
&lt;p&gt;To put this into our app, add the &lt;code&gt;Notifications&lt;/code&gt; component to &lt;code&gt;src/components/dashboard.tsx&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	import {
		RedirectToSignIn,
		SignedIn,
		SignedOut,
		UserButton,
	} from &apos;@clerk/clerk-react&apos;;
+	import { Notifications } from &apos;./notifications&apos;;

	import styles from &apos;./dashboard.module.css&apos;;

	export const Dashboard = () =&amp;gt; {
		return (
			&amp;lt;&amp;gt;
				&amp;lt;header className={styles.header}&amp;gt;
					&amp;lt;a className={styles.homeLink} href=&quot;/&quot; rel=&quot;home&quot;&amp;gt;
						Toofshine
					&amp;lt;/a&amp;gt;

					&amp;lt;nav className={styles.nav}&amp;gt;
						&amp;lt;SignedIn&amp;gt;
+							&amp;lt;Notifications /&amp;gt;
							&amp;lt;UserButton
	// ...unchanged below this point...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Save and look at your app dashboard. You&apos;ll see the inbox at the top right and the user ID logged in the console.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:211e2abf/lwj/blog/courier-notifications-05-inbox-added.jpg&quot; alt=&quot;The example app with the notifications icon showing in the header and the
empty inbox
displayed.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;h2&gt;Send a notification manually&lt;/h2&gt;
&lt;p&gt;To send a notification manually, copy the user ID and head to the &lt;a href=&quot;https://app.courier.com/users&quot;&gt;Courier users page&lt;/a&gt;. (As always, make sure you&apos;re still in test mode.) Create a new user — all you&apos;ll need is the ID, which should match the ID from Clerk.&lt;/p&gt;
&lt;p&gt;Save this, then click the &quot;Home&quot; option, then the &quot;send a message&quot; button on the home page.&lt;/p&gt;
&lt;p&gt;You&apos;ll see a form. On the right-hand side of the screen, choose &quot;Push&quot; from the channel dropdown, then Courier from the provider dropdown.&lt;/p&gt;
&lt;p&gt;In the &quot;To&quot; field, add the same user ID, then click &quot;send now&quot;.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:211e2abf/lwj/blog/courier-notifications-06-manual-send.jpg&quot; alt=&quot;The Courier dashboard on the “send a message”
screen.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;Back in the app, you&apos;ll see a new notification in the inbox.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:211e2abf/lwj/blog/courier-notifications-07-first-notification.jpg&quot; alt=&quot;The example app dashboard showing the notification sent manually in the
previous
step.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;h2&gt;Automatically sync users to Courier&lt;/h2&gt;
&lt;p&gt;To make it possible to send notifications without manually creating each user, you&apos;ll need a way to sync users from your user management tool into Courier. In Clerk, this can be done using a webhook that will be called whenever a user is created, updated, or deleted.&lt;/p&gt;
&lt;h3&gt;Get a Courier API key&lt;/h3&gt;
&lt;p&gt;To set this up, go to your &lt;a href=&quot;https://app.courier.com/settings/api-keys&quot;&gt;Courier API keys page&lt;/a&gt; and copy the key with &quot;Published&quot; scope. Set it as the value of &lt;code&gt;COURIER_API_KEY&lt;/code&gt; in &lt;code&gt;.env&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Configure Clerk with a webhook and get the signing secret&lt;/h3&gt;
&lt;p&gt;Next, head to your &lt;a href=&quot;https://dashboard.clerk.com/&quot;&gt;Clerk dashboard&lt;/a&gt;, choose your app, and click the &quot;Webhooks&quot; option in the left-hand nav.&lt;/p&gt;
&lt;p&gt;Inside, set any URL to start (we&apos;ll change this in a later step) and check the &quot;user&quot; box so the webhook is called for all user events.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:211e2abf/lwj/blog/courier-notifications-08-clerk-webhook.jpg&quot; alt=&quot;The Clerk dashboard in the webhook creation flow. All user events are
checked.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;On the next screen, look for the &quot;Signing Secret&quot; toward the bottom of the right-hand column. Copy that value and set it as &lt;code&gt;CLERK_WEBHOOK_SECRET&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Set up a live URL for testing local dev with Netlify Dev&lt;/h3&gt;
&lt;p&gt;To make debugging easier, we&apos;ll be using Netlify Dev, which will set up a live tunnel and allow us to create a public URL that can access our local code while we&apos;re developing. This is extremely helpful for debugging things like webhooks because it removes the need to deploy every change to see if it works.&lt;/p&gt;
&lt;p&gt;In your terminal, install the Netlify CLI if you don&apos;t have it, make sure you&apos;re logged in, and then create a new project.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# if you don&apos;t already have it, install the Netlify CLI globally
npm i -g netlify-cli

# make sure you&apos;re logged in
ntl login

# create a new project
ntl init
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can keep the defaults during initialization.&lt;/p&gt;
&lt;p&gt;Once the project is hooked up to Netlify, run Netlify Dev with the &lt;code&gt;--live&lt;/code&gt; flag to get a public URL:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ntl dev --live dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will create a live URL that looks like &lt;code&gt;https://dev--&amp;lt;your_site_name&amp;gt;.netlify.live&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Back in the Clerk webhook configuration, edit the webhook URL to be your live dev URL from Netlify and add the path &lt;code&gt;/api/user-sync&lt;/code&gt; to the end. We&apos;ll build this endpoint in the next step.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:211e2abf/lwj/blog/courier-notifications-09-updated-webhook.jpg&quot; alt=&quot;The Courier dashboard showing the Netlify Dev live URL as the webhook
endpoint.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;h3&gt;Build the user sync webhook&lt;/h3&gt;
&lt;p&gt;Open &lt;code&gt;netlify/functions/user-sync.ts&lt;/code&gt; in your code and replace the contents with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import type { Handler } from &apos;@netlify/functions&apos;;
import type { WebhookEvent } from &apos;@clerk/clerk-sdk-node&apos;;
import { Webhook } from &apos;svix&apos;;
import { CourierClient } from &apos;@trycourier/courier&apos;;

const courier = CourierClient({
  authorizationToken: process.env.COURIER_API_KEY,
});

type ProfileData = {
  email?: string;
  phone_number?: string;
};

function validateWebhook(req) {
  try {
    const wh = new Webhook(process.env.CLERK_WEBHOOK_SECRET!);
    wh.verify(req.body ?? &apos;&apos;, req.headers as any);
    return true;
  } catch (err) {
    console.log(err);
    return false;
  }
}

function getProfileDetails({ email_addresses, phone_numbers }): ProfileData {
  const profile: ProfileData = {};

  if (email_addresses.length &amp;gt; 0 &amp;amp;&amp;amp; email_addresses[0].email_address) {
    profile.email = email_addresses[0].email_address;
  }

  if (phone_numbers.length &amp;gt; 0 &amp;amp;&amp;amp; phone_numbers[0].phone_number) {
    profile.phone_number = phone_numbers[0].phone_number;
  }

  return profile;
}

export const handler: Handler = async (req) =&amp;gt; {
  if (!validateWebhook(req)) {
    return {
      statusCode: 400,
      body: &apos;Bad Request&apos;,
    };
  }

  const event = JSON.parse(req.body ?? &apos;&apos;) as WebhookEvent;

  switch (event.type) {
    case &apos;user.created&apos;:
    case &apos;user.updated&apos;:
      await courier.replaceProfile({
        recipientId: event.data.id,
        profile: getProfileDetails(event.data),
      });
      break;

    case &apos;user.deleted&apos;:
      if (!event.data.id) {
        break;
      }

      await courier.deleteProfile({
        recipientId: event.data.id,
      });
      break;

    default:
      return {
        statusCode: 400,
        body: &apos;Bad Request&apos;,
      };
  }

  return {
    statusCode: 200,
    body: &apos;OK&apos;,
  };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At the top, we create a new instance of the Courier Node SDK with our API key. This provides methods for us to handle all the Courier-related tasks we need to accomplish.&lt;/p&gt;
&lt;p&gt;Next we define a couple helper functions:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;validateWebhook&lt;/code&gt; uses the signing secret from Clerk to ensure that the webhook is legitimate to prevent someone from impersonating Clerk and messing with user data&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getProfileDetails&lt;/code&gt; pulls the primary email address and phone number out of the Clerk user data and drops everything else since Courier won&apos;t need it&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;In the actual exported handler, the incoming request is validated, and then we set up a switch on the event type.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;When a user is created or updated, we want to sync their user ID, phone number, and email to Courier to make sure they&apos;re getting notifications properly&lt;/li&gt;
&lt;li&gt;When a user is deleted, we want to remove them from Courier entirely&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Finally, we return a 200 status to let Clerk know the webhook was processed successfully.&lt;/p&gt;
&lt;p&gt;To test this, Clerk has a &quot;Testing&quot; tab in the webhook config. Choose &quot;user.created&quot; from the dropdown and then click &quot;Send Example&quot;. You&apos;ll see a successful call in the dev console, and the &lt;a href=&quot;https://app.courier.com/test/users&quot;&gt;Courier users page&lt;/a&gt; will update with the example user.&lt;/p&gt;
&lt;p&gt;Now, whenever a user is created, updated, or deleted, the necessary information required by Courier to send notifications will be automatically synced to Courier.&lt;/p&gt;
&lt;h2&gt;Create a custom audience in Courier&lt;/h2&gt;
&lt;p&gt;Sending notifications to individual users can be done using their IDs, but what if you want to send a notification to every active user — for instance, when a new feature launches?&lt;/p&gt;
&lt;p&gt;In the Courier users page, click the &quot;+ Audience&quot; button, name the audience &quot;Active Members&quot;, and save. The ID will automatically generate as &lt;code&gt;active-members&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Inside, click &quot;+Add Condition&quot; and choose email, then scroll down to &quot;exists&quot; and choose &quot;True&quot;.&lt;/p&gt;
&lt;p&gt;Next, click the &quot;+&quot; at the end of email and choose phone_number, then set &quot;exists&quot; to &quot;True&quot;.&lt;/p&gt;
&lt;p&gt;Use the &quot;Calculate Audience&quot; button to see the users that will be targeted. This should include your user ID from Clerk.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:211e2abf/lwj/blog/courier-notifications-10-custom-audience.jpg&quot; alt=&quot;The Courier dashboard showing the audience details page for the
active-members
audience.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;Now you&apos;re able to send a notification to everyone in this audience, which greatly simplifies the process of sending notifications.&lt;/p&gt;
&lt;h2&gt;Send a notification using the Courier Node SDK&lt;/h2&gt;
&lt;p&gt;Now that users are properly synced and our notification channels are configured, let&apos;s take a look at how the Courier Node SDK allows you to send notifications.&lt;/p&gt;
&lt;p&gt;Open &lt;code&gt;netlify/functions/send-notification.ts&lt;/code&gt; and replace the contents with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import type { Handler } from &apos;@netlify/functions&apos;;
import { CourierClient } from &apos;@trycourier/courier&apos;;

const courier = CourierClient({
  authorizationToken: process.env.COURIER_API_KEY,
});

export const handler: Handler = async (req) =&amp;gt; {
  const { title, body } = JSON.parse(req.body ?? &apos;&apos;);

  if (req.httpMethod !== &apos;POST&apos; || !title || !body) {
    return {
      statusCode: 400,
      body: &apos;Bad Request&apos;,
    };
  }

  const res = await courier.send({
    message: {
      to: {
        audience_id: &apos;active-members&apos;,
      },
      content: {
        title,
        body,
      },
    },
  });

  return {
    statusCode: 200,
    body: JSON.stringify(res),
  };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a Netlify Function that sets up the Courier Node SDK, pulls the &lt;code&gt;title&lt;/code&gt; and &lt;code&gt;body&lt;/code&gt; out of the &lt;code&gt;POST&lt;/code&gt; request, and then uses the &lt;code&gt;send&lt;/code&gt; method of the SDK to deliver the notification to everyone in the &lt;code&gt;active-members&lt;/code&gt; audience.&lt;/p&gt;
&lt;p&gt;It then returns a 200 to let you know the function ran successfully.&lt;/p&gt;
&lt;p&gt;With your Netlify Dev process still running, use a tool like Postman or a cURL command to send a &lt;code&gt;POST&lt;/code&gt; request to the &lt;code&gt;/api/send-notification&lt;/code&gt; endpoint of your live URL.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:211e2abf/lwj/blog/courier-notifications-11-postman-send-notification.jpg&quot; alt=&quot;The Postman app interface showing a notification sent via
API.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;The notification will show up in your in-app inbox.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:211e2abf/lwj/blog/courier-notifications-12-in-app-notification.jpg&quot; alt=&quot;The example app dashboard showing the API-sent notification displayed in the
inbox.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;If you look at the &lt;a href=&quot;https://app.courier.com/test/logs/messages&quot;&gt;Courier logs&lt;/a&gt;, you&apos;ll see that the notification was sent to the inbox &lt;em&gt;and also&lt;/em&gt; to the best of either SMS or email. If you registered using GitHub, it&apos;s likely that you only have an email on your account, so you&apos;ll get an email from Clerk with your notification.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:211e2abf/lwj/blog/courier-notifications-13-log.jpg&quot; alt=&quot;The Courier dashboard showing the logs for the API-sent
notification.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;If you update Clerk to use phone numbers for registration and create a new account, sending notifications will now hit the inbox and SMS for that user.&lt;/p&gt;
&lt;h2&gt;Notifications don&apos;t have to be hard or annoying&lt;/h2&gt;
&lt;p&gt;Courier has made it possible to get notifications running within an app in a few minutes and without much additional code at all, all while making it less complicated to send notifications in a way that won&apos;t overwhelm or annoy your app&apos;s users.&lt;/p&gt;
&lt;p&gt;Thanks again to Courier for making this tutorial possible.&lt;/p&gt;
&lt;p&gt;Go build something cool (with notifications)!&lt;/p&gt;
&lt;h2&gt;Resources and further reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/learnwithjason/courier-notifications&quot;&gt;Review the source code for this app&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://lwj.dev/courier&quot;&gt;Learn more about Courier&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Add SSO to a SaaS app using WorkOS &amp; Okta in under 10 minutes</title><link>https://codetv.dev/blog/workos-sso-okta-idp/</link><guid isPermaLink="true">https://codetv.dev/blog/workos-sso-okta-idp/</guid><description>Many devs (like me!) are intimidated by enterprise features like single sign-on (SSO), but the tools are WAY better now. You can add it to SaaS apps fast!
</description><pubDate>Thu, 07 Sep 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/f_auto,q_auto/lwj/blog/workos-sso-okta-node.jpg&quot; alt=&quot;Add SSO to a SaaS app using WorkOS &amp;amp; Okta in under 10 minutes&quot; /&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://youtu.be/vlCg1UYl36A&quot;&gt;Watch on YouTube&lt;/a&gt;&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Want to jump to the end?&lt;/strong&gt; &lt;a href=&quot;https://github.com/learnwithjason/workos-six-figure-saas-contracts&quot;&gt;Check out the source code on
GitHub.&lt;/a&gt;&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;The scenario: enterprise-sized deals require enterprise-level auth support&lt;/h2&gt;
&lt;p&gt;Your team has been growing your SaaS offering quickly, and your first big customer is ready to sign a six-figure annual contract, but they&apos;ve got Okta as an identity provider (IdP) and they&apos;ll only finalize the deal if your app will let their team use single sign-on (SSO).&lt;/p&gt;
&lt;p&gt;For any developers who have implemented a SAML 2.0 workflow or other federated identity management manually in the past, the thought of adding SSO to a SaaS app might cause you to break into a cold sweat. I can remember spending days struggling to get it set up for a client project years ago — it was not a great experience.&lt;/p&gt;
&lt;p&gt;But things have improved significantly in this space, and tools like WorkOS make adding SSO to a Node app a task that you can fit in before lunch. In this tutorial, we&apos;ll add SSO powered by WorkOS, using Okta as an IdP.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;Thanks to WorkOS for sponsoring this tutorial. &lt;a href=&quot;https://lwj.dev/workos&quot;&gt;Create an account and get
your app ready for enterprise deals&lt;/a&gt;.&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;Create a WorkOS organization and connect it to Okta as an identity provider&lt;/h2&gt;
&lt;p&gt;First, register or log in at https://workos.com.&lt;/p&gt;
&lt;h3&gt;Add a redirect URI&lt;/h3&gt;
&lt;p&gt;As part of the SSO flow, the user will be redirected to a URI that can exchange a temporary code for an auth token. For security, these redirect URIs need to be explicitly added to WorkOS.&lt;/p&gt;
&lt;p&gt;In your WorkOS dashboard, click configuration in the left-hand nav, then add &lt;code&gt;http://localhost:3000/auth/sso/redirect&lt;/code&gt; as a redirect URI for local dev. You&apos;ll need to come back to this page later to add more URIs for your other environments, such as staging and production.&lt;/p&gt;
&lt;h3&gt;Create an organization in WorkOS&lt;/h3&gt;
&lt;p&gt;Once logged in, click &quot;Organizations&quot; in the left-hand sidebar, then click the button to create a new one. Give your organization a name and add the domain(s) associated with users in your IdP.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:a8fffb/lwj/blog/workos-sso-01-create-org.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;On the organization home page, look for &quot;Single Sign-On&quot; under the Features section. Click &quot;Configure&quot;.&lt;/p&gt;
&lt;p&gt;Choose Okta from the dropdown and give the connection a name (I chose &quot;Okta&quot;).&lt;/p&gt;
&lt;p&gt;On the next screen you&apos;ll see several URLs and configuration options. Keep this page open, then continue with the next section.&lt;/p&gt;
&lt;h3&gt;Create an application in Okta&lt;/h3&gt;
&lt;p&gt;Sign up or register for Okta. We&apos;ll be using Okta as our Identity Provider (IdP), so our users&apos; data will be stored here, and we&apos;ll use an app integration to allow WorkOS to manage the SSO flow.&lt;/p&gt;
&lt;p&gt;In your account, navigate to Applications using the top-left menu.&lt;/p&gt;
&lt;p&gt;Create a new app integration and choose SAML 2.0 as the sign-in method. Give the app a name, then click next.&lt;/p&gt;
&lt;p&gt;Under SAML settings, add the ACS URL from WorkOS as the Single sign-on URL in Okta.&lt;/p&gt;
&lt;p&gt;Next, add the SP Entity ID from WorkOS as the Audience URI in Okta.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:a8fffb/lwj/blog/workos-sso-02-saml-config.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;Under attribute statements, map the four bits of user data we want for this app:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;id =&amp;gt; user.id&lt;/li&gt;
&lt;li&gt;email =&amp;gt; user.email&lt;/li&gt;
&lt;li&gt;firstName =&amp;gt; user.firstName&lt;/li&gt;
&lt;li&gt;lastName =&amp;gt; user.lastName&lt;/li&gt;
&lt;/ul&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:a8fffb/lwj/blog/workos-sso-03-attribute-statements.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;Click next.&lt;/p&gt;
&lt;p&gt;On the next screen, choose &quot;I&apos;m an Okta customer adding an internal app&quot; and click Finish.&lt;/p&gt;
&lt;p&gt;The next screen will be your Okta app settings, and under &quot;SAML 2.0&quot; you&apos;ll see the &quot;Metadata URL&quot; — copy this, then head to your WorkOS organization. Under the URLs you copied before is a section called &quot;Identity Provider Configuration&quot;. Click &quot;Edit Configuration&quot;.&lt;/p&gt;
&lt;p&gt;Paste in the metadata URL and save. Your connection will now show active!&lt;/p&gt;
&lt;h3&gt;Assign users to your app integration&lt;/h3&gt;
&lt;p&gt;In order to allow your users to log in via WorkOS SSO, you&apos;ll need to assign the app to them in Okta. This is one of the major reasons companies choose SSO: it allows them to manage user access to all apps in a single location.&lt;/p&gt;
&lt;p&gt;To do this, go to the Directory in the menu, then choose Groups. Select the Applications tab, then click &quot;Assign applications&quot; and choose the WorkOS SSO app you just created.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; You can also assign apps to individuals or create additional groups
in Okta to give yourself finer grained control over who is allowed to use the
app. How you choose to assign apps does not affect how WorkOS SSO works.&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;Set up your dev environment&lt;/h2&gt;
&lt;p&gt;Our starting point is a Node.js app built with Fastify, which is very similar to Express. There&apos;s a home page that&apos;s public, and a dashboard that requires the user to be logged in.&lt;/p&gt;
&lt;p&gt;Clone the start branch of the repo to get started:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# clone the start branch
gh repo clone learnwithjason/workos-six-figure-saas-contracts -- -b start

# move into the directory
cd workos-six-figure-saas-contracts/

# install dependencies
npm i
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Add environment variables&lt;/h3&gt;
&lt;p&gt;Make a copy of &lt;code&gt;.env.EXAMPLE&lt;/code&gt; and rename it to &lt;code&gt;.env&lt;/code&gt;. Inside, add the following values.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;SESSION_SECRET&lt;/code&gt; — a random value (I used a &lt;a href=&quot;https://www.uuidgenerator.net/&quot;&gt;UUID generator&lt;/a&gt; to get one) used by the Fastify session plugin&lt;/li&gt;
&lt;li&gt;&lt;code&gt;REDIRECT_URI&lt;/code&gt; — this is pre-filled for development. If you change this, make sure it matches the value set as a redirect URI in your WorkOS organization&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WORKOS_API_KEY&lt;/code&gt; — click &quot;API Keys&quot; in the left-hand nav of your WorkOS account and copy the secret key&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WORKOS_CLIENT_ID&lt;/code&gt; — the client ID is displayed at the top of the API Keys screen in your WorkOS account&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WORKOS_ORG_ID&lt;/code&gt; — head to your organization in the WorkOS dashboard and the organization ID will be displayed at the top of the org home page&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Add a route to handle SSO login&lt;/h2&gt;
&lt;p&gt;In &lt;code&gt;src/app.ts&lt;/code&gt;, make the following changes:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	import &quot;dotenv/config&quot;;

	import type { FastifyPluginAsync } from &quot;fastify&quot;;
	import type { StaticRequest } from &quot;./types.js&quot;;
	import * as path from &quot;path&quot;;
	import { fileURLToPath } from &quot;url&quot;;
	import ejs from &quot;ejs&quot;;
	import fastifyView from &quot;@fastify/view&quot;;
	import fastifyStatic from &quot;@fastify/static&quot;;
+	import { WorkOS } from &quot;@workos-inc/node&quot;;

	const __filename = fileURLToPath(import.meta.url);
	const __dirname = path.dirname(__filename);
	const publicDirRoot = path.join(__dirname, &quot;..&quot;, &quot;public&quot;);

+	const workos = new WorkOS(process.env.WORKOS_API_KEY);

	const app: FastifyPluginAsync = async (fastify, _opts) =&amp;gt; {
		fastify.register(fastifyStatic, { root: publicDirRoot });
		fastify.register(fastifyView, { engine: { ejs } });
+
+		// WorkOS SSO flow
+		fastify.get(&quot;/auth/sso&quot;, async (_req, res) =&amp;gt; {
+			const authorizationUrl = workos.sso.getAuthorizationURL({
+				clientID: process.env.WORKOS_CLIENT_ID!,
+				organization: process.env.WORKOS_ORG_ID,
+				redirectURI: process.env.REDIRECT_URI!,
+			});
+
+			res.redirect(authorizationUrl);
+		});

		// App views
		fastify.get(&quot;/&quot;, async (req, reply) =&amp;gt; {
			return reply.view(&quot;src/templates/index.ejs&quot;, {
				user: undefined,
			});
		});

		fastify.get(&quot;/dashboard&quot;, async (req, reply) =&amp;gt; {
			// TODO only show the dashboard if the user is logged in

			return reply.view(&quot;src/templates/dashboard.ejs&quot;, {
				user: { first_name: &quot;Guest&quot; },
			});
		});

		// all other requests fall through to static files
		fastify.get&amp;lt;StaticRequest&amp;gt;(&quot;/:filename&quot;, async function (req, reply) {
			const { filename } = req.params;

			return reply.sendFile(filename);
		});
	};

	export default app;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Using the &lt;code&gt;@workos-inc/node&lt;/code&gt; package and our API key, this code registers a new route at &lt;code&gt;/auth/sso&lt;/code&gt;, which calls the &lt;code&gt;getAuthorizationUrl&lt;/code&gt; method. Using our client ID, organization ID, and redirect URI, this method generates a URL that will ask the user to log into their IdP (in this example, Okta).&lt;/p&gt;
&lt;p&gt;Once the user has authenticated with their IdP, they&apos;ll be redirected to the &lt;code&gt;redirectURI&lt;/code&gt; specified here.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Remember!&lt;/strong&gt; The redirect URI &lt;em&gt;must&lt;/em&gt; be set in WorkOS and match the value in
our app code.&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;Handle calls to the redirect URI&lt;/h2&gt;
&lt;p&gt;When a user has successfully authenticated with their IdP, they&apos;ll be sent to the redirect URI with a &lt;code&gt;code&lt;/code&gt; that can be exchanged for an auth token.&lt;/p&gt;
&lt;p&gt;To handle this exchange, add the redirect URI route in &lt;code&gt;src/app.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	import &quot;dotenv/config&quot;;

	import type { FastifyPluginAsync } from &quot;fastify&quot;;
-	import type { StaticRequest } from &quot;./types.js&quot;;
+	import type { StaticRequest, RedirectRequest } from &quot;./types.js&quot;;
	import * as path from &quot;path&quot;;
	import { fileURLToPath } from &quot;url&quot;;
	import ejs from &quot;ejs&quot;;
	import fastifyView from &quot;@fastify/view&quot;;
	import fastifyStatic from &quot;@fastify/static&quot;;
	import { WorkOS } from &quot;@workos-inc/node&quot;;

	const __filename = fileURLToPath(import.meta.url);
	const __dirname = path.dirname(__filename);
	const publicDirRoot = path.join(__dirname, &quot;..&quot;, &quot;public&quot;);

	const workos = new WorkOS(process.env.WORKOS_API_KEY);

	const app: FastifyPluginAsync = async (fastify, _opts) =&amp;gt; {
		fastify.register(fastifyStatic, { root: publicDirRoot });
		fastify.register(fastifyView, { engine: { ejs } });

		// WorkOS SSO flow
		fastify.get(&quot;/auth/sso&quot;, async (_req, res) =&amp;gt; {
			const authorizationUrl = workos.sso.getAuthorizationURL({
				clientID: process.env.WORKOS_CLIENT_ID!,
				organization: process.env.WORKOS_ORG_ID,
				redirectURI: process.env.REDIRECT_URI!,
			});

			res.redirect(authorizationUrl);
		});
+
+		fastify.get&amp;lt;RedirectRequest&amp;gt;(&quot;/auth/sso/redirect&quot;, async (req, res) =&amp;gt; {
+			const { code } = req.query;
+			const { profile } = await workos.sso.getProfileAndToken({
+				code,
+				clientID: process.env.WORKOS_CLIENT_ID!,
+			});
+
+			// TODO store the user profile in the session
+			console.log({ profile });
+
+			return res.redirect(&quot;/dashboard&quot;);
+		});

		// App views
		fastify.get(&quot;/&quot;, async (req, reply) =&amp;gt; {
			return reply.view(&quot;src/templates/index.ejs&quot;, {
				user: undefined,
			});
		});

		fastify.get(&quot;/dashboard&quot;, async (req, reply) =&amp;gt; {
			// TODO only show the dashboard if the user is logged in

			return reply.view(&quot;src/templates/dashboard.ejs&quot;, {
				user: { first_name: &quot;Guest&quot; },
			});
		});

		// all other requests fall through to static files
		fastify.get&amp;lt;StaticRequest&amp;gt;(&quot;/:filename&quot;, async function (req, reply) {
			const { filename } = req.params;

			return reply.sendFile(filename);
		});
	};

	export default app;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Save, then navigate to &lt;code&gt;http://localhost:3000/auth/sso&lt;/code&gt;. You&apos;ll be redirected to the Okta login. After signing in, you&apos;ll end up on the app dashboard and your profile data will be logged in the console.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:a8fffb/lwj/blog/workos-sso-04-okta-login.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;Log in with your Okta credentials and you&apos;ll land on &lt;code&gt;http://localhost:3000/dashboard&lt;/code&gt;. This means the login flow is working, but our app isn&apos;t checking for a logged in user yet or using that to restrict access to private pages.&lt;/p&gt;
&lt;h2&gt;Store user profile data in a session&lt;/h2&gt;
&lt;p&gt;To let the app display user data and prevent the user from needing to log in on every page navigation, let&apos;s add the user&apos;s profile data to a session cookie. In this app, we&apos;ll use the &lt;code&gt;@fastify/session&lt;/code&gt; package, but the approach of storing the user data in a cookie will work in just about every framework.&lt;/p&gt;
&lt;p&gt;To add support for sessions, bring in the required Fastify plugins in &lt;code&gt;src/app.ts&lt;/code&gt; and register them.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	import &quot;dotenv/config&quot;;

	import type { FastifyPluginAsync } from &quot;fastify&quot;;
	import type { StaticRequest, RedirectRequest } from &quot;./types.js&quot;;
	import * as path from &quot;path&quot;;
	import { fileURLToPath } from &quot;url&quot;;
	import ejs from &quot;ejs&quot;;
	import fastifyView from &quot;@fastify/view&quot;;
	import fastifyStatic from &quot;@fastify/static&quot;;
+	import fastifyCookie from &quot;@fastify/cookie&quot;;
+	import fastifySession from &quot;@fastify/session&quot;;
	import { WorkOS } from &quot;@workos-inc/node&quot;;

	const __filename = fileURLToPath(import.meta.url);
	const __dirname = path.dirname(__filename);
	const publicDirRoot = path.join(__dirname, &quot;..&quot;, &quot;public&quot;);

	const workos = new WorkOS(process.env.WORKOS_API_KEY);

	const app: FastifyPluginAsync = async (fastify, _opts) =&amp;gt; {
		fastify.register(fastifyStatic, { root: publicDirRoot });
		fastify.register(fastifyView, { engine: { ejs } });
+		fastify.register(fastifyCookie);
+		fastify.register(fastifySession, {
+			secret: process.env.SESSION_SECRET!,
+			cookie:
+				process.env.NODE_ENV === &quot;production&quot;
+					? {
+							path: &quot;/&quot;,
+							maxAge: 86400,
+							httpOnly: true,
+							secure: true,
+							sameSite: true,
+					  }
+					: { secure: false },
+		});

		// WorkOS SSO flow
		fastify.get(&quot;/auth/sso&quot;, async (_req, res) =&amp;gt; {
			const authorizationUrl = workos.sso.getAuthorizationURL({
				clientID: process.env.WORKOS_CLIENT_ID!,
				organization: process.env.WORKOS_ORG_ID,
				redirectURI: process.env.REDIRECT_URI!,
			});

			res.redirect(authorizationUrl);
		});

		fastify.get&amp;lt;RedirectRequest&amp;gt;(&quot;/auth/sso/redirect&quot;, async (req, res) =&amp;gt; {
			const { code } = req.query;
			const { profile } = await workos.sso.getProfileAndToken({
				code,
				clientID: process.env.WORKOS_CLIENT_ID!,
			});

-			// TODO store the user profile in the session
-			console.log({ profile });
+			req.session.user = {
+				id: profile.id,
+				first_name: profile.first_name,
+				last_name: profile.last_name,
+				email: profile.email,
+			};

			return res.redirect(&quot;/dashboard&quot;);
		});

		// App views
		fastify.get(&quot;/&quot;, async (req, reply) =&amp;gt; {
			return reply.view(&quot;src/templates/index.ejs&quot;, {
+				user: req.session.user,
			});
		});

		fastify.get(&quot;/dashboard&quot;, async (req, reply) =&amp;gt; {
			// TODO only show the dashboard if the user is logged in

			return reply.view(&quot;src/templates/dashboard.ejs&quot;, {
				user: { first_name: &quot;Guest&quot; },
			});
		});

		// all other requests fall through to static files
		fastify.get&amp;lt;StaticRequest&amp;gt;(&quot;/:filename&quot;, async function (req, reply) {
			const { filename } = req.params;

			return reply.sendFile(filename);
		});
	};

	export default app;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The required plugins allow our app to store data in a session, and we use that to store the user profile in the redirect handler after a user successfully logs in with their IdP.&lt;/p&gt;
&lt;p&gt;On the home route (&lt;code&gt;/&lt;/code&gt;), we pass the user object from the session into the template, which will allow the app UI to be updated with logged in user data.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:a8fffb/lwj/blog/workos-sso-05-logged-in-home.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;h2&gt;Only show the dashboard to logged in users&lt;/h2&gt;
&lt;p&gt;To use the SSO auth data to protect the authenticated area, make the following changes to the &lt;code&gt;/dashboard&lt;/code&gt; route in &lt;code&gt;src/app.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	fastify.get(&quot;/dashboard&quot;, async (req, reply) =&amp;gt; {
-		// TODO only show the dashboard if the user is logged in
+		if (!req.session.user?.id) {
+			return reply.redirect(&quot;/auth/sso&quot;);
+		}

		return reply.view(&quot;src/templates/dashboard.ejs&quot;, {
-			user: { first_name: &quot;Guest&quot; },
+			user: req.session.user,
		});
	});
&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:a8fffb/lwj/blog/workos-sso-06-logged-in-dashboard.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;h2&gt;Handle logout&lt;/h2&gt;
&lt;p&gt;Finally, to handle logout, all we need to do is destroy the session. Add a new route definition for &lt;code&gt;/auth/sso/logout&lt;/code&gt; in &lt;code&gt;src/app.ts&lt;/code&gt; to handle logging out:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fastify.get(&apos;/auth/logout&apos;, async (req, reply) =&amp;gt; {
  req.session.destroy();
  reply.redirect(&apos;/&apos;);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will remove the user&apos;s data from the session and send them back to the public home page. They&apos;ll need to log in again to return to the dashboard.&lt;/p&gt;
&lt;h2&gt;Go land those big contracts!&lt;/h2&gt;
&lt;p&gt;Believe it or not, that&apos;s it. Single sign on for enterprise customers used to be a nightmare, but the developer experience these days is actually pretty pleasant thanks to WorkOS.&lt;/p&gt;
&lt;p&gt;So don&apos;t let adding SAML 2.0 or other SSO solutions to your app be a blocker for your product to land larger customers. Happy building!&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;Thanks again to WorkOS for sponsoring this tutorial. Sign up now to add SSO
and more to your app — the whole process is self-serve with no sales calls
required.&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;Resources and further reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/learnwithjason/workos-six-figure-saas-contracts&quot;&gt;Review the source code for this app&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://lwj.dev/workos&quot;&gt;Learn more about WorkOS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.okta.com/signup/&quot;&gt;Okta Developer Edition&lt;/a&gt; (for testing)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://fastify.dev/&quot;&gt;Fastify&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Handle webhooks, user auth, database, &amp; file storage with Convex</title><link>https://codetv.dev/blog/react-sms-to-database-convex-twilio-clerk/</link><guid isPermaLink="true">https://codetv.dev/blog/react-sms-to-database-convex-twilio-clerk/</guid><description>What does it take to process incoming SMS with auth, image storage, and a real-time database? With Convex, you can add it to your app with &lt; 200 lines of code.
</description><pubDate>Tue, 22 Aug 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/f_auto,q_auto/lwj/blog/convex-http-actions-v2.jpg&quot; alt=&quot;Handle webhooks, user auth, database, &amp;amp; file storage with Convex&quot; /&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://youtu.be/gR9ghXOyIQ4&quot;&gt;Watch on YouTube&lt;/a&gt;&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Want to jump to the end?&lt;/strong&gt; &lt;a href=&quot;https://github.com/learnwithjason/convex-twilio-text-log&quot;&gt;Check out the source code on
GitHub.&lt;/a&gt;&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;I had a wild idea recently: what if I wanted to send a text message to my app and have it show up on the web? Could I send images, too? How hard would that be to build?&lt;/p&gt;
&lt;p&gt;My gut instinct was that it&apos;s possible, but it would be really complicated. I&apos;d need to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Have a way to turn incoming SMS messages into code&lt;/li&gt;
&lt;li&gt;Set up a webhook to handle those incoming SMS messages&lt;/li&gt;
&lt;li&gt;Validate them to make sure they&apos;re real&lt;/li&gt;
&lt;li&gt;Set up user auth on the app&lt;/li&gt;
&lt;li&gt;Set up a database to store the messages&lt;/li&gt;
&lt;li&gt;Set up file storage for incoming images&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That feels like a lot, right?&lt;/p&gt;
&lt;p&gt;But modern dev tooling is, like, &lt;em&gt;good&lt;/em&gt; good. &lt;a href=&quot;https://www.twilio.com/&quot;&gt;Twilio&lt;/a&gt; makes handling incoming SMS extremely approachable. &lt;a href=&quot;https://clerk.com/&quot;&gt;Clerk&lt;/a&gt; makes user auth so fast to set up that it feels like cheating. And &lt;a href=&quot;https://lwj.dev/convex&quot;&gt;Convex&lt;/a&gt; handles literally everything else on the requirement list, from storing data to exposing webhooks to storing images.&lt;/p&gt;
&lt;p&gt;So let&apos;s build it. &lt;strong&gt;Today we&apos;ll build a React + TypeScript app that you can send text and images to via SMS, and we&apos;ll power it all with Convex, Twilio, and Clerk.&lt;/strong&gt;&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;Huge thanks to &lt;a href=&quot;https://lwj.dev/convex&quot;&gt;Convex&lt;/a&gt; for sponsoring this tutorial.&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;Set up your dev environment&lt;/h2&gt;
&lt;p&gt;For this app, we&apos;ll focus on the database specifically. Clone the &lt;code&gt;start&lt;/code&gt; branch of the demo app&apos;s repo to get an app that&apos;s working except for data.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# clone the start branch of the repo using the GitHub CLI
gh repo clone learnwithjason/convex-twilio-text-log -- -b start

# move into the repo
cd convex-twilio-text-log/

# install dependencies
npm i
&lt;/code&gt;&lt;/pre&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; This app is built on &lt;a href=&quot;https://vitejs.dev/guide/&quot;&gt;Vite&apos;s React + TypeScript
template&lt;/a&gt;.&lt;/p&gt;&lt;/aside&gt;
&lt;h3&gt;Set up Clerk&lt;/h3&gt;
&lt;p&gt;This app uses &lt;a href=&quot;https://clerk.com&quot;&gt;Clerk&lt;/a&gt; to allow users to create accounts and log in, so before we can start developing we&apos;ll need a Clerk account and a publishable key.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Sign up or sign in at https://clerk.com&lt;/li&gt;
&lt;li&gt;Click the &quot;add application&quot; button&lt;/li&gt;
&lt;li&gt;Give your new application a name (e.g. &quot;Snack Tracker&quot;)&lt;/li&gt;
&lt;li&gt;Under &quot;how will your users sign in?&quot;, choose &lt;em&gt;only&lt;/em&gt; &quot;phone number&quot; — our whole app is built around texting, so this is important!&lt;/li&gt;
&lt;li&gt;Click create application&lt;/li&gt;
&lt;li&gt;On the next screen, copy your publishable key&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Back in your code, rename &lt;code&gt;.env.local.EXAMPLE&lt;/code&gt; to &lt;code&gt;.env.local&lt;/code&gt; and paste the publishable key as the value of &lt;code&gt;VITE_CLERK_PUBLISHABLE_KEY&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Start the dev server&lt;/h3&gt;
&lt;p&gt;Once the Clerk publishable key is saved, start the dev server.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Open &lt;code&gt;http://localhost:5173&lt;/code&gt; in your browser to see the app.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:a8fffb/lwj/blog/convex-actions-01-local-dev.jpg&quot; alt=&quot;the login screen of the demo app. it displays a &amp;quot;sign up&amp;quot; and &amp;quot;sign in&amp;quot;
button as well as a description of why an account is
needed&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;Sign up with your phone number and you&apos;ll see the logged-in view of the app dashboard.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:a8fffb/lwj/blog/convex-actions-02-logged-in.jpg&quot; alt=&quot;the demo app dashboard with no messages. it displays instructions on how to
create
entries&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;h3&gt;Get a Twilio phone number to accept incoming SMS messages&lt;/h3&gt;
&lt;p&gt;For this app to work, we need a way to relay incoming SMS messages to our code. Twilio makes this possible. If you&apos;ve never used Twilio before, you can get a &lt;a href=&quot;https://www.twilio.com/try-twilio&quot;&gt;free trial and a trial number for testing&lt;/a&gt;. If you already use Twilio, setting up a new phone number costs $1.15/month (in the US at the time of writing).&lt;/p&gt;
&lt;p&gt;To set up your Twilio number:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Go to the &lt;a href=&quot;https://console.twilio.com&quot;&gt;Twilio console&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Find &quot;phone numbers&quot; either in the left-hand sidebar or by searching&lt;/li&gt;
&lt;li&gt;Create a new number
&lt;ul&gt;
&lt;li&gt;If you&apos;re in a free trial, click &quot;get a trial number&quot;&lt;/li&gt;
&lt;li&gt;If you&apos;re not, click &quot;buy a number&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;On the next screen, choose your country, make sure &quot;SMS&quot; and &quot;MMS&quot; are checked under Capabilities, and choose any number.&lt;/li&gt;
&lt;li&gt;Buy the number and copy it&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Open &lt;code&gt;.env.local&lt;/code&gt; in your code and add the phone number, including country code, as the value of &lt;code&gt;VITE_TWILIO_PHONE_NUMBER&lt;/code&gt;. It should be formatted like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;VITE_TWILIO_PHONE_NUMBER=&quot;+1 555-555-5555&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; This format isn&apos;t &lt;em&gt;strictly&lt;/em&gt; required, but I tested that it works in
links, so if you want to play it safe and avoid errors, match this format.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;Save and you&apos;ll see your Twilio number displayed in the app.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:a8fffb/lwj/blog/convex-actions-03-twilio-phone-number.jpg&quot; alt=&quot;the same app dashboard screenshot as above, except now the Twilio phone
number is prominently
displayed&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;At this point, you&apos;re ready to add a database!&lt;/p&gt;
&lt;h2&gt;Set up Convex&lt;/h2&gt;
&lt;p&gt;Now that the app is up and running, let&apos;s add a database to store messages sent by users and the webhook that will handle new incoming messages.&lt;/p&gt;
&lt;p&gt;Install Convex in the app by adding the &lt;code&gt;convex&lt;/code&gt; package:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm i convex
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, start the Convex dev process in a second terminal window (the app&apos;s dev process should still be running) to initialize Convex for your app:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npx convex dev
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Choose &quot;create a new project&quot;&lt;/li&gt;
&lt;li&gt;If necessary, you&apos;ll be prompted to create an account or log in&lt;/li&gt;
&lt;li&gt;Choose which team the project belongs to&lt;/li&gt;
&lt;li&gt;Give the project a name (e.g. &lt;code&gt;snack-tracker&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This creates a new folder called &lt;code&gt;convex&lt;/code&gt; in the app, which is where all of the schema, data access, and HTTP actions for the app will be created and managed.&lt;/p&gt;
&lt;h2&gt;Create a database table to store messages&lt;/h2&gt;
&lt;p&gt;Before we do anything else, let&apos;s define a schema for our messages. Create a new file at &lt;code&gt;convex/schema.ts&lt;/code&gt; and add the following code:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { defineSchema, defineTable } from &apos;convex/server&apos;;
import { v } from &apos;convex/values&apos;;

export const MessageFields = {
  text: v.string(),
  sender: v.string(),
  image: v.union(
    v.object({
      id: v.string(),
      url: v.union(v.string(), v.null()),
    }),
    v.null()
  ),
};

export default defineSchema({
  messages: defineTable(MessageFields).index(&apos;by_sender&apos;, [&apos;sender&apos;]),
});
&lt;/code&gt;&lt;/pre&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; Creating a schema is optional in Convex, but recommended. Adding a
schema gives the app TypeScript checks, as well as runtime type checking.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;Exporting &lt;code&gt;MessageFields&lt;/code&gt; separately means we can import that to use as a TypeScript type anywhere we need it in our app.&lt;/p&gt;
&lt;p&gt;Using &lt;code&gt;defineSchema&lt;/code&gt;, we pass in an object that describes all the tables in our app. We pass &lt;code&gt;MessageFields&lt;/code&gt; to the &lt;code&gt;defineTable&lt;/code&gt; function to use that schema for our messages, and to speed up searching by sender (which will be how we query for messages), we add an index to the &lt;code&gt;messages&lt;/code&gt; table on the &lt;code&gt;sender&lt;/code&gt; field.&lt;/p&gt;
&lt;p&gt;After saving, Convex will automatically update and create the &lt;code&gt;messages&lt;/code&gt; table, which you can view in &lt;a href=&quot;https://dashboard.convex.dev&quot;&gt;the Convex dashboard&lt;/a&gt;.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:a8fffb/lwj/blog/convex-actions-04-messages-table.jpg&quot; alt=&quot;the Data tab of the Convex dashboard showing an empty messages
table&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;h2&gt;Connect Convex to Clerk auth&lt;/h2&gt;
&lt;p&gt;This app requires a user to be logged in to view posts, and they&apos;re only able to see their own posts. How this translates to code is that we need to get the currently logged in user from Clerk and use that as part of our query to Convex.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; The reason we only allow seeing your own posts in this tutorial is
that everything coming in is unmoderated, so this protects people from seeing
anything problematic uploaded by others. If you intend to make posts visible
to other users, make sure you&apos;ve got a moderation plan in place!&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;Fortunately, &lt;a href=&quot;https://docs.convex.dev/auth/clerk&quot;&gt;Clerk and Convex have a first-class integration&lt;/a&gt;, so we&apos;re able to do this with a few clicks and a few lines of code.&lt;/p&gt;
&lt;h3&gt;Configure Clerk to integrate with Convex&lt;/h3&gt;
&lt;p&gt;Head to the &lt;a href=&quot;https://dashboard.clerk.com/&quot;&gt;Clerk dashboard&lt;/a&gt; and choose your app.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Click &quot;JWT Templates&quot; from the left-hand nav&lt;/li&gt;
&lt;li&gt;Click &quot;New template&quot;&lt;/li&gt;
&lt;li&gt;Choose Convex&lt;/li&gt;
&lt;li&gt;Copy the &quot;Issuer&quot; URL that appears on the next screen&lt;/li&gt;
&lt;li&gt;Click &quot;apply changes&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Add auth config to Convex&lt;/h3&gt;
&lt;p&gt;With the issuer URL copied, create a new file at &lt;code&gt;convex/auth.config.js&lt;/code&gt; and add the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export default {
  providers: [
    {
      domain: &apos;YOUR_CLERK_ISSUER_URL&apos;,
      applicationID: &apos;convex&apos;,
    },
  ],
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Save and Convex will auto-detect the new config and update.&lt;/p&gt;
&lt;p&gt;This tells Convex to use Clerk&apos;s configuration for auth, and will give us access to the currently logged in user within our Convex calls.&lt;/p&gt;
&lt;h2&gt;Add a Convex provider to the app&lt;/h2&gt;
&lt;p&gt;To use Convex in the app UI, we need to add a provider. Since we&apos;re using Clerk for auth, we&apos;ll use a special provider from Convex called &lt;code&gt;ConvexProviderWithClerk&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The provider accepts a &lt;code&gt;client&lt;/code&gt;, which we need to configure with our Convex URL. To get this, go to the Convex dashboard, choose your project, and navigate to settings. Click the toggle to show your development credentials and copy the Deployment URL.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:a8fffb/lwj/blog/convex-actions-05-convex-url.jpg&quot; alt=&quot;the deployment settings screen on the Convex dashboard with a red arrow
pointing to where the deployment URL is
displayed&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;Store the deployment URL as &lt;code&gt;VITE_CONVEX_URL&lt;/code&gt; in &lt;code&gt;.env.local&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Next, open &lt;code&gt;src/main.tsx&lt;/code&gt; and make the following changes:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	import React from &apos;react&apos;;
	import ReactDOM from &apos;react-dom/client&apos;;
+	import { ConvexProviderWithClerk } from &apos;convex/react-clerk&apos;;
+	import { ConvexReactClient } from &apos;convex/react&apos;;
-	import { ClerkProvider } from &apos;@clerk/clerk-react&apos;;
+	import { ClerkProvider, useAuth } from &apos;@clerk/clerk-react&apos;;
	import { App } from &apos;./components/app&apos;;

	import &apos;./styles/global.css&apos;;

+	const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL as string);

	ReactDOM.createRoot(document.getElementById(&apos;root&apos;) as HTMLElement).render(
		&amp;lt;React.StrictMode&amp;gt;
			&amp;lt;ClerkProvider publishableKey={import.meta.env.VITE_CLERK_PUBLISHABLE_KEY}&amp;gt;
+				&amp;lt;ConvexProviderWithClerk client={convex} useAuth={useAuth}&amp;gt;
					&amp;lt;App /&amp;gt;
+				&amp;lt;/ConvexProviderWithClerk&amp;gt;
			&amp;lt;/ClerkProvider&amp;gt;
		&amp;lt;/React.StrictMode&amp;gt;,
	);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This change allows Clerk to provide auth data to the Convex provider via the &lt;code&gt;useAuth&lt;/code&gt; hook — and that&apos;s all the setup that&apos;s required to integrate Convex and Clerk.&lt;/p&gt;
&lt;h2&gt;Add a query to load messages by user&lt;/h2&gt;
&lt;p&gt;Now that we have access to Convex and the current user in our app, we need a way to query for the messages they&apos;re authorized to see.&lt;/p&gt;
&lt;p&gt;To do that, we&apos;ll define our first Convex query. Create a new file at &lt;code&gt;convex/messages.ts&lt;/code&gt; and add the following code:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { query } from &apos;./_generated/server&apos;;

export const get = query({
  handler: async (ctx) =&amp;gt; {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity || !identity.phoneNumberVerified) {
      return [];
    }

    const messages = await ctx.db
      .query(&apos;messages&apos;)
      .withIndex(&apos;by_sender&apos;, (q) =&amp;gt; q.eq(&apos;sender&apos;, identity.phoneNumber!))
      .collect();

    return messages;
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;query&lt;/code&gt; helper from Convex is loaded from the &lt;code&gt;_generated&lt;/code&gt; directory, which Convex uses to provide us with autocompletion and other quality-of-life enhancements as we write our apps.&lt;/p&gt;
&lt;p&gt;Inside we define a handler that receives a context object (&lt;code&gt;ctx&lt;/code&gt;) from Convex. This object contains multiple helpful utilities, including &lt;code&gt;auth&lt;/code&gt;, which has a method for loading the current user&apos;s details, and &lt;code&gt;db&lt;/code&gt;, which has methods for querying our database tables.&lt;/p&gt;
&lt;p&gt;After loading the current user (and returning an empty result set if no user is found), this code queries the &lt;code&gt;messages&lt;/code&gt; table using that &lt;code&gt;by_sender&lt;/code&gt; index we defined earlier, and uses the &lt;code&gt;q&lt;/code&gt; helper to filter down results to only those where the sender of the stored message matches the phone number of the current user.&lt;/p&gt;
&lt;h2&gt;Run the query in the React UI&lt;/h2&gt;
&lt;p&gt;To use this query in the app, make the following changes in &lt;code&gt;src/components/messages.tsx&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;+	import { useQuery } from &apos;convex/react&apos;;
+	import { api } from &apos;../../convex/_generated/api&apos;;
+
	import styles from &apos;./messages.module.css&apos;;

	const Empty = () =&amp;gt; {
		const phone = import.meta.env.VITE_TWILIO_PHONE_NUMBER;

		return (
			&amp;lt;div className={styles.empty}&amp;gt;
				&amp;lt;h1&amp;gt;Ready to start logging?&amp;lt;/h1&amp;gt;
				&amp;lt;p&amp;gt;
					Text a photo and any details or description you want to include about it
					to:
					&amp;lt;a href={`sms:${phone}`}&amp;gt;{phone}&amp;lt;/a&amp;gt;
					Send your first text to see it logged here!
				&amp;lt;/p&amp;gt;
			&amp;lt;/div&amp;gt;
		);
	};

	export const Messages = () =&amp;gt; {
-		// TODO: load all messages from the currently logged in user
-		const messages = [];
+		// load all messages from the currently logged in user
+		const messages = useQuery(api.messages.get) || [];

		return (
			&amp;lt;section className={styles.wrapper}&amp;gt;
				{messages.length &amp;gt; 0 ? (
					&amp;lt;ul className={styles.messages}&amp;gt;
-						{/* TODO: display messages */}
+						{messages.map(({ _id, _creationTime, text, image }) =&amp;gt; {
+							return (
+								&amp;lt;li key={_id} className={styles.message}&amp;gt;
+									{image &amp;amp;&amp;amp; image.url ? (
+										&amp;lt;img className={styles.image} src={image.url} alt={text} /&amp;gt;
+									) : null}
+									&amp;lt;p className={styles.text}&amp;gt;{text}&amp;lt;/p&amp;gt;
+									&amp;lt;p className={styles.meta}&amp;gt;
+										Posted {new Date(_creationTime).toLocaleString()}
+									&amp;lt;/p&amp;gt;
+								&amp;lt;/li&amp;gt;
+							);
+						})}
					&amp;lt;/ul&amp;gt;
				) : (
					&amp;lt;Empty /&amp;gt;
				)}
			&amp;lt;/section&amp;gt;
		);
	};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Convex generates an &lt;code&gt;api&lt;/code&gt; object that will autocomplete with all available tables and their related queries, mutations, and actions. The &lt;code&gt;useQuery&lt;/code&gt; hook runs the &lt;code&gt;get&lt;/code&gt; query we just defined and returns an array of messages.&lt;/p&gt;
&lt;p&gt;To display the messages, we loop over them and destructure out the fields we need. We also use two system-generated fields: &lt;code&gt;_id&lt;/code&gt;, which is an auto-generated unique identifier for each entry, and &lt;code&gt;_creationTime&lt;/code&gt;, which is the timestamp at which the entry was created.&lt;/p&gt;
&lt;p&gt;Right now our table is empty. You can create an entry or two manually through the Convex dashboard to test this if you&apos;d like. This has the bonus effect of showing the automatic real-time nature of working with Convex: as soon as you save an entry, it will show up in the app UI in real time.&lt;/p&gt;
&lt;h2&gt;Create new messages from incoming SMS messages&lt;/h2&gt;
&lt;p&gt;To allow our users to create messages, we need a way to process SMS messages sent to our Twilio number. To do this, we&apos;ll use &lt;a href=&quot;https://docs.convex.dev/functions/http-actions&quot;&gt;Convex HTTP actions&lt;/a&gt;, which are similar to queries and mutations, but are exposed as HTTP endpoints so they can interact with third-party systems.&lt;/p&gt;
&lt;p&gt;This makes Convex HTTP actions an ideal solution for building webhooks.&lt;/p&gt;
&lt;h3&gt;Create a Convex HTTP action&lt;/h3&gt;
&lt;p&gt;A Convex HTTP action receives a &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Request&quot;&gt;standard &lt;code&gt;Request&lt;/code&gt; object&lt;/a&gt; in addition to the Convex context, and it needs to return a &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Response&quot;&gt;standard &lt;code&gt;Response&lt;/code&gt; object&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Export a new method called &lt;code&gt;save&lt;/code&gt; from &lt;code&gt;convex/messages.ts&lt;/code&gt; with the following code:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import type { WithoutSystemFields } from &apos;convex/server&apos;;
import type { Doc } from &apos;./_generated/dataModel&apos;;
import { httpAction, query } from &apos;./_generated/server&apos;;

type Message = WithoutSystemFields&amp;lt;Doc&amp;lt;&apos;messages&apos;&amp;gt;&amp;gt;;

export const get = query({
  /* unchanged */
});

/*
 * An HTTP action exposes an endpoint, which we’ll add as the webhook URL for
 * incoming Twilio SMS messages. This will be called every time someone texts
 * the phone number provided by our app.
 */
export const save = httpAction(async (ctx, req) =&amp;gt; {
  const body = await req.text();

  // Twilio params: https://www.twilio.com/docs/messaging/guides/webhook-request
  const message = new URLSearchParams(body);

  // TODO: validate the webhook for security

  const text = message.get(&apos;Body&apos;) ?? &apos;&apos;;
  const sender = message.get(&apos;From&apos;);
  const imageUrl = message.get(&apos;MediaUrl0&apos;);

  if (!sender) {
    return new Response(null, {
      status: 400,
    });
  }

  const msg: Message = {
    text,
    sender,
    image: null,
  };

  if (imageUrl) {
    // TODO: store any images sent by the user
  }

  // TODO: save the message
  console.log(JSON.stringify(msg, null, 2));

  return new Response(null, {
    status: 200,
  });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At the top, we import the &lt;code&gt;httpAction&lt;/code&gt; helper, as well as two types: &lt;code&gt;WithoutSystemFields&lt;/code&gt; and &lt;code&gt;Doc&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The two types allow us to create a &lt;code&gt;Message&lt;/code&gt; type that matches our message table schema but leaves out fields that are generated by Convex, such as &lt;code&gt;_id&lt;/code&gt;. This lets us add type checking without having to worry about missing system fields before saving.&lt;/p&gt;
&lt;p&gt;In the &lt;code&gt;save&lt;/code&gt; function, we get the body of the request as text because Twilio sends the body as query parameters (e.g. &lt;code&gt;key1=val1&amp;amp;key2=val2&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;Our function needs the sender&apos;s phone number, the text from the message, and the URL of the first image, if any were sent.&lt;/p&gt;
&lt;p&gt;Organize those details in to an object that matches the &lt;code&gt;Message&lt;/code&gt; type and it&apos;s ready for saving! We&apos;ll add the mutation to actually save entries in a moment, but for now this is good enough to test that it&apos;s working once we integrate with Twilio.&lt;/p&gt;
&lt;h3&gt;Expose the HTTP action in a URL endpoint&lt;/h3&gt;
&lt;p&gt;To make our HTTP action callable, we need to give it a public URL. To do this, create a new file at &lt;code&gt;convex/http.ts&lt;/code&gt; and add the following code:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { httpRouter } from &apos;convex/server&apos;;
import { save } from &apos;./messages&apos;;

const http = httpRouter();

http.route({
  path: &apos;/messages&apos;,
  method: &apos;POST&apos;,
  handler: save,
});

export default http;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We define a new route at &lt;code&gt;/messages&lt;/code&gt;, then add our &lt;code&gt;save&lt;/code&gt; function as the handler for requests sent to that endpoint via &lt;code&gt;POST&lt;/code&gt; requests. Once we save, our HTTP action is now usable by a third-party service.&lt;/p&gt;
&lt;p&gt;HTTP actions are exposed at &lt;code&gt;https://&amp;lt;your deployment name&amp;gt;.convex.site&lt;/code&gt;. Grab the value you stored in &lt;code&gt;VITE_CONVEX_URL&lt;/code&gt; and replace &lt;code&gt;.cloud&lt;/code&gt; with &lt;code&gt;.site&lt;/code&gt;, then append &lt;code&gt;/messages&lt;/code&gt; for the full URL to your HTTP action (e.g. &lt;code&gt;https://energized-rooster-480.convex.site/messages&lt;/code&gt;).&lt;/p&gt;
&lt;h3&gt;Register the Convex HTTP action as a webhook for incoming Twilio messages&lt;/h3&gt;
&lt;p&gt;In the &lt;a href=&quot;https://console.twilio.com/&quot;&gt;Twilio console&lt;/a&gt;, navigate to your active numbers and choose the one you purchased earlier.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Under the &quot;Configure&quot; tab, scroll down to &quot;Messaging Configuration&quot;&lt;/li&gt;
&lt;li&gt;In the section for &quot;A message comes in&quot;, make sure &quot;Webhook&quot; is selected&lt;/li&gt;
&lt;li&gt;Add your HTTP action endpoint as the webhook URL&lt;/li&gt;
&lt;li&gt;Make sure &quot;HTTP POST&quot; is selected&lt;/li&gt;
&lt;li&gt;Click &quot;Save configuration&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:a8fffb/lwj/blog/convex-actions-06-configure-twilio-webhook.jpg&quot; alt=&quot;the Twilio console configuration screen for an active
number&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;Once this is saved, send a text message to your Twilio number, then look at the logs in Convex. You&apos;ll see your number and the contents of your text message logged there, which means the webhook is configured properly.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:a8fffb/lwj/blog/convex-actions-07-incoming-webhook.jpg&quot; alt=&quot;composite image of an iPhone text message sent to the Twilio number with an
arrow drawn from the text to the entry in the Convex dashboard
log&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;h3&gt;Validate Twilio webhook requests&lt;/h3&gt;
&lt;p&gt;To make sure some mischief-maker out there doesn&apos;t spam or otherwise abuse the app, let&apos;s make sure every request received by our HTTP action is a &lt;a href=&quot;https://www.twilio.com/docs/usage/webhooks/webhooks-security&quot;&gt;valid Twilio request&lt;/a&gt; before taking any action.&lt;/p&gt;
&lt;p&gt;Validation will be handled in a Convex &lt;a href=&quot;https://docs.convex.dev/functions/internal-functions&quot;&gt;internal function&lt;/a&gt;. To do that, we&apos;ll use &lt;a href=&quot;https://github.com/twilio/twilio-node&quot;&gt;Twilio&apos;s Node SDK&lt;/a&gt;, which we can install by running the following in our terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm i twilio
&lt;/code&gt;&lt;/pre&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; Using a Node package requires Convex to run in Node (Convex uses a
&lt;a href=&quot;https://docs.convex.dev/functions/runtimes&quot;&gt;custom JavaScript runtime&lt;/a&gt; by
default). Fortunately, we can do this with a one-liner at the top of the file.
We only need Node for this one step, so we&apos;ll pull it out into its own file so
the rest of our Convex code can execute in the optimized environment.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;Create a new file at &lt;code&gt;convex/validate.ts&lt;/code&gt; with the following code inside:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&apos;use node&apos;;

import { v } from &apos;convex/values&apos;;
import twilio from &apos;twilio&apos;;
import { internalAction } from &apos;./_generated/server&apos;;

export const twilioWebhook = internalAction({
  args: {
    signature: v.string(),
    url: v.string(),
    params: v.any(), // Twilio sends a lot of fields that might vary
  },
  handler: async (_, args) =&amp;gt; {
    return twilio.validateRequest(
      process.env.TWILIO_AUTH_TOKEN!,
      args.signature,
      args.url,
      args.params
    );
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To run this code, we&apos;ll need our Twilio auth token:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Navigate to https://console.twilio.com&lt;/li&gt;
&lt;li&gt;Copy the &quot;Auth token&quot; field&lt;/li&gt;
&lt;li&gt;Open the Convex dashboard&lt;/li&gt;
&lt;li&gt;Choose your project&lt;/li&gt;
&lt;li&gt;Click &quot;Settings&quot; in the left-hand nav&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;TWILIO_AUTH_TOKEN&lt;/code&gt; as the key of a new environment variable&lt;/li&gt;
&lt;li&gt;Add your copied auth token as the value&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To actually call the validation action, make the following changes to &lt;code&gt;convex/messages.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	import type { WithoutSystemFields } from &apos;convex/server&apos;;
	import type { Doc } from &apos;./_generated/dataModel&apos;;
	import { httpAction, query } from &apos;./_generated/server&apos;;
+	import { internal } from &apos;./_generated/api&apos;;

	type Message = WithoutSystemFields&amp;lt;Doc&amp;lt;&apos;messages&apos;&amp;gt;&amp;gt;;

	export const get = query({
		handler: async (ctx) =&amp;gt; {
			const identity = await ctx.auth.getUserIdentity();
			if (!identity || !identity.phoneNumberVerified) {
				return [];
			}

			const messages = await ctx.db
				.query(&apos;messages&apos;)
				.withIndex(&apos;by_sender&apos;, (q) =&amp;gt; q.eq(&apos;sender&apos;, identity.phoneNumber!))
				.collect();

			return messages;
		},
	});

	/*
	 * An HTTP action exposes an endpoint, which we’ll add as the webhook URL for
	 * incoming Twilio SMS messages. This will be called every time someone texts
	 * the phone number provided by our app.
	 */
	export const save = httpAction(async (ctx, req) =&amp;gt; {
		const body = await req.text();

		// Twilio params: https://www.twilio.com/docs/messaging/guides/webhook-request
		const message = new URLSearchParams(body);

-		// TODO: validate the webhook for security
+		const isValidWebhook = await ctx.runAction(internal.validate.twilioWebhook, {
+			url: req.url,
+			signature: req.headers.get(&apos;x-twilio-signature&apos;) ?? &apos;&apos;,
+			params: Object.fromEntries(message.entries()),
+		});
+
+		if (!isValidWebhook) {
+			return new Response(null, {
+				status: 422,
+			});
+		}

		const text = message.get(&apos;Body&apos;) ?? &apos;&apos;;
		const sender = message.get(&apos;From&apos;);
		const imageUrl = message.get(&apos;MediaUrl0&apos;);

		/* unchanged below this point */
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This code grabs the URL, request signature, and parameters sent by Twilio, then passes them to our internal validation action. If the request is valid, the code continues to run as usual, but if the signatures don&apos;t match our HTTP action will now return a &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422&quot;&gt;422 HTTP response code&lt;/a&gt; (&quot;unprocessable content&quot;).&lt;/p&gt;
&lt;p&gt;Send another text to your Twilio number to validate that it still works as expected with valid requests. If you want to test invalid requests, you can use something like Postman to send a POST request to the HTTP action — you&apos;ll now receive a 422 response.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:a8fffb/lwj/blog/convex-actions-08-422-response.jpg&quot; alt=&quot;a screenshot of the Postman UI making a POST call to the webhook. a red
arrow points to the 422 response that was
received&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;h3&gt;Save new messages in Convex&lt;/h3&gt;
&lt;p&gt;Now that we&apos;re receiving messages from Twilio and we&apos;re confident that the requests are valid, let&apos;s save them in the database.&lt;/p&gt;
&lt;p&gt;To do that, we&apos;ll use another internal function, but this time it&apos;ll be a mutation. Make the following changes to &lt;code&gt;convex/messages.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	import type { WithoutSystemFields } from &apos;convex/server&apos;;
	import type { Doc } from &apos;./_generated/dataModel&apos;;
-	import { httpAction, query } from &apos;./_generated/server&apos;;
+	import { httpAction, query, internalMutation } from &apos;./_generated/server&apos;;
	import { internal } from &apos;./_generated/api&apos;;
+	import { MessageFields } from &apos;./schema&apos;;

	type Message = WithoutSystemFields&amp;lt;Doc&amp;lt;&apos;messages&apos;&amp;gt;&amp;gt;;

	export const get = query({ /* unchanged */ });

	/*
	* An HTTP action exposes an endpoint, which we’ll add as the webhook URL for
	* incoming Twilio SMS messages. This will be called every time someone texts
	* the phone number provided by our app.
	*/
	export const save = httpAction(async (ctx, req) =&amp;gt; {
		const body = await req.text();

		// Twilio params: https://www.twilio.com/docs/messaging/guides/webhook-request
		const message = new URLSearchParams(body);

		// TODO: validate the webhook for security
		const isValidWebhook = await ctx.runAction(internal.validate.twilioWebhook, {
			url: req.url,
			signature: req.headers.get(&apos;x-twilio-signature&apos;) ?? &apos;&apos;,
			params: Object.fromEntries(message.entries()),
		});

		if (!isValidWebhook) {
			return new Response(null, {
				status: 422,
			});
		}

		const text = message.get(&apos;Body&apos;) ?? &apos;&apos;;
		const sender = message.get(&apos;From&apos;);
		const imageUrl = message.get(&apos;MediaUrl0&apos;);

		if (!sender) {
			return new Response(null, {
				status: 400,
			});
		}

		const msg: Message = {
			text,
			sender,
			image: null,
		};

		if (imageUrl) {
			// TODO: store any images sent by the user
		}

-		// TODO: save the message
-		console.log(JSON.stringify(msg, null, 2));
+		ctx.runMutation(internal.messages.saveMessage, msg);

		return new Response(null, {
			status: 200,
		});
	});
+
+	export const saveMessage = internalMutation({
+		args: MessageFields,
+		handler: async (ctx, args) =&amp;gt; {
+			await ctx.db.insert(&apos;messages&apos;, args);
+		},
+	});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Save, then send a text message to your Twilio number. After a few seconds it will appear in your app UI.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:a8fffb/lwj/blog/convex-actions-09-saved-message.jpg&quot; alt=&quot;screenshot of the app dashboard with a message
displayed&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;This is already pretty dang cool, but we want to make it better: let&apos;s add support for sending and saving images as well.&lt;/p&gt;
&lt;h2&gt;Save incoming images in messages to Convex&lt;/h2&gt;
&lt;p&gt;Twilio automatically sends along images in webhook requests. In our app, we want to save those images to the same place as the rest of our data, so we&apos;ll be using &lt;a href=&quot;https://docs.convex.dev/file-storage&quot;&gt;Convex file storage&lt;/a&gt; to download and deliver them.&lt;/p&gt;
&lt;p&gt;And before you wave this off as too complicated: this will take about 20 lines of code to implement!&lt;/p&gt;
&lt;p&gt;Make the following changes in &lt;code&gt;convex/messages.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	import type { WithoutSystemFields } from &apos;convex/server&apos;;
	import type { Doc } from &apos;./_generated/dataModel&apos;;
	import {
+		type ActionCtx,
		httpAction,
		query,
		internalMutation,
	} from &apos;./_generated/server&apos;;
	import { internal } from &apos;./_generated/api&apos;;
	import { MessageFields } from &apos;./schema&apos;;

	type Message = WithoutSystemFields&amp;lt;Doc&amp;lt;&apos;messages&apos;&amp;gt;&amp;gt;;

	export const get = query({ /* unchanged */ });

	/*
	* An HTTP action exposes an endpoint, which we’ll add as the webhook URL for
	* incoming Twilio SMS messages. This will be called every time someone texts
	* the phone number provided by our app.
	*/
	export const save = httpAction(async (ctx, req) =&amp;gt; {
		const body = await req.text();

		// Twilio params: https://www.twilio.com/docs/messaging/guides/webhook-request
		const message = new URLSearchParams(body);

		// TODO: validate the webhook for security
		const isValidWebhook = await ctx.runAction(internal.validate.twilioWebhook, {
			url: req.url,
			signature: req.headers.get(&apos;x-twilio-signature&apos;) ?? &apos;&apos;,
			params: Object.fromEntries(message.entries()),
		});

		if (!isValidWebhook) {
			return new Response(null, {
				status: 422,
			});
		}

		const text = message.get(&apos;Body&apos;) ?? &apos;&apos;;
		const sender = message.get(&apos;From&apos;);
		const imageUrl = message.get(&apos;MediaUrl0&apos;);

		if (!sender) {
			return new Response(null, {
				status: 400,
			});
		}

		const msg: Message = {
			text,
			sender,
			image: null,
		};

		if (imageUrl) {
+			try {
+				msg.image = await storeImage(ctx, imageUrl);
+			} catch (err) {
+				console.error(`failed to store image (url: ${imageUrl})`);
+			}
		}

		ctx.runMutation(internal.messages.saveMessage, msg);

		return new Response(null, {
			status: 200,
		});
	});

	export const saveMessage = internalMutation({
		args: MessageFields,
		handler: async (ctx, args) =&amp;gt; {
			await ctx.db.insert(&apos;messages&apos;, args);
		},
	});

+	export const storeImage = async (ctx: ActionCtx, imageUrl: string) =&amp;gt; {
+		const res = await fetch(imageUrl);
+
+		if (!res.ok) {
+			console.error(res);
+			return null;
+		}
+
+		const blob = await res.blob();
+		const id = await ctx.storage.store(blob);
+		const url = await ctx.storage.getUrl(id);
+
+		return { id, url };
+	};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;storeImage&lt;/code&gt; function loads the provided image from its Twilio URL, then sends it to Convex&apos;s storage as a blob. The returned ID of the stored file is then used to generate its public URL, and both the ID and URL are returned.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;save&lt;/code&gt; action runs &lt;code&gt;storeImage&lt;/code&gt; if there&apos;s an image in the message and wraps it in a &lt;code&gt;try ... catch&lt;/code&gt; block just in case the file is incompatible or otherwise unusable.&lt;/p&gt;
&lt;p&gt;And... that&apos;s it. Save this and send an image to your app&apos;s phone number to see it show up in the dashboard.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:a8fffb/lwj/blog/convex-actions-10-images-saving.jpg&quot; alt=&quot;the app dashboard showing six entries with images, all of food and
beverages&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;h2&gt;Stop worrying that databases are too hard and go build cool stuff&lt;/h2&gt;
&lt;p&gt;I&apos;ve let a lot of good ideas die because I didn&apos;t want to deal with setting up or managing a database. These days, though, tools like Convex make it so dang easy that I can&apos;t make excuses — it&apos;s &lt;em&gt;fun&lt;/em&gt; to put together a database like this. It&apos;s &lt;em&gt;fun&lt;/em&gt; to hook up different third-party APIs.&lt;/p&gt;
&lt;p&gt;I really love the web today, because these tools are here to let me just go build my ideas instead of having to spend all my time creating the boilerplate that makes my ideas function.&lt;/p&gt;
&lt;p&gt;I&apos;m excited for what this unlocks for the web. I hope you&apos;re excited, too. I hope you show me what you build.&lt;/p&gt;
&lt;h2&gt;Resources and further reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/learnwithjason/convex-twilio-text-log&quot;&gt;Review the source code for this app&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://lwj.dev/convex&quot;&gt;Learn more about Convex&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.convex.dev/functions/http-actions&quot;&gt;Convex HTTP actions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.convex.dev/auth/clerk&quot;&gt;Clerk integration with Convex&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.twilio.com/docs/usage/webhooks/webhooks-security&quot;&gt;Validating Twilio webhooks&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Use AI to moderate abusive and vulgar comments (full tutorial)</title><link>https://codetv.dev/blog/comment-moderation-ai-airplane/</link><guid isPermaLink="true">https://codetv.dev/blog/comment-moderation-ai-airplane/</guid><description>Build an internal dashboard to view and moderate comments in this full tutorial. Plus, learn how to use OpenAI to automatically flag the worst comments.
</description><pubDate>Mon, 24 Jul 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/f_auto,q_auto/lwj/blog/airplane-openai-comment-moderation-dashboard.jpg&quot; alt=&quot;Use AI to moderate abusive and vulgar comments (full tutorial)&quot; /&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://youtu.be/cdgj96bFXhs&quot;&gt;Watch on YouTube&lt;/a&gt;&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Want to jump to the end?&lt;/strong&gt; &lt;a href=&quot;https://github.com/learnwithjason/airplane-content-moderation&quot;&gt;Check out the source code on
GitHub.&lt;/a&gt;&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;Internal tools don&apos;t get prioritized. That&apos;s why so many developers have horror stories of teams running superadmin commands against the production database that they copy-pasted from a doc somewhere.&lt;/p&gt;
&lt;p&gt;In this tutorial, you&apos;ll &lt;strong&gt;learn how to build an internal dashboard to moderate a custom comments database.&lt;/strong&gt; You won&apos;t have to write much code, but everything you create will be stored as human-readable code that can be checked into source control and edited manually without breaking the low-code workflow.&lt;/p&gt;
&lt;p&gt;As an added bonus, we&apos;ll also look at how we can &lt;strong&gt;integrate AI (using OpenAI/Chat-GPT integrated into Airplane) to automatically flag the most abusive and vulgar comments&lt;/strong&gt;, decreasing the psychic damage taken by human moderators.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;Thanks to &lt;a href=&quot;https://lwj.dev/airplane&quot;&gt;Airplane&lt;/a&gt; for sponsoring this tutorial.
Go get a free account so you can follow along with this tutorial and start
using their developer platform for building internal tools quickly and with
minimal code.&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;Create a new Airplane project&lt;/h2&gt;
&lt;p&gt;To start, &lt;a href=&quot;https://lwj.dev/airplane&quot;&gt;register for or sign into your Airplane account&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Next, &lt;a href=&quot;https://docs.airplane.dev/platform/airplane-cli?utm_source=learnwithjason&amp;amp;utm_medium=tutorial&amp;amp;utm_campaign=content-moderation-dashboard&quot;&gt;install the Airplane CLI&lt;/a&gt; so you can develop locally:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;brew install airplanedev/tap/airplane
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Clone the starter repo, then move into it and start Airplane&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# clone the start branch of the repo
gh repo clone learnwithjason/airplane-content-moderation -- -b start

# move into the cloned app directory
cd airplane-content-moderation/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Inside, this app contains the boilerplate for a project — such as a &lt;code&gt;tsconfig.json&lt;/code&gt; and a &lt;code&gt;package.json&lt;/code&gt; — as well as a file called &lt;code&gt;airplane.yaml&lt;/code&gt;. The only thing inside this file is a note about the Node version (set to Node 18). This is a pretty cool feature of Airplane: there&apos;s very little boilerplate required.&lt;/p&gt;
&lt;p&gt;There&apos;s also an example app folder, which you can ignore for now. We&apos;ll come back to that after we&apos;ve got the Airplane tasks and views built.&lt;/p&gt;
&lt;p&gt;Inside the project folder, use the Airplane CLI to start the project.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# start Airplane in dev mode (will prompt for login on first run)
airplane dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first time you run this command, you&apos;ll be asked to log in.&lt;/p&gt;
&lt;p&gt;Next, the CLI will start the dev server and give you the option to press enter to open the Airplane Studio, which is a UI for local development that updates your local files in real time. Press enter to open the studio.&lt;/p&gt;
&lt;h2&gt;Set up the database and initial queries&lt;/h2&gt;
&lt;p&gt;Our first step will be to make sure we have data to work with and that we&apos;re able to read it out of our database in Airplane.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; Airplane provides a demo database we can use for this tutorial. In a
real project, you&apos;ll need to &lt;a href=&quot;https://docs.airplane.dev/resources/overview&quot;&gt;connect your own database as a
resource&lt;/a&gt;.&lt;/p&gt;&lt;/aside&gt;
&lt;h3&gt;Create a task to initialize the comments table&lt;/h3&gt;
&lt;p&gt;To begin, let&apos;s create a table to store comments and add a few entries so we can verify things are working as expected.&lt;/p&gt;
&lt;p&gt;To get the work done that our tutorial requires, we&apos;ll be using &lt;a href=&quot;https://docs.airplane.dev/tasks/overview&quot;&gt;Airplane tasks&lt;/a&gt;. These can take a few forms, but we&apos;ll &lt;a href=&quot;https://docs.airplane.dev/getting-started/sql&quot;&gt;start with a SQL task&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Create a new task in the Studio called &quot;comments_db_reset&quot; and choose the SQL option.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; You can call your tasks whatever you want. I&apos;ve chosen the format of
placing the affected data (comments) first, followed by what the task does.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;Once you create the task, two new files will be created in your working directory:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;comments_db_reset.sql&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;comments_db_reset.task.yaml&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It&apos;s possible to edit these files directly — we&apos;ll do that for a future task — but in many cases it&apos;s much more convenient to use the Studio UI.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:a8fffb/lwj/blog/airplane-01-studio-reset-db.jpg&quot; alt=&quot;the Airplane dashboard showing config for the reset comments database
task&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;In the Studio, update the details in the &quot;Define&quot; section:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Name: Reset comments database&lt;/li&gt;
&lt;li&gt;Description: Deletes the current comments table, including all comment entries, then creates a new comments table with a few seed entries.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The local files will update as you type.&lt;/p&gt;
&lt;p&gt;Next, scroll down to the &quot;Build&quot; section and choose &quot;[Demo DB]&quot; from the &quot;Database Resource&quot; dropdown.&lt;/p&gt;
&lt;p&gt;Under &quot;Query&quot;, add the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DROP TABLE comments;

CREATE TABLE IF NOT EXISTS
  comments (
    id      SERIAL PRIMARY KEY,
    comment TEXT NOT NULL,
    flagged BOOLEAN
  );

INSERT INTO
  comments (comment, flagged)
VALUES
  (&apos;this looks so delicious omg&apos;, false),
  (&apos;I think you suck&apos;, true),
  (&apos;I do not want to eat this&apos;, false),
  (&apos;eat poop&apos;, true)
;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a set of SQL instructions that removes the comments table, creates a new one with the necessary fields, and then inserts example entries into it that we can use to build out the rest of our dashboard.&lt;/p&gt;
&lt;p&gt;Our changes save as we type, so once everything is entered, we can click the &quot;Execute Task&quot; button in the top panel.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:a8fffb/lwj/blog/airplane-02-executed-successfully.jpg&quot; alt=&quot;the Airplane dashboard showing the result of the reset comments database
task
execution&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;That&apos;s the whole process for setting up Airplane and modifying a database. I love this flow because it feels magical, but nothing that happens is &quot;magic&quot; or hidden from me — everything I entered in the UI is stored in my code base now.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:a8fffb/lwj/blog/airplane-03-task-code-output.jpg&quot; alt=&quot;the generated YAML and SQL that makes up the reset comments database task
shown in VS
Code&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;The UI is a convenience, not a requirement — I can choose not to use it if I prefer.&lt;/p&gt;
&lt;h3&gt;Create a task to list all comments&lt;/h3&gt;
&lt;p&gt;Next, let&apos;s add another task to list all of our comments. Create a new task in the Studio, choose SQL, and name it &quot;comments_list_all&quot;. Add the following details in the &quot;Define&quot; section:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Name: List all comments&lt;/li&gt;
&lt;li&gt;Description: Lists all comments in the database, regardless of &lt;code&gt;flagged&lt;/code&gt; status&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Choose the demo DB from the database dropdown and add the following query:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT
  id,
  comment,
  flagged
FROM
  comments
ORDER BY
  flagged
;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Click the &quot;Execute Task&quot; button and you&apos;ll see the seed comments listed on the screen.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:a8fffb/lwj/blog/airplane-04-list-all-comments.jpg&quot; alt=&quot;the airplane dashboard showing results of the list all comments task in a
table&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;h3&gt;Create a task to list flagged comments&lt;/h3&gt;
&lt;p&gt;Next, repeat this process to create a task called &quot;comments_list_flagged&quot; to list only flagged comments using the following details:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Name: List flagged comments&lt;/li&gt;
&lt;li&gt;Description: List all comments that have been flagged as abusive or otherwise problematic.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Choose the demo DB and add the following query:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT
  id,
  comment,
  flagged
FROM
  comments
WHERE
  flagged = true
;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Create a task to list approved comments&lt;/h3&gt;
&lt;p&gt;We&apos;ll also need to be able to select all the unflagged (approved) comments. For these, we can copy-paste the &lt;code&gt;comments_list_flagged&lt;/code&gt; files and make small adjustments right in the code.&lt;/p&gt;
&lt;p&gt;Rename both files to &lt;code&gt;comments_list_approved&lt;/code&gt;, keeping their respective extensions. In &lt;code&gt;comments_list_approved.task.yaml&lt;/code&gt;, make the following edits:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	slug: comments_list_approved
	name: List approved comments
	description: List all comments that have been approved.
	sql:
	  resource: demo_db
	  entrypoint: comments_list_approved.sql
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, replace the contents of &lt;code&gt;comments_list_approved.sql&lt;/code&gt; with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT
  id,
  comment,
  flagged
FROM
  comments
WHERE
  flagged = false
;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Build a comment moderation dashboard view&lt;/h2&gt;
&lt;p&gt;So far, we&apos;ve only looked at tasks in Airplane. Next, let&apos;s dig into how &lt;a href=&quot;https://docs.airplane.dev/views/overview&quot;&gt;Airplane views&lt;/a&gt; work.&lt;/p&gt;
&lt;h3&gt;Create an Airplane view&lt;/h3&gt;
&lt;p&gt;In the Studio, create a new view by clicking the &lt;code&gt;+&lt;/code&gt; at the top of the explorer. Give it the following details:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Name: Comment Moderation Dashboard&lt;/li&gt;
&lt;li&gt;Description: Allows admins to see all approved comments, and optionally see flagged comments. They&apos;re also able to change the approved/flagged state of a comment and delete comments permanently.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This will create a new file in your local directory called &lt;code&gt;CommentModerationDashboard.airplane.tsx&lt;/code&gt; — you can rename this if you want, but we&apos;ll leave it as-is for this project.&lt;/p&gt;
&lt;h3&gt;Update the view with a table to display approved comments&lt;/h3&gt;
&lt;p&gt;Inside, you&apos;ll see an example component. Replace the file contents with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { Heading, Stack, Table } from &apos;@airplane/views&apos;;
import airplane from &apos;airplane&apos;;

const CommentModerationDashboard = () =&amp;gt; {
  return (
    &amp;lt;Stack&amp;gt;
      &amp;lt;Heading&amp;gt;Comment Moderation Dashboard&amp;lt;/Heading&amp;gt;

      &amp;lt;Table
        title=&quot;Approved Comments&quot;
        task=&quot;comments_list_approved&quot;
        defaultPageSize={20}
        hiddenColumns={[&apos;flagged&apos;]}
      /&amp;gt;
    &amp;lt;/Stack&amp;gt;
  );
};

export default airplane.view(
  {
    slug: &apos;comment_moderation_dashboard&apos;,
    name: &apos;Comment Moderation Dashboard&apos;,
    description:
      &quot;Allows admins to see all approved comments, and optionally see flagged comments. They&apos;re also able to change the approved/flagged state of a comment and delete comments permanently.&quot;,
  },
  CommentModerationDashboard
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.airplane.dev/views/components&quot;&gt;Airplane provides a suite of React UI components&lt;/a&gt; to make building dashboards as straightforward as snapping together components.&lt;/p&gt;
&lt;p&gt;A &lt;code&gt;Stack&lt;/code&gt; is a container for content, and inside we&apos;ve added a &lt;code&gt;Heading&lt;/code&gt; to let the viewer know what this dashboard is for, followed by a &lt;code&gt;Table&lt;/code&gt; to display approved comments.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;Table&lt;/code&gt; accepts a few props. The &lt;code&gt;title&lt;/code&gt; is displayed at the top, the &lt;code&gt;defaultPageSize&lt;/code&gt; tells the table how many rows to show before paginating, and &lt;code&gt;hiddenColumns&lt;/code&gt; lets us leave out columns from the table that we don&apos;t need.&lt;/p&gt;
&lt;p&gt;The really interesting prop here is the &lt;code&gt;task&lt;/code&gt; prop. &lt;a href=&quot;https://docs.airplane.dev/views/task-backed-components&quot;&gt;Many Airplane components can be task-backed&lt;/a&gt;, which means we can perform tasks (such as loading data or performing a query) using their slugs. This is a great productivity boost, because we don&apos;t have to mess with calling APIs to load data, then looping through them to build table views — we just say, &quot;Give me a table with the result of the task called &lt;code&gt;comments_list_approved&lt;/code&gt;&quot; and Airplane does the rest. Neat!&lt;/p&gt;
&lt;p&gt;Once we&apos;ve saved this component, we&apos;ll see a new icon pop up in the explorer for a view called &quot;Comment Moderation Dashboard&quot;. Click on it and you&apos;ll see the layout you just built, including the approved comment entries displayed in the table.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:a8fffb/lwj/blog/airplane-05-comment-moderation-step-1.jpg&quot; alt=&quot;the comment moderation dashboard in Airplane
Studio&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;h3&gt;Add another table to display flagged comments&lt;/h3&gt;
&lt;p&gt;We also need a way to see flagged comments, so add another &lt;code&gt;Table&lt;/code&gt; that uses the &lt;code&gt;comments_list_flagged&lt;/code&gt; task:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	import { Heading, Stack, Table } from &apos;@airplane/views&apos;;
	import airplane from &apos;airplane&apos;;

	const CommentModerationDashboard = () =&amp;gt; {
		return (
			&amp;lt;Stack&amp;gt;
				&amp;lt;Heading&amp;gt;Comment Moderation Dashboard&amp;lt;/Heading&amp;gt;

				&amp;lt;Table
					title=&quot;Approved Comments&quot;
					task=&quot;comments_list_approved&quot;
					defaultPageSize={20}
					hiddenColumns={[&apos;flagged&apos;]}
				/&amp;gt;
+
+				&amp;lt;Table
+					title=&quot;Flagged Comments&quot;
+					task=&quot;comments_list_flagged&quot;
+					defaultPageSize={20}
+					hiddenColumns={[&apos;flagged&apos;]}
+				/&amp;gt;
			&amp;lt;/Stack&amp;gt;
		);
	};

	export default airplane.view(
		{
			slug: &apos;comment_moderation_dashboard&apos;,
			name: &apos;Comment Moderation Dashboard&apos;,
			description:
				&quot;Allows admins to see all approved comments, and optionally see flagged comments. They&apos;re also able to change the approved/flagged state of a comment and delete comments permanently.&quot;,
		},
		CommentModerationDashboard,
	);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Save and the flagged comments appear, but this isn&apos;t ideal — we don&apos;t want to subject our admins to potentially abusive comments every time they load the dashboard. Instead, let&apos;s hide the flagged comments by default and only show them if the admin clicks a checkbox to confirm that they want to review flagged comments.&lt;/p&gt;
&lt;p&gt;Inside the view, let&apos;s add a &lt;code&gt;Checkbox&lt;/code&gt; from the Airplane component library, as well as the &lt;code&gt;useComponentState&lt;/code&gt; hook that will let us check whether it&apos;s checked or not:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	import {
+		type CheckboxState,
		Heading,
		Stack,
		Table,
+		Checkbox,
+		useComponentState,
	} from &apos;@airplane/views&apos;;
	import airplane from &apos;airplane&apos;;

	const CommentModerationDashboard = () =&amp;gt; {
+		const { id, checked } = useComponentState&amp;lt;CheckboxState&amp;gt;();

		return (
			&amp;lt;Stack&amp;gt;
				&amp;lt;Heading&amp;gt;Comment Moderation Dashboard&amp;lt;/Heading&amp;gt;

				&amp;lt;Table
					title=&quot;Approved Comments&quot;
					task=&quot;comments_list_approved&quot;
					defaultPageSize={20}
					hiddenColumns={[&apos;flagged&apos;]}
				/&amp;gt;

+				&amp;lt;Checkbox
+					id={id}
+					label=&quot;Show flagged comments (view at your own risk!)&quot;
+				/&amp;gt;
+
+				{checked ? (
					&amp;lt;Table
						title=&quot;Flagged Comments&quot;
						task=&quot;comments_list_flagged&quot;
						defaultPageSize={20}
						hiddenColumns={[&apos;flagged&apos;]}
					/&amp;gt;
+				) : null}
			&amp;lt;/Stack&amp;gt;
		);
	};

	export default airplane.view(
		{
			slug: &apos;comment_moderation_dashboard&apos;,
			name: &apos;Comment Moderation Dashboard&apos;,
			description:
				&quot;Allows admins to see all approved comments, and optionally see flagged comments. They&apos;re also able to change the approved/flagged state of a comment and delete comments permanently.&quot;,
		},
		CommentModerationDashboard,
	);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the dashboard hides the comments that could ruin someone&apos;s day by default, and they only have to be viewed if it becomes necessary to review them.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:a8fffb/lwj/blog/airplane-06-dashboard-flagged-hidden.jpg&quot; alt=&quot;the comment moderation dashboard with the checkbox unchecked and flagged
comments
hidden&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:a8fffb/lwj/blog/airplane-07-dashboard-flagged-visible.jpg&quot; alt=&quot;the comment moderation dashboard with the checkbox checked and flagged
comments
visible&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;h3&gt;Add a task to flag comments as abusive&lt;/h3&gt;
&lt;p&gt;If a comment is approved by mistake, we need the ability to manually flag it. To do that, create a new task called &quot;comment_flag&quot; with the following details:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Name: Flag comment as abusive&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Under parameters, click the &quot;Add parameter&quot; button and add the following values:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Name: id&lt;/li&gt;
&lt;li&gt;Description: The ID of the comment to flag.&lt;/li&gt;
&lt;li&gt;Type: Integer&lt;/li&gt;
&lt;li&gt;Required: true&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All the other values can remain unchanged.&lt;/p&gt;
&lt;p&gt;Click Update to save.&lt;/p&gt;
&lt;p&gt;Next, choose the demo DB as the database resource and add the following query:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;UPDATE
  comments
SET
  flagged = true
WHERE
  id = :id
;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally, set the query argument to be &quot;id&quot; and the value to be &lt;code&gt;{{params.id}}&lt;/code&gt;, which connects the parameter of the task to the query argument of this query.&lt;/p&gt;
&lt;p&gt;To test, grab an ID from one of the approved comments on the dashboard, enter it into the ID field of the &quot;Flag comment as abusive&quot; task, and click the &quot;Execute task&quot; button.&lt;/p&gt;
&lt;p&gt;The previously approved comment will now be flagged, which you can verify by visiting the dashboard again.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:a8fffb/lwj/blog/airplane-08-dashboard-flagged-comment.jpg&quot; alt=&quot;the comment moderation dashboard showing a manually flagged comment
correctly moved to the flagged
table&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;h3&gt;Add a task to unflag comments&lt;/h3&gt;
&lt;p&gt;Now, it&apos;s not against our site rules to dislike something, so that comment should be approved. Let&apos;s add another task to allow us to do that.&lt;/p&gt;
&lt;p&gt;Create a task called &quot;comment_approve&quot; with the following details:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Name: Approve comment&lt;/li&gt;
&lt;li&gt;Description: Approve a comment for public display.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Add a parameter called &lt;code&gt;id&lt;/code&gt; as an integer with the description, &quot;The ID of the comment to approve&quot;.&lt;/p&gt;
&lt;p&gt;Next, set the demo DB as the database resource and add this query:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;UPDATE
  comments
SET
  flagged = false
WHERE
  id = :id
;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For query arguments, add a new one called &lt;code&gt;id&lt;/code&gt; with the value of &lt;code&gt;{{params.id}}&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Use the same comment ID that you just flagged and execute the task. It will now be back on the approved list in the dashboard.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:a8fffb/lwj/blog/airplane-09-dashboard-comment-approved.jpg&quot; alt=&quot;the comment moderation dashboard showing the previously flagged comment back
in the approved table after manual
approval&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;h3&gt;Create a SQL task to delete comments&lt;/h3&gt;
&lt;p&gt;After an admin has reviewed a flagged comment to confirm that, yep, this comment is terrible, we want to let them delete it permanently — no reason for anyone else to have to see that trash.&lt;/p&gt;
&lt;p&gt;To do that, create a new task called &quot;comment_delete&quot; with the following details:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Name: Delete comment permanently&lt;/li&gt;
&lt;li&gt;Description: Removes a comment permanently. There is no undo for this action!&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Add a parameter called &lt;code&gt;id&lt;/code&gt; as an integer with the description, &quot;The ID of the comment to delete&quot;.&lt;/p&gt;
&lt;p&gt;Next, set the demo DB as the database resource and add this query:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DELETE FROM
  comments
WHERE
  id = :id
;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For query arguments, add a new one called &lt;code&gt;id&lt;/code&gt; with the value of &lt;code&gt;{{params.id}}&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Test this by adding a comment ID in the field and clicking &quot;execute task&quot;.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; Remember that you can always run the &quot;Reset comments database&quot; task
to revert the comments table to its starting state, so don&apos;t worry about
deleting things.&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;Call tasks from table rows in Airplane&lt;/h2&gt;
&lt;p&gt;At this point, what we&apos;ve built is already pretty useful. We can:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;View comments (both flagged and approved)&lt;/li&gt;
&lt;li&gt;Toggle the flagged status of comments&lt;/li&gt;
&lt;li&gt;Delete comments&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This &lt;em&gt;could&lt;/em&gt; be considered good enough. But we can make this much more user friendly with only a few more lines of code thanks to a built-in Airplane feature called &lt;a href=&quot;https://docs.airplane.dev/views/table#row-actions&quot;&gt;row actions&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Add a row action to flag or approve a comment&lt;/h3&gt;
&lt;p&gt;In Airplane &lt;code&gt;Table&lt;/code&gt; components, we can add a &lt;code&gt;rowActions&lt;/code&gt; prop that adds a button in each row and performs the specified action for the current row when clicked.&lt;/p&gt;
&lt;p&gt;There are a few ways to do this, up to and including fully custom solutions. For our needs, the &lt;a href=&quot;https://docs.airplane.dev/views/table#task-backed-row-actions&quot;&gt;task-backed row actions&lt;/a&gt; are perfect: they will automatically pass through the current comment&apos;s ID — we only need to provide the task to be performed and label for it!&lt;/p&gt;
&lt;p&gt;In &lt;code&gt;CommentModerationDashboard.airplane.ts&lt;/code&gt;, make the following changes:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	import {
		type CheckboxState,
		Heading,
		Stack,
		Table,
		Checkbox,
		useComponentState,
	} from &apos;@airplane/views&apos;;
	import airplane from &apos;airplane&apos;;

	const CommentModerationDashboard = () =&amp;gt; {
		const { id, checked } = useComponentState&amp;lt;CheckboxState&amp;gt;();

		return (
			&amp;lt;Stack&amp;gt;
				&amp;lt;Heading&amp;gt;Comment Moderation Dashboard&amp;lt;/Heading&amp;gt;

				&amp;lt;Table
					title=&quot;Approved Comments&quot;
					task=&quot;comments_list_approved&quot;
					defaultPageSize={20}
					hiddenColumns={[&apos;flagged&apos;]}
+					rowActions={{
+						slug: &apos;comment_flag&apos;,
+						label: &apos;flag&apos;,
+					}}
				/&amp;gt;

				&amp;lt;Checkbox
					id={id}
					label=&quot;Show flagged comments (view at your own risk!)&quot;
				/&amp;gt;

				{checked ? (
					&amp;lt;Table
						title=&quot;Flagged Comments&quot;
						task=&quot;comments_list_flagged&quot;
						defaultPageSize={20}
						hiddenColumns={[&apos;flagged&apos;]}
+						rowActions={[
+							{
+								slug: &apos;comment_approve&apos;,
+								label: &apos;approve&apos;,
+							},
+							{
+								slug: &apos;comment_delete&apos;,
+								label: &apos;delete&apos;,
+							},
+						]}
					/&amp;gt;
				) : null}
			&amp;lt;/Stack&amp;gt;
		);
	};

	export default airplane.view(
		{
			slug: &apos;comment_moderation_dashboard&apos;,
			name: &apos;Comment Moderation Dashboard&apos;,
			description:
				&quot;Allows admins to see all approved comments, and optionally see flagged comments. They&apos;re also able to change the approved/flagged state of a comment and delete comments permanently.&quot;,
		},
		CommentModerationDashboard,
	);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Save, refresh the studio, and your dashboard will now show the &quot;flag&quot; option on approved comments, and the &quot;approve&quot; and &quot;delete&quot; options on flagged comments.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:a8fffb/lwj/blog/airplane-10-row-actions.jpg&quot; alt=&quot;the comment moderation dashboard with row actions added to allow flagging,
approving, and deleting
comments&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;Test out the buttons to see row actions in, uh, action!&lt;/p&gt;
&lt;h3&gt;Refresh other tables when row actions are performed&lt;/h3&gt;
&lt;p&gt;You may have noticed that right now, the table that contains the updated row updates, but other tables require a page refresh to show the changes.&lt;/p&gt;
&lt;p&gt;Let&apos;s fix that.&lt;/p&gt;
&lt;p&gt;Airplane &lt;a href=&quot;https://docs.airplane.dev/views/calling-tasks-with-react-hooks#usetaskquery&quot;&gt;provides a hook called &lt;code&gt;useTaskQuery&lt;/code&gt;&lt;/a&gt; that lets us, among other things, force a refetch of the given task, causing all components using it to update.&lt;/p&gt;
&lt;p&gt;Make the following changes in &lt;code&gt;CommentModerationDashboard.airplane.ts&lt;/code&gt; to refetch all tables whenever a comment is modified:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	import {
		type CheckboxState,
		Heading,
		Stack,
		Table,
		Checkbox,
		useComponentState,
+		useTaskQuery,
	} from &apos;@airplane/views&apos;;
	import airplane from &apos;airplane&apos;;

	const CommentModerationDashboard = () =&amp;gt; {
		const { id, checked } = useComponentState&amp;lt;CheckboxState&amp;gt;();
+		const flagged = useTaskQuery(&apos;comments_list_flagged&apos;);
+		const approved = useTaskQuery(&apos;comments_list_approved&apos;);

		return (
			&amp;lt;Stack&amp;gt;
				&amp;lt;Heading&amp;gt;Comment Moderation Dashboard&amp;lt;/Heading&amp;gt;

				&amp;lt;Table
					title=&quot;Approved Comments&quot;
					task=&quot;comments_list_approved&quot;
					defaultPageSize={20}
					hiddenColumns={[&apos;flagged&apos;]}
					rowActions={{
						slug: &apos;comment_flag&apos;,
						label: &apos;flag&apos;,
+						onSuccess: () =&amp;gt; flagged.refetch(),
					}}
				/&amp;gt;

				&amp;lt;Checkbox
					id={id}
					label=&quot;Show flagged comments (view at your own risk!)&quot;
				/&amp;gt;

				{checked ? (
					&amp;lt;Table
						title=&quot;Flagged Comments&quot;
						task=&quot;comments_list_flagged&quot;
						defaultPageSize={20}
						hiddenColumns={[&apos;flagged&apos;]}
						rowActions={[
							{
								slug: &apos;comment_approve&apos;,
								label: &apos;approve&apos;,
+								onSuccess: () =&amp;gt; approved.refetch(),
							},
							{
								slug: &apos;comment_delete&apos;,
								label: &apos;delete&apos;,
							},
						]}
					/&amp;gt;
				) : null}
			&amp;lt;/Stack&amp;gt;
		);
	};

	export default airplane.view(
		{
			slug: &apos;comment_moderation_dashboard&apos;,
			name: &apos;Comment Moderation Dashboard&apos;,
			description:
				&quot;Allows admins to see all approved comments, and optionally see flagged comments. They&apos;re also able to change the approved/flagged state of a comment and delete comments permanently.&quot;,
		},
		CommentModerationDashboard,
	);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Save, then try again. Now both tables update whenever a comment is modified.&lt;/p&gt;
&lt;h2&gt;Create a TypeScript task to add a new comment&lt;/h2&gt;
&lt;p&gt;To this point, we&apos;ve been using only SQL tasks, but &lt;a href=&quot;https://docs.airplane.dev/tasks/overview&quot;&gt;Airplane also supports tasks written in JavaScript, Python, and more&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Create a new task and choose JavaScript as the type. Give it the name &quot;comment_add&quot; and leave TypeScript selected. Use the following details:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Name: Add a comment&lt;/li&gt;
&lt;li&gt;Description: Save a new comment in the database. Includes a step to check for abusive comments and flag them to limit problematic content from becoming visible.&lt;/li&gt;
&lt;li&gt;Parameters: &lt;code&gt;comment&lt;/code&gt;, type &quot;Long text&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Add the demo DB as a resource, then open &lt;code&gt;comment_add.airplane.ts&lt;/code&gt; in your editor. The second argument to &lt;code&gt;airplane.task&lt;/code&gt; is an async function, which contains everything you want the task to do when called.&lt;/p&gt;
&lt;p&gt;Replace the boilerplate function with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import airplane from &apos;airplane&apos;;

export default airplane.task(
  {
    slug: &apos;comment_add&apos;,
    name: &apos;Add a comment&apos;,
    description:
      &apos;Save a new comment in the database. Includes a step to check for abusive comments and flag them to limit problematic content from becoming visible.&apos;,
    parameters: {
      comment: {
        name: &apos;comment&apos;,
        type: &apos;shorttext&apos;,
      },
    },
    resources: [&apos;demo_db&apos;],
  },
  async (params) =&amp;gt; {
    const { comment } = params;

    // TODO add check for abusive content
    const output = { flagged: false };

    const res = await airplane.sql.query(
      &apos;demo_db&apos;,
      &apos;INSERT INTO comments (comment, flagged) VALUES (:comment, :flagged);&apos;,
      { args: { comment, flagged: output.flagged } }
    );

    console.log(res);

    let message = `Your comment was saved.`;

    return { message, flagged: output.flagged };
  }
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here&apos;s what this code does:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Get the &lt;code&gt;comment&lt;/code&gt; out of the &lt;code&gt;params&lt;/code&gt; so we can work with it&lt;/li&gt;
&lt;li&gt;For now, temporarily hard-code the output, which we&apos;ll build for real in the next section&lt;/li&gt;
&lt;li&gt;Use the built-in &lt;code&gt;airplane.sql.query&lt;/code&gt; method to run an &lt;code&gt;INSERT&lt;/code&gt; in our demo database to save the new comment along with its &lt;code&gt;flagged&lt;/code&gt; status&lt;/li&gt;
&lt;li&gt;Return a message and whether the comment was flagged&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Save, then write a new comment in the task&apos;s input in the Studio and execute the task. Check the dashboard to see your new comment saved.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:a8fffb/lwj/blog/airplane-11-add-comment.jpg&quot; alt=&quot;the comment moderation dashboard showing a newly created comment in the
approved
table&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;h3&gt;Use AI to automatically flag abusive comments&lt;/h3&gt;
&lt;p&gt;Moderation is not optional when we&apos;re opening up spaces for public comments. Flagging abusive content is a must if you want to have a space free of harassment and other unacceptable behavior.&lt;/p&gt;
&lt;p&gt;The challenges with moderation are enormous and complicated. We won&apos;t cover all of them in this post, but we will look at two:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The people who moderate content prevent us from seeing the most terrible things that are said in the comments section — but for them to do their jobs, they have to read that awful content. A moderator is forced to confront the absolute worst the web has to offer every day, and that takes its toll.&lt;/li&gt;
&lt;li&gt;Good moderation means not showing comments until they&apos;ve been checked. This adds a significant delay between posting a comment and seeing the comment live, which can prevent conversations from happening because it takes too long to see responses.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;To address these two challenges, one possible solution is using a &lt;a href=&quot;https://en.wikipedia.org/wiki/Large_language_model&quot;&gt;large language model (LLM)&lt;/a&gt; as a kind of &quot;first line defense&quot; for comment moderation.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; Before we continue, let me add a giant, flashing asterisk to this
section. &lt;strong&gt;AI is cool, but it has &lt;em&gt;huge&lt;/em&gt; unanswered ethical questions.&lt;/strong&gt; We
haven&apos;t figured out how to control for the bias of the people training the
models yet, and that means &lt;strong&gt;we need to be &lt;em&gt;extremely&lt;/em&gt; careful about how we
use AI&lt;/strong&gt; — and we need to make sure we&apos;re checking its work thoroughly.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;For this use case, we&apos;re attempting to catch abusive and hateful comments. We&apos;re also saving all comments for a final human review before deletion, which will allow us to adjust the instructions we&apos;re providing the LLM if it&apos;s incorrectly flagging comments.&lt;/p&gt;
&lt;h3&gt;Use Airplane&apos;s built-in AI functions to moderate user input&lt;/h3&gt;
&lt;p&gt;Airplane provides several built-in operations, including &lt;a href=&quot;https://docs.airplane.dev/tasks/builtin-ai&quot;&gt;AI support for both OpenAI and Anthropic&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;For our app, we&apos;ll use &lt;a href=&quot;https://openai.com/&quot;&gt;OpenAI&lt;/a&gt;, which currently provides $5 in credit to new accounts, which is more than enough to build and test this feature.&lt;/p&gt;
&lt;p&gt;Sign up or log in to your OpenAI account and &lt;a href=&quot;https://platform.openai.com/account/api-keys&quot;&gt;create an API key&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Next, head to the the Airplane Studio and create a config var (the icon that looks like &lt;code&gt;(x)&lt;/code&gt; in the left-hand sidebar). Name the config &lt;code&gt;OPENAI_API_KEY&lt;/code&gt; and paste in your OpenAI key.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; This value will be added to &lt;code&gt;airplane.dev.yaml&lt;/code&gt;! Remember not to
commit this file to avoid leaking your API key.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;Go back to your &quot;Add a comment&quot; task in the explorer and scroll down to the &quot;build&quot; section of the config. Under &quot;Environment variables&quot;, click &quot;add variable&quot;. Name it &lt;code&gt;OPENAI_API_KEY&lt;/code&gt;, then choose &quot;From config var&quot; from the dropdown. In the new dropdown that appears, choose the &lt;code&gt;OPENAI_API_KEY&lt;/code&gt; config.&lt;/p&gt;
&lt;p&gt;This will update the task file with a reference to the config var, which makes it safe to commit the task file (but, again, &lt;em&gt;do not&lt;/em&gt; commit &lt;code&gt;airplane.dev.yaml&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;With the API key available, modify &lt;code&gt;comment_add.airplane.ts&lt;/code&gt; to add the AI moderation step:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	import airplane from &apos;airplane&apos;;

	export default airplane.task(
		{
			slug: &apos;comment_add&apos;,
			name: &apos;Add a comment&apos;,
			description:
				&apos;Save a new comment in the database. Includes a step to check for abusive comments and flag them to limit problematic content from becoming visible.&apos;,
			parameters: {
				comment: {
					name: &apos;comment&apos;,
					type: &apos;shorttext&apos;,
				},
			},
			resources: [&apos;demo_db&apos;],
			envVars: {
				OPENAI_API_KEY: {
					config: &apos;OPENAI_API_KEY&apos;,
				},
			},
		},
		async (params) =&amp;gt; {
			const { comment } = params;

-			// TODO add check for abusive content
-			const output = { flagged: false };
+			const getSentiment = airplane.ai.func(
+				&apos;Identify abusive and vulgar comments. Negative opinions are allowed but personal attacks are not.&apos;,
+				[
+					{
+						input: &apos;This is the shit!&apos;,
+						output: { flagged: false, sentiment: &apos;positive&apos; },
+					},
+					{
+						input: &apos;You are stupid!&apos;,
+						output: { flagged: true, sentiment: &apos;negative&apos; },
+					},
+					{
+						input: &apos;Burgers are gross&apos;,
+						output: { flagged: false, sentiment: &apos;negative&apos; },
+					},
+				],
+			);
+
+			const { output, confidence } = await getSentiment(comment);
+
+			if (typeof output === &apos;string&apos;) {
+				return { message: &apos;unparseable input&apos; };
+			}

			const res = await airplane.sql.query(
				&apos;demo_db&apos;,
				&apos;INSERT INTO comments (comment, flagged) VALUES (:comment, :flagged);&apos;,
				{ args: { comment, flagged: output.flagged } },
			);

			console.log(res);

-			let message = `Your comment was saved.`;
+			let message = `Your comment was saved. The sentiment was read as ${output.sentiment}.`;
+			if (output.flagged === true &amp;amp;&amp;amp; confidence &amp;gt;= 0.75) {
+				message = &apos;Wow, you kiss your mother with that mouth?&apos;;
+			}

			return { message, flagged: output.flagged };
		},
	);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Save, then add an abusive comment to make sure it&apos;ll get caught (I recommend trying, &quot;you&apos;re a doofus&quot;).&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:a8fffb/lwj/blog/airplane-12-openai-content-moderation.jpg&quot; alt=&quot;the add a comment task showing a result that was flagged, including the
custom message we coded
earlier&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;Less than 25 lines of code, and we&apos;ve got a pretty okay auto-moderation flow in place — that&apos;s pretty impressive!&lt;/p&gt;
&lt;p&gt;And again: remember that this is not a &lt;em&gt;replacement&lt;/em&gt; for human moderation. It&apos;s a tool that can help the moderators work more effectively.&lt;/p&gt;
&lt;h2&gt;Deploy your Airplane dashboard&lt;/h2&gt;
&lt;p&gt;So far we&apos;ve only been working locally. To make this dashboard available to your team, you&apos;ll need to deploy it.&lt;/p&gt;
&lt;h3&gt;Add config vars for production&lt;/h3&gt;
&lt;p&gt;For your tasks to work properly, you&apos;ll need to &lt;a href=&quot;https://app.airplane.dev/settings/config-vars&quot;&gt;add a production config var&lt;/a&gt; with your OpenAI API key. Name it &lt;code&gt;OPENAI_API_KEY&lt;/code&gt; — save it and you&apos;re all set.&lt;/p&gt;
&lt;h3&gt;Deploy Airplane tasks and views to production&lt;/h3&gt;
&lt;p&gt;To deploy, open the terminal and run the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;airplane deploy
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once the deployment completes, head to https://app.airplane.dev and check the library to see your deployed tasks and views.&lt;/p&gt;
&lt;h2&gt;Try the demo app to see the comment moderation flow in action&lt;/h2&gt;
&lt;p&gt;To provide a way to test the moderation workflow, the demo app for this project &lt;a href=&quot;https://docs.airplane.dev/api/tasks&quot;&gt;executes Airplane tasks directly via API&lt;/a&gt;. You could also set up a &lt;a href=&quot;https://docs.airplane.dev/tasks/webhooks&quot;&gt;webhook&lt;/a&gt; to run the moderation flow in the background after a comment is added to your production database.&lt;/p&gt;
&lt;p&gt;We won&apos;t go through exactly how to build this demo app, but the overview is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It&apos;s an Astro site&lt;/li&gt;
&lt;li&gt;The site runs in hybrid mode so it can process form submissions&lt;/li&gt;
&lt;li&gt;Approved comments are loaded by &lt;a href=&quot;https://docs.airplane.dev/api/tasks#tasks-execute&quot;&gt;executing&lt;/a&gt; the &lt;code&gt;comments_list_approved&lt;/code&gt; task and &lt;a href=&quot;https://docs.airplane.dev/api/runs#runs-getOutputs&quot;&gt;getting its outputs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;New comments are created by executing the &lt;code&gt;comment_add&lt;/code&gt; task and returning its outputs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To see the source code, &lt;a href=&quot;https://github.com/learnwithjason/airplane-content-moderation/tree/main/example-app&quot;&gt;check out the GitHub repo&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;For a user on the site, submitting a comment will take a few seconds and then immediate feedback will be sent: either the comment is approved and visible immediately, or it&apos;s flagged and the comment form will chide the user for posting abusive or vulgar comments.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:a8fffb/lwj/blog/airplane-13-comment-approved.jpg&quot; alt=&quot;two frame flow of an acceptable comment being posted and receiving a
confirmation
message&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:a8fffb/lwj/blog/airplane-14-comment-flagged.jpg&quot; alt=&quot;two frame flow of an unacceptable comment that is submitted, but not
displayed. instead, the user sees a message letting them know the comment was
not
posted&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;Congratulations! You&apos;ve built a complete comment moderation dashboard, including a first line of defense against the most vulgar and abusive comments powered by OpenAI.&lt;/p&gt;
&lt;h2&gt;Links and additional resources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Source code: https://github.com/learnwithjason/airplane-content-moderation&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/learnwithjason/airplane-content-moderation&quot;&gt;Get started with Airplane&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;More information on the &lt;a href=&quot;https://hai.stanford.edu/news/timnit-gebru-ethical-ai-requires-institutional-and-structural-change&quot;&gt;ethical concerns around AI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;A reminder that &lt;a href=&quot;https://www.scientificamerican.com/article/racial-bias-found-in-a-major-health-care-risk-algorithm/&quot;&gt;biases in AI can quite literally kill people&lt;/a&gt; if we don’t actively check the results&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Clean as you go</title><link>https://codetv.dev/blog/clean-as-you-go/</link><guid isPermaLink="true">https://codetv.dev/blog/clean-as-you-go/</guid><description>A story about a habit I picked up working in restaurants, and how the lesson I learned still helps me write better software to this day.
</description><pubDate>Mon, 10 Jul 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/f_auto,q_auto/lwj/blog/clean-as-you-go.jpg&quot; alt=&quot;Clean as you go&quot; /&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://youtu.be/a7_kghlRfeI&quot;&gt;Watch on YouTube&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I learned to cook in a restaurant. I was a prep cook.&lt;/p&gt;
&lt;p&gt;Every morning, I clocked in to stacks of produce I needed to have ready before the lunch rush. Every morning, I grabbed a cutting board and a knife, opened the first box of romaine lettuce, and got to chopping. And every morning, I&apos;d make an &lt;em&gt;enormous&lt;/em&gt; mess.&lt;/p&gt;
&lt;p&gt;As I chopped, I pushed the scraps aside into a pile to deal with them later. Cleaning, after all, happened &lt;em&gt;after&lt;/em&gt; I finished prep.&lt;/p&gt;
&lt;h2&gt;Big messes require big efforts to clean&lt;/h2&gt;
&lt;p&gt;But the scraps piled up, and my workspace got tighter and tighter. Onion skins stuck to the tomatoes; remnants of diced parsley speckled the cucumbers. My scrap pile got so out of control that I&apos;d occasionally end up dumping handfuls of veggie waste onto the floor by mistake.&lt;/p&gt;
&lt;p&gt;Working around my mess made prep stressful, but that paled in comparison to the cleanup. My prep technique left the kitchen looking like the aftermath of a food fight. It took me so long to clean up that I had to rush to avoid delaying the lunch shift from getting started. (There should have been an hour or so of downtime.)&lt;/p&gt;
&lt;p&gt;The more experienced staff was horrified by this.&lt;/p&gt;
&lt;p&gt;And, fortunately for me, they decided to help me instead of just firing me immediately.&lt;/p&gt;
&lt;h2&gt;Clean as you go to avoid big messes&lt;/h2&gt;
&lt;p&gt;One of the cooks very patiently introduced me to the concept of cleaning as you go. They had a bench scraper and a bleach bucket standing by, and positioned the trash can right next to the table. As I chopped, they encouraged me to move the scraps directly to the trash instead of making a pile. When I finished an ingredient, I grabbed the bench scraper to clear away all the left behind bits, then gave a quick wipe with a rag to reset my station to clean before starting the next one.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;If you cook, I can&apos;t recommend getting a &lt;a href=&quot;https://www.amazon.com/OXO-Multi-purpose-Stainless-Scraper-Chopper/dp/B00004OCNJ/&quot;&gt;bench
scraper&lt;/a&gt;
enough. Use it to scoop ingredients off the cutting board instead of using
your knife (and potentially dulling it), get all the bits off your flat
surfaces, cut and measure dough — it&apos;s a versatile tool that I&apos;ve noticed
people don&apos;t tend to know about unless they&apos;ve worked in restaurant kitchens
before.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;At first, I resisted this. Cleaning at each step &lt;em&gt;had&lt;/em&gt; to be slower, right? After all, &lt;a href=&quot;https://www.jason.energy/context-switching/&quot;&gt;context switching is a terrible idea&lt;/a&gt;, and this was clearly going to make me even slower for sure.&lt;/p&gt;
&lt;p&gt;But my options were to do it their way or get fired, so I stuck to it. And a few things happened:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The quality of my work went up. No more bits of the previous veggie showing up in the next one. And because my workspace was less cramped, I could see what I was doing and make fine adjustments more easily.&lt;/li&gt;
&lt;li&gt;The prep itself went faster. Starting with a clean station made it faster to do the prep because I wasn&apos;t working around previous mess.&lt;/li&gt;
&lt;li&gt;Cleanup went faster. By cleaning as I went, cleaning was as simple as a few swipes with a bench scraper and a quick wipe-down with a rag. Nothing piled up, so my last veggie was as easy to clean up after as the first one.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I wasn&apos;t rushing to get everything done and cleaned anymore. Instead of the cooks worrying about me, they started giving me additional training and opportunities.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;It felt counterintuitive, but cleaning as I went had made me both &lt;em&gt;better&lt;/em&gt; and &lt;em&gt;faster&lt;/em&gt; at my job.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;Isn&apos;t stopping to clean slower?&lt;/h2&gt;
&lt;p&gt;My knee-jerk reaction to cleaning as I went was to condemn it as multitasking, which makes work slower. But... was it?&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; I&apos;m taking a bit of artistic license with timelines to connect this
story. I learned about cleaning as you go in my teens; context switching
didn&apos;t formally enter my awareness until a decade or so later. With the
benefit of hindsight, I &lt;em&gt;think&lt;/em&gt; my initial distaste for cleaning as you go was
some vague sense of &quot;it&apos;ll take longer!&quot; — but honestly I was a teenager so I
may have just been against it because I was acting like a sulky prick.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;&lt;strong&gt;Cleaning as you go &lt;em&gt;seems&lt;/em&gt; like multitasking, but it&apos;s actually an efficient way of sequentially tackling work.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;Small messes clean up fast&lt;/h2&gt;
&lt;p&gt;By &lt;em&gt;not&lt;/em&gt; cleaning as we go, multiple messes compound on each other. A small mess is fast to clean, but a dozen or more small messes become a much bigger mess that&apos;s harder to clean up.&lt;/p&gt;
&lt;p&gt;In food prep, the scrap pile might spill off the table. Wet scraps might dry to the table, making them far more difficult to clean up. A large pile has a tendency to spread out and get additional dishes and utensils dirty.&lt;/p&gt;
&lt;p&gt;By contrast, a small mess is almost always a couple quick wipes away from being clean.&lt;/p&gt;
&lt;h2&gt;&quot;Clean as you go&quot; as a guiding principle for everything&lt;/h2&gt;
&lt;p&gt;This lesson didn&apos;t just save my high school job — it made a huge impression on my outlook on life. I try to clean as I go wherever I can these days, whether I&apos;m cooking dinner at home or writing code at work.&lt;/p&gt;
&lt;p&gt;In a codebase, we always make small messes as we work. We&apos;re solving problems, and until we hit a solution that works our code is full of small choices based on educated guesses that are &lt;em&gt;directionally&lt;/em&gt; correct, but that probably wander a bit since they lack the full context of the working solution.&lt;/p&gt;
&lt;p&gt;If we ship the code as-is, everything will be fine. The code works, and that&apos;s ultimately the point.&lt;/p&gt;
&lt;p&gt;But we left a small mess. And if we do that every time we build a new feature, the small messes start to build into a big mess — this is how we end up with tech debt that&apos;s so overwhelming that we start to argue that the only reasonable solution is to start over from scratch.&lt;/p&gt;
&lt;p&gt;If, instead, we choose to clean as we go — to do an immediate small refactor as part of submitting the PR, for example — we&apos;ll clean up those small messes &lt;em&gt;before&lt;/em&gt; they can pile up into big ones. And because the messes are being continually addressed, we have cleaner, easier to manage code, and that makes us faster.&lt;/p&gt;
&lt;h2&gt;Building new habits is hard, but worth it&lt;/h2&gt;
&lt;p&gt;Cleaning as you go is a tricky habit to build. It requires a concentrated effort to change the way you work, and if you&apos;re in a team it&apos;s even trickier because you&apos;re trying to shift the team culture.&lt;/p&gt;
&lt;p&gt;However, if you put in the work to build the habit, it pays dividends in every area of your life where you choose to adopt it.&lt;/p&gt;</content:encoded></item><item><title>It’s not worth switching tech stacks</title><link>https://codetv.dev/blog/switching-tech-stacks/</link><guid isPermaLink="true">https://codetv.dev/blog/switching-tech-stacks/</guid><description>We spend a lot of time online getting told our tech stack is wrong/bad/outdated/whatever. Ignore the nerds — it’s (probably) not worth switching.
</description><pubDate>Tue, 04 Jul 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/f_auto,q_auto/lwj/blog/frameworks-dont-matter.jpg&quot; alt=&quot;It’s not worth switching tech stacks&quot; /&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://youtu.be/u0j-DlsimZ4&quot;&gt;Watch on YouTube&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The JavaScript landscape is noisy. And it can be pushy. We hear a &lt;em&gt;lot&lt;/em&gt; of opinions about what we should and shouldn&apos;t be using to build websites.&lt;/p&gt;
&lt;p&gt;How do you choose when just about every JavaScript think piece contradicts the other advice you&apos;ve seen and also insists that you &lt;em&gt;need&lt;/em&gt; to learn the hot new thing?&lt;/p&gt;
&lt;p&gt;My hot take is that &lt;strong&gt;a lot of developers tend to switch to new stacks as a way of kicking the can down the road on building features that users actually want and care about&lt;/strong&gt; — so the best tech stack is the one you can build something useful in today.&lt;/p&gt;
&lt;h2&gt;We&apos;re just building websites here.&lt;/h2&gt;
&lt;p&gt;The fastest website you can build is HTML and CSS on a global CDN. Anything else we add to that site is going to slow it down — so when we&apos;re going to add extra steps, we need to make sure the trade-offs are actually worth it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ultimately, the technology we choose to build websites doesn&apos;t matter.&lt;/strong&gt; There are a huge number of thriving businesses that make eye-watering profits, and very few are using bleeding edge web frameworks to ship.&lt;/p&gt;
&lt;p&gt;In fact, most of them ship jQuery.&lt;/p&gt;
&lt;h2&gt;jQuery still powers 77% of websites.&lt;/h2&gt;
&lt;p&gt;Inside the tech hype bubbles, jQuery is DEAD-dead. We talk about it as if it&apos;s been fully abandoned for years and no one ever uses it anymore.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;In reality, &lt;a href=&quot;https://w3techs.com/technologies/overview/javascript_library&quot;&gt;3 of every 4 websites on the internet still ship jQuery&lt;/a&gt;.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Could you build a great website with jQuery today? You can! And people do!&lt;/p&gt;
&lt;h2&gt;Social media hype bubbles are small.&lt;/h2&gt;
&lt;p&gt;In online tech circles like Twitter, YouTube, Hacker News, Reddit, and their ilk, we can end up with a skewed perception of what the world of web dev actually looks like. A relatively tiny number of tech influencers — like me! — spend a lot of time discussing the bleeding edge of tech. We make it sound like &lt;em&gt;everyone&lt;/em&gt; is doing the new thing, and &lt;em&gt;everyone&lt;/em&gt; has left behind the old things.&lt;/p&gt;
&lt;p&gt;But that&apos;s not reality. That&apos;s just me and a bunch of nerds like me who are &lt;em&gt;extremely into web tech&lt;/em&gt; talking about our hobbies. Sure, we&apos;re using this bleeding edge stuff in production, but we&apos;re the &lt;em&gt;exception&lt;/em&gt;, not the rule.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The &lt;a href=&quot;https://www.ncbi.nlm.nih.gov/pmc/articles/PMC8283615/&quot;&gt;FoMO&lt;/a&gt; factory of social media remains a menace to actually shipping things.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;If you build something cool, the tools are irrelevant.&lt;/h2&gt;
&lt;p&gt;If you build a web app that people enjoy using and are willing to pay for, it doesn&apos;t really matter what you used to build it. What matters is that you can get the idea out of your head and into a working app. It doesn&apos;t matter if you built it with Java, .Net, Node, Ruby on Rails, or Dreamweaver.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Don&apos;t let someone who speaks authoritatively on Twitter make you feel bad about what you build with. What matters is that you built something.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;When does it make sense to switch?&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;When there&apos;s a valid reason to rebuild an entire app, it&apos;ll be fairly obvious and compelling.&lt;/strong&gt; Rebuilding an intranet that was pinned to IE6 has a host of compelling security and feature reasons to make the switch.&lt;/p&gt;
&lt;p&gt;Shaving 50 milliseconds off a page load by spending months switching from Node to Rust, though? It just doesn&apos;t make enough of an impact to be worth the cost.&lt;/p&gt;
&lt;h2&gt;The best tech stack is whatever you&apos;re shipping with.&lt;/h2&gt;
&lt;p&gt;Technology is a tool, and it only matters as a means of delivering ideas to people who will use, enjoy, and (hopefully) pay for the things we build with it.&lt;/p&gt;
&lt;p&gt;Outside of that, additional attention spent on tools is a hobby unless your job is building tools. Hobbies are wonderful and they move the web forward and we need folks on the bleeding edge to experiment, but &lt;strong&gt;you bear no obligation to change the way you build for the web just because it seems like your current stack is no longer trendy.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;We&apos;re in this field to solve real problems for real people. The rest is details.&lt;/p&gt;</content:encoded></item><item><title>How to make $200K+ as a dev</title><link>https://codetv.dev/blog/how-to-make-200k-as-a-dev/</link><guid isPermaLink="true">https://codetv.dev/blog/how-to-make-200k-as-a-dev/</guid><description>Moving up the career ladder and into higher salary bands as an engineer requires growth. But it’s less about code than you might think.
</description><pubDate>Mon, 26 Jun 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/f_auto,q_auto/lwj/blog/make-200k-as-a-dev.jpg&quot; alt=&quot;How to make $200K+ as a dev&quot; /&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://youtu.be/xzAwme2nlaY&quot;&gt;Watch on YouTube&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;As you advance in your career as a developer, the key to reaching the next level of influence, leadership, and salary shifts away from improving technical skills. What makes a senior engineer qualified for staff, principal, and beyond? Interpersonal, leadership, and problem-solving skills.&lt;/p&gt;
&lt;h2&gt;Senior-plus salary bands expect you to build teams, not code.&lt;/h2&gt;
&lt;p&gt;Hitting senior software developer means you should be capable of handling just about any technical task thrown your way. Advancing to a senior-plus level requires making an impact that&apos;s bigger than what you can code on your own.&lt;/p&gt;
&lt;p&gt;As a senior-plus dev, your impact isn&apos;t that you can code faster. Your impact is that you can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Break concepts down in a way that helps less experienced engineers move more quickly&lt;/li&gt;
&lt;li&gt;Convey context clearly and cross-functionally&lt;/li&gt;
&lt;li&gt;Communicate with stakeholders in language that they understand&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A senior-plus dev has to be capable of more than coding up something impressive — you need to be able to understand what to build (and why), then communicate with teammates and leadership well enough that they agree and support your insight.&lt;/p&gt;
&lt;h2&gt;Learn empathy for other departments — and how to communicate with them.&lt;/h2&gt;
&lt;p&gt;Learn the motivations and pain points of sales, marketing, product, and other departments in your organization. Know their metrics, objectives, and challenges. By understanding where they&apos;re coming from, you can reframe proposals in the context of their goals.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Sales needs new features and value adds that they can bring up in sales meetings so they can hit their sales goals&lt;/li&gt;
&lt;li&gt;Marketing is on the hook to launch things and drive conversation, interest, and new signups&lt;/li&gt;
&lt;li&gt;Product is under pressure to increase activation, retention, and performance metrics&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Instead of trying to convince these teams that they should care about the underlying tech involved, start thinking about how different engineering choices will impact all of these metrics, both now and in the future — it completely changes the conversations and allows you to wield much greater influence.&lt;/p&gt;
&lt;p&gt;The ability to communicate empathetically and persuasively to teammates and leaders in every department is a strong indicator of a senior-plus developer.&lt;/p&gt;
&lt;h2&gt;Become the stabilizer and foundation for your team.&lt;/h2&gt;
&lt;p&gt;A vital part of a senior-plus developer&apos;s role is to provide clarity to their team regarding the tasks and goals at hand. When the team is confident in the direction and deliverables, they can focus on being productive and efficient.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Support your manager in understanding project goals and specifications.&lt;/li&gt;
&lt;li&gt;Collaborate with other departments to ensure they know what they are asking for and that their requirements are complete.&lt;/li&gt;
&lt;li&gt;Consider user experience and user flow early in the project to prevent costly surprises midway through.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;At every step, you&apos;re not only thinking of the technical implementation, but also how the tech fits into the company&apos;s goal and long-term success.&lt;/p&gt;
&lt;h3&gt;What you really build as a senior-plus dev is a high-performing team.&lt;/h3&gt;
&lt;p&gt;A team that gels well together and has confidence in their work is a sign of strong leadership and effective communication. Senior-plus developers and leaders should focus on clarity and alignment across all teams, speaking the same language, and working towards the same outcomes.&lt;/p&gt;
&lt;p&gt;Reaching the next salary band requires growing your influence and leadership skills. Excellence as a coder is a requirement, but the higher levels require stronger communication and team-mindedness. By demonstrating that you understand the needs of stakeholders, that you can communicate effectively, and that you&apos;re focused not just on your own output, but on your entire team&apos;s success, you signal to leadership at your company that you&apos;re ready for your next promotion and raise.&lt;/p&gt;
&lt;h2&gt;Resources and further reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.levels.fyi/t/software-engineer&quot;&gt;Salary info on levels.fyi&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://career-ladders.dev/&quot;&gt;Career ladder info&lt;/a&gt; by Sarah Drasner&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>What you need to know about getting hired in devrel</title><link>https://codetv.dev/blog/get-hired-devrel/</link><guid isPermaLink="true">https://codetv.dev/blog/get-hired-devrel/</guid><description>Getting into developer relations (&quot;devrel&quot; for short) is tricky for a lot of reasons. The biggest is that no one seems to know what it actually is.
</description><pubDate>Tue, 20 Jun 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/f_auto,q_auto/lwj/blog/get-hired-in-devrel.jpg&quot; alt=&quot;What you need to know about getting hired in devrel&quot; /&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;I have yet to see a job description for devrel that matches what someone actually does in their day-to-day work.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/3X-EUEOg638&quot;&gt;Watch on YouTube&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Part of the problem is that a lot of devrel teams are built by someone who&apos;s had success in their own devrel career and is building a team around their own history and preferences. But most of the problem is that companies have an extremely poor understanding of what devrel actually means, so they tend to make it mean anything.&lt;/p&gt;
&lt;h2&gt;An incomplete list of all the different jobs I&apos;ve seen described as devrel.&lt;/h2&gt;
&lt;p&gt;I&apos;ve seen &quot;developer relations&quot; used to describe a wide range of entirely different roles and skillsets:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Community building&lt;/strong&gt; — creating spaces and systems for others to succeed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Content creation&lt;/strong&gt; — tutorials, videos, demos, etc.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Documentation&lt;/strong&gt; — a different kind of writing entirely&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sales support&lt;/strong&gt; — talk to prospective customers (usually their engineering teams)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Product support&lt;/strong&gt; — relaying community feedback for planning and prioritization&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Engineering support&lt;/strong&gt; — acting as &quot;User Zero&quot; for new APIs and features to give early feedback&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Marketing support&lt;/strong&gt; — helping shape the messaging and positioning of the company&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;R&amp;amp;D support&lt;/strong&gt; — prototyping and validation of wild ideas&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&quot;Being famous&quot;&lt;/strong&gt; — sometimes a company just wants to be associated with a well-known name (I have feelings about this, but that&apos;s for another time)&lt;/li&gt;
&lt;li&gt;...and this isn&apos;t even a complete list — it&apos;s just what I can remember about the work I did in my own devrel roles&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;None of these skills are necessarily required to get into devrel — but depending on how a given company understands devrel, they might be.&lt;/p&gt;
&lt;h2&gt;Companies don&apos;t think about devrel strategy — so you have to.&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Every company has a complex set of strengths, weaknesses, and needs. Devrel will take a different shape in each company.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If the company&apos;s leadership isn&apos;t comfortable going out in public and talking about the company&apos;s vision and product&apos;s value, devrel will probably need to tackle that. They&apos;ll be looking for someone to get up on stages and get a crowd hyped on your company&apos;s promised future.&lt;/p&gt;
&lt;p&gt;If the company doesn&apos;t have a Documentation team, devrel might get tapped for that.&lt;/p&gt;
&lt;p&gt;If the company is weak on Marketing, devrel can fill in for a while. Ditto for Product, Sales, Support, and a host of other roles.&lt;/p&gt;
&lt;h2&gt;Devrel is organizational duct tape.&lt;/h2&gt;
&lt;p&gt;Devrel has a tendency to become organizational duct tape. It&apos;s a role that attracts generalists who — while they can&apos;t do everything — are typically capable of handling &lt;em&gt;some&lt;/em&gt; of the tasks of many roles. This partial coverage can help companies handle shifting needs for a short time while they figure out more permanent solutions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The adaptability of devrel is one of its greatest strengths. But it&apos;s also one of the biggest challenges when joining a new company.&lt;/strong&gt; If the company hasn&apos;t really thought through how devrel fits into the organization, it&apos;s incredibly hard to predict what kind of tasks you&apos;re signing up for.&lt;/p&gt;
&lt;h2&gt;Ask the right questions so you don&apos;t end up with unpleasant surprises.&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;When you&apos;re interviewing, ask the company what the daily tasks for devrel are.&lt;/strong&gt; Make sure they expect you to do the things you want to do. And if they have no idea, you can make the choice on whether you want to do the work to shape the devrel team at this company — or whether you&apos;d prefer to look elsewhere for a team with a clearer idea of what success looks like in the role.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;I&apos;ve watched extremely talented people take devrel jobs, only to leave the job within their first year — often with a lot of frustration on both sides of the relationship.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If you&apos;re deeply technical and your goal in devrel is to be heavily engaged with the Engineering team, building demos, and workshopping APIs to ensure the developer experience is top-notch, that needs to be what the company is looking for in their devrel team.&lt;/p&gt;
&lt;p&gt;Otherwise, you might think devrel means digging into the tech, and the company might think it means writing two blog posts per week. Without &lt;a href=&quot;https://jason.energy/setting-expectations&quot;&gt;setting clear expectations&lt;/a&gt;, there could be mismatched goals — and you&apos;ll end up leaving the job unhappy.&lt;/p&gt;
&lt;h2&gt;Know what drives you — and what doesn&apos;t.&lt;/h2&gt;
&lt;p&gt;If you&apos;re looking to get into devrel, it&apos;s really important to understand all the different facets of what devrel can be and to make sure you know which ones you enjoy.&lt;/p&gt;
&lt;p&gt;For example, if you love writing and creating tutorials, make sure that&apos;s what the devrel team does. There might be a different team with technical writers — and you may want to apply for that role instead.&lt;/p&gt;
&lt;p&gt;If you want to go to conferences and give talks on stage, make sure that&apos;s what the company expects out of devrel. Especially today, with budgets tightening and conferences struggling to get attendees back after COVID put in-person events on hold, not every devrel team is going to have a travel budget to send you to conferences.&lt;/p&gt;
&lt;p&gt;Ultimately, you need to ask yourself: &lt;strong&gt;How do I create the role that gives me energy that makes me want to come to work that I&apos;m excited about doing?&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;Devrel is mostly a vibe — get the job details in writing.&lt;/h2&gt;
&lt;p&gt;Devrel is too new as a role to have any kind of standardized job description. &lt;strong&gt;Every company will define, measure, and prioritize devrel tasks differently.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I know folks who work full time in devrel at two different companies. In one, their job is exclusively conferences: they go to events, work the booth, and generate leads. The other spends exclusively works on writing tutorials, blog posts, and demo apps. Both teams have &quot;Developer Relations&quot; as a title, but the Venn diagram of their actual work is two separate circles.&lt;/p&gt;
&lt;p&gt;They&apos;re completely different jobs, despite having the same job title.&lt;/p&gt;
&lt;h2&gt;The Island of Misfit Toys&lt;/h2&gt;
&lt;p&gt;I often joke that the devrel is the Island of Misfit Toys. &lt;strong&gt;Our job in devrel is to understand the gaps and weaknesses in a company, and then fill those gaps.&lt;/strong&gt; That might mean that we&apos;re working on the product one day, then working in engineering the next. Other days, you&apos;re working on a marketing campaign or writing docs.&lt;/p&gt;
&lt;p&gt;All of these tasks fit under the devrel umbrella.&lt;/p&gt;
&lt;p&gt;But remember: depending on the company and the company&apos;s maturity (and who the company has already hired), you might never do any of those things — whether or not you want to.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;If you want to land a devrel job you&apos;ll enjoy and thrive in, you need to write your own ideal job description — then use it to filter the companies until you find one who defines the role the same way.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;Getting your first devrel job is hard, but you can give yourself an advantage.&lt;/h2&gt;
&lt;p&gt;Breaking into devrel is tough, especially if you don&apos;t already have a platform. However, having a clear understanding of the broad spectrum of what devrel can be, what your strengths and goals are, and how all that fits into the company&apos;s strategy puts you head and shoulders above most candidates.&lt;/p&gt;
&lt;p&gt;Most candidates aren&apos;t thinking about this.&lt;/p&gt;
&lt;p&gt;Most &lt;em&gt;companies&lt;/em&gt; aren&apos;t thinking about this.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Devrel is not about being an niche internet microcelebrity. You&apos;re there to do work, and you&apos;re there to provide value to the company.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;Remember: if you want to be in devrel, do devrel.&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://jason.energy/more-pie&quot;&gt;&lt;strong&gt;A career is a pie-eating contest where the prize for winning is more pie.&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If you want to do a thing, do that thing! If you show somebody that you&apos;re good at it, they will reward you by offering you more of that work.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;If you show that you understand the space and put out work you want to get more of, you&apos;ll eventually land that job.&lt;/strong&gt; Lots of companies will make accommodations to allow you to write or speak about your work at the company. It&apos;s extra press for them, after all.&lt;/p&gt;
&lt;p&gt;And remember: it will take time. &lt;a href=&quot;https://jason.energy/constant-gentle-pressure&quot;&gt;Keep applying constant, gentle pressure&lt;/a&gt; and you&apos;ll get the job. 💜&lt;/p&gt;</content:encoded></item><item><title>How I balance tech debt vs. feature development</title><link>https://codetv.dev/blog/tech-debt-vs-features/</link><guid isPermaLink="true">https://codetv.dev/blog/tech-debt-vs-features/</guid><description>Treating velocity and maintainability as separate concerns is a mistake. Your shipping velocity is directly correlated to how maintainable your code is.
</description><pubDate>Tue, 13 Jun 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/f_auto,q_auto/v1683734135/lwj/blog/tech-debt-vs-features.jpg&quot; alt=&quot;How I balance tech debt vs. feature development&quot; /&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://youtu.be/VWCWZO62FUw&quot;&gt;Watch on YouTube&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;We should treat maintainability as an upstream factor that improves velocity as a downstream effect.&lt;/p&gt;
&lt;h3&gt;If we build code that’s easy to understand and maintain, velocity stays high.&lt;/h3&gt;
&lt;p&gt;I’ve worked on dozens of codebases throughout my career. In some cases, changes took weeks (or even months) and were full of frustration, chasing down other teams to address issues our changes would cause in their code, and other complex shenanigans. In others, changes shipped in hours, with minimum hassle and very little stress or cross-team challenges.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The specific architecture didn’t matter. What always made the difference was clear, strongly enforced API boundaries.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The benefit came from:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Extremely clear contracts establishing how the tool could be used (the API)&lt;/li&gt;
&lt;li&gt;Architectural enforcement that prevented working around those API boundaries (e.g. no way to reach into the internals to create hard-to-track dependencies)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;A clear API contract means changes only have to account for the internal code and the API contract itself. As long as the API surface is unchanged, the team only had to think about the internal code instead of reasoning through the entire company codebase for every update.&lt;/p&gt;
&lt;p&gt;This meant the code was separated into independent, reasonably sized, fully understandable chunks. Teams could deliver new features quickly because they needed far less context to feel confident and test their work.&lt;/p&gt;
&lt;h2&gt;Build for maintainability to boost shipping velocity.&lt;/h2&gt;
&lt;p&gt;Maintainable code should be easy to delete. &lt;a href=&quot;https://www.netlify.com/blog/2020/10/28/optimize-for-deletion-speed-up-development-without-adding-risk/&quot;&gt;Optimizing for deletion&lt;/a&gt; makes code easier to refactor, or even fully replace — and it can be hot-swapped without other systems ever noticing or needing to care.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Reducing the external surface area of the codebase starts as building for maintainability, but it results in much higher velocity.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;For example, your team might have an API written in Node. Later, you may decide to rebuild in Go or Rust to solve a performance bottleneck. As long as the rebuilt API has the same endpoints, arguments, and return values, none of the services consuming that API don’t even need to be made aware of the change — they’ll just see a performance boost once the new version rolls out.&lt;/p&gt;
&lt;p&gt;Nothing can reach its internals and touch things if the API stays accurate. You don&apos;t have to worry that changing a line of code will break the whole codebase. There&apos;s no way for some other piece of code to reach beyond the API boundary.&lt;/p&gt;
&lt;h2&gt;Maintainability should be considered a feature of velocity.&lt;/h2&gt;
&lt;p&gt;By treating maintainability and velocity as separate concerns, it opens the door for the team to say, “We’re going to build fast and not worry about code quality. We’ll come back later and clean up tech debt.”&lt;/p&gt;
&lt;p&gt;But later never comes — &lt;strong&gt;it is &lt;em&gt;extremely&lt;/em&gt; challenging to halt feature work for long enough to tackle a large-scale refactor solely for tech debt cleanup.&lt;/strong&gt; So instead, tech debt piles up, and every additional mess adds drag to the team’s ability to ship.&lt;/p&gt;
&lt;p&gt;The team loses momentum. And momentum is everything. Lost momentum can drain morale, which further decreases velocity as well as care for maintainability (”the app’s already a mess, so why worry about clean code?”) — and that’s a vicious cycle that grinds teams to a halt.&lt;/p&gt;
&lt;h2&gt;Treat maintainability as a facet of velocity instead of a separate component of the codebase.&lt;/h2&gt;
&lt;p&gt;In restaurants, cooks are trained to clean as they go. Wipe down surfaces in between steps; wash dishes while waiting for a pot to boil; put equipment away instead of to the side.&lt;/p&gt;
&lt;p&gt;All of this reduces clutter and means the kitchen never becomes a giant mess, because the restaurant can’t afford to keep customers waiting while the kitchen shuts down for cleaning mid-service.&lt;/p&gt;
&lt;p&gt;When I first started cooking, I struggled with this and pushed back. I’d ask, “Doesn’t this make us way slower?” And the chef would patiently explain that cleaning as part of cooking is what makes us fast. Clean stations reduce mistakes, waste, stress, and much more. We just have to build the habit.&lt;/p&gt;
&lt;p&gt;Coding on a team is much the same. &lt;strong&gt;It might &lt;em&gt;feel&lt;/em&gt; like fixing tech debt and building features are entirely separate tasks, but if we build the habit of cleaning as we go, we can use every pull request as an opportunity to handle a small amount of tech debt and avoid creating more.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;It’s not easy, but it can be done.&lt;/p&gt;
&lt;h2&gt;Change the way you talk about the codebase.&lt;/h2&gt;
&lt;p&gt;If maintainability and velocity are treated as two sides of the same coin (as opposed to a separate concern), it tends to change the discussion about how we build.&lt;/p&gt;
&lt;p&gt;We stop asking, “When will we come back to clean this up?”&lt;/p&gt;
&lt;p&gt;Instead, we start asking, “Are we willing to take the velocity hit required to rush this out the door now?”&lt;/p&gt;
&lt;p&gt;What used to be a vague “later” becomes a pretty clear “no”.&lt;/p&gt;
&lt;h2&gt;Companies that ignore this run into compounding slowdowns.&lt;/h2&gt;
&lt;p&gt;When a company chooses to ignore maintainability under the false assumption that it will make them faster, the tech debt starts to accumulate. At first, it’s not that noticeable — the features are shipping, the codebase is still small, the team still has all the context because it’s relatively new.&lt;/p&gt;
&lt;p&gt;But as time continues, as original team members leave and new team members join, as new tech debt stacks on top of old tech debt, things start to slow down. It’s more and more difficult to build things without breaking other things, and velocity grinds to a halt.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Momentum is everything in a company. If you have momentum, it allows for faster learning, quicker adjustment, tighter feedback loops, and overall healthier companies and products.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Maintainability enables velocity, and velocity creates momentum. If you want to build a culture of shipping fast, architect for maintainability and build the habit of cleaning as you go.&lt;/p&gt;</content:encoded></item><item><title>Add Feature Flags to a React App (for FREE)</title><link>https://codetv.dev/blog/feature-flags-react-devcycle/</link><guid isPermaLink="true">https://codetv.dev/blog/feature-flags-react-devcycle/</guid><description>If you want to ship quickly and be confident that customers actually want the new features you’re building, feature flags are a must-have in your dev toolbox.
</description><pubDate>Tue, 06 Jun 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/f_auto,q_auto/v1683734135/lwj/blog/feature-flags-react.jpg&quot; alt=&quot;Add Feature Flags to a React App (for FREE)&quot; /&gt;&lt;/p&gt;&lt;p&gt;In this tutorial, we&apos;ll look at how we can use feature flags to safely and quickly ship new ideas to a small number of users, allowing you to gather real data on how people use it. We&apos;ll do this by adding a feature in a React app using DevCycle for feature flags. And we&apos;ll build it all in minutes using just a few lines of code.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/i6j2hT7ox0c&quot;&gt;Watch on YouTube&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Demo: https://feature-flag-devcycle.netlify.app/&lt;/li&gt;
&lt;li&gt;Repo: https://github.com/learnwithjason/feature-flag-devcycle&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Why you should consider feature flags&lt;/h2&gt;
&lt;p&gt;There&apos;s a constant tension between &quot;shipping quickly&quot; and &quot;making sure we only ship things people want&quot; — it undermines our confidence, slows us down, and leads to pretty frustrating meetings.&lt;/p&gt;
&lt;p&gt;Feature flags are a programming pattern where we put functionality inside a conditional check, and only show it if the feature flag is set to the correct value.&lt;/p&gt;
&lt;p&gt;Used well, feature flags allow us to deploy to production with lower risk.&lt;/p&gt;
&lt;p&gt;By lowering the risk, you can ship faster.&lt;/p&gt;
&lt;p&gt;By shipping faster, you get real data and feedback.&lt;/p&gt;
&lt;p&gt;By getting real data and feedback, you validate that what you&apos;re building is the right thing.&lt;/p&gt;
&lt;p&gt;By validating your idea, you&apos;re far less likely to waste time building and shipping things that no one wants.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Feature flags get you out of hypotheticals and into reality faster.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;To get a feel for how to add feature flags into an existing app, let&apos;s ship a new feature behind a feature flag in a React app.&lt;/p&gt;
&lt;p&gt;Source code: https://github.com/learnwithjason/feature-flag-devcycle
Demo: https://feature-flag-devcycle.netlify.app/dashboard/&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;Companies like &lt;a href=&quot;https://devcycle.com/&quot;&gt;DevCycle&lt;/a&gt; handle the heavy lifting of managing feature flags for us, which means we get to use our time building and shipping features.&lt;/p&gt;&lt;p&gt;Huge thanks to DevCycle for sponsoring this video! We&apos;ll be using their free tier for this tutorial build. You can get your account and learn more at &lt;a href=&quot;https://devcycle.com/&quot;&gt;devcycle.com&lt;/a&gt;.&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;Set up your dev environment&lt;/h2&gt;
&lt;p&gt;To start, clone the start branch of the repo, fork it, and install dependencies:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# clone the repo (this uses the GitHub CLI)
gh repo clone learnwithjason/feature-flag-devcycle -- -b start

# move into the project
cd feature-flag-devcycle/

# fork the repo
gh repo fork

# install dependencies
npm i

# start the dev server
npm run dev
&lt;/code&gt;&lt;/pre&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Heads up:&lt;/strong&gt; forking this repo is optional, but if you want to push changes
and/or deploy this project, you&apos;ll need your own copy.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;Open &lt;code&gt;http://localhost:5173&lt;/code&gt; in your browser to see the app we&apos;re going to build.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:a8fffb/lwj/blog/devcycle-01-local-dev.jpg&quot; alt=&quot;the starting point for the site running locally in the
browser&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; this app uses &lt;a href=&quot;https://clerk.dev&quot;&gt;Clerk&lt;/a&gt; for authentication, and a
publishable test key has been added to the &lt;code&gt;.env&lt;/code&gt; file to make this tutorial
faster to set up and follow. If you want to deploy this site, you&apos;ll need to
create your own Clerk account and update that key.&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;Create a DevCycle account and project&lt;/h2&gt;
&lt;p&gt;We&apos;ll be using DevCycle to power the feature flags in our app. The free tier will be more than enough to handle our needs.&lt;/p&gt;
&lt;p&gt;Head to the DevCycle home page and click the &quot;create account&quot; button, then sign up. I used my GitHub account, but you can use whatever you prefer.&lt;/p&gt;
&lt;p&gt;Next, create a new project. You can name this whatever you like — it&apos;s only used internally and won&apos;t be visible to anyone outside your team.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:d459ab/lwj/blog/devcycle-02-create-project.jpg&quot; alt=&quot;Create a New Project modal on the DevCycle
website&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;On the next screen, you&apos;ll see your SDK keys for different environments. Copy the client key from the Development environment and put it into the &lt;code&gt;.env&lt;/code&gt; file in the app as the value of &lt;code&gt;VITE_DEVCYCLE_CLIENT_KEY&lt;/code&gt;.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:ffde38/lwj/blog/devcycle-03-env-vars.jpg&quot; alt=&quot;Environmnts and keys screen on the DevCycle
dashboard&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;Vite will detect that the &lt;code&gt;.env&lt;/code&gt; file has changed and restart automatically, which is pretty dang cool.&lt;/p&gt;
&lt;h2&gt;Create a feature flag in DevCycle&lt;/h2&gt;
&lt;p&gt;In the DevCycle dashboard, go to the Features tab and click &quot;Create New Feature&quot;. This opens up a modal that asks what type of feature you want to add.&lt;/p&gt;
&lt;p&gt;We want to do a limited release of a new feature, so select &quot;Release&quot;.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:a8fffb/lwj/blog/devcycle-04-create-feature-wizard.jpg&quot; alt=&quot;feature creator wizard in DevCycle&apos;s dashboard. the &amp;quot;release&amp;quot; option is
selected&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;On the next screen, name the feature &quot;Waff-fulfillment&quot;. The key and variable fields will autocomplete. Adding a description is optional, but will be helpful for remembering what the flag is for.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:d459ab/lwj/blog/devcycle-05-create-feature-details.jpg&quot; alt=&quot;create a feature modal in DevCycle&apos;s
dashboard&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;On the next screen, note that the variable key is a boolean value where &lt;code&gt;true&lt;/code&gt; maps to &quot;Variation On&quot; and &lt;code&gt;false&lt;/code&gt; maps to &quot;Variation Off&quot;. Each user on our site can have their own value for the feature flag, which is how we control who sees the new feature or not.&lt;/p&gt;
&lt;p&gt;To decide who will see the new feature, scroll down to the &quot;Users &amp;amp; Targeting&quot; section and find the settings for development.&lt;/p&gt;
&lt;p&gt;By default, the feature flag will turn the new feature on for all users. We have options to change which users we target, which we&apos;ll look at a bit later. We also have options on how to serve the variations of our feature, which is what we want to look at now.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:ffde38/lwj/blog/devcycle-06-users-and-targeting-defaults.jpg&quot; alt=&quot;users and targeting in the DevCycle
dashboard&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;Open the &quot;Serve&quot; dropdown and choose &quot;Random Distribution&quot;. This updates the UI to show percentages for both the &quot;on&quot; and &quot;off&quot; variations. By default they&apos;re set to 50/50, but we can choose any combination we want.&lt;/p&gt;
&lt;p&gt;For this app, 50/50 makes sense, but if you&apos;re working on a more established app it might make sense to only show 5% (or even 1%) of users the new feature at first to gather data and feedback before rolling out more widely.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:a8fffb/lwj/blog/devcycle-07-random-distribution.jpg&quot; alt=&quot;users and targeting updated to randomly distribute the feature to make it
available to 50% of
users&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;Click save to update the settings.&lt;/p&gt;
&lt;h2&gt;Add the DevCycle provider and identify the current user&lt;/h2&gt;
&lt;p&gt;With the SDK key in our environment and a feature flag set up in DevCycle, we&apos;re ready to write some code!&lt;/p&gt;
&lt;p&gt;Because we&apos;re working in React, accessing the feature flag data is made possible by wrapping our app in a provider. DevCycle will let us pass in the current user as an argument to the provider, which means the feature flags will be tied to a user account. This is useful because it means the user will have the same experience across all devices.&lt;/p&gt;
&lt;p&gt;To set this up, open &lt;code&gt;src/app.tsx&lt;/code&gt; and make the following changes:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	import { BrowserRouter, Route, Routes, useNavigate } from &apos;react-router-dom&apos;;
	import { ClerkProvider, SignIn, SignUp, useUser } from &apos;@clerk/clerk-react&apos;;
+	import {
+		useIsDVCInitialized,
+		withDVCProvider,
+	} from &apos;@devcycle/devcycle-react-sdk&apos;;
	import { Layout } from &apos;./components/_layout&apos;;
	import { HomePage } from &apos;./components/home&apos;;
	import { DashboardLayout } from &apos;./components/dashboard/_dashboard-layout&apos;;
	import { DashboardHome } from &apos;./components/dashboard/dashboard-home&apos;;
	import { DashboardWaffles } from &apos;./components/dashboard/dashboard-waffles&apos;;
	import { DashboardProgress } from &apos;./components/dashboard/dashboard-progress&apos;;

	import &apos;./styles/main.css&apos;;

	const MainApp = () =&amp;gt; {
-		const { isLoaded } = useUser();
+		const { isLoaded, user } = useUser();

+		// this little maneuver saves us from having yet another split out component
+		const MainAppWithFeatureFlags = withDVCProvider({
+			sdkKey: import.meta.env.VITE_DEVCYCLE_CLIENT_KEY,
+			user: {
+				user_id: user?.id,
+				name: user?.firstName ?? &apos;&apos;,
+				email: user?.emailAddresses[0].emailAddress,
+			},
+		})(() =&amp;gt; {
+			const dvcReady = useIsDVCInitialized();
+
-			if (!isLoaded) {
+			if (!dvcReady || !isLoaded) {
				return (
					&amp;lt;div className=&quot;loading&quot;&amp;gt;
						&amp;lt;p&amp;gt;loading...&amp;lt;/p&amp;gt;
					&amp;lt;/div&amp;gt;
				);
			}

			return (
				&amp;lt;Routes&amp;gt;
					&amp;lt;Route element={&amp;lt;Layout /&amp;gt;}&amp;gt;
						&amp;lt;Route path=&quot;/&quot; element={&amp;lt;HomePage /&amp;gt;} /&amp;gt;
						&amp;lt;Route
							path=&quot;/login/*&quot;
							element={&amp;lt;SignIn routing=&quot;path&quot; path=&quot;/login&quot; /&amp;gt;}
						/&amp;gt;
						&amp;lt;Route
							path=&quot;/register/*&quot;
							element={&amp;lt;SignUp routing=&quot;path&quot; path=&quot;/register&quot; /&amp;gt;}
						/&amp;gt;
						&amp;lt;Route path=&quot;/dashboard&quot; element={&amp;lt;DashboardLayout /&amp;gt;}&amp;gt;
							&amp;lt;Route index element={&amp;lt;DashboardHome /&amp;gt;} /&amp;gt;
							&amp;lt;Route path=&quot;waffles&quot; element={&amp;lt;DashboardWaffles /&amp;gt;} /&amp;gt;
							&amp;lt;Route path=&quot;progress&quot; element={&amp;lt;DashboardProgress /&amp;gt;} /&amp;gt;
						&amp;lt;/Route&amp;gt;
					&amp;lt;/Route&amp;gt;
				&amp;lt;/Routes&amp;gt;
			);
+		});
+
+		return &amp;lt;MainAppWithFeatureFlags /&amp;gt;;
	};

	/*
	* Clerk needs access to the React Router context, so we need to split out the
	* component to allow for that.
	*/
	const ClerkProviderWithRoutes = () =&amp;gt; {
		const navigate = useNavigate();

		return (
			&amp;lt;ClerkProvider
				publishableKey={import.meta.env.VITE_CLERK_PUBLISHABLE_KEY}
				navigate={(to) =&amp;gt; navigate(to)}
			&amp;gt;
				&amp;lt;MainApp /&amp;gt;
			&amp;lt;/ClerkProvider&amp;gt;
		);
	};

	export const App = () =&amp;gt; {
		return (
			&amp;lt;BrowserRouter&amp;gt;
				&amp;lt;ClerkProviderWithRoutes /&amp;gt;
			&amp;lt;/BrowserRouter&amp;gt;
		);
	};

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This code has a few key features:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The original output of &lt;code&gt;MainApp&lt;/code&gt; gets wrapped with DevCycle&apos;s provider using &lt;code&gt;withDVCProvider&lt;/code&gt;, which gets stored in a component and returned from &lt;code&gt;MainApp&lt;/code&gt;. This might look a bit strange, but it simplifies getting access to the &lt;code&gt;user&lt;/code&gt; value from Clerk&apos;s &lt;code&gt;useUser()&lt;/code&gt; hook.&lt;/li&gt;
&lt;li&gt;The SDK key and user details get passed as arguments to &lt;code&gt;withDVCProvider&lt;/code&gt;, which connects the app to your DevCycle account and ties the feature flag to the current user.&lt;/li&gt;
&lt;li&gt;An additional readiness check is added to make sure DevCycle is loaded before rendering the app using the &lt;code&gt;useIsDVCInitialized()&lt;/code&gt; hook.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once this is saved, the app is now ready for feature flagging!&lt;/p&gt;
&lt;h2&gt;Modify app navigation based on feature flags&lt;/h2&gt;
&lt;p&gt;The app dashboard right now shows the experimental &quot;WAF-FULFILLMENT&quot; feature in the left-hand navigation. Our first order of business is making sure only users in our test cohort can see this nav item.&lt;/p&gt;
&lt;p&gt;To do that, modify &lt;code&gt;src/components/dashboard/_dashboard-layout.tsx&lt;/code&gt; with the following code:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	import { RedirectToSignIn, SignedIn, SignedOut } from &apos;@clerk/clerk-react&apos;;
	import { NavLink, Outlet } from &apos;react-router-dom&apos;;
+	import { useVariableValue } from &apos;@devcycle/devcycle-react-sdk&apos;;
	import styles from &apos;./_dashboard-layout.module.css&apos;;

	export const DashboardLayout = () =&amp;gt; {
+		const showWaffFulfillment = useVariableValue(&apos;waff-fulfillment&apos;, false);
+
		return (
			&amp;lt;&amp;gt;
				&amp;lt;SignedIn&amp;gt;
					&amp;lt;div className={styles.dashboard}&amp;gt;
						&amp;lt;nav className={styles.nav}&amp;gt;
							&amp;lt;NavLink
								to=&quot;/dashboard&quot;
								className={({ isActive }) =&amp;gt; (isActive ? styles.active : &apos;&apos;)}
								end
							&amp;gt;
								Dashboard
							&amp;lt;/NavLink&amp;gt;
							&amp;lt;NavLink
								to=&quot;/dashboard/waffles&quot;
								className={({ isActive }) =&amp;gt; (isActive ? styles.active : &apos;&apos;)}
							&amp;gt;
								Your Waffles
							&amp;lt;/NavLink&amp;gt;
+							{showWaffFulfillment ? (
								&amp;lt;NavLink
									to=&quot;/dashboard/progress&quot;
									className={({ isActive }) =&amp;gt; (isActive ? styles.active : &apos;&apos;)}
							&amp;gt;
									Waff-fulfillment
								&amp;lt;/NavLink&amp;gt;
+							) : null}
						&amp;lt;/nav&amp;gt;
						&amp;lt;section className={styles.content}&amp;gt;
							&amp;lt;Outlet /&amp;gt;
						&amp;lt;/section&amp;gt;
					&amp;lt;/div&amp;gt;
				&amp;lt;/SignedIn&amp;gt;
				&amp;lt;SignedOut&amp;gt;
					&amp;lt;RedirectToSignIn /&amp;gt;
				&amp;lt;/SignedOut&amp;gt;
			&amp;lt;/&amp;gt;
		);
	};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Save the page and — if you&apos;re one of the 50% of users to whom the feature flag is set to &lt;code&gt;true&lt;/code&gt; — the nav item will disappear.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:d459ab/lwj/blog/devcycle-08-nav-item-hidden.jpg&quot; alt=&quot;the app dashboard with the feature’s nav item hidden due to the feature flag
being set to
false&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;h2&gt;Add additional targeting to allow easier development&lt;/h2&gt;
&lt;p&gt;During development, it&apos;s helpful to be able to toggle the feature flag on or off for your user to make sure things are working as expected. To do this, head back to your DevCycle dashboard and go back to the &quot;Users &amp;amp; Targeting&quot; section for development.&lt;/p&gt;
&lt;p&gt;Click the &quot;Add Targeting Rule&quot; option below the original definition, then use the up arrow button at the right to move the new targeting rule to the top of the list. These rules are evaluated in order, so more specific rules go first.&lt;/p&gt;
&lt;p&gt;Give the new rule a name of &quot;Developer Targeting&quot;. For the definition, select &quot;User Email&quot;. In the second dropdown select &quot;is&quot;. In the final input, add your email address.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:ffde38/lwj/blog/devcycle-09-target-devs.jpg&quot; alt=&quot;the DevCycle dashboard with a new targeting rule set to change the flag for
the user with a specific
email&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; a good pattern if you want to turn a feature on for your whole
company is to use the &quot;contains&quot; option and use just your email domain (e.g.
&lt;code&gt;@example.com&lt;/code&gt;). See &lt;a href=&quot;https://docs.devcycle.com/home/feature-management/features-and-variables/targeting-users#creating-a-targeting-rule&quot;&gt;targeting
rules&lt;/a&gt;
for details.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;With this in place, you can update the &quot;Serve&quot; option to be on or off, and when you save the flag will be updated in the app. This works without a reload, which is really powerful because it means you have full control.&lt;/p&gt;
&lt;p&gt;Turn the variation on and you&apos;ll see the nav item appear. Turn it off and it&apos;ll disappear.&lt;/p&gt;
&lt;h2&gt;Show or hide an announcement banner based on a feature flag&lt;/h2&gt;
&lt;p&gt;Next, let&apos;s update the dashboard so it only shows the announcement banner at the top if the feature flag is enabled.&lt;/p&gt;
&lt;p&gt;To do this, make the following changes in &lt;code&gt;src/components/dashboard/dashboard-home.tsx&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;+	import { useVariableValue } from &apos;@devcycle/devcycle-react-sdk&apos;;
	import { Link } from &apos;react-router-dom&apos;;
	import waffles from &apos;../../data/waffles.json&apos;;
	import styles from &apos;./_dashboard-layout.module.css&apos;;

	export const DashboardHome = () =&amp;gt; {
+		const showWaffFulfillment = useVariableValue(&apos;waff-fulfillment&apos;, false);
+
		return (
			&amp;lt;&amp;gt;
+				{showWaffFulfillment ? (
					&amp;lt;section className=&quot;box&quot;&amp;gt;
						&amp;lt;div className=&quot;boxTopper&quot;&amp;gt;
							&amp;lt;h2&amp;gt;NEW! Your Journey Toward Waff-fulfillment&amp;lt;/h2&amp;gt;
							&amp;lt;div className={styles.boxControls}&amp;gt;
								&amp;lt;Link to=&quot;/dashboard/progress&quot; className={styles.button}&amp;gt;
									check it out &amp;amp;rarr;
								&amp;lt;/Link&amp;gt;
							&amp;lt;/div&amp;gt;
						&amp;lt;/div&amp;gt;
					&amp;lt;/section&amp;gt;
+				) : null}
				&amp;lt;section className=&quot;box&quot;&amp;gt;
					{/* unchanged below this line */}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Save and the banner will disappear when the variation is turned off.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1800,q_auto,f_auto,b_rgb:a8fffb/lwj/blog/devcycle-10-banner-hidden.jpg&quot; alt=&quot;the app dashboard with the banner hidden due to feature flag
values&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;h2&gt;Disable a feature route using a feature flag&lt;/h2&gt;
&lt;p&gt;As it stands, there are no links presented to a user in the &quot;off&quot; variation. However, if they knew the URL they could still get to the feature manually.&lt;/p&gt;
&lt;p&gt;To disable the feature entirely, modify &lt;code&gt;src/app.tsx&lt;/code&gt; to only render the route if the feature flag is true for the given user.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	import { BrowserRouter, Route, Routes, useNavigate } from &apos;react-router-dom&apos;;
	import { ClerkProvider, SignIn, SignUp, useUser } from &apos;@clerk/clerk-react&apos;;
	import {
		useIsDVCInitialized,
+		useVariableValue,
		withDVCProvider,
	} from &apos;@devcycle/devcycle-react-sdk&apos;;
	import { Layout } from &apos;./components/_layout&apos;;
	import { HomePage } from &apos;./components/home&apos;;
	import { DashboardLayout } from &apos;./components/dashboard/_dashboard-layout&apos;;
	import { DashboardHome } from &apos;./components/dashboard/dashboard-home&apos;;
	import { DashboardWaffles } from &apos;./components/dashboard/dashboard-waffles&apos;;
	import { DashboardProgress } from &apos;./components/dashboard/dashboard-progress&apos;;

	import &apos;./styles/main.css&apos;;

	const MainApp = () =&amp;gt; {
		const { isLoaded, user } = useUser();

		// this little maneuver saves us from having yet another split out component
		const MainAppWithFeatureFlags = withDVCProvider({
			sdkKey: import.meta.env.VITE_DEVCYCLE_CLIENT_KEY,
			user: {
				user_id: user?.id,
				name: user?.firstName ?? &apos;&apos;,
				email: user?.emailAddresses[0].emailAddress,
			},
		})(() =&amp;gt; {
			const dvcReady = useIsDVCInitialized();
+			const showWaffFulfillment = useVariableValue(&apos;waff-fulfillment&apos;, false);

			if (!dvcReady || !isLoaded) {
				return (
					&amp;lt;div className=&quot;loading&quot;&amp;gt;
						&amp;lt;p&amp;gt;loading...&amp;lt;/p&amp;gt;
					&amp;lt;/div&amp;gt;
				);
			}

			return (
				&amp;lt;Routes&amp;gt;
					&amp;lt;Route element={&amp;lt;Layout /&amp;gt;}&amp;gt;
						&amp;lt;Route path=&quot;/&quot; element={&amp;lt;HomePage /&amp;gt;} /&amp;gt;
						&amp;lt;Route
							path=&quot;/login/*&quot;
							element={&amp;lt;SignIn routing=&quot;path&quot; path=&quot;/login&quot; /&amp;gt;}
						/&amp;gt;
						&amp;lt;Route
							path=&quot;/register/*&quot;
							element={&amp;lt;SignUp routing=&quot;path&quot; path=&quot;/register&quot; /&amp;gt;}
						/&amp;gt;
						&amp;lt;Route path=&quot;/dashboard&quot; element={&amp;lt;DashboardLayout /&amp;gt;}&amp;gt;
							&amp;lt;Route index element={&amp;lt;DashboardHome /&amp;gt;} /&amp;gt;
							&amp;lt;Route path=&quot;waffles&quot; element={&amp;lt;DashboardWaffles /&amp;gt;} /&amp;gt;
+							{showWaffFulfillment ? (
								&amp;lt;Route path=&quot;progress&quot; element={&amp;lt;DashboardProgress /&amp;gt;} /&amp;gt;
+							) : null}
						&amp;lt;/Route&amp;gt;
					&amp;lt;/Route&amp;gt;
				&amp;lt;/Routes&amp;gt;
			);
		});

		return &amp;lt;MainAppWithFeatureFlags /&amp;gt;;
	};

	/* unchanged below this line */
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With that change, it&apos;s no longer possible to access the feature in any way unless the feature flag is on for the current user.&lt;/p&gt;
&lt;h2&gt;Feature flags let you stop guessing and start learning&lt;/h2&gt;
&lt;p&gt;Feature flags are one of the best ways to take the risk out of shipping so you can gather real data instead of guessing what your users want. A good feature flagging workflow gives your team both safety and speed so you can ship new features and experiments faster than ever.&lt;/p&gt;
&lt;p&gt;Thanks again to DevCycle for sponsoring this video. Learn more about what you can do with DevCycle on &lt;a href=&quot;https://devcycle.com&quot;&gt;their website&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Resources &amp;amp; Next Steps&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://devcycle.com&quot;&gt;DevCycle for feature flags&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://clerk.dev&quot;&gt;Clerk for auth&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://vitejs.dev&quot;&gt;Vite for React apps&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>How I prep for frontend interviews</title><link>https://codetv.dev/blog/frontend-interview-prep/</link><guid isPermaLink="true">https://codetv.dev/blog/frontend-interview-prep/</guid><description>The frontend interview process is... wild. Here’s how I’ve navigated tech interviews in my own career.
</description><pubDate>Wed, 17 May 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/f_auto,q_auto/v1683996783/lwj/blog/frontend-interviews.jpg&quot; alt=&quot;How I prep for frontend interviews&quot; /&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://youtu.be/0FMLC3CARl0&quot;&gt;Watch on YouTube&lt;/a&gt;&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;IMPORTANT NOTE:&lt;/strong&gt; All I can share is my own experience, so please take
everything I’m about to say with an appropriately sized grain of salt.&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;How should devs prepare for frontend interviews?&lt;/h2&gt;
&lt;p&gt;Today’s interview landscape is wild. One company might ask you to code a page based on a design file. Another might ask you to implement a binary sorting algorithm on a whiteboard from scratch. The style and quality of technical interviewing varies so much between companies that it’s impossible to give a definitive “how to prep for a frontend interview” answer.&lt;/p&gt;
&lt;h2&gt;Use working in public to ease (or even bypass) technical interviews.&lt;/h2&gt;
&lt;p&gt;In my career, I’ve focused on preempting the technical interview through building lots of things and posting them in public places. For many companies — at least for the companies that chose to hire me — my public work was a sufficient technical interview because they could see tons of examples of my knowledge and skill level.&lt;/p&gt;
&lt;p&gt;When I was the hiring manager for my team, I would use someone’s public work as proof they could do the job, and the technical interview became a conversation where they’d walk me through a piece of code they were proud of and talk about what they liked and why.&lt;/p&gt;
&lt;h2&gt;To algorithm or not to algorithm?&lt;/h2&gt;
&lt;p&gt;A lot of companies aren’t willing to accept public work, however, and they use a templated technical interview. For me, this spells doom.&lt;/p&gt;
&lt;p&gt;I&apos;ll probably never work at Google, because I can’t pass their tech interview. I interviewed at Facebook about ten years ago and I failed the interview so badly that they thought I was the wrong person on the call.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;This may have changed, but I interviewed for a front-end position at
Facebook and my interview was &quot;write a binary sorting algorithm from
scratch&quot; and &quot;write the most efficient query to join two
tables&quot;. I felt like I was in the wrong interview.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;— Jason Lengstorf ⚡️ (@jlengstorf) &lt;a href=&quot;https://twitter.com/jlengstorf/status/938167994882183168?ref_src=twsrc%5Etfw&quot;&gt;December 5, 2017&lt;/a&gt;&lt;/p&gt;&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;I&apos;ve never passed an algorithms interview.&lt;/strong&gt; I don&apos;t have a background in computer science. I can&apos;t write a bubble sort from scratch. I work with languages that have implemented those algorithms for me already, and I’ve never been able to bring myself to care about learning things solely to pass an interview.&lt;/p&gt;
&lt;p&gt;In practice, that’s meant that if I’m in an interview loop, I call out early that I won’t be able to pass an algorithms interview and that we should end now if that’s a requirement. I’ve lost opportunities that way, but choosing to be upfront about it has led to less frustration on both sides.&lt;/p&gt;
&lt;h2&gt;If you care about working at big companies, you might need to practice your algorithms.&lt;/h2&gt;
&lt;p&gt;Especially at big companies, the desire to standardize often leads to one-size-fits-all tech interviews, and that often means computer science trivia and whiteboarding. If your goals include adding those companies to your resume, then it’s probably worth diving into something like &lt;a href=&quot;https://ocw.mit.edu/search/?d=Electrical%20Engineering%20and%20Computer%20Science&amp;amp;s=department_course_numbers.sort_coursenum&quot;&gt;MIT’s free computer science materials&lt;/a&gt; or &lt;a href=&quot;https://leetcode.com/&quot;&gt;LeetCode&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Is it practical knowledge that you’ll use every day? Probably not.&lt;/p&gt;
&lt;p&gt;But if it helps you land a job that’s meaningful to you, sometimes it’s worth jumping through silly hoops to get there.&lt;/p&gt;
&lt;h2&gt;Be open and curious to give yourself the best chances.&lt;/h2&gt;
&lt;p&gt;If your hiring manager is good (by my highly subjective definition of “good”), they’ll be looking at far more than just your ability to memorize an Intro to Algorithms course.&lt;/p&gt;
&lt;p&gt;They’ll be looking for someone who’s thoughtful, good at communication, aware of what the business does, and interested in things beyond code.&lt;/p&gt;
&lt;p&gt;Building lots of varied projects and sharing them in a place where a potential employer can find them is a great way to broaden your skillset. Thinking about the projects beyond the code — how would the business work? who would the customer be? what would the marketing positioning be? — also demonstrates breadth in a way that can really set you apart.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Learning doesn’t have downsides. Gaining new experience in virtually any field will help you throughout your career.&lt;/strong&gt; Broad experience means you have additional pools of knowledge to draw from, and that adds unique value.&lt;/p&gt;
&lt;h2&gt;Remember that employment is a relationship and it needs to be a good fit.&lt;/h2&gt;
&lt;p&gt;Interviewing is a grab bag of experiences. You get a glimpse into a company’s culture as you interview. Maybe they’ll show you empathy and the tech interview will be highly relevant to the work you’d be doing. Maybe they’ll play gotcha with algorithm trivia. Maybe they feel like they suffered in their coding interview, so they need to make you suffer in turn.&lt;/p&gt;
&lt;p&gt;In any case, don’t ignore that information. Remember that while the company is interviewing you, you’re also interviewing the company. If you see something that makes you worry, don’t shrug it off.&lt;/p&gt;
&lt;p&gt;Getting a job means you have to do the job you got hired for with the people who hired you. If the interview feels bad, it can often be a sign that the rest of your employment experience won’t be much better.&lt;/p&gt;
&lt;p&gt;It can be hard, but if the vibes are off it might be worth declining that job.&lt;/p&gt;
&lt;p&gt;Keep learning, keep building, keep sharing, keep participating in the community, and keep applying. You’ll find a job that’s a good fit.&lt;/p&gt;</content:encoded></item><item><title>JS vs. No-JS for Websites &amp; How Astro Bridges the Gap</title><link>https://codetv.dev/blog/js-vs-no-js-astro-bridge/</link><guid isPermaLink="true">https://codetv.dev/blog/js-vs-no-js-astro-bridge/</guid><description>There’s a heated debate over JS vs. no-JS. Astro rejects it as a false choice and says, “Let’s just build great websites!”
</description><pubDate>Wed, 10 May 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/f_auto,q_auto/v1683734135/lwj/blog/astro-why-not-both.jpg&quot; alt=&quot;JS vs. No-JS for Websites &amp;amp; How Astro Bridges the Gap&quot; /&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://youtu.be/28TEWzieygU&quot;&gt;Watch on YouTube&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Why am I so excited about Astro?&lt;/h2&gt;
&lt;p&gt;In a field that tends toward absolutes, I’ve really loved that Astro has identified a specific set of use cases, embraced that there’s no one-size-fits-all approach to building for the web, and given us smart defaults to start with, plus full control of how far we can choose to go with our apps.&lt;/p&gt;
&lt;p&gt;By default, Astro ships HTML and CSS. No JavaScript at all. This is ideal for a substantial portion of the sites on the internet — most of these sites show us text and images without much interactivity or state.&lt;/p&gt;
&lt;p&gt;However, unlike previous tools that are — implicitly or otherwise — &lt;em&gt;against&lt;/em&gt; JavaScript, Astro fully embraces it. You can add any JavaScript you want, using any framework you want (or no framework), and opt-in to shipping JS on the client side. Astro’s got you covered.&lt;/p&gt;
&lt;p&gt;And no matter which approach you choose, you get to keep the modern, component-based workflow that makes the developer experience so pleasant in all our favorite frameworks.&lt;/p&gt;
&lt;h2&gt;There is a big push right now to recenter on simplicity.&lt;/h2&gt;
&lt;p&gt;More and more people seem to be questioning why we’re spending so much time on complex solutions to problems that most of don’t have, and I’m extremely here for it.&lt;/p&gt;
&lt;p&gt;Web standards have made massive leaps forward since the last wave of JS frameworks rose to popularity. Many things that made something like React necessary in the first place have been implemented natively, and if you keep an eye on the spec discussions, it’s pretty clear that these working groups have no intention of slowing down.&lt;/p&gt;
&lt;h2&gt;The web is ready to evolve to its next, simpler iteration.&lt;/h2&gt;
&lt;p&gt;We moved to tools like React because they simplified the mental model of building web apps.&lt;/p&gt;
&lt;p&gt;But over time — as any successful tool will do — JavaScript frameworks have expanded.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The cost of success is complexity&lt;/strong&gt;: more use cases, more edge cases, backward compatibility, a Cambrian explosion of ecosystem tools, and devs using these frameworks to do things they were never intended for.&lt;/p&gt;
&lt;p&gt;All of this has led to commentary like Andrew Clark saying, “If you use React, you should be using a meta-framework.” This makes me wonder if we’ve reached the apex of React’s dominance (and maybe the all-in JS framework approach?), and the coming years will see a new approach move in as the “default modern approach” to building for the web.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;If you use React, you should be using a React framework. If your existing
app doesn&apos;t use a framework, you should incrementally migrate to one. If
you&apos;re creating a new React project, you should use a framework from the
beginning.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;— Andrew Clark (@acdlite)&lt;/p&gt;&lt;a href=&quot;https://twitter.com/acdlite/status/1617611126514266112?ref_src=twsrc%5Etfw&quot;&gt;&lt;p&gt;January 23, 2023&lt;/p&gt;&lt;/a&gt;&lt;/blockquote&gt;

&lt;aside&gt;&lt;p&gt;And to be clear, I don’t think React is going anywhere. We’ll be using it for
decades to come, most likely. I just think we’re witnessing the moment where
it becomes more like jQuery, which is &lt;a href=&quot;https://w3techs.com/technologies/details/js-jquery&quot;&gt;in use on 77.7% of all
websites&lt;/a&gt; but almost never
gets recommended anymore. (React is &lt;a href=&quot;https://w3techs.com/technologies/details/js-react&quot;&gt;used on 3.4% of all
websites&lt;/a&gt;, for comparison.)&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;We&apos;ve started applying some very high-complexity app approaches to very low-complexity websites.&lt;/h2&gt;
&lt;p&gt;When I’m lucky enough to get into a deeper conversation about how we build for the web with people who have been doing it for a long time, one of the more common pain points we talk about is how easy it is to go overboard with our projects “just in case” (or worse, “because it’s fun”).&lt;/p&gt;
&lt;p&gt;The assignment might be to build a marketing home page, newsletter capture, and blog for our company. Somehow, we end up shipping a server-side rendered behemoth that clocks in at 1.2 MB of JavaScript, and the only interactivity is a fun little easter egg we hid in the hero 🤡&lt;/p&gt;
&lt;p&gt;React was built to power Facebook, arguably the &lt;em&gt;most&lt;/em&gt; complex app on the internet at the time.&lt;/p&gt;
&lt;p&gt;It excels at building highly complex applications. And if we find ourselves building one of those, we should absolutely choose the right tool for the job. But if we’re &lt;strong&gt;*&lt;/strong&gt;not***** building something monstrously complex, well… we should still choose the right tool for the job, which might just be shipping only HTML and CSS.&lt;/p&gt;
&lt;h2&gt;There are no “sides” in building for the web.&lt;/h2&gt;
&lt;p&gt;This all brings me back to why I think what Astro is doing is so smart.&lt;/p&gt;
&lt;p&gt;They’re telling us to rely on the platform as a default, then giving us a clear, flexible API to add any frameworks and abstractions we need on top of it &lt;em&gt;as part of the Astro authoring experience&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;They’re giving us a bridge where other frameworks appear to draw lines in the sand.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;On one side, we have a raft of excellent tools like Eleventy, Hugo, and others that excel at producing plain HTML and CSS. However, they actively discourage JavaScript through a complete lack of API support.&lt;/p&gt;
&lt;p&gt;The guidance is, effectively: “We can’t stop you from using JS, but we won’t help you either.”&lt;/p&gt;
&lt;p&gt;On the other side, JS meta frameworks are pushing an all-JavaScript approach where every bit of the DOM has to be rehydrated into a JS-powered layout, even if there’s no interactivity on the page.&lt;/p&gt;
&lt;p&gt;Both sides have strong arguments for why their approach is correct (and, in many cases, it starts to sound like there’s an argument for which side is more “morally correct” which worries me quite a bit). Depending on the project, where I land in the argument will change.&lt;/p&gt;
&lt;h2&gt;What if the “JS vs. no-JS” argument didn’t have to exist?&lt;/h2&gt;
&lt;p&gt;Astro takes a completely different approach. “There’s nothing to argue about,” they seem to be saying. “If you don’t need JS, don’t ship it. (We’ll help.) And if you need JS, use whatever you need. (We’ll help with that, too.)”&lt;/p&gt;
&lt;p&gt;Astro embraces the experience of building with our favorite frameworks without the trade-off of shipping a pile of unnecessary JavaScript at the end.&lt;/p&gt;
&lt;p&gt;In many ways, we get to have our cake and eat it, too.&lt;/p&gt;
&lt;p&gt;Rather than accepting the false premise that everyone needs to “choose a side” in the JS wars, Astro just… builds websites. And I love that.&lt;/p&gt;</content:encoded></item><item><title>Animations that feel alive using GSAP randomization</title><link>https://codetv.dev/blog/gsap-randomized-animations/</link><guid isPermaLink="true">https://codetv.dev/blog/gsap-randomized-animations/</guid><description>Animations can make web apps feel more fun and alive. In this tutorial, learn how to use GSAP, randomization, and the MotionPath plugin to make your animations feel more lively.
</description><pubDate>Tue, 14 Mar 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Animations are more fun — and more engaging — when they feel &quot;alive&quot;. Let’s build a hero section to learn how to take animations from stiff and robotic to something much more natural-feeling with just a few lines of code using GreenSock.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/learnwithjason/gsap-randomized-animations&quot;&gt;Source code for this tutorial&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://gsap-randomized-animations.netlify.app/&quot;&gt;Demo of this tutorial&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Rather watch along than read? No problem!&lt;/h2&gt;
&lt;p&gt;A video version of this tutorial is available on YouTube. Don&apos;t forget to subscribe!&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/DIK5R04eqeM&quot;&gt;Watch on YouTube&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Set up the project with HTML and CSS&lt;/h2&gt;
&lt;p&gt;For our animation to work, we need four components:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A cutout image to be in the foreground&lt;/li&gt;
&lt;li&gt;A container with a background image that will hold our animations&lt;/li&gt;
&lt;li&gt;A button to trigger new animations&lt;/li&gt;
&lt;li&gt;A template that contains the markup we want to animate on each button click&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;To get the starter code that contains the styles and markup used in this tutorial, clone the &lt;code&gt;start&lt;/code&gt; branch of the repo:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# clone the start branch of the tutorial repo
git clone git@github.com:learnwithjason/gsap-randomized-animations.git -b start

# move into the cloned repo
cd gsap-randomized-animations/

# install dependencies
npm i
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Start the dev server&lt;/h2&gt;
&lt;p&gt;To start the site in local dev mode, run the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The server will start up the site running at &lt;code&gt;http://localhost:5173&lt;/code&gt;. Open it in your browser and you should see a hero banner extolling the virtues of donuts:&lt;/p&gt;
&lt;h2&gt;Animate an image whenever the button is clicked&lt;/h2&gt;
&lt;p&gt;First, let&apos;s get a basic animation in place. We&apos;ll be using &lt;a href=&quot;https://greensock.com/&quot;&gt;GreenSock&lt;/a&gt; — also known as GSAP — because it&apos;s extremely powerful, free, and compatible with any framework (or no framework at all).&lt;/p&gt;
&lt;p&gt;To do that, we will:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Get the template, container, and button using &lt;code&gt;document.querySelector&lt;/code&gt; so we can work with them&lt;/li&gt;
&lt;li&gt;Create a variable called &lt;code&gt;endY&lt;/code&gt; that will tell the heart where it should be at the end of the animation&lt;/li&gt;
&lt;li&gt;Add an event listener to the button for click events and prevent the default behavior&lt;/li&gt;
&lt;li&gt;Create a copy of the heart image&lt;/li&gt;
&lt;li&gt;Add the cloned heart inside the container&lt;/li&gt;
&lt;li&gt;Use GSAP to animate the heart using &lt;a href=&quot;https://greensock.com/docs/v3/GSAP/gsap.to()&quot;&gt;&lt;code&gt;.to()&lt;/code&gt;&lt;/a&gt;, from its starting position at the bottom of the container to the &lt;code&gt;endY&lt;/code&gt; value, which is out of view at the top&lt;/li&gt;
&lt;li&gt;At the end of the animation, remove the heart&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Removing the heart at the end is important with an animation like this. Since someone can click the button an unlimited number of times, we need to make sure we&apos;re not continuously increasing the number of DOM nodes on the page.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { gsap } from &apos;gsap&apos;;

import &apos;./style.css&apos;;

const heartTemplate: HTMLTemplateElement = document.querySelector(&apos;#heart&apos;)!;
const container = document.querySelector(&apos;.container&apos;)!;
const button = document.querySelector(&apos;.button&apos;)!;

// move hearts to a position 20% beyond the top of the container so they disappear
const endY = container.clientHeight * -1.2;

button?.addEventListener(&apos;click&apos;, (event) =&amp;gt; {
  event.preventDefault();

  // create a new node from the img element in the template
  const heart = heartTemplate.content.firstElementChild!.cloneNode(true);

  // add the new node to the DOM inside the container
  container.appendChild(heart);

  // animate the heart from its starting position to the endY we defined above
  gsap.to(heart, {
    duration: 2,
    y: endY,
    onComplete: () =&amp;gt; {
      container.removeChild(heart);
    },
  });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Save these changes, then click the button in your browser. The heart will animate in exactly the same way every time you click.&lt;/p&gt;
&lt;figure&gt;&lt;/figure&gt;
&lt;p&gt;This is good, but we can make it better. Let&apos;s add some randomness to make the animation feel less robotic.&lt;/p&gt;
&lt;h2&gt;Start the animation from a random position in the container&lt;/h2&gt;
&lt;p&gt;To make the animation feel a little more alive, we&apos;ll have each heart generate from a random position along the bottom of the container. To do that, we&apos;ll need to:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Figure out the width of the container&lt;/li&gt;
&lt;li&gt;Set the width of the heart&lt;/li&gt;
&lt;li&gt;Get a random value between 0 and the width of the container, minus the width of the heart&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;GSAP provides a helpful set of utility classes, including &lt;a href=&quot;https://greensock.com/docs/v3/GSAP/UtilityMethods/random()&quot;&gt;a &lt;code&gt;random&lt;/code&gt; helper&lt;/a&gt;. We&apos;ll use that to randomize the start position.&lt;/p&gt;
&lt;p&gt;Add the following changes to &lt;code&gt;src/main.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	import { gsap } from &quot;gsap&quot;;

	import &quot;./style.css&quot;;

	const heartTemplate: HTMLTemplateElement = document.querySelector(&quot;#heart&quot;)!;
	const container = document.querySelector(&quot;.container&quot;)!;
	const button = document.querySelector(&quot;.button&quot;)!;

	// move hearts to a position 20% beyond the top of the container so they disappear
	const endY = container.clientHeight * -1.2;
+	const w = container.clientWidth;

	button?.addEventListener(&quot;click&quot;, (event) =&amp;gt; {
		event.preventDefault();

		// create a new node from the img element in the template
		const heart = heartTemplate.content.firstElementChild!.cloneNode(true);

+		// vary the size of the hearts a bit
+		const width = gsap.utils.random(40, 70);

+		// choose a random starting point along the width of the container
+		const initialX = gsap.utils.random(0, w - width);

+		// set initial values for the heart
+		gsap.set(heart, {
+			width,
+			x: initialX,
+		});

		// add the new node to the DOM inside the container
		container.appendChild(heart);

		// animate the heart from its starting position to the endY we defined above
		gsap.to(heart, {
			duration: 2,
			y: endY,
			onComplete: () =&amp;gt; {
				container.removeChild(heart);
			},
		});
	});

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Save and click the button in your browser a few times. The hearts now show up at different sizes and from different positions.&lt;/p&gt;
&lt;figure&gt;&lt;/figure&gt;
&lt;p&gt;By randomizing the starting position and size, the animation feels more fun and &quot;alive&quot;. But we can do better!&lt;/p&gt;
&lt;h2&gt;Add a &quot;floating&quot; effect to the animation path&lt;/h2&gt;
&lt;p&gt;To polish off the animation, we&apos;ll bring in GSAP&apos;s &lt;a href=&quot;https://greensock.com/motionpath/&quot;&gt;MotionPathPlugin&lt;/a&gt;, which will allow us to make the hearts &quot;float&quot; as they animate upward. Combined with some randomization, we can make the hearts look like they&apos;re floating bubbles, drifting upward organically as we click.&lt;/p&gt;
&lt;p&gt;To accomplish this, we need to:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Import and register the MotionPathPlugin&lt;/li&gt;
&lt;li&gt;Create a &lt;code&gt;floatDirection&lt;/code&gt; variables to hold &lt;code&gt;-1&lt;/code&gt; or &lt;code&gt;1&lt;/code&gt;, which we&apos;ll use to determine if the heart should float left or right&lt;/li&gt;
&lt;li&gt;A function called &lt;code&gt;getNextX()&lt;/code&gt; to determine the distance each heart should drift (using the &lt;code&gt;floatDirection&lt;/code&gt; to determine to which side it drifts)&lt;/li&gt;
&lt;li&gt;Replace the &lt;code&gt;y&lt;/code&gt; value in &lt;code&gt;gsap.to()&lt;/code&gt; with &lt;code&gt;motionPath&lt;/code&gt; config.
&lt;ol&gt;
&lt;li&gt;The hearts should turn to match their path direction. &lt;code&gt;autoRotate: 90&lt;/code&gt; sets the top of the image (default is right side) as the part that should face the path&lt;/li&gt;
&lt;li&gt;Setting &lt;code&gt;curviness: 1.25&lt;/code&gt; &quot;softens&quot; the float. Try setting it to &lt;code&gt;0&lt;/code&gt; to see the hearts follow a jagged path&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;Finally, add an &lt;a href=&quot;https://greensock.com/docs/v3/Eases&quot;&gt;easing function&lt;/a&gt; to give the float a bit more dynamism.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Make the following changes to &lt;code&gt;src/main.ts&lt;/code&gt; to update the animation:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	import { gsap } from &quot;gsap&quot;;
+	import { MotionPathPlugin } from &quot;gsap/all&quot;;

	import &quot;./style.css&quot;;

+	gsap.registerPlugin(MotionPathPlugin);

	const heartTemplate: HTMLTemplateElement = document.querySelector(&quot;#heart&quot;)!;
	const container = document.querySelector(&quot;.container&quot;)!;
	const button = document.querySelector(&quot;.button&quot;)!;

	// move hearts to a position 20% beyond the top of the container so they disappear
	const endY = container.clientHeight * -1.2;
	const w = container.clientWidth;

	button?.addEventListener(&quot;click&quot;, (event) =&amp;gt; {
		event.preventDefault();

		// create a new node from the img element in the template
		const heart = heartTemplate.content.firstElementChild!.cloneNode(true);

		// vary the size of the hearts a bit
		const width = gsap.utils.random(40, 70);

		// choose a random starting point along the width of the container
		const initialX = gsap.utils.random(0, w - width);

+		// randomize the initial direction of the float
+		const floatDirection = gsap.utils.random([-1, 1]);

+		// get a distance between the starting point &amp;amp; 200px in the given direction
+		const getNextX = (dir: number): number =&amp;gt; {
+			return gsap.utils.random(initialX, initialX + 200 * dir);
+		};

		// set initial values for the heart
		gsap.set(heart, {
			width,
			x: initialX,
		});

		// add the new node to the DOM inside the container
		container.appendChild(heart);

		// animate the heart from its starting position to the endY we defined above
		gsap.to(heart, {
			duration: 2,
-			y: endY,
+			motionPath: {
+				autoRotate: 90,
+				curviness: 1.25,
+				path: [
+					{
+						x: getNextX(floatDirection),
+						y: endY / gsap.utils.random(2, 4), // switch up the turning point
+					},
+					{
+						x: getNextX(floatDirection * -1), // reverse float direction
+						y: endY,
+					},
+				],
+			},
+			ease: &quot;power1.in&quot;,
			onComplete: () =&amp;gt; {
				container.removeChild(heart);
			},
		});
	});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Save and click the button in your browser. You&apos;ll see floaty animated hearts!&lt;/p&gt;
&lt;figure&gt;&lt;/figure&gt;
&lt;h2&gt;Give your animations a little more life with randomized values&lt;/h2&gt;
&lt;p&gt;Adding animation makes apps more engaging, more interactive, and more likely to be shared. And now that you know how to use randomization to make your animations feel a little more alive, you&apos;re well on your way to building engaging, interactive, and shareable apps of your own!&lt;/p&gt;
&lt;p&gt;Share what you build in the comments! Don&apos;t forget to like and subscribe. If you want to learn more about animation, check out the recommended video. See you next time.&lt;/p&gt;
&lt;h2&gt;Resources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/learnwithjason/gsap-randomized-animations&quot;&gt;Source code for this tutorial&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://gsap-randomized-animations.netlify.app/&quot;&gt;Demo of this tutorial&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://greensock.com/docs/&quot;&gt;GreenSock documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.codetv.dev/topic/greensock&quot;&gt;Episodes about GSAP&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Type-safe, data-driven apps, even if databases freak you out</title><link>https://codetv.dev/blog/convex-type-safe-database/</link><guid isPermaLink="true">https://codetv.dev/blog/convex-type-safe-database/</guid><description>If you can write a TypeScript type, you can add a database to your app. Databases can be fun! Even if you don’t think you’re a “database dev”, give this tutorial a try.
</description><pubDate>Tue, 28 Feb 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/f_auto,q_auto/v1677567785/lwj/blog/convex/convex-database-fun.jpg&quot; alt=&quot;Type-safe, data-driven apps, even if databases freak you out&quot; /&gt;&lt;/p&gt;&lt;p&gt;You can build a database-powered app with end-to-end type safety &lt;em&gt;and&lt;/em&gt; real-time updates without needing to learn how to manage databases. This tutorial will show you how.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Finished app: https://convex-dashboard-react.netlify.app/&lt;/li&gt;
&lt;li&gt;Source code: https://github.com/learnwithjason/dashboard-convex-react&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Prefer to watch instead of reading? I&apos;ve got you!&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/_oZt0NuCqh8&quot;&gt;Watch on YouTube&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If you learn better by watching and listening, I&apos;ve got a &lt;a href=&quot;https://youtu.be/_oZt0NuCqh8&quot;&gt;video version of this tutorial up on YouTube&lt;/a&gt;. (Don&apos;t forget to subscribe if it&apos;s useful!)&lt;/p&gt;
&lt;h2&gt;For frontend devs, databases can be intimidating&lt;/h2&gt;
&lt;p&gt;If you&apos;re a web developer like me, you probably have exciting ideas for new apps all the time.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Most of my app ideas hit the same challenge: building them would require setting up a database.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;And I don&apos;t know you, but when I start thinking about all the work that goes into setting up and managing a database, I always tend to land on the same solution: &lt;strong&gt;I convince myself that my app idea wasn&apos;t that good, and I give up.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;At least that&apos;s how I used to feel. These days, there&apos;s a new wave of innovation happening in the database space that&apos;s flattening the learning curve for web developers who want to add data to their apps, even if they&apos;d describe themselves as a frontend developer or — as &lt;a href=&quot;https://bradfrost.com/blog/post/front-of-the-front-end-and-back-of-the-front-end-web-development/&quot;&gt;Brad Frost once said&lt;/a&gt; — a &quot;front-of-the-frontend&quot; developer.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;a href=&quot;https://www.convex.dev/&quot;&gt;Convex&lt;/a&gt; is one of the companies pushing the
boundaries of how databases are added to modern web apps, and they&apos;ve
sponsored this tutorial to help more frontend devs get comfortable building
data-driven apps.&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;Build a data-powered app for reacting to dogs&lt;/h2&gt;
&lt;p&gt;This tutorial will step through adding a Convex database to a web app and using it to store information about dogs and how people have reacted to those dogs. It will have a home page where people can react, and a stats view for showing aggregate data about reactions for each dog.&lt;/p&gt;
&lt;p&gt;The app is built using React started from the &lt;a href=&quot;https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts&quot;&gt;Vite React TypeScript starter&lt;/a&gt;. (&lt;code&gt;npm create vite --template react-ts&lt;/code&gt;)&lt;/p&gt;
&lt;h3&gt;Clone the repo and get local development running&lt;/h3&gt;
&lt;p&gt;For this app, we&apos;ll start with the frontend already built out and styled. Clone the &lt;code&gt;start&lt;/code&gt; branch of the tutorial repo, install dependencies, and start the dev server:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# clone the start branch
git clone git@github.com:learnwithjason/dashboard-convex-react.git -b start

# move into the project
cd dashboard-convex-react/

# install dependencies
npm install

# start the local dev server
npm run dev
&lt;/code&gt;&lt;/pre&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; It&apos;s probably a good idea to fork this repo so you can easily deploy
it later. Forking is &lt;em&gt;not&lt;/em&gt; required for local development, though!&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;This is a &lt;a href=&quot;https://vitejs.dev/&quot;&gt;Vite&lt;/a&gt;-powered project, so the local dev server starts at &lt;code&gt;http://localhost:5173&lt;/code&gt;. Open it in your browser and you&apos;ll see the app:&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1600,f_auto,q_auto,b_rgb:4893b8/v1677203406/lwj/blog/convex/convex-local-dev.png&quot; alt=&quot;Screenshot of the demo app. It shows a grid of cute dog photos with reaction
buttons displayed on each
one.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;The home page shows a list of pups, each with a few buttons to let us react. If we toggle to the stats page, we can see the aggregate of how many times each reaction has been hit for each pup.&lt;/p&gt;
&lt;p&gt;Right now all the data is hard-coded. We&apos;re going to use Convex to store both our pup data and reaction data.&lt;/p&gt;
&lt;h3&gt;Install and start Convex in the web app&lt;/h3&gt;
&lt;p&gt;Keep the local dev server running in your terminal. Open up a second terminal and install the &lt;code&gt;convex&lt;/code&gt; package.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm i convex
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, start Convex in dev mode:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npx convex dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will take you through the process of setting up a Convex account, then ask you for the name of your project and generate &lt;code&gt;.env&lt;/code&gt; files for local and production development.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;dev&lt;/code&gt; command will stay running while we work to keep our local app connected to the Convex datastore.&lt;/p&gt;
&lt;h3&gt;Define a type-safe database schema&lt;/h3&gt;
&lt;p&gt;Because we want type safety and autocomplete, our first step is to define a schema. The &lt;code&gt;convex&lt;/code&gt; package provides the helpers we need to define the overall schema, data tables, and the field types within the table.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; It&apos;s not required to create a schema in Convex. If you don&apos;t care
about autocomplete and type checking, you can jump straight into writing
queries and mutations!&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;Create a new file at &lt;code&gt;convex/schema.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { defineSchema, defineTable, s } from &apos;convex/schema&apos;;

export default defineSchema({
  pups: defineTable({
    name: s.string(),
    photo: s.string(),
  }),
  reactions: defineTable({
    pup: s.id(&apos;pups&apos;),
    type: s.union(
      s.literal(&apos;heart&apos;),
      s.literal(&apos;cute&apos;),
      s.literal(&apos;star_eyes&apos;)
    ),
  }),
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(&lt;a href=&quot;https://github.com/learnwithjason/dashboard-convex-react/commit/21e28c58afb561f1e4b34adba25727738acde72b#diff-00e6e27e65e72a0fd4a73f6942f41e1cf3f476a59f263513f3f47fdf801a0eb1&quot;&gt;see this change on GitHub&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;The Convex CLI will automatically update once this file is saved, configuring everything on the backend for us so we don&apos;t need to deal with dashboards or additional config.&lt;/p&gt;
&lt;h2&gt;Wrap the React app with Convex provider&lt;/h2&gt;
&lt;p&gt;To make sure our app has access to Convex data, wrap it in the Convex provider component by making the following changes in &lt;code&gt;src/main.tsx&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	import React from &apos;react&apos;;
	import ReactDOM from &apos;react-dom/client&apos;;
+	import { ConvexProvider, ConvexReactClient } from &apos;convex/react&apos;;
	import { Toggle } from &apos;./components/toggle&apos;;

	import &apos;./styles/global.css&apos;;

+	const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL);

	ReactDOM.createRoot(document.getElementById(&apos;root&apos;) as HTMLElement).render(
		&amp;lt;React.StrictMode&amp;gt;
+			&amp;lt;ConvexProvider client={convex}&amp;gt;
				&amp;lt;Toggle /&amp;gt;
+			&amp;lt;/ConvexProvider&amp;gt;
			&amp;lt;footer&amp;gt;
				a &amp;lt;a href=&quot;https://www.codetv.dev/&quot;&amp;gt;CodeTV&amp;lt;/a&amp;gt; creation
				·{&apos; &apos;}
				&amp;lt;a href=&quot;https://github.com/learnwithjason/dashboard-convex-react&quot;&amp;gt;
					source code
				&amp;lt;/a&amp;gt;
			&amp;lt;/footer&amp;gt;
		&amp;lt;/React.StrictMode&amp;gt;,
	);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(&lt;a href=&quot;https://github.com/learnwithjason/dashboard-convex-react/commit/21e28c58afb561f1e4b34adba25727738acde72b#diff-1cd8b18798a1a103bfe13bef54354c1f3a3bea29a31c8eea1a0c67a3a839b811&quot;&gt;see this change on GitHub&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;Creating a client lets the provider know which Convex project we&apos;re working with, and the provider makes sure we can run queries and mutations from any component that&apos;s rendered inside the provider.&lt;/p&gt;
&lt;p&gt;The environment variable (&lt;code&gt;import.meta.env.VITE_CONVEX_URL&lt;/code&gt;) was automatically added to &lt;code&gt;.env.local&lt;/code&gt; when we started Convex.&lt;/p&gt;
&lt;h2&gt;Create and display pup entries&lt;/h2&gt;
&lt;p&gt;Next, we can define how we create and read data to the tables we just defined. Let&apos;s start with the pups.&lt;/p&gt;
&lt;p&gt;Create a new file at &lt;code&gt;convex/pups.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { mutation, query } from &apos;./_generated/server&apos;;

export const add = mutation(async ({ db }, name, photo) =&amp;gt; {
  await db.insert(&apos;pups&apos;, {
    name,
    photo,
  });
});

export const get = query(async ({ db }) =&amp;gt; {
  return await db.query(&apos;pups&apos;).collect();
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(&lt;a href=&quot;https://github.com/learnwithjason/dashboard-convex-react/commit/21e28c58afb561f1e4b34adba25727738acde72b#diff-be338b7a1a9698d97606cb5bc27e4c321c012350756d5f579b91a51d637e2333&quot;&gt;see this change on GitHub&lt;/a&gt;)&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;A quick set of definitions in case you&apos;re seeing these terms for the first time:&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;A &quot;mutation&quot; is how we change the database, such as creating a new entry or deleting data.&lt;/li&gt;
&lt;li&gt;A &quot;query&quot; is how we read things out of the database — in this case, reading out our list of pups.&lt;/li&gt;
&lt;/ul&gt;&lt;/aside&gt;
&lt;p&gt;Make sure to type out the &lt;code&gt;db.insert()&lt;/code&gt; part of this code in your editor. It&apos;s extremely cool that the function autocompletes with the names of our two schema tables. Plus, once we select a table, the second argument to &lt;code&gt;db.insert()&lt;/code&gt; knows what the fields are.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;This is a killer feature of Convex: the database is &lt;em&gt;automatically&lt;/em&gt; type safe and we haven&apos;t defined any types ourselves, let alone had to mess with configuration files. It Just Works™.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Next up, let&apos;s modify the home page to add pups to the database and read them out on screen.&lt;/p&gt;
&lt;p&gt;Open &lt;code&gt;src/components/list.tsx&lt;/code&gt; and make the following changes:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;+	import { useQuery, useMutation } from &apos;../../convex/_generated/react&apos;;
	import { seedPups, reactionTypes } from &apos;../util/helpers&apos;;

	export function List() {
-		const pups = seedPups;
+		const pups = useQuery(&apos;pups:get&apos;);
		const addPup = (..._args: any) =&amp;gt; {};
		const addReaction = (..._args: any) =&amp;gt; {};

		if (!pups) {
			return null;
		}

		if (Array.isArray(pups) &amp;amp;&amp;amp; pups.length === 0) {
			seedPups.forEach((pup) =&amp;gt; {
				addPup(pup.name, pup.photo);
			});
		}

		return (
			&amp;lt;div className=&quot;pups&quot;&amp;gt;
				{pups?.map((pup) =&amp;gt; {
					return (
						&amp;lt;div className=&quot;pup&quot; key={pup._id.toString()}&amp;gt;
							&amp;lt;h2&amp;gt;{pup.name}&amp;lt;/h2&amp;gt;
							&amp;lt;img src={pup.photo} alt={pup.name} /&amp;gt;

							&amp;lt;div className=&quot;reactions&quot;&amp;gt;
								{reactionTypes.map((reaction) =&amp;gt; {
									return (
										&amp;lt;button
											onClick={() =&amp;gt; addReaction(pup._id, reaction.name)}
											key={reaction.label + pup._id}
										&amp;gt;
											&amp;lt;span role=&quot;img&quot; aria-label={reaction.name}&amp;gt;
												{reaction.label}
											&amp;lt;/span&amp;gt;
										&amp;lt;/button&amp;gt;
									);
								})}
							&amp;lt;/div&amp;gt;
						&amp;lt;/div&amp;gt;
					);
				})}
			&amp;lt;/div&amp;gt;
		);
	}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(&lt;a href=&quot;https://github.com/learnwithjason/dashboard-convex-react/commit/21e28c58afb561f1e4b34adba25727738acde72b#diff-df1a65b9b185f82d4ce192f2602a03caf16ed177d71cbb7c277f09c911ee5409R1-R6&quot;&gt;see this change on GitHub&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;Did you see &lt;code&gt;useQuery()&lt;/code&gt; autocomplete with our pup query? Again: this Just Works™. Hover over &lt;code&gt;pups&lt;/code&gt; and you&apos;ll see what data each pup includes.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/q_auto,f_auto/v1677204195/lwj/blog/convex/convex-types.png&quot; alt=&quot;Hovering over the pups variable in VS Code, showing the popover card with
type
data&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;Right now, the home page will be empty since there aren&apos;t any pups in the database yet. Let&apos;s fix that.&lt;/p&gt;
&lt;p&gt;Update the &lt;code&gt;addPup()&lt;/code&gt; function to use a Convex mutation by making the following change:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	import { useQuery, useMutation } from &apos;../../convex/_generated/react&apos;;
	import { seedPups, reactionTypes } from &apos;../util/helpers&apos;;

	export function List() {
		const pups = useQuery(&apos;pups:get&apos;);
-		const addPup = (..._args: any) =&amp;gt; {};
+		const addPup = useMutation(&apos;pups:add&apos;);
		const addReaction = (..._args: any) =&amp;gt; {};

		if (!pups) {
			return null;
		}

		if (Array.isArray(pups) &amp;amp;&amp;amp; pups.length === 0) {
			seedPups.forEach((pup) =&amp;gt; {
				addPup(pup.name, pup.photo);
			});
		}

		return (
			&amp;lt;div className=&quot;pups&quot;&amp;gt;
				{/* ...unchanged... */}
			&amp;lt;/div&amp;gt;
		);
	}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(&lt;a href=&quot;https://github.com/learnwithjason/dashboard-convex-react/commit/21e28c58afb561f1e4b34adba25727738acde72b#diff-df1a65b9b185f82d4ce192f2602a03caf16ed177d71cbb7c277f09c911ee5409R7&quot;&gt;see this change on GitHub&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;The code is already written to check if the pups array is empty and add the seed data if so, which means once we save this file, our pup data will be sent to Convex and we&apos;ll see it loaded on the page.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Heads up!&lt;/strong&gt; In dev mode, React might fire off the code to add pups more than
once. If you see duplicate pups, you can open &lt;a href=&quot;https://dashboard.convex.dev&quot;&gt;the Convex
dashboard&lt;/a&gt; to delete them.&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;Create and display reactions&lt;/h2&gt;
&lt;p&gt;Next, we want users in our app to be able to react to each pup and see how other people have reacted to them as well.&lt;/p&gt;
&lt;p&gt;Let&apos;s start by defining a mutation to store new reactions and a query to read them out grouped by reaction type and the pup that was reacted to. In a new file at &lt;code&gt;convex/reactions.ts&lt;/code&gt;, add the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { mutation, query } from &apos;./_generated/server&apos;;
import { reactionTypes } from &apos;../src/util/helpers&apos;;

interface DataPoint {
  name: string;
  count: number;
}

interface DataSeries {
  label: string;
  data: DataPoint[];
}

export const add = mutation(async ({ db }, pup, type) =&amp;gt; {
  await db.insert(&apos;reactions&apos;, {
    pup,
    type,
  });
});

export const getByPup = query(async ({ db }) =&amp;gt; {
  const reactionsRaw = await db.query(&apos;reactions&apos;).collect();

  const reactions = await Promise.all(
    reactionsRaw.map(async (reaction) =&amp;gt; {
      const pupEntry = await db.get(reaction.pup);

      return { ...reaction, name: pupEntry?.name };
    })
  );

  return reactionTypes.reduce&amp;lt;DataSeries[]&amp;gt;((dataseries, { name, label }) =&amp;gt; {
    return [
      ...dataseries,
      {
        label,
        data: reactions
          .filter((r) =&amp;gt; r.type === name)
          .reduce&amp;lt;DataPoint[]&amp;gt;((datapoints, r) =&amp;gt; {
            const index = datapoints.findIndex((d) =&amp;gt; d.name === r.name);

            if (index &amp;gt;= 0) {
              datapoints[index].count += 1;
            } else if (r.name) {
              datapoints.push({
                name: r.name,
                count: 1,
              });
            }

            return datapoints;
          }, []),
      },
    ];
  }, []);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(&lt;a href=&quot;https://github.com/learnwithjason/dashboard-convex-react/commit/21e28c58afb561f1e4b34adba25727738acde72b#diff-209734f0e5cf2c31ac7d431c33e33763b6a499251eee423957d4c6d9f67939f2&quot;&gt;see this change on GitHub&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;The mutation is pretty straightforward, but that query might look a little intense. For the bar chart to work, we need to deliver reaction data in the following format:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[
	{
		&quot;label&quot;: &quot;💜&quot;,
		&quot;data&quot;: [
			{
				&quot;name&quot;: &quot;Floof&quot;,
				&quot;count&quot;: 23
			},
			...
		]
	},
	...
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The code inside the query uses the reaction data to load the right pup data, then uses nested reducers (😅) to shape the data into the format we need.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Heads up!&lt;/strong&gt; By doing this work inside the &lt;code&gt;query()&lt;/code&gt;, we get to take
advantage of Convex&apos;s built-in caching. Each time the data changes, the result
of this query is calculated once, then cached for subsequent requests.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;Next, let&apos;s hook up our buttons to save a new reaction in the database. Make one last change to &lt;code&gt;src/components/list.tsx&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	import { useQuery, useMutation } from &apos;../../convex/_generated/react&apos;;
	import { seedPups, reactionTypes } from &apos;../util/helpers&apos;;

	export function List() {
		const pups = useQuery(&apos;pups:get&apos;);
		const addPup = useMutation(&apos;pups:add&apos;);
-		const addReaction = (..._args: any) =&amp;gt; {};
+		const addReaction = useMutation(&apos;reactions:add&apos;);

		if (!pups) {
			return null;
		}

		if (Array.isArray(pups) &amp;amp;&amp;amp; pups.length === 0) {
			seedPups.forEach((pup) =&amp;gt; {
				addPup(pup.name, pup.photo);
			});
		}

		return (
			&amp;lt;div className=&quot;pups&quot;&amp;gt;
				{/* ...unchanged... */}
			&amp;lt;/div&amp;gt;
		);
	}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(&lt;a href=&quot;https://github.com/learnwithjason/dashboard-convex-react/commit/21e28c58afb561f1e4b34adba25727738acde72b#diff-df1a65b9b185f82d4ce192f2602a03caf16ed177d71cbb7c277f09c911ee5409R8&quot;&gt;see this change in GitHub&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;Save, then click a few of the reactions on your favorite pups to add some reaction entries to the database.&lt;/p&gt;
&lt;p&gt;To display these reaction stats, open up &lt;code&gt;src/components/bar-chart.tsx&lt;/code&gt; and make the following change:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	import React from &apos;react&apos;;
	import { useMemo } from &apos;react&apos;;
	import { Chart } from &apos;react-charts&apos;;
-	import { getPlaceholderReactionData } from &apos;../util/helpers&apos;;
+	import { useQuery } from &apos;../../convex/_generated/react&apos;;

	interface DataPoint {
		name: string;
		count: number;
	}

	export function BarChart() {
		const primaryAxis = React.useMemo(
			() =&amp;gt; ({
				getValue: (datum: DataPoint) =&amp;gt; datum.name,
				showGrid: false,
				innerBandPadding: 0.3,
				innerSeriesBandPadding: 0.05,
			}),
			[],
		);

		const secondaryAxes = useMemo(() =&amp;gt; {
			return [
				{
					getValue: (d: DataPoint) =&amp;gt; d.count,
					hardMin: 0,
					showGrid: false,
				},
			];
		}, []);

-		const reactions = getPlaceholderReactionData();
+		const reactions = useQuery(&apos;reactions:getByPup&apos;);

		if (!reactions) {
			return null;
		}

		return (
			&amp;lt;div className=&quot;chart-container&quot;&amp;gt;
				&amp;lt;Chart
					options={{
						data: reactions,
						primaryAxis,
						secondaryAxes,
						interactionMode: &apos;primary&apos;,
					}}
				/&amp;gt;
			&amp;lt;/div&amp;gt;
		);
	}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(&lt;a href=&quot;https://github.com/learnwithjason/dashboard-convex-react/commit/21e28c58afb561f1e4b34adba25727738acde72b#diff-a1cf688e82dd6afc212b22c8ffec795aea8c1e39977acd08dcaee75a044d275a&quot;&gt;see this change in GitHub&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;Save and navigate to the stats and you&apos;ll see that the reaction data now corresponds to how many reactions you&apos;ve added.&lt;/p&gt;
&lt;h2&gt;Deploy a Convex app to production&lt;/h2&gt;
&lt;p&gt;To deploy the app, follow the &lt;a href=&quot;https://docs.convex.dev/using/hosting/netlify&quot;&gt;deployment instructions in the Convex docs&lt;/a&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Commit all your changes (including the &lt;code&gt;.env&lt;/code&gt; file — the Convex URL is not sensitive) and get them up on GitHub&lt;/li&gt;
&lt;li&gt;Go to app.netlify.com and either sign in or create an account using your GitHub login&lt;/li&gt;
&lt;li&gt;Get your deploy key from the &lt;a href=&quot;https://dashboard.convex.dev/&quot;&gt;Convex Dashboard&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Add it as an environment variable called &lt;code&gt;CONVEX_DEPLOY_KEY&lt;/code&gt; in Netlify&lt;/li&gt;
&lt;li&gt;Set your build command to &lt;code&gt;npx convex deploy &amp;amp;&amp;amp; npm run build&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The site will build and deploy to a public URL. Open it up and add some reactions! Your database-powered app is now deployed and running!&lt;/p&gt;
&lt;h2&gt;But wait, there&apos;s more!&lt;/h2&gt;
&lt;p&gt;If getting a fully type-safe data workflow isn&apos;t cool enough for you, there&apos;s one more built-in bonus when you choose Convex as your data layer: &lt;strong&gt;real-time updates Just Work™.&lt;/strong&gt;&lt;/p&gt;
&lt;figure&gt;&lt;/figure&gt;
&lt;p&gt;Open your live site in two browser windows. On one, load the home page with all the pups. In the other, load the stats. Add some reactions and watch the chart update in real time! For even more fun, open the site on your phone and watch the browser update as you tap away on your favorite pups.&lt;/p&gt;
&lt;p&gt;This reactivity is automatic, so you don&apos;t have to configure anything for it to work. You&apos;ll even see realtime updates if you edit your data directly in the Convex dashboard!&lt;/p&gt;
&lt;h2&gt;Databases are for everyone now — even front-of-the-frontend developers&lt;/h2&gt;
&lt;p&gt;For web devs who would describe themselves as frontend developers or even &quot;front of the frontend devs&quot;, working with data might feel like a lot — but hopefully this tutorial made it feel a little more approachable.&lt;/p&gt;
&lt;p&gt;Get out there and build all those fun app ideas!&lt;/p&gt;
&lt;h2&gt;Resources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Finished app: https://convex-dashboard-react.netlify.app/&lt;/li&gt;
&lt;li&gt;Source code: https://github.com/learnwithjason/dashboard-convex-react&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://youtu.be/_oZt0NuCqh8&quot;&gt;Watch this app get built on YouTube&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.convex.dev/introduction/&quot;&gt;Convex documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://unsplash.com/&quot;&gt;Unsplash&lt;/a&gt; — images of pups came from here&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://react-charts.tanstack.com/&quot;&gt;React Charts&lt;/a&gt; — used for the stats view&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Typesafe Markdown With Astro Content Collections</title><link>https://codetv.dev/blog/typesafe-markdown-astro-content-collections/</link><guid isPermaLink="true">https://codetv.dev/blog/typesafe-markdown-astro-content-collections/</guid><description>Typesafe Markdown might sound like an oxymoron, but with the new content collections released in Astro 2.0, you can now specify a schema for your Markdown frontmatter using Zod and get all the delicious validation and autocomplete that comes with it.
</description><pubDate>Tue, 24 Jan 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;If you want to learn how to get typesafety and autocomplete into your Markdown blog, we&apos;ll go through the whole process of creating a brand new blog powered by Astro content collections in this article.&lt;/p&gt;
&lt;h2&gt;If you prefer video, I&apos;ve got you covered&lt;/h2&gt;
&lt;p&gt;If you prefer learning by watching something get built, I recorded building the demo app in this post as a video tutorial. &lt;a href=&quot;https://youtu.be/TfD4RW2gR-s&quot;&gt;Learn about Astro content collections and typesafe Markdown on YouTube.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/TfD4RW2gR-s&quot;&gt;Watch on YouTube&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Skip to the end&lt;/h2&gt;
&lt;p&gt;If you&apos;d rather jump straight to looking at the source code, you can find it here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Repo: https://github.com/learnwithjason/astro-content-collections&lt;/li&gt;
&lt;li&gt;Demo: https://astro-content-collections.netlify.app&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Step 1: Set up a new Astro site&lt;/h2&gt;
&lt;p&gt;To get started, create a new Astro site:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm create astro
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This command kicks off a guided experience (complete with a cute robot mascot) that walks through setting up a new Astro site.&lt;/p&gt;
&lt;p&gt;You will be asked to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Choose a name for the directory where the project will live&lt;/li&gt;
&lt;li&gt;Select &quot;an empty project&quot; so we can focus on content collections.&lt;/li&gt;
&lt;li&gt;Install npm dependencies.&lt;/li&gt;
&lt;li&gt;Initialize a new git repository
&lt;ul&gt;
&lt;li&gt;This is only necessary if you want to deploy this site. If you&apos;re just playing with content collections to learn, you can skip this step.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once the site is created, move into the project directory:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd astro-content-collections/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Start the project to make sure everything is running as expected:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Open up &lt;code&gt;localhost:3000&lt;/code&gt; in your browser and you should see this bare-bones page:&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;/images/astro-content-collections/new-astro-site-minimal.png&quot; alt=&quot;a new Astro site that only displays an H1 that says,
&amp;quot;Astro&amp;quot;&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;Open the project in your code editor of choice and you&apos;re ready to code!&lt;/p&gt;
&lt;h2&gt;Step 2: Add a layout&lt;/h2&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Heads up!&lt;/strong&gt; If you&apos;re already familiar with Astro and just looking to learn how content collections work, you can skip this step.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;To give your blog a cohesive feel, create a layout for the site at &lt;code&gt;src/layouts/default.astro&lt;/code&gt; that will be shared across all pages:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
const { title } = Astro.props;
---

&amp;lt;html lang=&quot;en&quot;&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;utf-8&quot; /&amp;gt;
    &amp;lt;link rel=&quot;icon&quot; type=&quot;image/svg+xml&quot; href=&quot;/favicon.svg&quot; /&amp;gt;
    &amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width&quot; /&amp;gt;
    &amp;lt;meta name=&quot;generator&quot; content=&quot;{Astro.generator}&quot; /&amp;gt;

    &amp;lt;title&amp;gt;{title}&amp;lt;/title&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;header&amp;gt;
      &amp;lt;a href=&quot;/&quot; rel=&quot;home&quot;&amp;gt;
        &amp;lt;span role=&quot;img&quot; aria-label=&quot;Cheese&quot;&amp;gt;🧀&amp;lt;/span&amp;gt;
      &amp;lt;/a&amp;gt;
      &amp;lt;nav&amp;gt;
        &amp;lt;a href=&quot;/&quot;&amp;gt;Blog&amp;lt;/a&amp;gt;
      &amp;lt;/nav&amp;gt;
    &amp;lt;/header&amp;gt;
    &amp;lt;main&amp;gt;
      &amp;lt;slot /&amp;gt;
    &amp;lt;/main&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;

&amp;lt;style is:global&amp;gt;
  * {
    box-sizing: border-box;
  }

  html {
    color: #484844;
    font-family: system-ui, -apple-system, BlinkMacSystemFont, &apos;Segoe UI&apos;, Roboto,
      Oxygen, Ubuntu, Cantarell, &apos;Open Sans&apos;, &apos;Helvetica Neue&apos;, sans-serif;
    font-size: 18px;
    line-height: 1.45;
  }

  body {
    margin: 0;
  }

  h1,
  h2 {
    color: #383833;
    line-height: 1.1;
  }

  img {
    max-width: 100%;
  }
&amp;lt;/style&amp;gt;

&amp;lt;style&amp;gt;
  header {
    align-items: center;
    background-color: bisque;
    display: flex;
    justify-content: space-between;
    padding: 0.5rem 5%;
  }

  a[rel=&apos;home&apos;] {
    font-size: 2.5rem;
    text-decoration: none;
  }

  nav a {
    color: #383833;
    text-decoration: none;
  }

  main {
    margin: 3rem auto;
    width: min(54ch, 90%);
  }
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Edit the home page to use the new layout&lt;/h3&gt;
&lt;p&gt;Update the home page to use the layout by editing &lt;code&gt;src/pages/index.astro&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
import Layout from &apos;../layouts/default.astro&apos;;
---

&amp;lt;Layout&amp;gt;
  &amp;lt;h1&amp;gt;Please Read My Excellent Blog&amp;lt;/h1&amp;gt;
  &amp;lt;p&amp;gt;
    I write the truth many are too afraid to speak. But I’m not afraid to speak.
    Speaking is just making mouth sounds, and to be honest it’s not very scary.
  &amp;lt;/p&amp;gt;
&amp;lt;/Layout&amp;gt;

&amp;lt;style&amp;gt;
  article {
    margin-top: 2rem;
  }

  h2 {
    font-size: 1.25rem;
  }

  h2 a {
    color: inherit;
    text-decoration: none;
  }

  h2 a:focus,
  h2 a:hover {
    text-decoration: underline;
  }
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run &lt;code&gt;npm run dev&lt;/code&gt; to see the updated site using the layout:&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;/images/astro-content-collections/astro-site-layout.png&quot; alt=&quot;the site using the layout, including a new header and other
styles&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Heads up!&lt;/strong&gt; Because this post isn&apos;t about the structure or styling of a blog, we won&apos;t cover how the HTML or CSS works in this project. If you have questions, definitely &lt;a href=&quot;https://jason.energy/links&quot;&gt;let me know&lt;/a&gt;!&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;Step 3: Define a content collection schema&lt;/h2&gt;
&lt;p&gt;Create a new directory at &lt;code&gt;src/content/&lt;/code&gt; — this is both where our content collections are defined and where the content of our blog posts will live.&lt;/p&gt;
&lt;p&gt;Create a config file at &lt;code&gt;src/content/config.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { z, defineCollection } from &apos;astro:content&apos;;

export const collections = {};
&lt;/code&gt;&lt;/pre&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Heads up!&lt;/strong&gt; If your editor complains about &lt;code&gt;astro:content&lt;/code&gt; not existing, run &lt;code&gt;npx astro sync&lt;/code&gt; to force the types to update and clear up the issue.&lt;/p&gt;&lt;/aside&gt;
&lt;h3&gt;Decide what fields you want in your blog metadata&lt;/h3&gt;
&lt;p&gt;For our blog, we want the following metadata to be supplied:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Draft status — we don&apos;t want to show work-in-progress posts on the live site&lt;/li&gt;
&lt;li&gt;Publish date&lt;/li&gt;
&lt;li&gt;Title&lt;/li&gt;
&lt;li&gt;Category — each post needs to have exactly one category, and we only want to allow two categories: &quot;food&quot; and &quot;wisdom&quot;&lt;/li&gt;
&lt;li&gt;Tags — posts can optionally add tags for grouping posts together&lt;/li&gt;
&lt;li&gt;Sharing details — for social sharing cards, search results, and other external services, each post can specify custom details:
&lt;ul&gt;
&lt;li&gt;Image&lt;/li&gt;
&lt;li&gt;Title&lt;/li&gt;
&lt;li&gt;Description&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In a standard Markdown blog, this is a lot of metadata and we&apos;d almost certainly forget things, add extra stuff, or otherwise make a mess of our frontmatter. That&apos;s what Astro&apos;s content collections are here to help us solve: we can now be strict about what frontmatter is allowed and required for each post and provide helpful error messages if things aren&apos;t set up properly!&lt;/p&gt;
&lt;p&gt;To do that, we&apos;ll use the &lt;code&gt;defineCollection&lt;/code&gt; helper and &lt;a href=&quot;https://zod.dev/&quot;&gt;Zod&lt;/a&gt;, a schema validation library that&apos;s included as &lt;code&gt;z&lt;/code&gt; for convenience.&lt;/p&gt;
&lt;p&gt;In &lt;code&gt;src/content/config.ts&lt;/code&gt;, define your blog schema:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { z, defineCollection } from &apos;astro:content&apos;;

export const collections = {
  blog: defineCollection({
    schema: z.object({
      draft: z.boolean().default(false),
      date: z.date().transform((str) =&amp;gt; new Date(str)),
      title: z.string(),
      category: z.enum([&apos;food&apos;, &apos;wisdom&apos;]),
      tags: z.array(z.string()).optional(),
      share: z
        .object({
          image: z.string().url().optional(),
          title: z.string(),
          description: z.string(),
        })
        .strict(),
    }),
  }),
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Zod provides a clear API for defining how our frontmatter should look.&lt;/p&gt;
&lt;p&gt;For the most part, Zod works by defining the property of the schema and using one of Zod&apos;s types as the value. For example, &lt;code&gt;title: z.string()&lt;/code&gt; lets the schema know that a title must be set and it must be a string.&lt;/p&gt;
&lt;p&gt;For more specialized use cases, we can add defaults and transforms, as well as marking things as optional. This is done using &lt;a href=&quot;https://zod.dev/?id=schema-methods&quot;&gt;schema methods&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;For example, the &lt;code&gt;draft&lt;/code&gt; field can default to &lt;code&gt;false&lt;/code&gt; so that the field can be omitted on publishable posts. We specify that by chaining &lt;code&gt;.default(false)&lt;/code&gt; onto the property definition.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;date&lt;/code&gt; field will be used as a JavaScript &lt;code&gt;Date&lt;/code&gt; object, so we can use the &lt;code&gt;.transform()&lt;/code&gt; helper to convert the string representation of the date in frontmatter into a &lt;code&gt;Date&lt;/code&gt; object for use in our code.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;share&lt;/code&gt; object uses &lt;code&gt;.strict()&lt;/code&gt; to ensure that no additional fields are added to it.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Heads up!&lt;/strong&gt; There&apos;s a whole lot that can be done with Zod that we won&apos;t
cover in this post. For full details, see the &lt;a href=&quot;https://zod.dev/&quot;&gt;Zod docs&lt;/a&gt;.&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;Step 4: Show a list of blog posts on the blog home page&lt;/h2&gt;
&lt;p&gt;With the definitions in place, we can start using the collection to display blog posts on our site! Update &lt;code&gt;src/pages/index.astro&lt;/code&gt; to pull in our blog collection and display it on the home page:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
import { getCollection } from &apos;astro:content&apos;;
import Layout from &apos;../layouts/default.astro&apos;;

const posts = await getCollection(&apos;blog&apos;, (post) =&amp;gt; {
  return import.meta.env.MODE !== &apos;production&apos; || post.data.draft === false;
});
---

&amp;lt;Layout&amp;gt;
  &amp;lt;h1&amp;gt;Please Read My Excellent Blog&amp;lt;/h1&amp;gt;
  &amp;lt;p&amp;gt;
    I write the truth many are too afraid to speak. But I’m not afraid to speak.
    Speaking is just making mouth sounds, and to be honest it’s not very scary.
  &amp;lt;/p&amp;gt;

  {
    posts.map((post) =&amp;gt; {
      return (
        &amp;lt;article&amp;gt;
          &amp;lt;h2&amp;gt;
            &amp;lt;a href={`/blog/${post.slug}`}&amp;gt;{post.data.share.title}&amp;lt;/a&amp;gt;
          &amp;lt;/h2&amp;gt;
          &amp;lt;p&amp;gt;{post.data.share.description}&amp;lt;/p&amp;gt;
          &amp;lt;p&amp;gt;
            &amp;lt;a href={`/blog/${post.slug}`}&amp;gt;full post &amp;amp;rarr;&amp;lt;/a&amp;gt;
          &amp;lt;/p&amp;gt;
        &amp;lt;/article&amp;gt;
      );
    })
  }
&amp;lt;/Layout&amp;gt;

&amp;lt;style&amp;gt;
  /* styles are unchanged */
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;getCollection&lt;/code&gt; helper allows us to pull in everything in the &lt;code&gt;blog&lt;/code&gt; collection, which we then filter to show posts where &lt;code&gt;draft&lt;/code&gt; is &lt;code&gt;true&lt;/code&gt; in development mode, but not in production.&lt;/p&gt;
&lt;p&gt;In the page body, we map over the loaded posts and add markup to display a preview of each post.&lt;/p&gt;
&lt;p&gt;If we save this, nothing changes in the browser. This is a good thing: we haven&apos;t added any blog posts yet! Empty collections don&apos;t throw errors. We &lt;em&gt;could&lt;/em&gt; add logic to check for an empty &lt;code&gt;posts&lt;/code&gt; array and show an empty state, but in our case we&apos;re going to publish a couple blogs right away, so we won&apos;t need it.&lt;/p&gt;
&lt;h2&gt;Step 5: Create your first blog posts&lt;/h2&gt;
&lt;p&gt;Create your first blog post by adding the following to a new file at &lt;code&gt;src/content/blog/cheese.md&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
draft: true

date: 2023-01-14
title: Eat Cheese Every Day

category: food
tags:
  - gouda
  - cheddar
  - brie

share:
  image: https://res.cloudinary.com/jlengstorf/image/upload/v1674096555/blog/eat-cheese-every-day.jpg
  title: One Mind-Blowing Life Hack That Will Change The Way You Eat
  description: &amp;gt;
    Forget about keto, Atkins, Whole 30, and every other diet — this revolutionary breakthrough in how we eat will forever change your relationship with food.
---

The new food pyramid:

![a food pyramid where every tier is cheese](https://res.cloudinary.com/jlengstorf/image/upload/v1674096555/blog/eat-cheese-every-day.jpg)

Eat cheese every day and your whole life will change.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The home page will now show the blog preview:&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;/images/astro-content-collections/astro-content-collection-blog-listing.png&quot; alt=&quot;the site showing the title and description of the first
post&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;Create a second post by adding the following to a new file at &lt;code&gt;src/content/blog/good-advice.md&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
draft: false

date: 2023-01-17
title: The Yellow Ones Don’t Stop

tags:
  - survival

share:
  image: https://res.cloudinary.com/jlengstorf/image/upload/v1674108800/blog/yellow-ones-dont-stop.jpg
  title: The Harrowing Truth of Living In New York, Exposed!
  description: &amp;gt;
    The Big Apple may be called a concrete jungle, but if you don’t have the cheat codes for this game of Frogger then it’ll be game over for you, friendo.
---

Learn to spot New York’s apex predator.

![a yellow cab with the text overlay “the color of death”](https://res.cloudinary.com/jlengstorf/image/upload/v1674108800/blog/yellow-ones-dont-stop.jpg)

They’re everywhere. They’re looking for you. And they’re _very_ angry.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After saving, the homepage is now showing an error!&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;/images/astro-content-collections/astro-content-collection-validation-error.png&quot; alt=&quot;an Astro error informing the developer that the frontmatter for the new post
failed
validation&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;The error message says:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;blog → good-advice.md frontmatter does not match collection schema.
&quot;category&quot; is required.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What a great error message! We forgot to add a &lt;code&gt;category&lt;/code&gt; field to the frontmatter, and since that one is required the schema validation fails.&lt;/p&gt;
&lt;p&gt;This is ideal, because without the typesafety we might not notice this until our build failed, or — worse — until a user let us know that our site is broken.&lt;/p&gt;
&lt;p&gt;Edit &lt;code&gt;src/content/blog/good-advice.md&lt;/code&gt; to include &lt;code&gt;category: wisdom&lt;/code&gt; in the frontmatter, then save and check out the home page again. Everything works as expected!&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;/images/astro-content-collections/astro-content-collection-multiple-posts.png&quot; alt=&quot;the home page showing two blog
previews&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;Right now, though, if we click to read one of these posts we&apos;ll get a 404. In the next section we&apos;ll create individual blog pages from the collection.&lt;/p&gt;
&lt;h2&gt;Step 6: Create individual blog pages from an Astro content collection&lt;/h2&gt;
&lt;p&gt;Since we don&apos;t want to create an individual page every time we write a blog post, we&apos;ll use a &lt;a href=&quot;https://docs.astro.build/en/core-concepts/routing/?utm_source=jason-lengstorf&amp;amp;utm_medium=link&amp;amp;utm_term=2.0+launch&amp;amp;utm_campaign=blog#dynamic-routes&quot;&gt;dynamic route&lt;/a&gt; for our blog posts.&lt;/p&gt;
&lt;p&gt;Create a new file at &lt;code&gt;src/pages/blog/[slug].astro&lt;/code&gt; and add the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
import { CollectionEntry, getCollection } from &apos;astro:content&apos;;
import Layout from &apos;../../layouts/default.astro&apos;;

export async function getStaticPaths() {
  const posts = await getCollection(&apos;blog&apos;);

  return posts.map((post) =&amp;gt; {
    return {
      params: {
        slug: post.slug,
      },
      props: {
        post,
      },
    };
  });
}

interface Props {
  post: CollectionEntry&amp;lt;&apos;blog&apos;&amp;gt;;
}

const { post } = Astro.props;
const { Content } = await post.render();
---

&amp;lt;Layout&amp;gt;
  &amp;lt;header&amp;gt;
    &amp;lt;h1&amp;gt;{post.data.title}&amp;lt;/h1&amp;gt;
    &amp;lt;p&amp;gt;{post.data.share.description}&amp;lt;/p&amp;gt;
  &amp;lt;/header&amp;gt;

  &amp;lt;Content /&amp;gt;

  &amp;lt;footer&amp;gt;
    &amp;lt;ul class=&quot;tags&quot;&amp;gt;
      {post.data.tags?.map((tag) =&amp;gt; &amp;lt;li&amp;gt;{tag}&amp;lt;/li&amp;gt;)}
    &amp;lt;/ul&amp;gt;
    &amp;lt;p&amp;gt;
      &amp;lt;a href=&quot;/&quot;&amp;gt;&amp;amp;larr; back to all posts&amp;lt;/a&amp;gt;
    &amp;lt;/p&amp;gt;
  &amp;lt;/footer&amp;gt;
&amp;lt;/Layout&amp;gt;

&amp;lt;style&amp;gt;
  header {
    margin-bottom: 2rem;
  }

  header p {
    font-size: 1.125rem;
  }

  footer {
    border-top: 1px solid #d8d8d3;
    margin-top: 2rem;
  }

  .tags {
    color: #787873;
    display: flex;
    gap: 1rem;
    list-style: none;
    margin-bottom: 2rem;
    padding: 0;
  }

  .tags li::before {
    content: &apos;#&apos;;
  }
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Using &lt;code&gt;getCollection&lt;/code&gt; again, we load all the blog posts, then use &lt;code&gt;getStaticPaths&lt;/code&gt; let Astro know the slug and post data for each of our blog posts.&lt;/p&gt;
&lt;p&gt;Then, we grab the &lt;code&gt;post&lt;/code&gt; out of the props and define our markup in the Astro page body.&lt;/p&gt;
&lt;p&gt;To get autocomplete, we define an interface for &lt;code&gt;Props&lt;/code&gt; that uses the &lt;code&gt;CollectionEntry&amp;lt;&apos;blog&apos;&amp;gt;&lt;/code&gt; type for our post. This is &lt;em&gt;incredibly&lt;/em&gt; handy because it means we can press &lt;code&gt;control&lt;/code&gt; + &lt;code&gt;space&lt;/code&gt; to pull up a list of all the available properties on our &lt;code&gt;post&lt;/code&gt; object.&lt;/p&gt;
&lt;p&gt;Loading the Markdown content of the post is done by calling &lt;code&gt;await post.render()&lt;/code&gt;, which returns an object including a &lt;code&gt;Content&lt;/code&gt; component that we can place wherever we want the post body to be displayed.&lt;/p&gt;
&lt;p&gt;Save this, click on one of the blog posts, and you&apos;ve got a fully functional, fully typesafe Markdown blog running using Astro content collections!&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;/images/astro-content-collections/astro-single-post.png&quot; alt=&quot;a single post displayed on the
site&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;h2&gt;Resources and next steps&lt;/h2&gt;
&lt;p&gt;Congrats on building your first content collection-powered Astro site!&lt;/p&gt;
&lt;p&gt;To see this in action and review the source code, check the following links:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Repo: https://github.com/learnwithjason/astro-content-collections&lt;/li&gt;
&lt;li&gt;Demo: https://astro-content-collections.netlify.app&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Additional resources:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://astro.build/blog/astro-2/?utm_source=jason-lengstorf&amp;amp;utm_medium=link&amp;amp;utm_term=2.0+launch&amp;amp;utm_campaign=blog&quot;&gt;Read the Astro 2.0 announcement&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.astro.build/en/guides/content-collections/?utm_source=jason-lengstorf&amp;amp;utm_medium=link&amp;amp;utm_term=2.0+launch&amp;amp;utm_campaign=blog&quot;&gt;Astro docs on content collections&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://youtu.be/TfD4RW2gR-s&quot;&gt;Watch this tutorial as a video&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Hardware Accelerated Video Encoding on Apple Silicon (M1 Max, etc.) With FFmpeg</title><link>https://codetv.dev/blog/hardware-acceleration-ffmpeg-apple-silicon/</link><guid isPermaLink="true">https://codetv.dev/blog/hardware-acceleration-ffmpeg-apple-silicon/</guid><description>If you have an M1, M2, or other Apple Silicon chip in your computer, you can use hardware acceleration to speed up FFmpeg video encoding.
</description><pubDate>Thu, 19 Jan 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I &lt;a href=&quot;https://www.codetv.dev&quot;&gt;make a lot of video&lt;/a&gt;, and I work with &lt;a href=&quot;https://lemonproductions.ca&quot;&gt;an editor&lt;/a&gt; to polish things up before they go out into the world. My cameras&apos; recordings are huge, though (upwards of 50GB each), so we were running into time and storage issues trying to sync these huge files back and forth.&lt;/p&gt;
&lt;p&gt;To fix it, I use &lt;a href=&quot;https://ffmpeg.org/&quot;&gt;FFmpeg&lt;/a&gt; to transcode the videos at a smaller size. When I first started doing this, it took &lt;em&gt;forever&lt;/em&gt;. My recordings are hours long, and running FFmpeg with a software encoder was taking nearly as long as the video itself to encode — when I need to send over 3 camera angles of a 3-hour video, that&apos;s a full day of monitoring FFmpeg!&lt;/p&gt;
&lt;h2&gt;Use your Apple Silicon (M1 / M2 Max, etc.) to run FFmpeg on multiple cores with hardware acceleration&lt;/h2&gt;
&lt;p&gt;Fortunately, I have a computer with the M1 Max chip in it, so I can use hardware acceleration and the chip&apos;s multiple cores to drastically speed up FFmpeg on my MacBook Pro.&lt;/p&gt;
&lt;p&gt;Here&apos;s the command I use (pulled together from &lt;a href=&quot;https://superuser.com/questions/1671831/parallel-transcoding-with-ffmpeg-on-m1-mac&quot;&gt;helpful answers like this one&lt;/a&gt;) broken up by option with a comment explaining what each one does:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# command for easy copy-paste (replace filenames!)
ffmpeg -i original-file.mp4 -an -c:v h264_videotoolbox -q:v 50 output-file.mp4

# same command, broken up for legibility with comments:
ffmpeg \
  -i original-file.mp4 \   # the input file name
  -an \                    # this drops the audio entirely
  -c:v h264_videotoolbox \ # use Apple Silicon hardware acceleration
  -q:v 50 \                # quality (0 is worst, 100 is best)
  output-file.mp4          # output file name
&lt;/code&gt;&lt;/pre&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;HEADS UP:&lt;/strong&gt; I record audio and video separately, so this command &lt;em&gt;removes&lt;/em&gt;
audio entirely using &lt;code&gt;-an&lt;/code&gt; to save time and space. If you don&apos;t want to remove
audio, remove the &lt;code&gt;-an&lt;/code&gt; option and use &lt;code&gt;-acodec copy&lt;/code&gt; instead.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;Switching to the Video Toolbox hardware encoder sped things up to roughly 8.5x speed, meaning a 10-minute video will finish encoding in about 1.2 minutes. Encoding a full 3-hour video took just 21 minutes 34 seconds and dropped the file size from 54.83GB to 2.23GB.&lt;/p&gt;
&lt;p&gt;For comparison, using software encoding was closer to 1.5x speed.&lt;/p&gt;
&lt;p&gt;Now I can get all camera angles encoded in a couple hours and send less than 20GB of video to my editor. A huge win for a small change to my FFmpeg command setup!&lt;/p&gt;</content:encoded></item><item><title>I’m Going Full-Time on Learn With Jason</title><link>https://codetv.dev/blog/full-time-learn-with-jason/</link><guid isPermaLink="true">https://codetv.dev/blog/full-time-learn-with-jason/</guid><description>Hey, friends! Big news!
</description><pubDate>Mon, 05 Dec 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;I’m now full-time on &lt;em&gt;Learn With Jason&lt;/em&gt;!&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Since the show started in 2018, I’ve made over 300 episodes with close to 250 experts from around the web development community. Episodes of LWJ are now watched more than half a million times every year, with tens of thousands of developers subscribing to the show on YouTube, Twitch, and &lt;a href=&quot;/newsletter&quot;&gt;the &lt;em&gt;Learn With Jason&lt;/em&gt; newsletter&lt;/a&gt;.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/q_auto,f_auto/v1670193784/lwj/blog/then-and-now-comparison.jpg&quot; alt=&quot;A comparison of Jason’s streaming setup in 2018 for the first episode of Learn With Jason and in December 2022 for the most recent episode before this article was published. The 2018 image has a blurry image of the potato emoji underneath it. The 2022 image has an in-focus potato wearing sunglasses with sparkles around it.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;But until now, the show has always been what I’ve done &lt;em&gt;in addition to&lt;/em&gt; my day job. Most weeks I only had enough time to show up and record the episodes — there was no time left over for making the show better or exploring new ideas.&lt;/p&gt;
&lt;h2&gt;That’s why I’m incredibly excited to announce that &lt;em&gt;Learn With Jason&lt;/em&gt; is now my full-time job.&lt;/h2&gt;
&lt;p&gt;Focusing fully on LWJ will allow me to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Improve the quality of the show (e.g. 1080p streaming)&lt;/li&gt;
&lt;li&gt;Develop and release new features (e.g. &lt;a href=&quot;/newsletter&quot;&gt;a weekly newsletter!&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Experiment with new ideas&lt;/li&gt;
&lt;li&gt;Create additional content (e.g. more blogs, more videos)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I have a big ol’ backlog of things that I’ve been putting off, saying, “I’d love to do that — if only I had time.” I have time now. Let’s go!&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://lwj.dev/newsletter&quot;&gt;Subscribe to my newsletter for more like this!&lt;/a&gt;&lt;/p&gt;
&lt;p&gt; &lt;/p&gt;
&lt;h2&gt;How will &lt;em&gt;Learn With Jason&lt;/em&gt; pay the bills?&lt;/h2&gt;
&lt;p&gt;The show has had sponsors (&lt;a href=&quot;/&quot;&gt;check them out on the home page!&lt;/a&gt;) for quite a while now, and that pays the majority of the bills. If your company is interested in sponsoring the show, get in touch at &lt;a href=&quot;mailto:info@learnwithjason.dev&quot;&gt;info@learnwithjason.dev&lt;/a&gt;!&lt;/p&gt;
&lt;p&gt;In addition to sponsorship, I plan on offering a few services starting in 2023:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I’ll run workshops for companies to help teams create and/or improve their devrel programs&lt;/li&gt;
&lt;li&gt;I’ll partner with companies to create ambitious media and events (like &lt;a href=&quot;https://www.youtube.com/watch?v=gUlAMMborUI&amp;amp;list=PLzlG0L9jlhEM44HpXTZa_AP0CGCAUAUce&quot;&gt;this project for Netlify&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Since 2020, many of the “tried and true” ideas for connecting with developer communities don’t work as well anymore. I’ve been proving out new and different strategies, and I’m available to help your company adapt to the modern developer ecosystem.&lt;/p&gt;</content:encoded></item><item><title>Build a CSS Theme Switcher With No Flash of the Wrong Theme</title><link>https://codetv.dev/blog/css-color-theme-switcher-no-flash/</link><guid isPermaLink="true">https://codetv.dev/blog/css-color-theme-switcher-no-flash/</guid><description>Many JavaScript and CSS theme switchers have a momentary flash of the wrong theme. With edge functions, we can make that a thing of the past.
</description><pubDate>Wed, 12 Oct 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Not too long ago, the introduction of React Context led to a wave of color theme switchers. For a brief, glorious moment, color theme switchers dethroned TODO apps as the default tutorial.&lt;/p&gt;
&lt;p&gt;However, these color theme switchers all shared a flaw: they were all (a little bit) broken. When you selected a non-default theme, they would either:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Briefly flash the wrong color theme (which &lt;a href=&quot;https://css-tricks.com/flash-of-inaccurate-color-theme-fart/&quot;&gt;Chris Coyier dubbed FART&lt;/a&gt;, as only Chris Coyier can)&lt;/li&gt;
&lt;li&gt;Delay the rendering of the page until client-side JavaScript is able to load and execute to update the theme&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Both of these workarounds aren&apos;t ideal, and there haven&apos;t been many (any?) solutions offered that don&apos;t require running a server. In fact, Chris summed up his article on this problem by saying:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I’m not convinced there is a good way to avoid FART without a server-side language or force-delayed page renders.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;But — good news, everybody! — I have a solution that will allow any site, whether it&apos;s built with a server-side language, a JS framework, or plain ol&apos; HTML and CSS, to add a color theme switcher that works without client-side JS and doesn&apos;t delay the page render.&lt;/p&gt;
&lt;p&gt;To do this, we&apos;re going to use edge computing, which might sound like a lot, but only requires about 60 lines of code and a free Netlify account to add.&lt;/p&gt;
&lt;p&gt;Jump to the end: &lt;a href=&quot;https://theme-switcher-no-flash.netlify.app/&quot;&gt;demo&lt;/a&gt; · &lt;a href=&quot;https://github.com/learnwithjason/theme-switcher-no-flash&quot;&gt;source code&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;1. Start with a basic HTML and CSS site&lt;/h2&gt;
&lt;p&gt;For this tutorial, we&apos;ll be working from &lt;a href=&quot;https://github.com/jlengstorf/theme-switcher-no-flash&quot;&gt;this starter repo&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The solution we&apos;re building requires the use of &lt;a href=&quot;https://docs.netlify.com/edge-functions/overview/&quot;&gt;Netlify Edge Functions&lt;/a&gt;, which means we&apos;ll need the Netlify CLI for local testing and to deploy the site to Netlify.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Reminder:&lt;/strong&gt; all of the Netlify features we&apos;ll be using are free.&lt;/p&gt;&lt;/aside&gt;
&lt;h3&gt;Clone and deploy the repo&lt;/h3&gt;
&lt;p&gt;For convenience, &lt;a href=&quot;http://app.netlify.com/start/deploy?repository=https://github.com/jlengstorf/theme-switcher-no-flash&quot;&gt;click here to create and deploy the repo all at once&lt;/a&gt;. This will take you through Netlify&apos;s deployment flow, connect your GitHub, create a copy of the repo, and deploy it to your Netlify account.&lt;/p&gt;
&lt;h3&gt;Set up local development&lt;/h3&gt;
&lt;p&gt;Once you&apos;ve got that set up, set up the repo locally (make sure to replace &lt;code&gt;&amp;lt;username&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;repo&amp;gt;&lt;/code&gt; with your own username and repo name):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# clone your copy of the repo
git clone git@github.com:&amp;lt;username&amp;gt;/&amp;lt;repo&amp;gt;.git

# move into the cloned repo
cd &amp;lt;repo&amp;gt;

# OPTIONAL: install the Netlify CLI globally
# (v12.1.0 at time of writing)
npm i -g netlify-cli

# start the Netlify CLI
ntl dev

# or, if you don&apos;t have the CLI installed globally:
# npx ntl dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will start the site at &lt;code&gt;localhost:8888&lt;/code&gt;:&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;/images/css-theme-switcher-01.png&quot; alt=&quot;The CSS theme switcher demo running locally&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;There is a theme selection dropdown in the sidebar. Right now, choosing a new theme doesn&apos;t change anything — it just adds a &lt;code&gt;?theme=&amp;lt;selected_theme&amp;gt;&lt;/code&gt; to the URL.&lt;/p&gt;
&lt;h3&gt;Get familiar with the code&lt;/h3&gt;
&lt;p&gt;Inside the repo, you&apos;ll see a &lt;code&gt;src&lt;/code&gt; folder that has two HTML files and a CSS file inside.&lt;/p&gt;
&lt;p&gt;Open &lt;code&gt;src/index.html&lt;/code&gt; and take a look at the top of the file:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;!--     ℹ️ this is the important thing --&amp;gt;
&amp;lt;!--            ⌄⌄⌄⌄⌄⌄⌄⌄⌄⌄⌄⌄⌄⌄⌄⌄⌄⌄    --&amp;gt;
&amp;lt;html lang=&quot;en&quot; data-theme=&quot;default&quot;&amp;gt;
  &amp;lt;!--          ^^^^^^^^^^^^^^^^^^^^    --&amp;gt;
  &amp;lt;head&amp;gt;&amp;lt;/head&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;data-theme=&quot;default&quot;&lt;/code&gt; is the critical piece of this markup.&lt;/p&gt;
&lt;p&gt;Below that, there&apos;s a form allowing people to choose their theme:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;aside&amp;gt;
  &amp;lt;h2&amp;gt;Choose a Theme&amp;lt;/h2&amp;gt;
  &amp;lt;form&amp;gt;
    &amp;lt;select title=&quot;Choose a theme&quot; name=&quot;theme&quot;&amp;gt;
      &amp;lt;option value=&quot;default&quot;&amp;gt;System Default&amp;lt;/option&amp;gt;
      &amp;lt;option value=&quot;light&quot;&amp;gt;Light&amp;lt;/option&amp;gt;
      &amp;lt;option value=&quot;dark&quot;&amp;gt;Dark&amp;lt;/option&amp;gt;
      &amp;lt;option value=&quot;pink&quot;&amp;gt;Pink&amp;lt;/option&amp;gt;
    &amp;lt;/select&amp;gt;
    &amp;lt;button type=&quot;submit&quot;&amp;gt;Switch Theme&amp;lt;/button&amp;gt;
  &amp;lt;/form&amp;gt;
&amp;lt;/aside&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The rest of the markup sets up the example page, but doesn&apos;t have any impact on the theme switcher functionality and can be ignored.&lt;/p&gt;
&lt;p&gt;Next, open up &lt;code&gt;src/styles.css&lt;/code&gt; and look at the top of the file.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;html[data-theme=&apos;default&apos;],
html[data-theme=&apos;light&apos;] {
  --color-bg: #fcfaff;
  --color-header-bg: #2a1f3f;
  --color-header-text: #fcfaff;
  --color-border: #54495f;
  --color-link: #1600ed;
  --color-text: #464064;
  --color-text-heading: #1d1036;
  --color-text-muted: #575280;
}

html[data-theme=&apos;dark&apos;] {
  --color-bg: #242526;
  --color-header-bg: #18111f;
  --color-header-text: #e9e9f1;
  --color-border: #54495f;
  --color-link: #617ff5;
  --color-text: #cdcdcf;
  --color-text-heading: #dedeee;
  --color-text-muted: #bebebf;
}

/* ...full file omitted for brevity... */
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By targeting the &lt;code&gt;html&lt;/code&gt; element using the &lt;code&gt;data-theme&lt;/code&gt; attribute, we&apos;re able to update the color theme by changing CSS variable values.&lt;/p&gt;
&lt;p&gt;To see this in action, open the inspector in your browser and edit the attribute to &lt;code&gt;data-theme=&quot;pink&quot;&lt;/code&gt;. This will cause the site&apos;s theme to change.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;/images/css-theme-switcher-02.png&quot; alt=&quot;CSS theme switcher showing the pink theme&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;For our color switcher to work, we need to handle form submissions from the theme chooser, store that value somewhere, then use the value to update the &lt;code&gt;data-theme&lt;/code&gt; attribute.&lt;/p&gt;
&lt;h2&gt;2. Create an edge function&lt;/h2&gt;
&lt;h3&gt;What is an edge function?&lt;/h3&gt;
&lt;p&gt;An edge function is a small piece of business logic that&apos;s executed at the network&apos;s edge (which is another way of saying &quot;as close to the user as possible&quot;).&lt;/p&gt;
&lt;p&gt;This logic is executed between receiving the request from the user and returning the response from the network. An edge function can modify the response before it&apos;s returned — if you&apos;ve ever worked with middleware before, it&apos;s similar to that.&lt;/p&gt;
&lt;p&gt;The outcome of using edge functions is that you can modify responses based on details about the specific request — the user&apos;s cookies, geolocation, or other data sent in the request — without requiring the whole site to be powered by a server or sending client-side JavaScript. This allows us to create personalized experiences in a way that&apos;s performant and pleasant to use without a steep learning curve or changing our entire codebase to support edge functions.&lt;/p&gt;
&lt;h3&gt;Create a Netlify Edge Function in your project&lt;/h3&gt;
&lt;p&gt;To set up a Netlify Edge Function, create a new folder called &lt;code&gt;netlify&lt;/code&gt;, and another folder called &lt;code&gt;edge-functions&lt;/code&gt; inside of it. Edge functions will be automatically detected by Netlify when they&apos;re stored in this folder structure.&lt;/p&gt;
&lt;p&gt;Create a new file at &lt;code&gt;netlify/edge-functions/color-theme.ts&lt;/code&gt;. Inside, let’s create the &quot;hello world&quot; of edge functions:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export default async () =&amp;gt; {
  return new Response(&apos;hello world&apos;);
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, we need to configure which route or routes should trigger the edge function. For a color theme, we want that to affect every page on the site, so we&apos;ll target all routes.&lt;/p&gt;
&lt;p&gt;Open &lt;code&gt;netlify.toml&lt;/code&gt; at the root of your project and add the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  [build]
    publish = &quot;src&quot;

+ [[edge_functions]]
+   path = &quot;/*&quot;
+   function = &quot;color-theme&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Stop the server (&lt;code&gt;ctrl + C&lt;/code&gt; in your terminal), then restart it using &lt;code&gt;ntl dev&lt;/code&gt; — &lt;code&gt;localhost:8888&lt;/code&gt; is replaced by the response from our edge function.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;/images/css-theme-switcher-03.png&quot; alt=&quot;output of the edge function taking over the local dev site&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;h3&gt;Return the original response through the edge function&lt;/h3&gt;
&lt;p&gt;If an edge function doesn&apos;t return anything, by default the page will remain unchanged. This can be helpful for logging or setting cookies without modifying the response itself.&lt;/p&gt;
&lt;p&gt;In this example, however, we &lt;em&gt;do&lt;/em&gt; want to modify the response, so we need to send the original page response back.&lt;/p&gt;
&lt;p&gt;To do this, modify &lt;code&gt;netlify/edge-functions/color-theme.ts&lt;/code&gt; with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;+ import type { Context } from &apos;https://edge.netlify.com/&apos;;
+
- export default async () =&amp;gt; {
+ export default async (request: Request, context: Context) =&amp;gt; {
+   const res = await context.next();
+
+   console.log(request.url);
+
-   return new Response(&apos;hello world&apos;);
+   return res;
  };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Reload the page in your browser and the site will render the same as it did before the edge function was added. However, if you look in the terminal, you&apos;ll see that URLs were logged:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;◈ Reloading edge functions...
◈ Reloaded edge function color-theme
[color-theme] http://localhost:8888/
[color-theme] http://localhost:8888/styles.css
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The HTML file ran through the edge function, which is exactly what we wanted!&lt;/p&gt;
&lt;p&gt;However, the CSS file &lt;em&gt;also&lt;/em&gt; triggered the edge function, and that&apos;s &lt;em&gt;not&lt;/em&gt; what we wanted.&lt;/p&gt;
&lt;h3&gt;Only transform HTML files&lt;/h3&gt;
&lt;p&gt;To make sure edge functions only execute on the requests we intend to modify, we can check the &lt;code&gt;Content-Type&lt;/code&gt; header and bail if it&apos;s not &lt;code&gt;text/html&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Update &lt;code&gt;netlify/edge-functions/color-theme.ts&lt;/code&gt; with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  import type { Context } from &apos;https://edge.netlify.com/&apos;;

  export default async (request: Request, context: Context) =&amp;gt; {
    const res = await context.next();
+   const type = res.headers.get(&apos;content-type&apos;) as string;
+   if (!type.startsWith(&apos;text/html&apos;)) {
+     return;
+   }

    console.log(request.url);

    return res;
  };

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Reload the page and you&apos;ll see that only the URL for the HTML itself shows up in the terminal logs now:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;◈ Reloading edge functions...
◈ Reloaded edge function color-theme
[color-theme] http://localhost:8888/
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. Update the HTML with the correct theme&lt;/h2&gt;
&lt;p&gt;To update the HTML with our correct theme, we need to do three things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Check a cookie to track which theme is currently selected&lt;/li&gt;
&lt;li&gt;Set the cookie to a new theme value when the user makes a theme selection&lt;/li&gt;
&lt;li&gt;Transform the &lt;code&gt;data-theme&lt;/code&gt; attribute in the HTML to insert the current theme&lt;/li&gt;
&lt;li&gt;Update the &lt;code&gt;selected&lt;/code&gt; attribute of the theme switcher to make sure the current theme is selected in the dropdown&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Use a cookie to track the currently selected theme&lt;/h3&gt;
&lt;p&gt;In Netlify Edge Functions, cookie management is simplified thanks to the built in &lt;code&gt;context&lt;/code&gt; object.&lt;/p&gt;
&lt;p&gt;In &lt;code&gt;netlify/edge-functions/color-theme.ts&lt;/code&gt;, make the following changes:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  import type { Context } from &apos;https://edge.netlify.com/&apos;;

  export default async (request: Request, context: Context) =&amp;gt; {
    const res = await context.next();
    const type = res.headers.get(&apos;content-type&apos;) as string;
    if (!type.startsWith(&apos;text/html&apos;)) {
      return;
    }

-   console.log(request.url);
+   const theme = context.cookies.get(&apos;lwj-color-theme&apos;) ?? &apos;default&apos;;
+   console.log({ theme });

    return res;
  };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Reload the page and see the theme logged in the terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;◈ Reloaded edge function color-theme
[color-theme] { theme: &quot;default&quot; }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We haven&apos;t set the cookie yet, so it falls back to &quot;default&quot;.&lt;/p&gt;
&lt;h3&gt;Set a cookie with the selected theme&lt;/h3&gt;
&lt;p&gt;When someone chooses a new theme, it will submit the form to the same page and add the theme as a query string. This is the default behavior of a form that doesn&apos;t have an &lt;code&gt;action&lt;/code&gt; or &lt;code&gt;method&lt;/code&gt; set.&lt;/p&gt;
&lt;p&gt;In our app, the edge function will check for a query string and — if one is set — create a cookie using the selected theme.&lt;/p&gt;
&lt;p&gt;Add the following to &lt;code&gt;netlify/edge-functions/color-theme.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  import type { Context } from &apos;https://edge.netlify.com/&apos;;

  export default async (request: Request, context: Context) =&amp;gt; {
    const res = await context.next();
    const type = res.headers.get(&apos;content-type&apos;) as string;
    if (!type.startsWith(&apos;text/html&apos;)) {
      return;
    }

+   const url = new URL(request.url);
+
+   console.log(url.search);
+   if (url.searchParams.has(&apos;theme&apos;)) {
+     const availableThemes = [&apos;default&apos;, &apos;light&apos;, &apos;dark&apos;, &apos;pink&apos;];
+     const requestedTheme = url.searchParams.get(&apos;theme&apos;) ?? &apos;default&apos;;
+
+     console.log({ requestedTheme });
+     if (availableThemes.includes(requestedTheme)) {
+       context.cookies.set({
+         name: &apos;lwj-color-theme&apos;,
+         value: requestedTheme,
+         path: &apos;/&apos;,
+         secure: true,
+         httpOnly: true,
+         sameSite: &apos;Strict&apos;,
+         expires: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
+       });
+     }
+
+     return new Response(&apos;Redirecting...&apos;, {
+       status: 301,
+       headers: {
+         Location: url.pathname,
+         &apos;Cache-Control&apos;: &apos;no-cache&apos;,
+       },
+     });
+   }

    const theme = context.cookies.get(&apos;lwj-color-theme&apos;) ?? &apos;default&apos;;
    console.log({ theme });

    return res;
  };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This code starts by turning the requested URL into a &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/URL&quot;&gt;web standard &lt;code&gt;URL&lt;/code&gt; interface&lt;/a&gt;, then checks to see if the query parameters include &lt;code&gt;theme&lt;/code&gt;. If set, it checks the requested theme against the available themes, then sets a cookie with the requested theme.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Because this is &lt;em&gt;not&lt;/em&gt; client-side code, the cookie can be set securely in a way that isn&apos;t readable by client-side JavaScript or third parties. This is a big advantage when using edge functions!&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;Finally, to remove the query string from the URL, the code redirects to the current URL &lt;em&gt;without&lt;/em&gt; the query string.&lt;/p&gt;
&lt;h3&gt;Transform the &lt;code&gt;data-theme&lt;/code&gt; attribute&lt;/h3&gt;
&lt;p&gt;Now that we have the current theme name set in a cookie, we can use it to transform the request and set the displayed color theme.&lt;/p&gt;
&lt;p&gt;To do this, we&apos;ll use a powerful library called &lt;a href=&quot;https://developers.cloudflare.com/workers/runtime-apis/html-rewriter/&quot;&gt;HTMLRewriter&lt;/a&gt; that allows us to update elements in the response using CSS-like selectors and a straightforward API.&lt;/p&gt;
&lt;p&gt;Update &lt;code&gt;netlify/edge-functions/color-theme.ts&lt;/code&gt; to import &lt;code&gt;HTMLRewriter&lt;/code&gt; and the &lt;code&gt;Element&lt;/code&gt; type, then set up the transform for the &lt;code&gt;html&lt;/code&gt; element that updates the &lt;code&gt;data-theme&lt;/code&gt; attribute:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  import type { Context } from &apos;https://edge.netlify.com/&apos;;
+ import {
+   HTMLRewriter,
+   Element,
+ } from &apos;https://ghuc.cc/worker-tools/html-rewriter/index.ts&apos;;

  export default async (request: Request, context: Context) =&amp;gt; {
    const res = await context.next();
    const type = res.headers.get(&apos;content-type&apos;) as string;
    if (!type.startsWith(&apos;text/html&apos;)) {
      return;
    }

    const url = new URL(request.url);

    console.log(url.search);
    if (url.searchParams.has(&apos;theme&apos;)) {
      const availableThemes = [&apos;default&apos;, &apos;light&apos;, &apos;dark&apos;, &apos;pink&apos;];
      const requestedTheme = url.searchParams.get(&apos;theme&apos;) ?? &apos;default&apos;;

      console.log({ requestedTheme });
      if (availableThemes.includes(requestedTheme)) {
        context.cookies.set({
          name: &apos;lwj-color-theme&apos;,
          value: requestedTheme,
          path: &apos;/&apos;,
          secure: true,
          httpOnly: true,
          sameSite: &apos;Strict&apos;,
          expires: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
        });
      }

      return new Response(&apos;Redirecting...&apos;, {
        status: 301,
        headers: {
          Location: url.pathname,
          &apos;Cache-Control&apos;: &apos;no-cache&apos;,
        },
      });
    }

    const theme = context.cookies.get(&apos;lwj-color-theme&apos;) ?? &apos;default&apos;;
    console.log({ theme });

-   return res;
+   return new HTMLRewriter()
+     .on(&apos;html&apos;, {
+       element(element: Element) {
+         element.setAttribute(&apos;data-theme&apos;, theme);
+       },
+     })
+     .transform(res);
  };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Save this and reload the browser. Choose a theme other than the default and see that it actually updates now.&lt;/p&gt;
&lt;p&gt;However, the dropdown will always show &quot;system default&quot;, which isn&apos;t ideal.&lt;/p&gt;
&lt;h3&gt;Update the currently selected theme in the dropdown&lt;/h3&gt;
&lt;p&gt;To update the dropdown, we need to add another transformation in our &lt;code&gt;HTMLRewriter&lt;/code&gt; chain.&lt;/p&gt;
&lt;p&gt;Modify &lt;code&gt;netlify/edge-functions/color-theme.ts&lt;/code&gt; to transform the &lt;code&gt;option&lt;/code&gt; element with a &lt;code&gt;value&lt;/code&gt; that matches the currently selected theme and set that to &lt;code&gt;selected=&quot;selected&quot;&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  import type { Context } from &apos;https://edge.netlify.com/&apos;;
  import {
    HTMLRewriter,
    Element,
  } from &apos;https://ghuc.cc/worker-tools/html-rewriter/index.ts&apos;;

  export default async (request: Request, context: Context) =&amp;gt; {
    const res = await context.next();
    const type = res.headers.get(&apos;content-type&apos;) as string;
    if (!type.startsWith(&apos;text/html&apos;)) {
      return;
    }

    const url = new URL(request.url);

    console.log(url.search);
    if (url.searchParams.has(&apos;theme&apos;)) {
      const availableThemes = [&apos;default&apos;, &apos;light&apos;, &apos;dark&apos;, &apos;pink&apos;];
      const requestedTheme = url.searchParams.get(&apos;theme&apos;) ?? &apos;default&apos;;

      console.log({ requestedTheme });
      if (availableThemes.includes(requestedTheme)) {
        context.cookies.set({
          name: &apos;lwj-color-theme&apos;,
          value: requestedTheme,
          path: &apos;/&apos;,
          secure: true,
          httpOnly: true,
          sameSite: &apos;Strict&apos;,
          expires: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
        });
      }

      return new Response(&apos;Redirecting...&apos;, {
        status: 301,
        headers: {
          Location: url.pathname,
          &apos;Cache-Control&apos;: &apos;no-cache&apos;,
        },
      });
    }

    const theme = context.cookies.get(&apos;lwj-color-theme&apos;) ?? &apos;default&apos;;
    console.log({ theme });

    return new HTMLRewriter()
      .on(&apos;html&apos;, {
        element(element: Element) {
          element.setAttribute(&apos;data-theme&apos;, theme);
        },
      })
+     .on(`option[value=&apos;${theme}&apos;]`, {
+       element(element: Element) {
+         element.setAttribute(&apos;selected&apos;, &apos;selected&apos;);
+       },
+     })
      .transform(res);
  };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Save and reload — it all works!&lt;/p&gt;
&lt;h2&gt;Fast, no flash, no client-side JavaScript color themes are possible without managing a server&lt;/h2&gt;
&lt;p&gt;Before edge functions entered the equation, there wasn&apos;t a good solution for managing color themes without either some jank on the client side or some extra management on the server side. But today, we&apos;re now able to get the benefits of server side HTML rendering &lt;em&gt;without having to set up a server&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Edge computing gives us the capability to make small transforms on the fly, whether we&apos;re &lt;a href=&quot;https://www.codetv.dev/blog/html-transform-edge-function&quot;&gt;updating static HTML&lt;/a&gt; or &lt;a href=&quot;https://www.netlify.com/blog/rewrite-html-transform-page-props-in-nextjs/&quot;&gt;transforming SSR-generated pages in a framework like Next.js&lt;/a&gt;. It also means we can get some of the benefits of servers without having to pay for servers — edge functions are free!&lt;/p&gt;
&lt;p&gt;View the &lt;a href=&quot;https://github.com/learnwithjason/theme-switcher-no-flash&quot;&gt;full source code&lt;/a&gt; for this demo, or check out the &lt;a href=&quot;https://theme-switcher-no-flash.netlify.app/&quot;&gt;live demo&lt;/a&gt; to try it for yourself.&lt;/p&gt;</content:encoded></item><item><title>Replace Text &amp; Images Using Edge Functions and HTMLRewriter</title><link>https://codetv.dev/blog/html-transform-edge-function/</link><guid isPermaLink="true">https://codetv.dev/blog/html-transform-edge-function/</guid><description>Transform HTML at request time using HTMLRewriter and Edge Functions. Update text, element attributes, and more.
</description><pubDate>Tue, 03 May 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Edge Functions (also referred to as cloud workers or edge compute) are a powerful way to modify requests to web content without modifying the source code or using client-side JavaScript.&lt;/p&gt;
&lt;p&gt;Developers can use edge functions to manage a range of tasks, including:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Auth and access control&lt;/li&gt;
&lt;li&gt;Proxying and rewriting content&lt;/li&gt;
&lt;li&gt;Personalization and customization&lt;/li&gt;
&lt;li&gt;Localization and translation&lt;/li&gt;
&lt;li&gt;Content transformation&lt;/li&gt;
&lt;li&gt;Custom headers&lt;/li&gt;
&lt;li&gt;Cookies&lt;/li&gt;
&lt;li&gt;and more&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In this tutorial, we&apos;ll take a look at how to transform an HTML response using HTMLRewriter in a Netlify Edge Function.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Jump to the end:&lt;/strong&gt; &lt;a href=&quot;https://github.com/learnwithjason/html-transform-edge-function&quot;&gt;source code&lt;/a&gt; | &lt;a href=&quot;https://html-transform-edge-functions.netlify.app/&quot;&gt;demo&lt;/a&gt;&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;What is the &quot;edge&quot;?&lt;/strong&gt; To boost performance on the web, modern sites place assets on Content Delivery Networks (CDNs), also called &quot;edge networks&quot;. When you request a page, the CDN finds the closest node to you and responds to the request from there.&lt;/p&gt;&lt;p&gt;CDNs have traditionally only delivered static assets. With the introduction of Edge Functions, it&apos;s now possible to transform responses at the edge by distributing business logic closer to your users.&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;1. Set up an HTML page&lt;/h2&gt;
&lt;p&gt;For this example we&apos;ll work with a simple HTML page with a little bit of custom CSS to make it look nice.&lt;/p&gt;
&lt;strong&gt;Expand to see the full HTML and CSS used in this example&lt;/strong&gt;&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot; /&amp;gt;
    &amp;lt;meta http-equiv=&quot;X-UA-Compatible&quot; content=&quot;IE=edge&quot; /&amp;gt;
    &amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot; /&amp;gt;
    &amp;lt;title&amp;gt;A Treatise on Pizza&amp;lt;/title&amp;gt;
    &amp;lt;link rel=&quot;stylesheet&quot; href=&quot;/styles/main.css&quot; /&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;main&amp;gt;
      &amp;lt;h1&amp;gt;A Treatise on Pizza&amp;lt;/h1&amp;gt;

      &amp;lt;p&amp;gt;
        It is empirically provable that pizza is the superior foodstuff. In this
        essay I will lay out the data-driven case for pizza’s scientific
        superiority.
      &amp;lt;/p&amp;gt;

      &amp;lt;h2&amp;gt;Exhibit A: Melty Cheese&amp;lt;/h2&amp;gt;

      &amp;lt;img
        class=&quot;right cheesy&quot;
        src=&quot;https://images.unsplash.com/photo-1593504049359-74330189a345?ixlib=rb-1.2.1&amp;amp;ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&amp;amp;auto=format&amp;amp;fit=crop&amp;amp;w=600&amp;amp;ar=0.85:1&amp;amp;fit=crop&amp;amp;q=80&amp;amp;crop=entropy&quot;
        alt=&quot;melty cheese pizza&quot;
      /&amp;gt;

      &amp;lt;p&amp;gt;
        There’s nothing better in life than melty cheese. It has been proven: in
        a study conducted at my friend’s house, rats were offered a choice
        between a document containing the true meaning of life and a small
        amount of melty cheese. The rats chose the melty cheese
        &amp;lt;em&amp;gt;every time&amp;lt;/em&amp;gt; — it doesn’t get more conclusive than that.
      &amp;lt;/p&amp;gt;

      &amp;lt;p&amp;gt;
        In another study, I asked my friends to write a list of the things they
        were most willing to suffer for. 87% of respondents were willing to
        suffer intestinal distress if it meant they could eat more cheese.
      &amp;lt;/p&amp;gt;

      &amp;lt;h2&amp;gt;Exhibit B: Salty Meats&amp;lt;/h2&amp;gt;

      &amp;lt;img
        class=&quot;left salty&quot;
        src=&quot;https://images.unsplash.com/photo-1601924582970-9238bcb495d9?ixlib=rb-1.2.1&amp;amp;ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&amp;amp;auto=format&amp;amp;fit=crop&amp;amp;w=988&amp;amp;q=80&amp;amp;ar=0.85:1&amp;amp;fit=crop&quot;
        alt=&quot;a pepperoni pizza&quot;
      /&amp;gt;

      &amp;lt;p&amp;gt;
        The presence of a savory, salty ingredient adds a distinct advantage for
        pizza over lesser foodstuffs. If you’ve ever wondered why just about
        every restaurant table includes a salt shaker, but they rarely contain a
        shaker of, like, rosemary, it’s because the human palate has evolved to
        recognize the best flavors, and the best flavors are salty.
      &amp;lt;/p&amp;gt;

      &amp;lt;p&amp;gt;
        In a study where half of participants were given a salt shaker filled
        with salt, and the other half of participants were given a salt shaker
        filled with granulated sugar, 100% of respondents were very upset with
        me for putting sugar in a salt shaker.
      &amp;lt;/p&amp;gt;
      &amp;lt;p&amp;gt;This is evidence that 100% of people prefer salt.&amp;lt;/p&amp;gt;

      &amp;lt;h2&amp;gt;Conclusion: Pizza Is the Best Food&amp;lt;/h2&amp;gt;

      &amp;lt;p&amp;gt;
        Based on the ironclad scientific evidence above, we can conclude with
        absolute certainty that pizza, which is basically melty cheese and salty
        meats on bread (which is the second-best foodstuff), is the best
        foodstuff.
      &amp;lt;/p&amp;gt;
    &amp;lt;/main&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code&gt;* {
  box-sizing: border-box;
}

html {
  font-family: -apple-system, BlinkMacSystemFont, &apos;Segoe UI&apos;, Roboto,
    Oxygen-Sans, Ubuntu, Cantarell, &apos;Helvetica Neue&apos;, sans-serif;
  font-size: 18px;
}

main {
  margin: 4rem auto 6rem;
  width: min(90vw, 54ch);
}

h2 {
  clear: both;
}

.left,
.right {
  aspect-rato: 0.85 / 1;
  margin: 0.75rem 0 1.5rem;
  max-width: 40%;
}

.left {
  float: left;
  margin-right: 1.5rem;
}

.right {
  float: right;
  margin-left: 1.5rem;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create a folder for your project and place the HTML and CSS in a &lt;code&gt;public&lt;/code&gt; folder. The folder structure should look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.
└── public
    ├── index.html
    └── styles
        └── main.css
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2. Set up local dev and deployment&lt;/h2&gt;
&lt;p&gt;To make it possible to develop and test Edge Functions locally — and deploy them to production once they&apos;re running — create a &lt;code&gt;netlify.toml&lt;/code&gt; at the folder root and place the following inside:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[build]
  publish = &quot;public&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, install the Netlify CLI if you don&apos;t already have it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm i -g netlify-cli
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Start the site locally by running the following command from your project root:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ntl dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will open &lt;code&gt;http://localhost:8888&lt;/code&gt; in your browser and display the HTML.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;/images/html-transform-local-dev.png&quot; alt=&quot;Rendered HTML in a local dev server showing in the browser.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;h2&gt;3. Create an Edge Function&lt;/h2&gt;
&lt;p&gt;Create your first Edge Function by adding a new file at &lt;code&gt;netlify/edge-functions/pizza-to-tacos.js&lt;/code&gt;. Once it’s created, the file tree should look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;❯ tree .
.
├── netlify
│   └── edge-functions
│       └── pizza-to-tacos.js
├── netlify.toml
└── public
    ├── index.html
    └── styles
        └── main.css
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Inside the Edge Function, let&apos;s start by making sure it&apos;s running as expected. Add the following code:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export default async () =&amp;gt; {
  console.log(&apos;Edge Functions are working!&apos;);
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To turn this on, we need to modify our &lt;code&gt;netlify.toml&lt;/code&gt; with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  [build]
    publish = &quot;public&quot;
+
+ [[edge_functions]]
+   path = &quot;/&quot;
+   function = &quot;pizza-to-tacos&quot;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;path&lt;/code&gt; tells Netlify what URL path should trigger an Edge Function, and the &lt;code&gt;function&lt;/code&gt; identifies which Edge Function should be triggered.&lt;/p&gt;
&lt;p&gt;Restart your local dev server (&lt;code&gt;ctrl&lt;/code&gt; + &lt;code&gt;C&lt;/code&gt;, then run &lt;code&gt;ntl dev&lt;/code&gt; again), then load the homepage and check the console output to see the log. This means our Edge Function is running.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;/images/html-transform-console-log.png&quot; alt=&quot;CLI output of console log from the Edge Function.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;h2&gt;4. Transform streaming text with an Edge Function&lt;/h2&gt;
&lt;p&gt;In order to keep responses fast, &lt;a href=&quot;https://twitter.com/ascorbic/status/1523630720832311298&quot;&gt;Edge Functions will stream responses&lt;/a&gt;, which means that the response is sent to the browser in chunks instead of waiting until the full document is processed.&lt;/p&gt;
&lt;p&gt;This is great for performance, but it can be confusing for transformations because we can&apos;t just run a find-and-replace — it&apos;s possible that the text we&apos;re searching for will be broken across chunks! For example, the sentence &quot;A great big ol&apos; honkin&apos; donut.&quot; may be sent in two chunks: &quot;A great big &quot; and &quot;ol&apos; honkin&apos; donut.&quot; This means that if we were trying to replace &quot;big ol&apos;&quot; in our text, a straightforward find-and-replace would fail.&lt;/p&gt;
&lt;h3&gt;Set up HTMLRewriter&lt;/h3&gt;
&lt;p&gt;To solve for this, we can use the &lt;code&gt;HTMLRewriter&lt;/code&gt; tool, which was originally &lt;a href=&quot;https://developers.cloudflare.com/workers/runtime-apis/html-rewriter/&quot;&gt;created by the Cloudflare team&lt;/a&gt; and later &lt;a href=&quot;https://deno.land/x/html_rewriter&quot;&gt;ported for use with Deno and other open runtimes by Florian Klampfer&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Let&apos;s implement the &lt;code&gt;HTMLRewriter&lt;/code&gt; in our Edge Function by replacing the contents of &lt;code&gt;pizza-to-tacos.js&lt;/code&gt; with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { HTMLRewriter } from &apos;https://ghuc.cc/worker-tools/html-rewriter/index.ts&apos;;

export default async (request, context) =&amp;gt; {
  const response = await context.next();

  return new HTMLRewriter().transform(response);
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It&apos;s worth noting that the import is done using Deno-style, URL-based packages. This means we don&apos;t need a &lt;code&gt;package.json&lt;/code&gt; or other installation step — it will Just Work™.&lt;/p&gt;
&lt;p&gt;The Edge Function takes two arguments:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;request&lt;/code&gt; — &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Request&quot;&gt;a standards-based &lt;code&gt;Request&lt;/code&gt; object&lt;/a&gt; with information from the incoming request&lt;/li&gt;
&lt;li&gt;&lt;code&gt;context&lt;/code&gt; — a collection of &lt;a href=&quot;https://docs.netlify.com/netlify-labs/experimental-features/edge-functions/api/#netlify-specific-context-object&quot;&gt;Netlify-specific helpers and data&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To get the data that will be returned to the browser, we use the &lt;code&gt;context.next()&lt;/code&gt; helper. Next, we return a new instance of the &lt;code&gt;HTMLRewriter&lt;/code&gt; that calls the &lt;code&gt;transform()&lt;/code&gt; method on the response.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Right now our Edge Handler isn&apos;t actually transforming anything.&lt;/strong&gt; At this point we&apos;ve set up all of the foundations to start transforming, but we haven&apos;t written any transformation code yet.&lt;/p&gt;&lt;/aside&gt;
&lt;h3&gt;Write a transform for all text nodes&lt;/h3&gt;
&lt;p&gt;Transformation works by using an element selector (very similar to CSS selectors), then running a function against the matched elements.&lt;/p&gt;
&lt;p&gt;To transform text, we&apos;ll use the &lt;code&gt;text&lt;/code&gt; method, put each chunk of streamed text into a buffer, and wait until we reach the end of the text node before running our transform to ensure all text is properly transformed.&lt;/p&gt;
&lt;p&gt;Add the following code to transform every instance of the word &quot;pizza&quot; to the word &quot;TACOS&quot;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  import { HTMLRewriter } from &apos;https://ghuc.cc/worker-tools/html-rewriter/index.ts&apos;;

+ let buffer = &apos;&apos;;
  export default async (request, context) =&amp;gt; {
    const response = await context.next();

    return (
      new HTMLRewriter()
+       .on(&apos;*&apos;, {
+         text(text) {
+           buffer += text.text;
+
+           if (text.lastInTextNode) {
+             text.replace(buffer.replace(/pizza/gi, &apos;TACOS&apos;));
+             buffer = &apos;&apos;;
+           } else {
+             text.remove();
+           }
+         },
+       })
        .transform(response)
    );
  };

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Calling &lt;code&gt;text.remove()&lt;/code&gt; prevents the text from streaming to the browser before it&apos;s replaced. We can do this safely because we&apos;re storing all the text into the &lt;code&gt;buffer&lt;/code&gt; variable.&lt;/p&gt;
&lt;p&gt;Save the changes and reload your local page to see the transformation in action:&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;/images/html-transform-text-replacement.png&quot; alt=&quot;Rendered HTML with text replaced.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;h2&gt;5. Transform element attributes in streaming HTML responses&lt;/h2&gt;
&lt;p&gt;Our transformation isn&apos;t complete yet — we&apos;re still showing photos of pizza despite updating the text to be about tacos.&lt;/p&gt;
&lt;p&gt;To fix this, we&apos;ll add an additional transform to select images. On each matched image, we&apos;ll get the classes applied to each. If the classes contain either &lt;code&gt;cheesy&lt;/code&gt; or &lt;code&gt;salty&lt;/code&gt; as class names, we&apos;ll transform the &lt;code&gt;src&lt;/code&gt; attribute for the element to use a different image and add matching &lt;code&gt;alt&lt;/code&gt; text.&lt;/p&gt;
&lt;p&gt;Make the following changes to &lt;code&gt;pizza-to-tacos.js&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  import { HTMLRewriter } from &apos;https://ghuc.cc/worker-tools/html-rewriter/index.ts&apos;;

  let buffer = &apos;&apos;;
  export default async (request, context) =&amp;gt; {
    const url = new URL(request.url);
    if (url.searchParams.get(&apos;transform&apos;) === &apos;false&apos;) {
      return context.next();
    }

    const response = await context.next();

    return (
      new HTMLRewriter()
        .on(&apos;*&apos;, {
          text(text) {
            buffer += text.text;

            if (text.lastInTextNode) {
              text.replace(buffer.replace(/pizza/gi, &apos;TACOS&apos;));
              buffer = &apos;&apos;;
            } else {
              text.remove();
            }
          },
        })
+       .on(&apos;img&apos;, {
+         element(element) {
+           const classes = element.getAttribute(&apos;class&apos;);
+
+           let newSrc = false;
+           let newAlt = false;
+           if (classes.includes(&apos;cheesy&apos;)) {
+             newSrc =
+               &apos;https://images.unsplash.com/photo-1464219222984-216ebffaaf85?ixlib=rb-1.2.1&amp;amp;ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&amp;amp;auto=format&amp;amp;fit=crop&amp;amp;w=600&amp;amp;q=80&amp;amp;ar=0.85:1&amp;amp;crop=entropy&apos;;
+             newAlt = &apos;hands pulling chips from a cheesy pile of nachos&apos;;
+           } else if (classes.includes(&apos;salty&apos;)) {
+             newSrc =
+               &apos;https://images.unsplash.com/photo-1513456852971-30c0b8199d4d?ixlib=rb-1.2.1&amp;amp;ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&amp;amp;auto=format&amp;amp;fit=crop&amp;amp;w=600&amp;amp;q=80&amp;amp;ar=0.85:1&amp;amp;crop=entropy&apos;;
+             newAlt = &apos;a close-up of nachos on a plate&apos;;
+           }
+
+           if (newSrc) {
+             element.setAttribute(&apos;src&apos;, newSrc);
+             element.setAttribute(&apos;alt&apos;, newAlt);
+           }
+         },
+       })
        .transform(response)
    );
  };

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Save the changes, then reload the browser to see the images transformed:&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;/images/html-transform-image-replacement.png&quot; alt=&quot;Rendered HTML with images replaced.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;h2&gt;6. Deploy to production&lt;/h2&gt;
&lt;p&gt;Now that we have a working Edge Function transforming our HTML, we can deploy it to production on Netlify.&lt;/p&gt;
&lt;p&gt;To start, make sure you&apos;re logged into your Netlify account in the CLI:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ntl login
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, create a new GitHub repository and push your local code to it. We&apos;ll use the GitHub CLI for this example.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git init

# don&apos;t track local development files
echo .netlify &amp;gt;&amp;gt; .gitignore

# add all the files we&apos;ve created
git add -A
git commit -m &apos;feat: site ready to deploy&apos;

# create a new repo on GitHub using the GitHub CLI
gh repo create
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once the repo is created and you&apos;ve pushed up your code, initialize a new site with the Netlify CLI:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ntl init
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create a new site, then link your new GitHub repository to it. It will start building immediately and will rebuild any time you push to GitHub.&lt;/p&gt;
&lt;p&gt;To view the site, click the link from the CLI output, visit your Netlify dashboard, or run this CLI command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ntl open:site
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Your site is deployed to production and the HTML is being transformed. 😎&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;/images/html-transform-deployed.png&quot; alt=&quot;The transformed HTML deployed to Netlify&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;To see this site running in production, &lt;a href=&quot;https://html-transform-edge-functions.netlify.app/&quot;&gt;check out the demo&lt;/a&gt;. You can also review the &lt;a href=&quot;https://github.com/learnwithjason/html-transform-edge-function&quot;&gt;source code on GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Resources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/let-s-learn-netlify-edge-functions&quot;&gt;&lt;em&gt;Learn With Jason&lt;/em&gt; episode on Edge Functions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.netlify.com/netlify-labs/experimental-features/edge-functions/&quot;&gt;Netlify Edge Functions docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.netlify.com/blog/deep-dive-into-netlify-edge-functions/&quot;&gt;A deep dive into Edge Functions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.cloudflare.com/workers/runtime-apis/html-rewriter/&quot;&gt;HTMLRewriter docs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Acknowledgments&lt;/h2&gt;
&lt;p&gt;Huge thanks to the Cloudflare team and Florian Klampfer for making HTMLRewriter available. Thanks to &lt;code&gt;@harris&lt;/code&gt; on the Cloudflare team for writing up an example of &lt;a href=&quot;https://community.cloudflare.com/t/htmlrewriter-dynamic-text-replacement-on-all-elements/119685/22&quot;&gt;using HTMLRewriter on streaming responses&lt;/a&gt;. Also shout-out to Phil and Salma for creating a &lt;a href=&quot;https://edge-functions-examples.netlify.app/&quot;&gt;library of Edge Functions examples&lt;/a&gt; that made writing this so much easier.&lt;/p&gt;</content:encoded></item><item><title>Use Promise.all to Stop Async/Await from Blocking Execution in JS</title><link>https://codetv.dev/blog/keep-async-await-from-blocking-execution/</link><guid isPermaLink="true">https://codetv.dev/blog/keep-async-await-from-blocking-execution/</guid><description>When writing asynchronous code, async/await is a powerful tool — but it comes with risks! Learn how to avoid code slowdowns in this tutorial.
</description><pubDate>Fri, 05 Jun 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Promises are extremely powerful for handling asynchronous operations in JavaScript. Async functions make them easier to read and reason about. However, they also introduce some sneaky traps that can lead to slowdowns if we&apos;re not careful.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;Async ops are commonly used for data fetching, which is also referred to as &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/Guide/AJAX&quot;&gt;Asynchronous JavaScript and XML (AJAX)&lt;/a&gt;, a technique for sending requests from a web page that didn’t require refreshing the browser. This was relatively complex and involved the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest&quot;&gt;&lt;code&gt;XMLHttpRequest&lt;/code&gt; API&lt;/a&gt;, but it really became popular with &lt;a href=&quot;https://api.jquery.com/jQuery.ajax/&quot;&gt;jQuery’s &lt;code&gt;$.ajax()&lt;/code&gt; function&lt;/a&gt;, which made it more approachable.&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;tl;dr&lt;/h2&gt;
&lt;p&gt;To make sure your async code isn’t slowing down your apps, check your code to ensure that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If you’re calling async functions with &lt;code&gt;await&lt;/code&gt;, don’t let unrelated async calls block each other.&lt;/li&gt;
&lt;li&gt;Don’t use &lt;code&gt;await&lt;/code&gt; inside loops. Create an array of Promises and &lt;code&gt;await Promise.all&lt;/code&gt; instead.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This article will walk through a few examples and how they can be refactored to avoid blocking execution when using &lt;code&gt;await&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Promises are a wonderful, powerful tool.&lt;/h2&gt;
&lt;p&gt;When sending off requests to load third-party data or do other asynchronous work, using a &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise&quot;&gt;&lt;code&gt;Promise&lt;/code&gt;&lt;/a&gt; has become a common pattern for telling our code to wait until the async work is done before continuing.&lt;/p&gt;
&lt;p&gt;We can see this in action by writing a function that simulates a slow network connection: when the function is called, it will wait 1 second before resolving (that&apos;s Promise-jargon for &quot;the asynchronous work is complete&quot;).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// -------------------------------------------------------------------
// this function simulates a request that needs to run async
// -------------------------------------------------------------------
function getFakeData() {
  return new Promise((resolve) =&amp;gt; {
    setTimeout(() =&amp;gt; {
      resolve({ action: &apos;boop&apos; });
    }, 1000);
  });
}

// -------------------------------------------------------------------
// this is the code that actually loads data
// -------------------------------------------------------------------
getFakeData().then((response) =&amp;gt; {
  console.log(response.action);
  //=&amp;gt; boop
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;This is a code-along article!&lt;/h2&gt;
&lt;p&gt;All the code samples in this article can be run in your console. To see these in action, open up developer tools (&lt;code&gt;command + option + I&lt;/code&gt;), copy paste the code sample into the console, and press enter.&lt;/p&gt;
&lt;figure&gt;&lt;/figure&gt;
&lt;p&gt;If you don&apos;t want to use your console, you can also use &lt;a href=&quot;https://codepen.io/&quot;&gt;CodePen&lt;/a&gt; or create a JavaScript file in your favorite editor to run in your browser.&lt;/p&gt;
&lt;h2&gt;Promises power many of our data fetching workflows.&lt;/h2&gt;
&lt;p&gt;One of the most common ways we work with Promises is when loading data with the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch&quot;&gt;Fetch API&lt;/a&gt; or a library like &lt;a href=&quot;https://www.npmjs.com/package/axios&quot;&gt;Axios&lt;/a&gt;. For example, if we want to load a random dog image from &lt;a href=&quot;https://dog.ceo/&quot;&gt;Dog CEO&lt;/a&gt;, our code might use the Fetch API (and Promises) like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function getData() {
  // the Fetch API returns a Promise
  fetch(&apos;https://dog.ceo/api/breeds/image/random&apos;)
    .then((response) =&amp;gt; {
      // `.then()` is called after the request is complete
      // this is part of the Fetch API for handling JSON-encoded responses
      return response.json();
    })
    .then((response) =&amp;gt; {
      // We can do whatever we want with the data now!
      console.log(response);
    });
}

getData();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Fetch sends off a request to the Dog CEO REST API and waits for a response. Once the response comes back, it resolves a Promise with the data that came back. It provides helpers for handling different response types (JSON in this case), which are called in a &lt;code&gt;.then&lt;/code&gt;, and after that point we can do whatever we want with the data by chaining additional &lt;code&gt;.then&lt;/code&gt; calls.&lt;/p&gt;
&lt;h2&gt;Promises don’t return values for use outside themselves.&lt;/h2&gt;
&lt;p&gt;When I started learning Promises, I struggled to wrap my head around the idea that there’s no straightforward way to get the data &lt;em&gt;out&lt;/em&gt; of a Promise; you have to work inside the &lt;code&gt;.then&lt;/code&gt; once you’ve started using them:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// -------------------------------------------------------------------
// this function simulates a request that needs to run async
// -------------------------------------------------------------------
function favoritePupper(favorite) {
  return new Promise((resolve, reject) =&amp;gt; {
    if (favorite === &apos;corgi&apos;) {
      resolve(favorite);
    } else {
      reject(&apos;your preference is incorrect&apos;);
    }
  });
}

// -------------------------------------------------------------------
// this is the code that actually loads data
// -------------------------------------------------------------------

// 🚫 this doesn’t work
function getFavoritePupperBroken() {
  // the return value is a Promise, not the value that resolved the Promise
  const pupper = favoritePupper(&apos;corgi&apos;);

  // that means we can’t return the value from this function 😱
  console.log(`My favorite pupper is a ${pupper}.`);
  //=&amp;gt; My favorite pupper is a [object Promise].
}

// ✅ this works
function getFavoritePupper() {
  const pupper = favoritePupper(&apos;corgi&apos;);

  // we can only get to the value using the `.then` chain
  pupper.then((doggo) =&amp;gt; {
    console.log(`My favorite pupper is a ${doggo}.`);
    //=&amp;gt; My favorite pupper is a corgi.
  });
}

getFavoritePupper();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The reasoning makes sense: Promises are asynchronous, but they shouldn&apos;t block synchronous code that doesn&apos;t depend on them, so the asynchronous code has to be isolated.&lt;/p&gt;
&lt;p&gt;However, trying to &quot;think in Promises&quot; has caused me a lot of headaches. It always takes me a minute to get my brain into the right mode to write Promise-based code.&lt;/p&gt;
&lt;h2&gt;Nested Promises are hard to keep track of.&lt;/h2&gt;
&lt;p&gt;If our content has multiple async steps, the nesting in Promises can become challenging to keep track of:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// -------------------------------------------------------------------
// this function simulates a request that needs to run async
// -------------------------------------------------------------------
function doOtherAsyncThing() {
  return new Promise((resolve) =&amp;gt; {
    setTimeout(() =&amp;gt; resolve(&apos;it’s done!&apos;), 500);
  });
}

// -------------------------------------------------------------------
// this is the code that actually loads data
// -------------------------------------------------------------------
function getData() {
  fetch(&apos;https://dog.ceo/api/breeds/image/random&apos;)
    .then((response) =&amp;gt; response.json())
    .then((response) =&amp;gt; {
      doOtherAsyncThing().then((otherResponse) =&amp;gt; {
        // do stuff with `response` and `otherResponse`
        console.log({ response, otherResponse });
      });
    });
}

getData();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Because next steps have to happen inside a &lt;code&gt;.then()&lt;/code&gt;, each async action further nests our code, which adds cognitive overhead and can make code hard to refactor and maintain as time goes on.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Heads up!&lt;/strong&gt; There are ways to clean this up, such as pulling out the functions passed to &lt;code&gt;.then&lt;/code&gt; into to top-level scope and calling them like &lt;code&gt;.then(handleResponse)&lt;/code&gt; — this article won&apos;t cover that flow, but it can be a very effective approach!&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;Async functions make Promises easier to use...&lt;/h2&gt;
&lt;p&gt;To make Promises easier to work with, &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function&quot;&gt;async functions&lt;/a&gt; introduce the &lt;code&gt;async&lt;/code&gt; and &lt;code&gt;await&lt;/code&gt; keywords that allow us to get the benefits of Promises — waiting for an async all to complete before continuing — without the mental overhead of chaining &lt;code&gt;.then&lt;/code&gt; calls and nesting Promises.&lt;/p&gt;
&lt;p&gt;Let‘s refactor the code we’ve written so far in this article using async functions to make it a little easier to read:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async function getData() {
  // using await means the resolved value of the Promise is returned!
  const response = await fetch(&apos;https://dog.ceo/api/breeds/image/random&apos;).then(
    (response) =&amp;gt; response.json()
  ); // .then still works when it makes sense!

  const otherResponse = await doOtherAsyncThing();

  // do stuff with `response` and `otherResponse`
  console.log({ response, otherResponse });
}

getData();

async function getFavoritePupper() {
  const pupper = await favoritePupper(&apos;corgi&apos;);

  console.log(`My favorite pupper is a ${pupper}.`);
  //=&amp;gt; My favorite pupper is a corgi.
}

getFavoritePupper();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;await&lt;/code&gt; tells our code to wait for the Promise to resolve, then hands back the resolved value of the Promise as the return value. This removes a lot of boilerplate associated with Promises, and that’s a Good Thing™.&lt;/p&gt;
&lt;h2&gt;...but async functions also introduce new challenges.&lt;/h2&gt;
&lt;p&gt;Unfortunately, the convenience of async functions comes with some traps that aren&apos;t immediately obvious.&lt;/p&gt;
&lt;p&gt;Let’s take a look at two examples:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Two unrelated async operations that load data, then do separate things.&lt;/li&gt;
&lt;li&gt;An async chain: one operation that loads an array, then a series of calls that depend on the result of the original call.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Two unrelated async operations&lt;/h3&gt;
&lt;p&gt;With Promises, we might set this up like so:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// -------------------------------------------------------------------
// these functions simulate requests that need to run async
// -------------------------------------------------------------------
function asyncThing1() {
  return new Promise((resolve) =&amp;gt; {
    setTimeout(() =&amp;gt; resolve(&apos;Thing 1 is done!&apos;), 2000);
  });
}

function asyncThing2() {
  return new Promise((resolve) =&amp;gt; {
    setTimeout(() =&amp;gt; resolve(&apos;Thing 2 is done!&apos;), 2000);
  });
}

// -------------------------------------------------------------------
// this is the code that actually loads data
// -------------------------------------------------------------------
function doThings() {
  asyncThing1().then((thing1) =&amp;gt; {
    // do something with the first response
    console.log(thing1);
  });

  asyncThing2().then((thing2) =&amp;gt; {
    // do something with the second response
    console.log(thing2);
  });
}

doThings();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you run this in your console, you’ll see that Thing 1 and Thing 2 complete at nearly the same time.&lt;/p&gt;
&lt;p&gt;If we refactor to use async functions, our code might look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 🚫 don’t do this in your real code; it’s slow!
async function doThings() {
  const thing1 = await asyncThing1();
  console.log(thing1);

  const thing2 = await asyncThing2();
  console.log(thing2);
}

doThings();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This looks nice and clean, but if we run it in our console we’ll notice an issue: the code now takes &lt;em&gt;twice as long&lt;/em&gt; to run! 😱&lt;/p&gt;
&lt;p&gt;Promises run in parallel because they don’t care what happens outside of them — that‘s why we can only access their content inside a &lt;code&gt;.then&lt;/code&gt;. This means that both &lt;code&gt;asyncThing1()&lt;/code&gt; and &lt;code&gt;asyncThing2()&lt;/code&gt; run at the same time in our first example.&lt;/p&gt;
&lt;p&gt;In async functions, &lt;code&gt;await&lt;/code&gt; blocks any code that follows from executing until the Promise has resolves, which means that our refactored code doesn&apos;t even start &lt;code&gt;asyncThing2()&lt;/code&gt; until &lt;code&gt;asyncThing1()&lt;/code&gt; has completed — that’s not good.&lt;/p&gt;
&lt;p&gt;There’s good news, though: we can fix this without giving up on the benefits of async functions!&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ✅ do this — async code is run in parallel!
async function doThings() {
  const p1 = asyncThing1();
  const p2 = asyncThing2();

  const [thing1, thing2] = await Promise.all([p1, p2]);

  console.log(thing1);
  console.log(thing2);
}

doThings();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Because async functions are Promises under the hood, we can run both &lt;code&gt;asyncThing1()&lt;/code&gt; and &lt;code&gt;asyncThing2()&lt;/code&gt; in parallel by calling them without &lt;code&gt;await&lt;/code&gt;. Then we can use &lt;code&gt;await&lt;/code&gt; and &lt;code&gt;Promise.all&lt;/code&gt;, which returns an array of results once all Promises have completed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;This allows the Promises to run in parallel again, but still gives us a pleasant-to-use syntax that avoids chaining and lets us treat the values in our Promises as standard return values.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;Async operations with dependencies&lt;/h2&gt;
&lt;p&gt;In some cases, we&apos;ll have async operations that depend on the results of previous async operations. For example, if we want to load a list of blog posts from one API endpoint, then load comment data for each blog post from another.&lt;/p&gt;
&lt;p&gt;With Promises, that setup might look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// -------------------------------------------------------------------
// these functions simulate network requests to load data from an API
// -------------------------------------------------------------------
function getBlogPosts() {
  const posts = [
    { id: 1, title: &apos;Post One&apos;, body: &apos;A blog post!&apos; },
    { id: 2, title: &apos;Post Two&apos;, body: &apos;Another blog post!&apos; },
    { id: 3, title: &apos;Post Three&apos;, body: &apos;A third blog post!&apos; },
  ];

  return new Promise((resolve) =&amp;gt; {
    setTimeout(() =&amp;gt; resolve(posts), 200);
  });
}

function getBlogComments(postId) {
  const comments = [
    { postId: 1, comment: &apos;Great post!&apos; },
    { postId: 2, comment: &apos;I like it.&apos; },
    { postId: 1, comment: &apos;You make interesting points.&apos; },
    { postId: 3, comment: &apos;Needs more corgis.&apos; },
    { postId: 2, comment: &apos;Nice work!&apos; },
  ];

  // get comments for the given post
  const postComments = comments.filter((comment) =&amp;gt; comment.postId === postId);

  return new Promise((resolve) =&amp;gt; {
    setTimeout(() =&amp;gt; resolve(postComments), 300);
  });
}

// -------------------------------------------------------------------
// this is the code that actually loads data
// -------------------------------------------------------------------
function loadContent() {
  getBlogPosts().then((posts) =&amp;gt; {
    for (const post of posts) {
      getBlogComments(post.id).then((comments) =&amp;gt; {
        console.log({ ...post, comments });
      });
    }
  });
}

loadContent();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This code is perfectly fine. However, the nesting is pretty deep here, and it&apos;s a little challenging to track where things are happening — we&apos;ve got callbacks in loops in callbacks.&lt;/p&gt;
&lt;p&gt;To clean this up, our first instinct might be to refactor this code to use async functions:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async function loadContent() {
  const posts = await getBlogPosts();

  for (post of posts) {
    // using await here means the Promise has to resolve before the loop continue
    const comments = await getBlogComments(post.id);

    console.log({ ...post, comments });
  }
}

loadContent();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So much cleaner! 😍&lt;/p&gt;
&lt;p&gt;Unfortunately, each request for comments now has to wait for the one before it to complete, meaning our code is now &lt;em&gt;significantly&lt;/em&gt; slower — and it&apos;ll only get worse as we add more posts!&lt;/p&gt;
&lt;p&gt;To clean this up, we can use &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map&quot;&gt;&lt;code&gt;.map&lt;/code&gt;&lt;/a&gt; instead of a for loop and create an array of Promises, then use &lt;code&gt;Promise.all&lt;/code&gt; to wait for them to complete.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async function loadContent() {
  const posts = await getBlogPosts();

  // instead of awaiting this call, create an array of Promises
  const promises = posts.map((post) =&amp;gt; {
    return getBlogComments(post.id).then((comments) =&amp;gt; {
      return { ...post, comments };
    });
  });

  // use await on Promise.all so the Promises execute in parallel
  const postsWithComments = await Promise.all(promises);

  console.log(postsWithComments);
}

loadContent();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This code is still much more readable than the nested Promises and for loops, and it allows the requests to load comments to happen in parallel.&lt;/p&gt;
&lt;h2&gt;Keep async functions fast!&lt;/h2&gt;
&lt;p&gt;This is just one optimization to keep in mind when writing asynchronous code. Tuck it into your toolkit and keep your async functions fast!&lt;/p&gt;
&lt;p&gt;Have other ideas for speeding up async functions? &lt;a href=&quot;https://twitter.com/compose/tweet?text=I%20just%20learned%20how%20to%20keep%20async/await%20function%20fast%20from%20@jlengstorf&amp;amp;url=https://codetv.dev/blog/keep-async-await-from-blocking-execution/&quot;&gt;Hit me up on Twitter!&lt;/a&gt;&lt;/p&gt;</content:encoded></item><item><title>WTF is the Jamstack? A goofy name for a great web architecture.</title><link>https://codetv.dev/blog/wtf-is-jamstack/</link><guid isPermaLink="true">https://codetv.dev/blog/wtf-is-jamstack/</guid><description>Are you already using the Jamstack? Boost your understanding of modern web dev and learn what the Jamstack is — and what it’s not — in this overview.
</description><pubDate>Fri, 27 Mar 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;At a high level, the Jamstack is an architectural approach to web apps that focuses on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;generating cacheable, static assets at build time whenever possible,&lt;/li&gt;
&lt;li&gt;deploying those assets to CDNs, and&lt;/li&gt;
&lt;li&gt;using client-side JavaScript to call third-party APIs and serverless functions for dynamic interactions and data.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For individual developers, the Jamstack lowers the barrier to entry, cuts down on the number of tools to learn, and provides modern tooling to build web apps.&lt;/p&gt;
&lt;p&gt;For teams, the Jamstack helps enforce a more maintainable architecture and decreases iteration time, enabling faster delivery along with increased security and scalability.&lt;/p&gt;
&lt;p&gt;For users, Jamstack apps typically load faster and are more reliable, which makes the web a less frustrating place.&lt;/p&gt;
&lt;h2&gt;The Jamstack is not a traditional tech stack.&lt;/h2&gt;
&lt;p&gt;The Jamstack is not a “stack” in the same way the term is commonly used, such as the LAMP stack (Linux, Apache, MySQL, PHP) or MERN stack (Mongo, Express, React, Node).&lt;/p&gt;
&lt;p&gt;It &lt;em&gt;is&lt;/em&gt; true that the origin of Jamstack comes from the acronym JAM, standing for JavaScript, APIs, and Markup (or &lt;a href=&quot;https://dev.to/shortdiv/what-does-the-m-in-jamstack-actually-mean-5hnf&quot;&gt;maybe Markdown? who knows?&lt;/a&gt;). However, as people started adopting the principals of Jamstack, it immediately outgrew this stack and came to represent a more generalized approach.&lt;/p&gt;
&lt;p&gt;Instead of being a descriptive acronym describing a particular tech stack, Jamstack joins “&lt;a href=&quot;https://www.etymonline.com/word/radar&quot;&gt;radar&lt;/a&gt;” and “&lt;a href=&quot;https://www.etymonline.com/word/laser&quot;&gt;laser&lt;/a&gt;” as an often-inaccurately-used acronym that represents a general idea instead of a precise origin.&lt;/p&gt;
&lt;h2&gt;Jamstack is more of an &lt;em&gt;architecture&lt;/em&gt;.&lt;/h2&gt;
&lt;p&gt;The Jamstack is similar to terms like “&lt;a href=&quot;https://en.wikipedia.org/wiki/Microservices&quot;&gt;microservices&lt;/a&gt;“ and “&lt;a href=&quot;https://en.wikipedia.org/wiki/Monolithic_application&quot;&gt;monolith&lt;/a&gt;“ because it doesn’t describe the specifics of implementation. Instead, these terms communicate the high-level details about how the code is organized.&lt;/p&gt;
&lt;p&gt;If we hear someone describe a codebase as a monolith or microservices, we get a broad idea of the architecture and can make some high-level assumptions about how the code works. However, &lt;strong&gt;knowing the high-level architecture doesn’t tell us anything about the specifics of how the code is implemented.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Jamstack is like that: if we hear an app described as a Jamstack app, we can make broad architectural assumptions, but the implementation details can vary widely between teams.&lt;/p&gt;
&lt;h2&gt;What architectural decisions make an app a Jamstack app?&lt;/h2&gt;
&lt;p&gt;If the Jamstack allows us to make architectural assumptions, &lt;em&gt;what are those assumptions?&lt;/em&gt; In the broadest strokes, the software architecture decisions that make up the Jamstack are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Assets are generated at build time&lt;/strong&gt;, not request time.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Apps are deployed to CDNs&lt;/strong&gt; instead of always-on servers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deployments ship atomic, static assets&lt;/strong&gt; instead of dynamic, derived assets.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dynamic interactions use APIs and serverless functions&lt;/strong&gt; instead of monolithic servers.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Assets are generated at build time, not request time&lt;/h3&gt;
&lt;p&gt;Any performance-minded developer will tell you to cache responses to both speed up our apps and reduce load on our servers. In just about any production stack out there, we can expect to find caching in place.&lt;/p&gt;
&lt;p&gt;Often, this is done by waiting for a site visitor to hit a URL, doing work on the server at request time to generate the assets required to render the page, then caching that response so the next visitor is able to get the assets without waiting for the server to do the work.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/q_auto,f_auto/v1585355836/lwj/blog/wtf-is-jamstack/rtr-vs-btr.png&quot; alt=&quot;Flowcharts of both request time rendering and build time rendering.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;The Jamstack takes this one step further: &lt;strong&gt;if you generate the cache at build time, no one ever has to wait for assets to be generated.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This gives us much more certainty about our deployment process because, as &lt;a href=&quot;https://dev.to/philhawksworth/prerendering-is-the-key-to-a-tasty-jamstack-22pp&quot;&gt;Phil Hawksworth puts it&lt;/a&gt;, “by pre-rendering our sites we can be certain that our pages are correct before we deploy them”.&lt;/p&gt;
&lt;h3&gt;Apps are deployed to CDNs instead of always-on servers&lt;/h3&gt;
&lt;p&gt;By thinking of our apps as pre-generated caches, we eliminate the need for always-on servers. This creates several huge benefits:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Our sites load faster&lt;/strong&gt; because we can deploy to Content Delivery Networks (CDNs), putting our app assets closer to the people trying to load them.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;We cut down on costs.&lt;/strong&gt; Using CDNs means we don’t have to pay for massively scalable servers to handle traffic spikes. We’re shipping more reliable sites &lt;em&gt;and&lt;/em&gt; reducing operational overhead.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Our apps are more secure&lt;/strong&gt; because there’s no active connection to a server or database. Sites are made of pre-generated, static assets, which means we don’t &lt;em&gt;need&lt;/em&gt; a server or database to display them.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Our apps are more stable.&lt;/strong&gt; There are very few moving parts between your site visitors and the content they’re requesting, so there are fewer points of failure in the request chain.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Deployments ship atomic, static assets instead of dynamic, derived assets&lt;/h3&gt;
&lt;p&gt;Because we build the &lt;em&gt;entire&lt;/em&gt; site once, we get a very cool benefit: atomic deploys. Once the site is built, we have all of the markup, styles, scripts, data — &lt;em&gt;everything&lt;/em&gt; in one place that won&apos;t be modified again after the build. This means that if we make a mistake, we can roll back to a previous deploy by straight-up replacing the bad deploy’s files with a previous good deploy.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/q_auto,f_auto/v1585363148/lwj/blog/wtf-is-jamstack/atomic-deploy.png&quot; alt=&quot;Flowcharts of rolling back an atomic deploy.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;This &lt;em&gt;also&lt;/em&gt; means we can set up previews for new ideas quickly and easily: we can create a branch in our repo, run a build, and then host that site at a private, temporary URL to send the idea around for feedback.&lt;/p&gt;
&lt;p&gt;At Netlify, where I work, &lt;a href=&quot;https://www.netlify.com/products/build/?utm_source=learnwithjason&amp;amp;utm_medium=wtf-is-jamstack-jl&amp;amp;utm_campaign=devex&quot;&gt;instant rollbacks, branch deploys, and deploy previews for pull requests&lt;/a&gt; are all powered by this concept. I rarely hear anyone talk about it, but this is one of the most powerful benefits of the Jamstack.&lt;/p&gt;
&lt;h3&gt;Dynamic data uses APIs and serverless functions instead of monolithic servers&lt;/h3&gt;
&lt;p&gt;Not everything can be cached, however: what if our site’s users log in and need to see their own data? It doesn’t make sense to try and build that ahead of time — it would be a huge security risk, for one thing — so we still need a way to handle user input and load personalized data on-demand.&lt;/p&gt;
&lt;p&gt;Handling dynamic data and interactions is a core part of the Jamstack approach, which may seem counterintuitive after we’ve just spent most of this article talking about generating static assets.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;This is an important distinction: static assets do not mean static apps.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A JavaScript file is a static asset. Using JavaScript, we can make calls out to other APIs, third-party services, and all sorts of other data sources — this means we have all the same flexibility to generate dynamic content that we have with a traditional server request.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;I’ve written a whole article with &lt;a href=&quot;https://www.smashingmagazine.com/2019/12/dynamic-async-functionality-jamsstack-websites/&quot;&gt;examples of dynamic patterns in the Jamstack&lt;/a&gt;, so I won’t go into detail here, but the short version is: &lt;strong&gt;Jamstack apps can take advantage of third-party APIs and our own serverless functions to create just about any kind of dynamic interactions we can imagine.&lt;/strong&gt;&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;The benefit here is that we get to focus just on the data and business logic instead of building out all the boilerplate for a service and dealing with the operational overhead of keeping it deployed and maintained.&lt;/p&gt;
&lt;h2&gt;Isn’t this the same as what we used to do?&lt;/h2&gt;
&lt;p&gt;In a sense, the Jamstack is a return to a very old method of building websites: we create a static asset — an HTML file — and put that online somewhere. If you’ve ever dragged a file into an FTP program to make website changes, this may feel familiar.&lt;/p&gt;
&lt;p&gt;The difficulty of manually editing every file on a site and uploading the changes via FTP was high, and it left a lot of room for human error.&lt;/p&gt;
&lt;p&gt;As web development evolved, more tooling was created to generate assets automatically. We saw the rise of PHP, followed by the even more dramatic rise of WordPress. We saw Node enter the scene, with frameworks like Express for building our own template-driven servers. We saw Grunt, Gulp, Webpack, and other tools enter the mix to automate complicated build processes.&lt;/p&gt;
&lt;p&gt;And somewhere along the way, it became &lt;em&gt;complicated&lt;/em&gt; to get a website up on the web.&lt;/p&gt;
&lt;p&gt;Jamstack returns to the old way of deploying static files as a folder — we’re swapping out traditional hosting for a CDN — and introduces new innovations that make the process of creating sites boring again.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Jamstack combines tried-and-true development and deployment strategies with more ergonomic tooling, better abstractions around common infrastructure, and improvements to the JavaScript ecosystem have helped smooth out the developer experience around all this powerful tooling, decreasing the friction to build and deploy web apps.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;The Jamstack is not just marketing fluff.&lt;/h2&gt;
&lt;p&gt;A common criticism of the Jamstack is that it’s just a marketing term for stuff that already exists.&lt;/p&gt;
&lt;p&gt;Depending on what you’re predisposed to believe, this statement can fall anywhere on the spectrum of true and false.&lt;/p&gt;
&lt;p&gt;Put cynically, the Jamstack is “just putting static files on S3”.&lt;/p&gt;
&lt;p&gt;Put charitably, the Jamstack is “revolutionizing web development by removing barriers and giving frontend developers superpowers”.&lt;/p&gt;
&lt;p&gt;Somewhere in the middle lies a more objective assessment: the &lt;strong&gt;Jamstack is a label for a broad assortment of website-building techniques.&lt;/strong&gt; These techniques are based on several mature solutions that have existed long before the Jamstack was around to describe it, and &lt;em&gt;also&lt;/em&gt; introduces new innovations that streamline the process and make building in this way more approachable.&lt;/p&gt;
&lt;h2&gt;The Jamstack is dope.&lt;/h2&gt;
&lt;p&gt;To recap, let’s run through the Big Ideas™ of the Jamstack that we covered in this article:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The Jamstack is not just marketing fluff.&lt;/li&gt;
&lt;li&gt;The Jamstack is not actually a tech stack.&lt;/li&gt;
&lt;li&gt;The Jamstack is an architectural approach to building web apps that focuses on static assets shipped to CDNs using APIs and/or serverless functions to add dynamic functoinality.&lt;/li&gt;
&lt;li&gt;For a large number of web apps, the Jamstack will be faster, more reliable, and more efficient than other architectural approaches.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I’m obviously biased, given that I work at Netlify. However, I love the Jamstack and have been recommending it since long before I took a job at Netlify — I was pushing for this approach when I worked at IBM back in 2016 and didn’t have a word to describe the approach yet. 💜&lt;/p&gt;
&lt;p&gt;Are you exploring the Jamstack for an upcoming project? What questions do you have about it? I really want to hear from you — &lt;a href=&quot;http://twitter.com/compose/tweet?text=Hey%20@jlengstorf!%20I%E2%80%99m%20looking%20into%20the%20Jamstack%20for%20a%20project%20and%20I%20have%20questions.&quot;&gt;hit me up on Twitter&lt;/a&gt; or reply to any of &lt;a href=&quot;/newsletter&quot;&gt;my newsletter&lt;/a&gt; messages! (I’m serious, too: hearing what you find confusing, inspiring, frustrating, etc. about the Jamstack helps me know what I should create content about, so &lt;em&gt;please&lt;/em&gt; don’t hold back!)&lt;/p&gt;</content:encoded></item><item><title>Access Query String Parameters in Serverless Functions</title><link>https://codetv.dev/blog/query-strings-serverless-functions/</link><guid isPermaLink="true">https://codetv.dev/blog/query-strings-serverless-functions/</guid><description>How do you use query parameter arguments in a serverless function? This quick tutorial will show you how to get values from query string parameters.
</description><pubDate>Fri, 10 Jan 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Serverless functions really start to show their potential when we can accept user input and respond to it. The most straightforward way to do this is with query parameters in the browser using &lt;code&gt;GET&lt;/code&gt; requests. In this article, we&apos;ll look at how to retrieve query parameters in a serverless function and use them to affect the output of our function.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Heads up!&lt;/strong&gt; This tutorial uses &lt;a href=&quot;https://www.netlify.com/products/functions/?utm_source=learnwithjason&amp;amp;utm_medium=sls-query-string-jl&amp;amp;utm_campaign=devex&quot;&gt;Netlify Functions&lt;/a&gt; for development and deployment. If you&apos;d like more information, I&apos;ve written a &lt;a href=&quot;/blog/serverless-functions/deploy-first-serverless-function&quot;&gt;primer on how to deploy serverless functions to Netlify&lt;/a&gt;.&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;Create your dev environment&lt;/h2&gt;
&lt;p&gt;If you&apos;ve been following along with the &lt;a href=&quot;/blog/serverless-functions/overview&quot;&gt;full series on serverless functions&lt;/a&gt;, you can use the same codebase you originally set up. If you&apos;re starting fresh, you can clone &lt;a href=&quot;https://github.com/jlengstorf/serverless-functions/tree/starter&quot;&gt;the starter repo&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# clone the starter branch of the repo
git clone --single-branch --branch starter https://github.com/jlengstorf/serverless-functions.git

# move into the project
cd serverless-functions/

# install dependencies (can also use `npm install`)
yarn

# install the Netlify CLI (can also use npm i -g netlify-cli)
yarn global add netlify-cli
&lt;/code&gt;&lt;/pre&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; check out the &lt;a href=&quot;https://docs.netlify.com/cli/get-started/?utm_source=learnwithjason&amp;amp;utm_medium=sls-query-string-jl&amp;amp;utm_campaign=devex&quot;&gt;Netlify CLI docs&lt;/a&gt; for more details.&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;Write the serverless function&lt;/h2&gt;
&lt;p&gt;To start, let’s create a serverless function that will allow us to boop our friends. Create a file called &lt;code&gt;boop-a-friend.js&lt;/code&gt; in the &lt;code&gt;functions&lt;/code&gt; folder of your project and write the following code inside:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;exports.handler = async () =&amp;gt; {
  // TODO get this value from the query string
  const boopee = &apos;Jason&apos;;

  return {
    statusCode: 200,
    body: `You booped ${boopee} on the nose. Boop!`,
  };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run &lt;code&gt;netlify dev&lt;/code&gt; and visit &lt;code&gt;http://localhost:9999/.netlify/functions/boop-a-friend&lt;/code&gt; and we see the return text, &quot;You booped Jason on the nose. Boop!&quot;&lt;/p&gt;
&lt;p&gt;We want to be able to choose who we boop, though, and we want to do that from the browser — meaning we&apos;ll be using the &lt;code&gt;GET&lt;/code&gt; method to call our serverless function. In practice, it will look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http://localhost:9999/.netlify/functions/boop-a-friend?boopee=Marisa
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We&apos;re using a &lt;a href=&quot;https://en.wikipedia.org/wiki/Query_string&quot;&gt;query string&lt;/a&gt; (the &lt;code&gt;?boopee=Marisa&lt;/code&gt; part) to set our &lt;code&gt;boopee&lt;/code&gt; value to &quot;Marisa&quot;, because that&apos;s who we want to boop.&lt;/p&gt;
&lt;p&gt;Next, we need to use the query string value in our serverless function.&lt;/p&gt;
&lt;h2&gt;Retrieve values from query strings in serverless functions&lt;/h2&gt;
&lt;p&gt;Because we&apos;re using &lt;a href=&quot;https://www.netlify.com/products/functions/?utm_source=learnwithjason&amp;amp;utm_medium=sls-query-string-jl&amp;amp;utm_campaign=devex&quot;&gt;Netlify Functions&lt;/a&gt;, we receive the &lt;code&gt;event&lt;/code&gt; as the first argument to our serverless function, and it contains a property called &lt;code&gt;queryStringParameters&lt;/code&gt;, which contains any query string values as an object.&lt;/p&gt;
&lt;p&gt;What this looks like in practice is that if we call our serverless function using the query string above:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http://localhost:9999/.netlify/functions/boop-a-friend?boopee=Marisa
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can access query parameters in our serverless function by making the following changes:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- exports.handler = async () =&amp;gt; {
+ exports.handler = async event =&amp;gt; {
+   console.log(event.queryStringParameters);

    // TODO get this value from the query string
    const boopee = &apos;Jason&apos;;

    return {
      statusCode: 200,
      body: `You booped ${boopee} on the nose. Boop!`,
    };
  };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When we run &lt;code&gt;netlify dev&lt;/code&gt; and call our function, the logs will show us the following output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Request from ::1: GET /.netlify/functions/boop-a-friend?boopee=Marisa
[Object: null prototype] { boopee: &apos;Marisa&apos; }
Response with status 200 in 0 ms.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Our &lt;code&gt;boopee&lt;/code&gt; is there!&lt;/p&gt;
&lt;p&gt;Now we can use it by making a few more adjustments to our code:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  exports.handler = async event =&amp;gt; {
-   console.log(event.queryStringParameters);
-
-   // TODO get this value from the query string
-   const boopee = &apos;Jason&apos;;
+   const querystring = event.queryStringParameters;
+   const boopee = querystring.boopee || &apos;a friend&apos;;

    return {
      statusCode: 200,
      body: `You booped ${boopee} on the nose. Boop!`,
    };
  };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we can call our function by visiting &lt;code&gt;http://localhost:9999/.netlify/functions/boop-a-friend?boopee=Marisa&lt;/code&gt; and we&apos;ll see that we&apos;re booping Marisa, just like we wanted to!&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;/images/boop-marisa.png&quot; alt=&quot;Browser showing output with a query parameter: “You booped Marisa on the nose. Boop!”&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;If we don&apos;t add a query string, we fall back to the default text:&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;/images/boop-default.png&quot; alt=&quot;Browser showing output without a query parameter: “You booped a friend on the nose. Boop!”&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;We did it! We can now send, parse, and use query string parameters in our serverless functions!&lt;/p&gt;
&lt;h2&gt;What to do next&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/blog/serverless-functions/overview&quot;&gt;See the full collection of serverless function examples&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Read the &lt;a href=&quot;https://docs.netlify.com/cli/get-started/?utm_source=learnwithjason&amp;amp;utm_medium=first-serverless-function-jl&amp;amp;utm_campaign=devex#continuous-deployment&quot;&gt;Netlify CLI docs on setting up continuous deployment&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Deploy Your First Serverless Function Using JavaScript</title><link>https://codetv.dev/blog/deploy-first-serverless-function/</link><guid isPermaLink="true">https://codetv.dev/blog/deploy-first-serverless-function/</guid><description>With serverless functions, the JavaScript powering our front-ends enables us to add back-end logic. Deploy your first serverless function in this tutorial!
</description><pubDate>Wed, 08 Jan 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;Serverless functions are a powerful solution for adding additional functionality to Jamstack sites.&lt;/strong&gt; In this post, we&apos;ll step through creating and deploying your first serverless function using &lt;a href=&quot;https://www.netlify.com/products/functions/?utm_source=learnwithjason&amp;amp;utm_medium=first-serverless-function-jl&amp;amp;utm_campaign=devex&quot;&gt;Netlify Functions&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Write your first serverless function&lt;/h2&gt;
&lt;p&gt;Our first step is to write the serverless function itself. In an empty folder, create a folder called &lt;code&gt;functions&lt;/code&gt;, and create a new file called &lt;code&gt;my-first-function.js&lt;/code&gt; inside with the following code:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;exports.handler = async () =&amp;gt; ({
  statusCode: 200,
  body: &apos;boop&apos;,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That&apos;s all there is to it — you&apos;ve just written your first serverless function! 🎉 The rest of this article is about getting this function online; we&apos;re done coding now.&lt;/p&gt;
&lt;h3&gt;What are the requirements of a serverless function?&lt;/h3&gt;
&lt;p&gt;There are three required components in a serverless function:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The file needs to export a function named &lt;code&gt;handler&lt;/code&gt; — this is what &lt;code&gt;exports.handler&lt;/code&gt; is doing on line 1 above&lt;/li&gt;
&lt;li&gt;The function needs to return an object with a &lt;code&gt;statusCode&lt;/code&gt; matching a valid HTTP response code&lt;/li&gt;
&lt;li&gt;The response object also needs to include a &lt;code&gt;body&lt;/code&gt; value, which is plain text by default&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Configure your project for deployment to Netlify&lt;/h2&gt;
&lt;p&gt;With Netlify Functions, we only need two lines of configuration, which we need to save in &lt;code&gt;netlify.toml&lt;/code&gt; at the root of the folder:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[build]
  functions = &quot;functions&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This tells Netlify that our functions live in the &lt;code&gt;functions&lt;/code&gt; folder.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Heads up!&lt;/strong&gt; Check the docs for details on &lt;a href=&quot;https://docs.netlify.com/configure-builds/file-based-configuration/?utm_source=learnwithjason&amp;amp;utm_medium=first-serverless-function-jl&amp;amp;utm_campaign=devex#continuous-deployment&quot;&gt;how Netlify config files work&lt;/a&gt;.&lt;/p&gt;&lt;/aside&gt;
&lt;h3&gt;Create the repo and push to GitHub&lt;/h3&gt;
&lt;p&gt;At this point, we‘re ready to get this function on the internet!&lt;/p&gt;
&lt;p&gt;Create a new repo on GitHub, then add and push our code to it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# add your new repo as an origin
# IMPORTANT: make sure to use your own username/repo name!
git remote add origin git@github.com:yourusername/yourreponame.git

# add all the files
git add -A

# commit the files
git commit -m &apos;my first serverless function&apos;

# push the changes to GitHub
git push -u origin master
&lt;/code&gt;&lt;/pre&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;IMPORTANT:&lt;/strong&gt; make sure to use your own username and repo name when you add the origin above!&lt;/p&gt;&lt;/aside&gt;
&lt;h3&gt;Create a new Netlify site&lt;/h3&gt;
&lt;p&gt;You can create your site through the Netlify dashboard or through the CLI. The CLI is really convenient and powerful, so let&apos;s use that for this site.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# install the Netlify CLI globally
npm install --global netlify-cli

# log into your Netlify account
netlify login

# initialize a new site
netlify init
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This command will set up a new Netlify site in your account connected to the GitHub repo we just created.&lt;/p&gt;
&lt;p&gt;It will ask several questions:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;What would you like to do?&lt;/strong&gt; — choose &quot;Create &amp;amp; configure a new site&quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Team&lt;/strong&gt; — choose which Netlify team you want to add this site to&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Site name (optional)&lt;/strong&gt; — choose a name for your site, or press enter to get a randomly-generated name&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Your build command&lt;/strong&gt; — press enter to leave this blank; we don&apos;t need it for running functions&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Directory to deploy&lt;/strong&gt; — hit backspace to remove the suggested value, then press enter to leave it blank&lt;/li&gt;
&lt;/ol&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;/images/netlify-init.png&quot; alt=&quot;Screenshot of terminal output from running netlify init with the above settings.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;Once the site has been created, we can grab the URL from the terminal output. In the above screenshot, the generated site name was:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://confident-nightingale-4e5a0b.netlify.com/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By default, Netlify functions live at the URL endpoint &lt;code&gt;/.netlify/functions/&amp;lt;function-name&amp;gt;&lt;/code&gt; — this is to minimize the chances that the route will conflict with other routes on your site. (We can &lt;a href=&quot;https://docs.netlify.com/routing/redirects/?utm_source=learnwithjason&amp;amp;utm_medium=first-serverless-function-jl&amp;amp;utm_campaign=devex#continuous-deployment&quot;&gt;customize our function URLs with redirects&lt;/a&gt;, if we want to.)&lt;/p&gt;
&lt;p&gt;Our function file is called &lt;code&gt;my-first-function.js&lt;/code&gt;, so it will be accessible on the web at &lt;a href=&quot;https://confident-nightingale-4e5a0b.netlify.com/.netlify/functions/my-first-function&quot;&gt;https://confident-nightingale-4e5a0b.netlify.com/.netlify/functions/my-first-function&lt;/a&gt;. Go ahead and click that link — it works!&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;/images/serverless-boop-function.png&quot; alt=&quot;Browser showing the “boop” returned by the serverless function.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;That&apos;s all there is to it! You&apos;ve successfully deployed your first serverless function to Netlify.&lt;/p&gt;
&lt;h2&gt;What to do next&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/blog/serverless-functions/overview&quot;&gt;See the full collection of serverless function examples&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Read the &lt;a href=&quot;https://docs.netlify.com/cli/get-started/?utm_source=learnwithjason&amp;amp;utm_medium=first-serverless-function-jl&amp;amp;utm_campaign=devex#continuous-deployment&quot;&gt;Netlify CLI docs on setting up continuous deployment&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Learn how to &lt;a href=&quot;https://docs.netlify.com/routing/redirects/?utm_source=learnwithjason&amp;amp;utm_medium=first-serverless-function-jl&amp;amp;utm_campaign=devex#continuous-deployment&quot;&gt;use redirects in Netlify&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>What Are Serverless Functions and How Do I Use Them?</title><link>https://codetv.dev/blog/serverless-functions-overview/</link><guid isPermaLink="true">https://codetv.dev/blog/serverless-functions-overview/</guid><description>Serverless functions enable front-end developers to add powerful &quot;back-end&quot; logic to our apps just by writing JavaScript — no devops, no servers, just results.
</description><pubDate>Wed, 08 Jan 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Serverless functions are an approach to writing back-end code that doesn&apos;t require writing a back-end.&lt;/p&gt;
&lt;p&gt;In the simplest terms: we write a function using our preferred language, like JavaScript; we send that function to a serverless provider; and then we can call that function just like any API using HTTP methods.&lt;/p&gt;
&lt;p&gt;This is &lt;em&gt;huge&lt;/em&gt; for front-end developers. &lt;strong&gt;We&apos;re now able to add powerful &quot;back-end&quot; logic to our apps just by writing JavaScript — no devops, no server code, no fuss.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This &lt;em&gt;also&lt;/em&gt; means that our &lt;strong&gt;Jamstack apps gain all the benefits that come along with server-hosted apps, but with significantly lower setup and maintenance costs.&lt;/strong&gt; We can deploy in seconds using &lt;a href=&quot;https://www.netlify.com/products/functions/?utm_source=learnwithjason&amp;amp;utm_medium=serverless-intro-jl&amp;amp;utm_campaign=devex&quot;&gt;Netlify Functions&lt;/a&gt; and use our serverless functions immediately.&lt;/p&gt;
&lt;p&gt;Put another way: serverless functions turn front-end developers into full-stack developers &lt;em&gt;without requiring us to learn or manage the full stack&lt;/em&gt;.&lt;/p&gt;
&lt;h2&gt;Serverless function examples and tutorials&lt;/h2&gt;
&lt;p&gt;To explore what serverless functions can do, I created a bunch of small examples — I&apos;ve collected them in this post, and I&apos;ll add additional posts as they’re published.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/blog/serverless-functions/deploy-first-serverless-function&quot;&gt;Deploy your first serverless function&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/serverless-functions/query-strings-serverless-functions&quot;&gt;Access query string parameters in serverless functions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Heads up!&lt;/strong&gt; &lt;a href=&quot;https://twitter.com/tzmanics&quot;&gt;Tara Z. Manicsic&lt;/a&gt; wrote a solid, short overview of serverless. If you&apos;re not familiar with the term, check it out: &lt;em&gt;&lt;a href=&quot;https://dev.to/tzmanics/what-is-serverless-besides-a-bad-name-for-using-servers-35ag&quot;&gt;What Is Serverless Besides a Bad Name for Using Servers&lt;/a&gt;&lt;/em&gt;.&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;What to do next&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Check out the &lt;a href=&quot;https://github.com/jlengstorf/serverless-functions&quot;&gt;source code for these examples&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Read the &lt;a href=&quot;https://docs.netlify.com/functions/overview/?utm_source=learnwithjason&amp;amp;utm_medium=serverless-intro-jl&amp;amp;utm_campaign=devex&quot;&gt;Netlify Functions docs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Automatically Generate Social Images for Blog Posts</title><link>https://codetv.dev/blog/auto-generate-social-image/</link><guid isPermaLink="true">https://codetv.dev/blog/auto-generate-social-image/</guid><description>Make sure your content stands out in social media timelines by automatically generating social media sharing cards for your blog posts.
</description><pubDate>Mon, 06 Jan 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Creating sharing images for social media is critical if you want your content to stand out, but a lot of content — for example, code-related blog posts — don&apos;t always have great visuals to share. If our Twitter cards don&apos;t have images, they get lost in the noise — but at the same time, a random image from &lt;a href=&quot;https://unsplash.com/&quot;&gt;Unsplash&lt;/a&gt; doesn&apos;t really communicate what the post contains, either.&lt;/p&gt;
&lt;p&gt;The need to create a social media image creates extra chores before publishing: in addition to writing the post, you now need to go find (or create) an image for sharing. It may not be a ton of work, but it&apos;s still one more hurdle between you and a published post.&lt;/p&gt;
&lt;p&gt;Fortunately, tools exist that allow us to &lt;strong&gt;automatically generate social media images&lt;/strong&gt;. In this post, we&apos;ll use &lt;a href=&quot;https://jason.energy/cloudinary&quot;&gt;Cloudinary&lt;/a&gt;, which combines asset hosting with APIs to modify media. This requires a Cloudinary account — the free tier should be more than enough for most personal sites.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Heads up!&lt;/strong&gt; The code in this article is now &lt;a href=&quot;https://www.npmjs.com/package/@jlengstorf/get-share-image&quot;&gt;available as an npm package&lt;/a&gt;.&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;Prior art and alternatives.&lt;/h2&gt;
&lt;p&gt;Before we start, there are a few other options for generating social sharing images out there. These require a little more setup, but they&apos;re built completely from open source tools.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://twitter.com/chrisbiscardi&quot;&gt;Chris Biscardi&lt;/a&gt; created &lt;a href=&quot;https://www.npmjs.com/package/gatsby-plugin-printer&quot;&gt;&lt;code&gt;gatsby-plugin-printer&lt;/code&gt;&lt;/a&gt;, which can be used to generate social media images as part of the build process if you&apos;re using Gatsby. (The docs are limited, so you&apos;ll probably want to look at the &lt;a href=&quot;https://github.com/ChristopherBiscardi/christopherbiscardi.github.com&quot;&gt;source of Chris&apos;s website&lt;/a&gt; for how this works.)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://twitter.com/swyx&quot;&gt;Shawn Wang&lt;/a&gt; wrote a &lt;a href=&quot;https://www.swyx.io/writing/jamstack-og-images/&quot;&gt;post detailing options for creating social media images&lt;/a&gt;, including a DIY solution at the bottom.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For me, this was more than I wanted to take on and maintain, so I opted for a (free) hosted service — Cloudinary — that lets me forget about it entirely after setting it up the first time.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Heads up!&lt;/strong&gt; If this article helps you, you can &lt;a href=&quot;https://jason.energy/cloudinary&quot;&gt;sign up for Cloudinary with this link&lt;/a&gt; — you’ll get a free Cloudinary account, and I get a little bonus cash to help cover the costs of running this site.&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;Make a plan: what is the desired outcome?&lt;/h2&gt;
&lt;p&gt;Before we get started, we need to know where we&apos;re headed — so let&apos;s &lt;a href=&quot;https://jason.energy/mise-en-place&quot;&gt;make a plan&lt;/a&gt; and identify our outcomes. By the end of this article, we want to have a utility function that we can call to generate a social media-friendly image URL that will be unique to our post and help it stand out on people&apos;s timelines.&lt;/p&gt;
&lt;p&gt;It should look something like this:&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1280,h_669,c_fill,q_auto,f_auto/w_760,c_fit,co_rgb:232129,g_south_west,x_480,y_254,l_text:lwj-title.otf_64_line_spacing_-10:Deploy%20Your%20First%20Serverless%20Function%20Using%20JavaScript/w_760,c_fit,co_rgb:232129,g_north_west,x_480,y_445,l_text:lwj-tagline.otf_48:%23front-end%20%23serverless%20%23jamstack/lwj/blog-post-card&quot; alt=&quot;Social sharing card with the title “Deploy Your First Serverless Function Using JavaScript” and the tagline “#front-end #serverless #jamstack”.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Heads up!&lt;/strong&gt; I wrote a post about &lt;a href=&quot;/blog/design-social-sharing-card&quot;&gt;how to design a social sharing card&lt;/a&gt; that goes over the choices made in this design to make it flexible enough to handle auto-generated text. Give it a read if you&apos;re interested in the design process or want to create your own social sharing card.&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;Write a function to dynamically generate cards&lt;/h2&gt;
&lt;p&gt;Now that we know how to build the Cloudinary URLs to create custom social sharing cards, we can write a utility function that builds them for us.&lt;/p&gt;
&lt;p&gt;To start, create a new file and add a simple function declaration:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export default function generateSocialImage() {
  // TODO write our function
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Our image URL has four major components:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The image transforms&lt;/li&gt;
&lt;li&gt;The title text overlay configuration&lt;/li&gt;
&lt;li&gt;The tagline text overlay configuration&lt;/li&gt;
&lt;li&gt;The sharing image template&apos;s public ID&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Each of those configuration options should have good defaults, but allow us to customize if we want.&lt;/p&gt;
&lt;h2&gt;Set up the base dimensions and configuration for the social card template&lt;/h2&gt;
&lt;p&gt;Our first step is to create a configuration object that will be passed to the function. We&apos;ll set defaults where possible to give a good outcome without needing to tweak the settings.&lt;/p&gt;
&lt;p&gt;Then we&apos;ll create our first transformation block, which are the &lt;a href=&quot;https://cloudinary.com/documentation/image_transformation_reference?ap=lwj&quot;&gt;Cloudinary transforms&lt;/a&gt; that set the dimensions, cropping, quality, and format of the final image:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export default function generateSocialImage({
  imageWidth = 1280,
  imageHeight = 669,
}: Config): string {
  // configure social media image dimensions, quality, and format
  const imageConfig = [
    `w_${imageWidth}`,
    `h_${imageHeight}`,
    &apos;c_fill&apos;,
    &apos;q_auto&apos;,
    &apos;f_auto&apos;,
  ].join(&apos;,&apos;);

  // TODO finish the function
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This places each transform option for the sharing image template into an array, allowing the width and height to optionally be set through the config, and then joined using commas to create a URL-ready transformation.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; We could also do this by creating a single string, but it gets hard to read with more complex transformations. This is a stylistic choice, so feel free to refactor this code however you like.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;We can repeat that process for the title and tagline transformation blocks, adding the transformation configurations and the config arguments to make them customizable.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  export default function generateSocialImage({
+   title,
+   tagline,
+   titleFont = &apos;arial&apos;,
+   titleExtraConfig = &apos;&apos;,
+   taglineExtraConfig = &apos;&apos;,
+   taglineFont = &apos;arial&apos;,
    imageWidth = 1280,
    imageHeight = 669,
+   textAreaWidth = 760,
+   textLeftOffset = 480,
+   titleBottomOffset = 254,
+   taglineTopOffset = 445,
+   textColor = &apos;000000&apos;,
+   titleFontSize = 64,
+   taglineFontSize = 48,
  }) {
    // configure social media image dimensions, quality, and format
    const imageConfig = [
      `w_${imageWidth}`,
      `h_${imageHeight}`,
      &apos;c_fill&apos;,
      &apos;q_auto&apos;,
      &apos;f_auto&apos;,
    ].join(&apos;,&apos;);

+   // configure the title text
+   const titleConfig = [
+     `w_${textAreaWidth}`,
+     &apos;c_fit&apos;,
+     `co_rgb:${textColor}`,
+     &apos;g_south_west&apos;,
+     `x_${textLeftOffset}`,
+     `y_${titleBottomOffset}`,
+     `l_text:${titleFont}_${titleFontSize}${titleExtraConfig}:${encodeURIComponent(
+       title,
+     )}`,
+   ].join(&apos;,&apos;);
+
+   // configure the tagline text
+   const taglineConfig = [
+     `w_${textAreaWidth}`,
+     &apos;c_fit&apos;,
+     `co_rgb:${textColor}`,
+     &apos;g_north_west&apos;,
+     `x_${textLeftOffset}`,
+     `y_${taglineTopOffset}`,
+     `l_text:${taglineFont}_${taglineFontSize}${taglineExtraConfig}:${encodeURIComponent(
+       tagline,
+     )}`,
+   ].join(&apos;,&apos;);
+
    // TODO finish the function
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Only the &lt;code&gt;title&lt;/code&gt; and &lt;code&gt;tagline&lt;/code&gt; config options are required — everything else has defaults set to match the &lt;a href=&quot;https://res.cloudinary.com/jlengstorf/raw/upload/v1578342420/social-sharing-cards/learnwithjason-social-card-template.fig&quot;&gt;social sharing card template&lt;/a&gt; designed in a &lt;a href=&quot;/blog/design-social-sharing-card&quot;&gt;companion post&lt;/a&gt;.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Heads up!&lt;/strong&gt; If you’ve never seen text overlays before, I wrote a post with more information on &lt;a href=&quot;/blog/add-text-overlay-cloudinary&quot;&gt;how text overlays work in Cloudinary&lt;/a&gt;. Give that a read if the configuration code is confusing.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;Next, we need to add these three configuration blocks into a valid Cloudinary URL and add the version (if one is provided) and the image public ID:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  export default function generateSocialImage({
    title,
    tagline,
+   cloudName,
+   imagePublicID,
+   cloudinaryUrlBase = &apos;https://res.cloudinary.com&apos;,
+   version = null,
    titleFont = &apos;arial&apos;,
    titleExtraConfig = &apos;&apos;,
    taglineExtraConfig = &apos;&apos;,
    taglineFont = &apos;arial&apos;,
    imageWidth = 1280,
    imageHeight = 669,
    textAreaWidth = 760,
    textLeftOffset = 480,
    titleBottomOffset = 254,
    taglineTopOffset = 445,
    textColor = &apos;000000&apos;,
    titleFontSize = 64,
    taglineFontSize = 48,
  }: Config): string {
    // configure social media image dimensions, quality, and format
    const imageConfig = [
      `w_${imageWidth}`,
      `h_${imageHeight}`,
      &apos;c_fill&apos;,
      &apos;q_auto&apos;,
      &apos;f_auto&apos;,
    ].join(&apos;,&apos;);

    // configure the title text
    const titleConfig = [
      `w_${textAreaWidth}`,
      &apos;c_fit&apos;,
      `co_rgb:${textColor}`,
      &apos;g_south_west&apos;,
      `x_${textLeftOffset}`,
      `y_${titleBottomOffset}`,
      `l_text:${titleFont}_${titleFontSize}${titleExtraConfig}:${encodeURIComponent(
        title,
      )}`,
    ].join(&apos;,&apos;);

    // configure the tagline text
    const taglineConfig = [
      `w_${textAreaWidth}`,
      &apos;c_fit&apos;,
      `co_rgb:${textColor}`,
      &apos;g_north_west&apos;,
      `x_${textLeftOffset}`,
      `y_${taglineTopOffset}`,
      `l_text:${taglineFont}_${taglineFontSize}${taglineExtraConfig}:${encodeURIComponent(
        tagline,
      )}`,
    ].join(&apos;,&apos;);

+   // combine all the pieces required to generate a Cloudinary URL
+   const urlParts = [
+     cloudinaryUrlBase,
+     cloudName,
+     &apos;image&apos;,
+     &apos;upload&apos;,
+     imageConfig,
+     titleConfig,
+     taglineConfig,
+     version,
+     imagePublicID,
+   ];
+
+   // remove any falsy sections of the URL (e.g. an undefined version)
+   const validParts = urlParts.filter(Boolean);
+
+   // join all the parts into a valid URL to the generated image
+   return validParts.join(&apos;/&apos;);
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Because the &lt;code&gt;version&lt;/code&gt; might be null, we run a quick filter to remove any falsy values (like &lt;code&gt;null&lt;/code&gt;) from the array, then join all parts using a forward slash (&lt;code&gt;/&lt;/code&gt;) to combine everything into a valid URL.&lt;/p&gt;
&lt;p&gt;Now we can use our template to create custom title cards!&lt;/p&gt;
&lt;h2&gt;Create custom sharing cards using the helper function&lt;/h2&gt;
&lt;p&gt;I&apos;ve uploaded my template to Cloudinary, and the URL I got back is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://res.cloudinary.com/jlengstorf/image/upload/v1578253116/lwj/blog-post-card.jpg
&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/v1578253116/lwj/blog-post-card.jpg&quot; alt=&quot;The social sharing card template with no text.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;The defaults in our function are based on the template we designed earlier, so with no changes we should end up pretty close to the desired outcome. Let&apos;s start by plugging in our cloud name, image public ID, and the title and tagline we want to display:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const socialImage = getShareImage({
  title: &apos;This Is a Post With an Automatically Generated Social Sharing Card&apos;,
  tagline: &apos;Writing blog posts is fun when the robots do some of the work!&apos;,
  cloudName: &apos;jlengstorf&apos;,
  imagePublicID: &apos;lwj/blog-post-card&apos;,
});

console.log(socialImage);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If we visit the URL this produces, we&apos;ll see the social card generated with the defaults:&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1280,h_669,c_fill,q_auto,f_auto/w_760,c_fit,co_rgb:000000,g_south_west,x_480,y_254,l_text:arial_64:This%20Is%20a%20Post%20With%20an%20Automatically%20Generated%20Social%20Sharing%20Card/w_760,c_fit,co_rgb:000000,g_north_west,x_480,y_445,l_text:arial_48:Writing%20blog%20posts%20is%20fun%20when%20the%20robots%20do%20some%20of%20the%20work!/lwj/blog-post-card&quot; alt=&quot;The social sharing card with the defined title and tagline using default settings.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;Not bad!&lt;/p&gt;
&lt;p&gt;However, I want to use a custom font, tweak the color a bit, and — because I can’t help myself — make a minor adjustment to the line-height of the title. To do that, let&apos;s add a few more thing to the config:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  const socialImage = getShareImage({
    title: &apos;This Is a Post With an Automatically Generated Social Sharing Card&apos;,
    tagline: &apos;Writing blog posts is fun when the robots do some of the work!&apos;,
    cloudName: &apos;jlengstorf&apos;,
    imagePublicID: &apos;lwj/blog-post-card&apos;,
+   titleFont: &apos;lwj-title.otf&apos;,
+   titleExtraConfig: &apos;_line_spacing_-10&apos;,
+   taglineFont: &apos;lwj-tagline.otf&apos;,
+   textColor: &apos;232129&apos;,
  });

  console.log(socialImage);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the URL shows us a more customized-looking sharing card!&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_1280,h_669,c_fill,q_auto,f_auto/w_760,c_fit,co_rgb:232129,g_south_west,x_480,y_254,l_text:lwj-title.otf_64_line_spacing_-10:This%20Is%20a%20Post%20With%20an%20Automatically%20Generated%20Social%20Sharing%20Card/w_760,c_fit,co_rgb:232129,g_north_west,x_480,y_445,l_text:lwj-tagline.otf_48:Writing%20blog%20posts%20is%20fun%20when%20the%20robots%20do%20some%20of%20the%20work!/lwj/blog-post-card&quot; alt=&quot;The social sharing card with the defined title and tagline using custom fonts and other settings.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;That&apos;s more like it! We now get an automatically generated social card that has my branding on it — this will save me a lot of time as I write more content, and it looks the same as it would if I opened Figma and filled out the template manually.&lt;/p&gt;
&lt;h2&gt;What to do next&lt;/h2&gt;
&lt;p&gt;At this point, you have all the tools you need to add automatically generated social media sharing cards to your own blog!&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Learn how to &lt;a href=&quot;/blog/design-social-sharing-card&quot;&gt;design a social media sharing card&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Read the &lt;a href=&quot;https://cloudinary.com/documentation/image_transformations?ap=lwj#adding_text_captions&quot;&gt;Cloudinary docs on text overlays&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Use &lt;a href=&quot;https://res.cloudinary.com/jlengstorf/raw/upload/v1578342420/social-sharing-cards/learnwithjason-social-card-template.fig&quot;&gt;the Figma template&lt;/a&gt; to create your own design&lt;/li&gt;
&lt;li&gt;Install the &lt;a href=&quot;https://www.npmjs.com/package/@jlengstorf/get-share-image&quot;&gt;npm package for automatically generating social sharing images&lt;/a&gt; to add this function to your site&lt;/li&gt;
&lt;li&gt;See the &lt;a href=&quot;https://github.com/jlengstorf/learnwithjason.dev/blob/070468828e8c758d150a8d573fd471d786278243/packages/%40jlengstorf/gatsby-theme-code-blog/src/gatsby-theme-blog-core/components/post.js#L55-L64&quot;&gt;source code for this site&lt;/a&gt; to see how to use the package in production&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;After you’ve implemented this, &lt;a href=&quot;https://twitter.com/jlengstorf&quot;&gt;send me a note on Twitter&lt;/a&gt; so I can see what you come up with!&lt;/p&gt;</content:encoded></item><item><title>Design a Flexible Social Card Template for Sharing Content</title><link>https://codetv.dev/blog/design-social-sharing-card/</link><guid isPermaLink="true">https://codetv.dev/blog/design-social-sharing-card/</guid><description>Creating eye-catching social sharing images doesn’t have to take a ton of time. In this post, learn how to create a reusable template for sharing your posts.
</description><pubDate>Mon, 06 Jan 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;When sharing content on Twitter, Facebook, and other social media platforms, adding an eye-catching image is critical to making sure your content doesn&apos;t get lost in the timeline. In this post, we&apos;ll look at a strategy for &lt;strong&gt;designing a flexible, reusable template for sharing posts&lt;/strong&gt; that helps brand your content without requiring you to create a bespoke image for each post.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Heads up!&lt;/strong&gt; I’ve turned the template in this article into a &lt;a href=&quot;https://res.cloudinary.com/jlengstorf/raw/upload/v1578342420/social-sharing-cards/learnwithjason-social-card-template.fig&quot;&gt;Figma template&lt;/a&gt;. Download it to create your own custom social sharing card!&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;Define a template for social media sharing images&lt;/h2&gt;
&lt;p&gt;In order to &lt;strong&gt;create a reusable template for our social sharing images&lt;/strong&gt;, we need to create a template that will work for any post we create.&lt;/p&gt;
&lt;p&gt;For our template, we want to include:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Our logo&lt;/strong&gt; (or photo) — showing our brand helps associate us with our content.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Post title&lt;/strong&gt; — this should be limited by the length that Google will show in search results. We can use an &lt;a href=&quot;https://app.sistrix.com/en/serp-snippet-generator&quot;&gt;SEO snippet generator&lt;/a&gt; to check the length of our post titles.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tagline&lt;/strong&gt; — this can be any additional text we want to show on the sharing image. In this example, we&apos;ll use the post tags.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Our template will put the branding on the left, with the title and tagline on the right, like this:&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;/images/template.jpg&quot; alt=&quot;Template with areas for a logo at the left, title at the top-right, and tagline at the bottom-right.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;This means that we need &lt;em&gt;two&lt;/em&gt; text overlays — and it needs to look good with any length of title and tagline.&lt;/p&gt;
&lt;h2&gt;Position the heading on the image&lt;/h2&gt;
&lt;p&gt;Let&apos;s start by getting the heading in the right place on our image.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Because the title needs to be near the tagline no matter what the length of either, we want the title to always be positioned at the bottom of its available area.&lt;/strong&gt; We&apos;ll do this using the gravity transform, setting it to south (for &quot;bottom&quot;) and west (for left).&lt;/p&gt;
&lt;p&gt;Cloudinary does alignment using directions (i.e. north, south, west, east) instead of calling it top, bottom, left, right — this can be a little confusing the first time you use it if you&apos;re not ready for it.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;/images/template-title-long.jpg&quot; alt=&quot;Arrows showing the alignment of the title area.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;This means that the title will still align well with the tagline, even when the title is short:&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;/images/template-title-short.jpg&quot; alt=&quot;Arrows showing the aligment of the title area with a shorter title.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;Next, we&apos;ll set up the tagline.&lt;/p&gt;
&lt;h2&gt;Position the tagline on the image&lt;/h2&gt;
&lt;p&gt;The tagline should be near the title no matter its length, so we&apos;ll use north (for &quot;top&quot;) and west (for &quot;left&quot;) as the gravity settings.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;/images/template-tagline-long.jpg&quot; alt=&quot;Arrows showing the alignment of the tagline area.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;This means that even short titles and taglines still look right in the image:&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;/images/template-tagline-short.jpg&quot; alt=&quot;Arrows showing the aligment of the tagline area with a shorter tagline.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;With this setup, we can use any combination of title and tagline that fits within their respective areas and it&apos;ll appear properly aligned on the card.&lt;/p&gt;
&lt;h2&gt;The image template can be anything as long as it leaves the text area clear&lt;/h2&gt;
&lt;p&gt;Now that we have our text overlays positioned, our sharing image can look any way we want — the only requirement is that the text areas stay clear and have &lt;a href=&quot;https://webaim.org/resources/contrastchecker/&quot;&gt;enough contrast&lt;/a&gt; to be legible.&lt;/p&gt;
&lt;p&gt;The simplest possible image might just be a white background with our logo:&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;/images/template-simple-short.jpg&quot; alt=&quot;Template showing placeholder title and tagline with a box around the logo area.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;For my site, adding the logo looks like this:&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;/images/template-logo-short.jpg&quot; alt=&quot;Template replacing the logo area with the Learn With Jason logo.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;This already looks pretty good! However, I wanted to add a little something extra to make it look more branded, so I put in top and bottom borders and a subtle background pattern, which ended up here:&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;/images/template-complete-short.jpg&quot; alt=&quot;The template with additional styles applied.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;A longer title and tagline still looks right in this template:&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;/images/template-complete-long.jpg&quot; alt=&quot;The template with additional styles using a longer title and tagline.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;This looks pretty good, if I do say so myself. 😎&lt;/p&gt;
&lt;h2&gt;What to do next&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Use the Figma template to &lt;a href=&quot;https://res.cloudinary.com/jlengstorf/raw/upload/v1578342420/social-sharing-cards/learnwithjason-social-card-template.fig&quot;&gt;design your own social sharing card&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Use Cloudinary to &lt;a href=&quot;/blog/auto-generate-social-image&quot;&gt;automatically generate your social sharing cards&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Upload a Custom Font to Cloudinary Using the Media Library UI</title><link>https://codetv.dev/blog/upload-custom-font-cloudinary-media-library/</link><guid isPermaLink="true">https://codetv.dev/blog/upload-custom-font-cloudinary-media-library/</guid><description>A tutorial on how to upload via UI and use custom fonts in text overlays with Cloudinary to generate images with custom text.
</description><pubDate>Sun, 05 Jan 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;If you want to use custom fonts with &lt;a href=&quot;https://jason.energy/cloudinary&quot;&gt;Cloudinary&lt;/a&gt;, you need to upload them as authenticated assets.&lt;/p&gt;
&lt;p&gt;While you can &lt;a href=&quot;/blog/upload-custom-font-cloudinary-node&quot;&gt;use Cloudinary Node SDK to upload custom fonts&lt;/a&gt; (or one of the &lt;a href=&quot;https://cloudinary.com/documentation?ap=lwj&quot;&gt;other SDKs&lt;/a&gt;), it&apos;s sometime more convenient to use the &lt;a href=&quot;https://cloudinary.com/console/media_library?ap=lwj&quot;&gt;Media Library UI&lt;/a&gt; to drag and drop fonts. However, it&apos;s not immediately clear &lt;em&gt;how&lt;/em&gt; to upload fonts as authenticated assets.&lt;/p&gt;
&lt;p&gt;In this post, we&apos;ll look at how to configure the Media Library to upload font files as authenticated assets so we can use them with &lt;a href=&quot;/blog/add-text-overlay-cloudinary&quot;&gt;text overlays&lt;/a&gt;.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Heads up!&lt;/strong&gt; If this article helps you, you can &lt;a href=&quot;https://jason.energy/cloudinary&quot;&gt;sign up for Cloudinary with this link&lt;/a&gt; — you’ll get a free Cloudinary account, and I get a little bonus cash to help cover the costs of running this site.&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;Create an upload preset&lt;/h2&gt;
&lt;p&gt;In your Cloudinary console, create a new upload preset by visiting &lt;a href=&quot;https://cloudinary.com/console/lui/upload_presets/new?ap=lwj&quot;&gt;https://cloudinary.com/console/lui/upload_presets/new&lt;/a&gt; — you can navigate to this page by clicking the gear in the top-right, then the &quot;upload&quot; tab, and scrolling down near the bottom and clicking the link that says, &quot;Add upload preset&quot;.&lt;/p&gt;
&lt;p&gt;You&apos;ll see the following screen:&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/q_auto,f_auto/v1643780202/lwj/blog/cloudinary-upload-preset.jpg&quot; alt=&quot;Cloudinary app UI for adding an upload preset.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;On this page, we need to:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Give the preset a name — I recommend using something obvious, like &quot;custom_font_upload_preset&quot;&lt;/li&gt;
&lt;li&gt;Choose &quot;Authenticated&quot; from the &quot;Delivery type&quot; dropdown&lt;/li&gt;
&lt;li&gt;Click &quot;Save&quot; at the top-right of the page&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Use the upload preset for raw files in the media library&lt;/h2&gt;
&lt;p&gt;Now that we have a preset, we need to tell Cloudinary to use it for &quot;raw&quot; files, which is any file that&apos;s not an image or video.&lt;/p&gt;
&lt;p&gt;Head to &lt;a href=&quot;https://cloudinary.com/console/settings/upload?ap=lwj&quot;&gt;https://cloudinary.com/console/settings/upload&lt;/a&gt; — or click the gear, then the &quot;Upload&quot; tab — and scroll all the way to the bottom to find the &quot;Media library’s upload presets&quot;.&lt;/p&gt;
&lt;p&gt;Under &quot;Raw&quot;, choose the upload preset you just created.&lt;/p&gt;
&lt;p&gt;Click &quot;Save&quot; at the bottom of the page.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/q_auto,f_auto/v1643780262/lwj/blog/cloudinary-media-library-preset.jpg&quot; alt=&quot;Cloudinary settings screen for choosing upload presets.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;h2&gt;Upload a custom font using the media library&lt;/h2&gt;
&lt;p&gt;Now that the preset is enabled, we can test it out by uploading a custom font. We&apos;re going to use the delightful &lt;a href=&quot;http://www.stereo-type.fr/fonts/snowballs/&quot;&gt;Snowballs font from Stereo Type&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; Make sure you have the appropriate licenses for any fonts you upload!&lt;/p&gt;
&lt;p&gt;Drag the TTF file onto the media library at &lt;a href=&quot;https://cloudinary.com/console/media_library?ap=lwj&quot;&gt;https://cloudinary.com/console/media_library&lt;/a&gt; — once it&apos;s finished, we&apos;ll see it show up in our list of assets.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Heads up!&lt;/strong&gt; Depending on your settings, you may see the font uploaded with a random string instead of the font name — if this happens, you can click on the font and edit the name in the right-hand sidebar that appears.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/q_auto,f_auto/v1643780332/lwj/blog/cloudinary-custom-font.jpg&quot; alt=&quot;Custom font displayed in Cloudinary’s media library.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;We know that our upload preset is working because the font has a lock in the bottom-right corner, which means that it&apos;s an authenticated upload — this means that there&apos;s no public access to the font, which is necessary to avoid violating the terms of most font licenses.&lt;/p&gt;
&lt;h2&gt;Use the custom font in a text overlay&lt;/h2&gt;
&lt;p&gt;Once our custom font is uploaded, we can use it by adding its public ID as the font name in a &lt;a href=&quot;/blog/add-text-overlay-cloudinary&quot;&gt;text overlay transformation&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Using an image from our own media library, we can add the text overlay like so:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://res.cloudinary.com/jlengstorf/image/upload/w_800/g_west,x_30,w_350,c_fit,co_white,bo_4px_solid_black,l_text:snowballs.ttf_180_stroke:Let%20it%20snow!/corgi.jpg
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The URL above will display this image with a text overlay using our custom font!&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_800/g_west,x_30,w_350,c_fit,co_white,bo_4px_solid_black,l_text:snowballs.ttf_180_stroke:Let%20it%20snow!/corgi.jpg&quot; alt=&quot;A corgi with the text “Let it snow!” overlaid.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;h2&gt;What to do next&lt;/h2&gt;
&lt;p&gt;For next steps:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Learn how to use text overlays to &lt;a href=&quot;/blog/auto-generate-social-image&quot;&gt;automatically generate social sharing cards for blog posts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Get more detail on &lt;a href=&quot;/blog/add-text-overlay-cloudinary&quot;&gt;how text overlays work&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Use the &lt;a href=&quot;/blog/upload-custom-font-cloudinary-node&quot;&gt;Node SDK to upload custom fonts programmatically&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Read the &lt;a href=&quot;https://cloudinary.com/documentation/image_transformations?ap=lwj#adding_text_captions&quot;&gt;Cloudinary docs on text overlays&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Upload a Custom Font to Cloudinary Using the Node SDK</title><link>https://codetv.dev/blog/upload-custom-font-cloudinary-node/</link><guid isPermaLink="true">https://codetv.dev/blog/upload-custom-font-cloudinary-node/</guid><description>A tutorial on how to upload and use custom fonts in text overlays with Cloudinary to generate images with custom text.
</description><pubDate>Sun, 05 Jan 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;If you want to use custom fonts with Cloudinary, you need to upload them as authenticated assets. This script will allow you to upload an OTF or TTF to use when adding text overlays to images.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;IMPORTANT:&lt;/strong&gt; only upload fonts if you have the appropriate licenses.&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;Create a project and install dependencies&lt;/h2&gt;
&lt;p&gt;Make sure you&apos;ve created a folder, initialized the Node project, and installed the &lt;code&gt;cloudinary&lt;/code&gt; Node SDK:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir upload-fonts

cd upload-fonts/

npm init -y

npm install --save cloudinary
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Upload the custom font to Cloudinary&lt;/h2&gt;
&lt;p&gt;Create a file in this folder called &lt;code&gt;upload.js&lt;/code&gt; and add the following inside:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const cloudinary = require(&apos;cloudinary&apos;).v2;

// this is shown at the top right of https://cloudinary.com/console
const CLOUDINARY_CLOUD_NAME = &apos;&amp;lt;YOUR CLOUD NAME&amp;gt;&apos;;

// find these at https://cloudinary.com/console/settings/security
const CLOUDINARY_API_KEY = &apos;&amp;lt;YOUR CLOUDINARY API KEY&amp;gt;&apos;;
const CLOUDINARY_API_SECRET = &apos;&amp;lt;YOUR CLOUDINARY API SECRET&amp;gt;&apos;;

// path to the custom font (TTF or OTF only), relative to this file
const PATH_TO_FILE = &apos;my-font.ttf&apos;;

// used in Cloudinary URLs — no underscores allowed!
const PUBLIC_ID = &apos;my-font.ttf&apos;;

const uploadToCloudinary = async () =&amp;gt; {
  cloudinary.config({
    cloud_name: CLOUDINARY_CLOUD_NAME,
    api_key: CLOUDINARY_API_KEY,
    api_secret: CLOUDINARY_API_SECRET,
  });

  const result = await cloudinary.uploader.upload(PATH_TO_FILE, {
    resource_type: &apos;raw&apos;,
    type: &apos;authenticated&apos;,
    public_id: PUBLIC_ID,
  });

  console.log(result);
};

uploadToCloudinary();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Update the information at the top with your own Cloudinary and font info, then run the script:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;node upload.js
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You will see the output of the script showing the upload details (or an error, if one occurred). It will look something like this (some details masked):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{ public_id: &apos;Tahu!.ttf&apos;,
  version: 1578268616,
  signature: &apos;342a1f68ea8815b2894dadf8ecc65935ca7daade&apos;,
  resource_type: &apos;raw&apos;,
  created_at: &apos;2020-01-05T23:56:56Z&apos;,
  tags: [],
  bytes: 39972,
  type: &apos;authenticated&apos;,
  etag: &apos;98c36c6da792d6726ffdba9e6d6989ab&apos;,
  placeholder: false,
  url:
    &apos;&amp;lt;http://res.cloudinary.com/jlengstorf/raw/authenticated/[redacted]/v1578268616/Tahu%21.ttf&amp;gt;&apos;,
  secure_url:
    &apos;&amp;lt;https://res.cloudinary.com/jlengstorf/raw/authenticated/[redacted]/v1578268616/Tahu%21.ttf&amp;gt;&apos;,
  access_mode: &apos;public&apos;,
  original_filename: &apos;Tahu!&apos; }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; this example uses the &lt;a href=&quot;https://www.behance.net/gallery/63159677/TAHU-FREE-SCRIPT-FONT&quot;&gt;free Tahu! font&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Use the custom font in a Cloudinary image&lt;/h2&gt;
&lt;p&gt;Now that we&apos;ve uploaded the font, we can use it for image overlays.&lt;/p&gt;
&lt;p&gt;Let&apos;s start with this image of a llama:&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_800,q_auto,f_auto/v1578269094/llama-kimmy-williams.jpg&quot; alt=&quot;a llama&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;The URL to display this image is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://res.cloudinary.com/jlengstorf/image/upload/w_800,q_auto,f_auto/v1578269094/llama-kimmy-williams.jpg
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can break this URL into four pieces:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;https://res.cloudinary.com/jlengstorf/image/upload&lt;/code&gt;&lt;/strong&gt; — this is the &lt;strong&gt;URL base&lt;/strong&gt;, which is going to be the same for all images in my Cloudinary account&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;/w_800,q_auto,f_auto&lt;/code&gt;&lt;/strong&gt; — these are &lt;strong&gt;image transforms&lt;/strong&gt; tells Cloudinary that I want the image at 800 pixels wide and that the format and quality should be set automatically to best suit this browser&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;/v1578269094&lt;/code&gt;&lt;/strong&gt; — this is teh &lt;strong&gt;version&lt;/strong&gt;, an optional piece that allows Cloudinary to cache the image aggressively to cut down on bandwidth and processing&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;/llama-kimmy-williams.jpg&lt;/code&gt;&lt;/strong&gt; — this is the &lt;strong&gt;public ID&lt;/strong&gt; of the image we want to display and an optional extension telling Cloudinary what format should be used (this will be overridden by &lt;code&gt;f_auto&lt;/code&gt;, though)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;To insert our custom text overlay, we&apos;ll add a new piece in between the image transforms and the version, which will configure text:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/g_south,y_20,co_white,l_text:Tahu!.ttf_70:Have%20you%20been%20to%20Machu%20Picchu%3F
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This sets a few things for our text:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;g_south&lt;/code&gt;&lt;/strong&gt; — put the text at the bottom-center of the image&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;y_20&lt;/code&gt;&lt;/strong&gt; — move the text 20px away from the bottom of the image&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;co_white&lt;/code&gt;&lt;/strong&gt; — set the text color to white&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;l_text:Tahu!.ttf_70&lt;/code&gt;&lt;/strong&gt; — &lt;code&gt;l_text&lt;/code&gt; is the overlay config, then we use the custom font&apos;s public ID, then &lt;code&gt;_70&lt;/code&gt; to set the font size&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;:Have%20you%20been%20to%20Machu%20Picchu%3F&lt;/code&gt;&lt;/strong&gt; — we can set any text we want here — the text needs to be URI-encoded&lt;/li&gt;
&lt;/ul&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Heads up!&lt;/strong&gt; If you want more details on what’s going on here, I wrote a full post on &lt;a href=&quot;/blog/add-text-overlay-cloudinary&quot;&gt;how text overlays work in Cloudinary&lt;/a&gt;.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;Once we&apos;ve added this to the URL, it looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://res.cloudinary.com/jlengstorf/image/upload/w_800,q_auto,f_auto/g_south,y_20,co_white,l_text:Tahu!.ttf_70:Have%20you%20been%20to%20Machu%20Picchu%3F/v1578269094/llama-kimmy-williams.jpg
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And the image now includes our text overlay using the custom font!&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_800,q_auto,f_auto/g_south,y_20,co_white,l_text:Tahu!.ttf_70:Have%20you%20been%20to%20Machu%20Picchu%3F/v1578269094/llama-kimmy-williams.jpg&quot; alt=&quot;llama with text overlay saying, “Have you been to Machu Picchu?”&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;h2&gt;What to do next&lt;/h2&gt;
&lt;p&gt;For next steps:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Learn how to use text overlays to &lt;a href=&quot;/blog/auto-generate-social-image&quot;&gt;automatically generate social sharing cards for blog posts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Get more detail on &lt;a href=&quot;/blog/add-text-overlay-cloudinary&quot;&gt;how text overlays work&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Use the &lt;a href=&quot;/blog/upload-custom-font-cloudinary-media-library&quot;&gt;media library to upload custom fonts via UI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Read the &lt;a href=&quot;https://cloudinary.com/documentation/image_transformations?ap=lwj#adding_text_captions&quot;&gt;Cloudinary docs on text overlays&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Add Text Overlays to Images Using Cloudinary</title><link>https://codetv.dev/blog/add-text-overlay-cloudinary/</link><guid isPermaLink="true">https://codetv.dev/blog/add-text-overlay-cloudinary/</guid><description>In this post, learn how to use Cloudinary to add text overlays to images using URL-based APIs.
</description><pubDate>Sat, 04 Jan 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In many cases, it&apos;s helpful to add text overlays to images. Sharing content on social media, for example, can be more effective with a sharing image including information about the content.&lt;/p&gt;
&lt;p&gt;In this post, we&apos;ll look at how we can use &lt;a href=&quot;https://jason.energy/cloudinary&quot;&gt;Cloudinary&lt;/a&gt; to add text overlays to images using URL-based APIs.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Heads up!&lt;/strong&gt; If this article helps you, you can &lt;a href=&quot;https://jason.energy/cloudinary&quot;&gt;sign up for Cloudinary with this link&lt;/a&gt; — you’ll get a free Cloudinary account, and I get a little bonus cash to help cover the costs of running this site.&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;How do text overlays work with Cloudinary?&lt;/h2&gt;
&lt;p&gt;Cloudinary has a whole set of &lt;a href=&quot;https://cloudinary.com/documentation/image_transformation_reference?ap=lwj#overlay_parameter&quot;&gt;APIs to add overlays to images&lt;/a&gt;. We can combine those with other transforms to place text anywhere on an image. All of these transformations are added in the URL of our image.&lt;/p&gt;
&lt;p&gt;As a simplified example, let&apos;s add some text to a picture of a corgi:&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_800/corgi.jpg&quot; alt=&quot;corgi&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;The URL for this image is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://res.cloudinary.com/jlengstorf/image/upload/w_800/corgi.jpg
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will display the corgi image at 800px wide because we use the &lt;code&gt;w_800&lt;/code&gt; transform in the URL.&lt;/p&gt;
&lt;h2&gt;Add a text overlay to an image using Cloudinary&lt;/h2&gt;
&lt;p&gt;To add text, we&apos;re going to add a second transform — transforms in Cloudinary are separated by forward slashes (&lt;code&gt;/&lt;/code&gt;) — that tells Cloudinary to add a text overlay using the font Arial at 64pt size:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://res.cloudinary.com/jlengstorf/image/upload/w_800/l_text:arial_64:Ready%20to%20party/corgi.jpg
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The change here is this string:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/l_text:arial_64:Ready%20to%20Party
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;l_text:&lt;/code&gt; tells Cloudinary that we&apos;re going to do a text overlay&lt;/li&gt;
&lt;li&gt;&lt;code&gt;arial_64:&lt;/code&gt; configures the text overlay to use Arial at 64pt&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ready%20to%20party&lt;/code&gt; is the URL-encoded text we want to display&lt;/li&gt;
&lt;/ul&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Heads up!&lt;/strong&gt; Cloudinary supports several fonts by default — there’s no list available, so you’ll have to just try a font out and see if it works. If the font you’d like to use isn’t available by default, you can &lt;a href=&quot;/blog/upload-custom-font-cloudinary-media-library&quot;&gt;upload custom fonts for use with Cloudinary text overlays&lt;/a&gt; as well.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;If we open this URL in a browser, we’ll see the text overlaid on the image:&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_800/l_text:arial_64:Ready%20to%20party/corgi.jpg&quot; alt=&quot;Corgi with the text “Ready to party” overlaid.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;h2&gt;Position the text overlay on an image using Cloudinary&lt;/h2&gt;
&lt;p&gt;Our image looks okay, but it would be better if the text wasn&apos;t on top of our doggo. Fortunately, Cloudinary allows us to position text using additional transforms.&lt;/p&gt;
&lt;p&gt;First, let&apos;s &lt;a href=&quot;https://cloudinary.com/documentation/image_transformation_reference?ap=lwj#gravity_parameter&quot;&gt;set the &quot;gravity&quot;&lt;/a&gt; of the text so it&apos;s anchored to the bottom-left corner. This is done using directions for whatever reason, so bottom and left are considered south and west.&lt;/p&gt;
&lt;p&gt;Add &lt;code&gt;g_south_west&lt;/code&gt; to the text overlay transformation section of the URL, separating it with a comma:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://res.cloudinary.com/jlengstorf/image/upload/w_800/g_south_west,l_text:arial_64:Ready%20to%20party/corgi.jpg
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the image looks like this:&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_800/g_south_west,l_text:arial_64:Ready%20to%20party/corgi.jpg&quot; alt=&quot;Corgi with the text “Ready to party” overlaid.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;This is better, but now the text is crammed against the edge of the photo — not great.&lt;/p&gt;
&lt;h2&gt;Add X and Y offsets to text overlays in Cloudinary&lt;/h2&gt;
&lt;p&gt;We can adjust this by changing the X and Y offsets, though. The offsets start from wherever the gravity is set, so in this case our X offset will be from the left edge and the Y offset will be from the bottom.&lt;/p&gt;
&lt;p&gt;These are set as &lt;code&gt;x_40,y_40&lt;/code&gt; in the text overlay transformation section:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://res.cloudinary.com/jlengstorf/image/upload/w_800/g_south_west,x_40,y_40,l_text:arial_64:Ready%20to%20party/corgi.jpg
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the image is offset from the edge of the image by 40px at the left and bottom:&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_800/g_south_west,x_40,y_40,l_text:arial_64:Ready%20to%20party/corgi.jpg&quot; alt=&quot;Corgi with the text “Ready to party” overlaid.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;Almost there!&lt;/p&gt;
&lt;h2&gt;Set text overlays to wrap at a specified width in Cloudinary&lt;/h2&gt;
&lt;p&gt;Now let&apos;s get the text to wrap so it doesn&apos;t overlay the corgi at all. We can do this by setting a width and telling the text to &quot;fit&quot; using the crop settings.&lt;/p&gt;
&lt;p&gt;These are set as &lt;code&gt;w_250,c_fit&lt;/code&gt; in the transformation section:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://res.cloudinary.com/jlengstorf/image/upload/w_800/g_south_west,x_40,y_40,w_250,c_fit,l_text:arial_64:Ready%20to%20party/corgi.jpg
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now it looks pretty good!&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jlengstorf/image/upload/w_800/g_south_west,x_40,y_40,w_250,c_fit,l_text:arial_64:Ready%20to%20party/corgi.jpg&quot; alt=&quot;Corgi with the text “Ready to party” overlaid.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; because we set the gravity to &quot;south&quot;, the text will wrap so that the bottom-most line of text is at our given Y offset. This will be helpful for keeping two separate blocks of text together in our template.&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;What to do next&lt;/h2&gt;
&lt;p&gt;To take your text overlays to the next level, try:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/blog/upload-custom-font-cloudinary-media-library&quot;&gt;using a custom font for Cloudinary text overlays&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/auto-generate-social-image&quot;&gt;creating custom social media cards automatically using text overlays&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;reading the &lt;a href=&quot;https://cloudinary.com/documentation/image_transformations?ap=lwj#adding_text_captions&quot;&gt;Cloudinary docs on text overlays&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Data abstraction in Gatsby themes (and React apps in general)</title><link>https://codetv.dev/blog/data-abstraction-in-apps/</link><guid isPermaLink="true">https://codetv.dev/blog/data-abstraction-in-apps/</guid><description>How do we keep the content separate from presentation in React apps? Here’s one approach we’re using in Gatsby themes.
</description><pubDate>Mon, 25 Mar 2019 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;It’s an inevitability that, over time, any actively maintained application will grow in complexity.&lt;/p&gt;
&lt;p&gt;In general, tools are designed for a specific level of complexity. We call this tool “entry level”, while that one is “enterprise-ready”.&lt;/p&gt;
&lt;p&gt;Level-based tooling works on the assumption that applications won’t graduate from one level to the next. (Or, if they &lt;em&gt;do&lt;/em&gt; move to the next level of complexity, they’ll be able to support a complete rewrite in a tool better suited for their new needs.)&lt;/p&gt;
&lt;p&gt;This seems to be a generally accepted practice: “Just get something quick-and-dirty built to prove this out, then we’ll go back and rebuild it to be ‘enterprise-ready’ later.”&lt;/p&gt;
&lt;p&gt;But to me this feels... wasteful.&lt;/p&gt;
&lt;p&gt;Why shouldn’t our tools grow up with us? Why should we be forced to throw away previous work because our app needs new features?&lt;/p&gt;
&lt;h2&gt;We should build tools that adapt to increasing demands&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;If we’re deliberate about the architecture of our tools, we can use &lt;a href=&quot;/progressive-disclosure-of-complexity&quot;&gt;progressive disclosure of complexity&lt;/a&gt; to create apps that start as entry-level, beginner-friendly starter kits, but allow developers to opt out of specific abstractions on a per-case basis — all the way up to taking full control.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This is something we’ve been thinking &lt;em&gt;hard&lt;/em&gt; about at &lt;a href=&quot;https://gatsbyjs.org&quot;&gt;Gatsby&lt;/a&gt;, and it’s the philosophical underpinning of what we’re trying to accomplish with &lt;a href=&quot;https://www.gatsbyjs.org/blog/tags/themes/&quot;&gt;themes&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The goal is to start with nothing but a theme and a data source (like a folder full of Markdown files), but allow developers to progressively peel back the abstractions until they’re &lt;a href=&quot;https://www.gatsbyjs.org/docs/customization/&quot;&gt;poking at the underlying Webpack and Babel configuration&lt;/a&gt;.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;/images/data-abstraction.png&quot; alt=&quot;Visualization of chaotic data progressively becoming more organized.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;h2&gt;Layers of abstraction in a typical web app&lt;/h2&gt;
&lt;p&gt;If we leave the really hardcore customization out for now, we’re left with three main levels of abstraction:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;The content layer&lt;/strong&gt; — the information displayed on the site. This could be
a folder full of Markdown, a headless CMS, or some kind of dashboard or database. No code is required at this layer.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Presentation&lt;/strong&gt; — this is a theme that defines markup and styles for
presentational components. This layer is only concerned with the UI; it doesn’t care where data comes from.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data orchestration&lt;/strong&gt; — this theme executes queries and provides the data to
components as props. This layer is what actually connects to the source of content, whether that’s the filesystem, a database, or a third-party API.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;How to set up abstraction layers in Gatsby themes&lt;/h2&gt;
&lt;p&gt;As theme authors, we can choose the level of abstraction each theme addresses. Later in this post we’ll look at real site using a Gatsby theme, but — in reality — our theme is actually &lt;em&gt;two&lt;/em&gt; themes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a parent theme to handle the data layer&lt;/li&gt;
&lt;li&gt;a child theme to handle the presentation layer&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There’s a reason for this, and it’s among the more exciting parts of Gatsby themes in my mind: &lt;strong&gt;by abstracting data management, we can define a schema for data — and then stop worrying about where the data comes from.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In practical terms, this means — assuming the abstractions are done properly — a Gatsby theme for managing &lt;code&gt;Post&lt;/code&gt; data can be created as a schema contract, and dozens (or hundreds, or thousands) of presentation-layer themes can be built against that schema contract: they know that a &lt;code&gt;Post&lt;/code&gt; has a &lt;code&gt;title&lt;/code&gt; and &lt;code&gt;content&lt;/code&gt;, so they confidently grab that data and style it.&lt;/p&gt;
&lt;p&gt;But &lt;em&gt;then&lt;/em&gt; — and here’s where this gets really exciting — &lt;em&gt;any data source can be adapted into the &lt;code&gt;Post&lt;/code&gt; format&lt;/em&gt;. In this example we’re mapping &lt;code&gt;MarkdownRemark&lt;/code&gt; nodes to the &lt;code&gt;Post&lt;/code&gt; type, but there’s nothing stopping us from also mapping data from a headless CMS, JSON, or &lt;em&gt;literally any other data type&lt;/em&gt; to the &lt;code&gt;Post&lt;/code&gt; type.&lt;/p&gt;
&lt;p&gt;This is huge, because it means that we have the potential to create a shared pool of themes that work for &lt;em&gt;any&lt;/em&gt; data source. To the best of my knowledge, this has never been possible before; if you like a Woocommerce WordPress theme but want to use Shopify, you’d need a developer to port the WordPress theme into a Shopify theme.&lt;/p&gt;
&lt;p&gt;With data abstraction in Gatsby themes, the base theme would query against a &lt;code&gt;Product&lt;/code&gt; type, and product data from WordPress and product data from Shopify would be mapped to the &lt;code&gt;Product&lt;/code&gt; schema.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;This means that the same theme that worked for WordPress will work for Shopify with &lt;em&gt;zero code changes&lt;/em&gt;.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;To put these abstractions in context, let’s build a blog using a Gatsby theme, look at the layers of abstraction, then opt out of them to customize our app.&lt;/p&gt;
&lt;h2&gt;Build a site using Gatsby themes&lt;/h2&gt;
&lt;p&gt;To start, let’s create a site using a Gatsby theme that requires almost no code or configuration.&lt;/p&gt;
&lt;h3&gt;Step 1: create a folder for the site&lt;/h3&gt;
&lt;p&gt;To start, create a new directory and move into it. This will be our blog site.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Create the site folder and move into it.
mkdir data-abstraction-example-site
cd data-abstraction-example-site/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 2: add a &lt;code&gt;package.json&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Next, create a &lt;code&gt;package.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Create a package.json with the default settings.
npm init -y
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 3: install dependencies&lt;/h3&gt;
&lt;p&gt;To use a Gatsby theme, we need to install Gatsby, React, React DOM, and the theme itself:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install gatsby react react-dom @jlengstorf/gatsby-theme-style
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 4: tell Gatsby to use the theme&lt;/h3&gt;
&lt;p&gt;As our final code step, let’s tell Gatsby to use the theme by creating a &lt;code&gt;gatsby-config.js&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module.exports = {
  __experimentalThemes: [&apos;@jlengstorf/gatsby-theme-style&apos;],
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 5: add a post&lt;/h3&gt;
&lt;p&gt;Next, we can add content. Create a folder called &lt;code&gt;content/posts/&lt;/code&gt;, then add a new file called &lt;code&gt;foo.md&lt;/code&gt; with the following content:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
title: Super Sweet Post
date: 2019-02-28
author: Jason Lengstorf
---

This is blog content!

[Gatsby themes](https://www.gatsbyjs.org/blog/2019-03-11-gatsby-themes-roadmap/) are cool.
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 6: start the site&lt;/h3&gt;
&lt;p&gt;Once this is set up, we can start the app and see our post:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gatsby develop
&lt;/code&gt;&lt;/pre&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; This assumes you already have the Gatsby CLI installed. If you don’t, run &lt;code&gt;npm i -g gatsby-cli&lt;/code&gt; and try the above command again.&lt;/p&gt;&lt;/aside&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;/images/theme-post.jpg&quot; alt=&quot;The post preview with our theme styling.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;Once the server starts, we can open &lt;code&gt;http://localhost:8000/posts/&lt;/code&gt; to see the content we created.&lt;/p&gt;
&lt;h2&gt;What makes Gatsby themes different?&lt;/h2&gt;
&lt;p&gt;With almost no code, we’ve created a complete blog from scratch. On its own, this is exciting, but not groundbreaking. Pretty much every website builder out there has themes in one form or another.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;So what’s so special about Gatsby themes?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;What sets Gatsby themes apart from other theming systems is the ability to selectively opt out of parts of the abstraction. We can keep the convenience in all the places where the defaults suit our needs and make customizations where they don’t.&lt;/p&gt;
&lt;p&gt;In short: &lt;strong&gt;Gatsby themes don’t force us to choose between convenience and control.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;To understand the power of Gatsby themes, we need to start evolving our site and outgrowing the default settings.&lt;/p&gt;
&lt;h2&gt;Selectively opting out of abstractions&lt;/h2&gt;
&lt;p&gt;As our site grows, we may decide that we want more control over the layout. Perhaps we want to add a link to our Twitter to the end of each post.&lt;/p&gt;
&lt;p&gt;To do this, we take advantage of &lt;a href=&quot;https://www.gatsbyjs.org/docs/themes/api-reference#component-shadowing&quot;&gt;&lt;em&gt;component shadowing&lt;/em&gt;&lt;/a&gt;. This is a technique that allows us to selectively replace parts of the theme without needing to eject the entire theme.&lt;/p&gt;
&lt;p&gt;Gatsby handles component shadowing by looking in a site’s &lt;code&gt;src&lt;/code&gt; directory for a folder named after the theme being used, then looking for paths that match the theme’s structure.&lt;/p&gt;
&lt;p&gt;For example, if the theme is called &lt;code&gt;gatsby-theme-foo&lt;/code&gt; and it has a file located at &lt;code&gt;src/components/bar.js&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.
└── gatsby-theme-foo
   └── src
   └── components
   └── bar.js
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then we can shadow this component by creating a new file in our site here:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.
└── gatsby-theme-foo
└── src
└── gatsby-theme-foo
└── components
└── bar.js
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To shadow our &lt;code&gt;&amp;lt;Post&amp;gt;&lt;/code&gt; component, we need to create a new file in our site at &lt;code&gt;src/@jlengstorf/gatsby-theme-data/components/post.js&lt;/code&gt; and add the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import React from &apos;react&apos;;

const Post = ({ title, content, author, date }) =&amp;gt; (
  &amp;lt;React.Fragment&amp;gt;
    &amp;lt;h1 className=&quot;post-heading&quot;&amp;gt;{title}&amp;lt;/h1&amp;gt;
    &amp;lt;p className=&quot;post-byline&quot;&amp;gt;
      Posted on{&apos; &apos;}
      &amp;lt;time dateTime={new Date(date).toISOString()}&amp;gt;
        {new Date(date).toLocaleDateString(&apos;en-US&apos;, {
          year: &apos;numeric&apos;,
          month: &apos;long&apos;,
          day: &apos;numeric&apos;,
        })}
      &amp;lt;/time&amp;gt;{&apos; &apos;}
      by {author.name}
    &amp;lt;/p&amp;gt;
    &amp;lt;div
      className=&quot;post-content&quot;
      dangerouslySetInnerHTML={{ __html: content }}
    /&amp;gt;
    // highlight-start
    &amp;lt;p&amp;gt;
      For more content like this, you should{&apos; &apos;}
      &amp;lt;a href=&quot;https://twitter.com/jlengstorf&quot;&amp;gt;follow me on Twitter&amp;lt;/a&amp;gt;.
    &amp;lt;/p&amp;gt;
    // highlight-end
  &amp;lt;/React.Fragment&amp;gt;
);

export default Post;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After saving the new component, stop the site (&lt;code&gt;control&lt;/code&gt; + &lt;code&gt;C&lt;/code&gt;), then start it again:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gatsby develop
&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;/images/theme-post-shadowed.jpg&quot; alt=&quot;Post page with the new link displayed at the bottom&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;The post now shows a link to Twitter at the bottom.&lt;/p&gt;
&lt;h2&gt;We can go much deeper&lt;/h2&gt;
&lt;p&gt;It’s also possible to modify the underlying data, add new components, or even compose themes together (e.g. a blog theme and an ecommerce theme). We won’t get into that in this post, but the potential of Gatsby themes is &lt;em&gt;extremely&lt;/em&gt; high.&lt;/p&gt;
&lt;h2&gt;Further reading and resources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/jlengstorf/data-abstraction-example-site&quot;&gt;Source code for the example site&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/jlengstorf/gatsby-theme-data&quot;&gt;The data theme&lt;/a&gt; used in this example&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/jlengstorf/gatsby-theme-style&quot;&gt;The presentation theme&lt;/a&gt; used in this example&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.gatsbyjs.org/blog/tags/themes/&quot;&gt;Posts about Gatsby themes&lt;/a&gt; on the Gatsby blog&lt;/li&gt;
&lt;li&gt;A deeper exploration of &lt;a href=&quot;https://jason.energy/progressive-disclosure-of-complexity&quot;&gt;progressive disclosure of complexity&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Automatic WordPress Deployment + Free SSL: Trellis How-To
</title><link>https://codetv.dev/blog/learn-trellis-wordpress-roots/</link><guid isPermaLink="true">https://codetv.dev/blog/learn-trellis-wordpress-roots/</guid><description>A step-by-step video tutorial on setting up a local WordPress development environment in minutes using Trellis, plus how to deploy FAST with free SSL.
</description><pubDate>Mon, 17 Oct 2016 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://youtu.be/Ls30HGKru8A&quot;&gt;Watch on YouTube&lt;/a&gt;&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Heads up:&lt;/strong&gt; this post was written in 2016, and some of the tools and prices may have changed. The code &lt;em&gt;should&lt;/em&gt; still work, but you may want to look for a more up-to-date tutorial.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;&lt;strong&gt;Elevator Pitch:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Set up a new WordPress site using the Roots stack.&lt;/p&gt;
&lt;h2&gt;Prerequisites&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;A private server (we&apos;ll walk through how to set this up for $5/month) — Trellis &lt;em&gt;cannot&lt;/em&gt; run on a shared host, so this isn&apos;t optional.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://brew.sh/&quot;&gt;Homebrew&lt;/a&gt; (v1.0.7 used in this tutorial)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# Install Homebrew.
ruby -e &quot;$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://git-scm.com/&quot;&gt;Git&lt;/a&gt; (v2.10.1 used in this tutorial)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# Install Git with Homebrew
brew install git
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.ansible.com/&quot;&gt;Ansible&lt;/a&gt; (v2.1.2.0 used in this tutorial)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# Install Ansible with Homebrew
brew install ansible
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://getcomposer.org/&quot;&gt;Composer&lt;/a&gt; (v1.2.1 used in this tutorial)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# Install Composer with Homebrew
brew install composer
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.virtualbox.org/wiki/Downloads&quot;&gt;Virtualbox&lt;/a&gt; (v5.0.18r106667 used in this tutorial)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.vagrantup.com/downloads.html&quot;&gt;Vagrant&lt;/a&gt; (v1.8.5 used in this tutorial)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Part I: Install Trellis&lt;/h2&gt;
&lt;p&gt;To get things rolling, we need to grab a copy of Trellis from GitHub.&lt;/p&gt;
&lt;h3&gt;Create a new directory for the site.&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# Move into the directory where where you keep dev projects.
cd ~/dev/code.lengstorf.com/projects/

# Create a new directory for this project
mkdir learn-trellis

# Move into the new directory.
cd learn-trellis/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Get a copy of Trellis to manage environments and deployment.&lt;/h3&gt;
&lt;p&gt;Since we want to track our copy of Trellis as its own project, we don&apos;t actually want the Trellis repo information. By adding &lt;code&gt;--depth=1&lt;/code&gt; to the &lt;code&gt;clone&lt;/code&gt; command, we&apos;re able to get only the most recently committed version (avoiding a lot of wasted bandwidth downloading the commit history). Then, we delete the &lt;code&gt;.git&lt;/code&gt; directory so we can have our own Git project.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Clone Trellis, but without all the Git history.
git clone --depth=1 git@github.com:roots/trellis.git

# Delete the `.git` file so we can have our own Git repo.
rm -rf trellis/.git
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Install Ansible dependencies.&lt;/h3&gt;
&lt;p&gt;One of the most powerful parts of Ansible is the ability to use community-supplied scripts — called &quot;roles&quot; in the Ansible world — rather than having to write them from scratch or copy-paste them from various forums and tutorials.&lt;/p&gt;
&lt;p&gt;This collection of community roles is called &lt;a href=&quot;https://galaxy.ansible.com/&quot;&gt;Galaxy&lt;/a&gt;, and it&apos;s home to thousands of roles. Trellis uses several of these to configure a WordPress server, so we need to get those installed.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Move into the Trellis directory
cd trellis/

# Install the Ansible dependencies for Trellis.
ansible-galaxy install -r requirements.yml
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Part II: Install Bedrock&lt;/h2&gt;
&lt;p&gt;We&apos;re not going to do talk much about Bedrock, but the short version is this: Bedrock makes WordPress development on a team much less frustrating by:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Using &lt;a href=&quot;https://roots.io/bedrock/docs/composer/&quot;&gt;Composer to manage all dependencies&lt;/a&gt; — including WordPress plugins and the core itself — to make it easier to work with version control and develop across teams&lt;/li&gt;
&lt;li&gt;Easier &lt;a href=&quot;https://roots.io/bedrock/docs/environment-variables/&quot;&gt;environment-specific configuration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Better security through &lt;a href=&quot;https://roots.io/bedrock/docs/folder-structure/&quot;&gt;directory structure improvements&lt;/a&gt; and the use of &lt;a href=&quot;https://github.com/roots/wp-password-bcrypt&quot;&gt;better password encryption&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Get a copy of Bedrock to make WordPress&apos;s file structure sane.&lt;/h3&gt;
&lt;p&gt;Just like we did with Trellis, we want to get a copy of Bedrock &lt;em&gt;without&lt;/em&gt; the Git repository. So we do a shallow clone and then remove the &lt;code&gt;.git&lt;/code&gt; folder.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Move back into the project root.
cd ..

# Clone Bedrock to the `site` directory.
git clone --depth=1 git@github.com:roots/bedrock.git site

# Remove the `.git` file so we can have our own Git repo.
rm -rf site/.git
&lt;/code&gt;&lt;/pre&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; We aren&apos;t installing any themes in this tutorial, but we would do so by installing the theme with Composer, which installs it in &lt;code&gt;site/web/app/themes/&lt;/code&gt;.&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;Part III: Configure a Development Site&lt;/h2&gt;
&lt;p&gt;With all the proper dependencies installed, we can start configuring our development site, which will run on our computer.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; If you&apos;ve never used local development sites before, they&apos;re typically available at http://localhost/ or http://127.0.0.1/, and can (usually) only be accessed from the computer you&apos;re currently using. If you&apos;re used to using FTP or some other remote form of development, local development will save &lt;em&gt;days&lt;/em&gt; of your life.&lt;/p&gt;&lt;/aside&gt;
&lt;h3&gt;Add site details to &lt;code&gt;wordpress_sites.yml&lt;/code&gt;.&lt;/h3&gt;
&lt;p&gt;Open &lt;code&gt;trellis/group_vars/development/wordpress_sites.yml&lt;/code&gt; in your editor:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Documentation: https://roots.io/trellis/docs/local-development-setup/
# `wordpress_sites` options: https://roots.io/trellis/docs/wordpress-sites
# Define accompanying passwords/secrets in group_vars/development/vault.yml

wordpress_sites:
  example.com:
    site_hosts:
      - canonical: example.dev
        redirects:
          - www.example.dev
    local_path: ../site # path targeting local Bedrock site directory (relative to Ansible root)
    admin_email: admin@example.dev
    multisite:
      enabled: false
    ssl:
      enabled: false
      provider: self-signed
    cache:
      enabled: false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you&apos;re not familiar with &lt;a href=&quot;http://yaml.org/&quot;&gt;YAML&lt;/a&gt;, it&apos;s a common way of describing data. It&apos;s indentation-based, so the default file creates a &lt;code&gt;wordpress_sites&lt;/code&gt; object, and that contains an &lt;code&gt;example.com&lt;/code&gt; object, which holds config properties (e.g. &lt;code&gt;site_hosts&lt;/code&gt;).&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; Trellis is really powerful because it allows us to define multiple WordPress sites. If we wanted to host two sites on the same box, all we&apos;d need to do is add another site to the &lt;code&gt;wordpress_sites&lt;/code&gt; object.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;Each site is identified by a key — in the example, the key is &lt;code&gt;example.com&lt;/code&gt; — which allows us to link together our development, staging, and production environments without a bunch of duplicated configuration.&lt;/p&gt;
&lt;p&gt;As a general rule, the production domain name is a good key to use.&lt;/p&gt;
&lt;p&gt;With that in mind, let&apos;s set up our site by making the following changes to &lt;code&gt;wordpress_sites.yml&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  wordpress_sites:
+   roots.code.lengstorf.com:
      site_hosts:
+       - canonical: roots.dev
-         redirects:
-           - www.example.dev
      local_path: ../site # path targeting local Bedrock site directory (relative to Ansible root)
+     admin_email: jason@lengstorf.com
      multisite:
        enabled: false
      ssl:
        enabled: false
        provider: self-signed
      cache:
        enabled: false

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I&apos;ll be deploying the site to a production domain of &lt;code&gt;roots.code.lengstorf.com&lt;/code&gt;, so that&apos;s my site key. For local development, we&apos;ll use &lt;code&gt;roots.dev&lt;/code&gt; as the URL, and we don&apos;t need the &lt;code&gt;redirects&lt;/code&gt; here, so we can remove them.&lt;/p&gt;
&lt;p&gt;Finally, we updated the &lt;code&gt;admin_email&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Save the changes and we&apos;re ready to move on.&lt;/p&gt;
&lt;h3&gt;Add credentials to &lt;code&gt;vault.yml&lt;/code&gt;.&lt;/h3&gt;
&lt;p&gt;Next, we&apos;ll open &lt;code&gt;trellis/group_vars/development/vault.yml&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Documentation: https://roots.io/trellis/docs/vault/
vault_mysql_root_password: devpw

# Variables to accompany `group_vars/development/wordpress_sites.yml`
# Note: the site name (`example.com`) must match up with the site name in the above file.
vault_wordpress_sites:
  example.com:
    admin_password: admin
    env:
      db_password: example_dbpassword
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here we can see that the site key is &lt;code&gt;example.com&lt;/code&gt;, so we&apos;ll need to update that.&lt;/p&gt;
&lt;p&gt;We also need to update &lt;code&gt;admin_password&lt;/code&gt;, which is the password we&apos;ll use to log into WordPress&apos;s dashboard.&lt;/p&gt;
&lt;p&gt;And finally, we&apos;ll add strong passwords for the MySQL root user and the site&apos;s DB access.&lt;/p&gt;
&lt;p&gt;Make the following changes in &lt;code&gt;vault.yml&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  # Documentation: https://roots.io/trellis/docs/vault/
+ vault_mysql_root_password: &quot;xy&amp;amp;G6o2kKH$#AFz247N.&quot;

  # Variables to accompany `group_vars/development/wordpress_sites.yml`
  # Note: the site name (`example.com`) must match up with the site name in the   above file.
  vault_wordpress_sites:
+   roots.code.lengstorf.com:
+     admin_password: &quot;DM93zj,o29KjT/bh$8G$&quot;
      env:
+       db_password: &quot;qP42q2*?hjt.P+x7Bzc6&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; The passwords are quoted because of all the garbage characters in them. Without the quotes, the installer may choke on them.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;Save the changes — now we&apos;re ready to fire up a local development box.&lt;/p&gt;
&lt;h2&gt;Part IV: Start a Local Instance of the WordPress Site Using Vagrant&lt;/h2&gt;
&lt;p&gt;Here&apos;s where the power of Trellis starts to become apparent.&lt;/p&gt;
&lt;p&gt;Assuming the required software is installed, only four steps are required to get to this point:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Clone Trellis.&lt;/li&gt;
&lt;li&gt;Clone Bedrock.&lt;/li&gt;
&lt;li&gt;Update &lt;code&gt;wordpress_sites.yaml&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Update &lt;code&gt;vault.yaml&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;When you compare that to the &quot;normal&quot; WordPress setup — clone WordPress, create a database, configure your local &lt;code&gt;hosts&lt;/code&gt; file to give you a development URL, and so on — Trellis is &lt;em&gt;far&lt;/em&gt; simpler. And we&apos;re not even to the really good stuff yet.&lt;/p&gt;
&lt;h3&gt;Start the development site.&lt;/h3&gt;
&lt;p&gt;To start the development site, it&apos;s one simple command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vagrant up
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first time you run this, it&apos;ll take a 5–10 minutes. This is because Vagrant needs to download and configure all the pieces required to get the box up and running properly. After the first time, a lot of dependencies will be cached, which makes things much quicker for subsequent calls to &lt;code&gt;vagrant up&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Check the development site on your local machine.&lt;/h3&gt;
&lt;p&gt;Once Vagrant is done, we can open the dev site by visiting &lt;code&gt;http://roots.dev/&lt;/code&gt; in our browser.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;/images/learn-roots-wordpress-01.jpg&quot; alt=&quot;The local instance of our WordPress site.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;h3&gt;Log into the WordPress dashboard.&lt;/h3&gt;
&lt;p&gt;To log into the WordPress dashboard, head to &lt;code&gt;http://roots.dev/wp/wp-admin/&lt;/code&gt; in your browser and use the &lt;code&gt;admin_password&lt;/code&gt; we set in &lt;code&gt;vault.yml&lt;/code&gt; earlier.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;/images/learn-roots-wordpress-02.jpg&quot; alt=&quot;The WordPress dashboard after logging in.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; Bedrock keeps WordPress in a subdirectory, &lt;code&gt;wp/&lt;/code&gt;, which helps us keep all of the WordPress core files separate from the rest of our app. This is helpful for managing the WordPress version with Composer.&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;Part V: Configure a Production Server&lt;/h2&gt;
&lt;p&gt;Wait, what? We&apos;re already deploying to production?&lt;/p&gt;
&lt;p&gt;Yep. That&apos;s how easy Trellis is.&lt;/p&gt;
&lt;h3&gt;Create a Digital Ocean droplet to host the site.&lt;/h3&gt;
&lt;p&gt;It&apos;s hard to beat $5/month for hosting a website, so Digital Ocean will be our choice of production host.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;If you don&apos;t have a DigitalOcean account, you can get $10 of credit — that&apos;s enough to run this site for two months — by signing up using this link: &lt;a href=&quot;https://m.do.co/c/9d561578e13a&quot;&gt;claim your $10 in DigitalOcean credit&lt;/a&gt;.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;To get started, create or log into your DigitalOcean account, then create a new droplet.&lt;/p&gt;
&lt;p&gt;Choose Ubuntu 16.04.1 x64 for the distribution, $5/mo for the size, and choose any datacenter region.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; It&apos;s not technically required for this tutorial, but you should add your SSH key here as well. (If you&apos;re not sure how to do this, &lt;a href=&quot;https://help.github.com/articles/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent/&quot;&gt;GitHub has a great guide&lt;/a&gt; on creating and finding SSH keys.)&lt;/p&gt;&lt;/aside&gt;
&lt;h3&gt;Update &lt;code&gt;hosts/production&lt;/code&gt;.&lt;/h3&gt;
&lt;p&gt;To tell Trellis where the production server lives, we need to add its IP address to &lt;code&gt;trellis/hosts/production&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Add each host to the [production] group and to a &quot;type&quot; group such as [web] or [db].
# List each machine only once per [group], even if it will host multiple sites.

[production]
your_server_hostname

[web]
your_server_hostname
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Make the following edits:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  # Add each host to the [production] group and to a &quot;type&quot; group such as [web] or [db].
  # List each machine only once per [group], even if it will host multiple sites.

  [production]
+ 162.243.171.188

  [web]
+ 162.243.171.188
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Enable free SSL and caching for production.&lt;/h3&gt;
&lt;p&gt;Setting up the production site configuration is nearly identical to the development site, except we need to add a few more settings for things like security. We can get these in place by editing &lt;code&gt;trellis/group_vars/production/wordpress_sites.yml&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Documentation: https://roots.io/trellis/docs/remote-server-setup/
# `wordpress_sites` options: https://roots.io/trellis/docs/wordpress-sites
# Define accompanying passwords/secrets in group_vars/production/vault.yml

wordpress_sites:
  example.com:
    site_hosts:
      - canonical: example.com
        redirects:
          - www.example.com
    local_path: ../site # path targeting local Bedrock site directory (relative to Ansible root)
    repo: git@github.com:example/example.com.git # replace with your Git repo URL
    repo_subtree_path: site # relative path to your Bedrock/WP directory in your repo
    branch: master
    multisite:
      enabled: false
    ssl:
      enabled: false
      provider: letsencrypt
    cache:
      enabled: false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Inside, make the following edits:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  # Documentation: https://roots.io/trellis/docs/remote-server-setup/
  # `wordpress_sites` options: https://roots.io/trellis/docs/wordpress-sites
  # Define accompanying passwords/secrets in group_vars/production/vault.yml

  wordpress_sites:
+   roots.code.lengstorf.com:
      site_hosts:
+       - canonical: roots.code.lengstorf.com
-         redirects:
-           - www.example.com
      local_path: ../site # path targeting local Bedrock site directory (relative to Ansible root)
+     repo: git@github.com:jlengstorf/roots.code.lengstorf.com.git # replace with your Git repo URL
      repo_subtree_path: site # relative path to your Bedrock/WP directory in your repo
      branch: master
      multisite:
        enabled: false
      ssl:
+       enabled: true
        provider: letsencrypt
      cache:
+       enabled: true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In addition to setting the site&apos;s URL, we also tell Trellis where the source code can be found with the &lt;code&gt;repo&lt;/code&gt; setting, and enable SSL and caching for a faster, more secure site.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;IMPORTANT:&lt;/strong&gt; You should &lt;em&gt;always&lt;/em&gt; enable SSL for a production site. First, Google is starting to penalize sites that don&apos;t use SSL in search engine results, and with a warning in the Chrome browser. Second, it&apos;s free and really, &lt;em&gt;really&lt;/em&gt; easy thanks to &lt;a href=&quot;https://letsencrypt.org/&quot;&gt;Let&apos;s Encrypt&lt;/a&gt; — seriously, &lt;strong&gt;the only thing you need to do to enable SSL is to change this setting.&lt;/strong&gt; No joke. So do it.&lt;/p&gt;&lt;/aside&gt;
&lt;h3&gt;Add security settings to &lt;code&gt;vault.yml&lt;/code&gt;.&lt;/h3&gt;
&lt;p&gt;Next, we need to add passwords and encryption keys for the site. We do this by editing &lt;code&gt;trellis/group_vars/production/vault.yml&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Documentation: https://roots.io/trellis/docs/vault/
vault_mysql_root_password: productionpw

# Documentation: https://roots.io/trellis/docs/security/
vault_users:
  - name: &apos;{{ admin_user }}&apos;
    password: example_password
    salt: &apos;generateme&apos;

# Variables to accompany `group_vars/production/wordpress_sites.yml`
# Note: the site name (`example.com`) must match up with the site name in the above file.
vault_wordpress_sites:
  example.com:
    env:
      db_password: example_dbpassword
      # Generate your keys here: https://roots.io/salts.html
      auth_key: &apos;generateme&apos;
      secure_auth_key: &apos;generateme&apos;
      logged_in_key: &apos;generateme&apos;
      nonce_key: &apos;generateme&apos;
      auth_salt: &apos;generateme&apos;
      secure_auth_salt: &apos;generateme&apos;
      logged_in_salt: &apos;generateme&apos;
      nonce_salt: &apos;generateme&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Make the following changes:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  # Documentation: https://roots.io/trellis/docs/vault/
+ vault_mysql_root_password: &quot;z3Q6m3x8y?j@k&amp;amp;3+xuBq&quot;

  # Documentation: https://roots.io/trellis/docs/security/
  vault_users:
    - name: &quot;{{ admin_user }}&quot;
+     password: &quot;vH.827WQ2,t9?vyZyuB@&quot;
+     salt: &quot;jRMB764/EpB+,j(hvL98&quot;

  # Variables to accompany `group_vars/production/wordpress_sites.yml`
  # Note: the site name (`example.com`) must match up with the site name in the above file.
  vault_wordpress_sites:
+   roots.code.lengstorf.com:
      env:
+       db_password: &quot;#RLLE3)h9z9RDMT6d/4%&quot;
        # Generate your keys here: https://roots.io/salts.html
+       auth_key: &quot;&amp;amp;/qSrw23*@HeP2Kk#{^Ntx[!N&amp;gt;7#IdA=pCtI5gkpfgnn8{gDQ]2PQye]OkI.-p9f&quot;
+       secure_auth_key: &quot;LwX}3v}-P72LyH&amp;lt;o+kK&amp;amp;&amp;amp;M]^F3/#*&amp;amp;[um5$OiV@v:b!052Kaq%b]OQy=$@7F&amp;gt;=fF&quot;
+       logged_in_key: &quot;k+;dLHoBtR)5Y4VfyxMmm(fKp+Z&amp;lt;Uy]1PZvS_f#o`xGy7e=GNN1BEkd11s035t1:&quot;
+       nonce_key: &quot;!7#7ow=)d17Y[RlzSVA)_?GH&amp;lt;.e7-|SvD*&amp;amp;|;_5Y7)J@w]Dl,Q9_o!hUP8]G]n.k&quot;
+       auth_salt: &quot;G)0!0Z?MGKS?K,s$03=4e5Xu+[l:hw|X5Llr^H.e#[^Yd*m[)uOBYLh9/Zdwp{ir&quot;
+       secure_auth_salt: &quot;&amp;lt;tXa0,1PN;4}]VkkY|-[B$`AGi]KT{z5H:F/0EtFCBJ,KE/j%(5[.$pZ&amp;lt;1WP8y&amp;lt;I&quot;
+       logged_in_salt: &quot;lT*P7xsVeh=f}u9?b#F&amp;gt;4h8dY?]?&amp;gt;t{5cXby=jziz:1!o,gGO#z*lIw|[#y%,/SN&quot;
+       nonce_salt: &quot;BZqsYn4aC}?z@`HSi22n]z$qw&amp;gt;?2Y^$&amp;gt;M:PZ1eMHj*ucI)rnYi1jKld3):n/|1(5&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note that you can automatically generate YAML-formatted encryption keys using &lt;a href=&quot;https://roots.io/salts.html&quot;&gt;the Roots salt generator&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Encrypt sensitive data with Ansible Vault.&lt;/h3&gt;
&lt;p&gt;Since it&apos;s &lt;em&gt;always&lt;/em&gt; a bad idea to commit plain text credentials in a repo, we&apos;re going to use &lt;a href=&quot;http://docs.ansible.com/ansible/playbooks_vault.html&quot;&gt;Ansible&apos;s built-in encryption&lt;/a&gt; to keep passwords and other sensitive data safe.&lt;/p&gt;
&lt;p&gt;To do this, we create a password that will only be stored on our computer, which Ansible can use to decrypt the files. Trellis is already configured to ignore the password, so someone would need physical access to our computer to get the credentials.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; I am &lt;em&gt;not&lt;/em&gt; a security expert. The Trellis team still recommends only committing Trellis configuration to private repositories, but &lt;code&gt;ansible-vault&lt;/code&gt; is specifically designed to allow keeping sensitive data in source control — unless you&apos;ve got numerous, well-funded, highly-motivated enemies, I&apos;m fairly sure you&apos;ll be alright with encrypted files in a public repo. (If I&apos;m wrong, please &lt;a href=&quot;https://github.com/jlengstorf/roots.code.lengstorf.com/issues&quot;&gt;create an issue and explain&lt;/a&gt; so I can update this article — and my worldview.)&lt;/p&gt;&lt;/aside&gt;
&lt;h4&gt;Create a password for encrypting and decrypting files.&lt;/h4&gt;
&lt;p&gt;First, create a new file at &lt;code&gt;trellis/.vault_pass&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Make sure we’re in the `trellis/` directory
pwd
# Output =&amp;gt; /Users/jlengstorf/dev/code.lengstorf.com/projects/learn-trellis/trellis

# Create a new file called `.vault_pass`
touch .vault_pass
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Open &lt;code&gt;trellis/.vault_pass&lt;/code&gt; for editing, then add a strong password:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Z+6Cm&amp;gt;TaofG=[379sED6
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Save and close this file.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Heads up!&lt;/strong&gt; Make sure you use your own password!&lt;/p&gt;&lt;/aside&gt;
&lt;h4&gt;Update the Ansible config to use the vault password.&lt;/h4&gt;
&lt;p&gt;Open &lt;code&gt;trellis/ansible.cfg&lt;/code&gt; for editing and add the highlighted line:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  [defaults]
  callback_plugins = ~/.ansible/plugins/callback_plugins/:/usr/share/ansible_plugins/callback_plugins:lib/trellis/plugins/callback
  stdout_callback = output
  filter_plugins = ~/.ansible/plugins/filter_plugins/:/usr/share/ansible_plugins/filter_plugins:lib/trellis/plugins/filter
  force_color = True
  force_handlers = True
  inventory = hosts
  nocows = 1
  roles_path = vendor/roles
  vars_plugins = ~/.ansible/plugins/vars_plugins/:/usr/share/ansible_plugins/vars_plugins:lib/trellis/plugins/vars
+ vault_password_file = .vault_pass

  [ssh_connection]
  ssh_args = -o ForwardAgent=yes -o ControlMaster=auto -o ControlPersist=60s
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This tells Ansible where to look for the vault password, rather than prompting for it.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;IMPORTANT:&lt;/strong&gt; DO NOT commit &lt;code&gt;.vault_pass&lt;/code&gt; into source control, or the added security is worthless — anyone with &lt;code&gt;.vault_pass&lt;/code&gt; can decrypt the Ansible files. Trellis already includes &lt;code&gt;.vault_pass&lt;/code&gt; in its &lt;code&gt;.gitignore&lt;/code&gt;, so this shouldn&apos;t be an issue.&lt;/p&gt;&lt;/aside&gt;
&lt;h4&gt;Use &lt;code&gt;ansible-vault&lt;/code&gt; to encrypt the files.&lt;/h4&gt;
&lt;p&gt;To actually encrypt the files, run the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ansible-vault encrypt group_vars/production/vault.yml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The output is a garbled mass of shit. This is a good thing — it means it&apos;s encrypted. Here&apos;s a snippet of what the encrypted file looks like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ANSIBLE_VAULT;1.1;AES256
62343434643862366430393366333661306366363937623561323637363033353366636134336230
3765633530336234393636306130346434333239636532650a336235356564303133303562626462
35363437383263653830313766646463646164303338626666366130396161383930373963613066
3366313862333134620a356563656432306335636331633063653163626638306532666335306239
...
&lt;/code&gt;&lt;/pre&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; To edit the file, you can use &lt;code&gt;ansible-vault edit group_vars/production/vault.yml&lt;/code&gt;, which allows you to make changes in the console using your default editor, or &lt;code&gt;ansible-vault decrypt group_vars/production/vault.yml&lt;/code&gt;, which returns the file to its pre-encrypted state for editing wherever you want. &lt;strong&gt;Don&apos;t forget to re-encrypt the file after editing.&lt;/strong&gt;&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;Part VI: Provision the Production Server&lt;/h2&gt;
&lt;p&gt;Now that the proper configuration is in place, we need to let Trellis &lt;em&gt;provision&lt;/em&gt; — or configure — the production server. Before we can do that, we need to update a few more configuration options, then make sure our source code is in a public repo and our domain name points to the production server.&lt;/p&gt;
&lt;h3&gt;Set up DNS so Let&apos;s Encrypt can run properly.&lt;/h3&gt;
&lt;p&gt;Since Let&apos;s Encrypt needs to verify the domain name in order to create an SSL certificate, we need to make sure the domain name points to the server &lt;em&gt;before&lt;/em&gt; we try to provision it. Otherwise, we&apos;ll get an error during the SSL step.&lt;/p&gt;
&lt;p&gt;In your DNS manager of choice (typically the site you bought the domain name through, e.g. Namecheap or GoDaddy), &lt;strong&gt;update the A record for your domain to point to your DigitalOcean droplet&apos;s IP address.&lt;/strong&gt;&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;/images/learn-roots-wordpress-03.jpg&quot; alt=&quot;The DNS record that points the subdomain to the Digital Ocean droplet.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;TIP:&lt;/strong&gt; You can also &lt;a href=&quot;https://www.digitalocean.com/community/tutorials/how-to-set-up-a-host-name-with-digitalocean&quot;&gt;configure your domain name using DigitalOcean&lt;/a&gt; if you prefer.&lt;/p&gt;&lt;/aside&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; Test that your domain is working by typing &lt;code&gt;ping &amp;lt;YOUR_DOMAIN&amp;gt;&lt;/code&gt; in the console. The IP address in the output (e.g. &lt;code&gt;PING code.lengstorf.com (104.18.35.89): 56 data bytes&lt;/code&gt;) should match your droplet.&lt;/p&gt;&lt;/aside&gt;
&lt;h3&gt;Use your GitHub keys for deployment.&lt;/h3&gt;
&lt;p&gt;To make sure cloning the repo on the production server goes smoothly, Ansible needs to know about your GitHub account&apos;s public keys.&lt;/p&gt;
&lt;p&gt;Open &lt;code&gt;trellis/group_vars/all/users.yml&lt;/code&gt; for editing, uncomment the GitHub URLs, and edit them to reflect your GitHub username:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Documentation: https://roots.io/trellis/docs/ssh-keys/
admin_user: admin

  # Also define &apos;vault_users&apos; (`group_vars/staging/vault.yml`, `group_vars/production/vault.yml`)
  users:
    - name: &quot;{{ web_user }}&quot;
      groups:
        - &quot;{{ web_group }}&quot;
      keys:
        - &quot;{{ lookup(&apos;file&apos;, &apos;~/.ssh/id_rsa.pub&apos;) }}&quot;
+       # IMPORTANT: Add YOUR GitHub username here.
+       - https://github.com/jlengstorf.keys
    - name: &quot;{{ admin_user }}&quot;
      groups:
        - sudo
      keys:
        - &quot;{{ lookup(&apos;file&apos;, &apos;~/.ssh/id_rsa.pub&apos;) }}&quot;
+       # IMPORTANT: Add YOUR GitHub username here.
+       - https://github.com/jlengstorf.keys

  web_user: web
  web_group: www-data
  web_sudoers:
    - &quot;/usr/sbin/service php7.0-fpm *&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Disable root login for better security.&lt;/h3&gt;
&lt;p&gt;This step is optional, but unless you have a &lt;em&gt;really&lt;/em&gt; good reason to keep the root user enabled, it&apos;s a good idea to disable it. The root user can wreak havoc on a server, so we can sleep better knowing that it&apos;s disabled and can&apos;t hurt anyone anymore.&lt;/p&gt;
&lt;p&gt;Open &lt;code&gt;trellis/group_vars/all/security.yml&lt;/code&gt; for editing and change the highlighted line:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ferm_input_list:
    - type: dport_accept
      dport: [http, https]
      filename: nginx_accept
    - type: dport_accept
      dport: [ssh]
      saddr: &quot;{{ ip_whitelist }}&quot;
    - type: dport_limit
      dport: [ssh]
      seconds: 300
      hits: 20

  # Documentation: https://roots.io/trellis/docs/security/
  # If sshd_permit_root_login: false, admin_user must be in &apos;users&apos; (`group_vars/all/users.yml`) with sudo group
  # and in &apos;vault_users&apos; (`group_vars/staging/vault.yml`, `group_vars/production/vault.yml`)
+ sshd_permit_root_login: false
  sshd_password_authentication: false
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Commit the site&apos;s code to GitHub.&lt;/h3&gt;
&lt;p&gt;For this tutorial, I created a repo called &lt;a href=&quot;https://github.com/jlengstorf/roots.code.lengstorf.com&quot;&gt;roots.code.lengstorf.com&lt;/a&gt;. You&apos;ll want to create your own for this step.&lt;/p&gt;
&lt;p&gt;To get our site&apos;s code up to that repo, run the following commands:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Move to the project root
cd ..
pwd
# Output =&amp;gt; /Users/jlengstorf/dev/code.lengstorf.com/projects/learn-trellis

# Initialize a Git repository.
git init

# Add all the files to the repo.
git add -A

# Commit the files.
git commit -m &apos;Initial commit.&apos;

# Add the GitHub repo as a remote repository.
# IMPORTANT: Replace this with YOUR GitHub repository!
git remote add origin git@github.com:jlengstorf/roots.code.lengstorf.com.git

# Push the `master` branch to GitHub and set it up for tracking.
git push --set-upstream origin master
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Run the server provisioning script.&lt;/h3&gt;
&lt;p&gt;With the repo ready, the DNS configured, and some security measures in place, we&apos;re ready to provision the server.&lt;/p&gt;
&lt;p&gt;Provisioning means getting the server ready: Ansible will run through a series of commands that download, install, and configure our server based on the information we&apos;ve provided in the configuration files.&lt;/p&gt;
&lt;p&gt;To make it happen, all we need to do is run the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Move into the Trellis directory.
cd trellis/

# Provision the server using the `production` configuration options.
ansible-playbook server.yml -e env=production
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This takes about 5–10 minutes. The output in the command line walks through everything that&apos;s being installed and configured, so if you&apos;re curious you can follow along and see what the Roots maintainers consider best practices for server configuration.&lt;/p&gt;
&lt;p&gt;Or, we can walk away and let the robots do our bidding.&lt;/p&gt;
&lt;p&gt;When it&apos;s all done, we&apos;ll see a &quot;play recap&quot; from Ansible that looks something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PLAY RECAP **\*\***\*\*\*\***\*\***\*\***\*\***\*\*\*\***\*\***\***\*\***\*\*\*\***\*\***\*\***\*\***\*\*\*\***\*\***
162.243.171.188 : ok=130 changed=93 unreachable=0 failed=0
localhost : ok=0 changed=0 unreachable=0 failed=0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With 0 failures, we&apos;re ready to deploy!&lt;/p&gt;
&lt;h2&gt;Part VII: Deploy the Site to Production&lt;/h2&gt;
&lt;p&gt;Our last step — deploying our WordPress site to the DigitalOcean droplet — is &lt;em&gt;really fucking easy&lt;/em&gt; — even compared to the rest of the steps in this walkthrough. We&apos;ve already configured everything. We&apos;ve already committed all our site&apos;s files to GitHub. We&apos;ve already provisioned a server.&lt;/p&gt;
&lt;p&gt;Now we just need Ansible to copy the site files and fire it up.&lt;/p&gt;
&lt;p&gt;Enter the following to make it happen:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Make sure we&apos;re in the `trellis/` directory.
pwd
# Output =&amp;gt; /Users/jlengstorf/dev/code.lengstorf.com/projects/learn-trellis/trellis

# Deploy the `roots.code.lengstorf.com` site using `production` options.
# NOTE: Replace this site key with YOUR site key.
./bin/deploy.sh production roots.code.lengstorf.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This takes a minute or two, and ends with a &quot;play recap&quot;, just like provisioning. It should look something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PLAY RECAP **\*\***\*\*\*\***\*\***\*\***\*\***\*\*\*\***\*\***\***\*\***\*\*\*\***\*\***\*\***\*\***\*\*\*\***\*\***
162.243.171.188 : ok=27 changed=12 unreachable=0 failed=0
localhost : ok=0 changed=0 unreachable=0 failed=0
&lt;/code&gt;&lt;/pre&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; At the time of recording, the deploy script was at the root of the Trellis directory. The command has been updated in this article, but not in the video.&lt;/p&gt;&lt;/aside&gt;
&lt;h3&gt;Check the Live Site&lt;/h3&gt;
&lt;p&gt;And that&apos;s it. &lt;strong&gt;We&apos;ve successfully deployed an SSL-secured (for free), painlessly-source-controlled WordPress site on a live domain name, all for about 20 minutes and $5/month.&lt;/strong&gt;&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;/images/learn-roots-wordpress-04.jpg&quot; alt=&quot;After deploying, the live site is accessible.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;h2&gt;BONUS: Synchronize Databases Using WP Sync DB&lt;/h2&gt;
&lt;p&gt;Trellis only manages files, so you&apos;ll notice that the new site doesn&apos;t match up with the development site. This isn&apos;t a big deal when you first start out, but as the production site starts to grow and the content builds up, it&apos;s a pain in the ass to pull a copy of the database so you can develop using real data.&lt;/p&gt;
&lt;p&gt;Fortunately, the free plugin &lt;a href=&quot;https://github.com/wp-sync-db/wp-sync-db&quot;&gt;WP Sync DB&lt;/a&gt; makes synchronizing databases really painless. So let&apos;s install that and try it out.&lt;/p&gt;
&lt;h3&gt;Install WP Sync DB on the site.&lt;/h3&gt;
&lt;p&gt;Since we&apos;re using Bedrock, installing the plugin is as simple as telling Composer we want to use it.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Make sure we’re in the site/ directory.
cd ../site
pwd
# Output =&amp;gt; /Users/dev/code.lengstorf.com/projects/learn-trellis/site

# Use Composer to install WP Sync DB
composer require wp-sync-db/wp-sync-db:dev-master@dev

# Use Composer to install the media attachments plugin for WP Sync DB
composer require wp-sync-db/wp-sync-db-media-files:dev-master
&lt;/code&gt;&lt;/pre&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; The &lt;code&gt;:dev-master@dev&lt;/code&gt; is required because WP Sync DB doesn&apos;t specify a stable version, which causes Composer to complain if we&apos;re not &lt;em&gt;very specific&lt;/em&gt; about getting the development branch.&lt;/p&gt;&lt;/aside&gt;
&lt;h3&gt;Commit and redeploy to production.&lt;/h3&gt;
&lt;p&gt;After WP Sync DB is installed, we need to get it installed on the production server. This is a good example of how easy it is to deploy changes using Trellis: if you run &lt;code&gt;git status&lt;/code&gt;, you&apos;ll see that &lt;code&gt;composer.json&lt;/code&gt; and &lt;code&gt;composer.lock&lt;/code&gt; have been updated; simply commit and push those files, then redeploy to production.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Commit the changes and push them.
git commit -Am &apos;feat(wp-sync-db): installed WP Sync DB&apos;
git push

# Deploy the changes to production.
cd ../trellis
./bin/deploy.sh production roots.code.lengstorf.com
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Activate WP Sync DB on the production site and get the API key.&lt;/h3&gt;
&lt;p&gt;Once the deploy is complete, go to the production site&apos;s WordPress dashboard (e.g. &lt;code&gt;https://roots.code.lengstorf.com/wp/wp-admin/&lt;/code&gt;), log in, then click &quot;Plugins&quot; in the left-hand menu.&lt;/p&gt;
&lt;p&gt;Activate both the &quot;WP Sync DB&quot; and &quot;WP Sync DB Media Files&quot; plugins, then navigate to &quot;Tools&quot;, then &quot;Migrate DB&quot; in the left-hand menu.&lt;/p&gt;
&lt;p&gt;Click the &quot;Settings&quot; tab, then do the following:&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;/images/learn-roots-wordpress-05.jpg&quot; alt=&quot;The WP Sync DB plugin on the live site.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;ol&gt;
&lt;li&gt;Check the &quot;Accept &lt;strong&gt;push&lt;/strong&gt; requests...&quot; option.&lt;/li&gt;
&lt;li&gt;Copy the link in the &quot;Connection Info&quot; box.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Now the plugin is able to receive a database update from an external site (our development site, in this example).&lt;/p&gt;
&lt;h3&gt;Activate WP Sync DB on the development site and configure the sync.&lt;/h3&gt;
&lt;p&gt;On your development site, head to the &quot;Plugins&quot; page on your WordPress dashboard to activate the WP Sync DB and WP Sync DB Media Files plugins, then navigate to Tools &amp;gt; Migrate DB.&lt;/p&gt;
&lt;p&gt;On the &quot;Migrate&quot; tab, update the settings as shown:&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;/images/learn-roots-wordpress-06.jpg&quot; alt=&quot;Settings for the WP Sync DB plugin on the development site.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;ol&gt;
&lt;li&gt;Choose the &quot;Push&quot; option.&lt;/li&gt;
&lt;li&gt;Paste the production site&apos;s connection info into the text area that appears (what we copied in the previous step).&lt;/li&gt;
&lt;li&gt;Under Advanced options:
&lt;ul&gt;
&lt;li&gt;Uncheck the &quot;Replace GUIDs&quot; option.&lt;/li&gt;
&lt;li&gt;Check the &quot;exclude spam comments&quot; option.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;[OPTIONAL] Check the &quot;Backup the remote database before replacing it&quot; option. This is probably a good idea if the production site has real data on it.&lt;/li&gt;
&lt;li&gt;Check the &quot;Media Files&quot; option.&lt;/li&gt;
&lt;li&gt;[OPTIONAL] Check the &quot;Save Migration Profile&quot; option and create a name so you can quickly push changes to production.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Once all these settings have been updated, click the &quot;Migrate DB &amp;amp; Save&quot; button.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;/images/learn-roots-wordpress-07.jpg&quot; alt=&quot;WP Sync DB in progress.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;/images/learn-roots-wordpress-08.jpg&quot; alt=&quot;WP Sync DB after a successful sync.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;Once this is done, you can reload the production site and see that the database from the development site is now live on production.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;IMPORTANT:&lt;/strong&gt; The default database migration moves everything, including the &lt;code&gt;wp_users&lt;/code&gt; table. This means that the username and password of the WordPress users are also migrated. So after migrating from development to production, the development username and password now logs into production as well. This is extremely important if your development credentials are available unencrypted in Trellis (you can solve this with &lt;code&gt;ansible-vault&lt;/code&gt;).&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;Further Reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;http://davekiss.com/develop-wordpress-sites-like-a-goddamn-champion/&quot;&gt;Develop WordPress Sites Like a Goddamn Champion&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://roots.io/trellis/docs/installing-trellis/&quot;&gt;Trellis documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Bundle Stylesheets and Add LiveReload With Rollup
</title><link>https://codetv.dev/blog/learn-rollup-css/</link><guid isPermaLink="true">https://codetv.dev/blog/learn-rollup-css/</guid><description>Learn how to use the JavaScript bundler Rollup to process stylesheets using PostCSS and rebuild &amp; reload files when changes are made in this tutorial.
</description><pubDate>Thu, 25 Aug 2016 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://youtu.be/hJ2RVXEIgkk&quot;&gt;Watch on YouTube&lt;/a&gt;&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Heads up:&lt;/strong&gt; this post was written in 2016, and some of the tools and prices may have changed. The code &lt;em&gt;should&lt;/em&gt; still work, but you may want to look for a more up-to-date tutorial.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;In the first part of this series, we walked through the process of &lt;a href=&quot;/code/learn-rollup-js/&quot;&gt;setting up Rollup as a front-end build tool for JavaScript&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This article covers parts two and three.&lt;/p&gt;
&lt;p&gt;First, we’ll continue working on that project in &lt;a href=&quot;#stylesheets&quot;&gt;Part II&lt;/a&gt; to add support for stylesheet processing through &lt;a href=&quot;http://rollupjs.org/&quot;&gt;Rollup&lt;/a&gt;, using &lt;a href=&quot;https://github.com/postcss/postcss&quot;&gt;PostCSS&lt;/a&gt; to run some transforms and allow us to use syntactic sugar like simpler variable syntax and nested rules.&lt;/p&gt;
&lt;p&gt;After that, we’ll wrap up with &lt;a href=&quot;#livereload&quot;&gt;Part III&lt;/a&gt;, where we’ll add file watching and live reloading to the project so we don’t have to manually regenerate the bundle whenever files are changed.&lt;/p&gt;
&lt;h2&gt;Prerequisites&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;We’ll be continuing with the project we started last week, so if you haven’t gone through that part yet, it’s &lt;a href=&quot;/code/learn-rollup-js&quot;&gt;probably worth a look&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; If you don’t have a copy of the project, you can clone the project as it stands at the end of Part I using this command: &lt;code&gt;git clone -b part-2-starter --single-branch https://github.com/jlengstorf/learn-rollup.git&lt;/code&gt;&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;Series Navigation&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/blog/learn-rollup-js&quot;&gt;Part I: How to Use Rollup to Process and Bundle JavaScript Files&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/learn-rollup-css&quot;&gt;Part II: How to Use Rollup to Process and Bundle Stylesheets&lt;/a&gt; &amp;lt;— you are here&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Part II: How to Use Rollup.js for Your Next Project: PostCSS&lt;/h2&gt;
&lt;p&gt;Another part of Rollup that’s nice, depending on how your project is set up, is that you can easily process CSS and inject it into the &lt;code&gt;head&lt;/code&gt; of the document.&lt;/p&gt;
&lt;p&gt;On the plus side, this keeps all your build steps in one place, which keeps the complexity down in our development process — that’s a big help, especially if we’re working on a team.&lt;/p&gt;
&lt;p&gt;But on the down side, we’re making our stylesheets rely on JavaScript, and creating a brief flicker of unstyled HTML before the styles are injected. So this approach may not make sense for some projects, and should be weighed against approaches like using PostCSS separately.&lt;/p&gt;
&lt;p&gt;Since this article is about Rollup, though: fuck it. Let’s use Rollup!&lt;/p&gt;
&lt;h3&gt;Step 0: Load the stylesheet in &lt;code&gt;main.js&lt;/code&gt;.&lt;/h3&gt;
&lt;p&gt;This is a little funky if you’ve never used a build tool before, but stick with me. To use our styles in the document, we’re not going to use a &lt;code&gt;&amp;lt;link&amp;gt;&lt;/code&gt; tag like we normally would; instead, we’re going to use an &lt;code&gt;import&lt;/code&gt; statement in &lt;code&gt;main.min.js&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Right at the top of &lt;code&gt;src/scripts/main.js&lt;/code&gt;, load the stylesheet:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;+ // Import styles (automatically injected into &amp;lt;head&amp;gt;).
+ import &apos;../styles/main.css&apos;;

  // Import a couple modules for testing.
  import { sayHelloTo } from &apos;./modules/mod1&apos;;
  import addArray from &apos;./modules/mod2&apos;;

  // Import a logger for easier debugging.
  import debug from &apos;debug&apos;;
  const log = debug(&apos;app:log&apos;);

  // The logger should only be disabled if we’re not in production.
  if (ENV !== &apos;production&apos;) {

    // Enable the logger.
    debug.enable(&apos;*&apos;);
    log(&apos;Logging is enabled!&apos;);
  } else {
    debug.disable();
  }

  // Run some functions from our imported modules.
  const result1 = sayHelloTo(&apos;Jason&apos;);
  const result2 = addArray([1, 2, 3, 4]);

  // Print the results on the page.
  const printTarget = document.getElementsByClassName(&apos;debug__output&apos;)[0];

  printTarget.innerText = `sayHelloTo(&apos;Jason&apos;) =&amp;gt; ${result1}\n\n`;
  printTarget.innerText += `addArray([1, 2, 3, 4]) =&amp;gt; ${result2}`;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 1: Install PostCSS as a Rollup plugin.&lt;/h3&gt;
&lt;p&gt;The first thing we need is Rollup’s PostCSS plugin, so install that with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install --save-dev rollup-plugin-postcss
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 2: Update &lt;code&gt;rollup.config.js&lt;/code&gt;.&lt;/h3&gt;
&lt;p&gt;Next, let’s add the plugin to our &lt;code&gt;rollup.config.js&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  // Rollup plugins
  import babel from &apos;rollup-plugin-babel&apos;;
  import eslint from &apos;rollup-plugin-eslint&apos;;
  import resolve from &apos;rollup-plugin-node-resolve&apos;;
  import commonjs from &apos;rollup-plugin-commonjs&apos;;
  import replace from &apos;rollup-plugin-replace&apos;;
  import uglify from &apos;rollup-plugin-uglify&apos;;
+ import postcss from &apos;rollup-plugin-postcss&apos;;

  export default {
    entry: &apos;src/scripts/main.js&apos;,
    dest: &apos;build/js/main.min.js&apos;,
    format: &apos;iife&apos;,
    sourceMap: &apos;inline&apos;,
    plugins: [
+     postcss({
+       extensions: [ &apos;.css&apos; ],
+     }),
      resolve({
        jsnext: true,
        main: true,
        browser: true,
      }),
      commonjs(),
      eslint({
        exclude: [
          &apos;src/styles/**&apos;,
        ]
      }),
      babel({
        exclude: &apos;node_modules/**&apos;,
      }),
      replace({
        ENV: JSON.stringify(process.env.NODE_ENV || &apos;development&apos;),
      }),
      (process.env.NODE_ENV === &apos;production&apos; &amp;amp;&amp;amp; uglify()),
    ],
  };
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Take a look at the generated bundle.&lt;/h4&gt;
&lt;p&gt;Now that we’re able to process the stylesheet, we can regenerate the bundle and see how this all works.&lt;/p&gt;
&lt;p&gt;Run &lt;code&gt;./node_modules/.bin/rollup -c&lt;/code&gt;, then look at the generated bundle at &lt;code&gt;build/js/main.min.js&lt;/code&gt;, right near the top. You’ll see a new function called &lt;code&gt;__$styleInject()&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function __$styleInject(css) {
  css = css || &apos;&apos;;
  var head = document.head || document.getElementsByTagName(&apos;head&apos;)[0];
  var style = document.createElement(&apos;style&apos;);
  style.type = &apos;text/css&apos;;
  if (style.styleSheet) {
    style.styleSheet.cssText = css;
  } else {
    style.appendChild(document.createTextNode(css));
  }
  head.appendChild(style);
}
__$styleInject(&apos;/* Styles omitted for brevity... */&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In a nutshell, this function creates a &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; element, sets the stylesheet as its content, and appends that to the document’s &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Just below the function declaration, we can see that it’s called with the styles output by PostCSS. Pretty snazzy, right?&lt;/p&gt;
&lt;p&gt;Except right now, those styles aren’t actually being processed; PostCSS is just passing our stylesheet straight across. So let’s add the PostCSS plugins we need to make our stylesheet work in our target browsers.&lt;/p&gt;
&lt;h3&gt;Step 3: Install the necessary PostCSS plugins.&lt;/h3&gt;
&lt;p&gt;I love PostCSS. I started out in the LESS camp, found myself more or less forced into the Sass camp when everyone abandoned LESS, and then was extremely happy to learn that PostCSS existed.&lt;/p&gt;
&lt;p&gt;I like it because it gives me access to the parts of LESS and Sass that I liked — nesting, simple variables — and doesn’t open me up to the parts of LESS and Sass that I think were tempting and dangerous,[^dangerous] like logical operators.&lt;/p&gt;
&lt;p&gt;[^dangerous]: I say &quot;dangerous&quot; because the logical features of LESS/Sass always felt a little flimsy to me, and in discussions with people they were always a sticking point. That was a red flag: using them introduced a kind of brittleness in a stylesheet, and while one person may be perfectly clear on what’s going on, the rest of the team may feel like mixins are voodoo pixie magic — and that’s never good for maintainability.&lt;/p&gt;
&lt;p&gt;One of the things that I like most about it is the use of plugins, rather than an overarching language construct called &quot;PostCSS&quot;. We can choose only the features we’ll actually use — and more importantly, we can leave out the features we &lt;em&gt;don’t&lt;/em&gt; want used.&lt;/p&gt;
&lt;p&gt;So in our project, we’ll only be using four plugins — two for syntactic sugar, one to support new CSS features in older browsers, and one to compress and minify the resulting stylesheet:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/postcss/postcss-simple-vars&quot;&gt;&lt;code&gt;postcss-simple-vars&lt;/code&gt;&lt;/a&gt; — This allows the use of Sass-style variables (e.g. &lt;code&gt;$myColor: #fff;&lt;/code&gt;, used as &lt;code&gt;color: $myColor;&lt;/code&gt;) instead of the more verbose &lt;a href=&quot;https://www.w3.org/TR/css-variables/&quot;&gt;CSS syntax&lt;/a&gt; (e.g. &lt;code&gt;:root { --myColor: #fff; }&lt;/code&gt;, used as &lt;code&gt;color: var(--myColor);&lt;/code&gt;). This is purely preferential; I like the shorter syntax better.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/postcss/postcss-nested&quot;&gt;&lt;code&gt;postcss-nested&lt;/code&gt;&lt;/a&gt; — This allows rules to be nested. I actually don’t use this to nest rules; I use it as a shortcut for creating &lt;a href=&quot;http://getbem.com/naming/&quot;&gt;BEM-friendly selectors&lt;/a&gt; and grouping my blocks, elements, and modifiers into single CSS blocks.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://cssnext.io/&quot;&gt;&lt;code&gt;postcss-cssnext&lt;/code&gt;&lt;/a&gt; — This is a bundle of plugins that enables the most current CSS syntax (according to the &lt;a href=&quot;https://www.w3.org/Style/CSS/current-work&quot;&gt;latest CSS specs&lt;/a&gt;), transpiling it to work, even in older browsers that don’t support the new features.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://cssnano.co/&quot;&gt;&lt;code&gt;cssnano&lt;/code&gt;&lt;/a&gt; — This compresses and minifies the CSS output. This is to CSS what &lt;a href=&quot;https://github.com/mishoo/UglifyJS2&quot;&gt;UglifyJS&lt;/a&gt; is to JavaScript.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To install these plugins, use this command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install --save-dev postcss-simple-vars postcss-nested postcss-cssnext cssnano
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 4: Update &lt;code&gt;rollup.config.js&lt;/code&gt;.&lt;/h3&gt;
&lt;p&gt;Next, let’s include our PostCSS plugins in &lt;code&gt;rollup.config.js&lt;/code&gt; by adding a &lt;code&gt;plugins&lt;/code&gt; property to the &lt;code&gt;postcss&lt;/code&gt; configuration object:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  // Rollup plugins
  import babel from &apos;rollup-plugin-babel&apos;;
  import eslint from &apos;rollup-plugin-eslint&apos;;
  import resolve from &apos;rollup-plugin-node-resolve&apos;;
  import commonjs from &apos;rollup-plugin-commonjs&apos;;
  import replace from &apos;rollup-plugin-replace&apos;;
  import uglify from &apos;rollup-plugin-uglify&apos;;
  import postcss from &apos;rollup-plugin-postcss&apos;;

+ // PostCSS plugins
+ import simplevars from &apos;postcss-simple-vars&apos;;
+ import nested from &apos;postcss-nested&apos;;
+ import cssnext from &apos;postcss-cssnext&apos;;
+ import cssnano from &apos;cssnano&apos;;

  export default {
    entry: &apos;src/scripts/main.js&apos;,
    dest: &apos;build/js/main.min.js&apos;,
    format: &apos;iife&apos;,
    sourceMap: &apos;inline&apos;,
    plugins: [
      postcss({
+       plugins: [
+         simplevars(),
+         nested(),
+         cssnext({ warnForDuplicates: false, }),
+         cssnano(),
+       ],
        extensions: [ &apos;.css&apos; ],
      }),
      resolve({
        jsnext: true,
        main: true,
        browser: true,
      }),
      commonjs(),
      eslint({
        exclude: [
          &apos;src/styles/**&apos;,
        ]
      }),
      babel({
        exclude: &apos;node_modules/**&apos;,
      }),
      replace({
        ENV: JSON.stringify(process.env.NODE_ENV || &apos;development&apos;),
      }),
      (process.env.NODE_ENV === &apos;production&apos; &amp;amp;&amp;amp; uglify()),
    ],
  };
&lt;/code&gt;&lt;/pre&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; We pass &lt;code&gt;{ warnForDuplicates: false }&lt;/code&gt; to &lt;code&gt;cssnext()&lt;/code&gt; because both it and &lt;code&gt;cssnano()&lt;/code&gt; use &lt;a href=&quot;https://github.com/postcss/autoprefixer&quot;&gt;Autoprefixer&lt;/a&gt;, which triggers a warning. Rather than wrestling with the config, we’ll just know that it’s being run twice (which is harmless in this case) and silence the warning.&lt;/p&gt;&lt;/aside&gt;
&lt;h3&gt;Check the output in the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;.&lt;/h3&gt;
&lt;p&gt;With the plugins installed, we can rebuild our bundle (&lt;code&gt;./node_modules/.bin/rollup -c&lt;/code&gt;) and open &lt;code&gt;build/index.html&lt;/code&gt; in our browser. We’ll see that the page is now styled, and if we inspect the document we can see the stylesheet was injected in the head, compressed and minified and with all the vendor prefixes and other goodies we expected from PostCSS:&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;/images/learn-rollup-06.jpg&quot; alt=&quot;Injected stylesheet shown in the devtools inspector.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;Great! So now we have a pretty solid build process: our JavaScript is bundled, unused code is removed, and the output is compressed and minified, and our stylesheets are processed by PostCSS and injected into the head.&lt;/p&gt;
&lt;p&gt;However, it’s still kind of a pain in the ass to have to manually rebuild the bundle every time we make a change. So in the next part, we’ll have Rollup watch our files for changes and reload the browser whenever a file is changed.&lt;/p&gt;
&lt;h2&gt;Part III: How to Use Rollup.js for Your Next Project: LiveReload&lt;/h2&gt;
&lt;p&gt;At this point, our project is successfully bundling JavaScript and stylesheets, but it’s still a manual process. And since every manual step in a process is a higher risk for failure than an automated step — and because it’s a pain in the ass to have to run &lt;code&gt;./node_modules/.bin/rollup -c&lt;/code&gt; every time we change a file — we want to make rebuilding the bundle automatic.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; If you don’t have a copy of the project, you can clone the project as it stands at the end of Part II using this command: &lt;code&gt;git clone -b part-3-starter --single-branch https://github.com/jlengstorf/learn-rollup.git&lt;/code&gt;&lt;/p&gt;&lt;/aside&gt;
&lt;h3&gt;Step 0: Add a watch plugin to Rollup.&lt;/h3&gt;
&lt;p&gt;A common development tool when working with Node.js is a watcher. This may be familiar if you’ve worked with webpack, Grunt, Gulp, and other build tools in the past.&lt;/p&gt;
&lt;p&gt;A watcher is a process that runs while you work on a project, and when you change files in any of the folders it’s monitoring, it triggers an action. In the case of build tools, the most common action is to trigger a rebuild.&lt;/p&gt;
&lt;p&gt;In our project, we want to watch the &lt;code&gt;src/&lt;/code&gt; directory for any file changes, and when changes are detected we want to trigger a new bundle creation from Rollup.&lt;/p&gt;
&lt;p&gt;To do that, we’ll use the &lt;a href=&quot;https://github.com/rollup/rollup-watch&quot;&gt;&lt;code&gt;rollup-watch&lt;/code&gt;&lt;/a&gt; plugin, which is a little different from the other Rollup plugins we’ve used so far — but more on that in a bit. Let’s start by installing the plugin:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install --save-dev rollup-watch
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 1: Run Rollup with the &lt;code&gt;--watch&lt;/code&gt; flag.&lt;/h3&gt;
&lt;p&gt;The difference between &lt;code&gt;rollup-watch&lt;/code&gt; and other Rollup plugins is that we don’t have to make any changes to &lt;code&gt;rollup.config.js&lt;/code&gt; in order to use it.&lt;/p&gt;
&lt;p&gt;Instead, we add a flag to our terminal command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Run Rollup with the watch plugin enabled
./node_modules/.bin/rollup -c --watch
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After running the command, we see a different output in the console:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ ./node_modules/.bin/rollup -c --watch
checking rollup-watch version...
bundling...
bundled in 949ms. Watching for changes...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The process stays active, and it’s now watching for changes.&lt;/p&gt;
&lt;p&gt;So if we make a small change to &lt;code&gt;src/main.js&lt;/code&gt; — say adding a comment — as soon as we save it a new bundle is generated.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;/images/learn-rollup-07.gif&quot; alt=&quot;Watch mode catches a lint error.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;This is a big timesaver in development, but we can take it a step further. Right now we still have to refresh the browser to see the updated bundle — so let’s add a tool to refresh the browser &lt;em&gt;automatically&lt;/em&gt; when our bundle is updated.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;TIP:&lt;/strong&gt; To stop the watch process, press &lt;code&gt;control + C&lt;/code&gt; in the terminal window.&lt;/p&gt;&lt;/aside&gt;
&lt;h3&gt;Step 2: Install LiveReload to refresh the browser automatically.&lt;/h3&gt;
&lt;p&gt;A common tool for speeding up development is &lt;a href=&quot;https://www.npmjs.com/package/livereload&quot;&gt;LiveReload&lt;/a&gt;. This is a process that runs in the background and tells the browser to refresh whenever a file it’s watching is changed.&lt;/p&gt;
&lt;p&gt;To start, we need to install the plugin:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install --save-dev livereload
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 3: Inject the LiveReload script into the project.&lt;/h3&gt;
&lt;p&gt;Before LiveReload can work, we need to include a script in our page that talks to the LiveReload server.&lt;/p&gt;
&lt;p&gt;Since this is only required in development, we’re going to take advantage of our environment variables to only inject the script if we’re &lt;em&gt;not&lt;/em&gt; in &lt;code&gt;production&lt;/code&gt; mode.&lt;/p&gt;
&lt;p&gt;In &lt;code&gt;src/main.js&lt;/code&gt;, add the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  // Import styles (automatically injected into &amp;lt;head&amp;gt;).
  import &apos;../styles/main.css&apos;;

  // Import a couple modules for testing.
  import { sayHelloTo } from &apos;./modules/mod1&apos;;
  import addArray from &apos;./modules/mod2&apos;;

  // Import a logger for easier debugging.
  import debug from &apos;debug&apos;;
  const log = debug(&apos;app:log&apos;);

  // The logger should only be disabled if we’re not in production.
  if (ENV !== &apos;production&apos;) {

    // Enable the logger.
    debug.enable(&apos;*&apos;);
    log(&apos;Logging is enabled!&apos;);

+   // Enable LiveReload
+   document.write(
+     &apos;&amp;lt;script src=&quot;http://&apos; + (location.host || &apos;localhost&apos;).split(&apos;:&apos;)[0] +
+     &apos;:35729/livereload.js?snipver=1&quot;&amp;gt;&amp;lt;/&apos; + &apos;script&amp;gt;&apos;
+   );
  } else {
    debug.disable();
  }

  // Run some functions from our imported modules.
  const result1 = sayHelloTo(&apos;Jason&apos;);
  const result2 = addArray([1, 2, 3, 4]);

  // Print the results on the page.
  const printTarget = document.getElementsByClassName(&apos;debug__output&apos;)[0];

  printTarget.innerText = `sayHelloTo(&apos;Jason&apos;) =&amp;gt; ${result1}\n\n`;
  printTarget.innerText += `addArray([1, 2, 3, 4]) =&amp;gt; ${result2}`;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Save the file once you’ve made the changes, and now we’re ready to try it out.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; It’s not important to understand how the LiveReload server works, but the short version is that the command line process watches for file changes, then sends a message using websockets to the client-side script, which triggers a reload.&lt;/p&gt;&lt;/aside&gt;
&lt;h3&gt;Step 4: Run LiveReload.&lt;/h3&gt;
&lt;p&gt;With LiveReload installed and the script injected into our document, we can fire it up to watch our &lt;code&gt;build/&lt;/code&gt; directory:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;./node_modules/.bin/livereload &apos;build/&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; The reason we’re watching &lt;code&gt;build/&lt;/code&gt; is that we only need to rebuild when there’s a new bundle.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;This results in output similar to the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ ./node_modules/.bin/livereload &apos;build/&apos;
Starting LiveReload v0.5.0 for /Users/jlengstorf/dev/code.lengstorf.com/projects/learn-rollup/build on port 35729.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And if we open our browser to view &lt;code&gt;build/index.html&lt;/code&gt; — make sure to refresh the page &lt;em&gt;after&lt;/em&gt; starting LiveReload to ensure the socket connection is active — we can see that making a change to &lt;code&gt;build/index.html&lt;/code&gt; will automatically reload the browser and show us our changes:&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;/images/learn-rollup-08.gif&quot; alt=&quot;A change made to the source while live reload is running triggers an instant refresh of the built content.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;This is great, but we’re still a little stuck: right now, we can only get Rollup’s watch function &lt;em&gt;or&lt;/em&gt; LiveReload running unless we open multiple terminal sessions. That’s not ideal. In the next two steps, we’ll create a workaround for that.&lt;/p&gt;
&lt;h3&gt;Step 5: Use &lt;code&gt;package.json&lt;/code&gt; scripts to simplify startup.&lt;/h3&gt;
&lt;p&gt;So far in this tutorial, we’ve had to type the full path to the &lt;code&gt;rollup&lt;/code&gt; script, which — I’m sure you’ve noticed by now — is a pain in the ass.&lt;/p&gt;
&lt;p&gt;Because of this, and because the tool we’ll be using to run watch and LiveReload at the same time, we’re going to add both the &lt;code&gt;rollup&lt;/code&gt; command and the &lt;code&gt;livereload&lt;/code&gt; command as scripts in &lt;code&gt;package.json&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Open &lt;code&gt;package.json&lt;/code&gt; — it’s in the root of the &lt;code&gt;learn-rollup/&lt;/code&gt; project directory. Inside, you should see the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;name&quot;: &quot;learn-rollup&quot;,
  &quot;version&quot;: &quot;0.0.0&quot;,
  &quot;description&quot;: &quot;This is an example project to accompany a tutorial on using Rollup.&quot;,
  &quot;main&quot;: &quot;build/js/main.min.js&quot;,
  &quot;scripts&quot;: {
    &quot;test&quot;: &quot;echo \&quot;Error: no test specified\&quot; &amp;amp;&amp;amp; exit 1&quot;
  },
  &quot;repository&quot;: {
    &quot;type&quot;: &quot;git&quot;,
    &quot;url&quot;: &quot;git+ssh://git@github.com/jlengstorf/learn-rollup.git&quot;
  },
  &quot;author&quot;: &quot;Jason Lengstorf &amp;lt;jason@lengstorf.com&amp;gt; (@jlengstorf)&quot;,
  &quot;license&quot;: &quot;ISC&quot;,
  &quot;bugs&quot;: {
    &quot;url&quot;: &quot;https://github.com/jlengstorf/learn-rollup/issues&quot;
  },
  &quot;homepage&quot;: &quot;https://github.com/jlengstorf/learn-rollup#readme&quot;,
  &quot;devDependencies&quot;: {
    &quot;babel-preset-es2015-rollup&quot;: &quot;^1.2.0&quot;,
    &quot;cssnano&quot;: &quot;^3.7.4&quot;,
    &quot;livereload&quot;: &quot;^0.5.0&quot;,
    &quot;npm-run-all&quot;: &quot;^3.0.0&quot;,
    &quot;postcss-cssnext&quot;: &quot;^2.7.0&quot;,
    &quot;postcss-nested&quot;: &quot;^1.0.0&quot;,
    &quot;postcss-simple-vars&quot;: &quot;^3.0.0&quot;,
    &quot;rollup&quot;: &quot;^0.34.9&quot;,
    &quot;rollup-plugin-babel&quot;: &quot;^2.6.1&quot;,
    &quot;rollup-plugin-commonjs&quot;: &quot;^3.3.1&quot;,
    &quot;rollup-plugin-eslint&quot;: &quot;^2.0.2&quot;,
    &quot;rollup-plugin-node-resolve&quot;: &quot;^2.0.0&quot;,
    &quot;rollup-plugin-postcss&quot;: &quot;^0.1.1&quot;,
    &quot;rollup-plugin-replace&quot;: &quot;^1.1.1&quot;,
    &quot;rollup-plugin-uglify&quot;: &quot;^1.0.1&quot;,
    &quot;rollup-watch&quot;: &quot;^2.5.0&quot;
  },
  &quot;dependencies&quot;: {
    &quot;debug&quot;: &quot;^2.2.0&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;See that &lt;code&gt;scripts&lt;/code&gt; property? We’re going to add two new ones:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A script to run our Rollup bundle generation command&lt;/li&gt;
&lt;li&gt;A script to turn on LiveReload&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Add the following to &lt;code&gt;package.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  {
    &quot;name&quot;: &quot;learn-rollup&quot;,
    &quot;version&quot;: &quot;0.0.0&quot;,
    &quot;description&quot;: &quot;This is an example project to accompany a tutorial on using Rollup.&quot;,
    &quot;main&quot;: &quot;build/js/main.min.js&quot;,
    &quot;scripts&quot;: {
+     &quot;dev&quot;: &quot;rollup -c --watch&quot;,
+     &quot;reload&quot;: &quot;livereload &apos;build/&apos;&quot;,
      &quot;test&quot;: &quot;echo \&quot;Error: no test specified\&quot; &amp;amp;&amp;amp; exit 1&quot;
    },
    &quot;repository&quot;: {
      &quot;type&quot;: &quot;git&quot;,
      &quot;url&quot;: &quot;git+ssh://git@github.com/jlengstorf/learn-rollup.git&quot;
    },
    &quot;author&quot;: &quot;Jason Lengstorf &amp;lt;jason@lengstorf.com&amp;gt; (@jlengstorf)&quot;,
    &quot;license&quot;: &quot;ISC&quot;,
    &quot;bugs&quot;: {
      &quot;url&quot;: &quot;https://github.com/jlengstorf/learn-rollup/issues&quot;
    },
    &quot;homepage&quot;: &quot;https://github.com/jlengstorf/learn-rollup#readme&quot;,
    &quot;devDependencies&quot;: {
      &quot;babel-preset-es2015-rollup&quot;: &quot;^1.2.0&quot;,
      &quot;cssnano&quot;: &quot;^3.7.4&quot;,
      &quot;livereload&quot;: &quot;^0.5.0&quot;,
      &quot;npm-run-all&quot;: &quot;^3.0.0&quot;,
      &quot;postcss-cssnext&quot;: &quot;^2.7.0&quot;,
      &quot;postcss-nested&quot;: &quot;^1.0.0&quot;,
      &quot;postcss-simple-vars&quot;: &quot;^3.0.0&quot;,
      &quot;rollup&quot;: &quot;^0.34.9&quot;,
      &quot;rollup-plugin-babel&quot;: &quot;^2.6.1&quot;,
      &quot;rollup-plugin-commonjs&quot;: &quot;^3.3.1&quot;,
      &quot;rollup-plugin-eslint&quot;: &quot;^2.0.2&quot;,
      &quot;rollup-plugin-node-resolve&quot;: &quot;^2.0.0&quot;,
      &quot;rollup-plugin-postcss&quot;: &quot;^0.1.1&quot;,
      &quot;rollup-plugin-replace&quot;: &quot;^1.1.1&quot;,
      &quot;rollup-plugin-uglify&quot;: &quot;^1.0.1&quot;,
      &quot;rollup-watch&quot;: &quot;^2.5.0&quot;
    },
    &quot;dependencies&quot;: {
      &quot;debug&quot;: &quot;^2.2.0&quot;
    }
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These scripts allow us to use a shortcut for executing the script of our choice.&lt;/p&gt;
&lt;p&gt;To run Rollup, we can now use &lt;code&gt;npm run dev&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;To run LiveReload, we can use &lt;code&gt;npm run reload&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;All that’s left now is to get them both running together.&lt;/p&gt;
&lt;h3&gt;Step 6: Install a tool to run the watcher and LiveReload in parallel.&lt;/h3&gt;
&lt;p&gt;In order to get both Rollup and LiveReload working at the same time, we’re going to use a utility called &lt;a href=&quot;https://www.npmjs.com/package/npm-run-all&quot;&gt;&lt;code&gt;npm-run-all&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This is a powerful tool, so we won’t talk about everything it can do. What we’re using it for is its ability to run scripts &lt;em&gt;in parallel&lt;/em&gt; — meaning both run at the same time inside the same terminal session.&lt;/p&gt;
&lt;p&gt;Start by installing &lt;code&gt;npm-run-all&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install --save-dev npm-run-all
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, we need to add one more script to &lt;code&gt;package.json&lt;/code&gt; that calls &lt;code&gt;npm-run-all&lt;/code&gt;. In the &lt;code&gt;scripts&lt;/code&gt; block, add the following (note that I’ve left out most of the file for brevity):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    &quot;scripts&quot;: {
      &quot;dev&quot;: &quot;rollup -c --watch&quot;,
      &quot;reload&quot;: &quot;livereload &apos;build/&apos; -d&quot;,
+     &quot;watch&quot;: &quot;npm-run-all --parallel reload dev&quot;,
      &quot;test&quot;: &quot;echo \&quot;Error: no test specified\&quot; &amp;amp;&amp;amp; exit 1&quot;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Save your changes, then go to the terminal. We’re ready for the last step!&lt;/p&gt;
&lt;h3&gt;Step 7: Run the final &lt;code&gt;watch&lt;/code&gt; script.&lt;/h3&gt;
&lt;p&gt;That’s it. We did it.&lt;/p&gt;
&lt;p&gt;In your terminal, run the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm run watch
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then reload your browser, make a change in the CSS or JS, and watch the browser refresh with the updated bundle — all like magic!&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;/images/learn-rollup-09.gif&quot; alt=&quot;The site live reloading with updated stylesheets.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;We’re now Rollup masters. Our code bundles will be smaller and faster, and our development process will be painless and quick.&lt;/p&gt;
&lt;h2&gt;Further Reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;http://postcss.org/&quot;&gt;PostCSS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://stackoverflow.com/questions/26882177/react-js-inline-style-best-practices&quot;&gt;Some discussion about using JS to insert styles, and when/whether it’s appropriate&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>How to Bundle JavaScript With Rollup — Step-by-Step Tutorial
</title><link>https://codetv.dev/blog/learn-rollup-js/</link><guid isPermaLink="true">https://codetv.dev/blog/learn-rollup-js/</guid><description>Learn how to use Rollup as a smaller, more efficient alternative to webpack and Browserify to bundle JavaScript files in this step-by-step tutorial series.
</description><pubDate>Fri, 19 Aug 2016 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://youtu.be/ICYLOZuFMz8&quot;&gt;Watch on YouTube&lt;/a&gt;&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Heads up:&lt;/strong&gt; this post was written in 2016, and some of the tools and prices may have changed. The code &lt;em&gt;should&lt;/em&gt; still work, but you may want to look for a more up-to-date tutorial.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;This week, we&apos;re going to build our first project using &lt;a href=&quot;http://rollupjs.org/&quot;&gt;Rollup&lt;/a&gt;, which is a build tool for bundling JavaScript (and stylesheets, but we&apos;ll get to that next week).&lt;/p&gt;
&lt;p&gt;By the end of this tutorial, we&apos;ll have Rollup configured to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;combine our scripts,&lt;/li&gt;
&lt;li&gt;remove unused code,&lt;/li&gt;
&lt;li&gt;transpile it to work with older browsers,&lt;/li&gt;
&lt;li&gt;support the use of Node modules in the browser,&lt;/li&gt;
&lt;li&gt;work with environment variables, and&lt;/li&gt;
&lt;li&gt;compress and minify our code for the smallest possible file size.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Prerequisites&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;This will make more sense if you know at least a little bit of JavaScript.&lt;/li&gt;
&lt;li&gt;Initial familiarity with &lt;a href=&quot;https://github.com/getify/You-Dont-Know-JS/blob/master/es6%20%26%20beyond/ch3.md#modules&quot;&gt;ES2015 modules&lt;/a&gt; doesn&apos;t hurt, either.&lt;/li&gt;
&lt;li&gt;You&apos;ll need &lt;code&gt;npm&lt;/code&gt; installed on your machine. (Don&apos;t have it? &lt;a href=&quot;https://nodejs.org/&quot;&gt;Install Node.js here.&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Series Navigation&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/blog/learn-rollup-js&quot;&gt;Part I: How to Use Rollup to Process and Bundle JavaScript Files&lt;/a&gt; &amp;lt;— you are here&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/learn-rollup-css&quot;&gt;Part II: How to Use Rollup to Process and Bundle Stylesheets&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;What Is Rollup?&lt;/h2&gt;
&lt;p&gt;In their own words:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Rollup is a next-generation JavaScript module bundler. Author your app or library using ES2015 modules, then efficiently bundle them up into a single file for use in browsers and Node.js.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It&apos;s similar to &lt;a href=&quot;http://browserify.org/&quot;&gt;Browserify&lt;/a&gt; and &lt;a href=&quot;https://webpack.github.io&quot;&gt;webpack&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;You could also call Rollup a build tool, which would put it in the company of things like &lt;a href=&quot;http://gruntjs.com/&quot;&gt;Grunt&lt;/a&gt; and &lt;a href=&quot;https://github.com/gulpjs/gulp&quot;&gt;Gulp&lt;/a&gt;. However, it&apos;s important to note that while you can use Grunt and Gulp to handle tasks like creating JavaScript bundles, those tools would use something like Rollup, Browserify, or webpack under the hood.&lt;/p&gt;
&lt;h3&gt;Why should you care about Rollup?&lt;/h3&gt;
&lt;p&gt;What makes Rollup exciting, though, is its ability to keep files small. This gets pretty nerdy, so the &lt;strong&gt;tl;dr&lt;/strong&gt; version is this: compared to the other tools for creating JavaScript bundles, Rollup will almost always create a smaller, faster bundle.&lt;/p&gt;
&lt;p&gt;This happens because Rollup is based on ES2015 modules, which are more efficient than CommonJS modules, which are what webpack and Browserify use. It&apos;s also much easier for Rollup to remove unused code from modules using something called &lt;em&gt;tree-shaking&lt;/em&gt;, which basically just means only the code we actually need is included in the final bundle.&lt;/p&gt;
&lt;p&gt;Tree-shaking becomes really important when we&apos;re including third-party tools or frameworks that have dozens of functions and methods available. If we&apos;re only using one or two — think &lt;a href=&quot;https://lodash.com/&quot;&gt;lodash&lt;/a&gt; or &lt;a href=&quot;https://jquery.com/&quot;&gt;jQuery&lt;/a&gt; — there&apos;s a &lt;em&gt;lot&lt;/em&gt; of wasted overhead in loading the rest of the library.&lt;/p&gt;
&lt;p&gt;Browserify and webpack will end up including a lot of unused code right now. But Rollup doesn&apos;t — it&apos;ll only bring in what we&apos;re actually using.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;UPDATE (2016-08-22):&lt;/strong&gt; To clarify, Rollup can only do tree-shaking on ES modules. CommonJS modules — which both lodash and jQuery are at the time of writing — cannot be tree-shaken. However, tree-shaking is &lt;em&gt;not&lt;/em&gt; the only speed/performance benefit of Rollup. See &lt;a href=&quot;https://www.reddit.com/r/javascript/comments/4yprc5/how_to_bundle_javascript_with_rollup_stepbystep/d6qzgzm&quot;&gt;Rich Harris&apos;s explanation&lt;/a&gt; and &lt;a href=&quot;https://www.reddit.com/r/javascript/comments/4yprc5/how_to_bundle_javascript_with_rollup_stepbystep/d6qzmgh?context=3&quot;&gt;Nolan Lawson&apos;s added info&lt;/a&gt; for more information.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;And that&apos;s &lt;em&gt;huge&lt;/em&gt;.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; Due in part to Rollup&apos;s efficiency, &lt;a href=&quot;http://www.2ality.com/2015/12/webpack-tree-shaking.html&quot;&gt;webpack 2 is going to include tree-shaking&lt;/a&gt;.&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;Part I: How to Use Rollup to Process and Bundle JavaScript Files&lt;/h2&gt;
&lt;p&gt;To show how effective Rollup is, let&apos;s walk through the process of building an extremely simple project that uses Rollup to bundle JavaScript.&lt;/p&gt;
&lt;h3&gt;Step 0: Create a project with JavaScript and CSS to be compiled.&lt;/h3&gt;
&lt;p&gt;In order to get started, we need to have some code to work with. For this tutorial, we&apos;ll be working with a small app, available on &lt;a href=&quot;https://github.com/jlengstorf/learn-rollup&quot;&gt;GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The folder structure looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;learn-rollup/
├── build/
│ └── index.html
├── src/
│ ├── scripts/
│ │ ├── modules/
│ │ │ ├── mod1.js
│ │ │ └── mod2.js
│ │ └── main.js
│ └── styles/
│ └── main.css
└── package.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can install the app we&apos;ll be working with during this tutorial by running the following command into your terminal.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Move to the folder where you keep your dev projects.
cd /path/to/your/projects

# Clone the starter branch of the app from GitHub.
git clone -b step-0 --single-branch https://github.com/jlengstorf/learn-rollup.git

# The files are downloaded to /path/to/your/projects/learn-rollup/
&lt;/code&gt;&lt;/pre&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; If you don&apos;t clone the repo, make sure to copy the contents of &lt;code&gt;build/index.html&lt;/code&gt; into your own code. The HTML isn&apos;t discussed in this tutorial.&lt;/p&gt;&lt;/aside&gt;
&lt;h3&gt;Step 1: Install Rollup and create a configuration file.&lt;/h3&gt;
&lt;p&gt;To get started, install Rollup with the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install --save-dev rollup
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, create a new file called &lt;code&gt;rollup.config.js&lt;/code&gt; in the &lt;code&gt;learn-rollup&lt;/code&gt; folder. Inside, add the following.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export default {
  entry: &apos;src/scripts/main.js&apos;,
  dest: &apos;build/js/main.min.js&apos;,
  format: &apos;iife&apos;,
  sourceMap: &apos;inline&apos;,
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let&apos;s talk about what each of these configuration options actually does:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;entry&lt;/code&gt; — this is the file we want Rollup to process. In most apps, this would be the main JavaScript file, which initializes everything and actually starts the show.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;dest&lt;/code&gt; — this is the location where the processed scripts should be saved.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;format&lt;/code&gt; — Rollup supports several output formats. Since we&apos;re running in the browser, we want to use an &lt;a href=&quot;http://benalman.com/news/2010/11/immediately-invoked-function-expression/&quot;&gt;immediately-invoked function expression&lt;/a&gt; (IIFE).&lt;/p&gt;
&lt;p&gt;(This is a fairly complex concept to understand, so don&apos;t stress if it doesn&apos;t make total sense. In a nutshell, we want our code to be inside its own scope, which prevents conflicts with other scripts. An IIFE is a &lt;a href=&quot;http://skilldrick.co.uk/2011/04/closures-explained-with-javascript/&quot;&gt;closure&lt;/a&gt; that contains our code in its own scope.)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;sourceMap&lt;/code&gt; — it&apos;s extremely helpful for debugging to provide a sourcemap. This option adds a sourcemap inside the generated file, which keeps things simple.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; For the other &lt;code&gt;format&lt;/code&gt; options and why you might need them, see &lt;a href=&quot;https://github.com/rollup/rollup/wiki/JavaScript-API#format&quot;&gt;Rollup&apos;s wiki&lt;/a&gt;.&lt;/p&gt;&lt;/aside&gt;
&lt;h4&gt;Test the Rollup configuration.&lt;/h4&gt;
&lt;p&gt;Once we&apos;ve created the config file, we can test that everything is working by running the following command in our terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;./node_modules/.bin/rollup -c
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will create a new folder called &lt;code&gt;build&lt;/code&gt; in your project, with a &lt;code&gt;js&lt;/code&gt; subfolder that contains our generated &lt;code&gt;main.min.js&lt;/code&gt; file.&lt;/p&gt;
&lt;p&gt;We can see that the bundle was created properly by opening &lt;code&gt;build/index.html&lt;/code&gt; in our browser:&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;/images/learn-rollup-01.jpg&quot; alt=&quot;Screenshot of the work-in-progress app.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; At this stage, only modern browsers will work without errors. To get this working with older browsers that don&apos;t support ES2015/ES6, we need to add some plugins.&lt;/p&gt;&lt;/aside&gt;
&lt;h4&gt;Look at the Bundled Output&lt;/h4&gt;
&lt;p&gt;What makes Rollup powerful is the fact that it uses &quot;tree-shaking&quot;, which leaves out unused code in the modules we reference. For example, in &lt;code&gt;src/scripts/modules/mod1.js&lt;/code&gt;, there&apos;s a function called &lt;code&gt;sayGoodbyeTo()&lt;/code&gt; that isn&apos;t used in our app — and since it&apos;s never used, Rollup doesn&apos;t include it in our bundle:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(function () {
  &apos;use strict&apos;;

  /**
   * Says hello.
   * @param  {String} name a name
   * @return {String}      a greeting for `name`
   */
  function sayHelloTo(name) {
    const toSay = `Hello, ${name}!`;

    return toSay;
  }

  /**
   * Adds all the values in an array.
   * @param  {Array} arr an array of numbers
   * @return {Number}    the sum of all the array values
   */
  const addArray = (arr) =&amp;gt; {
    const result = arr.reduce((a, b) =&amp;gt; a + b, 0);

    return result;
  };

  // Import a couple modules for testing.
  // Run some functions from our imported modules.
  const result1 = sayHelloTo(&apos;Jason&apos;);
  const result2 = addArray([1, 2, 3, 4]);

  // Print the results on the page.
  const printTarget = document.getElementsByClassName(&apos;debug__output&apos;)[0];

  printTarget.innerText = `sayHelloTo(&apos;Jason&apos;) =&amp;gt; ${result1}\n\n`;
  printTarget.innerText += `addArray([1, 2, 3, 4]) =&amp;gt; ${result2}`;
})();
//# sourceMappingURL=data:application/json;charset=utf-8;base64,...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In other build tools that&apos;s not always the case, and bundles can get &lt;em&gt;really&lt;/em&gt; large if we include everything inside a bigger library like &lt;a href=&quot;https://lodash.com/&quot;&gt;lodash&lt;/a&gt; just to reference one or two functions.&lt;/p&gt;
&lt;p&gt;For example, using &lt;a href=&quot;https://webpack.github.io&quot;&gt;webpack&lt;/a&gt;, the &lt;code&gt;sayGoodbyeTo()&lt;/code&gt; function is included, and the resulting bundle is more than double the size of what Rollup generates.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; It&apos;s important to keep in mind, though, that when we&apos;re dealing with such a small example app it doesn&apos;t take much to double a file size. The comparison at this point is ~3KB vs. ~8KB.&lt;/p&gt;&lt;/aside&gt;
&lt;h3&gt;Step 2: Set up Babel so we can use new JavaScript features now.&lt;/h3&gt;
&lt;p&gt;At this point, we&apos;ve got a code bundle that will work in modern browsers, but it&apos;ll break if the browser is even a couple versions old in some cases — that&apos;s not ideal.&lt;/p&gt;
&lt;p&gt;Fortunately, &lt;a href=&quot;https://babeljs.io&quot;&gt;Babel&lt;/a&gt; has us covered. This project &lt;a href=&quot;https://scotch.io/tutorials/javascript-transpilers-what-they-are-why-we-need-them&quot;&gt;transpiles&lt;/a&gt; new features of JavaScript (&lt;a href=&quot;https://github.com/getify/You-Dont-Know-JS/blob/master/es6%20%26%20beyond/ch1.md&quot;&gt;ES6/ES2015 and so on&lt;/a&gt;) into ES5, which will run on virtually any browser that&apos;s still used today.&lt;/p&gt;
&lt;p&gt;If you&apos;ve never used Babel, your life as a developer is about to change forever. Having access to the new features of JavaScript makes the language simpler, cleaner, and more pleasant in general.&lt;/p&gt;
&lt;p&gt;So let&apos;s make that part of our Rollup process so we don&apos;t have to think about it.&lt;/p&gt;
&lt;h4&gt;Install the necessary modules.&lt;/h4&gt;
&lt;p&gt;First, we need to install the &lt;a href=&quot;https://github.com/rollup/rollup-plugin-babel&quot;&gt;Babel Rollup plugin&lt;/a&gt; and the &lt;a href=&quot;https://www.npmjs.com/package/babel-preset-es2015&quot;&gt;appropriate Babel preset&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Install Rollup’s Babel plugin.
npm install --save-dev rollup-plugin-babel

# Install the Babel preset for transpiling ES2015.
npm install --save-dev babel-preset-es2015

# Install Babel’s external helpers for module support.
npm install --save-dev babel-plugin-external-helpers
&lt;/code&gt;&lt;/pre&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; A Babel preset is a collection of Babel plugins that tell Babel what we actually want to transpile&lt;/p&gt;&lt;/aside&gt;
&lt;h4&gt;Create a &lt;code&gt;.babelrc&lt;/code&gt;.&lt;/h4&gt;
&lt;p&gt;Next, create a new file called &lt;code&gt;.babelrc&lt;/code&gt; in your project&apos;s root directory (&lt;code&gt;learn-rollup/&lt;/code&gt;). Inside, add the following JSON:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;presets&quot;: [
    [
      &quot;es2015&quot;,
      {
        &quot;modules&quot;: false
      }
    ]
  ],
  &quot;plugins&quot;: [&quot;external-helpers&quot;]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This tells Babel which preset it should use during transpiling.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; In older versions of npm (&amp;lt; &lt;code&gt;v2.15.11&lt;/code&gt;), you may see an error with the &lt;code&gt;es2015-rollup&lt;/code&gt; preset. If you can&apos;t update npm, see &lt;a href=&quot;https://github.com/jlengstorf/learn-rollup/issues/2&quot;&gt;this issue&lt;/a&gt; for an alternative &lt;code&gt;.babelrc&lt;/code&gt; configuration.&lt;/p&gt;&lt;/aside&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;UPDATE (2016-11-13):&lt;/strong&gt; In the video, &lt;code&gt;.babelrc&lt;/code&gt; uses an older (outdated) configuration. &lt;a href=&quot;https://github.com/jlengstorf/learn-rollup/pull/17&quot;&gt;See this pull request for configuration changes&lt;/a&gt;, and &lt;a href=&quot;https://github.com/jlengstorf/learn-rollup/pull/37&quot;&gt;this one for the changes to &lt;code&gt;package.json&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;&lt;/aside&gt;
&lt;h4&gt;Update &lt;code&gt;rollup.config.js&lt;/code&gt;.&lt;/h4&gt;
&lt;p&gt;To make this actually do stuff, we need to update &lt;code&gt;rollup.config.js&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Inside, we &lt;code&gt;import&lt;/code&gt; the Babel plugin, then add it to a new configuration property called &lt;code&gt;plugins&lt;/code&gt;, which will hold an array of plugins.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Rollup plugins
import babel from &apos;rollup-plugin-babel&apos;;

export default {
  entry: &apos;src/scripts/main.js&apos;,
  dest: &apos;build/js/main.min.js&apos;,
  format: &apos;iife&apos;,
  sourceMap: &apos;inline&apos;,
  plugins: [
    babel({
      exclude: &apos;node_modules/**&apos;,
    }),
  ],
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In order to avoid transpiling third-party scripts, we set an &lt;code&gt;exclude&lt;/code&gt; config property to ignore the &lt;code&gt;node_modules&lt;/code&gt; directory.&lt;/p&gt;
&lt;h4&gt;Check the bundle output.&lt;/h4&gt;
&lt;p&gt;With everything installed and configured, we can rebuild the bundle:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;./node_modules/.bin/rollup -c
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When we look at the output, it looks &lt;em&gt;mostly&lt;/em&gt; the same. But there are a few key differences: for example, look at the &lt;code&gt;addArray()&lt;/code&gt; function:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var addArray = function addArray(arr) {
  var result = arr.reduce(function (a, b) {
    return a + b;
  }, 0);

  return result;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;See how Babel converted the &lt;a href=&quot;https://strongloop.com/strongblog/an-introduction-to-javascript-es6-arrow-functions/&quot;&gt;fat-arrow function&lt;/a&gt; (&lt;code&gt;arr.reduce((a, b) =&amp;gt; a + b, 0)&lt;/code&gt;)to a regular function?&lt;/p&gt;
&lt;p&gt;That&apos;s transpiling in action: the result is the same, but the code is now supported back to IE9.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;IMPORTANT:&lt;/strong&gt; Babel also offers &lt;a href=&quot;https://babeljs.io/docs/usage/polyfill/&quot;&gt;&lt;code&gt;babel-polyfill&lt;/code&gt;&lt;/a&gt;, which makes things like &lt;code&gt;Array.prototype.reduce()&lt;/code&gt; available in IE8 and earlier where possible.&lt;/p&gt;&lt;/aside&gt;
&lt;h3&gt;Step 3: Add ESLint to check for common JavaScript errors.&lt;/h3&gt;
&lt;p&gt;It&apos;s always a good idea to use a linter for your code, because it enforces consistent coding practices and helps catch tricky bugs like missing brackets or parentheses.&lt;/p&gt;
&lt;p&gt;For this project, we&apos;ll be using &lt;a href=&quot;http://eslint.org/&quot;&gt;ESLint&lt;/a&gt;.&lt;/p&gt;
&lt;h4&gt;Install the Modules.&lt;/h4&gt;
&lt;p&gt;In order to use ESLint, we&apos;ll want to install the &lt;a href=&quot;https://github.com/TrySound/rollup-plugin-eslint&quot;&gt;ESLint Rollup plugin&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install --save-dev rollup-plugin-eslint
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Generate a &lt;code&gt;.eslintrc.json&lt;/code&gt;.&lt;/h4&gt;
&lt;p&gt;To make sure we only get errors we want, we need to configure ESLint first. Fortunately, we can automatically generate most of this configuration by running the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ ./node_modules/.bin/eslint --init
? How would you like to configure ESLint? Answer questions about your style
? Are you using ECMAScript 6 features? Yes
? Are you using ES6 modules? Yes
? Where will your code run? Browser
? Do you use CommonJS? No
? Do you use JSX? No
? What style of indentation do you use? Spaces
? What quotes do you use for strings? Single
? What line endings do you use? Unix
? Do you require semicolons? Yes
? What format do you want your config file to be in? JSON
Successfully created .eslintrc.json file in /Users/jlengstorf/dev/code.lengstorf.com/projects/learn-rollup
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you answer the questions as shown above, you&apos;ll get the following output in &lt;code&gt;.eslintrc.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;env&quot;: {
    &quot;browser&quot;: true,
    &quot;es6&quot;: true
  },
  &quot;extends&quot;: &quot;eslint:recommended&quot;,
  &quot;parserOptions&quot;: {
    &quot;sourceType&quot;: &quot;module&quot;
  },
  &quot;rules&quot;: {
    &quot;indent&quot;: [&quot;error&quot;, 4],
    &quot;linebreak-style&quot;: [&quot;error&quot;, &quot;unix&quot;],
    &quot;quotes&quot;: [&quot;error&quot;, &quot;single&quot;],
    &quot;semi&quot;: [&quot;error&quot;, &quot;always&quot;]
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Tweak &lt;code&gt;.eslintrc.json&lt;/code&gt;.&lt;/h4&gt;
&lt;p&gt;However, we need to make a couple adjustments to avoid errors for our project:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;We&apos;re using 2 spaces instead of 4.&lt;/li&gt;
&lt;li&gt;We will use a global variable called &lt;code&gt;ENV&lt;/code&gt; later, so we need to whitelist that.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Make the following adjustments — the &lt;code&gt;globals&lt;/code&gt; property and the adjustment to the &lt;code&gt;indent&lt;/code&gt; property — to your &lt;code&gt;.eslintrc.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;env&quot;: {
    &quot;browser&quot;: true,
    &quot;es6&quot;: true
  },
  &quot;globals&quot;: {
    &quot;ENV&quot;: true
  },
  &quot;extends&quot;: &quot;eslint:recommended&quot;,
  &quot;parserOptions&quot;: {
    &quot;sourceType&quot;: &quot;module&quot;
  },
  &quot;rules&quot;: {
    &quot;indent&quot;: [&quot;error&quot;, 2],
    &quot;linebreak-style&quot;: [&quot;error&quot;, &quot;unix&quot;],
    &quot;quotes&quot;: [&quot;error&quot;, &quot;single&quot;],
    &quot;semi&quot;: [&quot;error&quot;, &quot;always&quot;]
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Update &lt;code&gt;rollup.config.js&lt;/code&gt;.&lt;/h4&gt;
&lt;p&gt;Next, &lt;code&gt;import&lt;/code&gt; the ESLint plugin and add it to the Rollup configuration:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Rollup plugins
import babel from &apos;rollup-plugin-babel&apos;;
import eslint from &apos;rollup-plugin-eslint&apos;;

export default {
  entry: &apos;src/scripts/main.js&apos;,
  dest: &apos;build/js/main.min.js&apos;,
  format: &apos;iife&apos;,
  sourceMap: &apos;inline&apos;,
  plugins: [
    eslint({
      exclude: [&apos;src/styles/**&apos;],
    }),
    babel({
      exclude: &apos;node_modules/**&apos;,
    }),
  ],
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Check the console output.&lt;/h4&gt;
&lt;p&gt;At first, when we run &lt;code&gt;./node_modules/.bin/rollup -c&lt;/code&gt;, nothing seems to be happening. That&apos;s because as it stands, the app&apos;s code passes the linter without issues.&lt;/p&gt;
&lt;p&gt;But if we introduce an issue — say removing a semicolon — we&apos;ll see how ESLint helps:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ ./node_modules/.bin/rollup -c

/Users/jlengstorf/dev/code.lengstorf.com/projects/learn-rollup/src/scripts/main.js
12:64 error Missing semicolon semi

✖ 1 problem (1 error, 0 warnings)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Something that has the potential to introduce a mystery bug is now pointed out instantly, including the file, line, and column where the issue is happening.&lt;/p&gt;
&lt;p&gt;While this won&apos;t eliminate all of our problems with debugging, it goes a long way toward squashing bugs that are due to obvious typos and oversights.&lt;/p&gt;
&lt;p&gt;(As someone who has previously spent — &lt;em&gt;ahem&lt;/em&gt; — numerous hours chasing bugs that ended up being something as silly as a misspelled variable name, it&apos;s hard to overstate the efficiency boost that working with a linter provides.)&lt;/p&gt;
&lt;h3&gt;Step 4: Add plugins to handle non-ES modules.&lt;/h3&gt;
&lt;p&gt;This is important if any of your dependencies use Node-style modules. Without it, you&apos;ll get errors about &lt;code&gt;require&lt;/code&gt;.&lt;/p&gt;
&lt;h4&gt;Add a Node module as a dependency.&lt;/h4&gt;
&lt;p&gt;It would be easy to bang through this sample project without referencing a third-party module, but that&apos;s not going to cut it in real projects. So, in the interest of making our Rollup setup &lt;em&gt;actually useful&lt;/em&gt;, let&apos;s make sure we can also reference third-party modules in our code.&lt;/p&gt;
&lt;p&gt;For simplicity, we&apos;ll add a simple logger to our code using the &lt;a href=&quot;https://www.npmjs.com/package/debug&quot;&gt;&lt;code&gt;debug&lt;/code&gt;&lt;/a&gt; package. Start by installing it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install --save debug
&lt;/code&gt;&lt;/pre&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; Since this will be referenced in the main project, it&apos;s important to use &lt;code&gt;--save&lt;/code&gt;, which will avoid an error in production environments where the &lt;code&gt;devDependencies&lt;/code&gt; won&apos;t be installed.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;Then, inside &lt;code&gt;src/scripts/main.js&lt;/code&gt;, let&apos;s add some simple logging:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Import a couple modules for testing.
import { sayHelloTo } from &apos;./modules/mod1&apos;;
import addArray from &apos;./modules/mod2&apos;;

// Import a logger for easier debugging.
import debug from &apos;debug&apos;;
const log = debug(&apos;app:log&apos;);

// Enable the logger.
debug.enable(&apos;*&apos;);
log(&apos;Logging is enabled!&apos;);

// Run some functions from our imported modules.
const result1 = sayHelloTo(&apos;Jason&apos;);
const result2 = addArray([1, 2, 3, 4]);

// Print the results on the page.
const printTarget = document.getElementsByClassName(&apos;debug__output&apos;)[0];

printTarget.innerText = `sayHelloTo(&apos;Jason&apos;) =&amp;gt; ${result1}\n\n`;
printTarget.innerText += `addArray([1, 2, 3, 4]) =&amp;gt; ${result2}`;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So far so good, but when we run rollup we get a warning:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ ./node_modules/.bin/rollup -c
Treating &apos;debug&apos; as external dependency
No name was provided for external module &apos;debug&apos; in options.globals – guessing &apos;debug&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And if we check our &lt;code&gt;index.html&lt;/code&gt; again, we can see that a &lt;code&gt;ReferenceError&lt;/code&gt; was thrown for &lt;code&gt;debug&lt;/code&gt;:&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;/images/learn-rollup-02.jpg&quot; alt=&quot;Console error for debug.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;Well, shit. That didn&apos;t work at all.&lt;/p&gt;
&lt;p&gt;This happens because Node modules use &lt;a href=&quot;http://wiki.commonjs.org/wiki/Modules/1.1&quot;&gt;CommonJS&lt;/a&gt;, which isn&apos;t compatible with Rollup out of the box. To solve this, we need to add a couple plugins for handling Node dependencies and CommonJS modules.&lt;/p&gt;
&lt;h4&gt;Install the modules.&lt;/h4&gt;
&lt;p&gt;To work around this problem, we&apos;re going to add two plugins to Rollup:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/rollup/rollup-plugin-node-resolve&quot;&gt;&lt;code&gt;rollup-plugin-node-resolve&lt;/code&gt;&lt;/a&gt;, which allows us to load third-party modules in &lt;code&gt;node_modules&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/rollup/rollup-plugin-commonjs&quot;&gt;&lt;code&gt;rollup-plugin-commonjs&lt;/code&gt;&lt;/a&gt;, which coverts CommonJS modules to ES6, which stops them from breaking Rollup.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Install both plugins with the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install --save-dev rollup-plugin-node-resolve rollup-plugin-commonjs
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Update &lt;code&gt;rollup.config.js&lt;/code&gt;.&lt;/h4&gt;
&lt;p&gt;Next, &lt;code&gt;import&lt;/code&gt; and add the plugins to the Rollup config:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Rollup plugins
import babel from &apos;rollup-plugin-babel&apos;;
import eslint from &apos;rollup-plugin-eslint&apos;;
import resolve from &apos;rollup-plugin-node-resolve&apos;;
import commonjs from &apos;rollup-plugin-commonjs&apos;;

export default {
  entry: &apos;src/scripts/main.js&apos;,
  dest: &apos;build/js/main.min.js&apos;,
  format: &apos;iife&apos;,
  sourceMap: &apos;inline&apos;,
  plugins: [
    resolve({
      jsnext: true,
      main: true,
      browser: true,
    }),
    commonjs(),
    eslint({
      exclude: [&apos;src/styles/**&apos;],
    }),
    babel({
      exclude: &apos;node_modules/**&apos;,
    }),
  ],
};
&lt;/code&gt;&lt;/pre&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; The &lt;code&gt;jsnext&lt;/code&gt; property is part of an effort to &lt;a href=&quot;https://github.com/rollup/rollup/wiki/jsnext:main&quot;&gt;ease the migration to ES2015 modules for Node packages&lt;/a&gt;. The &lt;code&gt;main&lt;/code&gt; and &lt;code&gt;browser&lt;/code&gt; properties help the plugin decide which files should be used for the bundle.&lt;/p&gt;&lt;/aside&gt;
&lt;h4&gt;Check the console output.&lt;/h4&gt;
&lt;p&gt;Rebuild the bundle with &lt;code&gt;./node_modules/.bin/rollup -c&lt;/code&gt;, then check the browser again to see the output:&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;/images/learn-rollup-03.jpg&quot; alt=&quot;Debug logs in the console.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;h3&gt;Step 5: Add a plugin to replace environment variables.&lt;/h3&gt;
&lt;p&gt;Environment variables add a lot of power to our development flow, and give us the ability to do things such as turning logging off and on, injecting dev-only scripts, and more.&lt;/p&gt;
&lt;p&gt;So let&apos;s make sure Rollup will enable us to use them.&lt;/p&gt;
&lt;h4&gt;Add an &lt;code&gt;ENV&lt;/code&gt;-based conditional in &lt;code&gt;main.js&lt;/code&gt;.&lt;/h4&gt;
&lt;p&gt;Let&apos;s make use of an environment variable and only enable our logging script if we&apos;re not in &lt;code&gt;production&lt;/code&gt; mode. In &lt;code&gt;src/scripts/main.js&lt;/code&gt;, let&apos;s change the way our &lt;code&gt;log()&lt;/code&gt; is initialized:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Import a logger for easier debugging.
import debug from &apos;debug&apos;;
const log = debug(&apos;app:log&apos;);

// The logger should only be disabled if we’re not in production.
if (ENV !== &apos;production&apos;) {
  // Enable the logger.
  debug.enable(&apos;*&apos;);
  log(&apos;Logging is enabled!&apos;);
} else {
  debug.disable();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;However, after we rebuild our bundle (&lt;code&gt;./node_modules/.bin/rollup -c&lt;/code&gt;) and check the browser, we can see that this gives us a &lt;code&gt;ReferenceError&lt;/code&gt; for &lt;code&gt;ENV&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;That shouldn&apos;t be surprising, though, because we haven&apos;t defined it anywhere. But if we try something like &lt;code&gt;ENV=production ./node_modules/.bin/rollup -c&lt;/code&gt;, it still doesn&apos;t work. This is because setting an environment variable that way only makes it available to Rollup, not to the bundle created by Rollup.&lt;/p&gt;
&lt;p&gt;We&apos;ll need to use a plugin to pass our environment variables into the bundle.&lt;/p&gt;
&lt;h4&gt;Install the modules.&lt;/h4&gt;
&lt;p&gt;Start by installing &lt;a href=&quot;https://github.com/rollup/rollup-plugin-replace&quot;&gt;&lt;code&gt;rollup-plugin-replace&lt;/code&gt;&lt;/a&gt;, which is essentially just a find-and-replace utility. It can do a lot of things, but for our purposes we&apos;ll have it simply find an occurrence of an environment variable and replace it with the actual value (e.g. all occurrences of &lt;code&gt;ENV&lt;/code&gt; would be replaced with &lt;code&gt;&quot;production&quot;&lt;/code&gt; in the bundle).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install --save-dev rollup-plugin-replace
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Update &lt;code&gt;rollup.config.js&lt;/code&gt;.&lt;/h3&gt;
&lt;p&gt;Inside &lt;code&gt;rollup.config.js&lt;/code&gt;, let&apos;s &lt;code&gt;import&lt;/code&gt; the plugin and add it to our list of plugins.&lt;/p&gt;
&lt;p&gt;The configuration is pretty straightforward: we can just add a list of &lt;code&gt;key: value&lt;/code&gt; pairs, where the &lt;code&gt;key&lt;/code&gt; is the string to replace, and the &lt;code&gt;value&lt;/code&gt; is what it should be replaced with.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Rollup plugins
import babel from &apos;rollup-plugin-babel&apos;;
import eslint from &apos;rollup-plugin-eslint&apos;;
import resolve from &apos;rollup-plugin-node-resolve&apos;;
import commonjs from &apos;rollup-plugin-commonjs&apos;;
import replace from &apos;rollup-plugin-replace&apos;;

export default {
  entry: &apos;src/scripts/main.js&apos;,
  dest: &apos;build/js/main.min.js&apos;,
  format: &apos;iife&apos;,
  sourceMap: &apos;inline&apos;,
  plugins: [
    resolve({
      jsnext: true,
      main: true,
      browser: true,
    }),
    commonjs(),
    eslint({
      exclude: [&apos;src/styles/**&apos;],
    }),
    babel({
      exclude: &apos;node_modules/**&apos;,
    }),
    replace({
      exclude: &apos;node_modules/**&apos;,
      ENV: JSON.stringify(process.env.NODE_ENV || &apos;development&apos;),
    }),
  ],
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In our configuration, we&apos;re going to find every occurence of &lt;code&gt;ENV&lt;/code&gt; and replace it with &lt;em&gt;either&lt;/em&gt; the value of &lt;code&gt;process.env.NODE_ENV&lt;/code&gt; — the conventional way of setting the environment in Node apps — or &quot;development&quot;. We use &lt;code&gt;JSON.stringify()&lt;/code&gt; to make sure the value is wrapped in double quotes, since &lt;code&gt;ENV&lt;/code&gt; is not.&lt;/p&gt;
&lt;p&gt;To make sure we don&apos;t cause issues with third-party code, we also set the &lt;code&gt;exclude&lt;/code&gt; property to ignore our &lt;code&gt;node_modules&lt;/code&gt; directory and all the packages it contains. (Thanks to &lt;a href=&quot;https://github.com/jlengstorf/learn-rollup/issues/3&quot;&gt;@wesleycoder for the heads-up on this&lt;/a&gt;.)&lt;/p&gt;
&lt;h4&gt;Check the results.&lt;/h4&gt;
&lt;p&gt;To start, rebuild the bundle and check the browser. The console log should show up, just like before. That&apos;s good — that means our default value was applied.&lt;/p&gt;
&lt;p&gt;To see where the power comes in, let&apos;s run the command in &lt;code&gt;production&lt;/code&gt; mode:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NODE_ENV=production ./node_modules/.bin/rollup -c
&lt;/code&gt;&lt;/pre&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; On Windows, use &lt;code&gt;SET NODE_ENV=production ./node_modules/.bin/rollup -c&lt;/code&gt; to avoid an error setting environment variables. If you have issues with that command, see &lt;a href=&quot;https://github.com/jlengstorf/learn-rollup/issues/30&quot;&gt;this issue&lt;/a&gt; for additional information.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;When we reload the browser, there&apos;s nothing logged to the console:&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;/images/learn-rollup-04.jpg&quot; alt=&quot;Console shows no logging.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;h3&gt;Step 6: Add UglifyJS to compress and minify our generated script.&lt;/h3&gt;
&lt;p&gt;The last JavaScript step we&apos;ll go through in this tutorial is adding UglifyJS to minify and compress the bundle. This can &lt;em&gt;hugely&lt;/em&gt; reduce the size of a bundle by removing comments, shortening variable names, and otherwise mangling the hell out of the code — which makes it more or less unreadable for humans, but much more efficient to deliver over a network.&lt;/p&gt;
&lt;h4&gt;Install the plugin.&lt;/h4&gt;
&lt;p&gt;We&apos;ll be using &lt;a href=&quot;https://github.com/mishoo/UglifyJS2/&quot;&gt;UglifyJS&lt;/a&gt; to compress the bundle, by way of &lt;a href=&quot;https://github.com/TrySound/rollup-plugin-uglify&quot;&gt;&lt;code&gt;rollup-plugin-uglify&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Install it with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install --save-dev rollup-plugin-uglify
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Update &lt;code&gt;rollup.config.js&lt;/code&gt;.&lt;/h4&gt;
&lt;p&gt;Next, let&apos;s add Uglify to our Rollup config. However, for legibility during development, let&apos;s make uglification a production-only feature:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Rollup plugins
import babel from &apos;rollup-plugin-babel&apos;;
import eslint from &apos;rollup-plugin-eslint&apos;;
import resolve from &apos;rollup-plugin-node-resolve&apos;;
import commonjs from &apos;rollup-plugin-commonjs&apos;;
import replace from &apos;rollup-plugin-replace&apos;;
import uglify from &apos;rollup-plugin-uglify&apos;;

export default {
  entry: &apos;src/scripts/main.js&apos;,
  dest: &apos;build/js/main.min.js&apos;,
  format: &apos;iife&apos;,
  sourceMap: &apos;inline&apos;,
  plugins: [
    resolve({
      jsnext: true,
      main: true,
      browser: true,
    }),
    commonjs(),
    eslint({
      exclude: [&apos;src/styles/**&apos;],
    }),
    babel({
      exclude: &apos;node_modules/**&apos;,
    }),
    replace({
      ENV: JSON.stringify(process.env.NODE_ENV || &apos;development&apos;),
    }),
    process.env.NODE_ENV === &apos;production&apos; &amp;amp;&amp;amp; uglify(),
  ],
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We&apos;re using something called &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Logical_Operators#Short-Circuit_Evaluation&quot;&gt;short-circuit evaluation&lt;/a&gt;, which is a common (though &lt;a href=&quot;http://stackoverflow.com/questions/5049006/using-s-short-circuiting-as-an-if-statement&quot;&gt;debatably evil&lt;/a&gt;) shortcut for conditionally setting a value. (For example, it&apos;s pretty common to see this used to assign default values, like &lt;code&gt;var value = maybeThisExists || &apos;default&apos;&lt;/code&gt;.)&lt;/p&gt;
&lt;p&gt;In our case, we&apos;re only loading &lt;code&gt;uglify()&lt;/code&gt; if &lt;code&gt;NODE_ENV&lt;/code&gt; is set to &quot;production&quot;.&lt;/p&gt;
&lt;h4&gt;Check the minified bundle.&lt;/h4&gt;
&lt;p&gt;With the configuration saved, let&apos;s run Rollup with &lt;code&gt;NODE_ENV&lt;/code&gt; in production:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NODE_ENV=production ./node_modules/.bin/rollup -c
&lt;/code&gt;&lt;/pre&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; On Windows, use &lt;code&gt;SET NODE_ENV=production ./node_modules/.bin/rollup -c&lt;/code&gt; to avoid an error setting environment variables.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;The output isn&apos;t pretty, but it&apos;s &lt;em&gt;much&lt;/em&gt; smaller. Here&apos;s a screenshot of what &lt;code&gt;build/js/main.min.js&lt;/code&gt; looks like now:&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;/images/learn-rollup-05.jpg&quot; alt=&quot;Minified JavaScript code.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;Before, our bundle was ~42KB. After running it through UglifyJS, it&apos;s down to ~29KB — we just saved over 30% on file size with no additional effort.&lt;/p&gt;
&lt;h2&gt;Coming Up Next&lt;/h2&gt;
&lt;p&gt;In the next installment of this series, we&apos;ll look at handling stylesheets through Rollup using &lt;a href=&quot;https://github.com/postcss/postcss&quot;&gt;PostCSS&lt;/a&gt;, as well as adding live reloading so we can see our changes near-instantaneously as we make them.&lt;/p&gt;
&lt;h2&gt;Further Reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://nolanlawson.com/2016/08/15/the-cost-of-small-modules/&quot;&gt;The cost of small modules&lt;/a&gt; — this is the article that got me interested in Rollup, because it shows some significant advantages of Rollup over webpack and Browserify.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://rollupjs.org/guide/&quot;&gt;Rollup&apos;s getting started guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/rollup/rollup/wiki/Command-Line-Interface&quot;&gt;Rollup&apos;s CLI docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/rollup/rollup/wiki/Plugins&quot;&gt;A list of Rollup plugins&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Deploy a Node.js App to DigitalOcean with SSL</title><link>https://codetv.dev/blog/deploy-nodejs-ssl-digitalocean/</link><guid isPermaLink="true">https://codetv.dev/blog/deploy-nodejs-ssl-digitalocean/</guid><description>This step-by-step tutorial walks through the process of deploying a Node.js app to a DigitalOcean droplet with free SSL from Let’s Encrypt for $5/month.
</description><pubDate>Tue, 09 Aug 2016 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://youtu.be/kR06NoSzAXY&quot;&gt;Watch on YouTube&lt;/a&gt;&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Heads up:&lt;/strong&gt; this post was written in 2016, and some of the tools and prices may have changed. The code &lt;em&gt;should&lt;/em&gt; still work, but you may want to look for a more up-to-date tutorial.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;In this post, we&apos;ll walk through the process, from start to finish, of creating a new server, deploying a Node.js app, securing it (for free!) with an SSL certificate, and pointing a domain name to it.&lt;/p&gt;
&lt;p&gt;Watch the video above to see the whole process live — with clever commentary, of course — or jump to just the bits you need in the write-up below.&lt;/p&gt;
&lt;h2&gt;Prerequisites&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;A domain name that you can modify DNS records for.&lt;/li&gt;
&lt;li&gt;A sense of adventure.&lt;/li&gt;
&lt;/ul&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; This tutorial uses the command line. But don&apos;t worry — we&apos;ll get through this together.&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;Set Up and Configure Your Server&lt;/h2&gt;
&lt;p&gt;Before we can do anything, we need a server that can be accessed publicly. There are lots of options out there, so don&apos;t feel locked into DigitalOcean — however, for this tutorial it&apos;ll probably be easiest to follow if you&apos;re using exactly the same setup.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;If you don&apos;t have a DigitalOcean account, you can get $10 of credit — that&apos;s enough to run this app for two months — by signing up using this link: &lt;a href=&quot;https://m.do.co/c/9d561578e13a&quot;&gt;claim your $10 in DigitalOcean credit&lt;/a&gt;.&lt;/p&gt;&lt;/aside&gt;
&lt;h3&gt;Create a new droplet on DigitalOcean.&lt;/h3&gt;
&lt;p&gt;To start &lt;a href=&quot;https://m.do.co/c/9d561578e13a&quot;&gt;create an account on DigitalOcean&lt;/a&gt;, or &lt;a href=&quot;https://cloud.digitalocean.com/login&quot;&gt;log into your existing account&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Once you&apos;re logged in, click the &quot;Create Droplet&quot; button at the top of your screen.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;/images/nodejs-ssl-deploy-01.jpg&quot; alt=&quot;The DigitalOcean dashboard.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;Choose the $5/month option with Ubuntu 16.04.1 x64. Select a region closest to your users.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;/images/nodejs-ssl-deploy-02.jpg&quot; alt=&quot;Creating a $5/month DigitalOcean droplet with Ubuntu 16.04.1.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;Finally, add your SSH key and&lt;/p&gt;
&lt;h4&gt;How to find your SSH key&lt;/h4&gt;
&lt;p&gt;First, open Terminal[^windows] and check for existing SSH keys:&lt;/p&gt;
&lt;p&gt;[^windows]: If you&apos;re on Windows, check out &lt;a href=&quot;https://git-scm.com/&quot;&gt;Git Bash&lt;/a&gt; for a way to run these commands on your computer.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ls -la ~/.ssh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you already have SSH keys set up, you should see a file called &lt;code&gt;id_rsa.pub&lt;/code&gt;. (If there&apos;s a file ending in &lt;code&gt;.pub&lt;/code&gt;, it&apos;s very likely an SSH key.)&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;If you need to create an SSH key, use &lt;a href=&quot;https://help.github.com/articles/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent/&quot;&gt;GitHub&apos;s guide&lt;/a&gt; to get yourself straightened out.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;To copy your SSH key, use one of the following commands:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# This copies the key so you can paste with command + V
pbcopy &amp;lt; ~/.ssh/id_rsa.pub

# This prints it in the command line for manual copying
cat ~/.ssh/id_rsa.pub
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Add your SSH key to the droplet&lt;/h4&gt;
&lt;p&gt;Back on the DigitalOcean droplet creation screen, click the &quot;New SSH Key&quot; button and paste your SSH key into the field that opens.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;/images/nodejs-ssl-deploy-03.jpg&quot; alt=&quot;The SSH key field on the Digital Ocean dashboard.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;Click &quot;Add SSH Key&quot; to save it, then make sure it&apos;s selected, name your droplet, and hit the big &quot;Create&quot; button to get your server online.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;/images/nodejs-ssl-deploy-04.jpg&quot; alt=&quot;Droplet creating on the Digital Ocean dashboard.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;Your new droplet will display its IP address once it&apos;s set up. You can click on it to copy the IP to your clipboard.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;/images/nodejs-ssl-deploy-05.jpg&quot; alt=&quot;Droplet IP address on the Digital Ocean dashboard.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;h4&gt;Connect to the server using SSH&lt;/h4&gt;
&lt;p&gt;DigitalOcean droplets are created with a &lt;code&gt;root&lt;/code&gt; user, and since we added our SSH keys, we can now log in without a password. Like magic!&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Make sure to replace the IP below with your server&apos;s IP address
ssh root@192.168.1.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You will most likely be asked if you want to continue connecting the first time you log in. Type &lt;code&gt;yes&lt;/code&gt; to continue, and you&apos;ll see something similar to the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ ssh root@138.68.11.65
The authenticity of host &apos;138.68.11.65 (138.68.11.65)&apos; can&apos;t be established.
ECDSA key fingerprint is SHA256:f1qsLkumkNyRNfDVgjJk2R7kRlonuce1IMoEVTL2sfE.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added &apos;138.68.11.65&apos; (ECDSA) to the list of known hosts.
Welcome to Ubuntu 16.04.1 LTS (GNU/Linux 4.4.0-31-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

0 packages can be updated.
0 updates are security updates.



The programs included with the Ubuntu system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.

root@nodejs-ssl-deploy:~#
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Configure the server with basic security.&lt;/h3&gt;
&lt;p&gt;Once we&apos;re logged into the server, we need to get a few things configured to keep it secure.[^security]&lt;/p&gt;
&lt;p&gt;[^security]: Keep in mind that this is &lt;em&gt;not&lt;/em&gt; a security tutorial, and these are bare minimum security measures. For a production app, consult a security specialist.&lt;/p&gt;
&lt;h4&gt;Create an SSH user&lt;/h4&gt;
&lt;p&gt;First, we&apos;re going to add a new user with &lt;code&gt;sudo&lt;/code&gt; privileges. To do this, run the following command while logged into your droplet:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# You can choose any username you want here.
adduser jason
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This command prompts us for a password, and then for some additional, optional details.&lt;/p&gt;
&lt;p&gt;Afterward, we can see that our user has been created by running &lt;code&gt;id &amp;lt;your_username&amp;gt;&lt;/code&gt;, which should output something like the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;root@nodejs-ssl-deploy:~# id jason
uid=1000(jason) gid=1000(jason) groups=1000(jason)
&lt;/code&gt;&lt;/pre&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; Make sure to save this password somewhere. It’s required for installing or modifying settings on the server later.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;In order to run some of the commands on the server, such as restarting services, we need to add our new user to the &lt;code&gt;sudo&lt;/code&gt; group. Do this by running the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Don&apos;t forget: use your own username here
usermod -aG sudo jason
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now if we run &lt;code&gt;id jason&lt;/code&gt; we can see the &lt;code&gt;sudo&lt;/code&gt; group has been applied.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;root@nodejs-ssl-deploy:~# id jason
uid=1000(jason) gid=1000(jason) groups=1000(jason),27(sudo)
&lt;/code&gt;&lt;/pre&gt;
&lt;aside&gt;&lt;p&gt;&lt;code&gt;sudo&lt;/code&gt; is short for &quot;superuser do&quot;. It&apos;s roughly equivalent to running a command as &lt;code&gt;root&lt;/code&gt;.&lt;/p&gt;&lt;/aside&gt;
&lt;h4&gt;Add your SSH key for the new user&lt;/h4&gt;
&lt;p&gt;Next, we need to add our SSH key to the new user. This allows us to log in without a password, which is important because we&apos;re planning to disable password logins for this server.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Become the new user
su - jason

# Create a new directory for SSH stuff
mkdir ~/.ssh

# Set the permissions to only allow this user into it
chmod 700 ~/.ssh

# Create a file for SSH keys
nano ~/.ssh/authorized_keys
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;nano&lt;/code&gt; editor allows us to copy-paste your SSH key — the same one we copied to DigitalOcean when we created the droplet — into the new file, then press &lt;code&gt;control + X&lt;/code&gt; to exit. Type &lt;code&gt;Y&lt;/code&gt; to save the file, and press &lt;code&gt;enter&lt;/code&gt; to confirm the file name.&lt;/p&gt;
&lt;p&gt;We can make sure the SSH key is saved by running &lt;code&gt;cat ~/.ssh/authorized_keys&lt;/code&gt;; if the SSH key is printed in the terminal, it&apos;s been saved.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Set the permissions to only allow this user to access it
chmod 600 ~/.ssh/authorized_keys

# Stop acting as the new user and become root again
exit
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Disable password login&lt;/h4&gt;
&lt;p&gt;Since every server has a default &lt;code&gt;root&lt;/code&gt; account that&apos;s a target for automated server attacks — and because that account has unlimited power inside the server — it&apos;s a good idea to make sure no one can use it.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;There are other security benefits as well; check out &lt;a href=&quot;http://security.stackexchange.com/questions/114721/why-is-disabling-root-necessary-for-security&quot;&gt;this discussion&lt;/a&gt; if you want to climb down the rabbit hole.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;After the previous step, you should be logged into your server as &lt;code&gt;root&lt;/code&gt;. Let&apos;s make sure the new account works and has &lt;code&gt;sudo&lt;/code&gt; access:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Log out of the server as root
exit

# Log into your server as the new user
ssh jason@138.68.11.65
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Inside, we need to update the SSH configuration to disable password logins, and to disable logging in as &lt;code&gt;root&lt;/code&gt; altogether.&lt;/p&gt;
&lt;p&gt;To do this, use the following command to open the SSH configuration file for editing:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo nano /etc/ssh/sshd_config
&lt;/code&gt;&lt;/pre&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; You will be asked for a password when you use the &lt;code&gt;sudo&lt;/code&gt; command. This is the password you used when you created the user earlier in this tutorial.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;Inside, you need to update two settings:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Find &lt;code&gt;PermitRootLogin yes&lt;/code&gt; and change it to &lt;code&gt;PermitRootLogin no&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Find &lt;code&gt;#PasswordAuthentication yes&lt;/code&gt; and change it to &lt;code&gt;PasswordAuthentication no&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;TIP:&lt;/strong&gt; There&apos;s a quick way in &lt;code&gt;nano&lt;/code&gt; to find those settings: press &lt;code&gt;control + W&lt;/code&gt; to search for text. Otherwise, you can just press the down arrow until you see the settings that need to change.&lt;/p&gt;&lt;/aside&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;IMPORTANT:&lt;/strong&gt; Make sure to remove the &lt;code&gt;#&lt;/code&gt; before &lt;code&gt;PasswordAuthentication&lt;/code&gt;; that&apos;s used to comment out the setting, which means it won&apos;t be applied.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;Save the file by pressing &lt;code&gt;control + X&lt;/code&gt;, then &lt;code&gt;Y&lt;/code&gt;, then &lt;code&gt;enter&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Finally, restart the SSH service with this command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Reloads the configuration we just changed
sudo systemctl reload sshd
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Test your login by opening a new tab in Terminal (&lt;code&gt;command + T&lt;/code&gt; on Mac) and logging into your server again.&lt;/p&gt;
&lt;p&gt;If we log in as our new user, everything works as expected. However, if we try to log in as &lt;code&gt;root&lt;/code&gt;, we get an error:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ ssh root@138.68.11.65
Permission denied (publickey).
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Set up a basic firewall&lt;/h4&gt;
&lt;p&gt;Next, we&apos;re going to configure a simple firewall. We&apos;re going to configure it to deny all traffic except through standard web traffic ports (&lt;code&gt;80&lt;/code&gt; for HTTP, and &lt;code&gt;443&lt;/code&gt; for HTTPS), and to allow SSH logins.&lt;/p&gt;
&lt;p&gt;This, in theory at least, should eliminate a lot of security risks on our server. (But again — this is &lt;em&gt;not&lt;/em&gt; a security article; these are just basic precautions.)&lt;/p&gt;
&lt;p&gt;We&apos;re going to run three commands to configure the firewall — called &lt;code&gt;ufw&lt;/code&gt; — and then we&apos;ll enable it. Enter the following while logged into the server:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Enable OpenSSH connections
sudo ufw allow OpenSSH

# Enable HTTP traffic
sudo ufw allow http

# Enable HTTPS traffic
sudo ufw allow https

# Turn the firewall on
sudo ufw enable
&lt;/code&gt;&lt;/pre&gt;
&lt;aside&gt;&lt;p&gt;When you enable &lt;code&gt;ufw&lt;/code&gt;, you&apos;ll get a notice that enabling the firewall might disrupt your connection. Don&apos;t worry about that — you&apos;ve enabled SSH connections.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;To check the status of the firewall, run &lt;code&gt;sudo ufw status&lt;/code&gt;, which will give you the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;jason@nodejs-ssl-deploy:~$ sudo ufw status
Status: active

To                         Action      From
--                         ------      ----
OpenSSH                    ALLOW       Anywhere
80                         ALLOW       Anywhere
443                        ALLOW       Anywhere
OpenSSH (v6)               ALLOW       Anywhere (v6)
80 (v6)                    ALLOW       Anywhere (v6)
443 (v6)                   ALLOW       Anywhere (v6)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Get Your App Up and Running&lt;/h2&gt;
&lt;p&gt;Now that the server is set up, we can get our app installed.&lt;/p&gt;
&lt;h3&gt;Install Git.&lt;/h3&gt;
&lt;p&gt;In order to get a copy of our app to this server, we&apos;re going to use &lt;a href=&quot;https://git-scm.com/&quot;&gt;Git&lt;/a&gt;. Fortunately, Ubuntu makes it really easy to install common tools, so all we need to do is run this command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get install git
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can validate that Git was installed properly by running &lt;code&gt;git --version&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;jason@nodejs-ssl-deploy:~$ git --version
git version 2.7.4
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Set up Node.js.&lt;/h3&gt;
&lt;p&gt;Node.js is a little more complex than Git, because there are several different versions of Node that are used in production environments. Therefore, we need to update &lt;code&gt;apt-get&lt;/code&gt; with the right version for our app before we install it.&lt;/p&gt;
&lt;h4&gt;Tell &lt;code&gt;apt-get&lt;/code&gt; which Node.js version to download&lt;/h4&gt;
&lt;p&gt;The folks at &lt;a href=&quot;https://github.com/nodesource/distributions&quot;&gt;NodeSource&lt;/a&gt; have made it really easy to install our desired Node version. For this tutorial, we&apos;ll be using the latest &lt;code&gt;6.x&lt;/code&gt; release.&lt;/p&gt;
&lt;p&gt;Run the following commands to download and execute the setup script:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash -
&lt;/code&gt;&lt;/pre&gt;
&lt;aside&gt;&lt;p&gt;This is a complex command, but essentially it uses &lt;code&gt;curl&lt;/code&gt; to download the setup script, then &lt;em&gt;pipes&lt;/em&gt; (using &lt;code&gt;|&lt;/code&gt;) the downloaded script to the next command, &lt;code&gt;bash&lt;/code&gt;, which actually executes it.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;This takes a few seconds to complete.&lt;/p&gt;
&lt;h4&gt;Install Node.js v6.x&lt;/h4&gt;
&lt;p&gt;With the NodeSource script complete, we can simply use &lt;code&gt;apt-get&lt;/code&gt; to install Node.js:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get install nodejs
&lt;/code&gt;&lt;/pre&gt;
&lt;aside&gt;&lt;p&gt;You may get a notice asking if you want to continue. Press &lt;code&gt;enter&lt;/code&gt; to continue the installation.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;Once it&apos;s complete, we can verify that &lt;code&gt;node&lt;/code&gt; is available by running &lt;code&gt;node --version&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;jason@nodejs-ssl-deploy:~$ node --version
v6.3.1
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Clone the app&lt;/h4&gt;
&lt;p&gt;Now we can actually clone a copy of our app to the server — things are really getting exciting now.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; If you don&apos;t have a Node app ready to deploy, you can use the &lt;a href=&quot;https://github.com/jlengstorf/tutorial-deploy-nodejs-ssl-digitalocean-app&quot;&gt;simple demo app&lt;/a&gt; that was created for this tutorial.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;It doesn&apos;t matter where you install the app, so let&apos;s create an &lt;code&gt;apps&lt;/code&gt; dir in our user&apos;s home folder and clone the app into a folder named after our domain — this makes it really easy to remember which app is which.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Make sure you’re in your home folder
cd ~

# Create the new directory and move into it
mkdir apps
cd apps/

# Clone your app into a new directory named for your domain
git clone https://github.com/jlengstorf/tutorial-deploy-nodejs-ssl-digitalocean-app.git app.example.com
&lt;/code&gt;&lt;/pre&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; Make sure to replace &lt;code&gt;app.example.com&lt;/code&gt; with your desired domain name.&lt;/p&gt;&lt;/aside&gt;
&lt;h4&gt;Test the app&lt;/h4&gt;
&lt;p&gt;To make sure your app is installed and working, move into the new folder and start it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Move into the app directory
cd app.example.com

# Start the app
node app
&lt;/code&gt;&lt;/pre&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; The example app&apos;s main file is at &lt;code&gt;app/index.js&lt;/code&gt;. You&apos;ll need to adjust your start command depending on how your app is set up.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;The example app listens at &lt;code&gt;http://localhost:5000&lt;/code&gt;, so we can test if it&apos;s working by opening a new Terminal session, logging into our server, and making a &lt;code&gt;curl&lt;/code&gt; request to the app.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;jason@nodejs-ssl-deploy:~$ curl http://localhost:5000/
&amp;lt;h1&amp;gt;I’m a Node app!&amp;lt;/h1&amp;gt;&amp;lt;p&amp;gt;And I’m &amp;lt;em&amp;gt;sooooo&amp;lt;/em&amp;gt; secure.&amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;aside&gt;&lt;p&gt;Since the app is running on &lt;code&gt;localhost&lt;/code&gt;, it can only be reached from the server itself. We&apos;ll correct that later on.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;Awesome — we have a running app. Now we just need to make it accessible to the outside world.&lt;/p&gt;
&lt;p&gt;We can &lt;code&gt;exit&lt;/code&gt; from the test session (the one we just ran the &lt;code&gt;curl&lt;/code&gt; command in), and we can stop the app in our other session using &lt;code&gt;control + C&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Start Your App Using a Process Manager&lt;/h2&gt;
&lt;p&gt;Simply starting the app manually is &lt;em&gt;technically&lt;/em&gt; enough to get the app deployed, but if the server restarts, that means we have to manually start the app again.&lt;/p&gt;
&lt;p&gt;And in production apps, we want to eliminate as many — if not all — manual steps to get the app deployed. So we&apos;re going to use a process manager called &lt;a href=&quot;http://pm2.keymetrics.io/&quot;&gt;PM2&lt;/a&gt; to run our app. This also gives us benefits like easy-to-access logs, and a simple way to start, stop, and restart the app.&lt;/p&gt;
&lt;p&gt;PM2 also allows us to start the app automatically when the server restarts, which means one less thing we need to worry about.&lt;/p&gt;
&lt;h3&gt;Install PM2&lt;/h3&gt;
&lt;p&gt;Unlike the other tools we&apos;ve installed, &lt;code&gt;pm2&lt;/code&gt; is a Node package. We install it using the &lt;code&gt;npm&lt;/code&gt; command, which is the default package manager for Node.js.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo npm install -g pm2
&lt;/code&gt;&lt;/pre&gt;
&lt;aside&gt;&lt;p&gt;Using &lt;code&gt;-g&lt;/code&gt; means that &lt;code&gt;pm2&lt;/code&gt; is available globally, which is necessary for it to work properly.&lt;/p&gt;&lt;/aside&gt;
&lt;h3&gt;Start your app using PM2&lt;/h3&gt;
&lt;p&gt;With PM2 installed, we can now start the app like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Make sure you&apos;re in the app directory
cd ~/apps/app.example.com

# Start the app with PM2
pm2 start app
&lt;/code&gt;&lt;/pre&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; You call &lt;code&gt;pm2 start&lt;/code&gt; with the same argument you&apos;d use to start the app with &lt;code&gt;node&lt;/code&gt;. If you start the app with &lt;code&gt;node index.js&lt;/code&gt;, you&apos;d run &lt;code&gt;pm2 start index.js&lt;/code&gt; here.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;Once the app is started, we see the status displayed:&lt;/p&gt;
&lt;p&gt;And, conveniently, the app is running without locking up our session. In the same session we&apos;re able to run &lt;code&gt;curl http://localhost:5000&lt;/code&gt; to make sure it&apos;s running properly:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;jason@nodejs-ssl-deploy:~/apps/app.example.com$ curl http://localhost:5000/

&amp;lt;h1&amp;gt;I’m a Node app!&amp;lt;/h1&amp;gt;&amp;lt;p&amp;gt;And I’m &amp;lt;em&amp;gt;sooooo&amp;lt;/em&amp;gt; secure.&amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Start your app automatically when the server restarts&lt;/h3&gt;
&lt;p&gt;We&apos;re &lt;em&gt;almost&lt;/em&gt; done with getting the app running — just one more step.&lt;/p&gt;
&lt;p&gt;The last thing to do is to make sure that when the server restarts, PM2 starts our app again.&lt;/p&gt;
&lt;p&gt;This is a two-step process, which we kick off by running &lt;code&gt;pm2 startup systemd&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;jason@nodejs-ssl-deploy:~/app.example.com$ pm2 startup
[PM2] You have to run this command as root. Execute the following command:
sudo su -c &quot;env PATH=$PATH:/usr/bin pm2 startup systemd -u jason --hp /home/jason&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;PM2 prints out a command that we need to run using &lt;code&gt;sudo&lt;/code&gt;. Copy-paste that to finish the process.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo su -c &quot;env PATH=$PATH:/usr/bin pm2 startup systemd -u jason --hp /home/jason&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will step through the process of updating the server to run PM2 on startup, and then you&apos;re all set.&lt;/p&gt;
&lt;p&gt;Now we&apos;ve got a running app — in the next section, we&apos;ll make the app securely accessible to the rest of the world!&lt;/p&gt;
&lt;h2&gt;Get a Free SSL Certificate With Let&apos;s Encrypt&lt;/h2&gt;
&lt;p&gt;SSL was a big hurdle for a long time for two reasons:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;It was expensive.&lt;/li&gt;
&lt;li&gt;It was hard.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Fortunately, some very smart, very kind-hearted people created &lt;a href=&quot;https://letsencrypt.org/&quot;&gt;Let&apos;s Encrypt&lt;/a&gt;, which is:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Free&lt;/li&gt;
&lt;li&gt;Easy&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;So now there&apos;s really no excuse not to set up SSL for our domains.&lt;/p&gt;
&lt;h3&gt;Install Let&apos;s Encrypt&lt;/h3&gt;
&lt;p&gt;To start, we need to install some tools that Let&apos;s Encrypt depends on, then clone the &lt;code&gt;letsencrypt&lt;/code&gt; repository to our server.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Install tools that Let’s Encrypt requires
sudo apt-get install bc

# Clone the Let’s Encrypt repository to your server
sudo git clone https://github.com/letsencrypt/letsencrypt /opt/letsencrypt
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Configure your domain to point to the server&lt;/h3&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;IMPORTANT:&lt;/strong&gt; This has to be done before Let&apos;s Encrypt will create a certificate. If your DNS isn&apos;t set up correctly, Let’s Encrypt will complain and then fail.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;Log into your DNS provider. I use CloudFlare; you&apos;ll want to log into whichever service you either bought your domain name through (Namecheap and GoDaddy, for example), or the service you use to manage your DNS (such as CloudFlare or DNSimple).&lt;/p&gt;
&lt;p&gt;Add an A record for your domain that points to your droplet&apos;s IP address.&lt;/p&gt;
&lt;p&gt;To check that the domain is pointing to your droplet, run the following (make sure to replace &lt;code&gt;app.example.com&lt;/code&gt; with the domain you just configured):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dig +short app.example.com
# output should be your droplet’s IP address, e.g. 138.68.11.65
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Generate the SSL certificate.&lt;/h3&gt;
&lt;p&gt;Now that the domain is pointed to our server, we can generate the SSL certificate:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Move into the Let’s Encrypt directory
cd /opt/letsencrypt

# Create the SSL certificate
./certbot-auto certonly --standalone
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The tool will run for a while to initialize itself, and then we&apos;ll be asked for an admin email address, to agree to the terms, and to specify our domain name or names. Once that&apos;s done, the certificate will be stored on the server for use with our app.&lt;/p&gt;
&lt;p&gt;For now, that&apos;s all we need. We&apos;ll come back to these in a minute when we configure the domain.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; If you want to support multiple subdomains (e.g. &lt;code&gt;example.com&lt;/code&gt; and &lt;code&gt;www.example.com&lt;/code&gt;), you&apos;ll need to specify them during the setup as a comma-separated list: &lt;code&gt;example.com,www.example.com&lt;/code&gt;&lt;/p&gt;&lt;/aside&gt;
&lt;h3&gt;Setup auto-renewal for the SSL certificate&lt;/h3&gt;
&lt;p&gt;For security, Let’s Encrypt certificates expire every 90 days, which seems pretty short. (By contrast, most paid SSL certificates are valid for at least a year.)&lt;/p&gt;
&lt;p&gt;It turns out, though, that Let’s Encrypt has an one-step command to renew certificates:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/opt/letsencrypt/certbot-auto renew
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This command checks if the certificate is near its expiration date and, when necessary, it generates an updated certificate that&apos;s good for another 90 days.&lt;/p&gt;
&lt;p&gt;Now, it would be a &lt;em&gt;huge&lt;/em&gt; pain in the ass if we had to manually log into the server and renew the certificate four times a year — and most likely we&apos;d end up forgetting at least once — so we&apos;re going to use a built-in tool called &lt;a href=&quot;http://www.unixgeeks.org/security/newbie/unix/cron-1.html&quot;&gt;&lt;code&gt;cron&lt;/code&gt;&lt;/a&gt; to handle the renewal automatically.&lt;/p&gt;
&lt;p&gt;To set this up, run the following command in the terminal to edit the server&apos;s &lt;code&gt;cron&lt;/code&gt; jobs:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo crontab -e
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We get an option for which editor to use here. Since &lt;code&gt;nano&lt;/code&gt; is easier than the others, we&apos;ll stick with that.&lt;/p&gt;
&lt;p&gt;When the editor opens, head to the bottom of the file and add the following two lines:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;00 1 * * 1 /opt/letsencrypt/certbot-auto renew &amp;gt;&amp;gt; /var/log/letsencrypt-renewal.log
30 1 * * 1 /bin/systemctl reload nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first line tells &lt;code&gt;cron&lt;/code&gt; to run the renewal command, with the output logged so we can check on it when necessary, every Monday at 1 in the morning.&lt;/p&gt;
&lt;p&gt;The second restarts NGINX — which we haven&apos;t set up yet, so don&apos;t worry — at 1:30 to make sure the new cert is being used.&lt;/p&gt;
&lt;p&gt;Save and exit by pressing &lt;code&gt;control + X&lt;/code&gt;, then &lt;code&gt;Y&lt;/code&gt;, then &lt;code&gt;enter&lt;/code&gt;.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; The command to reload &lt;code&gt;nginx&lt;/code&gt; doesn&apos;t do anything right now. We&apos;ll be installing and configuring NGINX in the next section.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;That&apos;s it for the SSL cert. The last thing left is to make your app accessible by visiting our domain name in a browser.&lt;/p&gt;
&lt;h2&gt;Point Your Domain to the App&lt;/h2&gt;
&lt;p&gt;In order to make our app accessible, we need to send visitors to our domain to our app. To do this, we&apos;ll be using &lt;a href=&quot;https://www.nginx.com/&quot;&gt;NGINX&lt;/a&gt; as a &lt;a href=&quot;https://en.wikipedia.org/wiki/Reverse_proxy&quot;&gt;reverse proxy&lt;/a&gt; because it&apos;s faster and less painful than handling it through Node.js.&lt;/p&gt;
&lt;h3&gt;Install NGINX&lt;/h3&gt;
&lt;p&gt;Installing NGINX is no different from most of the other tools we&apos;ve downloaded so far. Use &lt;code&gt;apt-get&lt;/code&gt; to download and install it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get install nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Make sure all traffic is secure&lt;/h4&gt;
&lt;p&gt;Next, we need to make sure that all traffic is served over SSL. To do this, we&apos;ll add a redirect for any non-SSL traffic to the SSL version. That way, if someone visits &lt;code&gt;http://app.example.com&lt;/code&gt;, they&apos;ll be automatically redirected to &lt;code&gt;https://app.example.com&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;To do this, we need to edit NGINX&apos;s configuration files. Run the following command to open the file for editing:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo nano /etc/nginx/sites-enabled/default
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Inside, delete everything and add the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# HTTP — redirect all traffic to HTTPS
server {
    listen 80;
    listen [::]:80 default_server ipv6only=on;
    return 301 https://$host$request_uri;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Save and exit by pressing &lt;code&gt;control + X&lt;/code&gt;, then &lt;code&gt;Y&lt;/code&gt;, then &lt;code&gt;enter&lt;/code&gt;.&lt;/p&gt;
&lt;h4&gt;Create a secure Diffie-Hellman Group&lt;/h4&gt;
&lt;p&gt;It only takes a couple extra minutes to create a &lt;em&gt;really&lt;/em&gt; secure SSL setup, so we might as well do it. One of the ways to do that is to use a strong &lt;a href=&quot;https://supportforums.cisco.com/document/6211/diffie-hellman-dh&quot;&gt;Diffie-Hellman group&lt;/a&gt;, which helps ensure that our secure app &lt;em&gt;stays&lt;/em&gt; secure.&lt;/p&gt;
&lt;p&gt;Run the following command on your server:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo openssl dhparam -out /etc/ssl/certs/dhparam.pem 2048
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This takes a minute or two — encryption should be hard for computers — and when it&apos;s done we can move on for now. We&apos;ll use this file in the next section.&lt;/p&gt;
&lt;h4&gt;Create a configuration file for SSL&lt;/h4&gt;
&lt;p&gt;Since I&apos;m not a security expert, we&apos;re going to defer to an &lt;a href=&quot;https://raymii.org/s/&quot;&gt;actual security expert&lt;/a&gt; for NGINX&apos;s SSL settings.&lt;/p&gt;
&lt;p&gt;We need to create a new file on our server to hold these settings — if we add another domain to this server, we can reuse them this way — which we&apos;ll do with the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo nano /etc/nginx/snippets/ssl-params.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Inside, we can copy-paste the following settings.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# See https://cipherli.st/ for details on this configuration
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ciphers &quot;EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH&quot;;
ssl_ecdh_curve secp384r1; # Requires nginx &amp;gt;= 1.1.0
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off; # Requires nginx &amp;gt;= 1.5.9
ssl_stapling on; # Requires nginx &amp;gt;= 1.3.7
ssl_stapling_verify on; # Requires nginx =&amp;gt; 1.3.7
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
add_header Strict-Transport-Security &quot;max-age=63072000; includeSubDomains; preload&quot;;
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;

# Add our strong Diffie-Hellman group
ssl_dhparam /etc/ssl/certs/dhparam.pem;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Save and exit by pressing &lt;code&gt;control + X&lt;/code&gt;, then &lt;code&gt;Y&lt;/code&gt;, then &lt;code&gt;enter&lt;/code&gt;.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; The &lt;code&gt;resolver&lt;/code&gt; parameter is set to Google&apos;s DNS resolvers. This is part of &lt;a href=&quot;https://en.wikipedia.org/wiki/OCSP_stapling&quot;&gt;OCSP stapling&lt;/a&gt;, which is a way to speed up certificate validation. If you have your own DNS resolver, you can update that value.&lt;/p&gt;&lt;/aside&gt;
&lt;h4&gt;Configure your domain to use SSL&lt;/h4&gt;
&lt;p&gt;This is the last configuration step, I promise.&lt;/p&gt;
&lt;p&gt;Now that we&apos;ve got a certificate, a strong Diffie-Hellman group, and a secure SSL configuration, all that&apos;s left to do is actually set up the reverse proxy.&lt;/p&gt;
&lt;p&gt;Open the site configuration again:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo nano /etc/nginx/sites-enabled/default
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Inside, add the following below the block we added earlier:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# HTTPS — proxy all requests to the Node app
server {
    # Enable HTTP/2
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name app.example.com;

    # Use the Let’s Encrypt certificates
    ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;

    # Include the SSL configuration from cipherli.st
    include snippets/ssl-params.conf;

    location / {
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-NginX-Proxy true;
        proxy_pass http://localhost:5000/;
        proxy_ssl_session_reuse off;
        proxy_set_header Host $http_host;
        proxy_cache_bypass $http_upgrade;
        proxy_redirect off;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Save and exit by pressing &lt;code&gt;control + X&lt;/code&gt;, then &lt;code&gt;Y&lt;/code&gt;, then &lt;code&gt;enter&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This configuration listens for connections to our domain on port &lt;code&gt;443&lt;/code&gt; (the HTTPS port), uses the certificate we generated to secure the connection, and then proxies our app&apos;s output out to the browser.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;IMPORTANT:&lt;/strong&gt; Don&apos;t forget to replace all instances of &lt;code&gt;app.example.com&lt;/code&gt; in the configuration details above with your domain name!&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;Before we start the server, we should test the NGINX configuration with &lt;code&gt;sudo nginx -t&lt;/code&gt;. If we didn&apos;t make any typos and everything looks good, we&apos;ll get the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;jason@nodejs-ssl-deploy:~$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Enable NGINX&lt;/h4&gt;
&lt;p&gt;The very last step in this process is to start NGINX.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl start nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Test Your App&lt;/h2&gt;
&lt;p&gt;And now: the big moment. We can now visit our domain in a browser, and we&apos;ll see our app.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; In some cases, you may see the &quot;Welcome to nginx!&quot; screen instead of your app. If that happens, restart Nginx with &lt;code&gt;sudo systemctl restart nginx&lt;/code&gt;, then hard refresh your browser (&lt;code&gt;command&lt;/code&gt; + &lt;code&gt;shift&lt;/code&gt; + &lt;code&gt;R&lt;/code&gt; on Mac) to see the live app.&lt;/p&gt;&lt;/aside&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;/images/nodejs-ssl-deploy-07.jpg&quot; alt=&quot;Working app with green SSL verification in the address bar.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;The configuration steps get pretty mind-numbing toward the end, but there&apos;s a huge payoff: we can now bask in the glory of a server that took about 30 minutes to set up, costs $5/month, and — as a bonus — gets &lt;a href=&quot;https://www.ssllabs.com/ssltest/index.html&quot;&gt;an A+ for SSL security&lt;/a&gt;.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;img src=&quot;/images/nodejs-ssl-deploy-08.jpg&quot; alt=&quot;A+ rating on Qualys SSL Labs report.&quot; /&gt;&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;Not bad for 30 minutes&apos; worth of setup, right?&lt;/p&gt;
&lt;h2&gt;Additional Resources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.digitalocean.com/community/tutorials/ufw-essentials-common-firewall-rules-and-commands&quot;&gt;Common firewall rules and commands&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://whatididtodowhatidid.wordpress.com/2014/03/14/subdomains-for-ports-on-same-ubuntu-server-with-nginx-reverse-proxy/&quot;&gt;How to configure NGINX as a reverse proxy for subdomains&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.digitalocean.com/community/tutorials/initial-server-setup-with-ubuntu-16-04&quot;&gt;Setting up a Ubuntu 16.04 server with a new user and a basic firewall&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.digitalocean.com/community/tutorials/how-to-set-up-a-node-js-application-for-production-on-ubuntu-16-04&quot;&gt;DigitalOcean&apos;s guide to setting up a Node.js app for production&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-16-04&quot;&gt;Securing NGINX with Let&apos;s Encrypt&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>How to Convert HTML Form Field Values to a JSON Object</title><link>https://codetv.dev/blog/get-form-values-as-json/</link><guid isPermaLink="true">https://codetv.dev/blog/get-form-values-as-json/</guid><description>Use built-in browser APIs to get form values as JSON. Zero dependencies and only a few lines of code!
</description><pubDate>Sun, 07 Aug 2016 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Getting form values as a JSON object can be a little confusing, but there’s good news! Browsers have implemented a built-in API for getting form values that makes this straightforward and approachable!&lt;/p&gt;
&lt;h2&gt;Use the FormData API to access form values in JavaScript&lt;/h2&gt;
&lt;p&gt;Before I learned about the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/FormData&quot;&gt;FormData API&lt;/a&gt;, I thought accessing form values in JavaScript was a pain. But after &lt;a href=&quot;https://twitter.com/noopkat&quot;&gt;Suz Hinton&lt;/a&gt; made me aware of it, that all changed.&lt;/p&gt;
&lt;p&gt;In a nutshell, the FormData API lets us access any field value in a submitted form using a straightforward API.&lt;/p&gt;
&lt;p&gt;For a quick example, let&apos;s assume we have this form:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;form&amp;gt;
  &amp;lt;label for=&quot;email&quot;&amp;gt;Email&amp;lt;/label&amp;gt;
  &amp;lt;input type=&quot;email&quot; name=&quot;email&quot; id=&quot;email&quot; /&amp;gt;

  &amp;lt;button type=&quot;submit&quot;&amp;gt;Submit&amp;lt;/button&amp;gt;
&amp;lt;/form&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To handle submissions in JavaScript, we can use the FormData API like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function handleSubmit(event) {
  event.preventDefault();

  const data = new FormData(event.target);

  const value = data.get(&apos;email&apos;);

  console.log({ value });
}

const form = document.querySelector(&apos;form&apos;);
form.addEventListener(&apos;submit&apos;, handleSubmit);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If we run this code and submit the form, the value we enter into the email input will be logged. I don’t know about you, but the first time I tried this I wept happy tears — this is &lt;em&gt;so much simpler&lt;/em&gt; than what I used to do! (My previous, more complicated approach is still at the bottom of this article if you want to compare.)&lt;/p&gt;
&lt;h2&gt;How to get all values from a form as a JSON object using the FormData API&lt;/h2&gt;
&lt;p&gt;If we want to get &lt;em&gt;all&lt;/em&gt; of the values from a form, there’s an extra step. Let’s expand our form with a name field:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;form&amp;gt;
  &amp;lt;label for=&quot;name&quot;&amp;gt;Name&amp;lt;/label&amp;gt;
  &amp;lt;input type=&quot;text&quot; name=&quot;name&quot; id=&quot;name&quot; /&amp;gt;

  &amp;lt;label for=&quot;email&quot;&amp;gt;Email&amp;lt;/label&amp;gt;
  &amp;lt;input type=&quot;email&quot; name=&quot;email&quot; id=&quot;email&quot; /&amp;gt;

  &amp;lt;button type=&quot;submit&quot;&amp;gt;Submit&amp;lt;/button&amp;gt;
&amp;lt;/form&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To access all entries,&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function handleSubmit(event) {
  event.preventDefault();

  const data = new FormData(event.target);

  // Do a bit of work to convert the entries to a plain JS object
  const value = Object.fromEntries(data.entries());

  console.log({ value });
}

const form = document.querySelector(&apos;form&apos;);
form.addEventListener(&apos;submit&apos;, handleSubmit);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The FormData API doesn’t directly convert form values to JSON, but we can get there by using the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/FormData/entries&quot;&gt;&lt;code&gt;entries&lt;/code&gt;&lt;/a&gt; method and passing its return value to &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/fromEntries&quot;&gt;&lt;code&gt;Object.fromEntries&lt;/code&gt;&lt;/a&gt;, which returns a plain JavaScript object.&lt;/p&gt;
&lt;p&gt;This is compatible with &lt;code&gt;JSON.stringify&lt;/code&gt;, so we can use it for sending JSON-encoded data to APIs or any other thing we might want to do with a JavaScript object.&lt;/p&gt;
&lt;h2&gt;Get multi-select values like checkboxes as JSON with the FormData API&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;fromEntries&lt;/code&gt; approach in the previous section works great for most form inputs, but if the input allows multiple values — such as a checkbox — we&apos;d only see one value in the resulting object.&lt;/p&gt;
&lt;p&gt;Fortunately, the workaround for this only requires one more line of JavaScript for each multi-value input.&lt;/p&gt;
&lt;p&gt;Let&apos;s add a field with multiple potential values to our form:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;form&amp;gt;
  &amp;lt;label for=&quot;name&quot;&amp;gt;Name&amp;lt;/label&amp;gt;
  &amp;lt;input type=&quot;text&quot; name=&quot;name&quot; id=&quot;name&quot; /&amp;gt;

  &amp;lt;label for=&quot;email&quot;&amp;gt;Email&amp;lt;/label&amp;gt;
  &amp;lt;input type=&quot;email&quot; name=&quot;email&quot; id=&quot;email&quot; /&amp;gt;

  &amp;lt;p&amp;gt;
    &amp;lt;input type=&quot;checkbox&quot; name=&quot;topics&quot; id=&quot;javascript&quot; value=&quot;javascript&quot; /&amp;gt;
    &amp;lt;label for=&quot;javascript&quot;&amp;gt;JavaScript&amp;lt;/label&amp;gt;

    &amp;lt;input type=&quot;checkbox&quot; name=&quot;topics&quot; id=&quot;html&quot; value=&quot;html&quot; /&amp;gt;
    &amp;lt;label for=&quot;html&quot;&amp;gt;HTML&amp;lt;/label&amp;gt;

    &amp;lt;input type=&quot;checkbox&quot; name=&quot;topics&quot; id=&quot;css&quot; value=&quot;css&quot; /&amp;gt;
    &amp;lt;label for=&quot;css&quot;&amp;gt;CSS&amp;lt;/label&amp;gt;
  &amp;lt;/p&amp;gt;

  &amp;lt;button type=&quot;submit&quot;&amp;gt;Submit&amp;lt;/button&amp;gt;
&amp;lt;/form&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Getting all the topics requires using the &lt;code&gt;.getAll()&lt;/code&gt; method:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function handleSubmit(event) {
  event.preventDefault();

  const data = new FormData(event.target);

  const value = Object.fromEntries(data.entries());

  value.topics = data.getAll(&quot;topics&quot;);

  console.log({ value });
}

const form = document.querySelector(&quot;form&quot;);
form.addEventListener(&quot;submit&quot;, handleSubmit);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the object contains an array in &lt;code&gt;topics&lt;/code&gt; that contains all checked values!&lt;/p&gt;
&lt;h2&gt;A full example of multiple input types with the FormData API&lt;/h2&gt;
&lt;p&gt;For a full example of using the FormData API with lots of different input types, check out &lt;a href=&quot;https://codepen.io/jlengstorf/pen/rNMpJNy&quot;&gt;this CodePen&lt;/a&gt; (the form values get printed below the form on submission).&lt;/p&gt;
&lt;p&gt;CodePen: &lt;a href=&quot;https://codepen.io/jlengstorf/pen/rNMpJNy&quot;&gt;[UPDATED] How to Get Form Field Data as JSON Using Plain JavaScript&lt;/a&gt;&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;Heads up!&lt;/strong&gt; The original version of this article used a more manual approach to getting form values. The FormData API makes this largely obsolete, but I’ve included the original article below so you can see how the approach has evolved.&lt;/p&gt;&lt;/aside&gt;
&lt;h2&gt;Original Article Text&lt;/h2&gt;
&lt;p&gt;Using AJAX is really common, but it’s still tricky to get the values out of a form without using a library.&lt;/p&gt;
&lt;p&gt;And that’s because it seems pretty intimidating to set up all the loops and checks required to deal with parsing a form and all its child elements. You get into &lt;a href=&quot;http://stackoverflow.com/a/9329476/463471&quot;&gt;heavy discussions of whether you should use &lt;code&gt;for&lt;/code&gt;, &lt;code&gt;for...in&lt;/code&gt;, &lt;code&gt;for...of&lt;/code&gt;, or &lt;code&gt;forEach&lt;/code&gt;&lt;/a&gt;, and after trying to keep up with the various performance, semantic, and stylistic reasons for making those choices, your brain starts to liquefy and drip out your ears — at which point it’s easy to just say, &quot;Forget it; let’s just use jQuery.&quot;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;But for simple sites that don’t need much beyond grabbing form data as an object to use with JSON&lt;/strong&gt;, jQuery (or any big library or framework) includes a lot of overhead for only one or two functions that you’ll be using.&lt;/p&gt;
&lt;p&gt;(Even if it’s not something we’d ever use in production, writing our own utility scripts is a fantastic way to increase our understanding of how things work. If we rely too much on a tool’s &quot;magic&quot; to make our apps work, it becomes &lt;em&gt;really&lt;/em&gt; hard to debug them when we find a problem that falls outside of the tool’s scope.)&lt;/p&gt;
&lt;p&gt;So in this walkthrough, we’ll be writing our own script — in plain JavaScript — to pull the values of a form’s fields into an object, which we could then use for AJAX, updating information on other parts of the page, and anything else you might want to do with a form’s data.&lt;/p&gt;
&lt;h3&gt;What We’ll Be Building&lt;/h3&gt;
&lt;p&gt;At the end of this walkthrough, we’ll have built the form shown in this pen:&lt;/p&gt;
&lt;p&gt;CodePen: &lt;a href=&quot;https://codepen.io/jlengstorf/pen/YWJLwz&quot;&gt;Finished code on Codepen.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If you fill the form and hit the “Send It!” button, the form data will be output as JSON in the “Form Data” section below.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; To show that &lt;code&gt;hidden&lt;/code&gt; inputs will be included, an input called &lt;code&gt;secret&lt;/code&gt; has been included in this form. Its value is a random GUID.&lt;/p&gt;&lt;/aside&gt;
&lt;h3&gt;Before We Get Started: Goals and Plans&lt;/h3&gt;
&lt;p&gt;To save ourselves a lot of headache and heartache, we’re going to &lt;a href=&quot;https://lengstorf.com/effective-project-planning/&quot;&gt;start our project with an clear plan&lt;/a&gt;. This’ll keep our goals clear, and helps define the structure and purpose of the code before we ever write a line.&lt;/p&gt;
&lt;h4&gt;Start with a goal: what should we end up with?&lt;/h4&gt;
&lt;p&gt;Before we write any JavaScript, let’s start by deciding how we want the output to look.&lt;/p&gt;
&lt;p&gt;If I’ve filled out the form above completely, we’d want the resulting object to look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;salutation&quot;: &quot;Mr.&quot;,
  &quot;name&quot;: &quot;Jason Lengstorf&quot;,
  &quot;email&quot;: &quot;jason@lengstorf.com&quot;,
  &quot;subject&quot;: &quot;I have a general question.&quot;,
  &quot;message&quot;: &quot;Is this thing on?&quot;,
  &quot;snacks&quot;: [&quot;pizza&quot;],
  &quot;secret&quot;: &quot;1b3a9374-1a8e-434e-90ab-21aa7b9b80e7&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each field’s &lt;code&gt;name&lt;/code&gt; attribute is used as the object’s key, and the field’s &lt;code&gt;value&lt;/code&gt; is set as the object’s value.&lt;/p&gt;
&lt;p&gt;This is ideal, because it means that we can do something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Find our form in the DOM using its class name.
const form = document.getElementByClassName(&apos;.contact-form&apos;)[0];

// Get the form data with our (yet to be defined) function.
const data = getFormDataAsJSON(form);

// Do something with the email address.
doSomething(data.email);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is straightforward, easy to read as a human, and also easy to send to APIs that accept &lt;code&gt;application/json&lt;/code&gt; data in requests (which is most of them these days).&lt;/p&gt;
&lt;p&gt;So let’s shoot for that.&lt;/p&gt;
&lt;h4&gt;Make a plan: how can we convert form fields to JSON?&lt;/h4&gt;
&lt;p&gt;When we’re finished, our JavaScript should accomplish the following goals:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Capture the form’s &lt;code&gt;submit&lt;/code&gt; event and prevent the default submission.&lt;/li&gt;
&lt;li&gt;Convert the form’s child elements to JSON.&lt;/li&gt;
&lt;li&gt;Check to make sure only form field elements are added to the object.&lt;/li&gt;
&lt;li&gt;Add a safeguard to only store checkable fields if the &lt;code&gt;checked&lt;/code&gt; attribute is set.&lt;/li&gt;
&lt;li&gt;Handle inputs that allow multiple values, like checkboxes.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Ready to flex that big-ass brain of yours?&lt;/strong&gt; Create a fork of the &lt;a href=&quot;http://codepen.io/jlengstorf/pen/GqYZra&quot;&gt;markup-and-styles-only pen&lt;/a&gt;, and let’s jump in and start writing some JavaScript.&lt;/p&gt;
&lt;h3&gt;Getting Started: Create a Form for Testing&lt;/h3&gt;
&lt;p&gt;To avoid the hassle of setting up front-end tooling (we’re using Babel to transpile the newer features of JavaScript, such as &lt;a href=&quot;https://github.com/getify/You-Dont-Know-JS/blob/master/scope%20%26%20closures/apC.md#appendix-c-lexical-this&quot;&gt;fat-arrow functions&lt;/a&gt;), we’re going to work through this project on Codepen.&lt;/p&gt;
&lt;p&gt;To start, create a fork of &lt;a href=&quot;http://codepen.io/jlengstorf/pen/GqYZra/&quot;&gt;this pen&lt;/a&gt;, which contains form markup with common inputs, and some styles to make it display nicely.&lt;/p&gt;
&lt;p&gt;CodePen: &lt;a href=&quot;https://codepen.io/jlengstorf/pen/GqYZra&quot;&gt;Starting code on Codepen.&lt;/a&gt;&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; The markup is written in Jade (which was recently renamed to &lt;a href=&quot;https://github.com/pugjs/pug&quot;&gt;Pug&lt;/a&gt;) because I find it faster and easier to read. If you prefer to look at plain HTML, you can hit the &quot;view compiled&quot; button at the bottom right of the Jade pane in the pen above.&lt;/p&gt;&lt;/aside&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; The styles for the form use &lt;a href=&quot;http://getbem.com/naming/&quot;&gt;BEM-style naming conventions&lt;/a&gt;, and I’m using &lt;a href=&quot;http://postcss.org/&quot;&gt;PostCSS&lt;/a&gt; to make it easy to group my styles together without &lt;em&gt;actually&lt;/em&gt; creating nested CSS. For me, this is &lt;em&gt;far&lt;/em&gt; easier to read than other ways of writing CSS, but if you prefer regular CSS, click the &quot;view compiled&quot; button at the bottom-right of the PostCSS pane in the pen above.&lt;/p&gt;&lt;/aside&gt;
&lt;h3&gt;Step 1: Add a Listener to the &lt;code&gt;submit&lt;/code&gt; Event for the Form&lt;/h3&gt;
&lt;p&gt;Before we do anything else, we need to listen for the &lt;code&gt;submit&lt;/code&gt; event on our form, and prevent it from doing its usual thing.&lt;/p&gt;
&lt;p&gt;To do this, let’s create a function called &lt;code&gt;handleSubmit()&lt;/code&gt;, then use &lt;code&gt;getElementsByClassName()&lt;/code&gt; to find our form, and attach the function to the form’s &lt;code&gt;submit&lt;/code&gt; event.&lt;/p&gt;
&lt;h4&gt;Create a &lt;code&gt;handleSubmit()&lt;/code&gt; function.&lt;/h4&gt;
&lt;p&gt;At the moment, this function isn’t going to do much. To start, we’ll prevent the default &lt;code&gt;submit&lt;/code&gt; action, create a variable called &lt;code&gt;data&lt;/code&gt; to store the output (which we’ll be building in a moment), then find our output container and print out the &lt;code&gt;data&lt;/code&gt; variable as JSON.&lt;/p&gt;
&lt;p&gt;In order to prevent the default action, this function needs to accept one argument: the &lt;code&gt;event&lt;/code&gt; that’s created when the user clicks the submit button on the form. We can stop the form from submitting the usual way (which triggers the browser to go somewhere else) using &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault&quot;&gt;&lt;code&gt;event.preventDefault()&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * A handler function to prevent default submission and run our custom script.
 * @param  {Event} event  the submit event triggered by the user
 * @return {void}
 */
const handleFormSubmit = (event) =&amp;gt; {
  // Stop the form from submitting since we’re handling that with AJAX.
  event.preventDefault();

  // TODO: Call our function to get the form data.
  const data = {};

  // Demo only: print the form data onscreen as a formatted JSON object.
  const dataContainer = document.getElementsByClassName(&apos;results__display&apos;)[0];

  // Use `JSON.stringify()` to make the output valid, human-readable JSON.
  dataContainer.textContent = JSON.stringify(data, null, &apos;  &apos;);

  // ...this is where we’d actually do something with the form data...
};
&lt;/code&gt;&lt;/pre&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; The &lt;code&gt;data&lt;/code&gt; variable is not JSON &lt;em&gt;yet&lt;/em&gt;. This is kind of confusing at first, but it’s a matter of nuance. Typically, a function that converts &quot;to JSON&quot; is &lt;em&gt;actually&lt;/em&gt; converting to an &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Grammar_and_types#Object_literals&quot;&gt;object literal&lt;/a&gt;. This allows us to access the data using JavaScript. In order to convert that data to a valid JSON string, we need to use &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify&quot;&gt;&lt;code&gt;JSON.stringify()&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;&lt;/aside&gt;
&lt;h4&gt;Attach an event listener to the form.&lt;/h4&gt;
&lt;p&gt;With the event handler created, we need to add a listener to the form so we can actually handle the event.&lt;/p&gt;
&lt;p&gt;To do this, we use &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Document/getElementsByClassName&quot;&gt;&lt;code&gt;getElementsByClassName()&lt;/code&gt;&lt;/a&gt; to target the form, then store the first item in the resulting collection as &lt;code&gt;form&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Next, using &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener&quot;&gt;&lt;code&gt;addEventListener()&lt;/code&gt;&lt;/a&gt;, we hook &lt;code&gt;handleSubmit()&lt;/code&gt; to the &lt;code&gt;submit&lt;/code&gt; event, which will allow it to run whenever the user clicks to submit the form.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const handleFormSubmit = (event) =&amp;gt; {
  /* omitted for brevity */
};

/*
 * This is where things actually get started. We find the form element using
 * its class name, then attach the `handleFormSubmit()` function to the
 * `submit` event.
 */
const form = document.getElementsByClassName(&apos;contact-form&apos;)[0];
form.addEventListener(&apos;submit&apos;, handleFormSubmit);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At this point we can test that things are working properly by clicking the &quot;Send It!&quot; button on the form. We should see &lt;code&gt;{}&lt;/code&gt; in the &quot;Form Data&quot; output box.&lt;/p&gt;
&lt;h3&gt;Step 2: Extract the Values of Form Fields As JSON&lt;/h3&gt;
&lt;p&gt;Next up, we need to actually grab values from the form fields.&lt;/p&gt;
&lt;p&gt;To do this, we’ll use something that — at first — might look scary as shit: &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce&quot;&gt;&lt;code&gt;reduce()&lt;/code&gt;&lt;/a&gt; combined with &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/call&quot;&gt;&lt;code&gt;call()&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;We’ll dive into the dirty details of what &lt;code&gt;reduce()&lt;/code&gt; is actually doing in the next section, but for now let’s focus on how we’re actually using it.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Retrieves input data from a form and returns it as a JSON object.
 * @param  {HTMLFormControlsCollection} elements  the form elements
 * @return {Object}                               form data as an object literal
 */
const formToJSON = (elements) =&amp;gt;
  [].reduce.call(
    elements,
    (data, element) =&amp;gt; {
      data[element.name] = element.value;
      return data;
    },
    {}
  );

const handleFormSubmit = (event) =&amp;gt; {
  // Stop the form from submitting since we’re handling that with AJAX.
  event.preventDefault();

  // Call our function to get the form data.
  const data = formToJSON(form.elements);

  // Demo only: print the form data onscreen as a formatted JSON object.
  const dataContainer = document.getElementsByClassName(&apos;results__display&apos;)[0];

  // Use `JSON.stringify()` to make the output valid, human-readable JSON.
  dataContainer.textContent = JSON.stringify(data, null, &apos;  &apos;);

  // ...this is where we’d actually do something with the form data...
};

const form = document.getElementsByClassName(&apos;contact-form&apos;)[0];
form.addEventListener(&apos;submit&apos;, handleFormSubmit);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I know. &lt;em&gt;I know.&lt;/em&gt; It looks hairy. But let’s dig in and see what this is doing.&lt;/p&gt;
&lt;p&gt;First, let’s break this into its component parts:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;We have a function called &lt;code&gt;formToJSON()&lt;/code&gt;, which accepts one argument: &lt;code&gt;form&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Inside that function, we return the value of &lt;code&gt;[].reduce.call()&lt;/code&gt;, which accepts three arguments: a form, a function, and an empty object literal (&lt;code&gt;{}&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;The function argument accepts the arguments &lt;code&gt;data&lt;/code&gt; and &lt;code&gt;child&lt;/code&gt;, and adds a new property with the key of &lt;code&gt;child.name&lt;/code&gt; and the value &lt;code&gt;child.value&lt;/code&gt;, finally returning the &lt;code&gt;data&lt;/code&gt; object&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;After we’ve added that code to our pen, we need to call the function from &lt;code&gt;handleSubmit()&lt;/code&gt;.&lt;/strong&gt; Find &lt;code&gt;const data = {};&lt;/code&gt; inside the function and replace it with &lt;code&gt;const data = formToJSON(form.elements);&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Now we can run it by clicking the &quot;Send It!&quot; button will now output this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;salutation&quot;: &quot;Ms.&quot;,
  &quot;name&quot;: &quot;&quot;,
  &quot;email&quot;: &quot;&quot;,
  &quot;subject&quot;: &quot;I have a problem.&quot;,
  &quot;message&quot;: &quot;&quot;,
  &quot;snacks&quot;: &quot;cake&quot;,
  &quot;secret&quot;: &quot;1b3a9374-1a8e-434e-90ab-21aa7b9b80e7&quot;,
  &quot;&quot;: &quot;&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There are some issues here — for example, neither &quot;Ms.&quot; nor &quot;Cake&quot; was actually selected on the form, and there’s an empty entry at the bottom (which is our button) — but this isn’t too bad for a first step.&lt;/p&gt;
&lt;p&gt;So how did that just happen? Let’s go step by step to figure it out.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; Using &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/elements&quot;&gt;&lt;code&gt;form.elements&lt;/code&gt;&lt;/a&gt;, we get pretty decent access to the form. For example, we can get the email using &lt;code&gt;form.elements.email.value&lt;/code&gt;. However, if we need to convert this to JSON for use with AJAX, it’s a disaster due to its inclusion of numerical indexes, IDs, and names. You can see this for yourself by adding &lt;code&gt;console.log( JSON.stringify(form.elements) );&lt;/code&gt; to &lt;code&gt;handleSubmit()&lt;/code&gt;. That’s why we’re building our own function instead of just using this built-in access.&lt;/p&gt;&lt;/aside&gt;
&lt;h4&gt;Step 2.1 — Understand how &lt;code&gt;reduce()&lt;/code&gt; works.&lt;/h4&gt;
&lt;p&gt;The simplest explanation for &lt;code&gt;reduce()&lt;/code&gt; is this:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The &lt;code&gt;reduce()&lt;/code&gt; method uses a function to convert an array into a single value.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This method is part of the &lt;code&gt;Array&lt;/code&gt; prototype, so it can be applied to any array value.&lt;/p&gt;
&lt;p&gt;It takes two arguments:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A reducer function, which is required.&lt;/li&gt;
&lt;li&gt;An initial value, which is optional (defaults to &lt;code&gt;0&lt;/code&gt;).&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The reducer function is applied to each element of the array. This function accepts four arguments:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The value returned by the reducer function when it ran on the previous element (or the initial value, if this is the first element).&lt;/li&gt;
&lt;li&gt;The current array element.&lt;/li&gt;
&lt;li&gt;The current array index.&lt;/li&gt;
&lt;li&gt;The whole array, in case the reducer needs a reference to it.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For our reducer, we only need the first two arguments.&lt;/p&gt;
&lt;h4&gt;A really simple example of reducing an array.&lt;/h4&gt;
&lt;p&gt;Let’s say we have an array of numbers, which represent sales for the day:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const sales = [100.12, 19.49, 10, 42.18, 99.62];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We need to determine total sales for the day, so we set up this simple function to add up sales:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function getTotalSales(previousTotal, currentSaleAmount) {
  return previousTotal + currentSaleAmount;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then we use &lt;code&gt;reduce()&lt;/code&gt; to apply the function to the array of sales:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const sales = [100.12, 19.49, 10, 42.18, 99.62];

function getTotalSales(previousTotal, currentSaleAmount) {
  return previousTotal + currentSaleAmount;
}

sales.reduce(getTotalSales);
// result: 271.41
&lt;/code&gt;&lt;/pre&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;TIP:&lt;/strong&gt; You can run these examples in your browser’s console to see the results for yourself.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;Now, if we want to condense this code a little, we can actually write the whole thing like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const sales = [100.12, 19.49, 10, 42.18, 99.62];
sales.reduce((prev, curr) =&amp;gt; prev + curr);
// result: 271.41
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When this is called, &lt;code&gt;reduce()&lt;/code&gt; starts with &lt;code&gt;0&lt;/code&gt; as the value of &lt;code&gt;prev&lt;/code&gt;, and takes the first element of the array, &lt;code&gt;100.12&lt;/code&gt;, as the value of &lt;code&gt;curr&lt;/code&gt;. It adds those together and returns them.&lt;/p&gt;
&lt;p&gt;Now &lt;code&gt;reduce()&lt;/code&gt; moves to the second element in the array, &lt;code&gt;19.49&lt;/code&gt;, and this time the value of &lt;code&gt;prev&lt;/code&gt; is the value returned last time: &lt;code&gt;100.12&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This process is repeated until all of the elements have been added together, and we end up with our total sales for the day: &lt;code&gt;271.41&lt;/code&gt;.&lt;/p&gt;
&lt;h4&gt;Step 2.2 — Deconstruct the function.&lt;/h4&gt;
&lt;p&gt;As it stands, &lt;code&gt;formToJSON()&lt;/code&gt; is actually made of three parts:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A &lt;em&gt;reducer function&lt;/em&gt; to combine our form elements into a single object.&lt;/li&gt;
&lt;li&gt;An initial value of &lt;code&gt;{}&lt;/code&gt; to hold our form data.&lt;/li&gt;
&lt;li&gt;A call to &lt;code&gt;reduce()&lt;/code&gt; using &lt;code&gt;call()&lt;/code&gt;, which allows us to force &lt;code&gt;reduce()&lt;/code&gt; to work with &lt;code&gt;elements&lt;/code&gt;, even though it’s &lt;em&gt;technically&lt;/em&gt; not an array.&lt;/li&gt;
&lt;/ol&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; The form elements are actually what’s called an &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormControlsCollection&quot;&gt;&lt;code&gt;HTMLFormControlsCollection&lt;/code&gt;&lt;/a&gt;, which is &quot;array-like&quot;, meaning it’s basically an array, but it’s missing some of the array methods, and has some of its own special properties and methods.&lt;/p&gt;&lt;/aside&gt;
&lt;h4&gt;Step 2.3 — Write the reducer function.&lt;/h4&gt;
&lt;p&gt;First up, we need to have our reducer function. In the simple example of reducing an array, we used single values, which won’t work in this case. Instead, we want to add each field to an object with a format like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    field_name: &quot;field_value&quot;,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So our reducer function works like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// This is the function that is called on each element of the array.
const reducerFunction = (data, element) =&amp;gt; {
  // Add the current field to the object.
  data[element.name] = element.value;

  // For the demo only: show each step in the reducer’s progress.
  console.log(JSON.stringify(data));

  return data;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;data&lt;/code&gt; object is the previous value of the reducer, and &lt;code&gt;element&lt;/code&gt; is the current form element in the array. We then add a new property to the object using the element’s &lt;code&gt;name&lt;/code&gt; property — this is the input’s &lt;code&gt;name&lt;/code&gt; attribute in the HTML — and store its &lt;code&gt;value&lt;/code&gt; there.&lt;/p&gt;
&lt;p&gt;When we return &lt;code&gt;data&lt;/code&gt;, we make the updated object available to the next call of the funciton, which allows us to add each field to the object, one by one.&lt;/p&gt;
&lt;h4&gt;Step 2.4 — Call the reducer.&lt;/h4&gt;
&lt;p&gt;To make it a little more obvious what’s happening in the &lt;code&gt;formToJSON()&lt;/code&gt; function, here’s what it looks like if we break it up into more verbose code:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const formToJSON_deconstructed = (elements) =&amp;gt; {
  // This is the function that is called on each element of the array.
  const reducerFunction = (data, element) =&amp;gt; {
    // Add the current field to the object.
    data[element.name] = element.value;

    // For the demo only: show each step in the reducer’s progress.
    console.log(JSON.stringify(data));

    return data;
  };

  // This is used as the initial value of `data` in `reducerFunction()`.
  const reducerInitialValue = {};

  // To help visualize what happens, log the inital value.
  console.log(&apos;Initial `data` value:&apos;, JSON.stringify(reducerInitialValue));

  // Now we reduce by `call`-ing `Array.prototype.reduce()` on `elements`.
  const formData = [].reduce.call(
    elements,
    reducerFunction,
    reducerInitialValue
  );

  // The result is then returned for use elsewhere.
  return formData;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the above example, we do exactly the same thing as in &lt;code&gt;formToJSON()&lt;/code&gt;, but we’ve broken it down into its component parts.&lt;/p&gt;
&lt;p&gt;We can see the output if we update &lt;code&gt;handleSubmit()&lt;/code&gt; and change the call to &lt;code&gt;formToJSON(form.elements)&lt;/code&gt; to &lt;code&gt;formToJSON_deconstructed(form.elements)&lt;/code&gt;. Check the console to see this output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Initial `data` value: {}
{&quot;salutation&quot;:&quot;Mr.&quot;}
{&quot;salutation&quot;:&quot;Mrs.&quot;}
{&quot;salutation&quot;:&quot;Ms.&quot;}
{&quot;salutation&quot;:&quot;Ms.&quot;,&quot;name&quot;:&quot;&quot;}
{&quot;salutation&quot;:&quot;Ms.&quot;,&quot;name&quot;:&quot;&quot;,&quot;email&quot;:&quot;&quot;}
{&quot;salutation&quot;:&quot;Ms.&quot;,&quot;name&quot;:&quot;&quot;,&quot;email&quot;:&quot;&quot;,&quot;subject&quot;:&quot;I have a problem.&quot;}
{&quot;salutation&quot;:&quot;Ms.&quot;,&quot;name&quot;:&quot;&quot;,&quot;email&quot;:&quot;&quot;,&quot;subject&quot;:&quot;I have a problem.&quot;,&quot;message&quot;:&quot;&quot;}
{&quot;salutation&quot;:&quot;Ms.&quot;,&quot;name&quot;:&quot;&quot;,&quot;email&quot;:&quot;&quot;,&quot;subject&quot;:&quot;I have a problem.&quot;,&quot;message&quot;:&quot;&quot;,&quot;snacks&quot;:&quot;pizza&quot;}
{&quot;salutation&quot;:&quot;Ms.&quot;,&quot;name&quot;:&quot;&quot;,&quot;email&quot;:&quot;&quot;,&quot;subject&quot;:&quot;I have a problem.&quot;,&quot;message&quot;:&quot;&quot;,&quot;snacks&quot;:&quot;cake&quot;}
{&quot;salutation&quot;:&quot;Ms.&quot;,&quot;name&quot;:&quot;&quot;,&quot;email&quot;:&quot;&quot;,&quot;subject&quot;:&quot;I have a problem.&quot;,&quot;message&quot;:&quot;&quot;,&quot;snacks&quot;:&quot;cake&quot;,&quot;secret&quot;:&quot;1b3a9374-1a8e-434e-90ab-21aa7b9b80e7&quot;}
{&quot;salutation&quot;:&quot;Ms.&quot;,&quot;name&quot;:&quot;&quot;,&quot;email&quot;:&quot;&quot;,&quot;subject&quot;:&quot;I have a problem.&quot;,&quot;message&quot;:&quot;&quot;,&quot;snacks&quot;:&quot;cake&quot;,&quot;secret&quot;:&quot;1b3a9374-1a8e-434e-90ab-21aa7b9b80e7&quot;,&quot;&quot;:&quot;&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can see here that the reducer is called for every form element, and the object grows with each subsequent call until we’ve got an entry for every &lt;code&gt;name&lt;/code&gt; value in the form.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Change &lt;code&gt;handleSubmit()&lt;/code&gt; back to using &lt;code&gt;formToJSON(form.elements)&lt;/code&gt;&lt;/strong&gt;, and let’s move on to cleaning up this output to only include fields it &lt;em&gt;should&lt;/em&gt; include.&lt;/p&gt;
&lt;h3&gt;Step 3: Add a Check to Make Sure Only the Fields We Want Are Collected&lt;/h3&gt;
&lt;p&gt;The first problem we can see in the output is that fields with both empty &lt;code&gt;name&lt;/code&gt; and empty &lt;code&gt;value&lt;/code&gt; attributes have been added to the array. This isn’t what we want in this case, so we need to add a quick check to verify that fields have both a &lt;code&gt;name&lt;/code&gt; and a &lt;code&gt;value&lt;/code&gt; before we add them.&lt;/p&gt;
&lt;h4&gt;Step 3.1 — Create a function to check for valid elements.&lt;/h4&gt;
&lt;p&gt;First, let’s add a new function to our pen called &lt;code&gt;isValidElement()&lt;/code&gt;. This function will accept one argument — the &lt;code&gt;element&lt;/code&gt; — and return either &lt;code&gt;true&lt;/code&gt; or &lt;code&gt;false&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;To return &lt;code&gt;true&lt;/code&gt;, the element must have:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A non-empty &lt;code&gt;name&lt;/code&gt; property.&lt;/li&gt;
&lt;li&gt;A non-empty &lt;code&gt;value&lt;/code&gt; property.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Implement this check like so:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Checks that an element has a non-empty `name` and `value` property.
 * @param  {Element} element  the element to check
 * @return {Bool}             true if the element is an input, false if not
 */
const isValidElement = (element) =&amp;gt; {
  return element.name &amp;amp;&amp;amp; element.value;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Pretty simple, right?&lt;/p&gt;
&lt;p&gt;This gives us a flag that lets us avoid unused elements (like the button) and unfilled fields (such as an empty Email field) from being added to the form data object.&lt;/p&gt;
&lt;h4&gt;Step 3.2 — Add the check to &lt;code&gt;formToJSON()&lt;/code&gt;.&lt;/h4&gt;
&lt;p&gt;Next, we need to add an &lt;code&gt;if&lt;/code&gt; check for whether or not our &lt;code&gt;element&lt;/code&gt; is valid in &lt;code&gt;formToJSON()&lt;/code&gt;. Since we don’t want to add anything if the element is &lt;em&gt;not&lt;/em&gt; valid, we can simply do the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const formToJSON = (elements) =&amp;gt;
  [].reduce.call(
    elements,
    (data, element) =&amp;gt; {
      // Make sure the element has the required properties.
      if (isValidElement(element)) {
        data[element.name] = element.value;
      }

      return data;
    },
    {}
  );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now when we submit our form, the output is much cleaner:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;salutation&quot;: &quot;Ms.&quot;,
  &quot;subject&quot;: &quot;I have a problem.&quot;,
  &quot;snacks&quot;: &quot;cake&quot;,
  &quot;secret&quot;: &quot;1b3a9374-1a8e-434e-90ab-21aa7b9b80e7&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;However, we’re still not there yet. In the next step, we’ll deal with checkable elements like radio inputs and checkboxes.&lt;/p&gt;
&lt;h3&gt;Step 4: Only Store Checkable Fields If a Field Is In &lt;code&gt;checked&lt;/code&gt; State&lt;/h3&gt;
&lt;p&gt;Now we need another check to identify whether or not an element should be added to the array. For instance, right now the &lt;code&gt;salutation&lt;/code&gt; field is being stored with the value &lt;code&gt;Ms.&lt;/code&gt;, even though that value &lt;em&gt;is not selected&lt;/em&gt; in the form.&lt;/p&gt;
&lt;p&gt;Obviously, this is bad news. So let’s fix it.&lt;/p&gt;
&lt;h4&gt;Step 4.1 — Create a function to check for checkable elements.&lt;/h4&gt;
&lt;p&gt;First, let’s add a new function to check whether or not an element’s value should be considered valid for inclusion in the object.&lt;/p&gt;
&lt;p&gt;Our criteria for determining a &quot;valid&quot; element are:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The element is &lt;em&gt;not&lt;/em&gt; a checkbox or radio input.&lt;/li&gt;
&lt;li&gt;If the element &lt;em&gt;is&lt;/em&gt; a checkbox or radio input, it has the &lt;code&gt;checked&lt;/code&gt; attribute.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Add the following to create this check:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Checks if an element’s value can be saved (e.g. not an unselected checkbox).
 * @param  {Element} element  the element to check
 * @return {Boolean}          true if the value should be added, false if not
 */
const isValidValue = (element) =&amp;gt; {
  return ![&apos;checkbox&apos;, &apos;radio&apos;].includes(element.type) || element.checked;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; The &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/includes&quot;&gt;&lt;code&gt;includes()&lt;/code&gt;&lt;/a&gt; method is an easy way to see if a value is present in an array. I prefer this to multiple &lt;code&gt;if&lt;/code&gt; checks, switch statements, or other ways of matching a value against an array.&lt;/p&gt;&lt;/aside&gt;
&lt;h4&gt;Step 4.2 — Add the check to &lt;code&gt;formToJSON()&lt;/code&gt;.&lt;/h4&gt;
&lt;p&gt;Now we can add this check to &lt;code&gt;formToJSON()&lt;/code&gt;, which is as simple as adding a second condition to our existing &lt;code&gt;if&lt;/code&gt; check:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const formToJSON = (elements) =&amp;gt;
  [].reduce.call(
    elements,
    (data, element) =&amp;gt; {
      // Make sure the element has the required properties and should be added.
      if (isValidElement(element) &amp;amp;&amp;amp; isValidValue(element)) {
        data[element.name] = element.value;
      }

      return data;
    },
    {}
  );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we can run our code and see that the output is much cleaner:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;subject&quot;: &quot;I have a problem.&quot;,
  &quot;secret&quot;: &quot;1b3a9374-1a8e-434e-90ab-21aa7b9b80e7&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is much better — now we only get elements that actually have a value set.&lt;/p&gt;
&lt;h3&gt;Step 5: If a Field Allows Multiple Values, Store Them In an Array&lt;/h3&gt;
&lt;p&gt;But we’re not quite done yet, because the form is still messing up the &lt;code&gt;snacks&lt;/code&gt; field — which is &lt;em&gt;clearly&lt;/em&gt; the most important field.&lt;/p&gt;
&lt;p&gt;Try selecting both &quot;Pizza&quot; and &quot;Cake&quot; to see the problem:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;subject&quot;: &quot;I have a problem.&quot;,
  &quot;snacks&quot;: &quot;cake&quot;,
  &quot;secret&quot;: &quot;1b3a9374-1a8e-434e-90ab-21aa7b9b80e7&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nope. This is a disaster. We need both pizza AND cake. So let’s make sure that can happen.&lt;/p&gt;
&lt;h4&gt;Step 5.1 — Create checks for elements that accept multiple values.&lt;/h4&gt;
&lt;p&gt;The check for whether or not multiple values are allowed has two parts, because there are two elements that allow multiple values.&lt;/p&gt;
&lt;p&gt;First, we need to add a check for any checkboxes. This is simple enough: we just check if the &lt;code&gt;type&lt;/code&gt; is &lt;code&gt;checkbox&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Checks if an input is a checkbox, because checkboxes allow multiple values.
 * @param  {Element} element  the element to check
 * @return {Boolean}          true if the element is a checkbox, false if not
 */
const isCheckbox = (element) =&amp;gt; element.type === &apos;checkbox&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Second, we need to add a check for a &lt;code&gt;select&lt;/code&gt; element with the &lt;code&gt;multiple&lt;/code&gt; attribute.&lt;/p&gt;
&lt;p&gt;This is a bit trickier, but still pretty straightforward. A &lt;code&gt;select&lt;/code&gt; has a property called &lt;code&gt;options&lt;/code&gt;, so we’ll check for that first. Next, we check for the &lt;code&gt;multiple&lt;/code&gt; property. If both exist, our check will return &lt;code&gt;true&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Checks if an input is a `select` with the `multiple` attribute.
 * @param  {Element} element  the element to check
 * @return {Boolean}          true if the element is a multiselect, false if not
 */
const isMultiSelect = (element) =&amp;gt; element.options &amp;amp;&amp;amp; element.multiple;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Step 5.2 — Handle checkboxes in &lt;code&gt;formToJSON()&lt;/code&gt;.&lt;/h4&gt;
&lt;p&gt;Inside &lt;code&gt;formToJSON()&lt;/code&gt;, we need to add another &lt;code&gt;if&lt;/code&gt; block for our &lt;code&gt;isCheckbox()&lt;/code&gt; function.&lt;/p&gt;
&lt;p&gt;If the current element is a checkbox, we need to store its value(s) in an array. Let’s take a look at the code first, and then we’ll talk about how it works.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const formToJSON = (elements) =&amp;gt;
  [].reduce.call(
    elements,
    (data, element) =&amp;gt; {
      // Make sure the element has the required properties and should be added.
      if (isValidElement(element) &amp;amp;&amp;amp; isValidValue(element)) {
        /*
         * Some fields allow for more than one value, so we need to check if this
         * is one of those fields and, if so, store the values as an array.
         */
        if (isCheckbox(element)) {
          data[element.name] = (data[element.name] || []).concat(element.value);
        } else {
          data[element.name] = element.value;
        }
      }

      return data;
    },
    {}
  );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since we need to get the element’s values into an array, we use a bit of shorthand in &lt;code&gt;(data[element.name] || [])&lt;/code&gt;, which means, &quot;use the existing array, or a new, empty one&quot;.&lt;/p&gt;
&lt;p&gt;Then we use &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/concat&quot;&gt;&lt;code&gt;concat()&lt;/code&gt;&lt;/a&gt; to add the current value to the array.&lt;/p&gt;
&lt;p&gt;Now if we check the options for both pizza and cake, we see the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;subject&quot;: &quot;I have a problem.&quot;,
  &quot;snacks&quot;: [&quot;pizza&quot;, &quot;cake&quot;],
  &quot;secret&quot;: &quot;1b3a9374-1a8e-434e-90ab-21aa7b9b80e7&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Much better. Crisis averted, everyone!&lt;/p&gt;
&lt;h4&gt;Step 5.3 — Write a function to retrieve values from multi-selects.&lt;/h4&gt;
&lt;p&gt;Our very last step before we can call this sucker done is to add a check for &lt;code&gt;select&lt;/code&gt; fields that support multiple selected options. I’m not a big fan of this input type, because I think it’s a confusing input for people to use — one that’s easily replaced with checkboxes — but for the sake of covering bases we’ll plug it in.&lt;/p&gt;
&lt;p&gt;The selected options from a multi-select are stored in their own array-like object, called an &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/HTMLOptionsCollection&quot;&gt;&lt;code&gt;HTMLOptionsCollection&lt;/code&gt;&lt;/a&gt;, so we need to run &lt;code&gt;reduce()&lt;/code&gt; on this as well.&lt;/p&gt;
&lt;p&gt;Let’s keep things clean by moving this out into its own function.&lt;/p&gt;
&lt;p&gt;This function will accept each options, check if the &lt;code&gt;selected&lt;/code&gt; property is &lt;code&gt;true&lt;/code&gt;, then add its value to an array called &lt;code&gt;values&lt;/code&gt;, which will ultimately be returned containing the values of all selected options.&lt;/p&gt;
&lt;p&gt;Add the following to the pen:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Retrieves the selected options from a multi-select as an array.
 * @param  {HTMLOptionsCollection} options  the options for the select
 * @return {Array}                          an array of selected option values
 */
const getSelectValues = (options) =&amp;gt;
  [].reduce.call(
    options,
    (values, option) =&amp;gt; {
      return option.selected ? values.concat(option.value) : values;
    },
    []
  );
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Step 5.4 — Handle multi-select values in &lt;code&gt;formToJSON()&lt;/code&gt;.&lt;/h4&gt;
&lt;p&gt;To put a bow on all this, we need to add an &lt;code&gt;else if&lt;/code&gt; block in our &lt;code&gt;formToJSON()&lt;/code&gt; function.&lt;/p&gt;
&lt;p&gt;After the &lt;code&gt;isCheckbox()&lt;/code&gt; check, we’ll add a &lt;code&gt;isMultiSelect()&lt;/code&gt; check. If that returns &lt;code&gt;true&lt;/code&gt;, we’ll add the select’s values to the object as an array using &lt;code&gt;getSelectValues()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Make the following updates to the pen:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const formToJSON = (elements) =&amp;gt;
  [].reduce.call(
    elements,
    (data, element) =&amp;gt; {
      // Make sure the element has the required properties and should be added.
      if (isValidElement(element) &amp;amp;&amp;amp; isValidValue(element)) {
        /*
         * Some fields allow for more than one value, so we need to check if this
         * is one of those fields and, if so, store the values as an array.
         */
        if (isCheckbox(element)) {
          data[element.name] = (data[element.name] || []).concat(element.value);
        } else if (isMultiSelect(element)) {
          data[element.name] = getSelectValues(element);
        } else {
          data[element.name] = element.value;
        }
      }

      return data;
    },
    {}
  );
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Run a quick test of multi-select values.&lt;/h4&gt;
&lt;p&gt;Since our current form doesn&apos;t have a select with the &lt;code&gt;multiple&lt;/code&gt; attribute, so let&apos;s quickly add that to the &lt;code&gt;subject&lt;/code&gt; field in our pen.&lt;/p&gt;
&lt;p&gt;Look for the &lt;code&gt;select&lt;/code&gt; in the HTML pane and add the &lt;code&gt;multiple&lt;/code&gt; attribute like so:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;      select#subject.contact-form__input.contact-form__input--select(
        name=&quot;subject&quot;
        multiple
      )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we can actually test. Click on both options and submit the form. The out put will now be:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;subject&quot;: [&quot;I have a problem.&quot;, &quot;I have a general question.&quot;],
  &quot;secret&quot;: &quot;1b3a9374-1a8e-434e-90ab-21aa7b9b80e7&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After we’ve tested, we can remove the &lt;code&gt;multiple&lt;/code&gt; attribute from the &lt;code&gt;select&lt;/code&gt; input.&lt;/p&gt;
&lt;aside&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; Reading values from a multi-select isn’t supported in IE11 and below using this script. Personally, I would recommend just using checkboxes — I think multi-selects are a confusing user experience for many people — but if you need to support them in older browsers you’ll need to modify this code.&lt;/p&gt;&lt;/aside&gt;
&lt;h3&gt;The Final Result: Form Field Values Are Collected in an Object for Use as JSON&lt;/h3&gt;
&lt;p&gt;At this point, we’ve built a small script that will extract the values from a form as an object literal, which can easily be converted to JSON using &lt;code&gt;JSON.stringify()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;We can test by filling out our form with dummy data and submitting it.&lt;/p&gt;
&lt;p&gt;Use your own fork of the pen, or enter dummy data in the form below:&lt;/p&gt;
&lt;p&gt;CodePen: &lt;a href=&quot;https://codepen.io/jlengstorf/pen/YWJLwz&quot;&gt;Work in progress on Codepen.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;After submitting, we’ll see the info we entered, and it’ll look something like the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;salutation&quot;: &quot;Ms.&quot;,
  &quot;name&quot;: &quot;Pac-Man&quot;,
  &quot;email&quot;: &quot;mspacman@example.com&quot;,
  &quot;subject&quot;: &quot;I have a problem.&quot;,
  &quot;message&quot;: &quot;These ghosts keep chasing me!&quot;,
  &quot;snacks&quot;: [&quot;pizza&quot;, &quot;cake&quot;],
  &quot;secret&quot;: &quot;1b3a9374-1a8e-434e-90ab-21aa7b9b80e7&quot;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item></channel></rss>