mklueh / gridsome-plugin-recommender

Improve userĀ“s average time on page of your Gridsome site by automatically finding related posts or products. Uses machine learning to determine recommendations of all kind, be it other posts, context-related ads, related products in your shop, affiliate products and more.
https://overflowed.dev/blog/building-a-gridsome-plugin-for-related-posts?utm_medium=github&utm_source=details
MIT License
13 stars 2 forks source link

Component Breaks When Navigating Between Recommender Links #14

Closed rylanharper closed 3 years ago

rylanharper commented 3 years ago

Hey there! First off let me say this plugin works great, I really appreciate all the work that has gone into this.

I am currently making an ecommerce shop using Gridsome, Shopify, and Sanity for a client and I am using this plugin for a recommended products component. However, I seem to be running into a bizarre issue.. For some reason whenever I click a product from the recommender component (the one using this plugin) it breaks an image carousel I am using, but when I load the page from any other path within the site or upon a page refresh the carousel works fine. I have a video attached to show what I mean (ignore the incomplete design this is a wip). Could this be because the plugin does not operate with the graphql layer? To me it appears the page does not mount correctly on the Vue side of things when navigating from a path using the recommender plugin. Unfortunately there are no console errors as well šŸ˜–

I have tested this with multiple carousel/gallery libraries (Vue Agile, Swiper, Vue Carousel, and this current one is Keen Slider) and the results are the same (breaks when navigating from a recommended link). If this is the case I may just opt out and not have a recommended product section, although its pretty damn cool lol.

https://user-images.githubusercontent.com/16216160/115977080-454f5000-a529-11eb-8905-30cb4b6b5d5f.mov

rylanharper commented 3 years ago

Small update after looking through my codebase.. So it turns out I actually have no idea how my product recommendations are even working. In fact I am not even sure how the plugin knows how to pull in related products, because I am querying the template, NOT the typeName of the shopify-source-plugin. When I input the typeName of the shopify source plugin (the default is set to Shopify) the terminal reads and error similar to this issue posted earlier. However, my code that I have currently does work correctly, but does not follow the plugin documentation (i.e I am querying the template title):

templates: {
// Shopify template
 ShopifyProduct: [
   {
     path: '/products/:handle',
     component: './src/templates/product.vue'
   }
 ],
 ShopifyCollection: [
   {
     path: '/collections/:handle',
     component: './src/templates/collection.vue'
   }
 ]
},

...

plugins: [
 {
   use: 'gridsome-source-shopify',
   options: {
     typeName: 'Shopify',
     storeName: process.env.GRIDSOME_SHOPIFY_STOREFRONT,
     storefrontToken: process.env.GRIDSOME_SHOPIFY_STOREFRONT_TOKEN
   }
 },
 {
   use: 'gridsome-plugin-recommender',
   options: {
     enabled: true,
     debug: true,
     typeName: 'ShopifyProduct',
     field: 'title',
     referenceField: 'title',
     relatedFieldName: 'related',
     referenceRelatedFieldName: 'related',
     minScore: 0.1,
     maxRelations: 4
   }
 }
]

and here is how the code is suppose to look based on this plugin documentation, but does not work correctly (draws a compiling error):

templates: {
// Shopify template
 ShopifyProduct: [
   {
     path: '/products/:handle',
     component: './src/templates/product.vue'
   }
 ],
 ShopifyCollection: [
   {
     path: '/collections/:handle',
     component: './src/templates/collection.vue'
   }
 ]
},

...

plugins: [
 {
   use: 'gridsome-source-shopify',
   options: {
     typeName: 'Shopify',
     storeName: process.env.GRIDSOME_SHOPIFY_STOREFRONT,
     storefrontToken: process.env.GRIDSOME_SHOPIFY_STOREFRONT_TOKEN
   }
 },
 {
   use: 'gridsome-plugin-recommender',
   options: {
     enabled: true,
     debug: true,
     typeName: 'Shopify',
     field: 'title',
     referenceField: 'title',
     relatedFieldName: 'related',
     referenceRelatedFieldName: 'related',
     minScore: 0.1,
     maxRelations: 4
   }
 }
]

This is likely the reason why my recommendation links are broken, but it is odd how I still retrieve the data, images, and product info via gql. Its like the plugin is working, but is breaking at the same time lol. I think I have raised more questions than answers, or maybe I am just misunderstanding how to properly use the plugin within my Gridsome config. Any info would be helpful

mklueh commented 3 years ago

Hi @rylanharper ,

first of all, always nice to see people using my plugin, glad it finds some users :) And of course thanks for the detailed issue explanation. I dinĀ“t know that it works together with the Shopify source, which is amazing!

IĀ“ve looked into the shopify source code and it seems the typeName you have to specify in their options is a bit misleading, as it rather seems to be a type prefix, see here:

  createTypeName (name) {
    let typeName = this.options.typeName
    // If typeName is blank, we need to add a prefix to these types anyway, as on their own they conflict with internal Gridsome types.
    const types = ['Page']
    if (!typeName && types.includes(name)) typeName = 'Shopify'

    return camelCase(`${typeName} ${name}`, { pascalCase: true })
  }

