Initial commit to github

This commit is contained in:
Keith Solomon
2025-08-22 15:40:01 -05:00
commit e8efdbeb34
230 changed files with 32213 additions and 0 deletions

251
lib/activation.php Normal file
View File

@@ -0,0 +1,251 @@
<?php
/*
* On Theme Activation adds Home and News pages, sets up reading options for front page and posts page, and erases sample page and post
*/
// phpcs:ignore
if ( isset( $_GET['activated'] ) && is_admin() ) {
// Set Blog Description to nothing
update_option( 'blogdescription', '' );
// List of pages to create with nested structure
$arrPages = array(
'Home' => array(),
'News' => array(),
'Page Not Found (Error 404)' => array(),
'Contact Us' => array(),
/**
* Sample nested structure
*
* 'Parent Page' => array(
* 'Subpage 1' => array(
* 'Sub-subpage 1',
* 'Sub-subpage 2'
* ),
* 'Subpage 2.2' => array()
* ),
*/
);
foreach ( $arrPages as $pageTitle => $childPages ) {
// phpcs:ignore
$pageExists = get_page_by_title($pageTitle);
if ( ! $pageExists ) {
// Create the parent page
$pageId = wp_insert_post(
array(
'post_title' => $pageTitle,
'post_content' => '',
'post_type' => 'page',
'post_status' => 'publish',
)
);
// If there are child pages, create them under the parent page
foreach ( $childPages as $childPageTitle => $subChildPages ) {
if ( is_array( $subChildPages ) ) {
$subpageId = wp_insert_post(
array(
'post_title' => $childPageTitle,
'post_content' => '',
'post_type' => 'page',
'post_status' => 'publish',
'post_parent' => $pageId,
)
);
foreach ( $subChildPages as $subChildPageTitle ) {
wp_insert_post(
array(
'post_title' => $subChildPageTitle,
'post_content' => '',
'post_type' => 'page',
'post_status' => 'publish',
'post_parent' => $subpageId,
)
);
}
} else {
wp_insert_post(
array(
'post_title' => $childPageTitle,
'post_content' => '',
'post_type' => 'page',
'post_status' => 'publish',
'post_parent' => $pageId,
)
);
}
}
}
}
// Use a static front page
// phpcs:ignore
$home = get_page_by_title('Home');
update_option( 'page_on_front', $home->ID );
update_option( 'show_on_front', 'page' );
// Set the blog/news page
// phpcs:ignore
$news = get_page_by_title('News');
update_option( 'page_for_posts', $news->ID );
// Trash the samples
wp_delete_post( 1, true );
wp_delete_post( 2, true );
// Flush rewrite rules to ensure new pages are recognized
flush_rewrite_rules();
/**
* Install and activate must-use plugins
*/
$muPlugins = array(
array(
'url' => 'https://docs.vincentdevelopment.ca/files/advanced-custom-fields-pro.zip',
'active' => true,
),
array(
'url' => 'https://docs.vincentdevelopment.ca/files/gravity-forms.zip',
'active' => true,
),
array(
'url' => 'https://updraftplus.com/wp-content/uploads/updraftplus.zip',
'active' => false,
),
array(
'url' => 'https://downloads.wordpress.org/plugin/simple-history.5.11.0.zip',
'active' => true,
),
array(
'url' => 'https://downloads.wordpress.org/plugin/autodescription.5.1.2.zip',
'active' => true,
),
array(
'url' => 'https://downloads.wordpress.org/plugin/better-search-replace.1.4.10.zip',
'active' => true,
),
array(
'url' => 'https://downloads.wordpress.org/plugin/google-site-kit.1.153.0.zip',
'active' => false,
),
);
// Custom log file
$logFile = WP_CONTENT_DIR . '/mu-plugin-install.log';
/**
* Simple logging function.
*
* @param string $message The message to log.
*/
function log_message( $message ) {
global $logFile;
$timestamp = gmdate( 'Y-m-d H:i:s' );
// phpcs:ignore
file_put_contents( $logFile, "[$timestamp] $message\n", FILE_APPEND );
}
// Include necessary WordPress files
require_once ABSPATH . 'wp-admin/includes/file.php';
require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
require_once ABSPATH . 'wp-admin/includes/plugin.php';
// Force direct filesystem access
add_filter( 'filesystem_method', fn() => 'direct' );
global $wp_filesystem;
if ( ! WP_Filesystem() ) {
log_message( 'Filesystem initialization failed.' );
return;
}
// Define a silent skin to avoid show_message() errors
// phpcs:disable
class Silent_Upgrader_Skin extends WP_Upgrader_Skin {
public function feedback( $string, ...$args ) {}
public function header() {}
public function footer() {}
public function error( $errors ) {
log_message( 'Upgrader error: ' . print_r( $errors, true ) );
}
public function before() {}
public function after() {}
}
// phpcs:enable
$skin = new Silent_Upgrader_Skin();
$upgrader = new Plugin_Upgrader( $skin );
// Process each plugin
foreach ( $muPlugins as $plug ) {
$plugUrl = $plug['url'];
$shouldActivate = ! empty( $plug['active'] );
// Download plugin
$response = wp_remote_get( $plugUrl );
if ( is_wp_error( $response ) ) {
log_message( "Failed to download plugin from {$plugUrl}: " . $response->get_error_message() );
continue;
}
// Extract filename from URL
$plugFileName = basename( wp_parse_url( $plugUrl, PHP_URL_PATH ) );
// Save the plugin zip
$plugZip = wp_upload_bits( $plugFileName, null, wp_remote_retrieve_body( $response ) );
if ( $plugZip['error'] ) {
log_message( "Failed to save plugin zip {$plugFileName}: " . $plugZip['error'] );
continue;
}
// Install the plugin
$installResult = $upgrader->install( $plugZip['file'] );
if ( is_wp_error( $installResult ) ) {
log_message( "Failed to install plugin {$plugFileName}: " . $installResult->get_error_message() );
// Cleanup temp zip
if ( file_exists( $plugZip['file'] ) ) {
wp_delete_file( $plugZip['file'] );
}
continue;
}
// Get plugin info ( folder/main file )
$plugInfo = $upgrader->plugin_info();
log_message( "plugin_info for {$plugFileName}: {$plugInfo}" );
if ( ! $plugInfo || ! file_exists( WP_PLUGIN_DIR . '/' . $plugInfo ) ) {
log_message( "Could not determine installed plugin file for {$plugFileName}." );
// Cleanup temp zip
if ( file_exists( $plugZip['file'] ) ) {
wp_delete_file( $plugZip['file'] );
}
continue;
}
log_message( "Successfully installed plugin {$plugInfo}." );
// Attempt activation if marked active
if ( $shouldActivate ) {
$activateResult = activate_plugin( $plugInfo );
if ( is_wp_error( $activateResult ) ) {
log_message( "Failed to activate plugin {$plugInfo}: " . $activateResult->get_error_message() );
} else {
log_message( "Successfully activated plugin {$plugInfo}." );
}
} else {
log_message( "Plugin {$plugInfo} installed but not activated ( per configuration )." );
}
// Cleanup temp zip
if ( file_exists( $plugZip['file'] ) ) {
wp_delete_file( $plugZip['file'] );
log_message( "Deleted temporary zip file {$plugZip['file']}." );
}
}
log_message( '=== Plugin installation and activation process completed ===' );
}

