Score:0

How to add some content every Nth comment?

sa flag

My use is case is that I wish to show an advert every 3rd comment.

The forum module added the comment field to the forum content type. So the field name is comment_forum.

I have a block that contains the embed code for the ads. Using twig_tweak module to render that block in my template using {{ drupal_entity('block_content', '5') }}

I then created a template file called field--comment-forum.html.twig.

So far I have this:

{%
  set classes = [
    'field',
    'field--name-' ~ field_name|clean_class,
    'field--type-' ~ field_type|clean_class,
    'field--label-' ~ label_display,
    'comment-wrapper',
  ]
%}
{%
  set title_classes = [
    'title',
    'comment-title-main',
    label_display == 'visually_hidden' ? 'visually-hidden',
  ]
%}

{% if comments and not label_hidden %}
    
  <div class="my-5 col-print-12" id="comment-container"> 
    <section{{ attributes.addclass(classes) }}>
      {{ title_prefix }}
      <h2{{ title_attributes.addClass(title_classes) }} {{ attributes.setAttribute('id', 'comments') }}>{{ label }}</h2>
      {{ title_suffix }}

          {% set pre_render = comments['#pre_render'] %}
            {% for key, item in comments if key|first != '#'%}
                {% if key != 'pager' %}
                    {% set item = { '#pre_render': pre_render, 0: item } %}
                    {% if loop.index0 is divisible by(3) %}
                        {{ item }}
                        <div class="ads">{{ drupal_entity('block_content', '5') }}</div>
                    {% else %}
                        {{ item }}
                    {% endif %}
                {% else %}
                  <div class="comment-pager">{{ item }}</div>
                {% endif %}
          {% endfor %}
    </section>
  </div>
{% endif %}

{% if comment_form %}
  <div class="container">
  <div class="row justify-content-md-center">
    <div class="col col-md-8 p-2 p-md-4 my-5 bg-light rounded comment-new-container d-print-none">
      <h2 class="title comment-form__title comment-new-title">{{ 'Add new comment'|t }}</h2>
      {{ comment_form }}
    </div>
  </div>
</div>
{% endif %}

This works, as in the block with the ads are shown every 3rd comment. However it messes up the markup.

Without the template the markup for a comment with replies is this:

Without template

This is the markup for the same comments with the above template:

with template

I also notice the comments_ajax_pager_wrap container div is also not included either. That's added by the comments_ajax_pager module.

This is my first Drupal 9 website. So I'm new to twig, any ideas? I feel like I may even be in the wrong template?

unusedspoon avatar
aq flag
Probably due to you using the pre_render array item in `{% set item = { '#pre_render': pre_render, 0: item } %}` Why are you using that?
gMaximus avatar
sa flag
@unusedspoon I got that from another post on here. If I'm honest, I don't understand it because I'm new to twig.
gMaximus avatar
sa flag
@unusedspoon This is my reference: https://drupal.stackexchange.com/questions/263158/cycle-comments-on-field-comment-html-twig
Score:1
cn flag

Seems like you've already got it working from our comments in your other answer (which you can probably just mark as accepted if you're using the approach), but just for your own reference, here's how you might be able to fix your field template approach.

Per your comment mentioning that your initial implementation in the field template was breaking comment_reply_count module, I think it's because of your structure. If you look at https://git.drupalcode.org/project/comment_reply_count/-/blob/8.x-1.x/js/comment_reply_count.js, it relies on $(this).parents('article.comment').next().slideToggle();. This line tries to toggle the element directly after article.comment, which is normally <div class="indented">. Since you're rendering your ad div after the comment, I'd wager that it's trying to toggle that instead.

