Developer

Creating Nimvio Theme

What we will do:

Introduction

Nimvio themes consist of a Nimvio project along with its frontend templates. A Nimvio project can serve as a theme if it already has a working frontend template that matches it.

Building on the previous guide on Structuring Reusable and Scalable Content, this guide will walk you through creating a Nimvio Theme using a project with a similar content structure. To follow this guide, it is necessary to have previous knowledge of front-end web development and JAMStack approach. Familiarity with Vue and Nuxt will also be helpful since we will be using the Royal Blue on White theme as a reference.

To make the most of this guide, we suggest beginning a Nimvio project using the Royal Blue on White theme as a starting point, which can be done by following the Quick Start Guide.

Step 1: Creating Content Templates

There are approximately 20 content templates that have been developed for the Royal Blue on White theme. The most commonly used template is named "Page", and you can find an example of how to use it in the guide on creating a new page in the Live Preview Editor.

Another noteworthy template is called "Widget", which is used for common website elements like headers, footers, breadcrumbs, and search bars. If you go to the Content Menu, you will see a comprehensive list of all content, including the templates that are currently being used. You can filter the list by template name, as demonstrated in this example, where the content that uses the template named "Widget" is filtered.

https://media.nimvio.com/Project_c1457a00-729a-456c-841b-5c10710e8a18/Media/Resources/Guide/Creating%20Nimvio%20Theme/Widgets%20Listing_published.png

Step 2: Implementing Website Layout

In this section, we will introduce the concept of a website Layout, which primarily serves to structure pages with outer HTML markup. A Layout consists of placeholders, and different layouts may have different placeholders. The image below shows two different layouts with varying compositions of placeholders.

https://media.nimvio.com/Project_c1457a00-729a-456c-841b-5c10710e8a18/Media/Resources/Guide/Creating%20Nimvio%20Theme/Layout%20Example_published.jpg

To associate a Layout with a Page Content Item, a Layout Item must be defined, as shown in the guide "Structuring Reusable and Scalable Content". The Layout Item named Default uses a Template named Layout, which requires two values to be filled:

  1. Layout Name: This will be the name of the Layout and will be used as a reference on the page Content Item.
  2. Layout Implementation: This will be the path where the layout is implemented in the frontend code. Although this value won't be consumed via API, it is crucial to let us know the location of the layout implementation.

When we open the content of the Default layout, we can see that the implementation code is located at /layouts/default.vue. You can find the code for it in the following repository: https://github.com/nimvio-github/theme-royal-blue-on-white.


<template>
  <div>
    <slot name="empty"></slot>

    <header class="header">
      <slot name="header"> </slot>
    </header>

    <slot name="main"> </slot>

    <footer class="footer">
      <slot name="footer"> </slot>
    </footer>
  </div>
</template>

<script>
export default {
  name: "Default",
};
</script>

<style lang="scss">
@media (min-width: 768px) {
  .footer {
    margin-top: 6rem;
  }
}

.footer {
  margin-top: 3rem;
  color: $nimvio-white;
  background-color: $nimvio-black;
}
</style>

The code snippet presented above displays placeholders in the template section, where slots are used in this example. The remaining section should be familiar to Vue developers as it is a typical Vue single-file component.

Step 3: Implementing Placeholders and Widgets

A placeholder serves as a location where widgets can be held. Widgets, which are user interface components, are assigned to placeholders and can be assigned to multiple placeholders within a layout. Please refer to the image below.

https://media.nimvio.com/Project_c1457a00-729a-456c-841b-5c10710e8a18/Media/Resources/Guide/Creating%20Nimvio%20Theme/Widgets_published.jpg

In contrast to placeholders, which are abstract concepts that don't require concrete content associated with them, widgets require a Widget Item to be defined. Let's take a closer look at the Common - Header Bar Widget Item, which uses a Content Template called Widget and has four fields that need to be filled:

  1. Name: This serves as the widget's reference during rendering.
  2. Implementation Path: This indicates where the code implementation for the widget can be found.
  3. Placeholder: This specifies the placeholder where the widget will be rendered.
  4. Datasource: This determines the data source that the widget will use.
https://media.nimvio.com/Project_c1457a00-729a-456c-841b-5c10710e8a18/Media/Resources/Guide/Creating%20Nimvio%20Theme/Common%20Header%20Widget_published.png

We will now analyze the implementation of the Header Bar widget, which is located in components/widgets/header-bar.vue.