59
lib/class-acf.php Normal file
View File

@@ -0,0 +1,59 @@
<?php
/**
* ACF (Advanced Custom Fields) support class & functions
*
* @package BasicWP
* @since 1.0.0
*/
namespace BasicWP;
/**
* Class ACF
*
* This class serves as a wrapper or utility for handling Advanced Custom Fields (ACF) functionality.
* It is part of the Basic-WP project and is located in the `lib` directory.
*
* @package Basic-WP
*/
class ACF {
/**
* Variable to hold the file path.
*
* @var string $path The file path associated with the class.
*/
public $path;
/** Constructor.
*
* This constructor initializes the class by setting the file path and
* adding filters for loading and saving JSON files related to ACF.
*/
public function __construct() {
$this->path = get_stylesheet_directory() . '/acf';
add_filter( 'acf/settings/load_json', array( $this, 'loadJson' ) );
add_filter( 'acf/settings/save_json', array( $this, 'saveJson' ) );
}
/** Save JSON.
*
* @param mixed $path The path to save the JSON file.
*/
// phpcs:ignore
public function saveJson( $path ) {
return $this->path;
}
/** Load JSON.
*
* @param mixed $paths The paths to load the JSON file.
*/
// phpcs:ignore
public function loadJson( $paths ) {
return array( $this->path );
}
}
if ( function_exists( 'get_fields' ) ) {
$acfInstance = new ACF();
}

396
lib/class-breadcrumbs.php Normal file
View File

