{"id":169583,"date":"2018-01-24T13:00:23","date_gmt":"2018-01-24T13:00:23","guid":{"rendered":"https:\/\/premium.wpmudev.org\/blog\/?p=169583"},"modified":"2018-01-16T05:03:18","modified_gmt":"2018-01-16T05:03:18","slug":"code-your-own-autosuggest-enabled-advanced-search-wordpress-plugin","status":"publish","type":"post","link":"https:\/\/wpmudev.com\/blog\/code-your-own-autosuggest-enabled-advanced-search-wordpress-plugin\/","title":{"rendered":"Code Your Own Autosuggest Enabled Advanced Search WordPress Plugin"},"content":{"rendered":"<p>Want to add advanced search functionality without depending on third-party plugins or themes? Build your own advanced WordPress Search Plugin with support for autosuggest, custom post types, taxonomies, custom fields, and caching from the ground up.<\/p>\n<p>In this article, I&#8217;ll show you how to build an object-oriented plugin to add a shortcode-based advanced search form that can be filtered using custom post types (CPTs), custom taxonomies as well as custom metadata created using Advanced Control Fields (ACF). The search form will also feature an AJAX powered autosuggest, and make good use of WordPress transients to cache results.<\/p>\n<p>While free plugins are a popular way to add greater functionality, they often come with their own set of problems. And there are <a href=\"https:\/\/wpmudev.com\/blog\/too-many-plugins\/\" target=\"_blank\">so many factors<\/a> to consider when getting that combination right. At times, developing your own plugin for areas that require a more fine-tuned approach may prove far more beneficial in the larger scheme of things. It will also allow you greater control over usability, performance and application security.<\/p>\n<p><i><b>Note: This article is intended for intermediate-advanced WordPress developers. It assumes that you have a working knowledge of PHP, JavaScript, the WordPress Loop, Transients, and the WordPress Plugin API. If you&#8217;d like a refresher, I recommend that you read through the following:<\/b><\/i><\/p>\n<ul>\n<li><a href=\"https:\/\/wpmudev.com\/blog\/creating-content-taxonomies-and-fields\/\" target=\"_blank\">Creating Custom Content in WordPress: Taxonomies and Fields<\/a><\/li>\n<li><a href=\"https:\/\/wpmudev.com\/blog\/wordpress-development-intermediate-building-plugins\/\" target=\"_blank\">WordPress Development for Intermediate Users: Building Plugins<\/a><\/li>\n<li><a href=\"https:\/\/wpmudev.com\/blog\/advanced-wordpress-development-transients\/\" target=\"_blank\">Advanced WordPress Development: Working With Transients<\/a><\/li>\n<li><a href=\"https:\/\/wpmudev.com\/blog\/advanced-wordpress-development-introduction-to-oop\" target=\"_blank\">Advanced WordPress Development: Introduction to Object-Oriented Programming<\/a><\/li>\n<\/ul>\n<p>For this article, I&#8217;ve prepared a custom WordPress plugin that adds an autosuggest enabled search form to any WordPress page using a shortcode. It also lets you fine-tune the search results to a single or multiple custom post types, and displays them in a flexbox grid layout. You can <a href=\"https:\/\/github.com\/nuancedesignstudio\/nds-advanced-search\" rel=\"noopener\" target=\"_blank\">download my search plugin from here<\/a> to follow along with the article.<\/p>\n<p>Later in the article, I&#8217;ll extend the same plugin, and add complex search features such as filtering on multiple taxonomies and custom fields for a fictitious video library but the example can be easily extended to any scenario such as a product search or even <a href=\"https:\/\/wpmudev.com\/blog\/custom-faceted-search\/\" target=\"_blank\" rel=\"noopener\">faceted search<\/a>.<\/p>\n<p>CPTs and ACFs add tremendous power and functionality to WordPress, and are fairly common in any bespoke development project. My goal is to help you integrate several such advanced WordPress features to play together in a real-world application. So let&#8217;s get started!<\/p>\n<p><b>Note:<\/b> For the rest of the article and throughout the code, the term <code>Post Type<\/code> refers to the default <code>Posts<\/code> and <code>Pages<\/code> post types as well as any registered <code>Custom Post Types<\/code>.<\/p>\n<h2>Flow of Control of the Custom Search Plugin<\/h2>\n<p>Before I dive into the code, here&#8217;s a high-level view of how the plugin works behind the scenes. The plugin&#8217;s search engine kicks in once the user enters the keywords in the search bar, or when the search form is submitted. And then, a lot depends on the state of the transient.<\/p>\n<div  class=\"wpdui-pic-regular  \">\n<figure class=\"wp-caption alignnone\" data-caption=\"true\"><img loading=\"lazy\" decoding=\"async\" class=\"attachment-600x600 size-600x600\" src=\"https:\/\/wpmudev.com\/blog\/wp-content\/uploads\/2017\/12\/advanced-search-plugin-flow-of-control.png\" alt=\"advanced-search-plugin-flow-of-control\" width=\"450\" height=\"600\" \/><figcaption class=\"wp-caption-text\">Flow of Control of the Custom Search Plugin<\/figcaption><\/figure>\n<\/div>\n<p>Note that I&#8217;m not only passing the cached post titles via <code>wp_localize_script<\/code> but also caching the data locally in JavaScript. This is because the autosuggest can have a huge impact on the performance if an AJAX request is made each time a search key is entered.<\/p>\n<div  class=\"wpdui-pic-regular  \">\n<figure class=\"wp-caption alignnone\" data-caption=\"true\"><img loading=\"lazy\" decoding=\"async\" class=\"attachment-600x600 size-600x600\" src=\"https:\/\/wpmudev.com\/blog\/wp-content\/uploads\/2017\/12\/advanced-search-plugin-delete-transients.png\" alt=\"advanced-search-plugin-delete-transients\" width=\"600\" height=\"309\" \/><figcaption class=\"wp-caption-text\">Sates in which the transient is deleted<\/figcaption><\/figure>\n<\/div>\n<p>For a resource-sensitive area like application search, I centered my implementation around the following ideas:<\/p>\n<ul>\n<li>Cache only what is needed in the Transient, and not the entire WP_Query object<\/li>\n<li>Ensure that as few AJAX calls are made for the autosuggest<\/li>\n<li>Ensure that the transient does not hold stale information<\/li>\n<li>Provide settings to control which Post Types will be included in the search<\/li>\n<\/ul>\n<p>Naturally, one size will not fit all, and some amount of tweaking will be required based on the complexity of your application.<\/p>\n<p>Let&#8217;s examine the structure of the plugin to get a sense of what goes where.<\/p>\n<h2>Structure of the Advanced Search Plugin<\/h2>\n<p>The <a href=\"https:\/\/github.com\/nuancedesignstudio\/nds-advanced-search\" rel=\"noopener\" target=\"_blank\">search plugin<\/a> is based on <a href=\"https:\/\/github.com\/nuancedesignstudio\/WordPress-Plugin-Boilerplate\" rel=\"noopener\" target=\"_blank\">my own plugin template<\/a> which is a fork of the original <a href=\"http:\/\/wppb.io\/\" rel=\"noopener\" target=\"_blank\">WordPress Plugin Boilerplate<\/a> project. Here\u2019s how it&#8217;s structured in the backend:<\/p>\n<ul>\n<li><code>inc\/core\/*<\/code> &#8211; Core functionality of the plugin<\/li>\n<li><code>inc\/core\/class-init.php<\/code> &#8211; Registration of hooks for the admin menu, shortcode, AJAX handler, admin notices, scripts and styles<\/li>\n<li><code>inc\/admin\/class-admin.php<\/code> &#8211; Functionality for the settings area of the plugin in the admin dashboard<\/li>\n<li><code>inc\/common\/class-common.php<\/code> &#8211; Functionality for providing the shortcode based search and autosuggest AJAX handler<\/li>\n<li><code>inc\/common\/views\/*<\/code> &#8211; The search form and the search results<\/li>\n<li><code>inc\/common\/js\/*<\/code> &#8211; Autosuggest Handler<\/li>\n<li><code>inc\/common\/css\/*<\/code> &#8211; CSS for the search form<\/li>\n<\/ul>\n<p>On activation, the plugin adds an <code><a href=\"https:\/\/codex.wordpress.org\/Creating_Options_Pages\" rel=\"noopener\" target=\"_blank\">Options Page<\/a><\/code> as a sub-level menu item to the <i>Settings<\/i> administration menu. It lists the registered <code>custom post types<\/code> along with the default <i>Posts<\/i> and <i>Pages<\/i> post types as options to include in the advanced search.<\/p>\n<div  class=\"wpdui-pic-regular  \">\n<figure class=\"wp-caption alignnone\" data-caption=\"true\"><img loading=\"lazy\" decoding=\"async\" class=\"attachment-600x600 size-600x600\" src=\"https:\/\/wpmudev.com\/blog\/wp-content\/uploads\/2017\/12\/advanced-search-plugin-settings.png\" alt=\"advanced-search-plugin-settings\" width=\"600\" height=\"416\" \/><figcaption class=\"wp-caption-text\">Plugin settings page to select post types<\/figcaption><\/figure>\n<\/div>\n<p>I won&#8217;t go into the details of this but to see how I added the <a href=\"https:\/\/wpmudev.com\/blog\/creating-wordpress-admin-pages\/\" target=\"_blank\" rel=\"noopener\">admin page<\/a>, take a look at the <code>define_admin_hooks()<\/code> method of <code>inc\/core\/class-init.php<\/code> and <code>add_plugin_admin_menu()<\/code> method of <code>inc\/admin\/class-admin.php<\/code>. The markup for the settings page can be found in <code>inc\/admin\/views\/html-nds-advanced-search-admin-options.php<\/code>.<\/p>\n<p>The place where things actually start to get interesting is <code>inc\/common\/class-common.php<\/code>. The entry-point to it is through the <code>define_common_hooks()<\/code> method in <code>inc\/core\/class-init.php<\/code>.<\/p>\n<div class=\"gist\" data-gist=\"0e584ae9450b1ab1b9b3c1d7d61c8b01\" data-gist-file=\"class-init.php\"><a class=\"loading\" href=\"https:\/\/gist.github.com\/0e584ae9450b1ab1b9b3c1d7d61c8b01.js?file=class-init.php\">Loading gist 0e584ae9450b1ab1b9b3c1d7d61c8b01<\/a><div class=\"gist-consent-notice\" style=\"display:none\"><p>Please <a href=\"javascript:Cookiebot.renew()\">update your cookie preferences<\/a> to enable preference cookies to view this gist.<\/p><\/div><\/div>\n<p>So let&#8217;s start with the shortcode.<\/p>\n<h2>Shortcode to Load the Custom Search Form<\/h2>\n<p>The <code>register_shortcodes()<\/code> method of <code>inc\/common\/class-common.php<\/code> adds the shortcode <code>[nds-advanced-search]<\/code> which is used to plug in the custom search form.<\/p>\n<div class=\"gist\" data-gist=\"5e52011542082b940ae71b6eb0f500b8\" data-gist-file=\"class-common.php\"><a class=\"loading\" href=\"https:\/\/gist.github.com\/5e52011542082b940ae71b6eb0f500b8.js?file=class-common.php\">Loading gist 5e52011542082b940ae71b6eb0f500b8<\/a><div class=\"gist-consent-notice\" style=\"display:none\"><p>Please <a href=\"javascript:Cookiebot.renew()\">update your cookie preferences<\/a> to enable preference cookies to view this gist.<\/p><\/div><\/div>\n<p>Looking at the gist above, you&#8217;ll notice that the content for the shortcode ( i.e. the search form ) is returned by the <code>shortcode_nds_advanced_search()<\/code> callback function. I could have done something like this:<\/p>\n<div class=\"gist\" data-gist=\"d41849a407c0dc49c79b25034a67f4d1\" data-gist-file=\"class-common.php\"><a class=\"loading\" href=\"https:\/\/gist.github.com\/d41849a407c0dc49c79b25034a67f4d1.js?file=class-common.php\">Loading gist d41849a407c0dc49c79b25034a67f4d1<\/a><div class=\"gist-consent-notice\" style=\"display:none\"><p>Please <a href=\"javascript:Cookiebot.renew()\">update your cookie preferences<\/a> to enable preference cookies to view this gist.<\/p><\/div><\/div>\n<p>However, I took a slightly different approach.<\/p>\n<p>Rather than directly output the markup of the search form in the shortcode callback, I used the <code><a href=\"https:\/\/developer.wordpress.org\/reference\/hooks\/get_search_form\/\" rel=\"noopener\" target=\"_blank\">get_search_form<\/a><\/code> filter and the <code><a href=\"https:\/\/developer.wordpress.org\/reference\/functions\/get_search_form\/\" rel=\"noopener\" target=\"_blank\">get_search_form()<\/a><\/code> function to load the search form.<\/p>\n<div class=\"gist\" data-gist=\"9ecd1b55fa3a31fa22379bdfe271de10\" data-gist-file=\"class-common.php\"><a class=\"loading\" href=\"https:\/\/gist.github.com\/9ecd1b55fa3a31fa22379bdfe271de10.js?file=class-common.php\">Loading gist 9ecd1b55fa3a31fa22379bdfe271de10<\/a><div class=\"gist-consent-notice\" style=\"display:none\"><p>Please <a href=\"javascript:Cookiebot.renew()\">update your cookie preferences<\/a> to enable preference cookies to view this gist.<\/p><\/div><\/div>\n<p>I&#8217;ve done this to allow you more control over the rendering of the search form. Let&#8217;s see how that works.<\/p>\n<h2>The get_search_form Filter Hook and Template Tag<\/h2>\n<p>The <b><code><a href=\"https:\/\/developer.wordpress.org\/reference\/hooks\/get_search_form\/\" rel=\"noopener\" target=\"_blank\">get_search_form()<\/a><\/code> template tag <\/b> is a great way of taking control over the display of the WordPress search. It first looks to see if the theme contains <code><a href=\"https:\/\/codex.wordpress.org\/Creating_a_Search_Page\" rel=\"noopener\" target=\"_blank\">searchform.php<\/a><\/code> and loads the markup defined by it; otherwise, it loads the built-in search form provided by WordPress.<\/p>\n<p>Or you could completely replace the search form using the <b><code><a href=\"https:\/\/developer.wordpress.org\/reference\/hooks\/get_search_form\/\" rel=\"noopener\" target=\"_blank\">get_search_form<\/a><\/code> filter hook<\/b>, in which case <code>get_search_form()<\/code> will return the markup defined by the filter callback.<\/p>\n<p>On submitting the default search form, WordPress will take over and render the search results using the <code><a href=\"https:\/\/wphierarchy.com\/\" rel=\"noopener\" target=\"_blank\">search.php<\/a><\/code> template, provided the following conditions are met:<\/p>\n<ul>\n<li>The search form does a <code>GET<\/code> to the homepage of the site<\/li>\n<li>The <code>name<\/code> attribute of the input text field is named <code>s<\/code><\/li>\n<\/ul>\n<p>In my example, I don&#8217;t want WordPress to handle the search, and I&#8217;ve replaced the default form using the filter hook: <code>add_filter( 'get_search_form', array( $this, 'advanced_search_form_markup' ) )<\/code><\/p>\n<div  class=\"wpdui-pic-regular  \">\n<figure class=\"wp-caption alignnone\" data-caption=\"true\"><img loading=\"lazy\" decoding=\"async\" class=\"attachment-600x600 size-600x600\" src=\"https:\/\/wpmudev.com\/blog\/wp-content\/uploads\/2018\/01\/wordpress_get_search_form_filter_hook.png\" alt=\"wordpress_get_search_form_filter_hook\" width=\"600\" height=\"388\" \/><figcaption class=\"wp-caption-text\">Contents of $form before the get_search_form filter is applied<\/figcaption><\/figure>\n<\/div>\n<p>Here, I&#8217;ve paused execution to inspect the <code>$form<\/code> variable passed by the filter. It holds the form HTML provided by <code>searchform.php<\/code> of the Twenty Sixteen theme which will be replaced by the markup of my search form in <code>inc\/common\/views\/html-nds-advanced-search-form.php<\/code>.<\/p>\n<div class=\"gist\" data-gist=\"e77c3df9fdc9f9e9f503aff73e3839c5\" data-gist-file=\"html-nds-advanced-search-form.php\"><a class=\"loading\" href=\"https:\/\/gist.github.com\/e77c3df9fdc9f9e9f503aff73e3839c5.js?file=html-nds-advanced-search-form.php\">Loading gist e77c3df9fdc9f9e9f503aff73e3839c5<\/a><div class=\"gist-consent-notice\" style=\"display:none\"><p>Please <a href=\"javascript:Cookiebot.renew()\">update your cookie preferences<\/a> to enable preference cookies to view this gist.<\/p><\/div><\/div>\n<p>As I&#8217;m not following any of the above rules about the form action or the name of the input attribute, WordPress will do nothing. This is fine as I want to take full control of the search results, and also load it on the same page as that of the shortcode.<\/p>\n<div  class=\"wpdui-pic-regular  \">\n<figure class=\"wp-caption alignnone\" data-caption=\"true\"><img loading=\"lazy\" decoding=\"async\" class=\"attachment-600x600 size-600x600\" src=\"https:\/\/wpmudev.com\/blog\/wp-content\/uploads\/2018\/01\/search-page-with-shorcode.png\" alt=\"custom search form with shortcode\" width=\"600\" height=\"516\" \/><figcaption class=\"wp-caption-text\">Loading the custom search form with a shortcode<\/figcaption><\/figure>\n<\/div>\n<p>The use of a custom form with <code>get_search_form<\/code> is purely a design decision, and you may have a different take on it. I intentionally kept the default search form and the one provided by the plugin separate. It also ensures that the search form used by a theme in the sidebar, header etc. are not affected by the custom search form.<\/p>\n<h3>Handling Form Submission<\/h3>\n<p>The search form of the plugin submits to the same page as that of the shortcode, and so, the shortcode callback handles the form submission as well.<\/p>\n<div class=\"gist\" data-gist=\"fb8729a02e6a536685b707b9254db726\" data-gist-file=\"class-common.php\"><a class=\"loading\" href=\"https:\/\/gist.github.com\/fb8729a02e6a536685b707b9254db726.js?file=class-common.php\">Loading gist fb8729a02e6a536685b707b9254db726<\/a><div class=\"gist-consent-notice\" style=\"display:none\"><p>Please <a href=\"javascript:Cookiebot.renew()\">update your cookie preferences<\/a> to enable preference cookies to view this gist.<\/p><\/div><\/div>\n<p>If, for some reason, you want to handle the form-data on a different page, have a look at my article on <a href=\"https:\/\/wpmudev.com\/blog\/handling-form-submissions\/\" target=\"_blank\" rel=\"noopener\">Handling Form Submissions<\/a> in WordPress.<\/p>\n<p>Let&#8217;s now see how the plugin renders the search results.<\/p>\n<h2>Using WP_Query for Querying Posts in Post Types &amp; the Search Expression<\/h2>\n<p>The search results are garnered using a custom <code>WP_Query<\/code> and rendered through <code>inc\/common\/views\/html-nds-advanced-search-results.php<\/code>.<\/p>\n<p>My custom <code>WP_Query<\/code> takes the following <a href=\"https:\/\/codex.wordpress.org\/Class_Reference\/WP_Query#Parameters\" rel=\"noopener\" target=\"_blank\">arguments<\/a>:<\/p>\n<ul>\n<li><code>'s' =&gt; $search_term<\/code> &#8211; Where <code>$search_term<\/code> is the keyword to search for<\/li>\n<li><code>'sentence' =&gt; true <\/code> &#8211; Perform a full phrase search<\/li>\n<li><code>'post_type' =&gt; $post_types <\/code> &#8211; Include posts belonging to <code>$post_types<\/code> which holds the Post Types specified in the plugin settings<\/li>\n<li><code>'post__in' =&gt; $cached_post_ids <\/code> &#8211; Only search against Post IDs available in <code>$cached_post_ids<\/code><\/li>\n<li><code>'no_found_rows' =&gt; true <\/code> &#8211; Speed up query execution by not counting the number of rows found<\/li>\n<\/ul>\n<p>The <code>sentence<\/code> parameter will cause <a href=\"https:\/\/codex.wordpress.org\/Class_Reference\/WP_Query\" rel=\"noopener\" target=\"_blank\">WP_Query<\/a> to return posts where the full search term is present. This is like a strict search, and will probably reduce the result set. It may or may not work in your favor, and you should tweak it based on your requirements. The <code><a href=\"https:\/\/core.trac.wordpress.org\/browser\/tags\/4.8.3\/src\/wp-includes\/class-wp-query.php#L684\" rel=\"noopener\" target=\"_blank\">no_found_rows<\/a><\/code> is another parameter that works for me as I don&#8217;t need pagination in my loop.<\/p>\n<p>I am also making use of the <code>post__in<\/code> parameter to further restrict the search to Posts belonging to specific Post IDs. The Post IDs will vary based on the selected Post Types, and as new posts are created or old ones are deleted.<\/p>\n<div class=\"gist\" data-gist=\"fd6a0ab49c878c07231dd15eb9325cea\" data-gist-file=\"html-nds-advanced-search-results.php\"><a class=\"loading\" href=\"https:\/\/gist.github.com\/fd6a0ab49c878c07231dd15eb9325cea.js?file=html-nds-advanced-search-results.php\">Loading gist fd6a0ab49c878c07231dd15eb9325cea<\/a><div class=\"gist-consent-notice\" style=\"display:none\"><p>Please <a href=\"javascript:Cookiebot.renew()\">update your cookie preferences<\/a> to enable preference cookies to view this gist.<\/p><\/div><\/div>\n<p>If you look at the flowchart at the beginning of the article, you&#8217;ll notice the conditional logic for checking cached posts in the <a href=\"https:\/\/wpmudev.com\/blog\/advanced-wordpress-development-transients\/\" target=\"_blank\" rel=\"noopener\">transient<\/a> in the gist above.<\/p>\n<h3>Using WordPress Transients to Cache Posts<\/h3>\n<p>The <code><a href=\"https:\/\/codex.wordpress.org\/Function_Reference\/get_transient\" rel=\"noopener\" target=\"_blank\">get_transient()<\/a><\/code> function returns the state of the transient. If it exists, it returns the array containing the cached Post IDs and Titles of posts for the required Post Types. If the transient expires or does not exist or is deleted during a post or settings update, it will return false, and <code>get_posts()<\/code> will be invoked through the <code>cache_posts_in_post_types()<\/code> method.<\/p>\n<div class=\"gist\" data-gist=\"5183a85c67fc40917f1d416686b59ce3\" data-gist-file=\"class-common.php\"><a class=\"loading\" href=\"https:\/\/gist.github.com\/5183a85c67fc40917f1d416686b59ce3.js?file=class-common.php\">Loading gist 5183a85c67fc40917f1d416686b59ce3<\/a><div class=\"gist-consent-notice\" style=\"display:none\"><p>Please <a href=\"javascript:Cookiebot.renew()\">update your cookie preferences<\/a> to enable preference cookies to view this gist.<\/p><\/div><\/div>\n<p>You may need to adjust the arguments to <code><a href=\"https:\/\/codex.wordpress.org\/Template_Tags\/get_posts#Parameters\" rel=\"noopener\" target=\"_blank\">get_posts()<\/a><\/code> based on your requirements as it controls which Post IDs are cached.<\/p>\n<p>The <code>cache_posts_in_post_types()<\/code> method is also used by the autosuggest handler to cache post titles. If you don&#8217;t intend to use autosuggest, you could further speed up <code>get_posts()<\/code> by only retrieving IDs using the <code><a href=\"https:\/\/codex.wordpress.org\/Class_Reference\/WP_Query#Return_Fields_Parameter\" rel=\"noopener\" target=\"_blank\">return fields<\/a><\/code> parameter.<\/p>\n<div  class=\"wpdui-pic-regular  \">\n<figure class=\"wp-caption alignnone\" data-caption=\"true\"><img loading=\"lazy\" decoding=\"async\" class=\"attachment-600x600 size-600x600\" src=\"https:\/\/wpmudev.com\/blog\/wp-content\/uploads\/2018\/01\/advanced-search-results-flexbox.png\" alt=\"advanced-search-results-flexbox\" width=\"495\" height=\"600\" \/><figcaption class=\"wp-caption-text\">Custom query search results in a flexbox layout<\/figcaption><\/figure>\n<\/div>\n<p>Let&#8217;s now look at the final piece of the puzzle &#8211; the search autosuggest.<\/p>\n<h2>Adding an Autosuggest Feature to the Search Form<\/h2>\n<p>I&#8217;ve made use of the <a href=\"https:\/\/jqueryui.com\/autocomplete\/\" rel=\"noopener\" target=\"_blank\">jQuery UI Autocomplete<\/a> widget to provide the search suggestions as it&#8217;s <a href=\"https:\/\/developer.wordpress.org\/reference\/functions\/wp_enqueue_script\/#default-scripts-included-and-registered-by-wordpress\" rel=\"noopener\" target=\"_blank\">already included in WordPress<\/a>. However, there are many other external libraries that you could also use.<\/p>\n<p>The <code>enqueue_scripts<\/code> method of <code>inc\/common\/class-common.php<\/code> takes care of loading the required script.<\/p>\n<div class=\"gist\" data-gist=\"b8172aa19d803b36a30236612cff3de2\" data-gist-file=\"class-common.php\"><a class=\"loading\" href=\"https:\/\/gist.github.com\/b8172aa19d803b36a30236612cff3de2.js?file=class-common.php\">Loading gist b8172aa19d803b36a30236612cff3de2<\/a><div class=\"gist-consent-notice\" style=\"display:none\"><p>Please <a href=\"javascript:Cookiebot.renew()\">update your cookie preferences<\/a> to enable preference cookies to view this gist.<\/p><\/div><\/div>\n<p>If cached post titles are available in the transient, they are also made available to the autosuggest JavaScript via the <code><a href=\"https:\/\/codex.wordpress.org\/Function_Reference\/wp_localize_script\" rel=\"noopener\" target=\"_blank\">wp_localize_script<\/a><\/code> function. This helps to minimize the number of AJAX calls made to WordPress for search suggestions.<\/p>\n<p>The code below is fairly rudimentary but serves the purpose of handling the autosuggest.<\/p>\n<div class=\"gist\" data-gist=\"38b541bf057403bf1c72aa3c3489995f\" data-gist-file=\"nds-advanced-search.js\"><a class=\"loading\" href=\"https:\/\/gist.github.com\/38b541bf057403bf1c72aa3c3489995f.js?file=nds-advanced-search.js\">Loading gist 38b541bf057403bf1c72aa3c3489995f<\/a><div class=\"gist-consent-notice\" style=\"display:none\"><p>Please <a href=\"javascript:Cookiebot.renew()\">update your cookie preferences<\/a> to enable preference cookies to view this gist.<\/p><\/div><\/div>\n<p>Here are few important aspects of the autosuggest script:<\/p>\n<ul>\n<li>It makes use of <code><a href=\"http:\/\/api.jquery.com\/jquery.grep\/\" rel=\"noopener\" target=\"_blank\">jQuery.grep()<\/a><\/code> to filter the post titles. You can further refine the match using <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/JavaScript\/Reference\/Global_Objects\/RegExp\" rel=\"noopener\" target=\"_blank\">Regular Expressions<\/a><\/li>\n<li>Search keys are again cached in a local object to reduce the <code>grep<\/code> calls but you could also take advantage of the <code><a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/API\/Web_Storage_API\/Using_the_Web_Storage_API\" rel=\"noopener\" target=\"_blank\">HTML Web Storage API<\/a><\/code> to make the cache persistent<\/li>\n<li>An AJAX request is made only when <code>wp_localize_script<\/code> passes an empty value for post titles<\/li>\n<\/ul>\n<p>So let&#8217;s see how the AJAX request is handled.<\/p>\n<h3>The Autosuggest AJAX Handler<\/h3>\n<p>The <code>action: \"nds_advanced_search_autosuggest\"<\/code> property of the AJAX request specifies the <code><a href=\"https:\/\/codex.wordpress.org\/Plugin_API\/Action_Reference\/wp_ajax_(action)\" rel=\"noopener\" target=\"_blank\">wp_ajax_{action}<\/a><\/code> WordPress hook to be executed and allows handling the AJAX request on the server-side.<\/p>\n<div class=\"gist\" data-gist=\"344acc19008825b234d40116bc1bc8a3\" data-gist-file=\"class-common.php\"><a class=\"loading\" href=\"https:\/\/gist.github.com\/344acc19008825b234d40116bc1bc8a3.js?file=class-common.php\">Loading gist 344acc19008825b234d40116bc1bc8a3<\/a><div class=\"gist-consent-notice\" style=\"display:none\"><p>Please <a href=\"javascript:Cookiebot.renew()\">update your cookie preferences<\/a> to enable preference cookies to view this gist.<\/p><\/div><\/div>\n<p>The handler indirectly invokes <code>get_posts()<\/code> through <code>cache_posts_in_post_types()<\/code> which you saw earlier, and then sends the post titles as the AJAX response using the <code><a href=\"https:\/\/developer.wordpress.org\/reference\/functions\/wp_send_json\/\" rel=\"noopener\" target=\"_blank\">wp_send_json()<\/a><\/code> function.<\/p>\n<div  class=\"wpdui-pic-regular  \">\n<figure class=\"wp-caption alignnone\" data-caption=\"true\"><img loading=\"lazy\" decoding=\"async\" class=\"attachment-600x600 size-600x600\" src=\"https:\/\/wpmudev.com\/blog\/wp-content\/uploads\/2018\/01\/advanced-search-autosuggest-in-action.png\" alt=\"advanced-search-autosuggest-in-action\" width=\"600\" height=\"571\" \/><figcaption class=\"wp-caption-text\">Autosuggest for the custom search.<\/figcaption><\/figure>\n<\/div>\n<h2>Ensuring Data in the Transient is Not Stale<\/h2>\n<p>It is important to delete the transient when posts belonging to the required post types are created, updated etc. The <code>delete_post_cache_for_post_type()<\/code> method of <code>class-admin.php<\/code> takes care of this, and is invoked using the <code><a href=\"https:\/\/codex.wordpress.org\/Post_Status_Transitions\" rel=\"noopener\" target=\"_blank\">transition_post_status<\/a><\/code> action hook in <code>define_common_hooks()<\/code> of <code>class-init.php<\/code>.<\/p>\n<div class=\"gist\" data-gist=\"92ff6b3233882349eeef6c814e662736\" data-gist-file=\"class-common.php\"><a class=\"loading\" href=\"https:\/\/gist.github.com\/92ff6b3233882349eeef6c814e662736.js?file=class-common.php\">Loading gist 92ff6b3233882349eeef6c814e662736<\/a><div class=\"gist-consent-notice\" style=\"display:none\"><p>Please <a href=\"javascript:Cookiebot.renew()\">update your cookie preferences<\/a> to enable preference cookies to view this gist.<\/p><\/div><\/div>\n<p>Also, note that expired Transients won&#8217;t clutter up the database as WordPress will clear them each time a reference is made to it.<\/p>\n<p>Great! At this stage, we have a fully functional advanced search plugin. Let&#8217;s take this to next level by implementing a search page with custom taxonomies and advanced custom fields.<\/p>\n<h2>Case Study: Implementing Custom Video Search with Search Filters<\/h2>\n<p>A more advanced search would typically also allow users to refine the search using filters. You can easily do this by using the post metadata such as the taxonomy terms and the custom fields for the search filters. This is what I mean:<\/p>\n<div  class=\"wpdui-pic-regular  \">\n<figure class=\"wp-caption alignnone\" data-caption=\"true\"><img loading=\"lazy\" decoding=\"async\" class=\"attachment-600x600 size-600x600\" src=\"https:\/\/wpmudev.com\/blog\/wp-content\/uploads\/2018\/01\/advanced-search-with-search-filters-cutomfields-taxonomies.png\" alt=\"advanced-search-with-search-filters-cutomfields-taxonomies\" width=\"534\" height=\"600\" \/><figcaption class=\"wp-caption-text\">Advanced search with search filters using custom taxonomies and advanced custom fields<\/figcaption><\/figure>\n<\/div>\n<p>I&#8217;ve used <a href=\"https:\/\/www.advancedcustomfields.com\/\" rel=\"noopener\" target=\"_blank\">Advanced Custom Fields<\/a> to add custom metadata for the Video custom post type. Some of these appear as search filters as shown above, and the rest are used in the display of the search results.<\/p>\n<div  class=\"wpdui-pic-regular  \">\n<figure class=\"wp-caption alignnone\" data-caption=\"true\"><img loading=\"lazy\" decoding=\"async\" class=\"attachment-600x600 size-600x600\" src=\"https:\/\/wpmudev.com\/blog\/wp-content\/uploads\/2018\/01\/custom-fields-with-acf.png\" alt=\"custom-fields-with-acf\" width=\"553\" height=\"600\" \/><figcaption class=\"wp-caption-text\">Custom meta for the Video custom post type created with Advanced Custom Fields<\/figcaption><\/figure>\n<\/div>\n<p>I&#8217;ve also added a couple of <a href=\"https:\/\/codex.wordpress.org\/Taxonomies\" rel=\"noopener\" target=\"_blank\">custom taxonomies<\/a>: Video Types and Video Locations to the custom post type. The taxonomy terms are used as checkboxes in the search filter.<\/p>\n<div  class=\"wpdui-pic-regular  \">\n<figure class=\"wp-caption alignnone\" data-caption=\"true\"><img loading=\"lazy\" decoding=\"async\" class=\"attachment-600x600 size-600x600\" src=\"https:\/\/wpmudev.com\/blog\/wp-content\/uploads\/2018\/01\/custom-taxonomies-for-cpt.png\" alt=\"custom-taxonomies-for-cpt\" width=\"582\" height=\"600\" \/><figcaption class=\"wp-caption-text\">Custom Taxonomies for the Video Custom Post Type<\/figcaption><\/figure>\n<\/div>\n<p><i><b>Note:<\/b> Here, I&#8217;ve assumed that the custom taxonomies and custom fields are registered for a single post type or are shared across the required post types. I&#8217;ve also assumed that the post types do not use the built-in <code>post_tag<\/code> and <code>category<\/code> taxonomies.<\/i><\/p>\n<p>The entire code for implementing the search filters <a href=\"https:\/\/github.com\/nuancedesignstudio\/nds-advanced-search-demo\" rel=\"noopener\" target=\"_blank\">can be downloaded from here<\/a> for reference.<\/p>\n<h3>Defining the Structure of the Form-Data<\/h3>\n<p>It&#8217;s a good idea to define the structure of the HTML form before adding the markup. I decided to use an associative array to group related information. On form submission, I wanted the form-data to be available in the <code>$_POST<\/code> superglobal as shown below:<\/p>\n<div  class=\"wpdui-pic-regular  \">\n<figure class=\"wp-caption alignnone\" data-caption=\"true\"><img loading=\"lazy\" decoding=\"async\" class=\"attachment-600x600 size-600x600\" src=\"https:\/\/wpmudev.com\/blog\/wp-content\/uploads\/2018\/01\/search-form-input-using-arrays.png\" alt=\"search-form-input-using-arrays\" width=\"494\" height=\"600\" \/><figcaption class=\"wp-caption-text\">Capturing form input stored in an associative array in the shortcode callback<\/figcaption><\/figure>\n<\/div>\n<p>This is purely a coding preference as it allows me to capture the form input into fewer variables that I can easily loop over, but it does make the form HTML more complex.<\/p>\n<p>So let&#8217;s start with modifying the search form in <code>inc\/common\/views\/html-nds-advanced-search-form.php<\/code> to add the custom taxonomies as search filters.<\/p>\n<h3>Creating a Dynamic List of Checkboxes Using Custom Taxonomies for the Search Filter<\/h3>\n<p>To retrieve the taxonomies associated with the Video post type, I made use of the <code><a href=\"https:\/\/developer.wordpress.org\/reference\/functions\/get_object_taxonomies\/\" rel=\"noopener\" target=\"_blank\">get_object_taxonomies()<\/a><\/code> function with the second parameter set to <code>objects<\/code>. This gave me access to the taxonomy&#8217;s slug, name and label, which I then used to collect the respective terms with the <code><a href=\"https:\/\/developer.wordpress.org\/reference\/functions\/get_terms\/\" rel=\"noopener\" target=\"_blank\">get_terms()<\/a><\/code> function as shown below:<\/p>\n<div class=\"gist\" data-gist=\"9b2256ea5b63f9af05519f137244b1c0\" data-gist-file=\"html-nds-advanced-search-form.php\"><a class=\"loading\" href=\"https:\/\/gist.github.com\/9b2256ea5b63f9af05519f137244b1c0.js?file=html-nds-advanced-search-form.php\">Loading gist 9b2256ea5b63f9af05519f137244b1c0<\/a><div class=\"gist-consent-notice\" style=\"display:none\"><p>Please <a href=\"javascript:Cookiebot.renew()\">update your cookie preferences<\/a> to enable preference cookies to view this gist.<\/p><\/div><\/div>\n<p>Based on the number of custom taxonomies and terms returned, the HTML markup for the checkboxes are dynamically created in the <code>for<\/code> loop.<\/p>\n<p>Querying posts using taxonomies will require you to set the <code><a href=\"https:\/\/developer.wordpress.org\/reference\/classes\/wp_query\/#taxonomy-parameters\" rel=\"noopener\" target=\"_blank\">tax_query<\/a><\/code> parameter of <code>WP_Query<\/code>.<\/p>\n<h3>Creating a Dynamic tax_query to Query Posts Using Custom Taxonomies<\/h3>\n<p>The <code>tax_query<\/code> parameter takes an array of arrays where each inner array represents a taxonomy.<\/p>\n<p>Here, I am creating a dynamic <code>tax_query<\/code> based on the terms selected by the user. If the user selects terms from multiple taxonomies in the search filter, multiple taxonomy arrays are generated along with the <code>Relation<\/code> parameter set to &#8216;OR&#8217; in the outer array.<\/p>\n<div class=\"gist\" data-gist=\"fe66e8acfc5c05b6f7d8886b9add44fb\" data-gist-file=\"html-nds-advanced-search-results.php\"><a class=\"loading\" href=\"https:\/\/gist.github.com\/fe66e8acfc5c05b6f7d8886b9add44fb.js?file=html-nds-advanced-search-results.php\">Loading gist fe66e8acfc5c05b6f7d8886b9add44fb<\/a><div class=\"gist-consent-notice\" style=\"display:none\"><p>Please <a href=\"javascript:Cookiebot.renew()\">update your cookie preferences<\/a> to enable preference cookies to view this gist.<\/p><\/div><\/div>\n<p>Also, the <code>tax_query<\/code> is added to the <code>WP_Query<\/code> arguments only if the user selects a term in the search filter checkboxes.<\/p>\n<p>Similarly, let&#8217;s see how custom fields can be used as search filters to further refine the custom search.<\/p>\n<h3>Creating Search Filters Using Custom Metadata Created with Advanced Custom Fields<\/h3>\n<p>The custom fields for the Video post type that you saw above were created using the <a href=\"https:\/\/www.advancedcustomfields.com\/resources\/group\/\" rel=\"noopener\" target=\"_blank\">Group field type<\/a> provided with the premium version of ACF. To use the language and duration custom fields as dropdown lists, I first had to extract their unique values. To achieve this, I made use of the ACF function <code><a href=\"https:\/\/www.advancedcustomfields.com\/resources\/get_field_object\/\" rel=\"noopener\" target=\"_blank\">get_field_object()<\/a><\/code> which returns a unique array based on the type of custom field.<\/p>\n<div  class=\"wpdui-pic-regular  \">\n<figure class=\"wp-caption alignnone\" data-caption=\"true\"><img loading=\"lazy\" decoding=\"async\" class=\"attachment-600x600 size-600x600\" src=\"https:\/\/wpmudev.com\/blog\/wp-content\/uploads\/2018\/01\/get-custom-fields-from-acf-group-field-type.png\" alt=\"get-custom-fields-from-acf-group-field-type\" width=\"600\" height=\"461\" \/><figcaption class=\"wp-caption-text\">Inspecting the data object returned by the get_field_object() function of ACF<\/figcaption><\/figure>\n<\/div>\n<p>This is where a debugger would come in very handy as identifying the required field in large arrays may get tedious. Once I identified that the information I needed was available in the <code>choices<\/code> array, I could generate the markup for the HTML <code>select<\/code> element.<\/p>\n<div class=\"gist\" data-gist=\"42e915395dffcea55933655da861f331\" data-gist-file=\"html-nds-advanced-search-form.php\"><a class=\"loading\" href=\"https:\/\/gist.github.com\/42e915395dffcea55933655da861f331.js?file=html-nds-advanced-search-form.php\">Loading gist 42e915395dffcea55933655da861f331<\/a><div class=\"gist-consent-notice\" style=\"display:none\"><p>Please <a href=\"javascript:Cookiebot.renew()\">update your cookie preferences<\/a> to enable preference cookies to view this gist.<\/p><\/div><\/div>\n<p>The search filters for the date-range were created using <code><a href=\"https:\/\/jqueryui.com\/datepicker\/#date-range\" rel=\"noopener\" target=\"_blank\">jQuery UI Datepicker<\/a><\/code>. I won&#8217;t go into the details of this but you can refer the entire code for the <a href=\"https:\/\/github.com\/nuancedesignstudio\/nds-advanced-search-demo\/blob\/master\/common\/views\/html-nds-advanced-search-form.php\" rel=\"noopener\" target=\"_blank\">search filters here<\/a>.<\/p>\n<p>Querying posts using custom fields will require you to set the <code>meta_query<\/code> parameter of <code>WP_Query<\/code>.<\/p>\n<h3>Creating a Dynamic meta_query to Query Posts Using Custom Fields<\/h3>\n<p>Similar to <code>tax_query<\/code> the <code><a href=\"https:\/\/codex.wordpress.org\/Class_Reference\/WP_Query#Custom_Field_Parameters\" rel=\"noopener\" target=\"_blank\">meta_query<\/a><\/code> parameter can also take an array of arrays, where each inner array represents metadata in the form of key\/value pairs. Here, based on the type of custom field selected by the user, I am generating the inner <code>meta_query<\/code> array.<\/p>\n<div class=\"gist\" data-gist=\"19ab4bcf48e8aa44aaba35cc80fd0fb5\" data-gist-file=\"html-nds-advanced-search-results.php\"><a class=\"loading\" href=\"https:\/\/gist.github.com\/19ab4bcf48e8aa44aaba35cc80fd0fb5.js?file=html-nds-advanced-search-results.php\">Loading gist 19ab4bcf48e8aa44aaba35cc80fd0fb5<\/a><div class=\"gist-consent-notice\" style=\"display:none\"><p>Please <a href=\"javascript:Cookiebot.renew()\">update your cookie preferences<\/a> to enable preference cookies to view this gist.<\/p><\/div><\/div>\n<p>In the gist above, you&#8217;ll notice that I&#8217;m using the <code>compare 'BETWEEN'<\/code> operator to query posts between a date range. For this to work, the date needs to be stored in <code>YYYY-MM-DD<\/code>, and also requires converting the date returned by <code>jQuery UI Datepicker<\/code> into the same format for the comparison to work.<\/p>\n<p><b>Note:<\/b> In my example, as the taxonomies and custom fields are not shared across post types, only posts belonging to the Video post type will be returned if the search filters are used.<\/p>\n<h3>Adding Filter Buttons to Search Results<\/h3>\n<p>To improve the usability and user experience of search forms, features such as sorting and filter buttons are commonly employed in applications. I&#8217;d recommend you take a look at a popular library <code><a href=\"https:\/\/isotope.metafizzy.co\/\" rel=\"noopener\" target=\"_blank\">Isotope.js<\/a><\/code> that does the job of sorting and filtering the DOM really well.<\/p>\n<p>Here&#8217;s a crude example of using HTML buttons to further filter the displayed search results with jQuery.<\/p>\n<div  class=\"wpdui-pic-regular  \">\n<figure class=\"wp-caption alignnone\" data-caption=\"true\"><img loading=\"lazy\" decoding=\"async\" class=\"attachment-600x600 size-600x600\" src=\"https:\/\/wpmudev.com\/blog\/wp-content\/uploads\/2018\/01\/sub-filter-buttons-in-search-results.png\" alt=\"sub-filter-buttons-in-search-results\" width=\"526\" height=\"600\" \/><figcaption class=\"wp-caption-text\">Adding sub filter buttons to search results<\/figcaption><\/figure>\n<\/div>\n<p>To make this work, I took advantage of the <a href=\"https:\/\/codex.wordpress.org\/The_Loop\" rel=\"noopener\" target=\"_blank\">WordPress Loop<\/a> where I added HTML buttons based on the selected taxonomy terms, and also the taxonomy of each post to the <code>class<\/code> attribute of its <code>list item<\/code> container.<\/p>\n<div class=\"gist\" data-gist=\"fe39c11fea5da4380fa8278247069231\" data-gist-file=\"nds-advanced-search.js\"><a class=\"loading\" href=\"https:\/\/gist.github.com\/fe39c11fea5da4380fa8278247069231.js?file=nds-advanced-search.js\">Loading gist fe39c11fea5da4380fa8278247069231<\/a><div class=\"gist-consent-notice\" style=\"display:none\"><p>Please <a href=\"javascript:Cookiebot.renew()\">update your cookie preferences<\/a> to enable preference cookies to view this gist.<\/p><\/div><\/div>\n<p>The entire code for rendering the search results with filter buttons is <a href=\"https:\/\/github.com\/nuancedesignstudio\/nds-advanced-search-demo\/blob\/master\/common\/views\/html-nds-advanced-search-results.php\" rel=\"noopener\" target=\"_blank\">available here<\/a> for reference.<\/p>\n<h3>Bonus Code: Loading Search Results in a Modal Lightbox<\/h3>\n<p>My case study for the video search won&#8217;t be complete without a mechanism to load the videos returned in the search results. Here, I am using a jQuery library <code><a href=\"http:\/\/www.jacklmoore.com\/colorbox\/\" rel=\"noopener\" target=\"_blank\">Colorbox<\/a><\/code> to load the videos returned by the search in a <code><a href=\"https:\/\/www.w3schools.com\/howto\/howto_css_modals.asp\" rel=\"noopener\" target=\"_blank\">Modal<\/a><\/code> dialog box.<\/p>\n<div  class=\"wpdui-pic-regular  \">\n<figure class=\"wp-caption alignnone\" data-caption=\"true\"><img loading=\"lazy\" decoding=\"async\" class=\"attachment-600x600 size-600x600\" src=\"https:\/\/wpmudev.com\/blog\/wp-content\/uploads\/2018\/01\/loading-search-results-in-modal-dialog.png\" alt=\"load-search-results-in-modal-dialog\" width=\"600\" height=\"565\" \/><figcaption class=\"wp-caption-text\">Loading Videos from the search results in a Modal Dialog box with Colorbox<\/figcaption><\/figure>\n<\/div>\n<p>The URL and the credits of the video are extracted from the advanced custom fields that you saw above.<\/p>\n<div class=\"gist\" data-gist=\"9268620a803f9868e94916025756ee91\" data-gist-file=\"html-nds-advanced-search-results.php\"><a class=\"loading\" href=\"https:\/\/gist.github.com\/9268620a803f9868e94916025756ee91.js?file=html-nds-advanced-search-results.php\">Loading gist 9268620a803f9868e94916025756ee91<\/a><div class=\"gist-consent-notice\" style=\"display:none\"><p>Please <a href=\"javascript:Cookiebot.renew()\">update your cookie preferences<\/a> to enable preference cookies to view this gist.<\/p><\/div><\/div>\n<p>I&#8217;ve used HTML <code><a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Learn\/HTML\/Howto\/Use_data_attributes\" rel=\"noopener\" target=\"_blank\">data attributes<\/a><\/code> to display the credits in the modal, but with this approach, you can load pretty much anything you want.<\/p>\n<div class=\"gist\" data-gist=\"7f314fb6c6bc8ef1df1d255f8a7c0441\" data-gist-file=\"nds-advanced-search.js\"><a class=\"loading\" href=\"https:\/\/gist.github.com\/7f314fb6c6bc8ef1df1d255f8a7c0441.js?file=nds-advanced-search.js\">Loading gist 7f314fb6c6bc8ef1df1d255f8a7c0441<\/a><div class=\"gist-consent-notice\" style=\"display:none\"><p>Please <a href=\"javascript:Cookiebot.renew()\">update your cookie preferences<\/a> to enable preference cookies to view this gist.<\/p><\/div><\/div>\n<h2>Wrapping Up<\/h2>\n<p>We&#8217;ve covered a lot of ground here, and I hope you&#8217;ve found this article useful. Feel free to use my <a href=\"https:\/\/github.com\/nuancedesignstudio\/nds-advanced-search\" rel=\"noopener\" target=\"_blank\">search-plugin<\/a> and the <a href=\"https:\/\/github.com\/nuancedesignstudio\/nds-advanced-search-demo\" rel=\"noopener\" target=\"_blank\">case study example<\/a> to build your own awesome advanced search page.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Want to add advanced search functionality without depending on third-party plugins or themes? Build your own advanced WordPress Search Plugin with support for autosuggest, custom post types, taxonomies, custom fields, and caching from the ground up. In this article, I&#8217;ll show you how to build an object-oriented plugin to add a shortcode-based advanced search form [&hellip;]<\/p>\n","protected":false},"author":573954,"featured_media":170482,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"blog_reading_time":"","wds_primary_category":0,"wds_primary_tutorials_categories":0,"footnotes":""},"categories":[557,263],"tags":[10843,10844,10842],"tutorials_categories":[],"class_list":["post-169583","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-development","category-tutorials","tag-custom-taxonomy-search","tag-search-custom-post-types","tag-wordpress-advanced-search"],"_links":{"self":[{"href":"https:\/\/wpmudev.com\/blog\/wp-json\/wp\/v2\/posts\/169583","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/wpmudev.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/wpmudev.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/wpmudev.com\/blog\/wp-json\/wp\/v2\/users\/573954"}],"replies":[{"embeddable":true,"href":"https:\/\/wpmudev.com\/blog\/wp-json\/wp\/v2\/comments?post=169583"}],"version-history":[{"count":466,"href":"https:\/\/wpmudev.com\/blog\/wp-json\/wp\/v2\/posts\/169583\/revisions"}],"predecessor-version":[{"id":170484,"href":"https:\/\/wpmudev.com\/blog\/wp-json\/wp\/v2\/posts\/169583\/revisions\/170484"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/wpmudev.com\/blog\/wp-json\/wp\/v2\/media\/170482"}],"wp:attachment":[{"href":"https:\/\/wpmudev.com\/blog\/wp-json\/wp\/v2\/media?parent=169583"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/wpmudev.com\/blog\/wp-json\/wp\/v2\/categories?post=169583"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/wpmudev.com\/blog\/wp-json\/wp\/v2\/tags?post=169583"},{"taxonomy":"tutorials_categories","embeddable":true,"href":"https:\/\/wpmudev.com\/blog\/wp-json\/wp\/v2\/tutorials_categories?post=169583"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}