Static websites are fantastic. I’m an enormous fan.
In addition they have their points. Particularly, static websites both are purely static or the frameworks that generate them fully lose out on true static era while you simply dip your toes within the path of server routes.
Astro has been watching the front-end ecosystem and is attempting to maintain one foot firmly embedded in pure static era, and the opposite in a robust set of server-side performance.
With Astro Actions, Astro brings a whole lot of the ability of the server to a website that’s nearly totally static. An excellent instance of this form of performance is coping with search. In case you have a content-based website that may be purely generated, including search is both going to be one thing dealt with totally on the entrance finish, through a software-as-a-service resolution, or, in different frameworks, changing your complete website to a server-side utility.
With Astro, we are able to generate most of our website throughout our construct, however have a small little bit of server-side code that may deal with our search performance utilizing one thing like Fuse.js.
On this demo, we’ll use Fuse to go looking by a set of non-public “bookmarks” which can be generated at construct time, however return correct outcomes from a server name.
Beginning the challenge
To get began, we’ll simply arrange a really primary Astro challenge. In your terminal, run the next command:
npm create astro@newest
Astro’s lovable mascot Houston goes to ask you just a few questions in your terminal. Listed below are the fundamental responses, you’ll want:
- The place ought to we create your new challenge? Wherever you’d like, however I’ll be calling my listing
./astro-search
- How would you want to start out your new challenge? Select the fundamental minimalist starter.
- Set up dependencies? Sure, please!
- Initialize a brand new git repository? I’d suggest it, personally!
It will create a listing within the location specified and set up all the things you’ll want to begin an Astro challenge. Open the listing in your code editor of selection and run npm run dev
in your terminal within the listing.
Whenever you run your challenge, you’ll see the default Astro challenge homepage.

We’re able to get our challenge rolling!
Primary setup
To get began, let’s take away the default content material from the homepage. Open the /src/pages/index.astro
file.
It is a pretty barebones homepage, however we would like it to be much more primary. Take away the <Welcome />
part, and we’ll have a pleasant clean web page.
For styling, let’s add Tailwind and a few very primary markup to the homepage to include our website.
npx astro add tailwind
The astro add
command will set up Tailwind and try to arrange all of the boilerplate code for you (useful!). The CLI will ask you if you’d like it so as to add the assorted parts, I like to recommend letting it, but when something fails, you possibly can copy the code wanted from every of the steps within the course of. Because the final step for attending to work with Tailwind, the CLI will inform you to import the types right into a shared format. Observe these directions, and we are able to get to work.
Let’s add some very primary markup to our new homepage.
---
// ./src/pages/index.astro
import Format from '../layouts/Format.astro';
---
<Format>
<div class="max-w-3xl mx-auto my-10">
<h1 class="text-3xl text-center">My newest bookmarks</h1>
<p class="text-xl text-center mb-5">That is solely 10 of A LARGE NUMBER THAT WE'LL CHANGE LATER</p>
</div>
</Format>
Your website ought to now appear like this.