@@ -0,0 +1,396 @@
<?php
namespace BasicWP;
/**
* Class Breadcrumbs
*
* This class is responsible for generating and managing breadcrumb navigation
* for the WordPress theme. Breadcrumbs provide a navigational aid that helps
* users understand their location within the website's hierarchy.
*
* @package Basic-WP
* @subpackage Breadcrumbs
*/
class Breadcrumbs {
/**
* Generates the breadcrumb navigation for the website.
*
* This method is responsible for creating and returning the breadcrumb
* trail, which helps users navigate the website by showing their current
* location within the site's hierarchy.
*
* @return array An array of breadcrumb items, where each item is typically an
* associative array containing 'title' and 'url' keys.
*/
public static function generate() {
global $post;
$breadcrumbs = array();
// Always start with Home
$breadcrumbs[] = self::getHomeBreadcrumb();
if ( is_front_page() ) {
// Front Page - do nothing
return $breadcrumbs;
} elseif ( is_home() ) {
// Blog Index - add the Blog Posts Index page title
$breadcrumbs[] = self::getBlogPostsIndexBreadcrumb();
} elseif ( is_singular( 'post' ) ) {
// Single Post - add the category and post title
$breadcrumbs = array_merge( $breadcrumbs, self::getSinglePostBreadcrumbs() );
} elseif ( is_singular() && ! is_page() ) {
// Single Custom Post Type - add the post type archive and post title
$breadcrumbs = array_merge( $breadcrumbs, self::getCustomPostTypeBreadcrumbs() );
} elseif ( is_page() ) {
// Static Page - add the parent pages if any, and the page title
$breadcrumbs = array_merge( $breadcrumbs, self::getStaticPageBreadcrumbs( $post ) );
} elseif ( is_category() || is_tag() || is_tax() ) {
// Taxonomy Archive - add the taxonomy term name
$breadcrumbs[] = self::getTaxonomyArchiveBreadcrumb();
} elseif ( is_post_type_archive() ) {
// Post Type Archive - add the post type name
$breadcrumbs[] = self::getPostTypeArchiveBreadcrumb();
} elseif ( is_day() ) {
// Daily Archive - add the month and day links
$breadcrumbs = array_merge( $breadcrumbs, self::getDateArchiveBreadcrumbs() );
} elseif ( is_month() ) {
// Monthly Archive - add the month link
$breadcrumbs[] = self::getMonthArchiveBreadcrumb();
} elseif ( is_year() ) {
// Yearly Archive - add the year link
$breadcrumbs[] = self::getYearArchiveBreadcrumb();
} elseif ( is_author() ) {
// Author Archive - add the author name
$breadcrumbs[] = self::getAuthorArchiveBreadcrumb();
} elseif ( is_search() ) {
// Search Results - add the search query
$breadcrumbs[] = self::getSearchBreadcrumb();
} elseif ( is_404() ) {
// 404 Not Found - add a 404 message
$breadcrumbs[] = self::get404Breadcrumb();
}
return $breadcrumbs;
}
/** Generates the breadcrumb for the home page.
*
* This method is responsible for creating the breadcrumb
* link or label that represents the home page in the breadcrumb
* navigation trail.
*
* @return string The HTML or text representation of the home breadcrumb.
*/
private static function getHomeBreadcrumb() {
return array(
'url' => home_url(),
'label' => 'Home',
);
}
/** Generates the breadcrumb for the blog posts index page.
*
* This method is responsible for creating a breadcrumb entry
* that represents the blog posts index page in the breadcrumb trail.
*
* @return array An associative array representing the breadcrumb for the blog posts index page.
*/
private static function getBlogPostsIndexBreadcrumb() {
return array( 'label' => get_the_title( get_option( 'page_for_posts' ) ) );
}
/** Generates the breadcrumb trail for a single post.
*
* This method is responsible for creating the breadcrumb navigation
* specific to single post views. It typically includes links to the
* homepage, category (or categories) the post belongs to, and the
* post title itself.
*
* @return array An array representing the breadcrumb trail, where each
* element is a breadcrumb item (e.g., URL and label).
*/
private static function getSinglePostBreadcrumbs() {
$breadcrumbs = array();
$categories = get_the_category();
$breadcrumbs[] = array(
'url' => get_permalink( get_option( 'page_for_posts' ) ),
'label' => get_the_title( get_option( 'page_for_posts' ) ),
);
if ( ! empty( $categories ) ) {
$cat = $categories[0];
$breadcrumbs[] = array(
'url' => get_category_link( $cat ),
'label' => $cat->name,
);
}
$breadcrumbs[] = array( 'label' => get_the_title() );
return $breadcrumbs;
}
/** Generates breadcrumbs for custom post types.
*
* This method is responsible for creating breadcrumb navigation
* specific to custom post types. It retrieves and formats the
* breadcrumb trail based on the custom post type's hierarchy
* and structure.
*
* @return array An array representing the breadcrumb trail for the custom post type.
*/
private static function getCustomPostTypeBreadcrumbs() {
$breadcrumbs = array();
$postType = get_post_type_object( get_post_type() );
if ( $postType && ! in_array( get_post_type(), array( 'post', 'page' ), true ) ) {
$breadcrumbs[] = array(
'url' => get_post_type_archive_link( $postType->name ),
'label' => $postType->labels->name,
);
}
$breadcrumbs[] = array( 'label' => get_the_title() );
return $breadcrumbs;
}
/** Generates breadcrumbs for a static page.
*
* This method is responsible for creating breadcrumb navigation
* for static pages in a WordPress theme. It utilizes the provided
* post object to determine the breadcrumb structure.
*
* @param WP_Post $post The WordPress post object representing the static page.
* @return array An array of breadcrumb items, where each item is typically
* an associative array containing 'title' and 'url' keys.
*/
private static function getStaticPageBreadcrumbs( $post ) {
$breadcrumbs = array();
if ( $post->post_parent ) {
$ancestors = array_reverse( get_post_ancestors( $post->ID ) );
foreach ( $ancestors as $ancestor ) {
$breadcrumbs[] = array(
'url' => get_permalink( $ancestor ),
'label' => get_the_title( $ancestor ),
);
}
}
$breadcrumbs[] = array( 'label' => get_the_title() );
return $breadcrumbs;
}
/** Generates a breadcrumb for taxonomy archive pages.
*
* This method is responsible for creating breadcrumb navigation
* specific to taxonomy archive pages, providing users with a clear
* path to navigate back to the taxonomy or related sections.
*
* @return array An array representing the breadcrumb trail for the taxonomy archive.
*/
private static function getTaxonomyArchiveBreadcrumb() {
$term = get_queried_object();
return $term && isset( $term->name ) ? array( 'label' => $term->name ) : array();
}
/** Generates a breadcrumb for the archive page of a custom post type.
*
* This method is responsible for creating a breadcrumb link that points
* to the archive page of a specific custom post type. It is typically used
* in breadcrumb navigation to provide users with a way to navigate back
* to the archive page of the post type they are currently viewing.
*
* @return array An array containing the breadcrumb data for the custom post type archive.
*/
private static function getPostTypeArchiveBreadcrumb() {
$postType = get_post_type_object( get_post_type() );
return $postType ? array( 'label' => $postType->labels->name ) : array();
}
/** Generates breadcrumbs for date-based archives.
*
* This method is responsible for creating breadcrumb navigation
* for WordPress date archive pages, such as year, month, or day archives.
*
* @return array An array representing the breadcrumb trail for the date archive.
*/
private static function getDateArchiveBreadcrumbs() {
return array(
array(
'url' => get_month_link( get_query_var( 'year' ), get_query_var( 'monthnum' ) ),
'label' => get_the_date( 'F Y' ),
),
array( 'label' => get_the_date() ),
);
}
/** Generates a breadcrumb for a monthly archive page.
*
* This method is responsible for creating a breadcrumb
* specific to WordPress monthly archive pages. It typically
* includes the year and month as part of the breadcrumb trail.
*
* @return string The HTML markup for the monthly archive breadcrumb.
*/
private static function getMonthArchiveBreadcrumb() {
return array( 'label' => get_the_date( 'F Y' ) );
}
/** Generates a breadcrumb for the year-based archive page.
*
* This method is responsible for creating a breadcrumb link
* specific to the year archive in a WordPress site. It is typically
* used to provide navigation for users when they are viewing posts
* filtered by a specific year.
*
* @return string The HTML markup for the year archive breadcrumb.
*/
private static function getYearArchiveBreadcrumb() {
return array( 'label' => get_the_date( 'Y' ) );
}
/** Generates the breadcrumb for the author archive page.
*
* This method is responsible for creating a breadcrumb trail
* specific to the author archive, providing navigation context
* for pages that display posts by a particular author.
*
* @return string The HTML markup for the author archive breadcrumb.
*/
private static function getAuthorArchiveBreadcrumb() {
$author = get_queried_object();
return array( 'label' => 'Author: ' . $author->display_name );
}
/** Generates the breadcrumb for search results.
*
* This method is responsible for creating a breadcrumb trail
* specific to search result pages. It helps users navigate back
* to the search context or other parts of the site.
*
* @return string The HTML markup for the search breadcrumb.
*/
private static function getSearchBreadcrumb() {
return array( 'label' => 'Search: ' . get_search_query() );
}
/** Generates the breadcrumb trail for a 404 error page.
*
* This method is responsible for creating a breadcrumb structure
* specifically for 404 error pages, providing users with a navigational
* context when a requested page is not found.
*
* @return array An array representing the breadcrumb trail for the 404 page.
*/
private static function get404Breadcrumb() {
return array( 'label' => '404 Not Found' );
}
/** Renders the breadcrumb navigation.
*
* This method generates and outputs the breadcrumb trail for the current page.
*
* @return void
*/
public static function render() {
$items = self::generate();
$metadata = array(
'@context' => 'https://schema.org',
'@type' => 'BreadcrumbList',
'itemListElement' => array(),
);
ob_start();
?>
<nav aria-label="Breadcrumbs" class="flex items-center py-2 -mx-2 leading-none" vocab="https://schema.org/" typeof="BreadcrumbList">
<?php foreach ( $items as $index => $item ) : ?>
<?php
$ldItem = array(
'@type' => 'ListItem',
'position' => $index + 1,
'name' => htmlspecialchars( $item['label'], ENT_QUOTES, 'UTF-8' ),
);
$isActive = ( $index === count( $items ) - 1 );
$activeClass = $isActive ? ' active' : '';
?>
<span class="p-2 breadcrumb-item<?php echo esc_attr( $activeClass ); ?>"
<?php
if ( $isActive ) :
?>
aria-current="page"<?php endif; ?>>
<?php if ( ! empty( $item['url'] ) ) : ?>
<a href="<?php echo esc_url( $item['url'] ); ?>">
<?php if ( $index === 0 ) : ?>
<svg class="flex-shrink-0 w-5 h-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" aria-label="Home">
<path d="M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" />
</svg>
<span class="sr-only"><?php echo esc_attr( $item['label'] ); ?></span>
<?php else : ?>
<?php echo esc_attr( $item['label'] ); ?>
<?php endif; ?>
</a>
<?php $ldItem['item'] = $item['url']; ?>
<?php else : ?>
<?php echo esc_attr( $item['label'] ); ?>
<?php endif; ?>
</span>
<?php $metadata['itemListElement'][] = $ldItem; ?>
<?php if ( ! $isActive ) : ?>
<svg class="flex-shrink-0 w-5 h-5 text-light" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
</svg>
<?php endif; ?>
<?php endforeach; ?>
</nav>
<script type="application/ld+json"><?php echo esc_js( wp_json_encode( $metadata ) ); ?></script>
<?php
echo wp_kses(
ob_get_clean(),
array(
'a' => array(
'class' => array(),
'href' => array(),
'title' => array(),
'rel' => array(),
'target' => array(),
'aria-label' => array(),
'aria-current' => array(),
),
'nav' => array(
'aria-label' => array(),
'class' => array(),
'vocab' => array(),
'typeof' => array(),
),
'span' => array(
'class' => array(),
'aria-current' => array(),
),
'svg' => array(
'class' => array(),
'xmlns' => array(),
'viewBox' => array(),
'fill' => array(),
'aria-hidden' => array(),
'aria-label' => array(),
),
'path' => array(
'd' => array(),
'fill-rule' => array(),
'clip-rule' => array(),
'fill' => array(),
'xmlns:xlink' => array(),
),
'script' => array(
'type' => array(),
'src' => array(),
),
)
);
}
}

