docs
  1. SCAYLE Resource Center
  2. Developer Guide
  3. Features
  4. Pages
  5. Product Listing Page
  6. Customization

Customization

The Product Listing Page (PLP) is designed with flexibility in mind, allowing you to tailor its appearance and functionality to meet specific business needs. Below are the key areas where customization is available either through our SCAYLE Panel or directly in the source code.

If you haven't already, please start by reading our General Overview of the PLP page.

Category Structure

The Category Structure is an essential part of any e-commerce shop. It provides a clear and structured way to display product categories and subcategories, helping users navigate complex structures with ease. Furthermore, a clear category structure encourages product exploration and improves the usability aspect of the overall shop.

Our category structure can be easily configured through the SCAYLE Panel. For detailed guidance on setting up new categories and updating the structure, refer to the Shop Categories Documentation.

Storefront Boilerplate automatically fetches the category tree from the Storefront API and displays the tree to the user to allow for easy navigation. There is a slight difference in the appearance of category structure between our two web stacks:

  • Desktop View
    • By default, the category tree is displayed as a side navigation panel, showing root categories and their immediate children. Selecting a child category will reveal its own children below.

Desktop category navigation

  • Mobile View
    • On mobile devices, the category tree is displayed using breadcrumbs for upper-level categories and chips for immediate children of the currently active category.

Mobile category navigation

Content Banners

To make your PLP visually stand out even more, it can be beneficial to show a related image at the top of the page. Besides the page being more visually appealing, by using the banner, you can define a focus point as it will be in the user's first viewport of your PLP page. This can help you boost specific marketing campaign, category page, product brand or something else.

This requires a working CMS Setup as described on our CMS Integrations page for either Storyblok or Contentful.

For this we will need two components that come out of the box with the SCAYLE Storefront CMS Module:

  1. CMSCategoryData which fetches the data and makes it available to the child components using a slot
  2. CMSImage which is the actual component that will render the image

When you open the file pages/c/[...categories]/[...slug]-[id].vue you will see the entry point and main code from the PLP. Upon further inspection, we can see that we render the CategorySideNavigation component and next to it, we have our main content of the page.

We now want to show an image on top of the PLP ahead of any other content.

The first step is to fetch the data using our CMSCategoryData component, like this:

<div class="w-full grow">
  <CMSCategoryData :selected-category="currentCategoryId" class="flex flex-col">
    <template #default="{ content, hasTeaserImage }">
      <!-- Child Rendering -->
    </template>
  </CMSCategoryData>
  <!-- ... -->
</div>

When rendered, this component will fetch the data from the selected CMS Provider and make the content available in the template slot.

We can then use this content provided to us to render the actual UI which can be easily achieved through the CMSImage component:

<CMSImage
  v-if="hasTeaserImage" # We only want to render the component if we have a teaser image defined for this category
  :blok="content" # The actual content where the image is defined we will show
  is-teaser # `is-teaser` selects the correct image from the `content`
  is-cover # `is-cover` makes sure we fill the whole space with the image, potentially cutting off parts of it
  class="h-[12.5rem] w-full" # we give the element a specific height and the full width
/>

The full code looks like this then:

<div class="w-full grow">
  <CMSCategoryData :selected-category="currentCategoryId" class="flex flex-col">
    <template #default="{ content, hasTeaserImage }">
      <CMSImage
        v-if="hasTeaserImage" # We only want to render the component if we have a teaser image defined for this category
        :blok="content" # The actual content where the image is defined we will show
        is-teaser # `is-teaser` selects the correct image from the `content`
        is-cover # `is-cover` makes sure we fill the whole space with the image, potentially cutting off parts of it
        class="mb-4 h-[12.5rem] w-full" # we give the element a specific height and the full width
      />
    </template>
  </CMSCategoryData>
  <!-- ... -->
</div>

The final output of the implementation is:

CMS Content Banner in the PLP page

Filters

Filters are crucial tools that help your users narrow down product searches and find items that meet their specific criteria. Well-maintained and business-focused filters should be a pillar of every successful e-commerce business.

Filter Flyout

Our filters are configured through the SCAYLE Panel and are presented in a filter flyout, as shown in the picture above. They consist of default filters and category-specific filters based on category attributes.

Default Filters

Default filters are set at the shop level and apply to all categories. Common examples of default filters include price or sale. For detailed information on configuring default filters, please refer to the Default Filters Setup section of the User Guide.

Category-Specific Filters

Category-specific filters are based on attributes unique to each category. For more details on configuring these filters, see the Category-Specific Filters Configuration section of the User Guide.

Color Filter

The color filter is special because it shows actual color bubbles instead of just the color names.

Color Filter

To achieve this, we have a mapping of color attribute value to hex codes as key-value pairs in our codebase.

Since the values for your color attribute can be different from ours, they can be easily adjusted in the constants/product.ts file.

