WordPress custom FAQ with Categories plugin

So I have been extracting a lot of code recently from several projects that I’ve done over 2020 and I am converting those independent solutions into plugins so that I can reuse them over time. Usually, my solutions allow developers to easily modify the styling and functionality.

In order to also understand the plugin I will go over it step by step, it will also help you to build your own plugin solutions.

Requirements

  1. SCSS installed with npm
  2. Some icons for toggle up and down. I usually grab them from fontawesome.

This is a quick overview on how this setup works

  1. Create a plugin file and folder structure

    In order for WordPress to read the plugin, you’ll need some info. Plus we need SCSS/CSS and Javascript files.

  2. Create Custom Post Type & Taxonomy for the FAQs

    In order to modify the data in WordPress, and also structure the content.

  3. Query & Display Custom Post Type & Taxonomy

    WP_Query as well as shortcode options in order to handle the display

1. Create Plugin Files

First let’s create a folder in your plugins folder. Name it faq. Then create inside this folder a file called faq.php. Start by adding the following content. Here you can specify some information about the plugin and how it looks inside the admin plugin’s interface.

/**
 * @wordpress-plugin
 * Plugin Name:       WP FAQ Playground
 * Plugin URI:        https://hkvlaanderen.com
 * Description:       Easy to modify plugin for displaying FAQs
 * Version:           1.0.0
 * Author:            Hendrik Vlaanderen
 * Author URI:        https://blog.hkvlaanderen.com
 * License:           GPL-2.0+
 * License URI:       http://www.gnu.org/licenses/gpl-2.0.txt
 * Text Domain:       wp-faq-playground
*/

defined( 'ABSPATH' ) or die( 'Direct script access disallowed.' );

Then, let’s create the folder structure as in the image. This helps with easy file management.

Folder for FAQ plugin

2. Create Custom Post Type and Taxonomy

In the faq.php file add the following code to initialise the custom post type and the taxonomy that belongs to it.

if ( ! function_exists('register_playground_faq') ) {

// Register Custom Post Type
    function register_playground_faq() {

        $labels = array(
            'name'                  => _x( 'FAQ', 'Post Type General Name', 'faq-playground' ),
            'singular_name'         => _x( 'FAQ', 'Post Type Singular Name', 'faq-playground' ),
            'menu_name'             => __( 'FAQ', 'faq-playground' ),
            'name_admin_bar'        => __( 'FAQ', 'faq-playground' ),
            'archives'              => __( 'FAQ', 'faq-playground' ),
            'attributes'            => __( 'Item Attributes', 'faq-playground' ),
            'parent_item_colon'     => __( 'Parent Item:', 'faq-playground' ),
            'all_items'             => __( 'All FAQ Items', 'faq-playground' ),
            'add_new_item'          => __( 'Add New FAQ Item', 'faq-playground' ),
            'add_new'               => __( 'Add FAQ Item', 'faq-playground' ),
            'new_item'              => __( 'New FAQ Item', 'faq-playground' ),
            'edit_item'             => __( 'Edit FAQ Item', 'faq-playground' ),
            'update_item'           => __( 'Update FAQ Item', 'faq-playground' ),
            'view_item'             => __( 'View FAQ Item', 'faq-playground' ),
            'view_items'            => __( 'View FAQ Items', 'faq-playground' ),
            'search_items'          => __( 'Search FAQ Items', 'faq-playground' ),
            'not_found'             => __( 'Not found', 'faq-playground' ),
            'not_found_in_trash'    => __( 'Not found in Trash', 'faq-playground' ),
            'featured_image'        => __( 'Featured Image', 'faq-playground' ),
            'set_featured_image'    => __( 'Set featured image', 'faq-playground' ),
            'remove_featured_image' => __( 'Remove featured image', 'faq-playground' ),
            'use_featured_image'    => __( 'Use as featured image', 'faq-playground' ),
            'insert_into_item'      => __( 'Insert into item', 'faq-playground' ),
            'uploaded_to_this_item' => __( 'Uploaded to this item', 'faq-playground' ),
            'items_list'            => __( 'Items list', 'faq-playground' ),
            'items_list_navigation' => __( 'Items list navigation', 'faq-playground' ),
            'filter_items_list'     => __( 'Filter items list', 'faq-playground' ),
        );
        $args = array(
            'label'                 => __( 'FAQ', 'faq-playground' ),
            'labels'                => $labels,
            'supports'              => array( 'title', 'editor', 'excerpt' ),
            'hierarchical'          => false,
            'public'                => true,
            'show_ui'               => true,
            'show_in_menu'          => true,
            'menu_position'         => 5,
            'menu_icon'             => 'dashicons-businessman',
            'show_in_admin_bar'     => true,
            'show_in_nav_menus'     => true,
            'can_export'            => true,
            'has_archive'           => true,
            'exclude_from_search'   => true,
            'publicly_queryable'    => false,
            'query_var' 			=> true,
            'capability_type'       => 'page',
            'rewrite' 				=> array('slug' => 'faq')
        );
        register_post_type( 'faq', $args );

    }
    add_action( 'init', 'register_playground_faq', 0 );

}