100
lib/class-enqueue.php Normal file
View File

@@ -0,0 +1,100 @@
<?php
/**
* BasicWP Theme Enqueue Class
*
* @package BasicWP
* @since 1.0.0
*/
namespace BasicWP;
/**
* Class Enqueue
*
* Handles the enqueueing of scripts and styles in a WordPress theme or plugin.
*
* @package Basic-WP
* @since 1.0.0
*/
class Enqueue {
/**
* Initialize hooks to enqueue scripts and styles.
*/
public function __construct() {
add_action( 'wp_enqueue_scripts', array( $this, 'enqFEAssets' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'enqBEAssets' ) );
}
/**
* Enqueue frontend CSS and JS files.
*/
public function enqFEAssets() {
$theme_dir = get_stylesheet_directory();
$theme_uri = get_stylesheet_directory_uri();
/**
* CSS
*/
$css_path = '/static/dist/theme.css';
if ( file_exists( $theme_dir . $css_path ) ) {
$version = filemtime( $theme_dir . $css_path );
wp_enqueue_style( 'basicwp-theme', $theme_uri . $css_path, array(), $version );
}
$font_ver = gmdate( 'U' );
wp_enqueue_style( 'raleway', 'https://fonts.googleapis.com/css2?family=Raleway:wght@100..900&display=swap', false, $font_ver );
/**
* JS
*/
$js_path = '/static/js/theme.js';
if ( file_exists( $theme_dir . $js_path ) ) {
$version = filemtime( $theme_dir . $js_path );
wp_enqueue_script_module( 'basicwp-theme', $theme_uri . $js_path, array( 'jquery' ), $version, true );
wp_enqueue_script_module( 'basicwp-button', $theme_uri . '/static/js/components/button.js', array( 'basicwp-theme' ), $version, true );
}
}
/**
* Enqueue backend (admin/editor) CSS and JS files.
*/
public function enqBEAssets() {
$theme_dir = get_stylesheet_directory();
$theme_uri = get_stylesheet_directory_uri();
/**
* Editor CSS
*/
$editor_css_path = '/styles/backend/editor.css';
if ( file_exists( $theme_dir . $editor_css_path ) ) {
$version = filemtime( $theme_dir . $editor_css_path );
wp_enqueue_style( 'basicwp-editor', $theme_uri . $editor_css_path, array(), $version );
}
$font_ver = gmdate( 'U' );
wp_enqueue_style( 'raleway', 'https://fonts.googleapis.com/css2?family=Raleway:wght@100..900&display=swap', false, $font_ver );
/**
* Admin CSS
*/
$admin_css_path = '/styles/backend/admin.css';
if ( file_exists( $theme_dir . $admin_css_path ) ) {
$version = filemtime( $theme_dir . $admin_css_path );
wp_enqueue_style( 'basicwp-admin', $theme_uri . $admin_css_path, array(), $version );
}
/**
* Admin JS
*/
$admin_js_path = '/static/js/admin.js';
if ( file_exists( $theme_dir . $admin_js_path ) ) {
$version = filemtime( $theme_dir . $admin_js_path );
wp_enqueue_script_module( 'basicwp-admin', $theme_uri . $admin_js_path, array( 'jquery' ), $version, true );
wp_enqueue_script_module( 'basicwp-button', $theme_uri . '/static/js/components/button.js', array( 'basicwp-admin' ), $version, true );
}
}
}
// Initialize the Enqueue class.
$enqueue = new Enqueue();

185
lib/class-menuitems.php Normal file
View File

