Build a Simple Event Listing in WordPress Without a Plugin

If you need a simple event listing on a WordPress site — with a start date, title, and description — you do not always need a dedicated events plugin. Registering a custom post type and combining it with an ACF date field gives you a clean, lightweight solution with full control over the output.

Problem: A site needs to list upcoming events with a date, location, and description — but installing a full events plugin adds unnecessary complexity and overhead for a simple listing.

Solution: Register a custom post type event with a date meta field, query upcoming events with WP_Query using a meta_query date comparison and meta_key ordering, and build a straightforward list or card template in the theme.

Step 1. Register the event post type in functions.php:

<?php
add_action( 'init', 'register_event_post_type' );

function register_event_post_type() {
    $labels = [
        'name'          => __( 'Events', 'theme-name' ),
        'singular_name' => __( 'Event', 'theme-name' ),
        'menu_name'     => __( 'Events', 'theme-name' ),
        'add_new_item'  => __( 'Add New Event', 'theme-name' ),
        'edit_item'     => __( 'Edit Event', 'theme-name' ),
        'not_found'     => __( 'No events found', 'theme-name' ),
    ];

    $args = [
        'labels'        => $labels,
        'supports'      => [ 'title', 'editor', 'excerpt', 'thumbnail' ],
        'show_in_rest'  => true,
        'public'        => true,
        'has_archive'   => false,
        'menu_position' => 6,
        'menu_icon'     => 'dashicons-calendar-alt',
        'rewrite'       => [ 'slug' => 'event', 'with_front' => true ],
    ];

    register_post_type( 'event', $args );
}

In ACF, create a Date Picker field named start_date and assign it to the event post type. Set the return format to F j, Y (e.g. October 15, 2019) so it is compatible with PHP's strtotime().

Step 2. Query and output upcoming events, filtering out any event whose start date has already passed:

<?php
// Collect IDs of past events to exclude
$all_events = new WP_Query( [
    'post_type'      => 'event',
    'posts_per_page' => -1,
    'fields'         => 'ids',
    'no_found_rows'  => true,
] );

$exclude_ids = [];
foreach ( $all_events->posts as $event_id ) {
    $start = get_field( 'start_date', $event_id );
    if ( $start && strtotime( current_time( 'F j, Y' ) ) > strtotime( $start ) ) {
        $exclude_ids[] = $event_id;
    }
}

// Main query: upcoming events ordered by start date ascending
$events_query = new WP_Query( [
    'post_type'      => 'event',
    'posts_per_page' => 10,
    'order'          => 'ASC',
    'orderby'        => 'meta_value',
    'meta_key'       => 'start_date',
    'post__not_in'   => $exclude_ids,
] );

if ( $events_query->have_posts() ) :
    while ( $events_query->have_posts() ) : $events_query->the_post();
        // Output event title, date, excerpt, etc.
    endwhile;
    wp_reset_postdata();
else :
    echo '<p>' . esc_html__( 'No upcoming events at the moment. Check back soon!', 'theme-name' ) . '</p>';
endif;

NOTE: This approach compares dates as strings via strtotime(), which works reliably when the ACF return format is consistent. For more complex date filtering — timezone awareness, multi-day events, or recurring events — a dedicated ACF meta query with a DATETIME comparison or a purpose-built plugin (The Events Calendar) will serve you better.