Serverless Marketing Automation for React
with Joel Hooks
In this episode, learn how Joel Hooks creates powerful marketing automation using serverless functions, CustomerIO, and Next.js. This is a peek behind the curtain at Egghead.io!
Resources & Links
Captions provided by White Coat Captioning (https://whitecoatcaptioning.com/). Communication Access Realtime Translation (CART) is provided in order to facilitate communication accessibility and may not be a totally verbatim record of the proceedings.
Jason: Hello, everyone. And welcome to another episode of Learn With Jason. Today on the show, we're bringing back Joel Hooks. What's up, man? How you doing?
**Joel: **Doing great.
Jason: I'm super excited to have you back. Before I go too far into this, maybe I should give you an opportunity to talk. Could you give us a little bit of a background on yourself?
Jason: But I think that's actually something interesting. One of the things that trips up a lot of folks when they start talking about getting into -- I want to do education, I want to teach people, I want to sell a product. If we really boil it down, there's only so many moving parts. They're all functionally borderline equivalent. The difference is really the content, not the mechanisms, right. So, have you found that, you know -- because you've sold everything through these kind of platforms from workshops to products to subscriptions. Is there a lot of variance in the actual implementation, or is it more just the packaging?
Jason: Yeah. One of the things I've seen that I really, really like is as I've started building out more things with serverless functions, when I'm interfacing with the back end, I built Learn With Jason on Sanity. Recently I've moved to have a Hasura layer in addition to Sanity for some more real-time stuff I've been doing with the stream. All of that, when I was working with it, the serverless functions didn't change. I just updated my utilities under the hood to stitch together Hasura and Sanity, but my APIs, they just got more data. That model felt really interesting, especially coming from the old days of working in monolithic, fully coupled front ends and back ends where you make a back-end change and that means you have to open pull requests to all the front ends that access it. There's this big, painful migration path. It feels like they're putting that nice guardrail in the middle. I write the logic that I want, and that happens here. Then I expose an API contract and my front-end consumes that. It all just feels nicely sectioned. Like, this is the thing I'm thinking about over here, this is the thing I'm thinking about over here. They don't have to be aware of each other, other than this is the data you're going to get, which I really, really like.
**Joel: **Yeah, and it provides a nice boundary, too. When you want to change it, it's not -- like, that surface area isn't across your entire front end. So, our Rails app, it was traditionally, the way you do that, it builds up and you make a change. You'd have to find and replace all over the app and see where different stuff has occurred. You still might run into that, but like you said, you can add data and remove data. The way we approach that is that if the data exists, use it. If it doesn't exist, don't use it. So from the design side, when we're working on a team and we make some changes and it's like, hey, I added a bunch of data, it doesn't break the application. What it does is now we can use that so we can make design decisions based on, you know, like new data that's available or if it doesn't exist and we remove it, then it just doesn't render that. That's been really kind of fantastic as well. It's very flexible which I like. I want a broad team. Our Rails app, it takes 35 minutes to deploy, which is -- like, that's the shame I bear. (Laughter) But it's a real struggle. If you're trying to work and take 30 minutes, how many times can I deploy during a day? We constantly deploy. We're at ship it squirrel shop. We deploy it, and we'll roll it back if it didn't work or whatever. Usually we're just constantly deploying. If those are taking 30 minutes, that's a problem. If they take five minutes to do that, so if we can decouple that and give people that power, that's another huge advantage, too. So, I want -- like the back-end shouldn't change as much. It's slowly deploying, but how do we also allow it to, like people to update and do things quicker and work and iterate in a faster way has been a real improvement with modern serverless architectures.
Jason: For sure.
**Joel: **Not that you can't probably get your deploy times up.
Jason: Yeah, but it's just kind of both, right? You want to do all of those things. You should be pushing on every available lever. If you can make your deploy times faster, absolutely. If you can decouple things, even better. It seems like you can kind of give everybody a little bit more autonomy, a little less overhead, and less complexity to get things live. You know, you win no matter which of those levers you push on. You can choose to push on one or all of them and still get good results, which is I think the power of this approach. So, that being said, I know that we have -- I saw your to-do list. It was ambitious. So, I am excited to -- I'm going to put a link to this very ambitious to-do list for today in the chat. Then I think we can do a quick switch over here. Let's talk before we switch over and just kind of state some goals here. So, our goal for today, I have had convert kit for a long time. I've been using it for years. I use it for, you know, all my different web properties, to do newsletters and a bunch of things that I learned from you that we're not talking about today, for exposing content and stuff like that. So, that's what we're going to use for enterprise Jamstack. We're just going to build out the marketing automation for enterprise Jamstack today. So, this is the project. This is a product that I'm working on right now with Joel and the Egghead team to teach people how to do Jamstack at scale, which is something that I spend a lot of time thinking about, as you may imagine. And something that I think is going to be super fun. So, to make this work then, we're going to get the form on enterprisejamstack.com. It's going to be connected to Convert Kit. This then we want to set up a bunch of automations using serverless functions, right? And that'll let us do like personalization on the site. Is that the goal?
**Joel: **It gives you a few different things. You can think of it almost like analytics. You have ConvertKit in the background. You start thinking about serverless and databases and where your data lies and how you're using Sanity. You mentioned using Sanity and Hasura, which does other things. That's how I view it. At the end of the day, ConvertKit is another database. It's kind of weird maybe sometimes to think about it that way. What ConvertKit really is for me is a database. So, it's like a customer management system to where you have a list of all your customers, and then there's properties that you can assign to them. You can tag them and stuff, too. We really are going to focus on properties and like adding those. You can do things like survey and store the answers to the survey inside of ConvertKit. Then take that answer -- so, what technology are you most interested in, you give them a list, and if they pick React, when they come back and look at this, you can change your marketing copy and your headlines and body text and what examples you present to be more relevant to their context. You can make very small changes, which is the wording and how you present it. Not to where you're like, oh, this is the perfect for React developers, but just to speak to them and their issues and speak the language of whatever their context is. So, it allows you -- and when we talk about personalization, that's what we're talking about. Not like a whole different product. It's just, you know, framing what's important to them in a forward way instead of like, this is everything for everybody, which frankly, isn't great. So, you want to be able to highlight that. Brendan Dunn is where most of this comes from for me. You can look at his mastering ConvertKit course, which is really fantastic. It kind of dives into it in a very deep way. He calls it behavioral segmentation.
**Joel: **Which is what we're ultimately going to try to do. But at the end of the day, right now all we're doing is getting it set up so we can actually start bringing in data. Until we do that -- here, I'm going to drop my affiliate link for his awesome course.
**Joel: **I don't know. Did it have a link? I write a lot.
Jason: It did not. I'm not sure. If you want to drop a link to the post you're thinking of, we can confirm or deny that. In the meantime, I'm going to take this opportunity to switch us over into paraprogramming mode. And let's do a shout out to the sponsors. So, the show is being live captioned right now. It is available at learnwithJason.dev. You can see the live captioning happening here because of Rachel from White Coat Captioning. Thanks for being here today. That's made possible through the generous support of our sponsors. We have Netlify, Fauna, Auth0, and Hasura all kicking in to make this show more accessible to more people, which means a lot to me. I hope it means a lot to you. If you want to follow along in text, just load the home page, and you'll see it happen in realtime. Which always blows me away. Some day I want to watch a captioner work because the speed with which they're able to write down my motor mouth is just mind-boggling. Also, make sure you go and follow Joel on Twitter. Joel is a -- what would you even call your Twitter persona? You're very helpful, but you're also a professional troll. (Laughter)
**Joel: **I like to -- like, subtweeting is my favorite thing. I do that a lot. I don't want to get into arguments with people on Twitter necessarily. But I want to talk about something. I think there's multiple ways you can subtweet. I try to do it in a way that's like not instigating or trying to be mean to people, but just so I can talk about things I see and notice without like brigading or bringing in people or shining a spotlight necessarily.
Jason: For sure.
**Joel: **I'm a little negative sometimes. I'm an optimistic nihilist. If you know me for a while, it might make sense. I don't know if it makes sense to you. I'm excited to see what happens, but I don't think anything really matters.
Jason: You know what, actually, I would say I feel the same way. I am also an optimistic nihilist. I think that if nothing matters because ultimately we're all going to end up dead in the end anyway, then the only meaning we get out of life is the meaning we assign to things. That's actually really great because it means that we can do things that we're excited about and that we care about, and that means we're doing it right. There is no right or wrong answer. There's only how we feel about it. That to me is like the -- that's the most optimistic thing or the most uplifting thing in my opinion. Now I don't have to worry about getting a report card. I just get to live a life that I'm happy with, and that seems like winning.
**Joel: **Yeah, and you know, I want to be a good person and help people and do all that stuff because that makes what happens next more interesting for me personally. It's kind of a weird philosophy, but it works out. It goes deeper than that, but that's a totally different talk show.
Jason: Yeah, we got to get an actual podcast together for that. (Laughter) Yeah, when the pandemic ends, we'll go sit in a bar. Anybody who wants to listen to that can come, get some drinks with us. It'll be wonderful. We are using a bunch of things that were pioneered at egghead.. I'm going to drop my affiliate. I love this platform. I run courses on it. I use the courses on this all the time. It's an extremely good platform. Really, really compact lessons and a bunch of brilliant people on here teaching all sorts of stuff. If you want to learn, you can go and learn. If you want to go deeper with serverless functions, I have a course that just came out on that. So you can go find that there. We are going to be working in this source code today. So, here is a link to that. Joel already posted it, but I'll post it again. That is all powering this site here, enterprisejamstack.com. This is a new product that I'm working on with Joel and the Egghead team that is going to basically be taking all the concepts that I've learned about Jamstack from my trying to move IBM toward the Jamstack to working at Gatsby to working at Netlify and bundle all that up into something I think can help teams get the most out of this approach and understand how it's an architecture, not a stack, and the ways if you kind of adopt these work flows, you're going to see yourself. All the things we've been talking about. You're faster, more confident, you can ship multiple times a day. All these things we really, really want when we get into building teams at any scale and how we can take those approaches and work them at literally any scale. So, all that being said, what we're trying to do today is this big old list here. So, we've got quite a ways to go. We've got about, let's say, an hour to do it. What should I do first? Where I'm at right now is I've downloaded the repo and installed node modules. That's as far as I've gotten.
**Joel: **Get it running in your local environment using Netlify, which hopefully should just work.
Jason: It should just work. I have access to the team here. So, here's me on the site. What I think I can do is run Netlify link. It should find it based on the Git remote origin. No matching site found. Okay. So, let's try again. Let's see if I can just enter a site ID. So, I'll go to my site settings, and let's get the actual site ID.
**Joel: **That's brute force right there.
Jason: Yeah, it's faster than trying to do anything else. So, then I'm going to Netlify dev. This is the reason this is important. In doing this, it just pulled all of our environment variables so that we don't have to configure those locally. Because I have access to the team, I have access to the environment variables. That means that our site should, fingers crossed, nothing up our sleeve, just work. There we go. Look at it go, everyone. Beautiful.
Holy buckets. Did that just work?
**Joel: **It did.
Jason: It did. It did, indeed, chat. Cool. So, I'm ready. Now that I've got this, let me open up a code editor, and I think we're in business, right?
**Joel: **Should be. The only thing this page is doing right now is presenting this kind of simple headline and then as you scroll down the page, there's a form. So, we want the first name, last name, and when you click this now inside the form component, what it's doing is just posting as a normal -- like a form action would do. So, it posts directly to ConvertKit. It's actually going to -- I believe it goes through ConvertKit and then -- yeah, we might be moving it. I'd have to look at the code exactly. But basically, it's subscribing somebody and moving them to this confirmed page. Pretty simple. If you want to open up the component, we can take a look.
Jason: Yeah, let's do it. In here we have our source, components, forms, subscribe. So, let me do that later. For now, just going to open this up. So, we've got our subscriber form. This is tail wind, right? So we have tail wind. The copy is right in line, which is nice for something like this because why set up a CMS for this?
**Joel: **You could change that spelling error in there if you wanted to, real quick.
Jason: Sure can. Okay. Then updates about the course. The action is the actual ConvertKit. We're using next environment variable so it shows up in client code. Then first name, email address, and we're in business. That's about as straightforward as it gets. It's HTML, which is pretty wonderful.
**Joel: **Yeah, so this form isn't special at all. These days with React, I'm going to put validations there. There's an on submit method that I can hook into. That's what I'm looking for. I want to kind of intercept this, what's happening here. We're just posting directly to ConvertKit. What happens when we post to ConvertKit, it accepts it like a normal -- I want to call it old-school form post. You submit this, and that goes in, sign somebody up, then redirects them to wherever you've configured it. Default is a ConvertKit page. We go through and make it to where it's pushing it to your domain so they get a nice branded experience and not a ConvertKit experience. So, you won't see anything about where we're going here, but you'll notice when you did that, it went to your confirmed page.
**Joel: **And I believe it went to the confirmed page on the deployed URL, not our local confirm page.
Jason: Whoops. Trying to open my email so I can confirm this. So, it needs me to confirm my subscription.
**Joel: **Even the page it just took you to. So, the tab to the left of that. Yeah, you'll see it's sending us to Jamstack with Jason, which was the domain that we were using before we decided we wanted to get those enterprise.
Jason: That's right. Yep, we renamed.
**Joel: **That's configured inside ConvertKit. So, we can go look at that, actually.
Jason: Yes, I'm going to try to make this so I don't -- I'm making the window smaller in hopes that I don't show any personally identifying information. It's just emails here.
**Joel: **Yeah, if you can navigate to the forms section, then that's what we need to look at. It shouldn't show anybody's. It might though, actually.
Jason: So, here we go. This is what I think we've got. We've got our landing pages and forms. We can go to this one here. This is our details. Then that part is configured how?
**Joel: **So, if you go to settings, yeah.
Jason: We can just swap these, right?
**Joel: **Yeah, so that one -- well, I don't know if we changed it already or not.
Jason: This is what I just got when I clicked the link in email. Then this is the one that I got after I subscribed. So, I think they just got swapped.
**Joel: **Yes, that makes sense. But you're going to want to change that to the Enterprise Jamstack too.
Jason: That's right. Enterprisejamstack.com. This is what someone sees after they submit the form. That's good. I'm going to save that. Then we've got a domain name.
**Joel: **So, I think incentive. That's the one. And that would be confirmed. We're going to make this all moot anyway because we're going to override it. But best to be correct. And you can visit those at any time if you're just curious.
Jason: Yeah, okay. Cool. So, those all work. Now we've got -- and we can see here the form that we're using is not this form at all. We just took the form ID, which is one of these in here. This one maybe.
**Joel: **Yeah, it's in the URL, actually.
Jason: So, this URL here. Then we can -- we drop that in, and that's what's in that environment variable. So, that's the value here.
**Joel: **That's our ConvertKit sign-up form. While you're in there, let's do one more little housekeeping.
**Joel: **Whenever you do this, this always kind of throws me off. In the top left, there's that. You can change that to enterprisejamstack. When you first do this, it'll be called Charlotte, or whatever the template name is. It's not obvious you need to rename it, and you'll end up with five Charlotte templates in your form names and be confused. At least, that's my story. Maybe that won't happen to you.
Jason: Question, metric gang. Is there a specific reason why ConvertKit? I use ConvertKit because ConvertKit does all the things that I need it to do without a bunch of additional things that I don't need it to do. And it doesn't cost me a million dollars. I pay for this out of pocket, right. So, ConvertKit is like more expensive than Mailchimp, but it's nowhere near the cost of Salesforce or I think Active Campaign is also expensive. So, it does enough, and there's a ton you can do with this. There's probably a whole session we could do just on stuff that ConvertKit is capable of. But we absolutely do not have time for that today.
**Joel: **It's pretty limited. Like, Active Campaign is definitely a larger scale product, but ConvertKit is pretty simple, so there's not a lot of options. It's built mainly for bloggers and people that just want a newsletter. They're adding e-commerce stuff in there. I've been lately in CustomerIO quite a bit for a lot of these things. I like its work flows. I've never actually used Active Campaign.
Jason: So, I've never actually really done a deep comparison of any of this stuff. So, let me pull these up. Here's Active Campaign.
**Joel: **ConvertKit also starts at a free tier, which is nice.
Jason: That part is definitely nice. You can get a certain number of people for free. But I think Active Campaign gets pretty spendy. Oh, this actually isn't as bad as I expected. I have no idea what the limitations are. So as you start going up, it starts to get -- this is pretty comparable to ConvertKit, actually. So, maybe I misspoke. But anyway, ConvertKit does all the things I need it to do, which is really, honestly, why I chose it.
**Joel: **Every one of these platforms has a reason to hate it and a reason to love it. Period. There are all trade-offs and give or take or targeting specific markets. ConvertKit -- so, Nathan Barry, the founder, is also a friend of mine. I like how they operate as a business. They're a bootstrap business and treat their employees extremely well and their product is great and they're constantly improving it. They're extremely transparent with their development processes. They're not a juggernaut. They just are trying to build a good product for people. That's important to me in the products that I use and recommend. I also -- like, if I use a product and think they're shady, I'll keep that on the down low and won't recommend them. That's the strategy there. When I use something and love it and it's also a good company, then I'll recommend it. That's how I feel about ConvertKit.
Jason: Yeah, and I have had the chance to meet Nathan and a couple other folks from the ConvertKit team. I've always gotten good vibes. So, I kind of join in on that. They seem like a good company, run by people who are trying to do good, and it's small so you don't have to worry about the weird kind of corporate incentives that get twisted as things get really large. All of which are good things, in my book. So, okay. We fixed ConvertKit up. We've got this form. What should we do next?
**Joel: **So, what we want to do is actually instead of doing this to where the form is posting, we want to have it still post, but we're going to post to a serverless function. So, we want to create a subscribe function that we can access and kind of override or intercept this form thing and do it ourselves via the API.
Jason: Okay. So, what I'm going to do, we just launched a new feature for this where we're like auto deploying functions. I think if we do Netlify functions subscribe.js, this should, I think, just work, which will be kind of cool.
**Joel: **Are you saying this is a TypeScript project?
Jason: Wait, do you want me to do it? This is going to get super weird.
**Joel: **I mean, I don't know. Does Netlify support the source page's API routes?
Jason: Yeah. Yeah, yeah. It'll do that.
**Joel: **We can do this how you know for now. Semicolons and all.
Jason: That's a good point. We could use API routes. In the interest of going fast, I'm going to do it this way. Then we can back it all out and do something else if we need to. Let me, I think, stop and restart this. It should pick up our functions automatically. Let's find out. Netlify functions subscribe. I don't know if Netlify dev picks this up yet. Oh, I don't think it does. It didn't run a server for us. Okay. So, that's fine. We'll have to set this up real quick. So, what we can do instead is we'll do a Netlify.toml, set a build, and then set functions to functions. If I spell it all right.
**Joel: **That's always the trick for me.
Jason: Netlify functions. All right. So let's try that one more time. Now it should pick up functions. Function server is listening. Good. That's what we wanted. So then I can go back out here, run this. Lambda handler is not a function. That's because I did a default export instead of the exports.handler, which is just me forgetting how code works.
**Joel: **It's very complicated.
Jason: There we go. So, this is now a working serverless function. We can do whatever we want with it.
**Joel: **Um, yeah. So, we're outside of the code paths. I don't know how weird this makes everything. We're off book for me.
Jason: If that's the case, I can definitely move into the API routes.
**Joel: **No, this should be fine. So, what we're ultimately trying to do is we send this function a couple things. The body of it -- so, it should be a post method, if that makes a difference. We want to get the events. I usually make sure it's a post method so the event.ht method equals post, that sort of thing. We want to parse the body. You should have the email address and first name. But with whatever those are from the function. Or the form that we introduced.
Jason: I think that'll work.
**Joel: **Usually it's a 404. Just send them 404 and it works. I think that's just being nice.
Jason: Yeah. So, that'll -- I think this is right. I might have to double check that method.
**Joel: **It is.
Jason: This will give us the first name -- or wait, this is going to be fields first name. Interesting. I wonder what this is going to look like.
**Joel: **And that is specifically how ConvertKit handles it. If you went in and said give me the embed code or give me the HTML code for my form, that's how it's going to handle it. We can change that up. We'll probably need to. I would just make it normal. Like first, underscore, name, and email, underscore, address.
Jason: So, to test this immediately, we can do something like open up Postman. Or I don't even need to. I can just submit this through the site, can't I? Why don't we do that. So, I can take this action out and a new action that will be Netlify functions subscribe. So, now when we submit, it's going to send to this function. If we're watching our console here, we'll be able to see --
**Joel: **Yeah, this is when I would have a nice fancy log.
**Joel: **And frankly, I would probably want to change that sooner rather than later. So we're using a nonsubmit instead and not using form data. That's just personally. So I'm reading it, and it kind of looks -- I don't know. It's the same either way, right? But we're going to want a nonsubmit because we want to handle the return. So, we want a nice async function that we can call and then handle the return.
Jason: Okay. So, then what we'll be able to do is we can like await fetch of Netlify function subscribe. We'll send a method of post. Data will be new form data event.target. Then we can do like the body, JSON stringify. Let's do it that way. Then we'll be able to do data.get first name. What's that going to do? I don't know. Let's find out what that does. Then email was data.get email address, I think. Okay. So that should work.
**Joel: **You might as well do first underscore name because that's how ConvertKit wants it. They use Rails.
Jason: Okay. So I have my first underscore name. Then down here we can update this to be on submit, handle submit. Then let's console log the data as well. So we can make sure something is actually happening. So, let's go back. Refresh, make sure it's -- okay. We should see a console log here. There's our object, good. Then our log is out here, first name and email. So, I screwed this up. Let's see what our -- actually, you know what. Let's not bother. Let's do what you said.
**Joel: **I think the name is because of the form, the way it was using the brackets or whatever down there. So, if you just take that out and say first name, then it should work, right.
Jason: Yeah, I think you're right. Oh, because it was fields, not first name. Yep, yep, yep. Okay.
**Joel: **When we push to ConvertKit, you send fields. So, I assume that's why it's doing that.
Jason: Okay. So, now we get our first name and email. That's available in our serverless function. So, that is now being sent, and we have it here. So I can pull out first name and email, and those are now available to us however we want.
**Joel: **So, I usually -- up at the top, I like to set constants for various things. For this, we want two constants. One is going to be the ConvertKit base URL. So, just the API base.
Jason: And that's --
**Joel: **Then the second one will be your form ID. So whatever that is. And we can find that in the URL from the previous one that's commented on right now. Oh, or it's in there too. Good call.
Jason: Yeah, so we can just grab this. If we wanted to, we could even make this private now. But it doesn't really need to be. It's not a secret value.
**Joel: **No, that's something that is exposed. Right. So you have those things. When we get into here, we have our email address and our first name. What I want to do now is actually make a call to ConvertKit, and we're just going to post it. So, we're going to post it ourselves. I use Axios, so I configure it and give it a base URL. So the base URL might not be valid here for using -- but I assume we can use fetch here. You can do it that way too.
Jason: Yeah, I use node fetch because it's a much lighter weight nodule, but it doesn't matter in a serverless function.
**Joel: **Mostly what you're comfortable with and like to use as a team.
Jason: Yeah, it's definitely a preference when you get to this point. Boy, I talked about it being small and then it freezes up on me like that. Come on now.
**Joel: **That's the power of the internet.
Jason: Here we go. Okay. Like all the fans on all my computers are running at full tilt right now.
**Joel: **Yeah, so you can start writing. (Music)
Jason: So, we have node fetch. Thank you for the elevator music, chat. I saw a couple friendly faces out there. Nicky, Prince, Tony, what's up, y'all? Thanks for coming and hanging out today. I saw Will earlier in the chat. What's up, Will?
**Joel: **I see you, Kevin.
Jason: Let's see. So, we're going to get a response. It's going to be await fetch, and we'll use the CK base URL. Then we can add -- or wait. We probably don't want the trailing slash.
**Joel: **Whatever you like.
Jason: What's the end point we need to hit?
**Joel: **So, it's forms.
Jason: Then the ID, right?
**Joel: **Then the ID and /subscribe.
Jason: CK form ID subscribe. Okay. And this is also a post?
**Joel: **It is a post, yeah.
Jason: And is the body JSON encoded?
**Joel: **It is.
Jason: Okay. And it wants first name and email address. Is that right?
**Joel: **Actually, email address is just going to be email.
Jason: Oh, perfect. Even easier. Any other fields?
**Joel: **Yes, the API key. Because you're subscribing people and anybody could have the public form -- this is actually -- because you can post to it. To make a new subscriber, you need to send your ConvertKit API key.
Jason: Okay. And that value is up here somewhere. So, I can just grab it in our Netlify dev call. ConvertKit API secret. Okay. So, is it API key like that?
**Joel: **It's API underscore key.
Jason: Then is it just straight up or do I have to add anything?
**Joel: **That's just it.
Jason: Okay, perfect. So, we can send that. This is another thing that's worth calling out about serverless functions. Because this is happening in a serverless function, we can do this. We couldn't put this in client-side code because it would expose it. Like, anybody who wanted to could grab that and impersonate me and screw up the ConvertKit stuff. Because it's done like this, this API key never gets sent over the wire, which means it's not exposed, and we can safely make these privileged calls in a serverless function, which is very, very nice. So, then we can do like if response.ok, then we can return like a status code of 500, if I can type, and a body of, like, something went wrong.
**Joel: **So, I always return those as 200s, just because I don't want to blow up my app if the form didn't -- but that's like a personal choice, right. And we'll talk about that more when we get into like the hook, where we're going to load up the data. What I don't want is like a 500 to blow up the app or even throw a message, right. Like, if it fails, I want it to be silent. This is a little different because they're signing up. Providing that user feedback might be interesting. Error handling is a whole episode in itself.
Jason: Really, really is. But what I can do here now is I can just take -- so, what we're going is we're sending off a call to ConvertKit. We get back that response. Then we're checking to see if the response is okay. This is part of the native fetch API. If not, we can send back an error like this. If it is okay, then we get to here, where we'll serialize the body into a JSON object and send that back to the component. So, we'll be able now, when we look in here, what we'll get back, this is now our subscriber data. And we can access that. So, I guess what we can do is console log that response, and let's give it a shot and see what happens.
**Joel: **I like how you pronounce it with a French accent. JSON.
Jason: I have to, otherwise it sounds like I'm speaking in the third person. Just need to add a little Jason to this. (Laughter) Okay. So I need to restart our server.
**Joel: **Do you have to weight the two JSON?
Jason: You are correct.
**Joel: **Oh, I see. You're doing that here. But you're fetching -- does fetch do the same thing on the client?
Jason: Here, yes.
**Joel: **So, when we get the response, you'll have to get the data in the same way.
Jason: Yep. There we go. Yep, yep, yep. So, now we'll get JSON.
**Joel: **One day I'm going to read why that exists like that.
Jason: It's because you get different types of encoding back, but you don't want the metadata necessarily. So, when you get the response, the response has headers and the okay and status code.
**Joel: **I get that part. What I don't understand is why that one is also async.
Jason: Oh, I think they just made the whole thing like a promise chain.
**Joel: **Like, you have to await both of them.
Jason: Which is my least favorite async await code, when people do the nested awaits. Okay. So, where am I going? I'm going here.
**Joel: **And if this works, we're going to --
Jason: I broke something. What did I break? I broke -- module not found. What are you not finding? Are you not finding node fetch?
**Joel: **Did we restart?
Jason: I thought so. What are you looking for? Oh, just the next code broke for some reason. I'll just start it over again. It lost that manifest, which makes me think that it's doing something odd. There we go. Oh, yeah, I need Lil Jason. I like that because it makes it look like I'm looking at myself. That's even better. That's silly. Okay. Anyway. Are you done? Why are you complaining? What don't you like? (Elevator music) Module not found, cannot resolve mdx-js react. How did that even break? We're going to remove node modules. We're going to remove the next folder. And let's remove the lock file. Then we'll just run all this again. I know what I did. I used npm to install node fetch and it broke everything.
**Joel: **Mm-hmm. We're a yarn shop, sir.
Jason: I'm sorry.
**Joel: **This is like the FNG.
Jason: FNG? I don't even know what that is.
**Joel: **Fucking new guy, Jason. (Laughter)
Jason: I deserved that. Let's try this again. It's going to work.
Jason: It's going to work because I did things the right way this time instead of the wrong way.
**Joel: **I feel good about it. It's that optimism I was talking about, though.
Jason: (Laughter) Compiled successfully. Here we go. Let's try that again. All right. So, now we've got a page, and if I submit, unexpected error. Okay. What went wrong? Let's take a look. We've got our subscribe function. Our request went out. That's what we expected. Our response that came back must have a string body. What? Stringified here. Stringified here. Oh, something must have gone wrong in here. That's not good. So, what does it say out here? Did we get any responses?
**Joel: **That's my favorite reason to use await, actually, to be able to use try-catches.
Jason: So, that should show us our error, at least. Let's try it again. So, it says something went wrong. That's fine. It should log.
**Joel: **I don't know if this'll expose your keys.
Jason: Oh, it's probably going to, isn't it? Unauthorized, though. Okay. So, it's saying it doesn't have something that it needs, which is fine. Why doesn't it say it's authorized? Interesting. Okay. So, maybe we can --
**Joel: **The API may be off screen. Like, you could run it and move your terminal off screen and look at that, see if it's logging that. That would be the only -- one, is that correct? And is it being loaded correctly through Netlify?
Jason: Yeah, that's a good call. Okay. So, what I'm going to do is I'm going to log -- yeah, let's just log the environment variable. Okay. And I've got my console off screen. So then I'm going to come out here, submit again, look at my terminal, and it logged nothing. Interesting. Try that one more time. That should at least get an undefined, I think. It's like not even trying to log.
**Joel: **So, I'm also going to check in on Netlify to look at the environment.
Jason: So, that one has a key. The key is there, but it still says unauthorized, which means I'm doing something wrong. The key matches my -- I'll pull the --
You hackers. You dirty hackers.
Jason: That's not it. Let me pull this one over because we don't need this anymore. If I look at my keys here, I'm just going to double check my keys match. They do, yeah. So, it's getting a valid key. I must be sending it wrong, which is absolutely a possibility.
**Joel: **I don't know if -- somebody in chat said do you need application JSON anywhere. I don't know. I feel like Axios might put that on there by default.
Jason: Oh, yeah. Yeah, that's actually -- well, we're learning now why we should just listen to Joel.
**Joel: **I've seen you do this on several streams where somebody is like use Axios and you're like, I think I'll use fetch.
Jason: I used to use Axios all the time. Everybody was like, why do you use Axios, it's so big? What I'm learning is you just can't win.
**Joel: **Yeah, you can't win. There's no winning.
Jason: Okay. So this one, I think, worked. Let me turn off the API key logging because it's still saying unauthorized, but it's not because of the key. So, we can try that again. So that's reloaded. Now when I submit, it still shows me the 401 unauthorized. So, maybe we look at the API.
**Joel: **Yeah, maybe it's time to look at the docs.
Jason: Sad day, everybody.
**Joel: **We have to look at the docs one? That's not too sad. We're doing pretty good.
Jason: (Laughter) So, let's get the subscribe. So the API -- oh! It's the public API key, not the secret one. I'm using the wrong key.
**Joel: **Yeah, that makes sense.
Jason: That's fine. Okay.
**Joel: **I don't think you have the public key.
Jason: I think we do. Let's see if it gets pulled in.
**Joel: **Oh, the base URL is actually in your --
Jason: Oh, the base URL is already there.
**Joel: **It's one of your environmental variables also.
Jason: Convert API secret.
**Joel: **They have two different kinds. When you're fetching subscribers, use API secret. Whenever they say API key, they're talking about public. They say API secret if it's your secret key. That makes total sense.
Jason: Got it. So, here's what I'm going to do. I'm going to create a new --
**Joel: **Why don't we just --
Jason: No, watch, I'm going to show you.
**Joel: **I'll learn. I'm into it.
Jason: API public. Then I can drop that in because it's a public key. Then first and foremost, if I Netlify dev, it'll load. So, we have the ConvertKit API public. Then the other thing that I have is I can run Netlify env -- what is it, import, export? I should probably look up before I start.
**Joel: **Probably exporting.
Jason: So, it's -- import and set environment variables from an environment file. Yeah. Then we can get -- so, basically we can import that. If I run Netlify env import, please don't show -- I'm going to do this off screen and bring it back if it doesn't show all my keys. So, the command is Netlify env.import.env. I run it, and yeah, check it out. It just set that value to Netlify. So, we didn't have to log into the UI or anything, which is extremely nice.
**Joel: **I'm going to refresh just to look at that awesomeness on the Netlify page. That's good. Good stuff.
Jason: So, then now that that's there. I can actually delete this .env because it's a duplicate. Then I can Netlify dev again. (Barking) Oh, here we go. Here's the Corgi storm. I'm really glad I got the music back for this. So, let's close all these. We just want the one. We're back up. We're built successfully. Let's submit this thing. Oh, wait, we need to actually change --
**Joel: **One thing I love about this, you don't have to refresh over and over again to do the page once we're overriding. Makes this sort of debugging way easier.
Jason: So now we've got the public API key. It has refreshed that page. So I'm going to submit, and it's just going to work. It's just going to work. Here's our subscription. Hooray. So, check this out. We have a subscription. We have a subscriber ID. We could have set the source if we wanted but we didn't. Whether or not they're an active subscriber, all that stuff. I'm already -- I'm pretty sure this email address is already on this list.
**Joel: **It actually doesn't matter. What it'll do, it's subscribing you to a forum. You can subscribe to tags. You can subscribe to sequences individually. Since we're just subscribing to a form, it'll now add you in that form's bucket, regardless of your previous subscriber state. It'll notice that you have the same email and give you back your ID based on that.
Jason: Cool. Very cool. That's actually one of the things I really like about ConvertKit. If you're running different lists, they don't -- they, like, de-duplicate accounts. If I have 15 sites and there's a 60% user base overlap, my bill just got 60% cheaper because those people can be subscribed to all of my lists without me paying like once per list. Which is really, really nice.
**Joel: **Some of them will charge you for unsubscribe folks unless you prune them. There's some really weasely companies in this particular space. So, that's great. We're subscribed. There's -- one thing I'd like to do before we move on from the serverless function, because it's almost there, and that's to set an actual cookie for the subscriber ID. So, after you get a good response, when we know we have a good response, we're going to -- first we need to -- so, I use the cookie library. It's just cookie, which is server cookies. If you know how to write them by hand, you can do that. Otherwise, I use this. The server cookies are strings and have a particular format. It's one of those times where libraries are my friend.
Jason: Okay. So let's do that. I have -- const cookie equals require cookie. And that's the extent of my knowledge about the cookie library. So, what do we do down here?
**Joel: **So, we want to -- you can just name a variable or a const. We're going to set that to whatever you named it. Cookie. Is that what you named it? So, you can name this cookie or whatever. It doesn't really matter.
Jason: Yeah, I named this cookie.
**Joel: **We're actually going to use a serialize method.
Jason: Oh, look at that. Okay.
**Joel: **So, serialize just takes two arguments, maybe three. The first one is the name. For this one, I'm going to use CK underscore subscriber underscore ID. I'll show you why here in a moment.
**Joel: **And the second argument is the actual subscriber ID. In our case, it's going to be data subscriber ID, however that turned out.
Jason: Yeah, how did that come back? It was data. -- let's just get a new one. So, it comes back as data.subscription.id. Okay. Then do we need any other values or just let cookie handle the defaults?
**Joel: **So, it's not subscription ID.
Jason: It's not?
**Joel: **No, let's see. Yeah, see subscriber? I made this mistake. I remember this mistake very clearly. So, that's the subscription ID. The actual ID of the subscription you just made. There's a subscriber object. This is the one we want. So, that's you, the user. The other one is this new --
Jason: Subscriber ID. Like that. Okay.
**Joel: **The next part is the cookie parameter. We have secure, which I use like process.environment node. Env equals production. That's how I denote whether it's going to be a secure one or not.
Jason: I got you. Okay.
**Joel: **Http only, I set that to true on these.
**Joel: **Path, I just use forward slash.
Jason: Because we want this to be for the entire site, like anywhere on enterprisejamstack.com.
**Joel: **Yeah. And then so the max age is the next one that I set. I make these go for a year. If you don't set these, they're by browser session. I learned this the hard way. It was like weeks of people, why do I log out every time on Egghead? Then I learned this. So, to do this, it's like -- I say const hour equals 3,600,000 for an hour.
**Joel: **Yeah, that's pro style. Now you just set this as the set cookie header on your return. So, that's capital set-cookie.
Jason: Then sub-cookie like this?
**Joel: **Now, if this works, we should be able to go to our application tab and see.
Jason: Okay. So I have set it. Some cookies are misusing the same site attribute. Soon be rejected because it has attribute set to none. Whatever.
**Joel: **That's because we're local. In prod, it's not going to do that.
Jason: Because it will be secure.
**Joel: **You can probably get rid of those warnings pretty easily.
Jason: I don't care. If they're only in dev, I don't care at all. But now we have our subscriber ID. So this is now available to us.
**Joel: **What's really cool is you're almost using this authentication. Not really, but you are able to identify -- you use an identification, not authentication. We're able to identify the user, and this is really need and kind of the next step when we want to start using ConvertKit information to personalize the experience on the website.
Jason: Okay. That's so cool. So, that means then I can do something like if I go -- I don't know. Let's go to the homepage here.
**Joel: **This one we still need to do the redirect.
Jason: That's right, that's right. Okay.
**Joel: **So, you can do -- it's a next step. You can like use router.
Jason: I need to do this up here, don't I?
**Joel: **Yeah, it has to be outside of there.
Jason: Use router. That doesn't seem right.
**Joel: **No, it's just next/router.
**Joel: **So, that's const router equals --
Jason: Come on, spelling. There we go. Okay. So down here, I forget how it works. It's like router.read?
**Joel: **Push. Then just /confirm.
Jason: Okay. So, when we --
**Joel: **This would be a good time if you're using analytics to put that in there. It's a good space for that sort of thing, too. You spelled necessary without a pause, and that's impressive to me.
Jason: (Laughter) So if all of that worked the way we expect, then we go here -- I'm going to clear this cookie. I'm going to reload the page. Now I'm going to subscribe. What should happen is it should redirect us to the confirm page and set this cookie. So, let's do that. Okay. It redirected. My cookie is missing. But is that just because I need to refresh? Oh, I just needed to reload this. So, our cookie is here. It did what we wanted. I am very happy now. Cool. What happens next?
**Joel: **Well, next on my list, so we are submitting the form. We know who it is. What I want to do is now that we have this ID and this set, I actually create a ConvertKit context and put that provider at the top of the application. Then I use a ConvertKit hook so I can load the subscriber anywhere inside the application that I want. Because we set that cookie up the way we did, we can actually use it in server-side rendering. So Git server-side props in Next. So, we can deliver that in so you can use that server-side rendering to personalize the page. That's a potentiality we could get to. But right now we're going to set up the hook. I usually do this in a hooks folder, like a sibling of components.
Jason: Okay. So, we'll set up hooks. And use subscriber.
**Joel: **ConvertKit is my jam. But either way.
Jason: No, let's follow your naming conventions because I don't want to dump a bunch of stuff on y'all. And this probably needs to be TypeScript, doesn't it?
**Joel: **Yeah, so our Egghead app is using CustomerIO and a bunch of other nefarious tracking schemes that you can go check out if you're interested in a broader use of this. So, inside that -- I don't know if you -- like, there's a few different ways you can set this up. To me, the way to do it is we're going to create a context. So, we create a React context inside that file. Then we're going to export a context ConvertKit provider.
**Joel: **And then we're going to use the provider. This is kind of separating the provider and the context, so we're not giving everybody the full context all the time. I think there's like some rendering advantages to doing this. So, ConvertKit context. Just empty object works fine. Then this next one we're going to export the provider. Then you'll pass in children.
Jason: And this one is going to return -- nope, not like that it's not. It's going to return one of these. Then it'll be ConvertKit context.provider, right?
Jason: Then is it value?
**Joel: **Value, yeah.
Jason: And for now, we don't have one.
**Joel: **Just empty object is fine for now.
Jason: Okay. Then why are you not autocompleting?
**Joel: **There's no arrow function on line five. The fat arrow is missing. No, no, you're fine right there. You just needed a fat arrow. This is TypeScript anyway, so I'm going to have to come back through and do the typing.
Jason: I'm sorry. So, I have our ConvertKit provider. Then I can do context.provider here. Okay. It's so mad. TypeScript is so mad. We'll put the children through.
**Joel: **I don't understand why it's griping at you, to be honest.
Jason: Yeah, what don't you like? Value -- do you see this, chat? Do you see what I'm doing wrong? Cannot find name space ConvertKit context. It's right here. What have I done? Does anybody see this?
**Joel: **Oh, tsx.
Jason: Oh, oh, oh.
**Joel: **Thank you. That throws me off quite a lot.
Jason: Okay. So, now we're just going to get the --
**Joel: **So, you can change this. If you do a colon after the variable name then say react.FC.
Jason: Oh, nice.
**Joel: **TypeScript is happy now. Oh, TypeScript. Always sad.
**Joel: **Yeah, so now we have this -- like, we have a provider we can work with. So at the bottom of this, the last thing we'll do is go ahead and export default function. Use ConvertKit.
Jason: Then that would be, what, like the --
**Joel: **React use context.
Jason: What do you usually call it?
**Joel: **You're actually going to export react. So, use context and ConvertKit context. We're just exporting the context here.
Jason: And that would be ConvertKit context. Okay. Straight up.
**Joel: **So you get all the values.
Jason: In here we'd be able to do something like the state. Then we'll pass that state in here.
**Joel: **What we want to -- so, what we're going to load -- and the state is actually going to be two pieces of state. One, loading subscriber, which is a boolean. So, we need subscriber and set subscriber and loading subscriber and the subscriber itself. I would use state for these.
Jason: Set loading subscriber. That will be use state. Are these importing properly?
**Joel: **Looked like it.
Jason: That's start out as true because we will be loading by default.
**Joel: **Always by default.
Jason: Then we'll send that through. We'll do another one of these, but we'll change it to -- what was the other one?
**Joel: **Subscriber and set subscriber.
Jason: Subscriber. Wow. That was not my best work. Okay. Then this will just be unset by default, right?
Jason: Okay. Now we have loading subscriber and our set subscriber. I'm assuming we want a use effect here to load the subscriber.
**Joel: **Yeah, we're going to use a use effect. Typically, it depends. Do you like to use async await or promises?
Jason: I'm happy to do either. In use effect, unless it's going to be pretty gnarly, I use promises just so I don't have to declare an extra async function somewhere else. But it's kind of --
**Joel: **Yeah, so I just make it run. That's like my default. Just make a run function.
Jason: Async function run. That will allow us to await something. Then down here we just call run. Okay.
**Joel: **So, what we want to do now is actually load -- we have the subscriber ID. We can get that from a couple different places. I actually don't know -- and this is something I'm frankly confused on. I think maybe I do it wrong. When we have a cookie, right -- oh, I guess we're trying to -- I want to set it because there's a setting in ConvertKit where every time they click a link to any site, you can have the CK subscriber ID as a property. Have you seen this? I don't know if you have it set.
Jason: Oh, yeah. I know what you're talking about. Basically, it can show whether or not somebody interacted with content. Then we could use that, like if somebody comes to my site, they've subscribed, and then they only look at state machines content and none of my React content, then I'll only send them State Machines because that's what they're interested in.
**Joel: **When they click, it'll add a query parameter. ConvertKit will automatically add a query parameter to every link they click. So when they arrive at the site, even if they haven't logged in, it's going to cookie them in both places so you can associate different sessions across devices with the same user. But first we want to actually load the subscriber. Loading the subscriber from ConvertKit is almost the same serverless function as the other one. So, we have subscribe. Then we're going to have subscriber. So, we're going to go ahead and do that, then we'll get into this fancy tagging business across devices.
Jason: Okay. So, here's subscriber. I've got most of this -- we're in pretty good shape here. So, what should I do next?
**Joel: **All right. So, this is different in that we're going to load. We're loading from ConvertKit instead of sending them data. So, this is subscriber -- I believe it's -- yeah, it's a Git. It's no longer a post. We don't need to check the method. That's fine. We're going to -- and I usually -- so, there's a chance you're not going to find a subscriber. If it doesn't exist and you don't have anything, the first thing we need to actually look at is the cookies itself. So, I think this is the -- I have a cheat sheet over here. So, we're going to use the cookie library again. Except this time, we're going to parse. We're going to parse server cookies. The server cookies come across in the event. So, it's event.headers.cookie. So, you probably need to pass in the event.
**Joel: **I think that's like generally speaking the recommendation, to go for that. What you're saying is we can't access it on the client. So how are we going to actually use that cookie that we put in there? The answer is because we have serverless functions everywhere and we're on the same server, whether we're server-side rendering or calling a function, we have access to that cookie, even in its protected state, which is nice.
**Joel: **So, now you have the cookie. I'm pretty sure from that you can pull out the ConvertKit ID.
Jason: So, we can just get it straight up?
**Joel: **It should be parse cookie and CK subscriber underscore ID.
Jason: CK subscriber ID. And this is probably -- once we get this subscriber ID, that's probably the end of what we're going to have time for. We have about eight minutes left. We'll get this subscriber ID, which we should console log CK subscriber ID. Then if we just kind of take all the rest of this out for now, then we can do an ok and kind of test this. So, if I go -- whoops. Here.
**Joel: **We didn't add the provider anywhere. We're going to want to do that.
Jason: Right. So, the provider needs to -- oh, that's right. The provider is going to be at the probably app level.
**Joel: **Yeah, it's going to be underscore app, where you put that.
Jason: Pages, underscore app. Then we will import ConvertKit provider from -- what did we call it? Hooks. And use ConvertKit. Then we can take this, wrap the whole shebang in it. Okay. So, now we should have access to that. That should fire off a call to -- let's do -- it'll be const subscriber equals await, fetch Netlify function subscriber, and that's just going to be a get. So we don't need to do anything with it. Okay. So, that should be fine. What I can do here then is instead of trying to -- we can just churn that ID.
**Joel: **Take that out. There you go.
Jason: Okay. So, that, theoretically, should do what we want. Let's just try it. Let's see what happens. Then we will console log our subscriber. So, before -- basically, by reloading the page, we should see this log immediately because we just wrapped it with the context, which means all of these pieces are there. So, let's take a look. Reload the page, and it doesn't like -- each child in a list should have a unique key prop. I kind of don't care about that. What are you getting over here? Subscriber, 404 not found. Oh, I have to stop and restart because we created a new function. That's why that happened. Okay. So as soon as this goes. Do-do-do-do. Okay. Call subscriber. Our response is a subscriber ID. Holy buckets, y'all. So, then from here, we have access anywhere -- well, we haven't set the context and everything.
**Joel: **Let's do one more thing. We're going to -- inside of our subscriber serverless function, we're going to use that ID real quick. So, we're going to -- that's a Git method. We're going to need to change the URL, the base URL we're using. The CK base is fine, then /subscribers. Then you're going to use the subscriber ID.
**Joel: **Then we're going to add a query pram to this. API underscore secret. Then process environment and use your secret.
Jason: Okay. So, this will send off a request.
**Joel: **You can get rid of the body.
Jason: Get rid of the body because it's a get. Okay. We can get rid of the headers. Okay. And we actually don't need anything at all because it's just a get. So, we can leave it all as default. So, we'll run that.
**Joel: **You probably need to do the response. You can just serialize -- I'm pretty sure we just serialize the response from that. That will be the subscriber itself.
Jason: Okay. All right. Let's try that one more time out here. We get our subscriber, and now we've got details.
**Joel: **So, check the fields. This is really interesting when you start to think about this because now we have all of the fields for that. If you use fields in ConvertKit, you can -- and you have to pre-set them up, but like favorite tech or job title. These are things -- like job title in particular, is this a person that manages a team, or is it a senior developer? Is it a junior developer? How do they identify themselves as a developer? So, what I would like to do that we don't have time for it in between when they hit the button and submit their email, before we get to the confirm screen or on the confirm screen, I'm going to pop up a light survey. It's going to say, hey, what's your job title? Give them that simple list and set that property. They're almost always going to click that because it's innocuous. It's a thing most people can answer. It gives you just one data point. Then over time, like across the app, if they're on a particular page or if they're reading an article, what you can do is actually go through and because you can load that subscriber from anywhere, you can take what survey said they answered and what is next. So what you can do is instead of always having a sign up for my email, now we can do something like answer a survey. If there's nothing left, tweet this post.
Jason: Oh, yeah.
**Joel: **But we're going to have one and only one call to action on any given page. Because this is pretty slick and honestly captures them from so many different devices -- when they click a link in ConvertKit, and we didn't do this. Inside the use ConvertKit hook, you can go in and check the query string and look for CK subscriber ID and pull that out. Then load them in and cookie them. So, I'm also -- when I load subscriber in that subscriber function, I'm going to also cookie -- like set that same server cookie and set that header because I might be loading subscribers for different reasons. I always want to make sure my cookie is fresh and put a year on it. Then when they click that link, they come in. Brennan calls it a Plinko board. Basically, you have all this data. You know what they've answered. You know what they've done, which means you can always present a call to action. This goes full circle because it goes back into your email, where now if they've answered that and told you, yeah, I'm a senior developer, I'm a manager, you can switch on that particular value in their emails and change -- it's just nudging the copy. It might be a subject that's get this for your team versus get your first job, get your next job, however you want to look at it.
Jason: Yeah, yeah.
**Joel: **It gives you this way to like present them. I'm honestly not terribly sophisticated in the usage, kind of ham fisted. But the potential is pretty great.
Jason: Yeah, and this is -- I mean, this is amazing stuff. I wish -- I wish we had more time, right. There's so much more we can do with this sort of stuff. But unfortunately, we are literally out of time. So, we got to wrap this up. I'm going to do a couple things here real quick. First, I'm going to send everybody over to Joel's Twitter. Do make sure you follow Joel. Also, if you want to check this out, the site is up at enterprisejamstack.com. We will be rolling these changes out. If you want to follow development as this gets put into place, we're going to do it all in the open. So, go check out that repo. You can see as things happen. The Egghead site is also on Next. It's got a bunch of these automations already in place. And if you want to go try Egghead in general, I highly recommend it. It's full of good resources. With that being said, I'm going to do another quick shout out to our sponsors. We've had Rachel from White Coat Captioning, who's now into overtime. Thank you so much, Rachel, for hanging out with us. That's made possible by Netlify, Fauna, Auth0, and Hasura, who all kicked in to make this show more accessible to everybody else. Definitely go check out the schedule and add this to Google calendar. We have so much good stuff coming up. We have Word Press, next.JS. We're going to be monitor and air tracking. We'll learn how toot state machines on Kubernetes. I don't even know what that means. We'll learn esbuild. We have Benn coming back to teach us view stuff. So many amazing things are happening. Jennifer is coming. A bunch I haven't even put up yet. Lots and lots of good stuff. Make sure you check that out. Joel, is there anywhere you want people to check out that I haven't just linked to?
Jason: No. (Laughter) Okay. Joel does run a blog that I should have mentioned at Joelhooks.com. Full of a bunch of brilliant advice.
**Joel: **It's eclectic.
Jason: It is, indeed, eclectic, but really, really good. There's all sorts of good stuff you should check out. With that being said, Joel, thank you so much for hanging out today. This was an absolute blast. Any parting words before we wrap it?
**Joel: **No, I don't have any parting words. If you want to see the full extent of this implementation, it's all on our -- on the Egghead next link we posted above. So, you can kind of trace it around and see the full thing.
Jason: Excellent. All right, Joel. Thank you so much. Chat, thank you, as always, for hanging out with us. This has been an absolute blast. We're going to find somebody to raid. Joel, we're going to have to have you back. This was super fun. There's so much more I want to explore. So, we'll make time for that. In the meantime, thank you all for hanging out, and we'll see you next time.