@@ -0,0 +1,185 @@
<?php
/**
* BasicWP MenuItems Class
*
* @package BasicWP
* @since 1.0.0
*/
namespace BasicWP;
/**
* Class MenuItems
*
* This class is responsible for managing menu items within the Basic-WP theme.
*
* @package Basic-WP
* @since 1.0.0
*/
class MenuItems {
/**
* Accepts menu name to populate navigation items.
*
* @var string $displayLocation
*/
protected $displayLocation;
/**
* First level menu items (menu_item_parent = 0).
*
* @var array $topLevelNavItems
*/
public $topLevelNavItems;
/**
* True if nav item has children items.
*
* @var bool $hasChildren
*/
public $hasChildren;
/**
* Children navigation items.
*
* @var array $nestedNavItems
*/
public $nestedNavItems;
/**
* True if the current page is the same as the menu item.
*
* @var bool $currentPage
*/
public $currentPage;
/**
* Constructor method for initializing the class with a specific menu name.
*
* @param string $menuName The name of the menu to be used. Defaults to 'main_navigation'.
*/
public function __construct( $menuName = 'main_navigation' ) {
$this->displayLocation = $menuName;
$this->topLevelNavItems = $this->getTopLevelNavItems();
$this->hasChildren = function ( $item ) {
return $this->hasChildren( $item );
};
$this->nestedNavItems = function ( $parentItem ) {
return $this->getNestedNavItems( $parentItem );
};
$this->currentPage = function ( $item ) {
return $this->currentPage( $item );
};
}
/**
* Retrieves the list of menus.
*
* @return array An array containing menu items.
*/
public function getMenus() {
$menus = get_nav_menu_locations();
return empty( $menus ) ? array() : $menus;
}
/**
* Retrieves the navigation items.
*
* @return array An array of navigation items.
*/
public function getNavItems() {
$locations = $this->getMenus();
// If the menu location doesn't exist, return empty array
if ( ! isset( $locations[ $this->displayLocation ] ) ) {
return array();
}
// Get the menu ID for this location
$menuId = $locations[ $this->displayLocation ];
// Direct call to wp_get_nav_menu_items with the menu ID
$items = wp_get_nav_menu_items( $menuId );
// Return empty array if no items
return is_array( $items ) ? $items : array();
}
/**
* Checks if the given parent item has child items.
*
* @param mixed $parentItem The parent item to check for children.
* @return bool Returns true if the parent item has children, false otherwise.
*/
public function hasChildren( $parentItem ) {
foreach ( $this->getNavItems() as $item ) {
if ( $item->menu_item_parent === strval( $parentItem->ID ) ) {
return true;
}
}
return false;
}
/**
* Retrieves the top-level navigation items.
*
* @return array An array of top-level navigation items.
*/
public function getTopLevelNavItems() {
$items = array();
foreach ( $this->getNavItems() as $item ) {
if ( $item->menu_item_parent === '0' ) {
array_push( $items, $item );
}
}
return $items;
}
/**
* Retrieves the nested navigation items for a given parent item.
*
* @param mixed $parentItem The parent navigation item for which nested items are to be retrieved.
* @return array The list of nested navigation items.
*/
public function getNestedNavItems( $parentItem ) {
$childrenItems = array();
foreach ( $this->getNavItems() as $item ) {
if ( $item->menu_item_parent === strval( $parentItem->ID ) ) {
array_push( $childrenItems, $item );
}
}
return $childrenItems;
}
/**
* Determines the current page based on the provided item.
*
* @param mixed $item The item used to determine the current page.
* @return mixed The current page information.
*/
public function currentPage( $item ) {
global $wp;
return ( $item->url === home_url( $wp->request ) . '/' ) ? 'true' : 'false';
}
/**
* Renders the output for the current context.
*
* This method is responsible for generating and returning the
* appropriate output for the current context or request.
*
* @return void
*/
public function render() {
global $views;
// Extract class properties to local variables
$location = $this->displayLocation;
$topLevelNavItems = $this->topLevelNavItems;
$hasChildren = $this->hasChildren;
$nestedNavItems = $this->nestedNavItems;
$currentPage = $this->currentPage;
include $views . '/components/menu-items/index.php';
}
}

129
lib/class-resources.php Normal file
View File

@@ -0,0 +1,129 @@
<?php
/**
* Resources custom post type & taxonomies
*
* @package BasicWP
* @since 1.0.0
*/
namespace BasicWP;
/**
* Class Resources
*
* This class is responsible for setting up the Resources post type and taxonomies.
*
* @package Basic-WP
*/
class Resources {
/**
* Constructor for the class.
*
* Initializes the class and sets up any necessary properties or methods.
*/
public function __construct() {
add_action( 'init', array( $this, 'registerPostType' ) );
add_action( 'init', array( $this, 'registerTaxonomy' ) );
add_filter( 'post_type_link', array( $this, 'postTypeLink' ), 10, 2 );
}
/**
* Registers a custom post type.
*
* This method is responsible for defining and registering a custom post type
* with WordPress. It should include all necessary arguments and labels
* required for the post type to function correctly.
*
* @return void
*/
public function registerPostType() {
register_post_type(
'resources',
array(
'labels' => array(
'name' => 'Resources',
'singular_name' => 'Resource',
'menu_name' => 'Resources',
'name_admin_bar' => 'Resource',
'add_new' => 'Add New Resource',
'add_new_item' => 'Add New Resource',
'edit_item' => 'Edit Resource',
'new_item' => 'New Resource',
'view_item' => 'View Resource',
'search_items' => 'Search Resources',
'not_found' => 'No resources found',
'not_found_in_trash' => 'No resources found in Trash',
),
'public' => true,
'has_archive' => true,
'rewrite' => array( 'slug' => 'resources' ),
'supports' => array( 'title', 'editor', 'excerpt', 'thumbnail', 'revisions', 'custom-fields' ),
'menu_position' => 20,
'menu_icon' => 'dashicons-hammer',
'show_in_rest' => true,
)
);
}
/**
* Registers a custom taxonomy.
*
* This method is responsible for defining and registering a custom taxonomy
* within the WordPress environment. It should include the necessary arguments
* and settings for the taxonomy to function as intended.
*
* @return void
*/
public function registerTaxonomy() {
register_taxonomy(
'resource_type',
array( 'resources' ),
array(
'labels' => array(
'name' => 'Resource Types',
'singular_name' => 'Resource Type',
'search_items' => 'Search Resource Types',
'all_items' => 'All Resource Types',
'parent_item' => 'Parent Resource Type',
'parent_item_colon' => 'Parent Resource Type:',
'edit_item' => 'Edit Resource Type',
'update_item' => 'Update Resource Type',
'add_new_item' => 'Add New Resource Type',
'new_item_name' => 'New Resource Type Name',
'menu_name' => 'Resource Types',
),
'public' => true,
'hierarchical' => true,
'show_admin_column' => true,
'rewrite' => array(
'slug' => 'resources',
'with_front' => false,
'hierarchical' => true,
),
'show_in_rest' => true,
)
);
}
/**
* Filters the permalink for a post of a specific post type.
*
* @param string $post_link The post's permalink.
* @param WP_Post $post The post object.
* @return string The filtered post permalink.
*/
public function postTypeLink( $post_link, $post ) {
if ( 'resources' === $post->post_type ) {
$terms = get_the_terms( $post->ID, 'resource_type' );
if ( $terms && ! is_wp_error( $terms ) ) {
$term_slug = $terms[0]->slug;
return home_url( "resources/{$term_slug}/{$post->post_name}" );
}
}
return $post_link;
}
}
new Resources();

260
lib/extras.php Normal file
View File

