Elementor FAQ Widget

Written by
Published
Updated
Typical Read
10 minutes

FAQs are a commonly overlooked optimization opportunity. Search engines sometimes have a hard time recognizing them as such. In this article, I'll go over how to create a FAQ Elementor widget that's optimized for search engines.

In a previous article, I went over how to create a custom Elementor widget. Today, we’ll go through how to create a SEO optimized Elementor FAQ widget to boost your chances of being included as a featured snippet in Google.

Elementor FAQ Widget

In May 2019, Google launched new schema markups that allow your on-page content to feature on the SERP as a rich snippet. One of these new markup is the FAQ schema.

This addition allows search engines to easily crawl and understand when FAQs are on the page. Optimizing your FAQs can help your changes of appearing in the top spot on Google as a featured snippet callout resulting in a big boost in traffic.

What are featured snippets? Featured snippets appear as a special box at the top of search results with a text description above the link. If you search with the Google Assistant, featured snippets might also be read aloud. When included in search results, this can give you a big boost in traffic. Learn more

How to Create a Elementor FAQ Widget

FAQ structured data is a piece of code that you include on your site in addition to your FAQs can increase your changes of being included in Google SERP as a rich snippet, in addition to Google Assistant when users do a voice search.

Step 1. Setup the Elementor FAQ widget plugin.

This first step in create your custom Elementor FAQ widget is to setup the plugin directory structure where the widget will live. Here’s the basic structure (see my article on building custom Elementor widgets for more details) of our FAQ widget:

  • elementor-faqs/elementor-faqs.php
  • elementor-faqs/plugin.php
  • elementor-faqs/widgets/faq.php
  • elementor-faqs/assets/css/elementor-faqs.css

Step 2. Create the core plugin file.

The core plugin file (elementor-faqs/elementor-faqs.php) includes verifications that the WordPress install has all the required components in order for the plugin to work (e.g. Elementor, PHP version, WordPress version, etc..)

elementor-faqs/elementor-faqs.php

<?php
/**
 * Elementor FAQs WordPress Plugin
 *
 * @package    ElementorFAQs
 * @subpackage WordPress
 * @since      1.0.0
 * @author     Ben Marshall
 * @copyright  2020 Ben Marshall
 * @license    GPL-2.0-or-later
 *
 * @wordpress-plugin
 * Plugin Name:       Elementor FAQs
 * Plugin URI:        https://www.benmarshall.me/elementor-faq-widget
 * Description:       Elementor widget add-on that allows you to place rich-snippet optimized FAQs on your site.
 * Version:           1.0.0
 * Requires at least: 5.2
 * Requires PHP:      7.2
 * Author:            Ben Marshall
 * Author URI:        https://www.benmarshall.me
 * Text Domain:       elementorfaq
 * License:           GPL v2 or later
 * License URI:       http://www.gnu.org/licenses/gpl-2.0.txt
 */
/**
 * Security Note: Blocks direct access to the plugin PHP files.
 */
defined( 'ABSPATH' ) or die( 'No script kiddies please!' );
/**
 * Main Elementor Awesomesauce Class
 *
 * The init class that runs the Elementor Awesomesauce plugin.
 * Intended To make sure that the plugin's minimum requirements are met.
 *
 * You should only modify the constants to match your plugin's needs.
 *
 * Any custom code should go inside Plugin Class in the plugin.php file.
 * @since 1.0.0
 */