Not precisely profitable any awards but! That’s alright. Let’s get our bookmarks loaded in.
Including bookmark information with Astro Content material Layer
Since not everybody runs their very own utility for bookmarking attention-grabbing gadgets, you possibly can borrow my information. Right here’s a small subset of my bookmarks, or you possibly can go get 110 gadgets from this hyperlink on GitHub. Add this information as a file in your challenge. I prefer to group information in a information
listing, so my file lives in /src/information/bookmarks.json
.
Open code
[
King Arthur Baking",
"url": "<https://www.kingarthurbaking.com/recipes/our-favorite-sandwich-bread-recipe>",
"description": "Classic American sandwich loaf, perfect for French toast and sandwiches.",
"id": "007y8pmEOvhwldfT3wx1MW"
,
CSS-Tricks ",
"url": "<https://css-tricks.com/automatic-social-share-images/>",
"description": "It's a pretty low-effort thing to get a big fancy link preview on social media. Toss a handful of specific <meta> tags on a URL and you get a big image-title-description thing ",
"id": "04CXDvGQo19m0oXERL6bhF"
,
ryanfiller.com",
"url": "<https://www.ryanfiller.com/blog/automatic-social-share-images/>",
"description": "Setting up automatic social share images with Puppeteer and Netlify Functions. ",
"id": "04CXDvGQo19m0oXERLoC10"
,
{
"pageTitle": "Emma Wedekind: Foundations of Design Systems / React Boston 2019 - YouTube",
"url": "<https://m.youtube.com/watch?v=pXb2jA43A6k>",
"description": "Emma Wedekind: Foundations of Design Systems / React Boston 2019 Presented by: Emma Wedekind – LogMeIn Design systems are in the world around us, from street...",
"id": "0d56d03e-aba4-4ebd-9db8-644bcc185e33"
},
{
"pageTitle": "Editorial Design Patterns With CSS Grid And Named Columns — Smashing Magazine",
"url": "<https://www.smashingmagazine.com/2019/10/editorial-design-patterns-css-grid-subgrid-naming/>",
"description": "By naming lines when setting up our CSS Grid layouts, we can tap into some interesting and useful features of Grid — features that become even more powerful when we introduce subgrids.",
"id": "13ac1043-1b7d-4a5b-a3d8-b6f5ec34cf1c"
},
{
"pageTitle": "Netlify pro tip: Using Split Testing to power private beta releases - DEV Community 👩💻👨💻",
"url": "<https://dev.to/philhawksworth/netlify-pro-tip-using-split-testing-to-power-private-beta-releases-a7l>",
"description": "Giving users ways to opt in and out of your private betas. Video and tutorial.",
"id": "1fbabbf9-2952-47f2-9005-25af90b0229e"
},
Jim Nielsen’s Weblog",
"url": "<https://blog.jim-nielsen.com/2019/netlify-public-folder-part-i-what/>",
"id": "2607e651-7b64-4695-8af9-3b9b88d402d5"
,
{
"pageTitle": "Why Is CSS So Weird? - YouTube",
"url": "<https://m.youtube.com/watch?v=aHUtMbJw8iA&feature=youtu.be>",
"description": "Love it or hate it, CSS is weird! It doesn't work like most programming languages, and it doesn't work like a design tool either. But CSS is also solving a v...",
"id": "2e29aa3b-45b8-4ce4-85b7-fd8bc50daccd"
},
{
"pageTitle": "Internet world despairs as non-profit .org sold for $$$$ to private equity firm, price caps axed • The Register",
"url": "<https://www.theregister.co.uk/2019/11/20/org_registry_sale_shambles/>",
"id": "33406b33-c453-44d3-8b18-2d2ae83ee73f"
},
{
"pageTitle": "Netlify Identity for paid subscriptions - Access Control / Identity - Netlify Community",
"url": "<https://community.netlify.com/t/netlify-identity-for-paid-subscriptions/1947/2>",
"description": "I want to limit certain functionality on my website to paying users. Now I’m using a payment provider (Mollie) similar to Stripe. My idea was to use the webhook fired by this service to call a Netlify function and give…",
"id": "34d6341c-18eb-4744-88e1-cfbf6c1cfa6c"
},
{
"pageTitle": "SmashingConf Freiburg 2019: Videos And Photos — Smashing Magazine",
"url": "<https://www.smashingmagazine.com/2019/10/smashingconf-freiburg-2019/>",
"description": "We had a lovely time at SmashingConf Freiburg. This post wraps up the event and also shares the video of all of the Freiburg presentations.",
"id": "354cbb34-b24a-47f1-8973-8553ed1d809d"
},
{
"pageTitle": "Adding Google Calendar to your JAMStack",
"url": "<https://www.raymondcamden.com/2019/11/18/adding-google-calendar-to-your-jamstack>",
"description": "A look at using Google APIs to add events to your static site.",
"id": "361b20c4-75ce-46b3-b6d9-38139e03f2ca"
},
CSS-Tricks",
"url": "<https://css-tricks.com/how-to-contribute-to-an-open-source-project/>",
"description": "The following is going to get slightly opinionated and aims to guide someone on their journey into open source. As a prerequisite, you should have basic",
"id": "37300606-af08-4d9a-b5e3-12f64ebbb505"
,
Netlify",
"url": "<https://www.netlify.com/docs/functions/>",
"description": "Netlify builds, deploys, and hosts your front end. Learn how to get started, see examples, and view documentation for the modern web platform.",
"id": "3bf9e31b-5288-4b3b-89f2-97034603dbf6"
,
{
"pageTitle": "Serverless Can Help You To Focus - By Simona Cotin",
"url": "<https://hackernoon.com/serverless-can-do-that-7nw32mk>",
"id": "43b1ee63-c2f8-4e14-8700-1e21c2e0a8b1"
},
{
"pageTitle": "Nuxt, Next, Nest?! My Head Hurts. - DEV Community 👩💻👨💻",
"url": "<https://dev.to/laurieontech/nuxt-next-nest-my-head-hurts-5h98>",
"description": "I clearly know what all of these things are. Their names are not at all similar. But let's review, just to make sure we know...",
"id": "456b7d6d-7efa-408a-9eca-0325d996b69c"
},
{
"pageTitle": "Consuming a headless CMS GraphQL API with Eleventy - Webstoemp",
"url": "<https://www.webstoemp.com/blog/headless-cms-graphql-api-eleventy/>",
"description": "With Eleventy, consuming data coming from a GraphQL API to generate static pages is as easy as using Markdown files.",
"id": "4606b168-21a6-49df-8536-a2a00750d659"
},
]
Now that the information is within the challenge, we want for Astro to include the information into its construct course of. To do that, we are able to use Astro’s new(ish) Content material Layer API. The Content material Layer API provides a content material configuration file to your src
listing that lets you run and acquire any variety of content material items from information in your challenge or exterior APIs. Create the file /src/content material.config.ts
(the identify of this file issues, as that is what Astro is in search of in your challenge).
import { defineCollection, z } from "astro:content material";
import { file } from 'astro/loaders';
const bookmarks = defineCollection({
schema: z.object({
pageTitle: z.string(),
url: z.string(),
description: z.string().elective()
}),
loader: file("src/information/bookmarks.json"),
});
export const collections = { bookmarks };
On this file, we import just a few helpers from Astro. We will use defineCollection
to create the gathering, z
as Zod, to assist outline our varieties, and file
is a selected content material loader meant to learn information recordsdata.
The defineCollection
methodology takes an object as its argument with a required loader and elective schema. The schema will assist make our content material type-safe and ensure our information is all the time what we count on it to be. On this case, we’ll outline the three information properties every of our bookmarks has. It’s essential to outline all of your information in your schema, in any other case it gained’t be accessible to your templates.
We offer the loader
property with a content material loader. On this case, we’ll use the file
loader that Astro supplies and provides it the trail to our JSON.
Lastly, we have to export the collections
variable as an object containing all of the collections that we’ve outlined (simply bookmarks
in our challenge). You’ll need to restart the native server by re-running npm run dev
in your terminal to choose up the brand new information.
Utilizing the brand new bookmarks content material assortment
Now that we have now information, we are able to use it in our homepage to indicate the newest bookmarks which have been added. To get the information, we have to entry the content material assortment with the getCollection
methodology from astro:content material
. Add the next code to the frontmatter for ./src/pages/index.astro
.
---
import Format from '../layouts/Format.astro';
import { getCollection } from 'astro:content material';
const bookmarks = await getCollection('bookmarks');
---
This code imports the getCollection
methodology and makes use of it to create a brand new variable that comprises the information in our bookmarks
assortment. The bookmarks
variable is an array of information, as outlined by the gathering, which we are able to use to loop by in our template.
---
import Format from '../layouts/Format.astro';
import { getCollection } from 'astro:content material';
const bookmarks = await getCollection('bookmarks');
---
<Format>
<div class="max-w-3xl mx-auto my-10">
<h1 class="text-3xl text-center">My newest bookmarks</h1>
<p class="text-xl text-center mb-5">
That is solely 10 of {bookmarks.size}
</p>
<h2 class="text-2xl mb-3">Newest bookmarks</h2>
<ul class="grid gap-4">
{
bookmarks.slice(0, 10).map((merchandise) => (
<li>
<a
href={merchandise.information?.url}
class="block p-6 bg-white border border-gray-200 rounded-lg shadow-sm hover:bg-gray-100 darkish:bg-gray-800 darkish:border-gray-700 darkish:hover:bg-gray-700">
<h3 class="mb-2 text-2xl font-bold tracking-tight text-gray-900 darkish:text-white">
{merchandise.information?.pageTitle}
</h3>
<p class="font-normal text-gray-700 darkish:text-gray-400">
{merchandise.information?.description}
</p>
</a>
</li>
))
}
</ul>
</div>
</Format>
This could pull the newest 10 gadgets from the array and show them on the homepage with some Tailwind types. The principle factor to notice right here is that the information construction has modified slightly. The precise information for every merchandise in our array truly resides within the information
property of the merchandise. This permits Astro to place extra information on the item with out colliding with any particulars we offer in our database. Your challenge ought to now look one thing like this.