@@ -0,0 +1,260 @@
<?php
/**
* Filters. etc
*
* @package BasicWP
* @since 1.0.0
*/
namespace BasicWP;
/** Get child pages of the current page, sorted by menu order.
*
* @return array Array of child page objects, with URL added to each.
*/
function getChildrenPages() {
$children = get_pages(
array(
'child_of' => get_the_ID(),
'sort_order' => 'ASC',
'sort_column' => 'menu_order',
)
);
foreach ( $children as &$child ) {
$child->url = get_page_link( $child->ID );
}
return $children;
}
// Modify which pages should render the sidebar.
add_filter(
'hasSidebar',
function ( $has_sidebar ) {
// Add post types that should never have a sidebar.
if ( is_page() && ! get_field( 'has_sidebar' ) ) {
return false;
}
return $has_sidebar;
}
);
/** Helper to check whether or not the sidebar should be rendered
* (to add/remove a sidebar from a page, edit the filter instead
* of modifying this function).
*/
function hasSidebar() {
return apply_filters( 'hasSidebar', true );
}
// Add extra body classes here.
add_filter(
'body_class',
function ( $classes ) {
if ( hasSidebar() ) {
$classes = array_merge( $classes, array( 'has-sidebar' ) );
}
return $classes;
}
);
/**
* Checks if the page should render a page header.
*
* @return bool true if page header should be rendered, false otherwise
*/
function hasPageHeader() {
global $post;
if ( get_field( 'hero_style' ) !== 'none' ) {
return false;
}
return true;
}
/** Create the Owner role.
*
* This function creates a new role named "Owner" with all the capabilities of the
* Administrator role, except for the following:
*
* - activate_plugins
* - delete_plugins
* - edit_plugins
* - install_plugins
* - update_plugins
* - switch_themes
* - edit_themes
* - delete_themes
* - install_themes
* - update_themes
* - update_core
* - manage_options
*
* This role is meant to be used by a person who should have almost all the same
* capabilities as an Administrator, but should not have the ability to update
* the WordPress core software, manage plugins or themes, or edit other site
* options.
*
* @return void
*/
function createOwnerRole() {
// First, remove the role if it exists.
remove_role( 'owner' );
// Get the administrator role.
$admin_role = get_role( 'administrator' );
$admin_capabilities = $admin_role->capabilities;
// Remove specific capabilities.
$capabilities_to_remove = array(
'activate_plugins',
'delete_plugins',
'edit_plugins',
'install_plugins',
'update_plugins',
'switch_themes',
'edit_themes',
'delete_themes',
'install_themes',
'update_themes',
'update_core',
'manage_options',
);
foreach ( $capabilities_to_remove as $capability ) {
unset( $admin_capabilities[ $capability ] );
}
// Add the Owner role with the modified capabilities.
add_role( 'owner', 'Owner', $admin_capabilities );
}
add_action( 'init', __NAMESPACE__ . '\\createOwnerRole' );
/** Retrieves the appropriate title for the current page context.
*
* The function determines the type of page being viewed and returns
* the corresponding title. It handles different page types, including
* the home page, single posts, archives, search results, and 404 pages.
* If none of these conditions apply, it defaults to fetching the page's title.
*
* @return string The title relevant to the current page context.
*/
function getTheTitle() {
$title = '';
if ( is_home() || is_single() ) {
$title = get_the_title( get_option( 'page_for_posts', true ) );
} elseif ( is_archive() ) {
$title = get_the_archive_title();
} elseif ( is_search() ) {
$title = sprintf(
/* translators: %s is replaced with the search query */
__( 'Search Results for "%s"', 'basicwp' ),
get_search_query()
);
} elseif ( is_404() ) {
$title = 'Page Not Found (error 404)';
} else {
$title = get_the_title();
}
return $title;
}
/** Wraps iframes and embed elements in a div with a specific class.
*
* This function searches for iframe and embed elements within the provided
* content and wraps each found element in a div with the class "embed".
* It is useful for applying consistent styling or responsive behavior
* to embedded media elements.
*
* @param string $content The HTML content containing iframes or embeds.
* @return string The modified content with wrapped iframes and embeds.
*/
function divWrapper( $content ) {
// match any iframes.
$pattern = '~<iframe.*</iframe>|<embed.*</embed>~';
preg_match_all( $pattern, $content, $matches );
foreach ( $matches[0] as $match ) {
// wrap matched iframe with div.
$wrappedframe = '<div class="embed">' . $match . '</div>';
// replace original iframe with new in content.
$content = str_replace( $match, $wrappedframe, $content );
}
return $content;
}
add_filter( 'the_content', __NAMESPACE__ . '\\divWrapper' );
/** Selectively add sidebar to page.
*
* This function adds a custom field group to the WordPress editor for
* pages, allowing users to specify whether a page should have a sidebar.
* Default is no sidebar.
*
* @return void
*/
add_action(
'acf/include_fields',
function () {
if ( ! function_exists( 'acf_add_local_field_group' ) ) {
return;
}
acf_add_local_field_group(
array(
'key' => 'group_6817d79573087',
'title' => 'Page Sidebar',
'fields' => array(
array(
'key' => 'field_6817d7954a168',
'label' => '',
'name' => 'has_sidebar',
'aria-label' => '',
'type' => 'true_false',
'instructions' => '',
'required' => 0,
'conditional_logic' => 0,
'wrapper' => array(
'width' => '',
'class' => '',
'id' => '',
),
'message' => 'Should this page have a sidebar?',
'default_value' => 0,
'allow_in_bindings' => 0,
'ui' => 0,
'ui_on_text' => '',
'ui_off_text' => '',
),
),
'location' => array(
array(
array(
'param' => 'post_type',
'operator' => '==',
'value' => 'page',
),
),
),
'menu_order' => 0,
'position' => 'side',
'style' => 'default',
'label_placement' => 'top',
'instruction_placement' => 'label',
'hide_on_screen' => '',
'active' => true,
'description' => '',
'show_in_rest' => 0,
)
);
}
);

155
lib/helpers.php Normal file
View File