final class Elementor_FAQs {
  /**
   * Plugin Version
   *
   * @since 1.0.0
   * @var string The plugin version.
   */
  const VERSION = '1.0.0';
  /**
   * Minimum Elementor Version
   *
   * @since 1.0.0
   * @var string Minimum Elementor version required to run the plugin.
   */
  const MINIMUM_ELEMENTOR_VERSION = '2.0.0';
  /**
   * Minimum PHP Version
   *
   * @since 1.0.0
   * @var string Minimum PHP version required to run the plugin.
   */
  const MINIMUM_PHP_VERSION = '7.0';
  /**
   * Constructor
   *
   * @since 1.0.0
   * @access public
   */
  public function __construct() {
    // Load translation
    add_action( 'init', array( $this, 'i18n' ) );
    // Init Plugin
    add_action( 'plugins_loaded', array( $this, 'init' ) );
  }
  /**
   * Load Textdomain
   *
   * Load plugin localization files.
   * Fired by `init` action hook.
   *
   * @since 1.2.0
   * @access public
   */
  public function i18n() {
    load_plugin_textdomain( 'elementorfaqs' );
  }
  /**
   * Initialize the plugin
   *
   * Validates that Elementor is already loaded.
   * Checks for basic plugin requirements, if one check fail don't continue,
   * if all check have passed include the plugin class.
   *
   * Fired by `plugins_loaded` action hook.
   *
   * @since 1.2.0
   * @access public
   */
  public function init() {
    // Check if Elementor installed and activated
    if ( ! did_action( 'elementor/loaded' ) ) {
      add_action( 'admin_notices', array( $this, 'admin_notice_missing_main_plugin' ) );
      return;
    }
    // Check for required Elementor version
    if ( ! version_compare( ELEMENTOR_VERSION, self::MINIMUM_ELEMENTOR_VERSION, '>=' ) ) {
      add_action( 'admin_notices', array( $this, 'admin_notice_minimum_elementor_version' ) );
      return;
    }
    // Check for required PHP version
    if ( version_compare( PHP_VERSION, self::MINIMUM_PHP_VERSION, '<' ) ) {
      add_action( 'admin_notices', array( $this, 'admin_notice_minimum_php_version' ) );
      return;
    }
    // Once we get here, We have passed all validation checks so we can safely include our plugin
    require_once( 'plugin.php' );
  }
  /**
   * Admin notice
   *
   * Warning when the site doesn't have Elementor installed or activated.
   *
   * @since 1.0.0
   * @access public
   */
  public function admin_notice_missing_main_plugin() {
    if ( isset( $_GET['activate'] ) ) {
      unset( $_GET['activate'] );
    }
    $message = sprintf(
      /* translators: 1: Plugin name 2: Elementor */
      esc_html__( '"%1$s" requires "%2$s" to be installed and activated.', 'elementorfaqs' ),
      '<strong>' . esc_html__( 'Elementor FAQs', 'elementorfaqs' ) . '</strong>',
      '<strong>' . esc_html__( 'Elementor', 'elementorfaqs' ) . '</strong>'
    );
    printf( '<div class="notice notice-warning is-dismissible"><p>%1$s</p></div>', $message );
  }
  /**
   * Admin notice
   *
   * Warning when the site doesn't have a minimum required Elementor version.
   *
   * @since 1.0.0
   * @access public
   */
  public function admin_notice_minimum_elementor_version() {
    if ( isset( $_GET['activate'] ) ) {
      unset( $_GET['activate'] );
    }
    $message = sprintf(
      /* translators: 1: Plugin name 2: Elementor 3: Required Elementor version */
      esc_html__( '"%1$s" requires "%2$s" version %3$s or greater.', 'elementorfaqs' ),
      '<strong>' . esc_html__( 'Elementor FAQs', 'elementorfaqs' ) . '</strong>',
      '<strong>' . esc_html__( 'Elementor', 'elementorfaqs' ) . '</strong>',
      self::MINIMUM_ELEMENTOR_VERSION
    );
    printf( '<div class="notice notice-warning is-dismissible"><p>%1$s</p></div>', $message );
  }
  /**
   * Admin notice
   *
   * Warning when the site doesn't have a minimum required PHP version.
   *
   * @since 1.0.0
   * @access public
   */
  public function admin_notice_minimum_php_version() {
    if ( isset( $_GET['activate'] ) ) {
      unset( $_GET['activate'] );
    }
    $message = sprintf(
      /* translators: 1: Plugin name 2: PHP 3: Required PHP version */
      esc_html__( '"%1$s" requires "%2$s" version %3$s or greater.', 'elementorfaqs' ),
      '<strong>' . esc_html__( 'Elementor FAQs', 'elementorfaqs' ) . '</strong>',
      '<strong>' . esc_html__( 'PHP', 'elementorfaqs' ) . '</strong>',
      self::MINIMUM_PHP_VERSION
    );
    printf( '<div class="notice notice-warning is-dismissible"><p>%1$s</p></div>', $message );
  }
}
// Instantiate Elementor_FAQs.
new Elementor_FAQs();

Step 3. Register the widget, CSS & JS files.

Next, we’ll tell Elementor about the new widget so it get’s added to the page builder. This is also where we’ll register the CSS file for the FAQ widget.

elementor-faqs/plugin.php

<?php
/**
 * Handles registering Elementor widgets
 *
 * @package ElementorFAQs
 * @since 1.0.0
 */
namespace ElementorFAQs;
/**
 * Class Plugin
 *
 * Main Plugin class
 * @since 1.0.0
 */