Now that we have now information and show, let’s get to work on our search performance.
Constructing search with actions and vanilla JavaScript
To begin, we’ll need to scaffold out a brand new Astro part. In our instance, we’re going to make use of vanilla JavaScript, however if you happen to’re conversant in React or different frameworks that Astro helps, you possibly can go for consumer Islands to construct out your search. The Astro actions will work the identical.
Organising the part
We have to make a brand new part to accommodate a little bit of JavaScript and the HTML for the search discipline and outcomes. Create the part in a ./src/parts/Search.astro
file.
<kind id="searchForm" class="flex mb-6 items-center max-w-sm mx-auto">
<label for="simple-search" class="sr-only">Search</label>
<div class="relative w-full">
<enter
kind="textual content"
id="search"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 darkish:bg-gray-700 darkish:border-gray-600 darkish:placeholder-gray-400 darkish:text-white darkish:focus:ring-blue-500 darkish:focus:border-blue-500"
placeholder="Search Bookmarks"
required
/>
</div>
<button
kind="submit"
class="p-2.5 ms-2 text-sm font-medium text-white bg-blue-700 rounded-lg border border-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 darkish:bg-blue-600 darkish:hover:bg-blue-700 darkish:focus:ring-blue-800">
<svg
class="w-4 h-4"
aria-hidden="true"
xmlns="<http://www.w3.org/2000/svg>"
fill="none"
viewBox="0 0 20 20">
<path
stroke="currentColor"
stroke-linecap="spherical"
stroke-linejoin="spherical"
stroke-width="2"
d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z"></path>
</svg>
<span class="sr-only">Search</span>
</button>
</kind>
<div class="grid gap-4 mb-10 hidden" id="outcomes">
<h2 class="text-xl font-bold mb-2">Search Outcomes</h2>
</div>
<script>
const kind = doc.getElementById("searchForm");
const search = doc.getElementById("search");
const outcomes = doc.getElementById("outcomes");
kind?.addEventListener("submit", async (e) => {
e.preventDefault();
console.log("SEARCH WILL HAPPEN");
});
</script>
The essential HTML is establishing a search kind, enter, and outcomes space with IDs that we’ll use in JavaScript. The essential JavaScript finds these components, and for the shape, provides an occasion listener that fires when the shape is submitted. The occasion listener is the place a whole lot of our magic goes to occur, however for now, a console log will do to ensure all the things is ready up correctly.
Organising an Astro Motion for search
To ensure that Actions to work, we want our challenge to permit for Astro to work in server or hybrid mode. These modes permit for all or some pages to be rendered in serverless features as a substitute of pre-generated as HTML through the construct. On this challenge, this shall be used for the Motion and nothing else, so we’ll go for hybrid mode.
To have the ability to run Astro on this approach, we have to add a server integration. Astro has integrations for a lot of the main cloud suppliers, in addition to a primary Node implementation. I sometimes host on Netlify, so we’ll set up their integration. Very like with Tailwind, we’ll use the CLI so as to add the bundle and it’ll construct out the boilerplate we want.
npx astro add netlify
As soon as that is added, Astro is operating in Hybrid mode. Most of our website is pre-generated with HTML, however when the Motion will get used, it should run as a serverless operate.
Organising a really primary search Motion
Subsequent, we want an Astro Motion to deal with our search performance. To create the motion, we have to create a brand new file at ./src/actions/index.js
. All our Actions reside on this file. You may write the code for each in separate recordsdata and import them into this file, however on this instance, we solely have one Motion, and that looks like untimely optimization.
On this file, we’ll arrange our search Motion. Very like establishing our content material collections, we’ll use a technique known as defineAction
and provides it a schema and on this case a handler. The schema will validate the information it’s getting from our JavaScript is typed accurately, and the handler will outline what occurs when the Motion runs.
import { defineAction } from "astro:actions";
import { z } from "astro:schema";
import { getCollection } from "astro:content material";
export const server = {
search: defineAction({
schema: z.object({
question: z.string(),
}),
handler: async (question) => {
const bookmarks = await getCollection("bookmarks");
const outcomes = await bookmarks.filter((bookmark) => {
return bookmark.information.pageTitle.contains(question);
});
return outcomes;
},
}),
};
For our Motion, we’ll identify it search
and count on a schema of an object with a single property named question
which is a string. The handler operate will get all of our bookmarks from the content material assortment and use a local JavaScript .filter()
methodology to examine if the question is included in any bookmark titles. This primary performance is able to take a look at with our front-end.
Utilizing the Astro Motion within the search kind occasion
When the consumer submits the shape, we have to ship the question to our new Motion. As an alternative of determining the place to ship our fetch request, Astro offers us entry to all of our server Actions with the actions
object in astro:actions
. Because of this any Motion we create is accessible from our client-side JavaScript.
In our Search part, we are able to now import our Motion instantly into the JavaScript after which use the search motion when the consumer submits the shape.
<script>
import { actions } from "astro:actions";
const kind = doc.getElementById("searchForm");
const search = doc.getElementById("search");
const outcomes = doc.getElementById("outcomes");
kind?.addEventListener("submit", async (e) => {
e.preventDefault();
outcomes.innerHTML = "";
const question = search.worth;
const { information, error } = await actions.search(question);
if (error) {
outcomes.innerHTML = `<p>${error.message}</p>`;
return;
}
// create a div for every search consequence
information.forEach(( merchandise ) => {
const div = doc.createElement("div");
div.innerHTML = `
<a href="https://css-tricks.com/powering-search-with-astro-actions-and-fuse-js/${merchandise.information?.url}" class="block p-6 bg-white border border-gray-200 rounded-lg shadow-sm hover:bg-gray-100 darkish:bg-gray-800 darkish:border-gray-700 darkish:hover:bg-gray-700">
<h3 class="mb-2 text-2xl font-bold tracking-tight text-gray-900 darkish:text-white">
${merchandise.information?.pageTitle}
</h3>
<p class="font-normal text-gray-700 darkish:text-gray-400">
${merchandise.information?.description}
</p>
</a>`;
// append the div to the outcomes container
outcomes.appendChild(div);
});
// present the outcomes container
outcomes.classList.take away("hidden");
});
</script>
When outcomes are returned, we are able to now get search outcomes!