@@ -0,0 +1,155 @@
<?php
/**
* BasicWP Theme Helpers
*
* @package BasicWP
* @since 1.0.0
*/
namespace BasicWP;
// Define global variables for theme and views folder paths.
global $theme, $views;
$theme = get_template_directory();
$views = $theme . '/views';
/** Retrieves a nested value from an ACF field.
*
* @param string $field_path The dot-notated path to the value. For example, 'contact_info.phone'.
*
* @return mixed The value at the specified path.
*/
function getFieldValue( $field_path ) {
$parts = explode( '.', $field_path );
$field = get_field( array_shift( $parts ), 'option' );
foreach ( $parts as $part ) {
$field = $field[ $part ] ?? '';
}
return $field;
}
// Add Global Fields options page.
if ( function_exists( 'acf_add_options_page' ) ) {
add_action(
'init',
function () {
acf_add_options_page(
array(
'page_title' => 'Global Fields',
'menu_title' => 'Global Fields',
'menu_slug' => 'global-fields',
'icon_url' => 'dashicons-admin-site',
)
);
}
);
}
/** Customizes the order of the admin menu items in WordPress.
*
* This function modifies the default menu order in the WordPress admin dashboard
* by specifying a custom sequence for menu items, separators, and additional
* options. If the menu order is not specified, it returns true to allow the
* default order to be used.
*
* @param bool $menu_ord Indicates whether the menu order has been specified.
*
* @return array|bool An array specifying the custom menu order, or true if the
* menu order is not specified.
*/
function customMenuOrder( $menu_ord ) {
if ( ! $menu_ord ) {
return true;
}
return array(
'index.php', // Dashboard.
'acf-options-global-fields', // Global Theme Fields.
'edit.php?post_type=acf-field-group', // ACF Field Groups.
'separator1', // First separator.
'edit.php', // Posts.
'edit.php?post_type=page', // Pages.
'edit.php?post_type=resources', // Resources.
'upload.php', // Media.
'separator2', // Second separator.
'edit.php?post_type=page-template', // Page Templates.
'edit.php?post_type=wp_block', // Reusable Blocks.
'edit.php?post_type=block-pattern', // Block Patterns.
'edit.php?post_type=element', // Elements.
'separator3', // Third separator.
'link-manager.php', // Links.
'edit-comments.php', // Comments.
'gf_edit_forms', // Gravity Forms.
'themes.php', // Appearance.
'plugins.php', // Plugins.
'separator-last', // Last separator.
'users.php', // Users.
'tools.php', // Tools.
'options-general.php', // Settings.
);
}
add_filter( 'custom_menu_order', __NAMESPACE__ . '\\customMenuOrder', 10, 1 );
add_filter( 'menu_order', __NAMESPACE__ . '\\customMenuOrder', 10, 1 );
/** Add custom block category for our blocks
*
* @param array $categories The existing block categories.
* @return array
*/
function blockCategories( $categories ) {
$vdi_cat = array(
'slug' => 'vdi-blocks',
'title' => 'VDI Custom Blocks',
'icon' => 'dashicons-admin-customizer',
);
array_unshift( $categories, $vdi_cat );
return $categories;
}
add_filter( 'block_categories_all', __NAMESPACE__ . '\\blockCategories', 10, 2 );
/**
* Creates a escaping function to allowed certain HTML for embed content.
* Needed for when echoing the innerblock HTML.
*
* @return array An array of HTML elements allowed.
*/
function escEmbeds() {
/**
* Return the allowed html
* These are the elements in the rendered embed block for youtube and vimeo videos.
* Therefore we need to allow these to keep the same structure.
*/
return array(
'iframe' => array(
'role' => true, // Add role="presentation" to iframes.
'presentation' => true, // Add role="presentation" to iframes.
'src' => true,
'height' => true,
'width' => true,
'frameborder' => true,
'allowfullscreen' => true,
),
'figure' => array(
'class' => true,
),
'div' => array(
'class' => true,
),
);
}
/** Print a variable to the console for debugging purposes.
*
* @param mixed $data The data to print to the console.
*/
function consoleLog( $data ) {
echo '<script>';
echo 'console.log(' . wp_json_encode( $data ) . ')';
echo '</script>';
}

251
lib/hooks.php Normal file
View File