export const ProductColor: Record<string, string | string[]> = {
  weiss_1: '#ffffff',
  beige: '#e3dad1',
  schwarz: '#000000',
  grau: '#6b7280',
  rot: '#ef4444',
  blau: '#3b82f6',
  gruen: '#22c55e',
  gelb: '#eab308',
  orange: '#f97316',
  braun: '#bfa094',
  pink: '#ec4899',
  lila: '#a855f7',
  mischfarben: ['#0000ff', '#ffa500', '#ff0000', '#008000'],
}

Sorting Options

In the Storefront Boilerplate, we provide the most common default sorting options out of the box:

  • Recommended
  • Newest
  • Price descending
  • Price ascending
  • Reduction descending

PLP's Sorting Options

The "Recommended" sorting option is selected by default. Additionally, the sorting option resets to default when changing categories.

In our Boilerplate, all of the sorting logic is handled within the composables/useProductListSort.ts file.

To adjust the sorting that is selected by default, you can change the DEFAULT_SORTING_KEY:

export const DEFAULT_SORTING_KEY = 'date_newest' satisfies keyof typeof sortingOptions

In the same file, you can also adjust the available sorting options, e.g. remove one of the options or reorder to your business requirements.

You can also change the sorting key that is used for the Recommended sorting option to a Custom Sorting Key or another Smart Sorting Key provided by SCAYLE.

const sortingOptions = {
  top_seller: {
    sortingKey: 'custom_sorting_key',
    direction: APISortOrder.Descending,
  },
  date_newest: {
    by: APISortOption.DateAdded,
    direction: APISortOrder.Descending,
  },
  price_desc: {
    by: APISortOption.Price,
    direction: APISortOrder.Descending,
  },
  price_asc: {
    by: APISortOption.Price,
    direction: APISortOrder.Ascending,
  },
  reduction_desc: {
    by: APISortOption.Reduction,
    direction: APISortOrder.Descending,
  },
}

Overall, you have the flexibility to customize these sorting options to better suit your specific business needs. These options can be modified using Sorting Keys, which you can configure through the SCAYLE Panel. For detailed instructions on setting up Smart or Custom Sorting Keys, please refer to the relevant section of the Developer Guide.

Product Cards

Product Cards in e-commerce are visual representations of individual products, which serve as a quick way for customers to browse and evaluate products throughout your shop. Even though it consists of a standard setup of components in our Boilerplate, they can also be customized to offer a more engaging & user-friendly shopping experience specific to the needs of your business. For example, its functionality can be extended to display additional product information or custom UI elements.

Product Cards

Our ProductCard composes multiple smaller components which then form the final ProductCard component.

We have the following components:

  • ProductCardImageSlider which is responsible to display the product images
  • WishlistToggle which renders the heart icon in the top right corner
  • ProductCardBadgesHeader which render the badges in the top left corner
  • ProductCardBadgesFooter which render the badges at the bottom of the ProductCardImageSlider
  • ProductCardDetails which renders the information below the ProductCardImageSlider

Image Logic

Which image is displayed first for each product can have a big impact on the user experience and conversion rate. It's important to select the best image and show this to the user.

In the Storefront Boilerplate, all image-related logic on how images are sorted and which image is shown first can be found in utils/image.ts.

By default, it includes a basic implementation using a primaryImage attribute to determine which image is displayed first. This attribute needs to be assigned to an image for each product. However, this setup may not align with your specific needs or business requirements.

The image logic essentially consists of two parts: first, a function that prioritizes and sorts all product images. This function dictates the order in which images are displayed on both the Product Card Component and the Product Detail Page.

export const sortProductImages = (images: ProductImage[]) => {
  return images.toSorted((imageA, imageB) => {
    if (isPrimaryImage(imageB)) {
      return 1
    }

    if (isPrimaryImage(imageA)) {
      return -1
    }

    return 0
  })
}

Additionally, the second part is a simple function that retrieves the primary image of a product. This function is used in scenarios where only a single image is displayed, without the need for an image slider.

export const getPrimaryImage = (images: ProductImage[]) => {
  return images.find(isPrimaryImage) ?? images[0]
}

Available Sizes

Customizing the available sizes on product cards involves displaying size options directly on the product card, allowing customers to quickly see their preferred size without having to navigate to the Product Detail Page. This customization can enhance the shopping experience and streamline the purchase process.

Here’s how it works in Storefront Boilerplate:

Let's start by opening our ProductCard component which is located in /components/product/card/ProductCard.vue.

Since we want to show this information below the product name, we need to go into ProductCardDetails component which renders the UI below the image.

First of all, we need to figure out which of the sizes of the product are available.
To do this, we set up a new computed value in our script which uses the variants of our product.

Variants are the most specific representation of a product. These items carry stock and price info and are being sold in the shops.

To learn more about the Product Structure in SCAYLE, head over to the Developer Guide.

We then need to filter out any variants that are not in stock any longer and we then from each variant take the size attribute value and use the label.

In our example the size attribute holds the differentiating attribute between the different variants, in a typical fashion e-commerce this would be S, M, or L. However, depending on your data structure in SCAYLE, you might use a different attribute for this.

// Import two helper functions from our NPM Package
import { getFirstAttributeValue, isInStock } from '@scayle/storefront-nuxt'