To maintain the functionality, I think you'd need to:

  1. Determine whether a given comment is a parent comment or a child/indented comment. Comments keep track of this on the pid value, which stands for Parent ID. If the PID is empty, it's a parent comment. Comments have a hasParent() method that checks this.
  2. If it is a parent, then render the ad before the comment if it is divisible by 3 (or 4, or whatever).
            {% set parent_comment_counter = 0 %}
            {% for key, item in comments if key|first != '#'%}
                {% if key != 'pager' %}

                    {% if not item['#comment'].hasParent() %}
                      {% set parent_comment_counter = top_level_comment_counter + 1 %}
                      {% if top_level_comment_counter is divisible by(3) %}
                        <div class="ads">{{ drupal_entity('block_content', '5') }}</div>
                      {% endif %}
                    {% endif %}

                    {% set item = { '#pre_render': pre_render, 0: item } %}
                    {{ item }}
                {% else %}
                  <div class="comment-pager">{{ item }}</div>
                {% endif %}
          {% endfor %}

If you wanted to do this in preprocess instead, you could do something like

hook_preprocess_field__comment_forum(&$variables) {
  $parent_comment_counter = 0;

  foreach($variables['comments'] as $k => $comment_item) {
    if(!is_array($comment_item) || !isset($comment['#comment'])) {
      continue;
    } 

    $variables[$k]['show_ad'] = false;

    /** @var Drupal\comment\Entity\CommentInterface $comment_entity */
    $comment_entity = $comment['#comment'];
    if($comment_entity->hasParent()) {
      continue;
    }

    $parent_comment_counter += 1;
    $variables[$k]['show_ad'] = $parent_comment_counter % 3 === 0;
  }
}

in which case your template could then look like

            {% set parent_comment_counter = 0 %}
            {% for key, item in comments if key|first != '#'%}
                {% if key != 'pager' %}

                    {% if item.show_ad %}
                        <div class="ads">{{ drupal_entity('block_content', '5') }}</div>
                    {% endif %}

                    {% set item = { '#pre_render': pre_render, 0: item } %}
                    {{ item }}
                {% else %}
                  <div class="comment-pager">{{ item }}</div>
                {% endif %}
          {% endfor %}

That's the gist of it. You'd need to modify it if you wanted to include the ad around indented comments to ensure that you aren't including the ad block above the first indented comment after a parent, as that would break it in the same way as before.

I'm not sure exactly why <div class="comments_ajax_pager_wrap"> was disappearing, though. That wrapper is added by comment_ajax_pager_entity_display_build_alter(), so maybe you could debug from there.

gMaximus avatar
sa flag
Thanks for all your help. I certainly learned some new code. All the best
Score:0
cn flag

This is EASY-PEASY!

I already build this in some of my previous projects.

In your template file field--comment-forum.html.twig, you can iterate over the comments using a loop and a counter variable that keeps track of the comment number. You can use the loop.index variable provided by Twig to get the current iteration index.

