Making a search bar for blogposts

Using the search API

AtomicServer comes with a fast full-text search API out of the box. @tomic/lib provides some convenient helper functions on Store to make using this API very easy.

To use search all you need to do is:

const results = await store.search('how to make icecream');

The method returns an array of subjects of resources that match the given query.

To further refine the query, we can pass a filter object to the method like so:

const results = await store.search('how to make icecream', { filters: { [core.properties.isA]: myPortfolio.classes.blogpost, }, });

This way the result will only include resources that have an is-a of blogpost.

Running code on the client

To make a working search bar, we will have to run code on the client. Astro code only runs on the server but there are a few ways to have code run on the client. The most commonly used option would be to use a frontend framework like React or Svelte but Astro also allows script tags to be added to components that will be included in the <head /> of the page.

To keep this guide framework-agnostic we will use a script tag and a web component but feel free to use any framework you're more comfortable with, the code should be simple enough to adapt to different frameworks.

First, we need to make a change to our environment variables because right now they are not available to the client and therefore getStore will not be able to access ATOMIC_SERVER_URL. To make an environment variable accessible to the client it needs to be prefixed with PUBLIC_.

In .env change ATOMIC_SERVER_URL to PUBLIC_ATOMIC_SERVER_URL.

// .env PUBLIC_ATOMIC_SERVER_URL=<REPLACE WITH URL TO YOUR ATOMIC SERVER> ATOMIC_HOMEPAGE_SUBJECT=<REPLACE WITH SUBJECT OF THE HOMEPAGE RESOURCE>

Now update src/helpers/getStore.ts to reflect the name change.

// src/helpers/getStore.ts import { Store } from '@tomic/lib'; import { initOntologies } from '../ontologies'; let store: Store; export function getStore(): Store { if (!store) { store = new Store({ serverUrl: import.meta.env.PUBLIC_ATOMIC_SERVER_URL, }); initOntologies(); } return store; }

In src/components create a file called Search.astro.

<blog-search></blog-search> <script> import { getStore } from '../../helpers/getStore'; import { core } from '@tomic/lib'; import { myPortfolio, type Blogpost } from '../../ontologies/myPortfolio'; class BlogSearch extends HTMLElement { // Get access to the store. (Since this runs on the client a new instance will be created) private store = getStore(); // Create an element to store the results in private resultsElement = document.createElement('div'); // Runs when the element is mounted. constructor() { super(); // We create an input element and add a listener to it that will trigger a search. const input = document.createElement('input'); input.placeholder = 'Search...'; input.type = 'search'; input.addEventListener('input', (e) => { this.searchAndDisplay(input.value); }); // Add the input and result list elements to the root of our webcomponent. this.append(input, this.resultsElement); } /** * Search for blog posts using the given query and display the results. */ private async searchAndDisplay(query: string) { if (!query) { // Clear the results of the previous search. this.resultsElement.innerHTML = ''; return; } const results = await this.store.search(query, { filters: { [core.properties.isA]: myPortfolio.classes.blogpost, }, }); // Map the result subjects to elements. const elements = await Promise.all( results.map(s => this.createResultItem(s)), ); // Clear the results of the previous search. this.resultsElement.innerHTML = ''; // Append the new results to the result list. this.resultsElement.append(...elements); } /** * Create a result link for the given blog post. */ private async createResultItem(subject: string): Promise<HTMLAnchorElement> { const post = await this.store.getResource<Blogpost>(subject); const resultLine = document.createElement('a'); resultLine.innerText = post.title; resultLine.style.display = 'block'; resultLine.href = `/blog/${post.props.titleSlug}`; return resultLine; } } // Register the custom element. customElements.define('blog-search', BlogSearch); </script>

If you've never seen web components before, <blog-search> is our custom element that starts as just an empty shell. We then add a <script> that Astro will add to the head of our HTML. In this script, we define the class that handles how to render the <blog-search> element. At the end of the script, we register the custom element class.

NOTE:
Eventhough the server will most likely keep up with this many requests, lower end devices might not so it's still a good idea to add some kind of debounce to your searchbar.

Now all that's left to do is use the component to the blog page.

// src/pages/blog/index.astro ... <Layout resource={homepage}> <h2>Blog 😁</h2> + <Search /> <ul> { posts.map(post => ( <li> <BlogCard subject={post} /> </li> )) } </ul> </Layout>

And there it is! A working real-time search bar 🎉

The end, what's next?

That's all for this guide. Some things you could consider adding next if you liked working with AtomicServer and want to continue building this portfolio:

  • Add some more styling
  • Add some interactive client components using one of many Astro integrations (Consider checking @tomic/react or @tomic/svelte)
  • Do some SEO optimisation by adding meta tags to your Layout.astro.