// Register Custom Taxonomy
function register_faq_playground_category() {

    $labels = array(
        'name'                       => _x( 'FAQ Categories', 'Taxonomy General Name', 'sanatio' ),
        'singular_name'              => _x( 'FAQ Category', 'Taxonomy Singular Name', 'sanatio' ),
        'menu_name'                  => __( 'FAQ Category', 'sanatio' ),
        'all_items'                  => __( 'All Categories', 'sanatio' ),
        'parent_item'                => __( 'Parent Item', 'sanatio' ),
        'parent_item_colon'          => __( 'Parent Item:', 'sanatio' ),
        'new_item_name'              => __( 'New Item Name', 'sanatio' ),
        'add_new_item'               => __( 'Add New Item', 'sanatio' ),
        'edit_item'                  => __( 'Edit Item', 'sanatio' ),
        'update_item'                => __( 'Update Item', 'sanatio' ),
        'view_item'                  => __( 'View Item', 'sanatio' ),
        'separate_items_with_commas' => __( 'Separate items with commas', 'sanatio' ),
        'add_or_remove_items'        => __( 'Add or remove items', 'sanatio' ),
        'choose_from_most_used'      => __( 'Choose from the most used', 'sanatio' ),
        'popular_items'              => __( 'Popular Items', 'sanatio' ),
        'search_items'               => __( 'Search Items', 'sanatio' ),
        'not_found'                  => __( 'Not Found', 'sanatio' ),
        'no_terms'                   => __( 'No items', 'sanatio' ),
        'items_list'                 => __( 'Items list', 'sanatio' ),
        'items_list_navigation'      => __( 'Items list navigation', 'sanatio' ),
    );
    $args = array(
        'labels'                     => $labels,
        'hierarchical'               => true,
        'public'                     => false,
        'show_ui'                    => true,
        'has_archive'                => false,
        'show_admin_column'          => true,
        'show_in_nav_menus'          => true,
        'show_tagcloud'              => true,
        'rewrite'                    => false,


    );
    register_taxonomy( 'faq_category', array( 'faq' ), $args );

}
add_action( 'init', 'register_faq_playground_category', 0 );

3. Query and Display Custom Post Type and Taxonomy

Next up, we should see the FAQ and Taxonomy in the wordpress admin panel. Let’s add a few so we can display them later.

Now, let’s make a shortcode to display them on a page that you want to showcase them on. Here you go, add this to your faq.php, and also add one file in the templates folder. I called it list.php.

require_once 'templates/list.php'; // references the get_faq_loop function below

add_shortcode('faq_items', 'faq_show_loop');

function faq_show_loop(){
    ob_start();

    get_faq_loop();

    $html = ob_get_contents();
    ob_end_clean();

    return $html;

}

Add this to list.php

<?php