{% set counter = 0 %}
{% for comment in comments %}
  {% set counter = counter + 1 %}
  {# Display the comment content #}
  {{ comment.content }}
  {# Display the ad after every 3rd comment #}
  {% if counter % 3 == 0 %}
    {{ drupal_entity('block_content', '5') }}
  {% endif %}
{% endfor %}

I hope this help!

gMaximus avatar
sa flag
Thanks for your time offering a solution. However the comments don't get rendered. Only the ads. My comments are threaded. So I'm not so sure this approach with a field template will work. If the comments did appear, I'm sure it would result in the markup in my original question. That's been my issue.
Score:0
sa flag

A week later and I changed tact completely. I questioned if it would be possible to work with comment.html.twig instead of the field template.

So in my MY_THEME.theme file I pass the comments weight to the template, like this:

Updated thanks to @mrweiner's help in the comments

/**
 * Implements hook_preprocess_comment().
 */
function MY_THEME_preprocess_comment(&$variables) {
    /** @var \Drupal\comment\CommentInterface $comment */
    $comment = $variables['elements']['#comment'];
    $node = $comment->getCommentedEntity();
    $cid = $comment->id();
    $entity_manager = \Drupal::entityTypeManager();
    try {
        
        // Load comments for the node
        /** @var \Drupal\comment\CommentStorageInterface $storage */
        $storage = $entity_manager->getStorage('comment');
        if ($node->hasField('comment_forum')) {
            /** @var \Drupal\comment\CommentFieldItemList $comment_field */
            $comment_field = $node->get('comment_forum');
        } elseif ($node->hasField('field_comments')) {
            /** @var \Drupal\comment\CommentFieldItemList $comment_field */
            $comment_field = $node->get('field_comments');
        }
        $comments = $storage->loadThread($node, $comment_field->getFieldDefinition()->getName(), \Drupal\comment\CommentManagerInterface::COMMENT_MODE_FLAT);

        // Ensure there are comments
        if (empty($comments)){
          return;
        }

       $comment_index = array_key_first(array_filter(array_values($comments), fn ($this_comment) => $this_comment->id() === $comment->id())); 
       $weight = $comment_index + 1;
       $variables['displayAds'] = $weight % 3 === 0;

      } catch (\Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException $e) {
      } catch (\Drupal\Component\Plugin\Exception\PluginNotFoundException $e) {
      }
}

After implementing that hook I simply added this to comment.html.twig where I wanted my block to appear:

{% if displayAds %}
       <div class="comment-ads">{{ drupal_entity('block_content', '5') }}</div>
{% endif %}
cn flag
A slightly cleaner way to do this would be to set a `$variables['display_ads']` or similar in your preprocess hook, instead of doing everything with $weights like you're doing now. So like `$variables['display_ads'] = $comment_weight % 3 === 0`. It's also kind of heavyhanded to do this loop for every comment. Could do something like `$comment_index = array_key_first(array_filter($comments, fn ($this_comment) => $this_comment->id() === $comment->id())); $weight = $comment_index + 1;` which avoids all of the looping. Adding the +1 since the comments array starts with key of 0.
gMaximus avatar
sa flag
Thank you @mrweiner. I'm going your route. Could have done with you a week ago lol. I have noticed that the ads appear in random places. Sometimes after 2, 3 or 4 comments? This is also the case when there are just a series of comments, not threaded.
gMaximus avatar
sa flag
@Mrweiner Is this relying on the comment ID being divisible by 3? I've been hoping to use the comments weight on the entity that they're attached to. Making them show every 3rd comment.
cn flag
I don’t think it should depend on the comment ID being divisible by 3. Does loadThread() return an array where the keys are incremental (0,1,2,…) or are the keys the comment ids? Sounds like it might be the latter. If the latter, you can use array_values($comments) to reset all of those keys to be incremental instead. I forgot that entity queries do use entity IDs as the keys.
cn flag
Also FWIW, I’d generally try to address this in the comment field template instead of the comment template itself, since the ads are really related to the container and not the comment itself, but given that you were trying that already and it didn’t work, this is the next best bet. Rendering comments in particular can be a PITA. Sometimes you just gotta get it workin!
gMaximus avatar
sa flag
Thank you, you're a legend. That's perfect. Your hunch was correct regarding the array being keyed on comment ID. It's all good now. Feel free to add your answer. Regarding the field template, that would work for non-threaded comments. When threaded, it results in the markup in my original question. This broke the functionality of the comment_reply_count module. A feature I've been determined to have.
cn flag
You can just accept this answer if you're going with this approach. Maybe just edit it to include whatever modified code you used from my comment. I added another answer with extra context for the field template just for future reference, but no need to dig into it if you're happy with how this solution works! :)
I sit in a Tesla and translated this thread with Ai:

mangohost

Post an answer

Most people don’t grasp that asking a lot of questions unlocks learning and improves interpersonal bonding. In Alison’s studies, for example, though people could accurately recall how many questions had been asked in their conversations, they didn’t intuit the link between questions and liking. Across four studies, in which participants were engaged in conversations themselves or read transcripts of others’ conversations, people tended not to realize that question asking would influence—or had influenced—the level of amity between the conversationalists.