Although, they’re extremely problematic. That is only a easy JavaScript filter, in any case. You may seek for “Favourite” and get my favourite bread recipe, however if you happen to seek for “favourite” (no caps), you’ll get an error… Not preferrred.
That’s why we should always use a bundle like Fuse.js.
Including Fuse.js for fuzzy search
Fuse.js is a JavaScript bundle that has utilities to make “fuzzy” search a lot simpler for builders. Fuse will settle for a string and based mostly on quite a few standards (and quite a few units of information) present responses that carefully match even when the match isn’t good. Relying on the settings, Fuse can match “Favourite”, “favourite”, and even misspellings like “favrite” all to the precise outcomes.
Is Fuse as highly effective as one thing like Algolia or ElasticSearch? No. Is it free and fairly darned good? Completely! To get Fuse transferring, we have to set up it into our challenge.
npm set up fuse.js
From there, we are able to use it in our Motion by importing it within the file and creating a brand new occasion of Fuse based mostly on our bookmarks assortment.
import { defineAction } from "astro:actions";
import { z } from "astro:schema";
import { getCollection } from "astro:content material";
import Fuse from "fuse.js";
export const server = {
search: defineAction({
schema: z.object({
question: z.string(),
}),
handler: async (question) => {
const bookmarks = await getCollection("bookmarks");
const fuse = new Fuse(bookmarks, {
threshold: 0.3,
keys: [
{ name: "data.pageTitle", weight: 1.0 },
{ name: "data.description", weight: 0.7 },
{ name: "data.url", weight: 0.3 },
],
});
const outcomes = await fuse.search(question);
return outcomes;
},
}),
};
On this case, we create the Fuse occasion with just a few choices. We give it a threshold worth between 0 and 1 to resolve how “fuzzy” to make the search. Fuzziness is unquestionably one thing that will depend on use case and the dataset. In our dataset, I’ve discovered 0.3
to be an incredible threshold.
The keys
array lets you specify which information must be searched. On this case, I need all the information to be searched, however I need to permit for various weighting for every merchandise. The title must be most essential, adopted by the outline, and the URL must be final. This fashion, I can seek for key phrases in all these areas.
As soon as there’s a brand new Fuse occasion, we run fuse.search(question)
to have Fuse examine the information, and return an array of outcomes.
Once we run this with our front-end, we discover we have now yet another difficulty to deal with.

