View counter in strapi: With lifecycle hooks? But how to use them?

Hello,

I want to save views of articles/posts in strapi as a simple integer value.

Do you know an approach how to have something like a view counter in strapi? So every time an articles is viewed in the client an counter should increment. Do you know a solution for this?

I want to get the most viewed articles - so I think I need this value.

My thought was to use lifecycle hooks for this. But which one is the right (maybe afterFindOne, but I’m not sure)? And how can I update the the data in the afterFindOne-hook? I seen an example for the beforeCreated-hook, but in this hook I have the data of the model/entity.

And another question: When an afterFindOne-hook should be a solution, will this hook be called when I query data with graphql or just for the rest api call?

Thanks for some help with this!

Best regards,

Timo

1 Like

I have a similar functionality in place. I manage it as per below.

In my article model, I have a integer field with 0 as default named viewCount.

And I have custom controller in place as below.
api/article/config/routes.json

{
     "method": "PATCH",
      "path": "/articles/:slug/view",
      "handler": "article.logView"
}

api/article/controllers/article.js

async logView(ctx) {
    const { slug } = ctx.params;
    try {
      const article = await strapi.services.article.findOne({ slug });
      await strapi.services.article.update(
        { id: article.id },
        { views: parseInt(article.views) + 1 }
      );
      return ctx.send({
        success: true,
      });
    } catch (error) {
      console.error(error);
      return ctx.send({
        success: false,
      });
    }
}

And then in mounted method (Nuxt) make a request to this endpoint.

PS: You can also have logview count based on id or other unique param instead slug as well.

3 Likes

Hello,

thanks a lot for your detailled reply!

Do you know if this can also reached with lifecycle hooks to keep this logic complete in strapi?

Best regards,

Timo

I think the afterFindOne won’t save the entry in db. But you can manually run the query to update the count something like below.

afterFindOne(result, params, populate){
      const article = await strapi.services.article.findOne({ slug: result.slug });
      await strapi.services.article.update({ id: article.id },{ views: parseInt(article.views) + 1 });
}

Great - again thanks a lot for your reply!

And maybe you can help me with my last question: Will this afterFindOne also run if I query the data with graphQL?

I don’t think so but I’m not sure.

Do you know another/better approach just based on graphql queries?

Best regards,

Timo

I would take it one step further and go with the model lifecycles: https://strapi.io/documentation/developer-docs/latest/concepts/models.html#lifecycle-hooks

And use the query system in say a beforeFind or beforeFindOne to get the existing count +1. There is a risk of a race condition so maybe the controller method is better as you can do more complex logic (like checking for false positives via IP checking ect)

I tried to implement an afterFindOne lifecycly-hook but I always get the success false.

I tried the following code:

module.exports = {
  lifecycles: {
    async afterFindOne(result, params, populate) {
      const article = strapi.query('article').findOne({ id: result.id });

      console.log('das ist ein test');
      console.log(article);

      await strapi.query('article').update(
        { id: article.id },
        { views: parseInt(article.views) + 1 }
      );
    }
  }
};

Do you have any idea how to solve this problem?

When I try it with fix values in the update part (for example with id 1 and views 15) it works, so it seems that the query.findOne is not working correctly. Or does this hook produces an infinite loop?

Unfortunately I don’t know how to see console.log output in postman for test/debug.

Thanks for your help!

Best regards,

Timo

Seems that the following hook is working now:

module.exports = {
  lifecycles: {
    async afterFindOne(result, params, populate) {
      const article = await strapi.query('article').find({ id: result.id });

      await strapi.query('article').update(
        { id: article[0].id },
        { views: parseInt(article[0].views) + 1 }
      );
    }
  }
};
1 Like

Seems that there was a problem with the afterfindOne-hook for unpublished articles. I got an 500 internal server error in this case.

With this hook it works:

module.exports = {
  lifecycles: {
    async afterFindOne(result, params, populate) {
      if(result) {
        const article = await strapi.query('article').find({id: result.id});

        if (article) {
          await strapi.query('article').update(
            {id: article[0].id},
            {views: parseInt(article[0].views) + 1}
          );
        }
      }
    }
  }
};

Problem could be that you were not waiting for the response here and that cause following weird problems.

You don’t need to make find and after article[0]. Make it with findOne as you did but do it with await…

I would simply do:

if (result) {
   strapi.query('article').update(
      { id: params.id },
      { views: parseInt(result.views) + 1} 
    );
}
  • Do not await for the update since it’s not required for the process (collateral action)
  • Simplified. Don’t need to find the article first, update it, if the article does not exist, it will update nothing.
  • If you need to control the possible error, then await the response and write later your code

Thanks for your answer and your improvement!

I just changed the {id: params.id } to {id: result.id } because I don’t know which data is in params but the result has an id.

Generally it’s really hard for me to debug the lifecycle-code. I insert some console.log but I don’t see them in browser or postman. Do you have any tip how to debug these hooks to see the console.log?

Best regards,

Timo

You will only see them server side @iparker so you will have to be watching the server console

Hello @DMehaffy :wave:

I was looking to implement the same functionality and I end up doing this way:

async afterFindOne(result, params, populate) {
      if (result) {
        strapi
          .query("past-shows")
          .update({ id: result.id }, { views: parseInt(result.views) + 1 });
      }
    },

However, the value only increases if I access the article in the Strapi Admin, if I access the API URL it not increase. What am I doing wrong? :thinking:

Thank you!

Are you using any cache middleware?

If you only want the logic to happen on the API side, then a route policy might be better (but a cache middleware will still conflict with this)

I’m still confused about how to add a view counter for each post on strapi. May I ask the source code for those of you who have the same problem to solve this problem. i stuck 2 days to solve this problem.

I’m using strapi version 3.6.8.

thanks.

Hi! Were you able to see the logs in the server side? I don’t see anything in my console (server-side). I’ve also tried attaching a debugger but nothing is logged in debugger.

Thanks,
I did it with controllers:

 async findOne(ctx) {
    const response = await super.findOne(ctx);

    await strapi.query("api::article.article")
      .update({
        where: { id: response.data.id },
        data: { views: parseInt(response.data.attributes.views) + 1 }
      });

    return response;
  },
1 Like

Hi e0eee,

How did you manage the permissions? I find it difficult to enable the update of a field like this without enabling updating the entire article. How do you manage permissions for certain fields of an entity?

(I am still on version 3.6.5 btw)

Kind regards,
Joran

Because you’re already in the endpoint controller you can update the entity with no permission check. The normal /update endpoint still performs the check.

Yes, thanks. I found that out while programming as well.

1 Like