Query Custom Post Types in WordPress: Guide

Query Custom Post Types in WordPress: Guide

Easily fetch and display custom post type data in WordPress using the powerful WP_Query class. This guide covers:

  • Understanding WP_Query parameters to query single or multiple custom post types
  • Querying custom post types with taxonomy terms
  • Advanced techniques like ordering, pagination, and meta queries
  • Displaying custom post type data with template tags and structured formatting
  • Code examples for common use cases
Key Point Details
Querying Basics Set 'post_type' => 'custom_post_type' in WP_Query arguments
Multiple Post Types Pass array of slugs: 'post_type' => array('type1', 'type2')
Taxonomy Queries Use 'tax_query' parameter to filter by taxonomy terms
Displaying Data Use template tags like the_title(), the_content() in the loop
Structuring Data Organize with headings, lists, tables based on custom fields

Start querying and displaying your custom content types in WordPress with this comprehensive guide.

Getting Started

To follow this guide, you’ll need:

  • A WordPress website with a custom post type already set up
  • Basic knowledge of WordPress and PHP

If you’re new to custom post types, check out the resources at the end of this article to learn how to create and register them.

Before querying custom post types, make sure you understand the WP_Query class, which allows you to fetch and display WordPress content.

Prerequisites

Requirement Description
WordPress Website You need a WordPress site with a custom post type registered.
WordPress Knowledge Basic familiarity with WordPress is necessary.
PHP Knowledge Some understanding of PHP is required.
WP_Query Familiarity You should have a basic grasp of the WP_Query class.

With these prerequisites met, you’re ready to learn how to use WP_Query to fetch and display custom post types in WordPress.

Understanding WP_Query

WP_Query

What is WP_Query?

WP_Query is a PHP class that lets you fetch and display posts, pages, and custom post types from the WordPress database. It’s the core tool for retrieving content in WordPress.

Using WP_Query

With WP_Query, you can specify various parameters to customize your queries and get the exact content you need. Here are some common parameters:

Parameter Description
post_type Specifies the type of content to retrieve (e.g., post, page, custom_post_type).
posts_per_page Limits the number of posts displayed per page.
orderby Sorts the retrieved posts (e.g., date, title, comment_count, rand).
category_name Retrieves posts from a specific category by providing the category slug.
author_name Retrieves posts from a specific author by providing the author’s username.

By combining these parameters, you can create complex queries to fetch specific content from the WordPress database.

In the next section, we’ll explore how to use WP_Query to query a single custom post type.

Querying a Single Custom Post Type

To display content from a single custom post type on your WordPress site, you need to use the WP_Query class. Here’s how to do it:

Set the ‘post_type’ Parameter

First, you’ll need to specify the custom post type you want to query. This is done by setting the post_type parameter in the WP_Query arguments. For example, if your custom post type is called podcast, you would set post_type to podcast.

Display Query Results

After setting the post_type parameter, you can loop through the query results using a while loop. Inside the loop, you can use template tags like the_title() and the_content() to display the post title and content.

Example Code

Here’s an example code snippet that queries a custom post type called podcast and displays the post title and content:

