If you want to put this on the event archive page, you can do so by checking out my other guide How To Edit Event Archive Elementor Free
- First you need to add the custom code in a snippet plugin in the video i am using WpCodeBox which is a paid plugin. You can also make it work with a free code snippet plugin, just remember to set the snippet to php. Its also possible to create a child theme and place the code inside the child themes functions file.
<?php
/**
* Linnet Advanced Post Grid Widget
*/
add_action( 'elementor/init', function() {
add_action( 'elementor/widgets/register', function( $widgets_manager ) {
class Linnet_Post_Grid_Widget extends \Elementor\Widget_Base {
public function get_name() {
return 'linnet_post_grid';
}
public function get_title() {
return __( 'Linnet Post Grid', 'linnet' );
}
public function get_icon() {
return 'eicon-posts-grid';
}
public function get_categories() {
return [ 'general' ];
}
protected function register_controls() {
$this->start_controls_section(
'content_section',
[
'label' => __( 'Content', 'linnet' ),
'tab' => \Elementor\Controls_Manager::TAB_CONTENT,
]
);
// Post Type selector
$post_types = get_post_types( [ 'public' => true ], 'objects' );
$post_type_options = [];
foreach ( $post_types as $post_type ) {
$post_type_options[ $post_type->name ] = $post_type->labels->singular_name;
}
$this->add_control(
'post_type',
[
'label' => __( 'Post Type', 'linnet' ),
'type' => \Elementor\Controls_Manager::SELECT,
'options' => $post_type_options,
'default' => 'post',
]
);
$this->add_control(
'posts_per_page',
[
'label' => __( 'Posts Per Page', 'linnet' ),
'type' => \Elementor\Controls_Manager::NUMBER,
'default' => 6,
]
);
// Order By
$this->add_control(
'order_by',
[
'label' => __( 'Order By', 'linnet' ),
'type' => \Elementor\Controls_Manager::SELECT,
'options' => [
'title_asc' => 'Title Ascending',
'title_desc' => 'Title Descending',
'post_date_asc' => 'Post Date Ascending',
'post_date_desc' => 'Post Date Descending',
'acf_date_asc' => 'ACF Date Ascending',
'acf_date_desc' => 'ACF Date Descending',
'acf_datetime_asc' => 'ACF Date/Time Ascending',
'acf_datetime_desc' => 'ACF Date/Time Descending',
],
'default' => 'post_date_desc',
]
);
$this->add_control(
'acf_order_field',
[
'label' => __( 'ACF Field Name (for ordering)', 'linnet' ),
'type' => \Elementor\Controls_Manager::TEXT,
'placeholder' => 'my_acf_date_field',
'condition' => [
'order_by' => [ 'acf_date_asc', 'acf_date_desc', 'acf_datetime_asc', 'acf_datetime_desc' ],
],
]
);
// Taxonomy filter — add empty option first
$taxonomy_options = [ '' => '— No Taxonomy Filter —' ];
$taxonomies = get_taxonomies( [ 'public' => true ], 'objects' );
foreach ( $taxonomies as $taxonomy ) {
$taxonomy_options[ $taxonomy->name ] = $taxonomy->labels->singular_name;
}
$this->add_control(
'filter_taxonomy',
[
'label' => __( 'Filter Taxonomy', 'linnet' ),
'type' => \Elementor\Controls_Manager::SELECT,
'options' => $taxonomy_options,
'default' => '',
]
);
$this->add_control(
'filter_terms_include',
[
'label' => __( 'Include Terms (comma separated IDs)', 'linnet' ),
'type' => \Elementor\Controls_Manager::TEXT,
'placeholder' => '1,2,3',
]
);
$this->add_control(
'filter_terms_exclude',
[
'label' => __( 'Exclude Terms (comma separated IDs)', 'linnet' ),
'type' => \Elementor\Controls_Manager::TEXT,
'placeholder' => '4,5,6',
]
);
// ACF Date/Datetime filter
$this->add_control(
'acf_date_filter_field',
[
'label' => __( 'ACF Date/DateTime Field for Filter', 'linnet' ),
'type' => \Elementor\Controls_Manager::TEXT,
'placeholder' => 'my_acf_date_field',
]
);
$this->add_control(
'acf_date_filter_past',
[
'label' => __( 'Show posts where date is in the PAST', 'linnet' ),
'type' => \Elementor\Controls_Manager::SWITCHER,
'default' => '',
]
);
$this->add_control(
'acf_date_filter_future',
[
'label' => __( 'Show posts where date is in the FUTURE', 'linnet' ),
'type' => \Elementor\Controls_Manager::SWITCHER,
'default' => '',
]
);
// Enable Pagination
$this->add_control(
'enable_pagination',
[
'label' => __( 'Enable Pagination', 'linnet' ),
'type' => \Elementor\Controls_Manager::SWITCHER,
'default' => 'yes',
]
);
// Custom Anchor ID
$this->add_control(
'anchor_id',
[
'label' => __( 'Custom Anchor ID (for pagination scroll)', 'linnet' ),
'type' => \Elementor\Controls_Manager::TEXT,
'placeholder' => 'linnet-post-grid',
'description' => __( 'If empty, default is "linnet-post-grid".', 'linnet' ),
]
);
// Columns per device
$this->add_responsive_control(
'columns',
[
'label' => __( 'Columns', 'linnet' ),
'type' => \Elementor\Controls_Manager::SELECT,
'default' => '3',
'options' => [
'1' => '1',
'2' => '2',
'3' => '3',
'4' => '4',
],
'selectors' => [
'{{WRAPPER}} .linnet-post-grid' => 'grid-template-columns: repeat({{VALUE}}, 1fr);',
],
]
);
// Display Order Repeater
$this->add_control(
'display_order',
[
'label' => __( 'Display Order', 'linnet' ),
'type' => \Elementor\Controls_Manager::REPEATER,
'fields' => [
[
'name' => 'element_type',
'label' => __( 'Element Type', 'linnet' ),
'type' => \Elementor\Controls_Manager::SELECT,
'options' => [
'title' => 'Post Title',
'featured_image' => 'Featured Image',
'acf_field' => 'ACF Field',
],
'default' => 'title',
],
[
'name' => 'acf_field_name',
'label' => __( 'ACF Field Name', 'linnet' ),
'type' => \Elementor\Controls_Manager::TEXT,
'placeholder' => 'my_acf_field',
'condition' => [
'element_type' => 'acf_field',
],
],
[
'name' => 'acf_field_class',
'label' => __( 'ACF Field CSS Class', 'linnet' ),
'type' => \Elementor\Controls_Manager::TEXT,
'placeholder' => 'my-class-name',
'condition' => [
'element_type' => 'acf_field',
],
],
],
'default' => [
[ 'element_type' => 'featured_image' ],
[ 'element_type' => 'title' ],
],
'title_field' => '{{{ element_type }}}',
]
);
$this->end_controls_section();
}
protected function render() {
$settings = $this->get_settings_for_display();
$anchor_id = ! empty( $settings['anchor_id'] ) ? $settings['anchor_id'] : 'linnet-post-grid';
$paged_param_name = 'linnet_paged_' . sanitize_title( $anchor_id );
$paged = isset( $_GET[ $paged_param_name ] ) ? max( 1, intval( $_GET[ $paged_param_name ] ) ) : 1;
$args = [
'post_type' => $settings['post_type'],
'posts_per_page' => $settings['posts_per_page'],
'paged' => $paged,
];
// ORDER BY
switch ( $settings['order_by'] ) {
case 'title_asc':
$args['orderby'] = 'title';
$args['order'] = 'ASC';
break;
case 'title_desc':
$args['orderby'] = 'title';
$args['order'] = 'DESC';
break;
case 'post_date_asc':
$args['orderby'] = 'date';
$args['order'] = 'ASC';
break;
case 'post_date_desc':
$args['orderby'] = 'date';
$args['order'] = 'DESC';
break;
case 'acf_date_asc':
case 'acf_date_desc':
case 'acf_datetime_asc':
case 'acf_datetime_desc':
$args['meta_key'] = $settings['acf_order_field'];
$args['orderby'] = 'meta_value';
$args['order'] = ( strpos( $settings['order_by'], 'asc' ) !== false ) ? 'ASC' : 'DESC';
break;
default:
$args['orderby'] = 'date';
$args['order'] = 'DESC';
break;
}
// Taxonomy filter — with proper empty handling
if ( ! empty( $settings['filter_taxonomy'] ) ) {
$has_include = ! empty( $settings['filter_terms_include'] );
$has_exclude = ! empty( $settings['filter_terms_exclude'] );
if ( $has_include || $has_exclude ) {
$tax_query = [
'taxonomy' => $settings['filter_taxonomy'],
'field' => 'term_id',
];
if ( $has_include ) {
$tax_query['terms'] = array_map( 'intval', explode( ',', $settings['filter_terms_include'] ) );
$tax_query['operator'] = 'IN';
} elseif ( $has_exclude ) {
$tax_query['terms'] = array_map( 'intval', explode( ',', $settings['filter_terms_exclude'] ) );
$tax_query['operator'] = 'NOT IN';
}
$args['tax_query'] = [ $tax_query ];
}
// ELSE → do NOT set tax_query → show all posts
}
// ACF Date Filter — IN PAST / IN FUTURE
if ( ! empty( $settings['acf_date_filter_field'] ) ) {
$now = current_time( 'Y-m-d H:i:s' ); // current time, in WP timezone
// Only IN PAST
if ( 'yes' === $settings['acf_date_filter_past'] && 'yes' !== $settings['acf_date_filter_future'] ) {
$args['meta_query'][] = [
'key' => $settings['acf_date_filter_field'],
'compare' => '<',
'type' => 'DATETIME',
'value' => $now,
];
}
// Only IN FUTURE
if ( 'yes' === $settings['acf_date_filter_future'] && 'yes' !== $settings['acf_date_filter_past'] ) {
$args['meta_query'][] = [
'key' => $settings['acf_date_filter_field'],
'compare' => '>',
'type' => 'DATETIME',
'value' => $now,
];
}
// If BOTH are ON → no meta_query needed → show all posts
}
$query = new WP_Query( $args );
$anchor_id = ! empty( $settings['anchor_id'] ) ? $settings['anchor_id'] : 'linnet-post-grid';
// Pagination styling
echo '<style>
.linnet-post-image img{
width:100%;
}
.linnet-pagination nav ul {
display: flex;
flex-wrap: wrap;
justify-content: center;
list-style: none;
padding: 0;
margin: 20px 0;
gap: 6px;
}
.linnet-pagination nav li {
display: inline-flex;
}
.linnet-pagination nav a,
.linnet-pagination nav span {
display: inline-block;
padding: 8px 12px;
border: 1px solid #ccc;
text-decoration: none;
color: #333;
border-radius: 4px;
font-size: 14px;
min-width: 36px;
text-align: center;
}
.linnet-pagination nav .current {
background: #333;
color: #fff;
border-color: #333;
}
.linnet-pagination nav a:hover {
background: #f0f0f0;
border-color: #888;
}
@media (max-width: 480px) {
.linnet-pagination nav a,
.linnet-pagination nav span {
padding: 6px 8px;
font-size: 13px;
min-width: auto;
}
}
</style>';
// Grid wrapper
echo '<div id="' . esc_attr( $anchor_id ) . '" class="linnet-post-grid" style="display:grid; grid-gap:20px;">';
if ( $query->have_posts() ) {
while ( $query->have_posts() ) {
$query->the_post();
echo '<div class="linnet-post-item" style="border:1px solid #ccc; padding:10px;">';
foreach ( $settings['display_order'] as $order_item ) {
switch ( $order_item['element_type'] ) {
case 'featured_image':
if ( has_post_thumbnail() ) {
echo '<div class="linnet-post-image">';
echo '<a href="' . esc_url( get_permalink() ) . '">';
the_post_thumbnail( 'medium' );
echo '</a>';
echo '</div>';
}
break;
case 'title':
echo '<h3 class="linnet-post-title"><a href="' . esc_url( get_permalink() ) . '">' . get_the_title() . '</a></h3>';
break;
case 'acf_field':
$field_name = $order_item['acf_field_name'];
$field_class = isset( $order_item['acf_field_class'] ) ? $order_item['acf_field_class'] : '';
if ( ! empty( $field_name ) ) {
$acf_value = get_field( $field_name );
if ( $acf_value ) {
echo '<div class="linnet-post-acf ' . esc_attr( $field_class ) . '">' . esc_html( $acf_value ) . '</div>';
}
}
break;
}
}
echo '</div>'; // linnet-post-item
}
echo '</div>'; // linnet-post-grid
if ( 'yes' === $settings['enable_pagination'] ) {
$pagination = paginate_links( [
'base' => add_query_arg( $paged_param_name, '%#%' ) . '#' . $anchor_id,
'format' => '',
'current' => max( 1, $paged ),
'total' => $query->max_num_pages,
'type' => 'list',
'prev_text' => __( '« Previous' ),
'next_text' => __( 'Next »' ),
] );
if ( $pagination ) {
echo '<div class="linnet-pagination"><nav>' . $pagination . '</nav></div>';
}
}
wp_reset_postdata();
} else {
echo '<p>' . __( 'No posts found.', 'linnet' ) . '</p>';
}
}
}
$widgets_manager->register( new \Linnet_Post_Grid_Widget() );
});
});
add_action( 'admin_notices', function() {
if ( ! did_action( 'elementor/loaded' ) ) {
echo '<div class="notice notice-warning"><p>Linnet Post Grid Widget requires Elementor to be active.</p></div>';
}
});
- Then go to your your archive template and edit it with Elementor. Linnet templates -> Templates.
- Go to widgets and search for linnet post grid, then add it to the page.
- In the module content settings, locate the post type option and set it to Event.
- Then click publish, now your events will show on the archive page.