which is called for multiple types to create several collections

   this.TYPENAMES = {
      ARTICLE: this.createTypeName('Article'),
      BLOG: this.createTypeName('Blog'),
      COLLECTION: this.createTypeName('Collection'),
      PRODUCT: this.createTypeName('Product'),
      PRODUCT_VARIANT: this.createTypeName('ProductVariant'),
      PAGE: this.createTypeName('Page'),
      PRODUCT_TYPE: this.createTypeName('ProductType'),
      PRODUCT_TAG: this.createTypeName('ProductTag'),
      IMAGE: 'ShopifyImage',
      PRICE: 'ShopifyPrice'
    }

So this should explain why you are correctly using 'ShopifyProduct' and 'Shopify' is not working, as there is no plain 'Shopify' collection.

Now regarding the actual carousel rendering issue, IĀ“m not sure what exactly goes wrong here. Are these just product preview images that get mixed with other products or do you use the recommender plugin as well for the product images?

Not sure how this can be related to the recommender plugin as this plugin has no impact on any page rendering and is only running during the build phase to create the required collections.

If you print out the .related field on your product site, and it contains valid recommendations, this is literally all you get from this plugin :)

Guessing a bit, IĀ“d investigate further when the carusel is loading, if anything is cached and if the component is reused in any way

For example, from the vue-agile FAQ:

FAQ
1. Using component with dynamic content
If content changes, you have to use reload or in some cases, you can use key property: <agile :key="mySlides.length">...</agile> (it'll rebuild the carousel after each change of mySlides length).

Oh and by the way, the min config in your case of the recommender plugin should be something like this:

{
   use: 'gridsome-plugin-recommender',
   options: {
     enabled: true,
     typeName: 'ShopifyProduct',
     field: 'title',
     minScore: 0.1,
     maxRelations: 4
   }
 }

the field names youĀ“ve specified are the defaults, and as you are building up relations between items of the same collection and not two different collections you donĀ“t need the "reference" options. Those are only required if you like to build up recommendations between for example:

Products <-> Categories Products <-> Product Articles / Blog Posts

etc

Hope I could help

rylanharper commented 3 years ago

Hey @mklueh

Wow thank you for the detailed response! After spending time looking into this issue you are correct that it is not the recommender plugin. I also checked the shopify-source-plugin code and realized that I just got lucky naming the related typeName lol! However, I really appreciate you looking into that and confirming that as well.

So what I have been able to find is that it is indeed that the carousel/slider component that is breaking due to the Vue instance not being properly destroyed within the page's lifecycle. I believe this is due to the Gridsome template (templates/product.vue) not refreshing due a dynamic data change. When I am on the current product.vue page and click a link from the recommended product component it says within the same instance on product.vue that was currently on and does not refresh into a new instance. I am not why it does that exactly since it technically is a new route change.. You can see the code example of how most carousel/slider libraries work where the component needs to be destroyed in order to render correctly for a new instance:

<template>
  <section class="product-carousel">
    <div ref="slider" class="keen-slider">
      <div v-for="image in images" :key="image.id" class="keen-slider__slide">
        <figure style="padding-bottom: 150%" class="product-carousel__image">
          <picture>
            <img
              v-lazy="image.originalSrc"
              :src="image.originalSrc"
              :alt="image.altText"
            />
          </picture>
        </figure>
      </div>
    </div>
    <div v-if="slider" class="dots">
      <button
        v-for="(slide, idx) in dotHelper"
        @click="slider.moveToSlideRelative(idx)"
        :class="{ dot: true, active: current === idx }"
        :key="idx"
      />
    </div>
  </section>
</template>

<script>
import KeenSlider from 'keen-slider'
import 'keen-slider/keen-slider.min.css'

export default {
  name: 'ProductCarousel',

  props: {
    images: {
      type: Array,
      require: true,
      default: () => []
    }
  },

  data() {
    return {
      current: 0,
      slider: null
    }
  },

  computed: {
    dotHelper() {
      return this.slider ? [...Array(this.slider.details().size).keys()] : []
    }
  },

  mounted() {
    this.slider = new KeenSlider(this.$refs.slider, {
      initial: this.current,
      slideChanged: s => {
        this.current = s.details().relativeSlide
      }
    })
  },

  beforeDestroy() {
    if (this.slider) this.slider.destroy()
  }
}
</script>

So in short, this appears to a Gridsome/slider component problem lol! There are some work arounds to this I can think of, but I will spend more time on that later (I already have a few ideas for things I can try). I really apologize for wasting your time and looking into an issue that wasn't related to the plugin you made, but again I do really appreciate it. Also thank you for the config example you provided. I have moved that into my current gridsome.config file! Thank you for the awesome plugin!:)

Edit: The solution was simple.. I just needed to add :key="$route.fullPath" to my product.vue template in order to "refresh" the instance on an internal link that uses the same template

mklueh commented 3 years ago

Hey @rylanharper ,

no problem :)

IĀ“m probably going to add your example to the readme for others that are using it together with Shopify. ItĀ“s really missleading that they are calling the prefix option field typeName.

Edit: The solution was simple.. I just needed to add :key="$route.fullPath" to my product.vue template in order to "refresh" the instance on an internal link that uses the same template

I see, great to hear šŸ‘ havenĀ“t you had the :key property at all before? Never ran into any side-effects with it before, just warnings if it was entirely missing, but good to get to know some sample issue now

rylanharper commented 3 years ago

Hey @mklueh

yes Iā€™ve had the :key property a million times haha, but I wasnā€™t aware of the fullPath key option to refresh the instance on an existing template page! I am fairly new to Gridsome (but not Vue/Nuxt) so this option doesnā€™t come up to much otherwise.

okay sounds good! Thanks for again for the help