class Plugin {
  /**
   * Instance
   *
   * @since 1.0.0
   * @access private
   * @static
   *
   * @var Plugin The single instance of the class.
   */
  private static $_instance = null;
  /**
   * Instance
   *
   * Ensures only one instance of the class is loaded or can be loaded.
   *
   * @since 1.2.0
   * @access public
   *
   * @return Plugin An instance of the class.
   */
  public static function instance() {
    if ( is_null( self::$_instance ) ) {
      self::$_instance = new self();
    }
    return self::$_instance;
  }
  /**
   * widget_scripts
   *
   * Load required plugin core files.
   *
   * @since 1.2.0
   * @access public
   */
  public function widget_scripts() {
    wp_register_style( 'elementor-faqs', plugins_url( '/assets/css/elementor-faqs.css', __FILE__ ), [], '1.0.0' );
  }
  /**
   * Include Widgets files
   *
   * Load widgets files
   *
   * @since 1.2.0
   * @access private
   */
  private function include_widgets_files() {
    require_once( __DIR__ . '/widgets/faqs.php' );
  }
  /**
   * Register Widgets
   *
   * Register new Elementor widgets.
   *
   * @since 1.2.0
   * @access public
   */
  public function register_widgets() {
    // Its is now safe to include Widgets files
    $this->include_widgets_files();
    // Register Widgets
    \Elementor\Plugin::instance()->widgets_manager->register_widget_type( new Widgets\FAQs() );
  }
  /**
   *  Plugin class constructor
   *
   * Register plugin action hooks and filters
   *
   * @since 1.2.0
   * @access public
   */
  public function __construct() {
    // Register widget scripts
    add_action( 'elementor/frontend/after_register_scripts', [ $this, 'widget_scripts' ] );
    // Register widgets
    add_action( 'elementor/widgets/widgets_registered', [ $this, 'register_widgets' ] );
  }
}
// Instantiate Plugin Class
Plugin::instance();

Step 4. Create the FAQ widget.

Now for the actual widget. This is where we’ll define the fields needed (i.e. a repeater, question & answer). It’s also where we’ll enqueue the CSS file when this widget appears on a page.

elementor-faqs/widgets/faq.php

<?php
namespace ElementorFAQs\Widgets;
use Elementor\Widget_Base;
use Elementor\Controls_Manager;
if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly
/**
 * @since 1.1.0
 */