function get_faq_loop(){
?>
 <div class="faq-blocks">

                        <?php

                        $taxonomy     = 'faq_category';
                        $orderby      = 'name';
                        $show_count   = 0;      // 1 for yes, 0 for no
                        $pad_counts   = 0;      // 1 for yes, 0 for no
                        $hierarchical = 1;      // 1 for yes, 0 for no
                        $title        = '';
                        $empty        = 0;
                        $args = array(
                            'taxonomy'     => $taxonomy,
                            'orderby'      => $orderby,
                            'show_count'   => $show_count,
                            'pad_counts'   => $pad_counts,
                            'hierarchical' => $hierarchical,
                            'title_li'     => $title,
                            'hide_empty'   => $empty
                        );
                        $all_categories = get_categories( $args );
                        ?>
                        <?php foreach($all_categories as $cat) :


                            if(!$cat->category_parent == 0) {

                                // if the parent isnt zero it must be a child.
                                // we dont do anything
                            } else {
                                // if the parent == 0 it must be a parent
                                // output the parent posts
                                faq_category_loop($cat, 'parent');
                            }


                        endforeach;
                        ?>


                </div>
<?php }


function faq_category_loop($cat){ ?>

    <div class="faq-block" id="<?= $cat->slug ?>">

        <div class="category-block">
            <h2><?php echo $cat->name ?></h2>
        </div>


        <?php

        $args = array(
            'post_type' => 'faq',
            'post_status' => 'publish',
            'posts_per_page' => -1,
            'posts_per_archive_page' => -1,
            'orderby' => 'menu_order',
            'order'     => 'ASC',
            'tax_query' => array(
                array (
                    'taxonomy' => 'faq_category',
                    'field' => 'slug',
                    'terms' => $cat->slug,
                    'include_children' => false
                )
            ),
        );



        $query = new WP_Query($args);

        ?>

        <div class="faq-items">
            <?php
            while ($query->have_posts()) : $query->the_post();
                ?>
                <div class="faq-item">
                    <button
                            class="toggle-button"
                            data-toggle="collapse"
                            data-target=".collapse.collapse-<?= get_the_ID() ?>"
                            data-text="Collapse"
                    >
                        <?php the_title() ?>
                    </button>

                    <div class="block collapse collapse-<?= get_the_ID() ?>">
                        <div class="block__content">
                            <?php the_content() ?>
                        </div>
                    </div>

                </div>
            <?php endwhile;
            ?>
        </div>
    </div>

    <?php
    wp_reset_postdata();

}

Also we can start adding the enqueue methods for including the js and css files. Add this also to faq.php.

function faq_playground_include_scripts(){

    $version = 1.0;

    wp_enqueue_style( 'faq-styles', plugins_url('faq') .'/css/faq.css', array(), $version );
    wp_enqueue_script('faq-script', plugins_url('faq') . '/js/faq.js', array('jquery'), $version, true);

}

add_action( 'wp_enqueue_scripts', 'faq_playground_include_scripts', 10);

Start styling & Interactivity

I’ve added in the scss folder a file called faq.scss with the following content. Note the following, there are several variables on the top deciding on the color and font. Also, I create a mixin for the breakpoints. This is used in several points in the file to style differently for mobile. Note, also that the icons are referenced here as SVGs in the icons folder. You’ll need to run sass for this SCSS to turn it into css. Here is the sass command. It watches the scss folder and exports it into css.

sass --watch scss:css
$bebas-bold:'sans-serif';
$dark-forest-green:#333;
$deep-aqua-new:blue;
$light-green:green;

@mixin bp($point) {

  $bp-phone: "(max-width: 768px)";
  $bp-tablet: "(min-width: 768px) and (max-width:1024px)";
  $bp-desktop: "(min-width: 1024px)";

  @if $point == phone {
    @media #{$bp-phone} { @content; }
  }
  @else if $point == tablet {
    @media #{$bp-tablet} { @content; }
  }
  @else if $point == desktop {
    @media #{$bp-desktop}  { @content; }
  }

}


.faq {
  margin-top:30px;
  position: relative;

  @include bp(desktop){
    margin-top:95px;
  }
}

#faq {
  .background__headline {
    top:180px;
    font-size: 330px;
  }
}