The construction of the information returned is just not fairly what it was with our easy JavaScript. Every consequence now has a refIndex
and an merchandise
. All our information lives on the merchandise, so we have to destructure the merchandise off of every returned consequence.
To do this, modify the front-end forEach
.
// create a div for every search consequence
information.forEach(({ merchandise }) => {
const div = doc.createElement("div");
div.innerHTML = `
<a href="https://css-tricks.com/powering-search-with-astro-actions-and-fuse-js/${merchandise.information?.url}" class="block p-6 bg-white border border-gray-200 rounded-lg shadow-sm hover:bg-gray-100 darkish:bg-gray-800 darkish:border-gray-700 darkish:hover:bg-gray-700">
<h3 class="mb-2 text-2xl font-bold tracking-tight text-gray-900 darkish:text-white">
${merchandise.information?.pageTitle}
</h3>
<p class="font-normal text-gray-700 darkish:text-gray-400">
${merchandise.information?.description}
</p>
</a>`;
// append the div to the outcomes container
outcomes.appendChild(div);
});
Now, we have now a totally working seek for our bookmarks.

Subsequent steps
This simply scratches the floor of what you are able to do with Astro Actions. For example, we should always in all probability add extra error dealing with based mostly on the error we get again. It’s also possible to experiment with dealing with this on the page-level and letting there be a Search web page the place the Motion is used as a kind motion and handles all of it as a server request as a substitute of with front-end JavaScript code. You may additionally refactor the JavaScript from the admittedly low-tech vanilla JS to one thing a bit extra sturdy with React, Svelte, or Vue.
One factor is for certain, Astro retains trying on the front-end panorama and studying from the errors and finest practices of all the opposite frameworks. Actions, Content material Layer, and extra are just the start for a very compelling front-end framework.