Transcript of Astro Search Tutorial (Full Tutorial)
Video Transcript:
Many sites need some kind of search functionality. Here I've got a basic Astro blog and we're gonna build out this search. So I can type something like Markdown and even if I misspell it, you can see that I actually get Markdown. It's a fuzzy search and we're doing this all client side. Now, one of the hardest things with Astro when it comes to adding search is just deciding how you want to do it. By default, Astro ships no client side JavaScript, but it does include options for including things like React or Svelte or some of these more dynamic frameworks. But before we get into how we're gonna do this, let me show you what we're going to build. As you can see here, we're gonna build something on the homepage where we can type in like MDX. Even if this is misspelled, there it goes, it gets MDX right here. And then it will actually list off anything that matches our search query. Now, when we're actually on the search route, you can see as I type, not only does it update my search result here and it makes sure that this stays up to date, but also it's updating the URL and also even the title of the actual page itself. And you can share this with anyone and then they would get the exact same search results you do. This obviously links to each of these pages. So I can click in here and see that as well. So you can add this kind of search templating anywhere on this Astra site. Now, in order to do this, we're gonna have this search component here that we're gonna create right here. And as we search, it's gonna basically grab JSON content of our posts. Then it will return that to us and use a library called FuseJS to then do the fuzzy searching. Now I'm using Fuse because it's super lightweight. So you can run it on the client without a lot of penalty and actually get back live search results as people type. We're actually setting this off as its own route. And that way you can share that route to somebody else. And they basically get all those results when they enter in that URL. We're also gonna protect this with something called DOM Purify that will allow us to strip out any malicious code that a user might add. In the end, we have a lightweight performant client side search that allows you to add search to any kind of basic site. Now, we are going to be using static site generation. And this is where I wanna start talking about how you might implement this in Astra. I've chosen a particular path, but there are lots of different options available to you. By default, because it serves no client side JavaScript by default, we usually, when you go to an Astra page, it sends a request to the server and the server sends back HTML. There are basically two paths for you though. If I come down here, you're gonna see, you can choose a static site generation, which is the default. What that does is basically pre-build all the HTML pages. When you go and ask for a page from the server then, it basically gives you this. Let me go ahead and drag this over here. And it just passes you back an HTML page because it's already pre-rendered and ready to go. It's statically rendered. So it's very, very quick. However, you can also do something called server-side rendered. So in Astra, you can choose a render that allows you to do this. Basically, the difference is when you request, it doesn't already have the HTML, but it has the code to generate the HTML. So it will generate the HTML and then same kind of thing, it passes back a plain HTML page. Now this is different from something like React. What React does is the very first time you get back, you don't get back HTML, you get back JavaScript. And then every subsequent request, every time you wanna ask something else, you can come in here and you start to ask and guess what interrupts it? JavaScript, that's right. And then it grabs this and it basically says, hey, I'm gonna just fill the DOM with what I know it needs because I already got my initial page load. And now React will basically intercept that request and generate that back for you. Now Astra gives you a couple of different options. You can actually do just static site. You can do just server-site and this should say server-side rendered or actually rendering technically. So you could server-side render, which again does all this. You can also mix the two to where you primarily use static and occasionally use the server-side. Or you can do it the other way where you primarily use server-side and occasionally pre-built pages. What we're gonna do is stick with the most basic. This is the default right here. And I'm gonna trust that if you know how to do server-side rendering, or if you know how to do the hybrid options that they give you, that you'll be able to figure out how to apply this to your particular use case. So we're gonna keep this as simple as possible and basically say, how can we just use client-side JavaScript on these HTML pages that get sent back over here? And let's say we have some JavaScript that then gets embedded here as well. This is client-side that we're sending back from the server that's been pre-built. How can we use this to basically generate our search? Now, like I mentioned, you could use something like React if you want to and have a particular React component. You could also do this server-side so that when you hit a request for the search page, it does all the logic and then spits it back. But again, I wanna keep it the most simple implementation or just a static site search. And just to briefly mention, you can grab all the code you need for all eight lessons directly here on my GitHub link, which will be in the description. Lesson one is this lesson and then subsequent lessons, you can grab the finished code from each of those if you ever get stuck. Now, even though there are eight parts, it's a relatively short series. In fact, I've released the entire thing as one single video for all my Patreons. And when the series is done, I will post it as well on YouTube Live for everyone. Okay, are you ready to build this thing out? Let's go. Hey, what's up? My name is Chris and welcome to Coding in Public. All right, so with that preview out of the way and the access to the code in the description, let's go ahead and get this thing up and running. I've got the astro.build website up. I'm just gonna go ahead and copy this opening command. We'll actually add astro search as the name here. And that way we can just go ahead and skip that first question, which is what should we name the project? As you can see, I'm using 2.66. You can grab the same code if you wanna do that or whatever you're using should work with the previous version of the project. What we're using should work with the project that we've got started. I'm gonna use the blog template. And the reason I'm using this is just to speed things up a bit on our site. Let's go ahead and install the dependencies. All right, as far as TypeScript, I don't wanna over-complicate this tutorial, so we're not gonna use TypeScript. And let's go ahead and initialize a new Git repository. And that's it. Let's go ahead and cd into astro search. And I'm gonna open this up with code .mb. Right back with you. All right, so just like that, we've got this open VS code on the left and here is just the astro.build on the right in Chrome. Let's go ahead and now come inside here and just type npm run dev. This dev command is what's given to us by astro in the package.json by default. That should come over here and we should be able to open up local host and it's 3000. All right, there we go. And I think I'm a little zoomed in here, so let's zoom out a touch just to see the normal size. So just like this, we've got this blog. This is what we're going to query right here. Looks like I've used this same template in the past because it's remembered I've clicked into it. All right, so if you've gotten this far, you're up and ready to go. So I will see you in the next lesson. Lesson two, where we will add our search input bar. Welcome to lesson two. Let's go ahead and type npm run dev to get this spun up in our terminal. And that should open up right over here at local host 3000. All right, so what we're gonna do is go ahead and add a component somewhere up here. Let's just do it maybe right below here. So let's come inside here into our pages directory and we're gonna look at our index.astro page. Let's go ahead and close down the sidebar. Let's also minimize that terminal. And then let's come right inside, actually maybe how about right here? All right, so we're gonna add a component in right here. So we'll call this like a search widget or something like that. But in order to pull it in, we've got to create it. So inside our components, I'm gonna add a new file and we'll just call this search widget.astro. And for now, let's just do like a paragraph tag that says hi or something like that. And then we'll come over here and replace this with search widget like that. The Astro extension allows you to just import these things automatically and I'll close that tag out. And you can see it's been added right here. You don't have that, you can come over to extensions. It's this one right here and it's a must have if you're working in Astro. Okay, so with that said, we should see hi and we do. So we've got something started correctly. I'm gonna come over here then and let's actually template this out the way we want it to look. So I'm gonna put this inside of an aside tag since it's kind of a complimentary thing to the main content of the homepage. And then inside here, I'm gonna have a form with a class of form just so we can style it a little bit differently. For now, let's now have an action. And inside here, I'm gonna have a couple things. First of all, I'm gonna have a div and this div is going to hold two items. It'll hold a label, which will point to an idea of search that we'll add in a second. And this will say something like search the blog and then we will have a span. So enter a search term or a phrase to search the blog. All right, so you see it's showing up right over here. This is my label and this is that span. Next, outside that div, we're gonna add one more thing and that would be an input. This will be a type of search. The reason I do search is because as you type, you get this little X and you can also hit the escape key and that will clear it as well. So you get that by default. So why not use that? We will make sure that this is required. I'm gonna say that you need to type in at least two characters before you hit enter. And then the max you can type is something like, let's do 24 maybe. Finally, I'm going to give this a name of search. We'll use that name to grab the form data and then I'll also add an ID of search as well. This ID will point to the label so that these two things are connected. And finally, let's add a placeholder. This will say something like enter a search term. All right, cool. So you see all this showing up right over here. Now let's go ahead and add some styling and we're gonna do something very similar on the actual search route that we'll create in a future video. So what I wanna do is use this class and I think I'll just globally declare these styles. Usually if I have like a little component like this, I'll actually declare a style tag down here which scopes these styles to the actual things in the component, the HTML in the component. But in this case, I'm gonna go ahead and open up my CSS file. I think it's just one global CSS file. And this is by default comes with the blog template. So up above here, we're gonna do a couple of things. First of all, I'm gonna actually declare something on the HTML. You know when you get like a site, let's come over here and actually close this down. You get this like scroll bar right here and then when it disappears, all the content shifts like that. See how that moves one way or the other. There's actually a new property out called scroll bar gutter and if you put stable on this on browsers that support it, which is not very many right now, it will minimize that. So in other words, if I come down here, it'll still reserve that space for the scroll bar and so you don't get that jumping back and forth. So I'm gonna add that to everything and it may not work on your browser, but I mean, by default, you're just gonna get whatever the default is anyhow. So it's an easy one to add for browsers that support it. Next, I'm gonna come into the body and I wanna add a min height of 100 VH. Now the reason I'm doing that is if I come like, let's come over to the about or how about the blog itself? You see how this is the body right here and it's only taking up this much of the viewport. So it's kind of stuck to the top. So what I wanna do is actually spread out all this middle content right here to take up most of the space and then just leave the header and the footer to take up the space they need. So to do that, I'm gonna do display grid and grid template rows and here what I want is auto and then one fr and auto. If I save it here automatically, you can see that the footer goes to the bottom of the page. This is the main content and the header stays at the top. I pull this up and you can see even cleaner. So here I've got the body. This is auto, it only takes up the space it needs. This is what takes up most of the space and then finally this takes up whatever's left, the auto as well. So just to make it a little bit cleaner so that as we add posts, they add in here and the footer isn't moving down as we go. Okay, so that's some basic kind of global styling. Let's also come in here. Let's come right below the body and add something for our form. So we're gonna say form and I do want this to be display of grid and so that we can see this. Let's go back to the homepage. Once I change it to grid, you'll notice that these are each grid children. So they take kind of the full allotted space and so we'll change that around in here in just a second. So gap of one rem. Here I'm gonna set a max width, which should also cap that search term to the max width of the form itself. We'll do something like 32 characters. Now let's set a border on here. We'll do pick two pixels solid and then we'll do two, two, two. This is one of the colors that they use on the blog by default. And I'm trying to really do my best to keep CSS to a minimum in this series. Next, let's add a border radius of like 0.5 rem. We'll add a padding of one rem. All right, just two more things I wanna add. Let's grab the form and let's grab the label inside of this. And I wanna set this font weight to bold and I'll set the font size to two rem. And finally, just to make sure all of our font properties are inherited, let's just grab the input itself, which would be the case for any inputs on the entire site. Let's set the font to inherit. You can see now it actually takes in a lot of those properties and makes it a little bit larger as well. All right, so that's the basics of adding a search widget. Now you can see here, if I start to type and I hit enter, the default for any browser is just to submit to the current route and to add on search equals whatever. What we're going to do is actually redirect this based on the submission. So when it submits, we wanna redirect to a new route that we're gonna call search and then we'll have our query off the front of that. So something like query or queue or we could do search or whatever and then it will be whatever that happens to be. Now, if I do this right now, you're gonna see that this does not exist, but in the next video, we're gonna set up the redirect and then finally, after that, we'll set up this search route. In lesson two, we set up this very basic input here where I can type in something and if I hit enter, it will submit to the current page. What we wanna do is capture that submitted event and redirect it to a new route we're gonna call search. All right, so let's go ahead and do that. I've got npm run dev up and running. What I wanna do is just come inside this search widget component we created last time and I'm gonna add a script tag. Now, this will go ahead and add this as a type of module. It will minify it and do a bunch of other stuff in Astra by default. So what we're gonna do is capture the event on this form. Let's go ahead and grab the form itself. We'll grab reference to it with document.querySelector and you might remember we've got a class of form. We've also just got the tag form. Let's just do that because again, this should be scoped. All right, so with that said, what we wanna do is add an event listener and we're going to listen for a submit event. Now, by doing the submit event, when they hit enter or if we had like a submit button, either way it would actually submit that for us. Once the form is submitted, this event will be fired. So I'm gonna grab the event which we'll just call e in this case and then I'm gonna go ahead and come down here and e.prevent default. Then just to make sure we see what's going on here, let's go ahead and console.log the e.target which would be the form itself. Now, you may have noticed that prettier here I think it is or maybe it's the Astra extension. I forget, added this question mark just to say, hey, just in case the form doesn't exist, let's go ahead and check first before we add this event listener. But that's what that is. Okay, so let's come over here. I can either open up the console over here or I've actually got an extension called console ninja that will show me a readout here as well. But as I type, let's come over here and say something. And then if I hit enter, you'll see that I get this right here. And I thought this would console log but for some reason it didn't. So here we go right over here. And notice it did not actually update the URL up here. This is from last time. So if I come back here and do the same thing, whatever I type, you'll see that I get this but it doesn't actually submit to the page because we've prevented the default. So what do we wanna do here? Well, what I wanna do is capture the actual text from that search input box. So there are a bunch of different ways to do this but typically whenever I'm submitting forms, I like to use the form data API. In this case, there's only one item. So it's probably overkill but I just do this as best practice. And so that's what we're gonna do. We'll say const form data equals new form data. And I wanna pass it the actual form itself or I could do e.target that happens to be the exact same thing but you can see that what I'm going to get back, if I go ahead and console log this, we run this one more time, whatever I type, you'll see that I actually get an object with several different methods. So I can append, I can delete, I can get the entries, all this kind of stuff I have available to me. So for instance, I could come in here and say, I want form data.get and maybe I wanna get the entry of search. That means if I come in here, I'm now running the method off the back of that and here I get the form data and this is the value of that search. So let's get rid of this console log. We're gonna capture this in a variable called search term. So we'll say, what we want is that right there and then let's go ahead and just make sure that it's a string. So if it exists, we'll convert it to a string using that method there. All right, so let's come over here, we'll add this and let's see, we forgot to console log it so we can't see what's going on but now if I do that, you'll see that I actually get a string and it looks the same as what we did before but now I'm actually converting that to a string and now console logging it. Now, by default, we actually made this required so I cannot submit the form. It's gonna tell me I have to submit something but just in case for some reason somebody does that, I can come in here and just say, hey, if there's no search term or if the search term.length equals zero, then I'm just gonna go ahead and return. So don't do anything else. However, if there is a search term and it's longer than the length of zero, then I wanna create a new URL and we're just gonna capture this in a variable called URL. We'll say new URL and here I wanna point it to search and I'm gonna use the base as my window.location.origin. So whatever the origin of the actual window happens to be, that will be the base and then it'll tack on forward slash search and the reason I'm doing it this way is because you can do URL search params and add a search param directly on here and we can set one and we can call it whatever we want. We can call it search. If we want it, we can call it queue. Maybe let's just do that and then what I wanna do is just pass along at the search term. Finally, with that new URL set and the search param set to it, let's go ahead and change the window.location and we're going to assign it to a new item and that would be the URL.toString and because the URL that we've created right here is an actual URL class, I need to actually convert this to a string. There are a couple other ways to do that as well and I'm assigning it so that you have history in the browser. So in other words, I could also replace this here but what that would do is not allow you to go back to the homepage or wherever you came from. So by assigning it, it basically keeps the history of your movement through the browser. So if I come in here and add search, now it should go to search and you see that I have queue equals search because I added queue right here. Now this doesn't exist so we're getting a problem but we'll fix that in the next video. Now there's one other thing I wanna think about and that is somebody could add something malicious in here. So like let's say they had an image and said like on error and then they ran some kind of malicious code. All right, so let's go ahead and close this down. If I enter this, you'll see that I get all this stuff in the URL. Well, I wanna account for anything malicious and so let's go ahead and strip this out because this is going to be in a URL that somebody could share and I wanna make sure that as I process that data and show it on the page that we don't have to worry about something like that. Now in this case, I don't think there's a whole lot you could do but it's always a good idea to strip out any possible malicious code from user input. There are several packages we can use here but what I'm gonna do is go and open up the terminal here, close this down and then we'll type npm i and I'm gonna use a package called dom purify. All right, and then npm run dev. Now remember that Astra makes these modules so that means I can import here. So I could import dom purify from dom purify. Let's go ahead then and when we get our search term right here, we're gonna go ahead and wrap this in dom purify.sanitize which is a method that comes along with it. We're just gonna sanitize that string. In other words, what it's going to do is basically strip out any possible malicious code. So if I came back in here and did our same thing like on error and then I did something, something, I'm gonna close this out and hit enter, you'll notice that it's stripped out a bunch of that stuff. So you can't really do anything malicious and there are a bunch of like customizations you can add to dom purify. I've done a video on it in the past, you're welcome to check out. So we've handled the form event, we've stripped out any possible malicious code and we've redirected to our new search route. That does not yet exist but it will in the next lesson, lesson four. In lesson three, we redirected this search widget successfully so I can type in a search term and it goes to a forward slash search. This does not yet exist though so let's go ahead and create that. What I'm gonna do is come inside the blog and just use this index.astro as a template here. Let's go ahead and copy and paste this and then I'm gonna call this search and we'll move it out of the blog folder into the pages directory. So as soon as I do that and then save it, now over here, this should work, all right? And it just looks the exact same as my blog right now but we will update that here in just a second. So first of all, I'm gonna go ahead and strip out this post because we're not going to be loading that on the page which also means I can get rid of this right here and eventually we'll also get rid of this as well. Now you can of course stylize this however you want. For now, this is the way I'm gonna do it just to keep things as simple as possible. So since we removed the post, let's also strip out looping over those posts and now I wanna do a couple of things. I'm gonna go ahead and leave this site all here, that's all fine. We could update this and say like, it's search or something like that but we're gonna actually update this in a second another way. So I wanna modify one thing and add two others. So by modifying it, what I mean is I wanna add in an ARIA label. This will basically set this off as a region and say that we want it to be about our search results. Next, I'm gonna add a paragraph with an ID of search readout. And this will be where we read out whatever the search term happens to be. Then finally, I wanna add an aside and we're gonna add another little search form right here where you can search on the actual page. Now this will be sufficiently different that we're gonna copy a lot of the code over here but we are going to update it for our use cases over here. So I'm gonna come in here and actually just remove that and paste that in there. And I'm gonna grab this class and apply it to the aside and I actually don't want this to be a form because I'm not gonna submit this. I'm just gonna handle the input whenever it changes. And because we use the class of form to style everything, everything should still look exactly as we expect except we're gonna handle the functionality different. So if I come in here and start typing here on the search route, nothing's gonna happen because again, it's not a form so it won't submit. We're gonna handle this a different way. The first thing I wanna do though is when we land on this route, I wanna see, hey, is there any query that's already existing? So we're gonna write a bunch of JavaScript here and I'm gonna do this inside of a script tag. You could also import this and have this as a separate file which probably would be best use case but just to kind of keep things as simple as possible, all the rest of the code we're gonna write in this tutorial is gonna be exactly in this script tag. So to start with, we need to grab a few selectors. I'm gonna grab the search selector. We'll just call this document.querySelector. And what I wanna select is anything with the ID of search. You can see that that is this right here, this input. Now notice when I save this, that I actually move this up inside my body and that's because the HTML tag actually needs to wrap all this. And the other page, it was just a single component here where I actually have like an entire HTML page declared. I also need one other thing and that would be my search readout and that's gonna be very similar to this. Just search readout. Now with those two things declared, what I can do is run an event on the window itself that after it loads, I could say if there's anything here, I wanna display it right here. So let's go ahead and come down here and say like event listeners just to keep things organized. And here on the window, I'm gonna add an event listener. And this will be DOM content loaded. And let's run this inline here. So whenever the DOM content is loaded and maybe let's move this over just a touch, I wanna first of all see if there are any URL params. Now the browser gives us a way to actually get the search params from the current window. And I can do this in a couple of ways, but one of the ways I can do it is URL search params. That would just be window.location. Let's go ahead and just console log this to see what we're getting. So when I do that, you're gonna see that I have a little readout here from console Ninja. It gives me my search params size. All right, so that's everything here. What I want is not that, but .search. And off the end of this, I wanna get the query param like that. Now, when I come over here, you're gonna see I'm getting search, which is the thing I actually wrote. So maybe let's change this just to make sure that we're getting what we think. So let's say C and I'm getting C right here. Okay, cool. So I'm actually getting what I want. Now what I wanna do is use that to update the text inside of here. We'll do that by setting the search.value to whatever those URL query params are. You can see, as soon as I save it, that's what I'm getting here. I'm getting C right in here because that's what's up here. If I were to retype this here and hit enter, you'll see that now we get all this as well. Now, because this is usually coming from the homepage, there's no malicious code if they entered it there, but you could technically come in here and add your own thing. And then you see that all that gets displayed right here. So I actually wanna do the same kind of purification here as well. So let's go ahead and add an import here and we'll import dom.purify from dom.purify. And then once again, let's go ahead and wrap all of this, dom.purify.sanitize, and that will sanitize our input. And when I land on this page, in addition to showing whatever the search is, I want them to be able to change it quickly. So one thing we could do is add search.focus. That will focus the browser. So if I refresh, you see it's just automatically focused on that input. There's two other things I wanna change briefly. One is to add the actual readout of whatever we're searching right here. And the second thing would be to update the title here in the browser. So we're gonna update those with two different functions we're going to write. One we'll call updateDocumentTitle, and we'll pass that our URL params. We'll add one more that we're gonna call updateSearchReadout. So let's do these one at a time. So updateDocumentTitle does not exist yet, so it is properly yelling at us, but let's come in here and add some functions. Say function, updateDocumentTitle, and this will take in whatever search we pass it, that URL params. All right, so in this function right here, what I wanna do is update the document.title, and I wanna set that to whatever the search happens to be. So you can see here that that's exactly what it does. That's not quite right though, is it? What I wanna do is say if there is a search, then update it like this, otherwise do something else. So we can do that quickly here with a little ternary. So if there is a search, what do I want it to read? Well, in backticks here, I'm gonna add search results four, and then here we'll add in quotation marks the search. Otherwise, I probably just want this to read something like search the blog. Now, of course, on page load, it's gonna actually change it up so I could change the actual text up here to start like that too. So I could say that this would be something like search the blog, and that way it doesn't have to update, but as I live type here, eventually it will get to a point where there are no search results. There, I also want it just to say search the blog. So that's why I have a ternary here, so it kind of accounts for both of those use cases. All right, let's also now create this updateSearchReadout, and I'm gonna go ahead and copy this function down as well. We'll just say updateSearchReadout. Now, in this case, I don't wanna update the document title. Instead, what I wanna update is the search readout. Now, we can do this in a couple of different ways, but to be safe, even though we've purified it, I actually also wanna just set this as the text content. That way we're not introducing HTML that's been typed in by the user. Now, here you'll notice because I've got no search, if I just leave this the same, it's gonna say search the blog down here. That's not exactly what I want. I actually want this to be an empty string. Then over here, I'll say search results four, and that should be just fine as well. If I do come in here and I add something, whatever this happens to be, you'll see I get search results four, and then eventually we will display those below. In addition to that, my document title has been updated, and now this text also reads the same. And when I refresh, you'll notice that I can immediately start typing because that is focused. Now, we've already done all this work. We can actually do a lot of the same things on change on that search itself. So let's come in here. We're gonna add the search right here. Remember, we grabbed access to the actual search input up top here. So down below, what we're gonna do is not check on the DOM content loaded, but on input. So whenever this changes, I wanna actually grab the search term that's being typed in. In this case, let's change this up, and this up, and this up, and this up to search term. I wanna DOM.purify, not the whole stuff from the URL, but the actual search.value. With that said, I wanna update the document title. I wanna update the readout. This is on text as I start to type, but I don't need to do these two things. So let's go ahead and get rid of those. So now if I were to come in here and I start to type, you're gonna notice that this updates, this updates, and this updates. So all this updates live as I type. And if I go all the way to nothing, it doesn't show anything down here, and this just says search the blog. And that's why we added this and this as options. All right, so as we type now, this is live. And because this is actually embedded in the URL, I can send this to somebody else, and they'll get the exact same search results as I did. Now, when we type, not only do we wanna update all these visual things, but we actually wanna fetch and see, hey, let's grab our data from our blog and then start to process it. So in the next video, what we're going to do is create a JSON file of all of our blog posts. In lesson five, we wanna generate an output that will be built every time we statically build our site. This output should contain all the post data that we need to then query when we type in our search. Now, really quickly, if you are behind or struggling at all, you can remember to grab the code. Just make sure you go to lesson four from the last lesson, download that, and you can pick up right where we're at right now. Now, why do we want to do this? Well, essentially what we're wanting to do is type in a search term. When we search, we then wanna come over here and grab all the content of our posts. This will just be JSON content that then gets loaded onto this search route. Then we can query that with client-side JavaScript, and that will allow us to basically search it using our fuzzy searcher, which we'll use in a future video. Now, Astra allows you to set up different endpoints. These can be any kind of static files like JS or TS files that then get put as JSON files. You can also do images, as you can see here. So there's a bunch of things you can do. What we're gonna do is basically create a JSON file and then basically query that every time we type in our search field, once again. So let's come over here, and underneath the pages directory, which is where you have to put it, we're gonna add something called search.json.js. Now, you need to export in a sync function. This function will be called get for a get request, and the function itself will simply return a new response, which will be JSON.stringify, and I'm going to stringify await get posts. Now, this will be a function we'll write in a second, but as the second argument, I need to actually pass in a status, so I'll say status of 200, and headers, content-type, application-json. Now, if I save this, we're gonna get an error. So let's go ahead and come in here, first of all, and just write this function up top. So this will be an async function called get posts. For now, let's just return like a string with, I don't know, nothing in it, or we could say like hi. All right, something like that. So we've saved that, let's now come over here, and I'm gonna add search.json here. You can see that I get a string with search.json. So I guess now that I think about it, this doesn't need to be a string, it can just be an array like this, and then here we go, it gets stringified here. Okay, so now that we've got this, what I wanna do is actually query all of our posts. Now, there is an actual route on this that does that. That's, let's see, right here. You might remember that we deleted this when we duplicated that page. So I knew it was already over there. Let's go ahead and use the same thing here. So I'm gonna go ahead and await that collection. If you hit control in the space bar, you can grab that from Astro content. Now what I can do is simply return the posts. Now, as soon as I do that and refresh here, you see I get all of my posts, which is pretty cool, right? Now, just to make sure we know what we're getting here, let's go ahead and console log this. Because this is server side, it should show up down below here, yeah, down here. So just to make it a little bit easier to see, I'm gonna go ahead and pull up console ninja and show this off to the site. Now, because I'm using Astro, I need to switch over to see the server logs and here it is. So I'm getting an array of posts and each of these posts have an ID, the slug, they have the body, which is the content that I get. So all the markdown that I wrote on that, you get a collection, you get all the data, which is the title, the description, the pub date, the hero image for each of these. Now, I don't actually want it to be this big. You could, but then you're having every user load this and once you have like 500 posts on your site, this will be a very slow route, the search route. So what I'm gonna do is kind of cool this down a little bit and just get exactly what I want. So you can return posts. I'm gonna go ahead and leave it there in case that's what you wanna do. You'd have to change a lot of the other code because I will basically be processing this and only returning what I want. In that case, what I wanna do is return the post.map. I wanna take each post and I wanna return an object for that post. So to return an object directly here, we need to wrap it in parentheses. Each object I want to have several different properties. Here, I want it to have a slug, which would be my post.slug. I'd also want it to have a title. And if you remember the way our data was structured, that's post.data.title. Let's copy this down two more times because I want the title here. I'll grab the first one, hit Command D. I also want the description. And then I also want the date or we could call it pub data, I guess, cause I think that's what it's called here. Yeah, pub date. Let's just leave it as date right here. So now I'm returning custom data from this collection query. And you can see that this is a lot smaller. Obviously, it will not produce quite as good a result. So if you want to include the entire post, you're welcome to do that or just maybe provide a more robust description to see exactly what's there. Now, once again, now that we've got this search route, what I wanna do is when I get to our actual search route here, when I start to type, I want to query that JSON file. And then just like I showed you earlier, this will pass it back and we can actually figure out what matches based on the user's input. Okay, see you there. Up to this point in the series, we've added a form on the homepage where you can add some kind of query. And then if you hit enter, it moves you to the search route and then actually adds it here, adds it here, updates the title. And then last time we created a JSON file at search.json like this, that actually gets all the content from our posts every time we build our site statically. So what we wanna do now is when we type over here or when we land on this page, either way, we want to go ahead and grab that data and load it on the client's browser. So let me go ahead and minimize that and we'll close down several of these routes here. I just basically need the search route open. So what I wanna do is upon listening to the event, I wanna first of all check to see if I've already downloaded it. I don't wanna keep fetching that data over and over again. So we can do this in a few different ways, but let's go ahead and maybe just come up here and then let's have a let variable. We'll call this something like a search data. I'm capitalizing this because I'm showing that it's kind of like static data. It shouldn't be changing. We're gonna query this once and basically every time they come to this page, if they haven't already downloaded this, then it will download it for them, but it only updates when we statically update our site. So it's not SSR or anything like that. So we're gonna declare this and then update it. Now we do need another function to actually do this work. So I'm gonna come in here and create a function. We'll call this something like fetch search results. Now I'm going to both fetch that JSON file and also process the results here. So I'm gonna go ahead and add in my search. So I'll eventually call this with that search. Let's go ahead and const res await fetch and I wanna fetch my search.json. We'll say const data await res.json. Now let's go ahead and console log this right here. And I do need to make sure this is async right here. Okay, so now that I've done that, you'll see if I open up the local terminal here, if I actually call this, which I have not yet, let's come right here and let's call this and we'll pass in URL search params. You'll see that I actually get back all five of those. Where's this coming from? Well, it's coming from that search.json file. Now it's always a good idea when you're doing this to go ahead and wrap this in a try fetch. And let's move this up and add a catch option where we'll catch the error, then console.error the error. Now typically you also wanna check to make sure that the network request is okay. So I could say if res.ok, this is not the case. Then I can throw a new error and I'll say something like something went wrong. Please try again. So let's say I actually mistyped this here. I should get something went wrong. Please try again here. It just says there's no search results. Now this only shows in the console here. So I could actually update this as well if I want to. I'll leave that to you if you wanna do that. But basically what I'm doing is fetching this, assuming that I get it, it will then return to me that data and here I'm console logging it. Otherwise it will basically throw an error in the console. Now this works just fine on page load, but if I grab the same thing and I come down here because I'd also wanna do the same thing whenever I start searching, right? The search term. And now you'll notice that every time I type, look, it's fetching data each time. And just to underscore that, if I come into the network and I click on fetch, you're gonna see all these fetch requests for the exact same data. See if I can actually get this to show. So there we go. And as I start typing, you're gonna see that every time it's refetching all the data. So I don't need to do that. So instead what I wanna do is actually just double check, first of all, if I already have done that, if I've already pulled in that data. So I'm gonna grab all of this and going to say, hey, if there's no search data, then do all this. Now, instead of just console logging this, what I need to do is come inside here and set my search data to my data. Now, as I start to type, you'll notice that it does not refetch it because it already exists. So it skips all of this right here. Now there's another thing to think about, and that would be if I don't actually have a search term, I don't wanna both do this and especially I don't wanna process everything because that's expensive. So what I can do is also come in here and say that if the search.length is equal to zero, then I can also just return out of here. So now whenever I've landed on the page or whenever I've typed, it's first checked to see if I have the data. If it doesn't, it fetches it. Once it's here, then it's basically kept in memory as long as they're on this page. And because I may rebuild and the data may be up to date, I don't want it to keep it in memory past that just while they're on that single session. Now, while it's fetching that data and processing it, I wanna show a spinner. So let me come up top here and let's create another little thing right up here. We're gonna call this spinner. And again, this shouldn't change. It's just gonna be a string of HTML and we can grab any icons, but let's go to like phosphor icons right here and let's explore the icons. And we're just gonna say like spinner. Instead of fill, I'll go like bold, something like that. And I'll go ahead and just copy this SVG. Let's paste it in here. And because it's an SVG, I can add a style tag and inside the style tag, we're actually gonna just do something directly on this SVG. So let's give this SVG a name. We'll call this spinner and I'll just grab the spinner and I'll set an animation spin one second, linear infinite and let's write those key frames. So key frames, spin, we'll do that 100% here. I want it to go ahead and transform, rotate 360. Okay, so if we did that all correctly, let's come back over this way and let's go ahead and add that here. So as I'm fetching search results, assuming that there is an actual search, then what I wanna do is update my results list, enter HTML with that spinner. Now, I don't think we've gotten access to that yet, but if I come back up top here, you're gonna see that I've got this search results list right here. So what I wanna do is add an ID here. We'll call this results. How about search results? And let's go ahead and grab access to that. So I'm gonna copy one of these things down. We'll do search results and search results. Now back inside our fetch, we'll say search results.innerHTML is equal to spinner. Now you can see that this thing should just keep spinning until my results show and I replace the inner HTML here with the content I get back. Okay, so that's all there is for this lesson. In the next lesson, we're going to look at Fuse.js. This is a fuzzy search library that's super lightweight, but allows us to do this kind of stuff on the client and we can query the data we've gotten back based on the user's input. I'll see you in that. I'll see you in lesson seven. In lesson seven, we're going to use this library Fuse.js to do some client side processing of their input. Now we've got everything working towards like showing this spinner bar right here. What we wanna do though is actually something while this is thinking. So let's come back over here and let's see. What we're gonna do is look at this weighted search option because essentially what we're gonna do is say, hey, we've got this data, which is right here. And what we wanna do is say, hey, which keys should you look? Now, if you might remember, if I come back over to my JSON file, we've got slug, title, description, and date. That's the keys that are available to us. And I can actually provide a weightedness to say, hey, this is more important or less important based on whatever they pass in. Now, there are a bunch of other options we can pass in as well. You'll see that we basically create a new instance of Fuse, pass that our options, pass that our data, and then we can use a method called search to search whatever the query happens to be. The output looks like this, where we get back data that gives us what the thing is. And if we ask for a score, it will give us this score back as well. So we've got two options right here. Now, to actually use this library, we need to install it. So we can do that with npm i views.js. Now let's get this back up and running. And now let's close this down and come back to our search route. Once again, I just said, we're gonna do everything we can in this file just to kind of keep things all contained. So right down here, when I actually search, I wanna update not just the search readout, the document title, but in this fetch search results, I wanna actually query that based on this Fuse.js library. So take my query and run it against that data using their fuzzy search algorithms. Now, once again, just like I did here, I don't wanna redo this every single time. If it's just being typed in, I wanna basically first check, hey, have I already created a new Fuse instance? Otherwise I'm gonna be doing that multiple times each time. So let me show you what I mean if that doesn't make sense. I have to actually create the new instance after I have the data since I have to pass that data in. In fact, let's come back over this way just to show you that. So I have to actually have the data already. So this is where I wanna do it. So I could say const Fuse equals new Fuse. And I wanna pass it my search data. I don't have to pass an option. So for now let's leave it like that. In order for this to work though, I have to actually import this. So let's come up top here to my imports. Here we will import Fuse from Fuse.js. Okay, so with that said, if I come back over this way, maybe let's kill the spinner for now, just so I don't have to keep watching that. So we'll try to remember to reload that in. But if I come over here, let's just say like console.log, generating new instance of Fuse. All right, so there, that works just fine. But every time I type, it's going to generate a new instance over and over again. I don't wanna do that. So just like before, we wanna wrap this in a little if statement. So I'm gonna come up top here. And once again, we're gonna have search data. We'll also have like Fuse instance or something like that. So let Fuse instance. And what I wanna do is come inside here and we'll say if there's no Fuse instance, then do this. Except in this case, I wanna update this to say Fuse instance is that. So now you'll notice it does it the first time. But as I keep typing, it doesn't re-initiate that. Now I do actually wanna pass in some search options. So I'm gonna say search options like this. And once again, this is gonna be static. So I'm just gonna capitalize that. At least that's my understanding of what people usually do. And here, this can be a const, say Fuse options. And here I wanna pass along a few options. If I jump back over here to the Fuse documentation, you can see an example. So let's go ahead and start with this. In this case, I've got a title, but I've also got a description. In this case, I'm gonna give the title more weight and the description will do like 0.75. I can also pass around a couple of different options. And if you're interested in Fuse more, I've done a video on it explaining these options. But one of the things I can do is say, I want it to should sort and I wanna set this to true. So it'll actually sort it. And then I can also pass in a threshold, which basically says, hey, how much does it have to match things? So in this case, I'll set it to like 0.5, I think 0.6 or something like that as the default. But just to show you can pass in additional options. Now these Fuse options, we passed them down below. This should be Fuse options. So now we've got access to this new Fuse instance. Now, one thing I might wanna do is just double check here that I actually have search data. So search data, I have to have that. And if I don't have a Fuse instance, then go ahead and generate this since I need the data inside of here. All right, so let's go ahead and get rid of this right here. And now one more check here, we'll just say if there is no Fuse instance, then go ahead and return. Otherwise, I'm assuming there is a Fuse instance. And now we can go ahead and do my search. So I'll say const search result. We'll set this equal to Fuse instance.search. Remember, that's that method that lives on there. And I will pass it my actual search right up here, right? So this is getting passed in and now it's querying it based on the data I already pulled in. So let's go ahead and just make sure this is all working properly. If I console log this and jump back over this way. All right, I'm not exactly sure what the problem is. Let's kill this. Actually, I wonder if it is the Fuse options. Let's jump back over here. Just make sure I'm showing this correctly. So it's an object. This is, let's make sure all this is working. So let me remove those two since we copied those in. Okay, so those seem to be the problem. Not that, looks like it's this threshold. And that's because this needs to be comma. Okay, here we go. So if I start to type, let's say something like markdown, you can see I'm actually now getting results back. And in this case, it's just one item. If I were to come over here and type just a single letter though, you can see I'm getting back five items, which is all the items. So I have an A matching in all of those items. Now inside here, you'll notice that I get the item itself and also a score. The item itself contains the four things I passed it. The date, the description, the slug, the title. So this is what we're going to use to then output our results right here. And we'll do that in the final and next video. I'll see you there. We are in the last lesson, lesson eight, where we're gonna kind of put all of this together. And we've got a few things to do. Number one, we wanna make sure we put that spinner back. Number two, we wanna make sure we show the actual search results. And we still haven't made it to where we can update the search results up here in the actual query param when we type in the input. So we've got at least those three things to do. So maybe let's start in reverse order. So before we start working on this fuse one more time, let's go ahead and just make sure that every time we update, it actually will rerun all of this. So to do that, I'm gonna go ahead and minimize that. We'll come in here and whenever we type, we want to also update the search page URL. And here, once again, we'll pass it our search term. Now we haven't yet written this. So let's come up here, update search term, and maybe let's do it right here. Now, all I wanna do is pass a new URL here. So I'm gonna call this URL. We'll just say new URL. So it'll be our window.location.href. So the current page that we're on at the moment, and then we'll go ahead and update the search params. So we will set the queue, which is what we called earlier, to our search itself. And then finally, we'll go ahead and window.history.replaceState. This is something that comes default in the browser. We've got three arguments we can pass in. The first two essentially don't matter. And then finally, we'll pass in the URL. So without going into great detail, now as we type, it will update up here. It will update the actual URL. It will update here and it will update here. Now, each time it's also passing right down here. It's also fetching the search results, which means we're also getting a new query each time right here. So in reverse order, let's now add in the text that we need. So here, what I wanna do, if I come back up top here, we already set the search results here, the inner HTML to a spinner. I wanna just copy this down and we're gonna update the search results again down this way. In this case, I first wanna do a little check here. So let's just put this on a new line. We're gonna say a search result, and that would be this right here. So this is a little confusing. Maybe let's rename this. Let's call this something like results list. How about that? So let's come back up top here, change this to results list, and also change this to results list. Okay, so with that said, let's come back down now that we know that the list is the actual HTML. And now I wanna run a little check here. We're gonna see if the search result.length is greater than zero. So in other words, I have to actually have something in my list to update the DOM. Now, if that's the case, I wanna generate a search list. I'm gonna pass it my search results. Otherwise, I simply wanna pass in some text that just says no results found. Now, right now I have yet to actually write this function, so it should be angry, and it is. Let's come in here and say function, generate search results, and here we'll say results. And all I wanna do is loop over those results that were passed here. So we'll return results.map. And for each result, which I'll just call r, we wanna do a few things. First of all, you might remember that in Fuse, we actually get back an item. So maybe just to make sure this is clear, we'll console log r here. The thing we cannot map over this, let's just make sure I'm getting what we think we are here. This is search result, like that. Okay, so here we go. Each thing we get back, we get back as an item. Then we also get a ref index, and we also get a score. So up top here, when we actually get this out, we don't wanna console log this. Instead, I just wanna extract out the few things that we need to actually update the DOM. In this case, I would need the title, the date, and the slug. I can get all of these from the r.item. So for each thing that's passed in, I want the title, the date, and the slug. Now, I also need to actually process that date. So in order to turn it into a date, I need to take the string of date, which is what I'm getting here from the Fuse result, and I need to have it as a date. So I'm gonna say date as date. This will be new date, and I'll be passing in the date that I got here from the item. Now, all I have to do is return a string here. And if I wanna know what I need, I can go to my index for my blog. This is essentially what I want right here. All right, so let's go ahead and just copy this out, and I'll paste this in. Now, we don't yet have this formatted date. This is an astro component. So let me open up formatted date, and we're just gonna extract all this right here, and we'll drop this in like this. All right, so let's add that in here. This itself needs to be a template string, so let's go ahead and do this with date as date. And same thing here, this needs to be template string, and we'll have date as date. And for this down this way, we don't need these anymore, but we are going to actually add this in, not as post.slug, but just as slug. Finally, this should say title. Again, we're getting both the title and the slug from up top. If I come back down this way, we do need to make sure that these are not template strings, but just normal strings here. And then it should just show me this. So there you go, I can now start typing anything, and you see it's actually showing it to me. You may notice I'm getting these commas in between them, and that's because this map right here is giving me an array of items. So on the very end of this, when I'm done with this, I wanna actually join them on nothing. And now this will actually give me them all as list items. So if I come in here, you can see now I've got list items here instead of list items with commas in between them. So if I type MDX, that shows up, something else, and that shows up. Now that leaves just one final thing, and that is to go ahead and change my spinner here to actually be showing. So when I'm trying to load something, let's say I might actually need to slow down my network. Let's come inside here, go to my network, and we'll throttle this to like slow 3G. There you go, it's actually showing my spinner, and then eventually the posts come in. Now this should be pretty fast no matter who is coming, so that shouldn't be a huge problem, but now we've actually got this spinner really briefly showing and then the post populating. And again, the cool thing is I can actually share this URL and the same person that I share it with will get the same results I got. So third, whatever, come in here. Now again, let's go back out to the homepage and search for like MDX. Remember that is going to move us to our search index. It's gonna do that query. It's gonna then show the results. All of this is what we've created in this series. I hope this was a huge help for you in kind of thinking through Astro and how you can use the different routes, even on a static site and a little bit of client-side JavaScript to actually get exactly what you need. If you have any questions or any suggestions for how I could have done this better, please let me know in the description. And of course, if you convert this to using SSR and actually do this on page load, all of that, that means you can avoid all the client-side stuff, then I'd love to see what that looks like as well. Well, thanks so much for watching. I'll catch you in the next one. Happy coding.
Astro Search Tutorial (Full Tutorial)
Channel: Coding in Public
Share transcript:
Want to generate another YouTube transcript?
Enter a YouTube URL below to generate a new transcript.