Part 1: Template

The template section of the component generates the HTML for dynamic elements like Company Logo and Navigation Items.


<template>
  <section class="navbar">
    <nav class="container navbar__wrapper">
      <div>
        <NuxtLink v-if="props.logo" to="/">
          <nuxt-img
            :src="props.logo"
            :alt="props.logoAlt || 'website logo'"
            class="navbar__logo"
            height="32"
            width="165"
            format="webp"
            fit="inside"
          />
        </NuxtLink>
      </div>

      <div class="navbar__menu-separator"></div>

      <!-- Desktop -->
      <div class="navbar__links">
        <template v-for="item in data.items">
          <header-nav-item
            v-if="item && item.isShow"
            :key="item.id"
            :data-nimvio-content-id="item.ContentID"
            :data-nimvio-template-name="item.TemplateName"
            :text="item.text"
            :to="item.to"
            :nav-childs="item.children"
          />
        </template>
      </div>
      <div class="navbar__menu-separator"></div>

      <header-nav-search
        href="javascript:void(0)"
        @click="state.searchOpen = !state.searchOpen"
      />
      <header-nav-hamburger
        :class="state.open && 'open'"
        @click="state.open = !state.open"
      />
    </nav>

    <header-nav-search-input
      v-if="state.searchOpen"
      class="navbar__search--desktop"
    />

    <!-- Mobile -->
    <nav v-if="state.open" class="navbar__wrapper--mobile">
      <header-nav-search-input />
      <hr />
      <div class="container navbar__links--mobile">
        <template v-for="item in data.items">
          <header-nav-item
            v-if="item && item.isShow"
            :key="item.id"
            :text="item.text"
            :data-nimvio-content-id="item.ContentID"
            :data-nimvio-template-name="item.TemplateName"
            :to="item.to"
            :nav-childs="item.children"
          />
        </template>
      </div>
    </nav>
  </section>
</template>

Part 2: Script

The script section is responsible for retrieving the necessary content items that the component needs to render correctly. It uses an async function such as `getChildPages` to abstract the data fetching logic, which internally makes a call to Nimvio Content Delivery API.

For this implementation, we use the preview API of GraphQL API (see Working with GraphQL API for further details). We also see the implementation with Nimvio Live Preview SDK, which enables Preview updates on content changes (see Nimvio Live Preview SDK for further details).


<script setup>
import { getChildPages } from "~~/utils/dataFetching";

const props = defineProps({
  navigationItemsId: {
    type: String,
    required: true,
  },
  logo: {
    type: String,
    required: true,
  },
  logoAlt: {
    type: String,
    default: "",
  },
});

const { data } = await useAsyncData("headerBar", async ({ $gqlClient }) => {
  const { data: pages } = await getChildPages(
    $gqlClient,
    props.navigationItemsId
  );

  const items = pages.map((page) => {
    const pageData = page.Data;

    return {
      text: pageData.navigationTitle || pageData.pageTitle,
      to: pageData.urlPath,
      navigationTitle: pageData.navigationTitle,
      children: [],
      isShow: pageData.navigation.showInMenu,
      ...page,
    };
  });

  return {
    items,
  };
});

const { $nimvioSdk } = useNuxtApp();
onBeforeMount(() => {
  $nimvioSdk.livePreviewUtils.onPreviewContentChange((formData) => {
    data.value.items = data.value.items.map((item) => {
      if (item.ContentID === formData.id) {
        return {
          ...item,
          text:
            formData.formData.navigationTitle || formData.formData.pageTitle,
          to: formData.formData.urlPath,
          navigationTitle: formData.formData.navigationTitle,
          isShow: formData.formData.navigation.showInMenu,
        };
      }
      return item;
    });
  });
});

const state = reactive({
  open: false,
  searchOpen: false,
});
</script>

Part 3: Style

The style section is fairly straightforward. In addition to desktop styling, it also manages the page's responsiveness using media queries.


<style lang="scss">
.navbar {
  background-color: $nimvio-white;
}

.navbar__wrapper {
  display: flex;
  position: relative;
  flex-wrap: wrap;
  align-items: center;
  height: 6rem;
  margin-left: auto;
  margin-right: auto;
}

.navbar__logo {
  width: auto;
  max-height: 2rem;
  height: auto;
}

.navbar__menu-separator {
  display: flex;
  flex-grow: 1;
}

