Developer

Creating a Dynamic Website

Introduction

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.

Prerequisite
You already read and follow Getting Started guide, and already create a Nimvio account, a project, and content(s) to be searched for.

What will we do:

  1. Step 1: Create a Search Field Component
  2. Step 2: Functionality in Search Field
  3. Step 3: Create a Search Result Page
  4. Step 4: Fetch Searched Data
  5. Step 5: Plotting Searched Data

Step 1: Create a Search Field Component

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.

https://media.nimvio.com/Project_c1457a00-729a-456c-841b-5c10710e8a18/Media/Resources/Guide/Create%20a%20Dynamic%20Page/royal-blue_search-component_published.png

Step 2: Functionality in Search Field

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.

https://media.nimvio.com/Project_c1457a00-729a-456c-841b-5c10710e8a18/Media/Resources/Guide/Create%20a%20Dynamic%20Page/royal-blue_search-passing-query_published.png

 

Step 3: Create a Search Result Page

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>

Step 4: Fetch Searched Data

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.

https://media.nimvio.com/Project_c1457a00-729a-456c-841b-5c10710e8a18/Media/Resources/Guide/Create%20a%20Dynamic%20Page/royal-blue_search-request_published.png

Step 5: Plotting Searched Data

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.

https://media.nimvio.com/Project_c1457a00-729a-456c-841b-5c10710e8a18/Media/Resources/Guide/Create%20a%20Dynamic%20Page/royal-blue_search-demo_published.gif

Congratulations! You have just finished the guide. Keep exploring others below: