![]() |
VOOZH | about |
We’re so glad you’re here. You can expect all the best TNS content to arrive Monday through Friday to keep you on top of the news and at the top of your game.
Check your inbox for a confirmation email where you can adjust your preferences and even join additional groups.
Follow TNS on your favorite social media networks.
Become a TNS follower on LinkedIn.
Check out the latest featured and trending stories while you wait for your first TNS newsletter.
// src/content/config.js
import { z, defineCollection } from 'astro:content';
export const collections = {
posts: defineCollection({
type: 'content',
schema: z.object({
draft: z.boolean().optional(),
audioFeedId: z.string().optional(),
base: z.string(),
title: z.string(),
tags: z.array(z.string()).optional(),
date: z.date(),
author: z.string(),
featuredImage: z.string(),
}),
}),
};
src in the repo here: src/content/config.js.
And for good measure, here’s the frontmatter for one of my blog posts (but all blog posts will use the same schema).
// src/content/posts/2024/02/the-qwik-astro-audiofeed-experiment.mdx --- base: posts title: The Qwik, Astro, Audiofeed Experiment tags: [Qwik, Astro, Audiofeed, AI] date: 2024-02-06 author: Paul Scanlon featuredImage: https://res.cloudinary.com/www-paulie-dev/image/upload/v1707261626/paulie.dev/2024/02/get-started-with-qwik-astro_qtxmyq.jpg ---
src in the repo here: the-qwik-astro-audiofeed-experiment.mdx.
all-content.json.js and it lives in the src/pages directory. E.g.:
// src/pages/all-content.json.js
import { getCollection } from 'astro:content';
export const GET = async () => {
const posts = await getCollection('posts');
const search = posts
.filter((item) => item.data.draft !== true)
.map((data) => {
const {
slug,
data: { base, title, date },
} = data;
return {
date: date,
title: title,
base: base,
path: `/${base}/${slug}`,
};
})
.sort((a, b) => b.date - a.date);
return new Response(JSON.stringify({ search }));
};
getCollection('posts'), I do a quick filter to remove any blog posts that might be in draft mode, then return just the fields from the frontmatter that will be helpful for the search, and then sort them by date.
The result is stringified and returned as a standard Response.
Here’s what the result looks like.
[
{
date: 2024-02-22T00:00:00.000Z,
title: 'How to Build a Survey With KwesForms and Astro',
base: 'posts',
path: '/posts/2024/02/how-to-build-a-survey-with-kwesforms-and-astro'
},
{
date: 2024-02-06T00:00:00.000Z,
title: 'The Qwik, Astro, Audiofeed Experiment',
base: 'posts',
path: '/posts/2024/02/the-qwik-astro-audiofeed-experiment'
}
...
]
src in the repo here: src/pages/all-content.json.js.
This data provides everything I’ll need to start building the search component.
// src/pages/index.astro --- import Layout from '../layouts/layout.astro'; --- <Layout> <h1>Lorem ipsum</h1> <p>...</p> </Layout>
src in the repo here: src/pages/index.astro.
And here’s the layout component which makes a server-side request to the endpoint.
// src/layouts/layout.astro
---
import Search from '../components/search';
const content = await fetch(`${import.meta.env.PROD ? 'https://tns-astro-site-search.netlify.app' : 'http://localhost:4321'}/all-content.json`);
const { search } = await content.json();
---
<html lang='en'>
<head>...</head>
<body>
<header>
<Search data={search} />
</header>
<main>
<slot />
</main>
</body>
</html>
PROD is true, the URL to the static endpoint will be https://tns-astro-site-search.netlify.app/all-content.json, and while in development the localhost URL is used.
Provided I am able to query the search data, I can pass it on to my search component via the data prop.
You can see the src in the repo here: src/layouts/layout.astro.
npm install fuse.js @qwikdev/astro
src can be found here: src/components/simple-search.jsx.
Note: The example used in my demo contains a lot of additional CSS and JavaScript to handle the modal, which isn’t required to create search functionality.
onInput$ event handler and create a function named handleInput to capture the keystrokes.
// src/components/simple-search.jsx
import { component$, $ } from '@builder.io/qwik';
const Search = component$(({ data }) => {
const handleInput = $(async (event) => {
const {
target: { value },
} = event;
});
return (
<div>
<input type='text' placeholder='Search' onInput$={handleInput} />
</div>
);
});
export default Search;
useSignal, and create two new constants to hold the values for all the data and the filtered data.
// src/components/simple-search.jsx
- import { component$, $ } from '@builder.io/qwik';
+ import { component$, $, useSignal } from '@builder.io/qwik';
const Search = component$(({ data }) => {
+ const all = useSignal(data);
+ const filtered = useSignal(data);
const handleInput = $(async (event) => {
const {
target: { value },
} = event;
});
return (
<div>
<input type='text' placeholder='Search' onInput$={handleInput} />
</div>
);
});
export default Search;
useSignal const (all.value) and will apply a fuzzy filter threshold of 0.5 when any input values match values for the title or date.
fuse.search can be used to filter out any items from the array that don’t meet the config parameters, and a new array is returned. I’ve called this new array “results.”
// src/components/simple-search.jsx
import { component$, $, useSignal } from '@builder.io/qwik';
const Search = component$(({ data }) => {
const all = useSignal(data);
const filtered = useSignal(data);
const handleInput = $(async (event) => {
const {
target: { value },
} = event;
+ const FuseModule = await import('fuse.js');
+ const Fuse = FuseModule.default;
+ const fuse = new Fuse(all.value, {
+ threshold: 0.5,
+ keys: ['title', 'date'],
+ });
+ const results = fuse.search(value).map((data) => {
+ const { item: { base, path, title, date } } = data;
+ return {
+ title,
+ date,
+ path,
+ base,
+ };
});
});
return (
<div>
<input type='text' placeholder='Search' onInput$={handleInput} />
</div>
);
});
export default Search;
if statement. If there’s a value captured from the HTML input, then I set useSignal filtered.value equal to the results, and if there’s no value captured from the HTML input then I set the useSignal filtered.value equal to the all.value.
This will either return a filtered list, or the whole list.
// src/components/simple.search.jsx
import { component$, $, useSignal } from '@builder.io/qwik';
const Search = component$(({ data }) => {
const all = useSignal(data);
const filtered = useSignal(data);
const handleInput = $(async (event) => {
...
+ if (value) {
+ filtered.value = results;
+ } else {
+ filtered.value = all.value;
+ }
});
return (
<div>
<input type='text' placeholder='Search' onInput$={handleInput} />
</div>
);
});
export default Search;
filtered.value (if it has length) and return a list of items. If there are no results, then I return null.
// src/components/simple-search.jsx
import { component$, $, useSignal } from '@builder.io/qwik';
const Search = component$(({ data }) => {
const all = useSignal(data);
const filtered = useSignal(data);
const handleInput = $(async (event) => {
...
});
return (
<div>
<input type='text' placeholder='Search' onInput$={handleInput} />
+ <ul>
+ {filtered.value.length > 0
+ ? filtered.value.map((data, index) => {
+ const { path, title } = data;
+ return (
+ <li key={index}>
+ <a href={path}>{title}</a>
+ </li>
+ );
+ })
+ : null}
+ </ul>
</div>
);
});
export default Search;