From CPT Custom Loop into Shortcode?

You can turn the loop into a shortcode like this for example. The key thing here is to return, with help of output buffer, the html output instead of echoing / printing it directly.

If you want, you can also allow users to pass parameters to the shortcode and let them modify the query. But you also need to have some args that are required to make the shortcode work as expected.

If you want, you could also move the html for the loop items into separate partial template file and call it within the loop with get_template_part(). The loop output wouldn’t change at all by this, but would make the code a bit more cleaner looking.

    add_shortcode( 'your_cpt_loop', 'your_cpt_loop_callback' ); // change these to your liking

function your_cpt_loop_callback( $atts ) {

    $args = array(
        'post_type' => 'studentensteden',
        // add other Wp_Query parameters that are required for the query
    );

    // query by post meta
    if ( ! empty( $atts['color'] ) ) {
        $args['meta_query'] = array(
            array(
                'key'       => 'color',
                'value'     => $atts['color'],
                'compare'   => '='
            ),
        );
    }

    // query by taxonomy
    if ( ! empty( $atts['my_taxonomy'] ) ) {
        $args['tax_query'] = array(
            array(
                'taxonomy' => 'my_taxonomy',
                'field'    => 'slug',
                'terms'    => $atts['my_taxonomy'],
            ),
        );
    }

  $default = array(
    'orderby' => 'title',
    // add other WP_Query parameters here based on what parameters you want to allow users to pass to the shortcode
  );
  $atts = shortcode_atts( $default, $atts, 'your_cpt_loop_callback' ); // this allows users to pass parameters with the shortcode, if needed, third parameter allows filtering for the user submitted args

  $args = array_merge( $atts, $args ); // force required parameters to the $args array;
  $the_query = new WP_Query( $args );

  $output=""; // shortcode should return its output instead of echoing/printing it

    ob_start(); // echo html to output buffer
  if ( $the_query->have_posts() ) :
    // you could also put the loop item html into a separate partial file and call it here with get_template_part( $slug, $name = null ), end result is the same, but the shortcode code would be a little cleaner looking
    while ( $the_query->have_posts() ) : $the_query->the_post(); ?>
    <a href="https://wordpress.stackexchange.com/questions/348593/<?php the_permalink(); ?>" title="<?php the_title_attribute(); ?>">
      <div class="sclisting">
        <?php if ( has_post_thumbnail() ) : ?>
          <a href="https://wordpress.stackexchange.com/questions/348593/<?php the_permalink(); ?>" title="<?php the_title_attribute(); ?>"><?php the_post_thumbnail( 'large' ); ?></a>
        <?php endif; ?>
        <h3 class="the-title"><?php the_title(); ?></h3>
        <div class="isbn-number">Provincie <?php the_field('provincie');?> | Gemeente <?php the_field('gemeente');?> |  <?php the_field('aantalinwoners');?> Inwoners</div>
        <div class="isbn-number">Opleidingen | Hogescholen | Universiteiten</div>
        <div class="isbn-number">In totaal studeren er <?php the_field('studentenstuderend');?> studenten en wonen er <?php the_field('studentenwoonachtig');?> studenten.</div>
        <div class="isbn-number">Je vindt er <?php the_field('aantalbioscopen');?> bioscopen, <?php the_field('aantalkroegen');?> kroegen, <?php the_field('aantalmusea');?> musea en <?php the_field('aantaltheaters');?> theaters.</div>
      </div>
    </a>
  <?php
    endwhile;
    wp_reset_postdata(); // After looping through a separate query, this function restores the $post global to the current post in the main query.
    else:
  ?>
    <p>Sorry, there are no posts to display</p>
  <?php
  endif;
    $output = ob_get_clean(); // push output buffer to variable and clear buffer

  return $output; // either returns the generated html or an empty string, if no posts were found

}