Skip to content

Commit

Permalink
#723 Add chapter about search to astro guide
Browse files Browse the repository at this point in the history
  • Loading branch information
Polleps authored and joepio committed Mar 21, 2024
1 parent 852b46d commit 27a8128
Show file tree
Hide file tree
Showing 11 changed files with 218 additions and 17 deletions.
2 changes: 1 addition & 1 deletion browser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
"cli"
]
},
"packageManager": "pnpm@8.6.12",
"packageManager": "pnpm@8.15.2",
"dependencies": {
"eslint-plugin-import": "^2.26.0"
}
Expand Down
1 change: 0 additions & 1 deletion browser/svelte/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
"module": "./dist/index.js",
"main-dev": "src/index.ts",
"name": "@tomic/svelte",
"packageManager": "[email protected]",
"peerDependencies": {
"@tomic/lib": "workspace:*"
},
Expand Down
1 change: 0 additions & 1 deletion docs/book.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ git-repository-url = "https://github.com/atomicdata-dev/atomic-server"
edit-url-template = "https://github.com/atomicdata-dev/atomic-server/edit/develop/docs/{path}"
additional-css = ["atomic.css"]
additional-js = ["discord.js"]
fold.enable = true

[output.html.fold]
enable = true
Expand Down
2 changes: 2 additions & 0 deletions docs/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,12 @@
- [Fetching data](astro-guide/7-fetching-data.md)
- [Using ResourceArray to display a list of projects](astro-guide/8-pojects.md)
- [Using Collections to build the blogs page](astro-guide/9-blogs.md)
- [Using the search API to build a search bar](astro-guide/10-search.md)

# Specification

- [Atomic Data Core](core/concepts.md)

- [Serialization](core/serialization.md)
- [JSON-AD](core/json-ad.md)
- [Querying](core/querying.md)
Expand Down
194 changes: 194 additions & 0 deletions docs/src/astro-guide/10-search.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
# 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:

```typescript
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:

```typescript
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
// .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.

```typescript
// 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;
}
```

## Creating the search bar

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

```html
<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.getResourceAsync<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: </br>
> 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.

```diff
// 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 🎉

<video loop autoplay muted>
<source src="videos/10-1.mp4">
</video>

## 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](https://docs.astro.build/en/guides/integrations-guide/) (Consider checking [@tomic/react](https://www.npmjs.com/package/@tomic/react) or [@tomic/svelte](https://www.npmjs.com/package/@tomic/svelte))
- Do some SEO optimisation by adding meta tags to your `Layout.astro`.
2 changes: 1 addition & 1 deletion docs/src/astro-guide/4-basic-data-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Next, create a property called `body-text` but change the datatype to `MARKDOWN`

The last property we'll add is `header-image`. The datatype should be `Resource`, this means it will reference another resource.
Since we want this to always be a file and not some other random class we are going to give it a classtype.
To do this click on the configure button after the datatype selector.
To do this click on the configure button next to the datatype selector.
A dialog should appear with additional settings for the property.
In the 'Classtype' field search for `file`.
An option with the text `file - A single binary file` should appear, select it and close the dialog.
Expand Down
2 changes: 1 addition & 1 deletion docs/src/astro-guide/7-fetching-data.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ Now that we have a store we can start fetching data.
## Fetching

To fetch data we are going to use the `store.getResourceAsync()` method.
This is an async method on Store that takes a subject and returns a promise that resolves to the fetches resource.
This is an async method on Store that takes a subject and returns a promise that resolves to a resource.
When `getResourceAsync` is called again with the same subject the store will return the cached version of the resource instead so don't worry about fetching the same resource again in multiple components.

`getResourceAsync` also accepts a type parameter, this type parameter is the subject of the resource's class.
Expand Down
6 changes: 3 additions & 3 deletions docs/src/astro-guide/8-pojects.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ This is basically an array of subjects pointing to other resources.
Click on the configure button next to datatype and in the classtype field type `project`, an option with the text `Create: project` should appear, click it and the new class will be added to the ontology.

<video controls>
<source src="/videos/8-1.mp4">
<source src="videos/8-1.mp4">
</video>

We are going to give `project` 3 required and 2 recommended properties.
Expand Down Expand Up @@ -98,7 +98,7 @@ const description = marked.parse(project.props.description);

```

The component takes a subject as a prop that we use to fetch the project resource using the `fetchResourceAsync` method.
The component takes a subject as a prop that we use to fetch the project resource using the `.getResourceAsync()` method.
We then fetch the image resource using the same method.

The description is markdown so we have to parse that first like we did on the homepage.
Expand Down Expand Up @@ -142,7 +142,7 @@ const bodyTextContent = marked.parse(homepage.props.bodyText);
</style>
```
Since a ResourceArray is just an array of subjects we can map through them and pass the subject over to the `<Project />` component.
Since a ResourceArray is just an array of subjects we can map through them and pass the subject to `<Project />`.
Our homepage is now complete and looks like this:
Expand Down
25 changes: 16 additions & 9 deletions docs/src/astro-guide/9-blogs.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,7 @@ Finally, you can also ask the collection to return the member at a certain index
const member = await collection.getMemberWithIndex(10);
```

## Creating the blog's content page

Let's add the new blog list page to our website. Inside `src/pages` create a folder called `blog` and in there a file called `index.astro`.
Let's add a new blog list page to our website. Inside `src/pages` create a folder called `blog` and in there create a file called `index.astro`.
This page will live on `https://<your domain>/blog`.
This will be a list of all our blog posts.