class FAQs extends Widget_Base {
  /**
   * Retrieve the widget name.
   *
   * @since 1.1.0
   *
   * @access public
   *
   * @return string Widget name.
   */
  public function get_name() {
    return 'elementor-faqs';
  }
  /**
   * Retrieve the widget title.
   *
   * @since 1.1.0
   *
   * @access public
   *
   * @return string Widget title.
   */
  public function get_title() {
    return __( 'FAQs', 'elementorfaqs' );
  }
  /**
   * Retrieve the widget icon.
   *
   * @since 1.1.0
   *
   * @access public
   *
   * @return string Widget icon.
   */
  public function get_icon() {
    return 'fas fa-question';
  }
  /**
   * Retrieve the list of categories the widget belongs to.
   *
   * Used to determine where to display the widget in the editor.
   *
   * Note that currently Elementor supports only one category.
   * When multiple categories passed, Elementor uses the first one.
   *
   * @since 1.1.0
   *
   * @access public
   *
   * @return array Widget categories.
   */
  public function get_categories() {
    return [ 'general' ];
  }
  public function get_style_depends() {
    $styles = [ 'elementor-faqs' ];
    return $styles;
  }
  public function get_script_depends() {
    $scripts = [];
    return $scripts;
  }
  /**
   * Register the widget controls.
   *
   * Adds different input fields to allow the user to change and customize the widget settings.
   *
   * @since 1.1.0
   *
   * @access protected
   */
  protected function _register_controls() {
    $this->start_controls_section(
      'section_content',
      [
        'label' => __( 'Content', 'elementorfaqs' ),
      ]
    );
    $repeater = new \Elementor\Repeater();
    $repeater->add_control(
      'faq_question', [
        'label' => __( 'Question', 'elementorfaqs' ),
        'type' => \Elementor\Controls_Manager::TEXT,
        'default' => __( 'How much can a woodchuck chuck wood?' , 'elementorfaqs' ),
        'label_block' => true,
      ]
    );
    $repeater->add_control(
			'faq_answer', [
				'label' => __( 'Answer', 'elementorfaqs' ),
				'type' => \Elementor\Controls_Manager::WYSIWYG,
				'default' => __( 'New York state wildlife expert Richard Thomas found that a woodchuck could (and does) chuck around 35 cubic feet of dirt in the course of digging a burrow. Thomas reasoned that if a woodchuck could chuck wood, he would chuck an amount equivalent to the weight of the dirt, or 700 pounds.' , 'elementorfaqs' ),
				'show_label' => true,
			]
		);
    $this->add_control(
			'faqs',
			[
				'label' => __( 'FAQs', 'elementorfaqs' ),
				'type' => \Elementor\Controls_Manager::REPEATER,
				'fields' => $repeater->get_controls(),
				'default' => [
					[
						'faq_question' => __( 'How much can a woodchuck chuck wood?', 'elementorfaqs' ),
						'faq_answer' => __( 'New York state wildlife expert Richard Thomas found that a woodchuck could (and does) chuck around 35 cubic feet of dirt in the course of digging a burrow. Thomas reasoned that if a woodchuck could chuck wood, he would chuck an amount equivalent to the weight of the dirt, or 700 pounds.', 'elementorfaqs' ),
					],
				],
				'title_field' => '{{{ faq_question }}}',
			]
		);
    $this->end_controls_section();
  }
  /**
   * Render the widget output on the frontend.
   *
   * Written in PHP and used to generate the final HTML.
   *
   * @since 1.1.0
   *
   * @access protected
   */
  protected function render() {
    $settings = $this->get_settings_for_display();
    if ( $settings['faqs'] ) {
      echo '<div class="elementor-faqs-container">';
      foreach (  $settings['faqs'] as $item ) {
        echo '<div class="elementor-faqs-item elementor-faqs-question--' . $item['_id'] . '">';
        echo '<div class="elementor-faqs-question">' . $item['faq_question'] . '</div>';
        echo '<div class="elementor-faqs-answer">' . $item['faq_answer'] . '</div>';
        echo '</div>';
      }
      echo '</div>';
      // Add the rich-snippet data below
      ?>
      <script type="application/ld+json">
      {
        "@context": "https://schema.org",
        "@type": "FAQPage",
        "mainEntity": [
          <?php $cnt = 0; foreach ( $settings['faqs'] as $item ) { ?>
          {
            "@type": "Question",
            "name": "<?php echo esc_attr( str_replace("\n", "", trim( $item['faq_question'] )) ); ?>",
            "acceptedAnswer": {
              "@type": "Answer",
              "text": <?php echo json_encode( $item['faq_answer'] ); ?>
            }
          }<?php $cnt++; if( $cnt < count( $settings['faqs'] ) ): ?>,<?php endif; ?>
          <?php }; ?>
        ]
      }
      </script>
      <?php
    }
  }
  /**
   * Render the widget output in the editor.
   *
   * Written as a Backbone JavaScript template and used to generate the live preview.
   *
   * @since 1.1.0
   *
   * @access protected
   */
  protected function _content_template() {
    ?>
    <# if ( settings.faqs.length ) { #>
      <div class="elementor-faqs-container">
        <# _.each( settings.faqs, function( item ) { #>
          <div class="elementor-faqs-item elementor-faqs-question--{{ item._id }}">
            <div class="elementor-faqs-question">{{{ item.faq_question }}}</div>
            <div class="elementor-faqs-answer">{{{ item.faq_answer }}}</div>
          </div>
        <# }); #>
      </div>
    <# } #>
    <?php
  }
}

Step 5. Create the CSS file.

Lastly, we’ll create some basic styles for the FAQ Elementor widget that we’ll include in a CSS file. Keep in mind, this file will only get loaded when need (i.e. when a FAQ widget appears on the page).

We’re keeping it relatively basic since sites sites want to apply their own styles. This keeps your code at a minimal & gives and above all, allows themers to customize based on site needs.

elementor-faqs/assets/css/elementor-faqs.css

.elementor-faqs-container {
  --elementor-faqs-question-weight: bold;
  --elementor-faqs-question-margin: 0 0 1em 0;
}
.elementor-faqs-question {
  font-weight: var(--elementor-faqs-question-weight);
  margin: var(--elementor-faqs-question-margin);
}

Pro Tip: Notice the use of CSS variables. This allows themes to easily change styles without having to rewrite entire classes. Instead they can just change the value of those variables as needed.

After that, only thing you need to do is activate plugin, go to the Elementor page builder and add your new custom FAQ widget.

Join the conversation.

Your email address will not be published. Required fields are marked *

All comments posted on 'Elementor FAQ Widget' are held for moderation and only published when on topic and not rude. Get a gold star if you actually read & follow these rules.

You may write comments in Markdown. This is the best way to post any code, inline like `<div>this</div>` or multiline blocks within triple backtick fences (```) with double new lines before and after.

Want to tell me something privately, like pointing out a typo or stuff like that? Contact Me.