// We set up a "computed" value so that our UI automatically updates 
// in case the product provided to the component changes.
const availableVariants = computed(() => {
  // Loop over all product variants, 
  // only include variants which are in Stock
  // and map each variant to the respective attribute group size label
  return props.product.variants
    ?.filter((variant) => isInStock(variant))
    .map((variant) => getFirstAttributeValue(variant.attributes, 'size')?.label)
    .filter((label): label is string => !!label) ?? []
})

We now have a reactive variable that updates automatically if our inputs change which is a list of the labels of the available sizes.

We can now use this variable in our template to render this information.

First, we need to locate the product name in our existing template which should look like this:

<p data-testid="product-card-product-name" class="truncate text-sm text-gray-600">
  {{ name }}
</p>

So we now want to create a new HTML component that will be responsible for rendering the available sizes of the product:

We only render this component if we have some availableVariants, this is handled by the v-if directive which is like a normal if statement, it just conditionally renders a component.

We also give our component a bit of styling to look also consistent with the application.

<p
  v-if="availableVariants"
  class="truncate text-sm text-gray-600"
>
  {{ $t('available-sizes', { sizes: availableVariants.join(', ') }) }}
</p>

To also have a bit of space between the product name and the available sizes, we can add the class mb-0.5 to the product name paragraph which will give the component a small bottom margin.

The final outcome of the customization looks like this:

Available sizes as part of PLP's Product Cards

Badges

Badges are used to provide quick, additional categorization of products, highlighting features, novelty, or other attributes. The current implementation includes the following badge types:

  • New In Badge:
    • States that a product is newly added. This badge is based on the isNew property, which can be configured in the SCAYLE Panel under Settings using the isNew flag. By default, products are displayed as "New In" for the first 30 days after they go live in a country shop. You can adjust this period in days through the configuration settings. For more detailed information, refer to the dedicated section of the User Guide.
  • Custom Badges:
    • Custom badges can be created to highlight specific product features or attributes (e.g., sustainability, unisex). To add or modify custom badges, navigate to SCAYLE Panel -> Settings -> Attribute. Look for an attribute group named storefrontBadge. If it doesn't exist, create it. Within this attribute group, define the custom value. For example, if you want the badge "unisex," set the value to "unisex."
  • Already in basket:
    • The badge is displayed on products that have already been added to the user's shopping basket. This badge helps users quickly identify items they've already selected, reducing the chance of accidental duplicate purchases.

Badge Display

  • Combining Badges: If both default (e.g., "New In") and custom badges are applied to a product, they are displayed together in one component, separated by a visual delimiter. In the current implementation, the number of badges displayed in this component is limited to two.

Combining Badges

This customization allows you to tailor the product tiles to better fit your store's branding and highlight key product attributes effectively.

Low Stock Badge

The Low Stock Badge offers many benefits for your business. It signals to users that the item is running out, encouraging them to make a purchase quickly before it's gone. Furthermore, it triggers FOMO (Fear of Missing Out), which motivates the users to buy now rather than risk missing out on the product entirely. This can help your business effectively drive sales & improve inventory turnover.

For instance, rather than always displaying the New In badge, you might want to prioritize showing a Low Stock badge instead.

The New In badge is located in the components/product/card/badges/ProductCardBadgesHeader.vue file.

First of all, we need to build our logic which decides whether or not the product itself is considered as low stock based on the variants and the quantity.

In our example, we will just consider all variants including sold-out ones and consider the product as having low stock if the average quantity is lower than 3.
For this, we need to set up a new computed variable that calculates the information based on the product that is in the properties.

The code would look like this:

const isLowStock = computed(() => {
  if (!props.product.variants) {
    return false
  }
  
  // Here we calculate the average stock for a product.
  const totalVariants = props.product.variants.length
  // We sum up all the stock of all variants
  const totalStock = props.product.variants.reduce(
    (stockSum, variant) => stockSum + variant.stock.quantity,
    0,
  )
  
  // Divide the total stock through the amount of variants
  const averageStock = Math.round(totalStock / totalVariants)
  
  // In case the average stock is below 3, we show a Low Stock Badge
  return averageStock <= 3
})

We can now adjust labels variable which handles the logic of which badges are in the end shown to the user.

It returns a list of up to two badges and we can adjust the first element to now also consider our new isLowStock variable.

const labels = computed(() => {
  return [
    // Here we now prepare the Label, in case we consider the item as low stock
    // we use the `badge_labels.low_stock` translation, otherwise we still check if the product is new,
    // and if that's the case, we use the `badge_labels.new` translation.
    //
    // We need to use `isLowStock.value` here since `isLow`
    isLowStock.value
      ? $i18n.t('badge_labels.low_stock')
      : props.product.isNew
      ? $i18n.t('badge_labels.new')
      : null,

    customAttributes.value?.label,
  ].filter((item): item is string => !!item)
})

We use isLowStock.value since our variable is a reactive reference and we can access the actual result of our computation through the .value property.

This way our labels will automatically update in case our isLowStock variable changes. To learn more about Vue's Reactivity System, head over to the Vue Docs.

The final outcome of the customization looks like this:

Low Stock Badge on the Product Card