Expand Down Expand Up @@ -180,14 +178,16 @@ We set `sort-by` to `published-at` so the list is sorted by publish date.
Then we set `sort-desc` to true so the list is sorted from newest to oldest.

We get an array of the post subjects using the `blogCollection.getAllMembers()`.
Then in the layout, we map over this array and render a `<BlogCard />` for each of the subjects.
Then in the markup, we map over this array and render a `<BlogCard />` for each of the subjects.

Save and navigate to `localhost:4321/blog` and you should see the new blog page.

![](img/9-3.webp)

Clicking on the links brings you to a 404 page because we haven't actually made the blog content pages yet so let's do that now.

## Creating the blog's content page

Our content pages will live on `https://<your domain>/blog/<title-slug>` so we need to use a route parameter to determine what blog post to show.
In Astro, this is done with square brackets in the file name.

Expand All @@ -200,13 +200,13 @@ This is because by default Astro generates all pages at build time (called: Stat
This is fixed by exporting a `getStaticPaths` function that returns a list of all URLs the route can have.

The other downside of static site generation is that to see any changes made in your data the site needs to be rebuilt.
Most hosting providers like Netlify and Vercel make this very easy so this might not be a big problem for you but if you have a content team that is churning out multiple units of content a day rebuilding each time is not a viable solution.
Most hosting providers like Netlify and Vercel make this very easy so this might not be a big problem for you but if you have a content team that is churning out multiple units of content a day, rebuilding each time is not a viable solution.

Luckily Astro also supports Server side rendering (SSR).
This means that it will render the page on the server when a user navigates to it.
When SSR is enabled you won't have to tell Astro what pages to build and therefore the `getStaticPaths` function can be skipped.
Changes in the data will also reflect on your website without needing to rebuild.
This guide will continue to use Static Site Generation however but feel free to enable SSR if you want to, if you did you can skip the next section about `getStaticPaths`.
This guide will continue to use Static Site Generation however but feel free to enable SSR if you want to, if you do you can skip the next section about `getStaticPaths`.
For more info on SSR and how to enable it check out [The Astro Docs](https://docs.astro.build/en/guides/server-side-rendering/).

### Generating routes with getStaticPaths()
Expand Down Expand Up @@ -266,7 +266,7 @@ const { subject } = Astro.props;
Here we define and export a `getStaticPaths` function.
In it, we create a collection of all blog posts in our drive.
We create an empty array that will house all possible params.
We create the empty array: `paths` that will house all possible params.
We then iterate over the collection, get the blog post from the store and push a new `GetStaticPathsItem` to the paths array.
In this item, we set the slug param to be the title-slug of the post and also add a `props` object with the subject of the post which we can access inside the component using `Astro.props`.
Expand All @@ -279,7 +279,9 @@ Now when you click on one of the blog posts on your blog page you should no long
### Building the rest of the page
If you opted to use SSR and skipped the `getStaticPaths` function replace `const {subject} = Astro.props` with:
<div style="background-color: #86caf836; padding: 1rem; border: 1px solid #86caf8;">
<strong>NOTE:</strong></br>
If you opted to use SSR and skipped the <code>getStaticPaths</code> function replace <code>const {subject} = Astro.props</code> with:
```ts
const { slug } = Astro.params;
Expand All @@ -299,6 +301,8 @@ if (!subject) {
}
```
</div>
The rest of the page is not very complex, we use the subject passed down from the getStaticPaths function to fetch the blog post and use marked to parse the markdown content:
```jsx
Expand Down Expand Up @@ -389,7 +393,7 @@ The blog post page should now look something like this:
The only thing left is a Header with the image and title of the blog post.
Create a new component in the components folder called `BlogPostHeader.astro`
Create a new component in the `src/components` folder called `BlogPostHeader.astro`
```jsx
---
Expand Down Expand Up @@ -495,3 +499,6 @@ const { resource } = Astro.props;
That should be it. Our blog post now has a beautiful header.
![](img/9-7.webp)
Our site is almost complete but it's missing that one killer feature that shows you're not a developer to be messed with.
A real-time search bar 😎.
Binary file added docs/src/astro-guide/videos/10-1.mp4
Binary file not shown.
Binary file added docs/src/astro-guide/videos/8-1.mp4
Binary file not shown.

0 comments on commit 27a8128

Please sign in to comment.