$args = array(     'post_type' => 'podcast',     'posts_per_page' => -1 // Retrieve all posts ); $query = new WP_Query($args);  if ($query->have_posts()) {     while ($query->have_posts()) {         $query->the_post();         echo '<h2>' . get_the_title() . '</h2>';         echo '<p>' . get_the_content() . '</p>';     }     wp_reset_postdata(); // Reset post data } else {     echo 'No podcasts found!'; } 

In this example:

  1. We create an array $args with the post_type set to podcast and posts_per_page set to -1 to retrieve all posts.
  2. We create a new WP_Query object with the $args array.
  3. We check if the query has any posts using $query->have_posts().
  4. If there are posts, we loop through them using a while loop.
  5. Inside the loop, we use $query->the_post() to set up the post data for the current post.
  6. We then display the post title using get_the_title() and the post content using get_the_content().
  7. After the loop, we call wp_reset_postdata() to reset the post data.
  8. If there are no posts, we display a message "No podcasts found!".

This code will display all posts from the podcast custom post type on your WordPress site.

Querying Multiple Custom Post Types

Sometimes, you may need to display content from multiple custom post types on your WordPress site. For example, you might want to show a mixed feed of posts from different custom post types on your homepage or create a custom archive page that lists posts from various custom post types.

Using an Array for the ‘post_type’ Parameter

To query multiple custom post types, you can pass an array of custom post type slugs to the post_type parameter. This allows you to retrieve posts from multiple custom post types in a single query.

For instance, if you have two custom post types, testimonial and casestudy, you can query them both using the following code:

$args = array(     'post_type' => array('testimonial', 'casestudy'),     'posts_per_page' => -1 // Retrieve all posts ); $query = new WP_Query($args); 

In this example, we create an array $args with the post_type parameter set to an array of custom post type slugs (testimonial and casestudy). We then create a new WP_Query object with the $args array.

Code Example

Here’s an example code snippet that demonstrates querying multiple custom post types:

$args = array(     'post_type' => array('testimonial', 'casestudy'),     'posts_per_page' => -1 // Retrieve all posts ); $query = new WP_Query($args);  if ($query->have_posts()) {     while ($query->have_posts()) {         $query->the_post();         echo '<h2>'. get_the_title(). '</h2>';         echo '<p>'. get_the_content(). '</p>';     }     wp_reset_postdata(); // Reset post data } else {     echo 'No posts found!'; } 

In this example, we query both testimonial and casestudy custom post types and display their titles and content using a while loop.

Common Use Cases

Querying multiple custom post types is useful in various scenarios, such as:

Use Case Description
Mixed Feed Displaying a mixed feed of posts from different custom post types on the homepage.
Custom Archive Page Creating a custom archive page that lists posts from multiple custom post types.
Custom Search Results Building a custom search results page that retrieves posts from multiple custom post types.
sbb-itb-77ae9a4

Querying with Taxonomies

Taxonomies allow you to categorize and organize your custom post types in a structured way. You can query custom post types based on their associated taxonomy terms, making it easier to retrieve specific posts.

Using the ‘tax_query’ Parameter

The tax_query parameter filters posts based on their taxonomy terms. You can use this parameter to query custom post types that have specific taxonomy terms associated with them. The tax_query parameter takes an array of taxonomy queries, where each query specifies the taxonomy, term, and field to query.

For example, let’s say you have a custom post type adverts with a taxonomy advert_tag. You can use the following code to query all adverts with the term politics:

$args = array(     'post_type' => 'adverts',     'tax_query' => array(         array(             'taxonomy' => 'advert_tag',             'field' => 'slug',             'terms' => 'politics'         )     ) ); $query = new WP_Query($args); 

Single Taxonomy Term Query

A common use case is querying custom post types with a single taxonomy term. For example, you might want to display all adverts with the term politics on a specific page. You can use the tax_query parameter to achieve this.

Multiple Taxonomy Terms Query

You can also query custom post types with multiple taxonomy terms using the tax_query parameter. For example, you might want to display all adverts with the terms politics and economy. You can use the following code to achieve this:

$args = array(     'post_type' => 'adverts',     'tax_query' => array(         array(             'taxonomy' => 'advert_tag',             'field' => 'slug',             'terms' => array('politics', 'economy')         )     ) ); $query = new WP_Query($args); 

Code Example

Here’s an example code snippet that demonstrates querying custom post types with multiple taxonomy terms:

$args = array(     'post_type' => 'adverts',     'tax_query' => array(         array(             'taxonomy' => 'advert_tag',             'field' => 'slug',             'terms' => array('politics', 'economy')         )     ) ); $query = new WP_Query($args);  if ($query->have_posts()) {     while ($query->have_posts()) {         $query->the_post();         echo '<h2>'. get_the_title(). '</h2>';         echo '<p>'. get_the_content(). '</p>';     }     wp_reset_postdata(); // Reset post data } else {     echo 'No posts found!'; } 

In this example, we query all adverts with the terms politics and economy and display their titles and content using a while loop.

Advanced Query Techniques

Additional Query Parameters

WP_Query offers several other parameters to refine your queries further:

Parameter Description
orderby Specifies the field to order the results by (e.g., date, title, comment_count, rand)
order Specifies the ordering direction (ASC for ascending or DESC for descending)
posts_per_page Sets the number of posts to retrieve per page
meta_query Allows querying posts based on custom field values

You can combine these parameters to create more targeted queries.

Combining Parameters

Combining multiple parameters lets you create highly specific queries. For example, you might want to retrieve posts with a certain taxonomy term, ordered by date, and limited to 5 posts per page.

Here’s an example:

$args = array(     'post_type' => 'adverts',     'tax_query' => array(         array(             'taxonomy' => 'advert_tag',             'field' => 'slug',             'terms' => 'politics'         )     ),     'orderby' => 'date',     'order' => 'DESC',     'posts_per_page' => 5 ); $query = new WP_Query($args); 

Resetting Post Data

After running custom queries, it’s crucial to reset the post data using wp_reset_postdata(). This ensures the global post data is restored to its original state, preventing conflicts with other queries or plugins.

Code Example

Here’s an advanced WP_Query example with multiple parameters:

$args = array(     'post_type' => 'adverts',     'tax_query' => array(         array(             'taxonomy' => 'advert_tag',             'field' => 'slug',             'terms' => array('politics', 'economy')         )     ),     'meta_query' => array(         array(             'key' => 'price',             'value' => 100,             'compare' => '>'         )     ),     'orderby' => 'price',     'order' => 'ASC',     'posts_per_page' => 10 ); $query = new WP_Query($args);  if ($query->have_posts()) {     while ($query->have_posts()) {         $query->the_post();         echo '<h2>'. get_the_title(). '</h2>';         echo '<p>'. get_the_content(). '</p>';     }     wp_reset_postdata(); // Reset post data } else {     echo 'No posts found!'; } 

This query retrieves all adverts with the terms politics and economy, with a price greater than 100, ordered by price in ascending order, and limited to 10 posts per page.

Displaying Custom Post Type Data

Showcasing your custom post type data is crucial for presenting your content effectively to website visitors. Here, we’ll explore different ways to display this data using template tags, structuring information, and code examples.

Using Template Tags

Template tags are functions that retrieve specific data from your custom post types. Common template tags include:

  • the_title(): Gets the title of the custom post type
  • the_content(): Gets the content of the custom post type
  • the_permalink(): Gets the permalink of the custom post type

You can use these template tags within your WordPress loop to display the data. For example:

<?php while ( $query->have_posts() ) : $query->the_post();?>   <h2><?php the_title();?></h2>   <p><?php the_content();?></p>   <a href="<?php the_permalink();?>">Read more</a> <?php endwhile;?> 

Structuring Data

When displaying custom post type data, it’s important to consider the structure and fields of your custom post type. This will help you determine which template tags to use and how to organize your data.

For example, if your custom post type has a field for "price," you’ll want to use a template tag that retrieves that specific field. You can use get_post_meta() to retrieve custom field data:

<?php $price = get_post_meta( get_the_ID(), 'price', true );?> <p>Price: <?php echo $price;?></p> 

Code Example

Here’s a code example that demonstrates how to display custom post type data using template tags and structuring data:

<?php $args = array(   'post_type' => 'products',   'posts_per_page' => 5 ); $query = new WP_Query( $args );  if ( $query->have_posts() ) :?>   <ul>     <?php while ( $query->have_posts() ) : $query->the_post();?>       <li>         <h2><?php the_title();?></h2>         <p><?php the_content();?></p>         <p>Price: <?php echo get_post_meta( get_the_ID(), 'price', true );?></p>       </li>     <?php endwhile;?>   </ul> <?php endif;?> 

This code example retrieves the "products" custom post type, displays the title, content, and price of each product, and structures the data using an unordered list.

Conclusion

In this guide, we’ve covered the key aspects of querying custom post types in WordPress using WP_Query. From understanding the basics to advanced techniques for querying multiple custom post types and taxonomies, we’ve explored various ways to retrieve and display custom post type data.

Main Points

  • WP_Query is a powerful tool for querying custom post types in WordPress.
  • Understanding the post_type and tax_query parameters is crucial for querying custom post types.
  • You can use WP_Query to query multiple custom post types and taxonomies.
  • Displaying custom post type data requires using template tags and structuring data effectively.

Practice and Explore

Now that you’ve learned the basics, it’s time to practice. Experiment with different WP_Query parameters and techniques to retrieve and display custom post type data on your WordPress site. Don’t be afraid to try new things and learn from mistakes.

Additional Resources

For further learning, check out these resources:

Resource Description
WordPress Codex: WP_Query Documentation on the WP_Query class
WordPress Codex: Custom Post Types Information on custom post types
WordPress Codex: Taxonomies Details on taxonomies

FAQs

How do I fetch custom post type data in WordPress?

WordPress

To fetch custom post type data in WordPress, you can use the WP_Query class. Here’s a simple guide:

  1. Set up an array with the parameters for your custom post type query:
$args = array(     'post_type' => 'your_custom_post_type',     'post_status' => 'published' ); 
  1. Create a new WP_Query instance and pass the $args array:
$query = new WP_Query($args); 
  1. Use a while loop to iterate through the query results:
while ($query->have_posts()) {     $query->the_post();     // Display your custom post type data here } 
  1. After the loop, reset the post data:
wp_reset_postdata(); 

By following these steps, you can fetch and display custom post type data in WordPress using WP_Query.

Common Use Cases

Here are some common use cases for fetching custom post type data:

Use Case Description
Custom Archives Display a list of custom post types on an archive page.
Custom Queries Retrieve specific custom post types based on criteria like taxonomy terms, meta data, or date ranges.
Custom Layouts Display custom post type data in custom layouts or templates.
Related Content Show related custom post types on individual post pages.

Tips and Best Practices

  • Understand the WP_Query parameters: Familiarize yourself with the various parameters available in WP_Query to create more targeted queries.
  • Use template tags: Utilize WordPress template tags like the_title(), the_content(), and the_permalink() to display custom post type data.
  • Structure data effectively: Organize your custom post type data in a clear and readable format, using headings, lists, and tables where appropriate.
  • Optimize queries: Optimize your queries by only fetching the data you need, and consider caching results for improved performance.
  • Reset post data: Always remember to reset the post data after running custom queries to avoid conflicts with other parts of your WordPress site.

Related posts

Filtering by Taxonomy: Slugs, Term IDs, and Names

The field parameter inside a tax_query clause tells WordPress how to interpret the values you pass in terms. The three you will actually use are slug, term_id, and name. Picking the wrong one is the single most common reason a query returns nothing.

Use slug when you are hard-coding readable values in a template or a shortcode. Slugs are URL-safe, lowercase, and stable, so they read well in code:

$args = array(
    'post_type' => 'movie',
    'tax_query' => array(
        array(
            'taxonomy' => 'genre',
            'field'    => 'slug',
            'terms'    => 'action',
        ),
    ),
);
$query = new WP_Query( $args );

Use term_id when the value comes from the database or from another query, for example a term selected in an admin field. IDs never change, so they are the safest choice for dynamic code:

$args = array(
    'post_type' => 'movie',
    'tax_query' => array(
        array(
            'taxonomy' => 'genre',
            'field'    => 'term_id',
            'terms'    => 45,
        ),
    ),
);
$query = new WP_Query( $args );

The name field matches against the human-readable term name and requires an exact match, including capitalization and spaces. Because of that fragility, reach for name only when you genuinely have nothing but the display label to work with, and prefer slug or term_id everywhere else.

AND vs OR Logic Across Multiple Terms and Taxonomies

Within a single clause, passing an array of terms matches posts in any of those terms, because the default operator is IN:

$args = array(
    'post_type' => 'restaurant',
    'tax_query' => array(
        array(
            'taxonomy' => 'city',
            'field'    => 'slug',
            'operator' => 'IN',
            'terms'    => array( 'athens', 'belgrade', 'berlin' ),
        ),
    ),
);
$query = new WP_Query( $args );

To require conditions across different taxonomies, add a top-level relation key and one clause per taxonomy. With 'relation' => 'AND', a post must satisfy every clause:

$args = array(
    'post_type' => 'restaurant',
    'tax_query' => array(
        'relation' => 'AND',
        array(
            'taxonomy' => 'city',
            'field'    => 'slug',
            'terms'    => array( 'barcelona', 'kuala-lumpur' ),
        ),
        array(
            'taxonomy' => 'dish',
            'field'    => 'slug',
            'terms'    => array( 'paella' ),
        ),
    ),
);
$query = new WP_Query( $args );

You can nest clauses to express more complex logic. Here the outer relation is AND and the inner relation is OR, which reads as “products that are laptops or tablets, and made by Apple”:

$args = array(
    'post_type' => 'product',
    'tax_query' => array(
        'relation' => 'AND',
        array(
            'relation' => 'OR',
            array(
                'taxonomy' => 'product-category',
                'field'    => 'slug',
                'terms'    => array( 'laptop' ),
            ),
            array(
                'taxonomy' => 'product-category',
                'field'    => 'slug',
                'terms'    => array( 'tablet' ),
            ),
        ),
        array(
            'taxonomy' => 'product-brand',
            'field'    => 'slug',
            'terms'    => array( 'apple' ),
        ),
    ),
);
$query = new WP_Query( $args );

EXISTS and NOT EXISTS Operators

The EXISTS and NOT EXISTS operators ignore specific terms and instead ask whether a post has any term in a taxonomy at all. They do not take a terms value. Use EXISTS to fetch only posts that have been categorized:

$args = array(
    'post_type' => 'portfolio',
    'tax_query' => array(
        array(
            'taxonomy' => 'project-type',
            'operator' => 'EXISTS',
        ),
    ),
);
$query = new WP_Query( $args );

Use NOT EXISTS to find the opposite, posts that were never assigned a term, which is handy for cleanup and content audits:

$args = array(
    'post_type' => 'course',
    'tax_query' => array(
        array(
            'taxonomy' => 'course-topic',
            'operator' => 'NOT EXISTS',
        ),
    ),
);
$query = new WP_Query( $args );

These operators combine cleanly with term-based clauses. The query below returns restaurants that have a city assigned but are not flagged as closed:

$args = array(
    'post_type' => 'restaurant',
    'tax_query' => array(
        'relation' => 'AND',
        array(
            'taxonomy' => 'city',
            'operator' => 'EXISTS',
        ),
        array(
            'taxonomy' => 'status',
            'field'    => 'slug',
            'terms'    => array( 'closed' ),
            'operator' => 'NOT IN',
        ),
    ),
);
$query = new WP_Query( $args );

Troubleshooting Taxonomy Queries That Return No Results

When a taxonomy query comes back empty even though you can see matching posts in the admin, work through these causes in order. Almost every failure is one of them:

  • Incorrect taxonomy name. The taxonomy key in your query must match exactly the name you passed to register_taxonomy(), not the label. A taxonomy registered as project-type will not respond to project_type.
  • Slug mismatch. Confirm the actual slug by editing the term in the WordPress admin and checking its slug field. WordPress often differs from what you expect, for example sci-fi rather than scifi.
  • Wrong field parameter. If you pass slugs you must set 'field' => 'slug' explicitly. If you pass numeric IDs, either omit field or set it to 'term_id'. Passing a slug while field defaults to term_id silently matches nothing.
  • Taxonomy not attached to the post type. Verify the taxonomy was registered against the post type you are querying. If the relationship is missing, no post of that type can carry the term.

To verify the term actually exists before you build the query, check it with get_term_by(). This turns a silent empty result into an explicit signal you can log:

$term = get_term_by( 'slug', 'action', 'genre' );

if ( false === $term ) {
    // The slug or taxonomy is wrong - the term does not exist.
    error_log( 'osom: term "action" not found in taxonomy "genre".' );
} else {
    $args = array(
        'post_type' => 'movie',
        'tax_query' => array(
            array(
                'taxonomy' => 'genre',
                'field'    => 'term_id',
                'terms'    => $term->term_id,
            ),
        ),
    );
    $query = new WP_Query( $args );
}

If everything looks correct and the query still fails, install Query Monitor and inspect the exact SQL WordPress generated. Seeing the real JOIN against the term tables usually makes the mismatch obvious immediately.

Why Custom Post Type Queries Get Slow

Custom post types make queries more complex, and that complexity is what slows a site down. Three things drive it. First, every query against the posts table can fan out into additional queries for metadata and terms. Second, custom taxonomies add JOIN operations against the term tables. Third, custom fields filtered through meta_query hit the wp_postmeta table, which is long and narrow and expensive to scan once it holds millions of rows.

The optimizations below all share one goal: ask the database for less work and reuse results you have already paid for.

Returning Less Data with the fields Parameter

By default WP_Query hydrates full post objects and primes the meta and term caches for every result. If you only need IDs, the fields parameter skips all of that:

$args = array(
    'post_type' => 'osom_event',
    'fields'    => 'ids',
);
$query = new WP_Query( $args );

// $query->posts is now a plain array of post IDs.

The fields parameter only accepts two values: 'ids' for an array of post IDs, and 'id=>parent' for an array mapping each ID to its parent. Use this whenever you need IDs to feed another function, build a menu, or check membership, rather than render full content.

Skipping the Row Count with no_found_rows

To support pagination, WordPress runs a second SQL_CALC_FOUND_ROWS pass to count every matching row. If a query does not need pagination, for example a sidebar list of five recent items, that count is pure waste. Set no_found_rows to true to skip it:

$args = array(
    'post_type'      => 'osom_event',
    'posts_per_page' => 5,
    'no_found_rows'  => true,
);
$query = new WP_Query( $args );

Only use this on queries where you will never call paginate_links() or read $query->max_num_pages, because both depend on the row count this flag removes.

Optimizing Meta Queries

A meta_query filters by custom fields, but it joins the wp_postmeta table, which has no index on the meta_value column. The basic structure looks like this:

$args = array(
    'post_type'  => 'osom_event',
    'meta_query' => array(
        array(
            'key'     => 'event_status',
            'value'   => 'upcoming',
            'compare' => '=',
        ),
    ),
);
$query = new WP_Query( $args );

Keep meta queries fast with a few rules. Always set the type when comparing numbers or dates so MySQL casts correctly. Avoid LIKE comparisons with a leading wildcard, which force a full table scan. And when you filter on the same key constantly, the durable fix is to add a database index on that key rather than scaling the meta query itself:

$args = array(
    'post_type'  => 'osom_event',
    'meta_query' => array(
        array(
            'key'     => 'event_start',
            'value'   => gmdate( 'Y-m-d' ),
            'compare' => '>=',
            'type'    => 'DATE',
        ),
    ),
    'orderby'  => 'meta_value',
    'order'    => 'ASC',
);
$query = new WP_Query( $args );

Caching Query Results

If a query is expensive and its results rarely change, run it once and store the output in a transient. Subsequent page loads read from the cache instead of touching the database:

function osom_get_featured_events() {
    $ids = get_transient( 'osom_featured_events' );

    if ( false === $ids ) {
        $query = new WP_Query( array(
            'post_type'      => 'osom_event',
            'posts_per_page' => 10,
            'fields'         => 'ids',
            'no_found_rows'  => true,
            'meta_query'     => array(
                array(
                    'key'   => 'is_featured',
                    'value' => '1',
                ),
            ),
        ) );

        $ids = $query->posts;

        // Cache for 12 hours.
        set_transient( 'osom_featured_events', $ids, 12 * HOUR_IN_SECONDS );
    }

    return $ids;
}

Because the cached value can go stale, clear it whenever an event is saved so editors see changes immediately:

function osom_clear_featured_events_cache( $post_id ) {
    if ( 'osom_event' === get_post_type( $post_id ) ) {
        delete_transient( 'osom_featured_events' );
    }
}
add_action( 'save_post', 'osom_clear_featured_events_cache' );

On sites with a persistent object cache (Redis or Memcached), transients are promoted to that backend automatically, so the same code scales without changes.

Database Indexing and Custom Tables

Indexing helps the database find rows without scanning the whole table, like a table of contents for your data. When one meta key is queried constantly, add a compound index covering the key and value columns of wp_postmeta:

global $wpdb;

$wpdb->query(
    "ALTER TABLE {$wpdb->postmeta}
     ADD INDEX osom_meta_key_value ( meta_key(191), meta_value(100) )"
);

For very large or highly structured datasets, the bigger win is to stop using wp_postmeta at all. Store the data in a purpose-built custom table with its own indexes, and query it directly. Always pass values through $wpdb->prepare() to avoid SQL injection:

global $wpdb;

$table = $wpdb->prefix . 'osom_event_data';

$results = $wpdb->get_results(
    $wpdb->prepare(
        "SELECT post_id, venue, capacity
         FROM {$table}
         WHERE region = %s AND capacity >= %d
         ORDER BY capacity DESC",
        'eu-west',
        500
    )
);

A custom table with the right index turns a slow, multi-join meta query into a single fast lookup, which is the approach worth reaching for once meta_query stops keeping up.


Discover more from WP Winners 🏆

Subscribe to get the latest posts sent to your email.

More WorDPRESS Tips, tutorials and Guides

Discover more from WP Winners 🏆

Subscribe now to keep reading and get access to the full archive.

Continue reading