$category-block-width:340/1080*100%;
$faq-items-width:524/1080*100%;

.block.collapse {
  display: block;
  max-height: 0px;
  overflow: hidden;
  transition: max-height 1.5s cubic-bezier(0, 1, 0, 1);
  &.show {
    max-height: 99em;
    transition: max-height 1.5s ease-in-out;
  }
}

.toggle-button {
  padding-left:0;
  padding-bottom:10px;
  width: 100%;
  border: 0;
  background: transparent;
  text-align: left;
  font-family: $bebas-bold;
  font-size: 20px;
  font-weight: bold;
  font-stretch: normal;
  font-style: normal;
  line-height: 0.9;
  letter-spacing: normal;
  color:$dark-forest-green;
  text-transform:uppercase;
  padding-right:20px;
  position: relative;
  &.active {
    &:after {
      transform:rotateX(180deg);
      background-image:url("../icons/chevron-down-aqua.svg")
    }
  }
  &.active, &:hover {
    color: $deep-aqua-new;
    border-bottom:1px solid $deep-aqua-new;

  }
  &:after {
    content:"";
    position:absolute;
    right:0;
    top:7px;
    background-repeat: no-repeat;
    background-size:8px;
    background-image:url("../icons/chevron-down-dark.svg");
    width:8px;
    height: 13px;
  }
  border-bottom:1px solid $dark-forest-green;

}
.faq-item {
  margin-bottom:15px;
}

.faq-block {
  margin-bottom:45px;
}


@include bp(desktop){
  .faq-block {
    margin-bottom:0;
  }
  .faq-item {
    margin-bottom:40px;
  }
  .toggle-button {
    font-size: 34px;
    line-height: 0.8;
    &:after {
      top:7px;
    }
  }
}
.block__content {
  margin-top:35px;
  @include p();
  color:$light-green;
}
.faq-blocks {
  width: 100%;
}
.faq-block {
  display: flex;
  flex-flow: row wrap;
}
.category-block {
  margin-bottom:50px;
}
.category-block, .faq-items {
  width:100%;
}

@include bp(desktop){
  .category-block {
    margin-bottom:0;
    width: $category-block-width;
    margin-right: 30px;
    text-transform:uppercase;
  }

  .faq-items {
    width:$faq-items-width;
  }

}

Then the final touch is to add the javascript for the actual toggle action. Here is the javascript.

;( function( $, window, undefined ) {

    'use strict'


    const fnmap = {
        'toggle': 'toggle',
        'show': 'add',
        'hide': 'remove'
    };
    const collapse = (selector, cmd) => {
        const targets = Array.from(document.querySelectorAll(selector));
        targets.forEach(target => {
            target.classList[fnmap[cmd]]('show');
        });
    };

    // Grab all the trigger elements on the page
    const triggers = Array.from(document.querySelectorAll('[data-toggle="collapse"]'));
    // Listen for click events, but only on our triggers
    window.addEventListener('click', (ev) => {

        const elm = ev.target;

        if (triggers.includes(elm)) {



            const selector = elm.getAttribute('data-target');
            if(elm.classList.contains('active')){
                elm.classList.remove('active');
            } else {
                [].forEach.call(triggers, function(el) {
                    // check if its active and if we are clicking on another one
                    if(el.classList.contains('active') && selector !== el.getAttribute('data-target')){
                        el.classList.remove("active");
                        const Sselector = el.getAttribute('data-target');
                        collapse(Sselector, 'toggle');
                    }



                });
                elm.classList.add('active');
            }

            collapse(selector, 'toggle');
        }
    }, false);

} ( jQuery, window ) );

What it does is, it either removes or adds the class ‘active’ to the faq item, so that it opens or closes. Quite simple vanilla script.

Conclusion

So, with this simple approach we have a lot of control over the styling as well as the functionality. Currently, it “groups” the different FAQ items according to the taxonomies but this can easily be modified in the list.php file. Or if you don’t like the styling this can be modified in the scss folder.

Hope this helps understanding how to build a plugin and let me know what you’ve came up with!

It’s also possible to download all the files here.

Leave a Reply