PluginProbe ʕ •ᴥ•ʔ
Spider Elements – Premium Elementor Widgets & Addons Library / 1.8.0
Spider Elements – Premium Elementor Widgets & Addons Library v1.8.0
trunk 1.0.0 1.1.0 1.5.0 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.7.0 1.8.0 1.9.0
spider-elements / includes / functions.php
spider-elements / includes Last commit date
Admin 5 months ago Frontend 5 months ago freemius 5 months ago filters.php 5 months ago functions.php 5 months ago
functions.php
774 lines
1 <?php
2 // Exit if accessed directly
3 if ( ! defined( 'ABSPATH' ) ) {
4 exit;
5 }
6
7 /**
8 * Check if the pro-plugin and plan is active.
9 *
10 * @return bool True if premium code can be used, false otherwise.
11 */
12 function spel_is_premium(): bool {
13 return spel_fs()->is_plan( 'pro' ) && spel_fs()->can_use_premium_code();
14 }
15
16 /**
17 * Check if the Docy theme is active
18 *
19 * @return bool
20 */
21 function spel_unlock_docy_theme(): bool {
22 $theme = wp_get_theme();
23 $theme_name = $theme->get( 'Name' );
24 $docy_themes = [ 'Docy', 'docy', 'Docy Child', 'docy-child' ];
25
26 return in_array( $theme_name, $docy_themes, true ) || spel_is_premium();
27 }
28
29 function spel_rtl(): string {
30 return is_rtl() ? 'true' : 'false';
31 }
32
33 /**
34 * Elementor is edit mode
35 *
36 * @return bool
37 */
38 function spider_elements_is_edit(): bool {
39 return \Elementor\Plugin::$instance->editor->is_edit_mode();
40 }
41
42 /**
43 * Elementor is preview mode
44 *
45 * @return bool
46 */
47 function spider_elements_is_preview(): bool {
48 return \Elementor\Plugin::$instance->preview->is_preview_mode();
49 }
50
51 /**
52 * Elementor Title tags
53 *
54 * @return array
55 */
56 if ( ! function_exists( 'spel_get_title_tags' ) ) {
57 function spel_get_title_tags(): array {
58 return [
59 'h1' => esc_html__( 'H1', 'spider-elements' ),
60 'h2' => esc_html__( 'H2', 'spider-elements' ),
61 'h3' => esc_html__( 'H3', 'spider-elements' ),
62 'h4' => esc_html__( 'H4', 'spider-elements' ),
63 'h5' => esc_html__( 'H5', 'spider-elements' ),
64 'h6' => esc_html__( 'H6', 'spider-elements' ),
65 'div' => esc_html__( 'Div', 'spider-elements' ),
66 'span' => esc_html__( 'Span', 'spider-elements' ),
67 'p' => esc_html__( 'Paragraph', 'spider-elements' ),
68 ];
69 }
70 }
71
72 /**
73 * Echo button link attributes.
74 *
75 * @param array $settings_key Settings array.
76 * @param bool $is_echo Whether to echo the attributes.
77 * @return void
78 */
79 if ( ! function_exists( 'spel_button_link' ) ) {
80 function spel_button_link( $settings_key, $is_echo = true ): void {
81 if ( $is_echo ) {
82 echo ! empty( $settings_key['url'] ) ? 'href="' . esc_url( $settings_key['url'] ) . '"' : '';
83 echo $settings_key['is_external'] ? ' target="_blank"' : '';
84 echo $settings_key['nofollow'] ? ' rel="nofollow"' : '';
85
86 if ( ! empty( $settings_key['custom_attributes'] ) ) {
87 $attrs = explode( ',', $settings_key['custom_attributes'] );
88
89 if ( is_array( $attrs ) ) {
90 foreach ( $attrs as $data ) {
91 $data_attrs = explode( '|', $data, 2 );
92 $attr_name = trim( $data_attrs[0] );
93 $attr_value = isset( $data_attrs[1] ) ? $data_attrs[1] : '';
94
95 // Security: Sanitize attribute name (allow alphanumeric, dashes, colons)
96 $attr_name = preg_replace( '/[^a-zA-Z0-9_\-:]/', '', $attr_name );
97
98 // Security: Prevent XSS by blocking event handlers (on*) and critical attributes
99 if ( preg_match( '/^(on|href|src|formaction)/i', $attr_name ) ) {
100 continue;
101 }
102
103 if ( ! empty( $attr_name ) ) {
104 echo ' ' . esc_attr( $attr_name ) . '="' . esc_attr( $attr_value ) . '"';
105 }
106 }
107 }
108 }
109 }
110 }
111 }
112
113 /**
114 * Category IDs
115 *
116 * @return array
117 * @since 1.0.0
118 */
119 if ( ! function_exists( 'spel_cat_ids' ) ) {
120 function spel_cat_ids() {
121 $taxonomys = get_terms( [
122 'taxonomy' => 'category',
123 'hide_empty' => true,
124 ] );
125 $taxonomy = [];
126 if ( is_array( $taxonomys ) ) {
127 foreach ( $taxonomys as $cat_id ) {
128 $taxonomy[ $cat_id->term_id ] = $cat_id->name;
129 }
130 }
131
132 return $taxonomy;
133 }
134 }
135
136 /**
137 * Day link to archive page
138 *
139 * @return void
140 **/
141 if ( ! function_exists( 'spel_day_link' ) ) {
142 function spel_day_link(): void {
143 $archive_year = get_the_time( 'Y' );
144 $archive_month = get_the_time( 'm' );
145 $archive_day = get_the_time( 'd' );
146 echo esc_url( get_day_link( $archive_year, $archive_month, $archive_day ) );
147 }
148 }
149
150 /**
151 * Retrieve the trimmed post title based on settings.
152 *
153 * This function fetches the current post title, applies a custom length
154 * from the provided settings (or falls back to a default length), and
155 * returns the trimmed version of the title. If no title exists, it will
156 * safely return an empty string.
157 *
158 * @param array $settings Settings array that may contain the title length.
159 * @param string $settings_key Array key used to find the title length inside settings.
160 * @param int $default Default title length if no value is found in settings. Default 10.
161 *
162 * @return string The trimmed post title, or empty string if no title exists.
163 * @since 1.0.0
164 */
165 function spel_get_title_length( array $settings, string $settings_key, int $default = 10 ): string {
166 $title = get_the_title();
167 $title_length = ! empty( $settings[ $settings_key ] ) ? (int) $settings[ $settings_key ] : $default;
168
169 return $title ? wp_trim_words( $title, $title_length, '' ) : '';
170 }
171
172
173 /**
174 * Post's excerpt text
175 *
176 * @param array $settings
177 * @param string $settings_key
178 * @param int $default
179 *
180 * @return string
181 * @since 1.0.0
182 */
183 if ( ! function_exists( 'spel_get_excerpt_length' ) ) {
184 function spel_get_excerpt_length( $settings, $settings_key, $default = 10 ): string {
185 $excerpt_length = ! empty( $settings[ $settings_key ] ) ? $settings[ $settings_key ] : $default;
186
187 return get_the_excerpt() ? wp_trim_words( get_the_excerpt(), $excerpt_length, '...' ) : wp_trim_words( get_the_content(), $excerpt_length, '...' );
188 }
189 }
190
191
192 /**
193 * Get the first category name
194 *
195 * @param string $term
196 *
197 * @return string
198 * @since 1.0.0
199 */
200 if ( ! function_exists( 'spel_get_first_taxonomy' ) ) {
201 function spel_get_first_taxonomy( $term = 'category' ): string {
202 $cats = get_the_terms( get_the_ID(), $term );
203 $cat = is_array( $cats ) ? $cats[0]->name : '';
204
205 return esc_html( $cat );
206 }
207 }
208
209
210 /**
211 * Get the first category link
212 *
213 * @param string $term
214 *
215 * @return string
216 * @since 1.0.0
217 */
218 if ( ! function_exists( 'spel_get_first_taxonomy_link' ) ) {
219 function spel_get_first_taxonomy_link( $term = 'category' ): string {
220
221 $cats = get_the_terms( get_the_ID(), $term );
222 $cat = is_array( $cats ) ? get_category_link( $cats[0]->term_id ) : '';
223
224 return esc_url( $cat );
225 }
226 }
227
228
229 /**
230 * Get categories array
231 *
232 * @param string $term
233 *
234 * @return array
235 * @since 1.0.0
236 */
237 if ( ! function_exists( 'spel_get_categories' ) ) {
238 function spel_get_categories( $term = 'category' ) {
239
240 $cats = get_terms( [
241 'taxonomy' => $term,
242 'hide_empty' => true
243 ] );
244
245 $cat_array = [];
246 $cat_array['all'] = esc_html__( 'All', 'spider-elements' );
247
248 if ( is_array( $cats ) ) {
249 foreach ( $cats as $cat ) {
250 $cat_array[ $cat->term_id ] = $cat->name;
251 }
252 }
253
254 return $cat_array;
255 }
256 }
257
258
259 /**
260 * Get a category list
261 *
262 * @return void
263 * @since 1.0.0
264 */
265 if ( ! function_exists( 'spel_get_post_category_list' ) ) {
266 function spel_get_post_category_list(): void {
267 $categories = get_categories();
268
269 if ( ! empty( $categories ) ) {
270 echo '<span class="blog-category">';
271
272 $category_names = [];
273
274 if ( is_array( $categories ) ) {
275 foreach ( $categories as $category ) {
276 $category_link = get_category_link( $category->term_id );
277 $category_names[] = '<a href="' . esc_url( $category_link ) . '">' . esc_html( $category->name ) . '</a>';
278 }
279 }
280
281 echo esc_html( implode( ', ', $category_names ) );
282
283 echo '</span>';
284 } else {
285 echo esc_html__( 'No categories found.', 'spider-elements' );
286 }
287 }
288 }
289
290
291 /**
292 * Get an author name array
293 *
294 * @return void
295 * @since 1.0.0
296 */
297 if ( ! function_exists( 'spel_get_post_author_name' ) ) {
298 function spel_get_post_author_name(): void {
299 global $post;
300 $byline = sprintf(
301 /* translators: %s: post author. */
302 esc_html_x( 'By: %s', 'post author', 'spider-elements' ),
303 '<span class="author"><a class="url fn n" href="' . esc_url( get_author_posts_url( $post->post_author ) ) . '">' . esc_html( get_the_author_meta(
304 'display_name',
305 $post->post_author
306 ) ) . '</a></span>'
307 );
308
309 echo wp_kses_post( $byline ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
310 }
311 }
312
313 /**
314 * Get Default Image Elementor
315 *
316 * @param array $settings_key
317 * @param string $alt
318 * @param string $class
319 * @param array $atts
320 * @return void
321 * @since 1.0.0
322 */
323 if ( ! function_exists( 'spel_el_image' ) ) {
324 function spel_el_image( $settings_key = [], $alt = '', $class = '', $atts = [] ): void {
325 if ( ! empty( $settings_key['id'] ) ) {
326 // WordPress handles escaping internally here
327 echo wp_get_attachment_image( $settings_key['id'], 'full', false, [ 'class' => esc_attr( $class ) ] );
328 } elseif ( ! empty( $settings_key['url'] ) && empty( $settings_key['id'] ) ) {
329 $class_attr = ! empty( $class ) ? ' class="' . esc_attr( $class ) . '"' : '';
330 $atts_str = '';
331
332 if ( ! empty( $atts ) ) {
333 foreach ( $atts as $k => $att ) {
334 // Security: Sanitize attribute name (allow alphanumeric, dashes, colons)
335 $k = preg_replace( '/[^a-zA-Z0-9_\-:]/', '', $k );
336
337 // Security: Prevent XSS by blocking event handlers (on*) and critical attributes
338 if ( empty( $k ) || preg_match( '/^(on|style|formaction|src|href)/i', $k ) ) {
339 continue;
340 }
341
342 $atts_str .= ' ' . esc_attr( $k ) . '="' . esc_attr( $att ) . '"';
343 }
344 }
345
346 printf(
347 '<img src="%1$s"%2$s alt="%3$s"%4$s />',
348 esc_url( $settings_key['url'] ),
349 // $class_attr contains safe HTML attribute string
350 $class_attr, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
351 esc_attr( $alt ),
352 // $atts_str contains safe HTML attribute string
353 $atts_str // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
354 );
355 }
356 }
357 }
358
359
360 /**
361 * Get Default Image Elementor Caption
362 *
363 * @param $image_id
364 *
365 * @return array
366 */
367 if ( ! function_exists( 'spel_el_image_caption' ) ) {
368 function spel_el_image_caption( $image_id = '' ): array {
369 $img_attachment = get_post( $image_id );
370
371 return [
372 'alt' => get_post_meta( $img_attachment->ID, '_wp_attachment_image_alt', true ),
373 'caption' => $img_attachment->post_excerpt,
374 'href' => get_permalink( $img_attachment->ID ),
375 'src' => $img_attachment->guid,
376 'title' => $img_attachment->post_title
377 ];
378 }
379 }
380
381
382 /**
383 * Filter text content to allow only safe HTML.
384 *
385 * @param string $content Text content to filter.
386 *
387 * @return string Filtered content containing only the allowed HTML.
388 * @since 1.0.0
389 */
390 if ( ! function_exists( 'spel_kses_post' ) ) {
391 function spel_kses_post( $content ): string {
392 $allowed_tag = [
393 'strong' => [],
394 'br' => [],
395 'p' => [
396 'class' => [],
397 'style' => [],
398 ],
399 'i' => [
400 'class' => [],
401 'style' => [],
402 ],
403 'ul' => [
404 'class' => [],
405 'style' => [],
406 ],
407 'li' => [
408 'class' => [],
409 'style' => [],
410 ],
411 'span' => [
412 'class' => [],
413 'style' => [],
414 ],
415 'a' => [
416 'href' => [],
417 'class' => [],
418 'title' => []
419 ],
420 'div' => [
421 'class' => [],
422 'style' => [],
423 ],
424 'h1' => [
425 'class' => [],
426 'style' => []
427 ],
428 'h2' => [
429 'class' => [],
430 'style' => []
431 ],
432 'h3' => [
433 'class' => [],
434 'style' => []
435 ],
436 'h4' => [
437 'class' => [],
438 'style' => []
439 ],
440 'h5' => [
441 'class' => [],
442 'style' => []
443 ],
444 'h6' => [
445 'class' => [],
446 'style' => []
447 ],
448 'img' => [
449 'class' => [],
450 'style' => [],
451 'height' => [],
452 'width' => [],
453 'src' => [],
454 'srcset' => [],
455 'alt' => [],
456 ],
457
458 ];
459
460 return wp_kses( $content, $allowed_tag );
461 }
462 }
463
464
465 /**
466 * Tab data
467 *
468 * @param array $getCats
469 * @param array $schedule_cats
470 *
471 * @return array
472 * @since 1.0.0
473 */
474 if ( ! function_exists( 'spel_get_tab_data' ) ) {
475 function spel_get_tab_data( $getCats, $schedule_cats ): array {
476 $tab_data = [];
477
478 foreach ( $getCats as $val ) {
479 $matching_data = [];
480
481 foreach ( $schedule_cats as $data ) {
482 if ( $val === $data['tab_title'] ) {
483 $matching_data[] = $data;
484 }
485 }
486
487 $tab_data[ $val ] = $matching_data;
488 }
489
490 return $tab_data;
491 }
492 }
493
494
495 /**
496 * Get reading time
497 *
498 * @param int $words_per_minute
499 *
500 * @return string
501 * @since 1.0.0
502 */
503 if ( ! function_exists( 'spel_get_reading_time' ) ) {
504 function spel_get_reading_time( $words_per_minute = 200 ): string {
505 $content = get_post_field( 'post_content', get_the_ID() );
506 $word_count = str_word_count( wp_strip_all_tags( $content ) );
507 $reading_time = ceil( $word_count / $words_per_minute );
508 $timer = _n( 'minute', 'minutes', $reading_time, 'spider-elements' );
509
510 return sprintf( '%d %s', $reading_time, $timer );
511 }
512 }
513
514 /**
515 * Render Dynamic Image
516 * @param $key
517 * @param $size
518 * @param $atts
519 * @return void
520 * @since 1.0.0
521 */
522 if ( ! function_exists( 'spel_dynamic_image' ) ) {
523 function spel_dynamic_image( $key, $size = 'full', $atts = [] ): void {
524 $image = wp_get_attachment_image( $key['id'], $size, '', $atts );
525 echo wp_kses( $image, [
526 'img' => [
527 'class' => [],
528 'style' => [],
529 'height' => [],
530 'width' => [],
531 'src' => [],
532 'srcset' => [],
533 'alt' => [],
534 ],
535 ] );
536 }
537 }
538
539
540 /**
541 * Retrieve a list of posts based on specified parameters.
542 *
543 * @param string $post_type The post-type to query.
544 * @param int $limit The maximum number of posts to retrieve.
545 * @param string $search The search term for post-titles.
546 *
547 * @return array An associative array with post-IDs as keys and post-titles as values.
548 * @since 1.0.0
549 */
550 if ( ! function_exists( 'spel_get_query_post_list' ) ) {
551 function spel_get_query_post_list( $post_type = 'any', $limit = -1, $search = '' ): array {
552 $args = [
553 'post_type' => $post_type,
554 'post_status' => 'publish',
555 'posts_per_page' => $limit,
556 's' => $search, // Search term
557 ];
558
559 $query = new WP_Query( $args );
560
561 $data = [];
562 if ( $query->have_posts() ) {
563 while ( $query->have_posts() ) {
564 $query->the_post();
565 $data[ get_the_ID() ] = get_the_title();
566 }
567 }
568
569 wp_reset_postdata(); // Reset post data after custom query
570
571 return $data;
572 }
573 }
574
575
576 /**
577 * Get all elementor page templates
578 *
579 * @param string|null $type
580 *
581 * @return array
582 * @since 1.0.0
583 */
584 if ( ! function_exists( 'spel_get_el_templates' ) ) {
585 function spel_get_el_templates( $type = null ): array {
586 $options = [];
587
588 if ( $type ) {
589
590 $args = [
591 'post_type' => 'elementor_library',
592 'posts_per_page' => -1,
593 ];
594
595 $args['tax_query'] = [
596 [
597 'taxonomy' => 'elementor_library_type',
598 'field' => 'slug',
599 'terms' => $type,
600 ],
601 ];
602
603 $page_templates = get_posts( $args );
604
605 if ( ! empty( $page_templates ) && ! is_wp_error( $page_templates ) ) {
606 foreach ( $page_templates as $post ) {
607 $options[ $post->ID ] = $post->post_title;
608 }
609 }
610 } else {
611 $options = spel_get_query_post_list( 'elementor_library' );
612 }
613
614 return $options;
615 }
616 }
617
618
619 /**
620 * Get information about the server environment.
621 *
622 * @return array Server environment information.
623 * @since 1.0.0
624 */
625 if ( ! function_exists( 'spel_get_environment_info' ) ) {
626 function spel_get_environment_info(): array {
627
628 // Figure out cURL version, if installed.
629 $curl_version = '';
630 if ( function_exists( 'curl_version' ) ) {
631 $curl_version = curl_version();
632 $curl_version = $curl_version['version'] . ', ' . $curl_version['ssl_version'];
633 }
634
635 // WP memory limit.
636 $wp_memory_limit = spel_readable_number( WP_MEMORY_LIMIT );
637 if ( function_exists( 'memory_get_usage' ) ) {
638 $wp_memory_limit = max( $wp_memory_limit, spel_readable_number( @ini_get( 'memory_limit' ) ) );
639 }
640
641 return [
642 'home_url' => get_option( 'home' ),
643 'site_url' => get_option( 'siteurl' ),
644 'version' => SPEL_VERSION,
645 'wp_version' => get_bloginfo( 'version' ),
646 'wp_multisite' => is_multisite(),
647 'wp_memory_limit' => $wp_memory_limit,
648 'wp_debug_mode' => ( defined( 'WP_DEBUG' ) && WP_DEBUG ),
649 'wp_cron' => ! ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ),
650 'language' => get_locale(),
651 'external_object_cache' => wp_using_ext_object_cache(),
652 'server_info' => isset( $_SERVER['SERVER_SOFTWARE'] ) ? wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) : '',
653 'php_version' => phpversion(),
654 'php_post_max_size' => spel_readable_number( ini_get( 'post_max_size' ) ),
655 'php_max_execution_time' => ini_get( 'max_execution_time' ),
656 'php_max_input_vars' => ini_get( 'max_input_vars' ),
657 'curl_version' => $curl_version,
658 'suhosin_installed' => extension_loaded( 'suhosin' ),
659 'max_upload_size' => wp_max_upload_size(),
660 'default_timezone' => date_default_timezone_get(),
661 'fsockopen_or_curl_enabled' => ( function_exists( 'fsockopen' ) || function_exists( 'curl_init' ) ),
662 'soapclient_enabled' => class_exists( 'SoapClient' ),
663 'domdocument_enabled' => class_exists( 'DOMDocument' ),
664 'gzip_enabled' => is_callable( 'gzopen' ),
665 'mbstring_enabled' => extension_loaded( 'mbstring' ),
666 ];
667
668 }
669 }
670
671
672 /**
673 * Convert a human-readable file size into bytes.
674 *
675 * @param string $size The size string (e.g., "1M", "2G", "500 K").
676 * @return int The equivalent size in bytes.
677 */
678 if ( ! function_exists( 'spel_readable_number' ) ) {
679 function spel_readable_number( $size ): int {
680
681 // Get the last character of the size string
682 $suffix = substr( $size, -1 );
683
684 // Remove the last character from the size string
685 $value = substr( $size, 0, -1 );
686
687 // Convert suffix to lowercase for case-insensitive comparison
688 $suffix = strtolower( $suffix );
689
690 $multipliers = [
691 'p' => 1024,
692 't' => 1024,
693 'g' => 1024,
694 'm' => 1024,
695 'k' => 1024,
696 ];
697
698 // Check if the suffix is a valid multiplier
699 if ( array_key_exists( $suffix, $multipliers ) ) {
700 $value *= $multipliers[ $suffix ];
701 }
702
703 // Return the result
704 return (int) $value;
705
706 }
707 }
708
709
710 if ( ! function_exists( 'spel_pagination' ) ) {
711 /**
712 * Pagination
713 *
714 * @param WP_Query $query
715 * @param string $class
716 * @param string $prev
717 * @param string $next
718 * @return void
719 */
720 function spel_pagination( $query, $class = 'spel-pagination', $prev = '', $next = '' ): void {
721
722 if ( $query->max_num_pages <= 1 ) {
723 return; // No pagination needed if only one page
724 }
725
726 $default_prev = '<img src="' . esc_url( SPEL_IMG . '/icons/prev.svg' ) . '" alt="' . esc_attr__( 'arrow-left', 'spider-elements' ) . '" class="me-2" />' . esc_html__( 'Prev', 'spider-elements' );
727 $default_next = esc_html__( 'Next', 'spider-elements' ) . '<img src="' . esc_url( SPEL_IMG . '/icons/next.svg' ) . '" alt="' . esc_attr__( 'arrow-right', 'spider-elements' ) . '" class="ms-2" />';
728
729 $prev_text = ! empty( $prev ) ? $prev : $default_prev;
730 $next_text = ! empty( $next ) ? $next : $default_next;
731
732 echo '<ul class="' . esc_attr( $class ) . '">';
733
734 $big = 999999999; // need an unlikely integer
735 $current = max( 1, get_query_var( 'paged' ) ? get_query_var( 'paged' ) : ( get_query_var( 'page' ) ? get_query_var( 'page' ) : 1 ) );
736
737 echo wp_kses_post(
738 paginate_links( [
739 'base' => str_replace( $big, '%#%', esc_url( get_pagenum_link( $big ) ) ),
740 'format' => '?paged=%#%',
741 'current' => $current,
742 'total' => $query->max_num_pages,
743 'prev_text' => $prev_text,
744 'next_text' => $next_text,
745 ] )
746 );
747
748
749 echo '</ul>';
750 }
751 }
752
753 /**
754 * Jobus pagination (Deprecated)
755 *
756 * @param WP_Query $query
757 * @return void
758 * @since 1.0.0
759 * @deprecated 1.8.0 No longer needed as we rely on native query handling.
760 */
761 if ( ! function_exists( 'spel_archive_query' ) ) {
762 /**
763 * Archive Query
764 *
765 * @param WP_Query $query
766 * @return void
767 */
768 function spel_archive_query( $query ): void {
769 // Optimization: Removed unbounded query override to prevent performance issues on archive pages
770 }
771
772 add_action( 'pre_get_posts', 'spel_archive_query' );
773 }
774