@@ -0,0 +1,251 @@
<?php
/**
* BasicWP Theme Hooks
*
* @package BasicWP
* @since 1.0.0
*/
namespace BasicWP;
/**
* Add preconnect for Google fonts to head
*
* @return void
*/
add_action(
'wp_head',
function () {
?>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<?php
},
0
);
/**
* Register the navigation menus.
*
* @link https://developer.wordpress.org/reference/functions/register_nav_menus/
*/
register_nav_menus(
array(
'main_navigation' => 'Main Navigation',
'aux_navigation' => 'Auxiliary Navigation',
'footer_navigation' => 'Footer Navigation',
)
);
/**
* Widget Areas
*
* Set up sidebar/widget areas for the theme.
*
* @return void
*/
add_action(
'widgets_init',
function () {
$config = array(
'before_widget' => '<div class="widget %1$s %2$s">',
'after_widget' => '</div>',
'before_title' => '<h2>',
'after_title' => '</h2>',
);
$cfg_foot = array(
'before_widget' => '<div class="widget %1$s %2$s">',
'after_widget' => '</div>',
'before_title' => '<h4>',
'after_title' => '</h4>',
);
register_sidebar(
array(
'name' => 'Primary Sidebar',
'id' => 'sidebar-primary',
) + $config
);
register_sidebar(
array(
'name' => 'Page Sidebar',
'id' => 'sidebar-page',
) + $config
);
register_sidebar(
array(
'name' => 'Footer Area 1',
'id' => 'footer-1',
) + $cfg_foot
);
register_sidebar(
array(
'name' => 'Footer Area 2',
'id' => 'footer-2',
) + $cfg_foot
);
register_sidebar(
array(
'name' => 'Footer Area 3',
'id' => 'footer-3',
) + $cfg_foot
);
}
);
/**
* Basic SEO
*
* {Site URL}: {Title}
*/
add_filter(
'wp_title',
function ( $title ) {
$site_name = get_bloginfo( 'name' );
return "{$site_name}: {$title}";
}
);
/**
* Excerpt
*/
add_filter(
'excerpt_more',
function () {
return '&hellip;';
}
);
/**
* Whether the page hero should hold the main h1 of the page.
*/
add_filter(
'include_page_title_in_hero',
function ( $include_title ) {
// Add post types that should not use the title in the hero.
if ( is_singular( ( 'post' ) ) ) {
return false;
}
return $include_title;
}
);
/**
* WP Cleanup
*/
function init() {
remove_action( 'wp_head', 'print_emoji_detection_script', 7 );
remove_action( 'wp_print_styles', 'print_emoji_styles' );
remove_action( 'admin_print_styles', 'print_emoji_styles' );
remove_action( 'admin_print_scripts', 'print_emoji_detection_script' );
remove_filter( 'the_content_feed', 'wp_staticize_emoji' );
remove_filter( 'comment_text_rss', 'wp_staticize_emoji' );
remove_filter( 'wp_mail', 'wp_staticize_emoji_for_email' );
wp_dequeue_style( 'wp-block-library' ); // Core block styles.
wp_dequeue_style( 'wp-block-library-theme' ); // Block theme styles.
wp_dequeue_style( 'global-styles' ); // Global styles.
wp_dequeue_style( 'core-block-supports' ); // Core block supports.
wp_dequeue_style( 'core-block-styles' ); // Core block styles.
remove_action( 'wp_enqueue_scripts', 'wp_enqueue_global_styles', 1 );
remove_action( 'wp_enqueue_scripts', 'wp_enqueue_classic_theme_styles', 1 );
remove_action( 'wp_head', 'wp_print_styles', 8 );
remove_action( 'wp_head', 'wp_print_head_scripts', 9 );
remove_action( 'wp_head', 'wp_generator' ); // WordPress version.
remove_action( 'wp_head', 'rsd_link' ); // RSD link.
remove_action( 'wp_head', 'wlwmanifest_link' ); // Windows Live Writer.
remove_action( 'wp_head', 'wp_shortlink_wp_head' ); // Shortlink.
remove_action( 'wp_head', 'rest_output_link_wp_head' ); // REST API link.
remove_action( 'wp_head', 'wp_oembed_add_discovery_links' ); // oEmbed discovery links.
remove_action( 'wp_head', 'rel_canonical' ); // Canonical URL.
remove_action( 'wp_head', 'wp_resource_hints', 2 ); // DNS Prefetch.
add_filter( 'wp_img_tag_add_width_and_height_attr', '__return_false' ); // Disable intrinsic image size.
add_filter( 'wp_img_tag_add_auto_sizes', '__return_false' ); // Disable auto sizes.
add_filter( 'xmlrpc_enabled', '__return_false' );
}
add_action( 'init', __NAMESPACE__ . '\\init', 1 );
/**
* Allow SVG uploads
*/
add_filter(
'wp_check_filetype_and_ext',
function ( $data, $file, $filename, $mimes ) {
global $wp_version;
if ( '4.7.1' !== $wp_version ) {
return $data;
}
$filetype = wp_check_filetype( $filename, $mimes );
return array(
'ext' => $filetype['ext'],
'type' => $filetype['type'],
'proper_filename' => $data['proper_filename'],
);
},
10,
4
);
add_filter(
'upload_mimes',
function ( $mimes ) {
$mimes['svg'] = 'image/svg+xml';
return $mimes;
}
);
/**
* Fix display issues with SVGs in admin
*/
add_action(
'admin_head',
function () {
echo '<style type="text/css">
.attachment-266x266, .thumbnail img {
width: 100% !important;
height: auto !important;
}
</style>';
}
);
/**
* Filters the email address used as the sender in outgoing emails.
*
* This function allows you to modify the default "from" email address
* used by WordPress when sending emails.
*
* @param string $old The original email address.
* @return string The new email address to use as the sender.
*/
// phpcs:ignore
function new_mail_from( $old ) {
return get_option( 'admin_email' );
}
/**
* Filters the name used as the sender in outgoing emails.
*
* This function allows you to modify the default "from" name
* used by WordPress when sending emails.
*
* @param string $old The original name.
* @return string The new name to use as the sender.
*/
// phpcs:ignore
function new_mail_from_name( $old ) {
return get_option( 'blogname' );
}
add_filter( 'wp_mail_from', __NAMESPACE__ . '\\new_mail_from' );
add_filter( 'wp_mail_from_name', __NAMESPACE__ . '\\new_mail_from_name' );

122
lib/search-features.php Normal file
View File

@@ -0,0 +1,122 @@
<?php
/**
* Search features for BasicWP theme.
*
* @package BasicWP
* @since 1.0.0
*/
namespace BasicWP;
/**
* Modifies the WordPress query object for page search functionality.
*
* @param WP_Query $query The WordPress query object.
*
* @return void
*/
function pageSearch( $query ) {
if ( ! is_admin() && $query->is_main_query() && $query->is_search ) {
$query->set( 'paged', ( get_query_var( 'paged' ) ) ? get_query_var( 'paged' ) : 1 );
$query->set( 'posts_per_page', -1 );
}
}
add_action( 'pre_get_posts', __NAMESPACE__ . '\\pageSearch' );
/**
* Sort WP_Post objects in reverse order (most recent first).
*
* @param string $key WP_Post object property used for sorting. 'post_date' is assumed.
*
* @return int
*/
function postSort( $key ) {
return function ( $a, $b ) use ( $key ) {
if ( $a->$key < $b->$key ) {
return 1;
} elseif ( $a->$key > $b->$key ) {
return -1;
} else {
// If first comparison is equal, use title as secondary sort key.
return strnatcasecmp( $a->post_title, $b->post_title );
}
};
}
/**
* Remove duplicate posts in combined list from default and secondary queries.
*
* @param array $posts Array of WP_Post objects.
* @param string $key Search key used to identify duplicate objects. Post ID is used.
*
* @return array
*/
function dedupe( $posts, $key ) {
$unique_posts = array();
$ids = array();
foreach ( $posts as $post ) {
if ( ! in_array( $post->$key, $ids, true ) ) {
$ids[] = $post->$key;
$unique_posts[] = $post;
}
}
return $unique_posts;
}
/**
* Posts_results filter hook callback.
*
* @param array $posts Array of WP_Post objects.
* @param object $query WP_Query object from default search.
*
* @return array
*/
function searchResultFilter( $posts, $query ) {
if ( \is_search() && $query->is_search() && ! \is_admin() ) {
$args = array(
'post_type' => array( 'post', 'page' ),
'posts_per_page' => -1,
'tax_query' => array(
'relation' => 'OR', // Include both tags and categories.
array(
'taxonomy' => 'post_tag',
'field' => 'name',
'terms' => $query->get( 's' ),
),
array(
'taxonomy' => 'category',
'field' => 'name',
'terms' => $query->get( 's' ),
),
),
);
// Remove callback to avoid infinite loop.
remove_filter( 'posts_results', __NAMESPACE__ . '\\searchResultFilter', 10 );
$secondary_query = new \WP_Query( $args );
$tagged_posts = $secondary_query->get_posts();
// Combine default search results with secondary query.
$all_posts = array_merge( $posts, $tagged_posts );
// Remove duplicate posts.
$unique_posts = dedupe( $all_posts, 'ID' );
// Sort by reverse post_date order (most recent first).
usort( $unique_posts, postSort( 'post_date' ) );
// Restore posts_results callback.
add_filter( 'posts_results', __NAMESPACE__ . '\\searchResultFilter', 10, 2 );
return $unique_posts;
} else {
return $posts;
}
}
add_filter( 'posts_results', __NAMESPACE__ . '\\searchResultFilter', 10, 2 );