Snippets

DACHCOM YoastSEO.js + ACF Fields

Created by DACHCOM last modified
Repo has been moved: https://github.com/dachcom-digital/acf-yoast-seo-validation

Comments (11)

  1. Tim Havinga

    I have tried your script in my website, which uses ACF PRO and Yoast SEO. In my console, I could see that the fields were picked up, but not all of them:

    1. When an ACF Repeater is formatted as a table, the jQuery selector div[data-name] (line 39) matches the parent repeater, and not the current element. I had to use div[data-name],td[data-name] instead. Alternatively, you could use [data-name] to match any element with this property.
    2. To get the ACF field key, the code currently looks for the first label (line 42). Again, in an ACF repeater row, which is formatted as a table, there is no label. Why not use the data-key-property which is already present at the matched element on line 39? I have changed line 42 to: $el.data('key'), (note: this line ends with a comma, because more variables are defined on the next lines.) The only 'drawback' of this approach is that the data-key is slightly different from the id that the current code uses. The data-key is of the format field_<code>, while the id is of the format acf-field_<code>.
    3. When I add another row in a repeater field, the function removeContent() is called and all of my custom field contents are removed from Yoast. I have not yet found a solution for this issue, but wonder why you are traversing the whole content array when an element is removed, instead of removing the current content provided in the $el variable.
    4. This script does not really support the repeater field. As each custom field inside a repeater field has the same ID, the previous value is overridden when the second element of a repeater is scanned, et cetera. To circumvent this, I have added a few lines of code after the declaration of value (inserted at line 45):
    var $parent = $el.parent('.acf-row');
    if ($parent.length === 1) {
        $fieldKey += '[' + $parent.data('id') + ']';
    }
    

    Note: this code only provides a way of adding repeater data, the removal of repeater elements is not yet provided.

    I will continue to use your script, and will post a working example here when it is ready. Please contact me if you need more information or have answers to the questions raised above.

    1. Tim Havinga

      An update to my previous comment: I have created a working script. Additional features:

      • Repeaters are stored as an object with sub-fields in the content object
      • For text fields, the custom class wrap-in-* can be added, which renders the text contents as an HTML tag for Yoast. This is particularly useful for fields that will be formatted as a heading in your template. For example, when you use wrap-in-h2 for a text field with value "Heading", the output privided to Yoast will be <h2>Heading</h2>. Caution: this is a very powerful feature, use it wisely. Alternatively, the regexp could be altered to /wrap-in-(h\d)/.
      • The custom fields provided to Yoast are separated by a newline character. This prevents words becoming one in subsequent text inputs.
      • Because the change-event is fired on the repeater when a repeater element is added or removed, the remove-event could be removed.
      • Add support for the textarea-element, as suggested by pavelgro on the ACF forum.
      /* global YoastSEO, acf */
      
      /**
       *
       * Add live SEO Validation to Yoast SEO while having custom ACF Fields.
       * Works also with Flexible Content and Repeater Fields.
       * https://bitbucket.org/snippets/dachcom/KKLM7/yoastseojs-acf-fields
       *
       * Include this Javascript ONLY in BackEnd.
       *
       * @author: DACHCOM.DIGITAL | Stefan Hagspiel <shagspiel@dachcom.ch>
       * @author: vevida.com | Tim Havinga <timhavinga@vevida.com>
       * @version: 1.1.0
       *
       * All rights reserved.
       *
       */
      (function ($) {
      
          var acfPlugin;
      
          var AcfPlugin = function () {
              this.content = {};
              this.pluginName = 'acfPlugin';
          };
      
          AcfPlugin.prototype.initContent = function ($el) {
              this.setContent($el, true, false);
          };
      
          /**
           * Set's the field content to use in the analysis
           *
           * @param {Object} $el The current element that was added or modified, as a
           * jQuery object
           * @param {boolean} updateYoast If Yoast should be updated, default true
           * @param {boolean} updateRepeater If repeaters should be updated, default true
           */
          AcfPlugin.prototype.setContent = function ($el, updateYoast, updateRepeater) {
              updateYoast = typeof updateYoast !== 'undefined' ? updateYoast : true;
              updateRepeater = typeof updateRepeater !== 'undefined' ? updateRepeater : true;
      
              $el = $el.closest('[data-name][data-type][data-key]');
      
              var key = $el.data('key'),
                      type = $el.data('type'),
                      value = null,
                      childID = null,
                      parentKey = null;
      
              var $parent = $el.parent('.acf-row');
              if ($parent.length === 1) {
                  childID = $parent.data('id');
                  parentKey = $parent.closest('[data-name][data-type][data-key]').data('key');
              }
      
              switch (type) {
                  case 'text' :
                      value = $el.find('input').val();
                      if ($el.is("[class*='wrap-in-']")) {
                          var wrapMatch = $el.attr('class').match(/wrap-in-(\w+)/);
                          value = '<' + wrapMatch[1] + '>' + value + "</" + wrapMatch[1] + ">";
                      }
                      break;
                  case 'image' :
                      value = $el.find("img[src!='']").prop('outerHTML');
                      break;
                  case 'gallery' :
                      value = '';
                      $el.find("div.thumbnail > img").each(function () {
                          value += $(this).prop('outerHTML');
                      });
                      break;
                  case 'textarea' :
                  case 'wysiwyg' :
                      value = $el.find('textarea').val();
                      break;
                  case 'repeater' :
                      if (updateRepeater) {
                          this.updateRepeater($el);
                      }
                  default :
                      value = null;
              }
      
              if (value !== null) {
                  if (childID === null) {
                      this.content[key] = value;
                  } else {
                      if (this.content[parentKey] === undefined) {
                          this.content[parentKey] = {};
                      }
                      if (this.content[parentKey][childID] === undefined) {
                          this.content[parentKey][childID] = {};
                      }
                      this.content[parentKey][childID][key] = value;
                  }
                  if (updateYoast) {
                      YoastSEO.app.pluginReloaded(this.pluginName);
                  }
              }
              return true;
          };
      
          /**
           * Update the fields of a repeater. This function removes and re-adds the
           * fields in a repeater.
           * @param {Object} $el The repeater element as jQuery object
           */
          AcfPlugin.prototype.updateRepeater = function($el) {
              var _this = this;
              // delete repeater field
              delete _this.content[$el.data('key')];
              // re-add the repeater subfields
              $el.find('.acf-row:not(.acf-clone) .acf-field').each(function(índex, element) {
                  _this.setContent($(element), false, false);
              });
              YoastSEO.app.pluginReloaded(this.pluginName);
          };
      
          /**
           * Registers plugin to YoastSEO
           */
          AcfPlugin.prototype.registerPlugin = function () {
              YoastSEO.app.registerPlugin(this.pluginName, {status: 'ready'});
          };
      
          /**
           * Registers modifications to YoastSEO
           */
          AcfPlugin.prototype.registerModifications = function () {
              YoastSEO.app.registerModification('content', this.addAcfDataToContent.bind(this), this.pluginName, 10);
          };
      
          /**
           * Adds ACF Data to content
           *
           * @param {String} yoastContent The page content, to be passed to Yoast
           * @returns {String} The page content with added extra field contents
           */
          AcfPlugin.prototype.addAcfDataToContent = function (yoastContent) {
              if (this.content.length === 0) {
                  return yoastContent;
              }
              yoastContent += '\n';
              $.each(this.content, function (k, v) {
                  if (typeof v === 'object') { // repeater
                      $.each(v, function(repeaterKey, repeaterValue) {
                          $.each(repeaterValue, function(subkey, subvalue) {
                              yoastContent += subvalue + '\n';
                          });
                      });
                  } else {
                      yoastContent += v + '\n';
                  }
              });
              return yoastContent;
          };
      
          $(window).on('YoastSEO:ready', function () {
              acfPlugin = new AcfPlugin();
              acfPlugin.registerPlugin();
              acfPlugin.registerModifications();
      
              acf.add_action('load_field', acfPlugin.initContent.bind(acfPlugin));
              acf.add_action('change', acfPlugin.setContent.bind(acfPlugin));
          });
      
      }(jQuery));
      

      ToDo:

      • Does not support nested repeaters.
      • Image fields are supported by fetching the thumbnail HTML. However, the image alt attribute is not provided in the thumbnail, so Yoast keeps complaining about missing alt tags.

      Edit: Work with Objects instead of arrays, because keys are not numeric for new repeater items.

  2. Arne Wiese

    Thanks for your work! I will test it tomorrow. Would you like to create a git repo for this? :) So I can easily update and support you will pull requests!

  3. Martin Jäcke

    @TimVevida Thanks also from me for your work. I would also prefer if you could create a git repo. I will make tests tomorrow!

  4. Tim Havinga

    As the script is originally created by @dachcom, I will wait for their response first. The script says 'all rights reserved', so I don't feel free creating a Git repository.

  5. DACHCOM

    hey guys. thanks for your work! i'll pack this snippet into a small plugin and then move the whole pile over to github. stay tuned.

  6. Tim Havinga

    @mjnet: I am interested in how you would test this code. See the corresponding issue on GitHub.

HTTPS SSH

You can clone a snippet to your computer for local editing. Learn more.