.navbar__links {
  display: none;
  gap: 2rem;
  align-items: center;
  height: 100%;
  margin-left: 1rem;
  margin-right: 1rem;
}

@media (min-width: 1024px) {
  .navbar__links {
    display: flex;
  }
}

.navbar__wrapper--mobile {
  display: block;
  width: 100%;
  background-color: $nimvio-white;
}

@media (min-width: 1024px) {
  .navbar__wrapper--mobile {
    display: none;
  }
}

.navbar__links--mobile {
  padding-top: 2rem;
  padding-bottom: 2rem;
  display: flex;
  flex-direction: column;
  gap: 1rem;
}
</style>

 

Step 4: Implementing Page Template

Given that Nuxt offers file-based routing (as documented in the Nuxt pages directory), let's examine the implementation of the top-level page, specifically the homepage.

Part 1: Template

The template section consists of a page that displays a list of widgets, which are rendered using a Component Renderer. Take a look at the ComponentRenderer.vue located in the /components directory. Its purpose is to display widgets by identifying the Content Template Name, and if the Template Name is Widget, it will render based on the Widget Item name field.


<template>
  <NuxtLayout :name="data.Data.layoutName ? data.Data.layoutName : 'default'">
    <template v-for="(contents, key) in data?.widgets" #[key]>
      <component-renderer
        :key="key"
        :components="contents"
      ></component-renderer>
    </template>
  </NuxtLayout>
</template>

Part 2: Script

The composable useAsyncData is utilized to retrieve content data with a callback function that modifies the content structure of the response. The callback function retrieves the response from the async function getContentById for newly created pages, enabling live preview functionality.

If the page already exists, the callback function calls another async function, getContentByPageSlug, to retrieve and transform the content.


<script setup>
import { getContentByPageSlug } from "~~/utils/dataFetching";
import transformContent from "~~/utils/transformContent";

const route = useRoute();
const currentPath = route.path === "/" ? "/home" : route.path;

const { data, refresh, pending } = await useAsyncData(
  route.path,
  async ({ $gqlClient }) => {
    // If it is inside the iframe (has isNewPage and contentId query), fetch using contentId
    if (route.query.isNewPage === "true" && route.query.contentId) {
      const { data: newResponse } = await getContentById(
        $gqlClient,
        route.query.contentId,
        {
          deep: true,
        }
      );

      return transformContent(newResponse);
    }

    const { data: response } = await getContentByPageSlug(
      $gqlClient,
      currentPath,
      {
        deep: true,
      }
    );

    return transformContent(response);
  }
);

const showEmpty = computed(() => {
  const { Data } = data.value;
  return !Data.layoutName && !Data.placeholder && !Data.contentTitle;
});

const updateContentById = (content, id, newContent, cache = {}) => {
  if (cache[content?.ContentID]) return null;
  cache[content?.ContentID] = true;

  if (content?.ContentID === id) {
    content.Data = newContent;
    return content;
  }

  const componentDataKeys = Object.keys(content.Data);

  for (let i = 0; i < componentDataKeys.length; i++) {
    const componentDataKey = componentDataKeys[i];

    // Check if it is linked content
    if (Array.isArray(content.Data[componentDataKey])) {
      for (let j = 0; j < content.Data[componentDataKey].length; j++) {
        const found = updateContentById(
          content.Data[componentDataKey][j],
          id,
          newContent,
          cache
        );
        if (found) {
          content.Data[componentDataKey][j] = found;
        }
      }
    }
  }
  return content;
};

const { $nimvioSdk } = useNuxtApp();
onBeforeMount(() => {
  $nimvioSdk.livePreviewUtils.onPreviewContentChange((formData) => {
    const newContent = updateContentById(
      data.value,
      formData.id,
      formData.formData
    );

    if (newContent) {
      data.value = transformContent(newContent);
    }
  });
});

useHead({
  title: data.value?.Data.pageTitle,
});
</script>

Conclusion

This guide covers the definition of Nimvio theme, which is a project including frontend templates. It explains various content templates that are designed to make the website highly flexible and easy to modify. It introduces the concept of layouts, placeholders, and widgets, and provides an implementation guide for each of these concepts. The guide concludes with a walkthrough of the page implementation. Even though a lot has been covered, there is still more to explore. You can also refer to the Royal Blue on White as a reference. May the force be with us!

What's Next?

Congratulations! You have finished this part of the guide. Keep exploring below: