In the previous guide we already tried to create a static website to show the content that we retrieved from Nimvio API CD. This guide will provide you with how to create a dynamic website powered by Nimvio. We will create a simple page with a search feature for our website at this time as this feature is common and often to use in every website.
Take a note before we start, in this guide sample we will use Nuxt as our framework to build the page.
What will we do:
First we need a search field component to search contents. The component must be able to pass any value that already input to the field.
<template>
<form @submit.prevent="handleSearch">
<div class="relative">
<input
id="search"
v-model="state.search"
type="search"
class="block focus:outline-0 p-4 py-3 bg-light-white w-full text-sm rounded-sm border border-dark-gray focus:ring-royal-blue focus:border-royal-blue"
placeholder="Type here to search"
/>
<!-- a custom button -->
<common-button-link
class="absolute right-0 top-0 h-full flex items-center justify-center rounded-r-sm rounded-l-none"
role="button"
@click.prevent="handleSearch"
>
Search
</common-button-link>
</div>
</form>
</template>
The v-model
attribute inside the <input>
is for passing the input value to our state and the handleSearch
is a function to run our search functionality that we will explain in the next step. For the result of this search component creation is like image below.
Next, we will add a function to our search component to pass the input value to the route or address of our website as a query.
<script setup>
const state = reactive({
search: "",
});
const router = useRouter();
const route = useRoute();
const searchQuery = route.query.q;
// passing the input value to the route query
const handleSearch = () => {
router.push(`/search?q=${state.search}`);
};
// define the value of the input from router query q
onMounted(() => {
state.search = searchQuery;
});
</script>
The result will looks like image below. Every time we input something inside the field then click the Search button, the address will update with q query with our input value in address bar.
After we complete to create the search component, the next step is showing the search result by creating a search result page.
<template>
<section v-if="data && !pending" class="container">
<div class="flex items-center flex-col lg:flex-row gap-2 py-8">
<h2 class="text-2xl font-bold text-royal-blue">
{{
`${data.totalItems} ${
data.totalItems > 1 ? "results" : "result"
} for '${route.query.q || ""}'`
}}
</h2>
<div class="grow"></div>
<span v-if="showPagination">{{
`Page ${state.page} of ${paginate.totalPages}`
}}</span>
</div>
<hr class="pb-5" />
<div class="flex flex-col gap-4">
<div
v-for="result in paginate.results"
:key="result.ContentID"
class="pb-5"
>
<!-- hardcode the URL temporary with a custom link component -->
<common-text-link :to="getRedirectRoutes(result)">
<h2 class="text-2xl text-royal-blue font-bold mb-5">
{{ result.Data.pageTitle }}
</h2>
</common-text-link>
<p
class="line-clamp-4 mb-5"
v-html="highlightResult(result.Data.content, route.query.q || '')"
></p>
<hr />
</div>
</div>
<!-- a custom pagination component -->
<common-pagination
v-if="showPagination"
v-model:page="state.page"
:page-size="state.pageSize"
:total-pages="paginate.totalPages"
/>
</section>
<div v-else class="h-[50vh] flex items-center justify-center">
<!-- a custom loading component when there's no data and at pending state -->
<common-loader />
</div>
</template>
When fetching the search content in Nuxt, we used useLazyAsyncData() and $fetch.
const { data, refresh, pending } = await useLazyAsyncData(
"search",
() => {
return $fetch(`${config.APIES_URL}/${config.projectId}`, {
method: "POST",
body: {
size: 100,
query: {
bool: {
must: [
{ match: { "Data.content": route.query.q || "" } },
{ match: { TemplateName: "Page" } },
],
},
},
},
});
},
{ server: false, initialCache: false }
);
In the code above, we add conditional in the fetch query to only fetch the content that match with our searched route query from search component and the content template is a Page. The request payload and result can be seen in image below.
Last step is to plotting our searched data to our Search page HTML.
const { public: config } = useRuntimeConfig();
const route = useRoute();
const state = reactive({
page: 1,
pageSize: 10,
results: (data && data.value && data.value.data) || [],
});
// Handle pagination
const paginate = computed(() => {
const pageSize = state.pageSize;
const startIdx = pageSize * (state.page - 1);
const results = [...state.results].splice(startIdx, pageSize);
const totalPages = Math.max(Math.ceil(state.results.length / pageSize), 1);
return {
results,
totalPages,
};
});
// Search and Highlight Utils
// Return concat str
function concatStr(str, length, start) {
let result = "";
if (str.length <= length && !start) {
return str;
}
if (start > 0) {
result += "...";
}
result += str.substr(start, length);
if (str.length - start > length) {
result += "...";
}
return result;
}
function countOccuringText(fullText, text) {
const pattern = new RegExp(text, "gi");
const matchResult = fullText.match(pattern);
return matchResult ? matchResult.length : 0;
}
// Get the most occuring search result
function getMostOccuringText(fullText, texts) {
const maxLength = 300;
let currentIdx = 0;
let maxCounter = 0;
let maxOccurenceIdx = 0;
while (fullText.length - currentIdx > maxLength) {
const currentSubstr = fullText.substr(currentIdx, maxLength);
let counter = 0;
for (const text of texts) {
counter += countOccuringText(currentSubstr, text);
}
if (counter >= maxCounter) {
maxCounter = counter;
maxOccurenceIdx = currentIdx;
}
currentIdx++;
}
return concatStr(fullText, maxLength, maxOccurenceIdx);
}
// ExtractContent from HTML https://stackoverflow.com/a/54344724
function extractContent(htmlString) {
if (!htmlString) return "";
return htmlString.replace(/<[^>]+>/g, "");
}
const highlightResult = (content, target) => {
let result = extractContent(content);
const targetArr = target.trim().split(" ");
result = getMostOccuringText(result, targetArr);
targetArr.forEach((text) => {
const pattern = new RegExp(text, "gi");
result = result.replace(pattern, `${text}`);
});
return result;
};
const showPagination = computed(() => {
return state.results && state.results.length > 0;
});
// Set results after data has been retrieved
watch(data, (value) => {
state.page = 1;
state.results = [...value.data];
});
watch(
() => route.query,
() => {
refresh();
}
);
onUnmounted(() => {
state.results = [];
});
const getRedirectRoutes = (item) => {
// Find routes from saved routes in the runtimeConfig
const foundRoute = config.routes.find(
(route) => route.ContentID === item.ContentID
);
if (foundRoute) {
return foundRoute.route;
}
};
The result will be looks like this.
Congratulations! You have just finished the guide. Keep exploring others below: