Reference Manual — Release 1.0.0

Savvy
Posts Grid

An Elementor widget for post archives that stay on one page. Visitors type, filter and page through your content while the grid re-renders in place — and each card can be a saved Elementor template of your own design.

WordPress 5.8+ PHP 7.4+ Elementor 3.5+ ACF Optional GPL-2.0-or-later
Chapter One

Getting Started

What the widget is, what it needs, and how to get a working grid on a page.

1.1Live Demo

The panel below runs on a loop: a query is typed out, the filter drawer opens, a category is picked, and the results settle. Nothing here talks to a server — it is a scripted mock of the real interaction, included so you can see the rhythm before installing anything.

Auto-playing
Design
Designing for Scale

Structuring components for very large sites.

Dev
Dev Notes: AJAX

Debounced requests that never block the UI.

News
Newsroom Update

What shipped across the platform this week.

Design
Design Systems 101

Tokens, primitives, and consistent UI at scale.

Dev
Building Fast Grids

Caching and query tuning for 10,000+ posts.

News
Weekly News Digest

The five updates worth your time today.

Loading demo…

1.2 Overview

Most post-grid widgets stop at "show me ten posts in three columns". This one is built for the archive that has to carry a real content library: tens of thousands of entries, a dozen ways to narrow them down, and an editor who wants the card to look like the rest of the site. Posts, pages and custom post types all render the same way.

Search as you type

Waits 300 ms after the last keystroke, then queries. Whatever you have filtered stays filtered.

📄

Three ways to page

Numbered links, a Load More button, or a scroll sentinel that fetches the next batch.

🎛️

Filters in a drawer

Slides in from either edge so the grid keeps the full width. Active choices show as removable chips.

🧩

Build your own filters

Taxonomies, author, date and post type out of the box; any ACF field found on the site joins the list.

🎨

Cards you designed

Point the widget at a saved template and every post renders through it, dynamic tags resolved per post.

🚀

Built to scale

Responses cache under a namespaced transient generation, and counting is skipped where it is not needed.

🔒

Nothing trusted from the client

Query arguments are re-read from the saved document server-side, so a tampered request cannot widen them.

Where the settings live There is no admin screen and no shortcode to memorise. Everything is a control on the widget itself, so the configuration travels with the page it belongs to. Activate, then drag Savvy Posts Grid off the widget panel.


1.3 Requirements

RequirementMinimumNotes
WordPress5.8Tested up to 6.8
PHP7.4Uses strict_types; PHP 8.x fully supported
Elementor3.5.0Free version is enough; tested up to 3.21
Elementor ProOptional. Pro templates & dynamic tags fully supported when present
ACF / ACF ProOptional. Fields are auto-detected and added to the filter builder

Browser support Anything current. The front-end script leans on IntersectionObserver for infinite scroll and AbortController to cancel superseded requests, so IE11 is out of scope.


1.4 Installation

  1. Put the files in placeDrop the savvy-posts-grid folder into /wp-content/plugins/, or hand the ZIP to Plugins → Add New → Upload Plugin.
  2. Switch it onActivate from the Plugins screen. Boot happens on plugins_loaded; if PHP or Elementor is too old you get an admin notice rather than a fatal error.
  3. Find the widgetOpen any page in Elementor and type "savvy" into the widget search. It also has its own category in the panel.
  4. Optional — design the cardBuild a layout under Templates → Saved Templates, then choose it in the widget's Card Template section. Skip this and the built-in card is used instead.

Activation & deactivation behavior

  • Activation stores spgrid_version for future migrations and flushes rewrite rules.
  • Deactivation flushes all plugin transients via CacheManager::flush_all().
  • Uninstall removes the spgrid_version and spgrid_cache_generation options and every spgrid_* transient (multisite-aware). Widget settings stored in Elementor documents are preserved so re-activation restores everything.

1.5 Quick Start

Six steps, and none of them involve leaving the Elementor editor.

  1. Place itDrag Savvy Posts Grid into whichever section should hold the archive.
  2. Say what to showUnder Query, pick a source — a custom query, a hand-picked set, posts related to the current one, or sticky posts — then the post types, how many per page, and the sort order.
  3. Shape the gridLayout holds the mode (Grid, List or Masonry) plus column count and gap, each settable per breakpoint.
  4. Add the filtersEach row in Filters is one control: choose what it filters on, how it should look, and whether multiple selections narrow or widen the result.
  5. Check search and pagingBoth arrive switched on. The only decision is which of the three paging styles suits the page.
  6. Make it yoursThe Style tab exposes typography, colour, spacing and border for every part of the widget.

They compose The three interactions share one request. A search does not clear your filters, and moving to page two does not clear either of them.


Chapter Two

Widget Reference

Every control in the Elementor panel, section by section, with its setting key and default.

2.1 Query

Everything that decides which posts land in the grid lives here. Worth knowing up front: when an AJAX request arrives, the server ignores whatever the browser claims these settings are and re-reads them from the saved Elementor document.

Sources

SourceDescription
Custom Query defaultBuild a query from post types, include/exclude categories, tags, and authors.
Manual SelectionComma-separated post IDs, displayed in the exact order you enter them (spgrid_manual_ids).
Specific PostsComma-separated post IDs (spgrid_specific_ids), ordered by your Order By setting.
Related PostsPosts sharing terms with the current post, by a chosen taxonomy (spgrid_related_taxonomy, default category).
Featured (Sticky)Sticky posts only.

Controls

ControlTypeDefaultDescription
spgrid_sourceSELECTqueryContent source (see table above).
spgrid_post_typeSELECT2['post']One or more public post types.
spgrid_per_pageNUMBER9Posts per page (1–100).
spgrid_orderbySELECTdatedate · title · menu_order · rand · comment_count · modified · meta_value · meta_value_num.
spgrid_orderby_metaTEXTMeta key, shown when ordering by meta value.
spgrid_orderSELECTDESCSort direction (ASC / DESC).
spgrid_include_categoriesSELECT2Limit to these categories (Custom Query only).
spgrid_exclude_categoriesSELECT2Exclude these categories (Custom Query only).
spgrid_include_tagsSELECT2Limit to these tags (Custom Query only).
spgrid_include_authorsSELECT2Limit to these authors (Custom Query only).
spgrid_specific_idsTEXTComma-separated IDs (Specific Posts source).
spgrid_manual_idsTEXTComma-separated IDs, kept in order (Manual source).
spgrid_related_taxonomySELECTcategoryTaxonomy used for Related Posts.

For developers The assembled argument array is handed to spgrid/query/args immediately before WP_Query runs, which is the place to bolt on anything the controls do not cover.


2.2 Layout

Three arrangements, each with its own column and gap values per breakpoint. Grid is the even-height default; List puts the image alongside the text; Masonry lets cards keep their natural heights and flow into columns.

ControlTypeDefaultDescription
spgrid_layout_modeSELECTgridLayout mode: grid, list, masonry. Applies prefix class spgrid-layout--*.
spgrid_columnsRESPONSIVE3 / 2 / 1Columns 1–6 (desktop / tablet / mobile). Exposed as CSS var --spgrid-columns.
spgrid_gapSLIDER24pxGap between items (--spgrid-gap).
spgrid_equal_heightSWITCHERyesStretch cards to equal heights (grid mode only).
spgrid_card_alignCHOOSEleftCard content alignment: left / center / right.

2.3 Search & Heading

Search

The field waits 300 ms after you stop typing before it queries, so a fast typist produces one request rather than ten. Each new keystroke aborts whatever request is still in flight. Searching drops you back to page one but leaves your filters untouched.

ControlTypeDefaultDescription
spgrid_enable_searchSWITCHERyesShow the search box.
spgrid_search_placeholderTEXTSearch posts…Input placeholder text.
spgrid_show_search_labelSWITCHERyesShow a label above/next to the input.
spgrid_search_label_textTEXTSearchLabel text.

Heading

ControlTypeDefaultDescription
spgrid_show_titleSWITCHERoffShow a heading above the grid.
spgrid_title_textTEXTpage titleHeading text (falls back to the current page title).
spgrid_title_tagSELECTh2HTML tag: h1–h6, p, span, div.

2.4 Pagination

Pick whichever suits the archive. All three fetch over AJAX and carry the current search term and filter selection with them.

TypeBehavior
Numbered defaultOrdinary page links. The grid is swapped out and the page eases back up to the top of the results.
Load MoreA button appends the next batch instead of replacing what is there, and removes itself once the last page has been reached. Total-row counting is skipped, which is measurably cheaper on large tables.
Infinite ScrollAn invisible sentinel sits below the grid; when it comes within 300 px of the viewport the next batch is already being fetched, so the seam rarely shows.
ControlTypeDefaultDescription
spgrid_enable_paginationSWITCHERyesEnable pagination.
spgrid_pagination_typeSELECTnumberednumbered · load_more · infinite.
spgrid_load_more_textTEXTLoad MoreButton label (Load More type only).

2.5 Filters & Filter Builder

Filters live in a drawer rather than a sidebar, so the grid keeps the page width. You describe each one as a row in a repeater; on the server those rows are folded into a single tax_query and meta_query rather than one query per filter.

Panel controls

ControlTypeDefaultDescription
spgrid_enable_filtersSWITCHERyesEnable the filter system.
spgrid_filters_positionSELECTleftSlide-in side: left or right.
spgrid_filters_button_textTEXTFiltersToolbar button label.
spgrid_filters_reset_textTEXTResetReset button label.
spgrid_filters_apply_textTEXTApplyApply button label.

Clearing a label restores its default These four text fields treat blank as “use the default” rather than rendering an empty button, so emptying one is how you undo an override. Note that a widget saved before a default changed keeps the old value — clear the field to pick up the new one.

The spgrid_filters repeater

One row becomes one collapsible group in the drawer. A Category row is present by default, so a freshly dropped widget already has a working filter — the toolbar's filter button only appears once at least one row exists.

FieldOptionsDescription
filter_sourcetaxonomies, author, date, date_range, post_type, acf:*What to filter by (see sources below).
filter_labelfree textOptional label; defaults to the source's name.
filter_displaydropdown · checkbox · radioHow options are rendered.
filter_logicOR (default) · ANDWith OR, ticking two boxes widens the results; with AND, a post must carry both to survive.

Native filter sources

  • Taxonomies — every public taxonomy (category, post_tag, customs…), keyed by taxonomy name.
  • author — all published authors.
  • date — month buckets (YYYYMM) covering the last 24 months.
  • date_range — From/To date inputs for a published-date range.
  • post_type — filter across the widget's selected post types.

Panel UX behavior

  • Open / close — toggle button opens the panel (is-open class); close via the ✕ button, overlay click, or Escape. Background scrolling is locked with body.spgrid-noscroll.
  • Accordion sections — each filter group collapses/expands (is-collapsed, with aria-expanded).
  • Adding is batched, removing is not — ticking boxes does nothing until Apply is pressed, so a visitor can assemble a selection without the grid thrashing. Unticking one, or dismissing a chip, takes effect immediately.
  • Chips mirror the drawer — whatever is applied appears as a dismissible chip above the grid, and dismissing one unticks the matching control inside the panel.
  • Reset — clears all inputs and reloads unfiltered results.

2.6 ACF Integration

Install ACF and its fields simply show up in the filter source list, prefixed acf:acf:project_status, and so on. Nothing needs registering. The field's own type decides what the filter looks like and how it is matched, so a date picker becomes a range and a checkbox becomes a choice list.

Supported field types

ACF Field TypeFilter InputMatching
select, checkbox, radio, button_groupChoice list (dropdown / checkbox / radio)Exact value match against the field's defined choices.
true_falseYes / No choicesBoolean (1/0).
taxonomyTerm listResolves selected term IDs.
post_object, relationship, page_linkPost list (max 200)Matches stored post IDs.
userUser listMatches stored user IDs.
text, textarea, email, url, wysiwyg, color_pickerText inputLIKE contains-search.
number, rangeMin / Max inputsNumeric comparison (min:X / max:Y).
date_picker, date_time_picker, time_pickerFrom / To date inputsDate range comparison (Ymd; from:DATE / to:DATE).

Performance & freshness

  • The ACF field map is cached for 1 hour in the spgrid_acf_fields_map_v2 transient.
  • Distinct meta values (for text/number filters) are cached for 10 minutes per field (spgrid_acf_vals_{hash}).
  • Caches flush automatically when field groups change (acf/update_field_group, trash, untrash, delete).

2.7 Card Templates

The built-in card is a starting point, not a ceiling. Design the card as a normal Elementor template, wire its text and images to dynamic tags, then point the widget at it — the template is repeated once per post with that post's data behind the tags.

ControlDefaultDescription
spgrid_template_id0 (built-in)Template used for each card. 0 uses the built-in card.
spgrid_empty_template_id0Template shown when no posts match. 0 shows the text message instead.
spgrid_empty_textNo posts found.Plain-text empty state (when no empty template selected).

How dynamic tags resolve

Before each card is rendered the loop context is made real: setup_postdata() runs, the global $post is swapped, and in_the_loop is set. Only then is the template rendered. The effect is that a dynamic tag behaves exactly as it would inside a Theme Builder loop — it has no way to tell the difference.

  • A template's CSS is emitted once per request, not once per card — fifty posts do not mean fifty copies of the same rules.
  • Cards that arrive by AJAX are handed back to Elementor's handler registry, so a carousel or lightbox inside your template still initialises on page four the same way it did on page one.

Which template type Any saved template will do, though section and container templates behave best as a repeating card. Pro is not a requirement — if it happens to be installed, its dynamic tags work here too.


2.8 Built-in Card

Leave spgrid_template_id at 0 and you get the bundled card instead. Every part of it — image, title, meta, excerpt, custom fields, button — can be switched off or moved, which covers most archives without opening the template editor at all.

Display toggles

ControlDefaultDescription
spgrid_show_imageyesFeatured image (size large, lazy-loaded).
spgrid_image_positiontopImage above or below the text content. Grid/masonry only — List layout always places the image in its side column.
spgrid_show_categoriesyesCategory links (max 2 terms; falls back to the first public taxonomy if the post has no categories). Rendered as a badge over the featured image, or inline in the meta row when the card has no image.
spgrid_show_metayesPublication date.
spgrid_date_formatm.d.yPHP date format (rendered via date_i18n()).
spgrid_show_authoroffAuthor in the meta row — the real post author, linked to their archive.
spgrid_author_prefixByShort prefix shown before the author name.
spgrid_author_customCustom static name (e.g. "Editorial Team") shown on every card instead of the post author; rendered as plain text, no link.
spgrid_show_excerptoffExcerpt text.
spgrid_excerpt_length20Excerpt length in words (1–100).
spgrid_show_read_moreoff"Read More" link.
spgrid_read_more_textRead MoreRead More label.

Custom fields on the card

Add rows to spgrid_card_acf_fields to surface field values on the card itself — they render between the excerpt and the Read More link. Each row is one value:

FieldDescription
acf_fieldPick any auto-detected ACF field, or choose Custom meta key… to use raw post meta.
acf_custom_keyThe meta key (shown when Custom meta key… is selected).
acf_labelOptional label rendered before the value (e.g. "Location:").
  • When ACF is present the value goes through get_field(), so return formats are honoured and a relationship or user field prints a name rather than an ID. Anything array-shaped is joined with commas.
  • Empty values drop out silently, and if every row is empty the list is not rendered at all — no stray bullet on posts that never filled the field in.
  • Markup: .spgrid-card__acf > .spgrid-card__acf-item > .spgrid-card__acf-label + .spgrid-card__acf-value.

Element ordering

Every element carries a number, and the card sorts by it — spgrid_order_category, spgrid_order_author, spgrid_order_date, spgrid_order_title, spgrid_order_excerpt, spgrid_order_acf and spgrid_order_read_more. It is the CSS order property underneath, so moving the date above the title costs a number change and no markup.

Markup

card markup (built-in)
<article class="spgrid-card">
  <div class="spgrid-card__body">
    <div class="spgrid-card__meta">
      <span class="spgrid-card__author">
        <span class="spgrid-card__author-prefix">By</span> <a href="…">Author Name</a>
      </span>
      <time class="spgrid-card__date">06.05.26</time>
    </div>
    <h3 class="spgrid-card__title"><a href="…">Post Title</a></h3>
    <p class="spgrid-card__excerpt">Excerpt text…</p>
    <ul class="spgrid-card__acf">
      <li class="spgrid-card__acf-item">
        <span class="spgrid-card__acf-label">Location:</span>
        <span class="spgrid-card__acf-value">Lahore</span>
      </li>
    </ul>
    <a class="spgrid-card__more" href="…">Read More →</a>
  </div>
  <div class="spgrid-card__thumb">
    <a class="spgrid-card__thumb-link" href="…" tabindex="-1" aria-hidden="true">
      <img src="…" alt="…" loading="lazy">
    </a>
    <span class="spgrid-card__terms spgrid-card__terms--badge">
      <a href="…">Category</a><span class="spgrid-card__sep">|</span><a href="…">Category</a>
    </span>
  </div>
</article>

Theme override

Copy templates/card-default.php from the plugin into your theme as yourtheme/spgrid/card.php to fully customize the built-in card markup.


2.9 Style Controls

Every visual aspect is controllable from Elementor's Style tab.

Style sections at a glance

SectionWhat you can style
CardsBackground, border, radius, shadow, padding, content spacing; title typography + colors (normal/hover); meta (category, author, date, separator) typography + colors; excerpt; ACF fields (typography, label/value colors, row spacing); read-more button (colors, backgrounds, border, padding, radius, full-width); image aspect ratio (16/9, 4/3, 3/2, 1/1, auto), hover zoom, opacity; hover lift (default 6 px) and hover shadow.
Empty StateTypography, color, alignment, padding.
ToolbarHeading typography/color/spacing; search box width, alignment, typography, border, and six color slots (text, background, label, icon, border, focus) per mode; icon buttons (size, radius, normal/hover/active colors and backgrounds).
FiltersPanel typography, text/background/title colors, panel width, overlay color; active chips (text/bg/border); section labels and option rows (typography, colors, checkbox accent); footer Apply/Reset buttons.
Pagination & Load MoreTypography; text, background, active, hover, and border colors per mode; radius; gap; Load More padding.

Key control IDs

GroupControl IDs
Card surfacespgrid_card_bg, spgrid_card_border, spgrid_card_radius, spgrid_card_shadow, spgrid_card_padding, spgrid_card_content_gap
Card titlespgrid_card_title_typo, spgrid_card_title_color, spgrid_card_title_hover_color
Category badgespgrid_card_terms_typo, spgrid_card_terms_color, spgrid_card_terms_hover_color, spgrid_card_terms_bg, spgrid_card_sep_color, spgrid_card_terms_border, spgrid_card_terms_padding, spgrid_card_terms_radius, spgrid_card_terms_position (which corner), spgrid_card_terms_offset
Card metaspgrid_card_meta_typo, spgrid_card_author_color, spgrid_card_author_hover_color, spgrid_card_date_color
Card ACF fieldsspgrid_card_acf_typo, spgrid_card_acf_label_color, spgrid_card_acf_value_color, spgrid_card_acf_gap
Read More buttonspgrid_card_more_typo, spgrid_card_more_color, spgrid_card_more_hover_color, spgrid_card_more_bg, spgrid_card_more_hover_bg, spgrid_card_more_border_color, spgrid_card_more_hover_border_color, spgrid_card_more_border, spgrid_card_more_padding, spgrid_card_more_radius, spgrid_card_more_width
Card imagespgrid_card_image_ratio, spgrid_card_image_hover_zoom, spgrid_card_image_opacity
Card hoverspgrid_card_hover_transform, spgrid_card_hover_shadow
Searchspgrid_search_width, spgrid_search_align, spgrid_search_typo, spgrid_search_color, spgrid_search_bg, spgrid_search_label_color, spgrid_search_icon_color, spgrid_search_border_color, spgrid_search_focus_color, spgrid_search_padding (default 12px 40px 12px 40px)
Icon buttonsspgrid_iconbtn_size, spgrid_iconbtn_radius, spgrid_iconbtn_color/_bg/_border_color with hover/active variants
Filter panelspgrid_filter_typo, spgrid_filter_color, spgrid_filter_panel_bg, spgrid_filter_title_color, spgrid_filter_panel_width, spgrid_filter_overlay_color
Chipsspgrid_chip_color, spgrid_chip_bg, spgrid_chip_border_color
Filter buttonsspgrid_filter_btn_typo, spgrid_filter_apply_color, spgrid_filter_apply_bg, spgrid_filter_reset_color, spgrid_filter_reset_border_color, spgrid_filter_btn_padding, spgrid_filter_btn_radius
Toolbar filter buttonspgrid_filter_toggle_typo, spgrid_filter_toggle_label_color
Paginationspgrid_pagination_typo, spgrid_pagination_color, spgrid_pagination_bg, spgrid_pagination_active_color, spgrid_pagination_active_bg, spgrid_pagination_hover_bg, spgrid_pagination_border_color, spgrid_pagination_radius, spgrid_pagination_gap, spgrid_loadmore_padding

Chapter Three

Developer

The AJAX contract, hooks, front-end behaviour, caching model and security posture.

3.1 AJAX API

Search, filtering and paging are all the same call. One endpoint is registered twice — wp_ajax_spgrid_query for signed-in users and wp_ajax_nopriv_spgrid_query for everyone else — and it returns rendered HTML rather than raw posts, so the card design stays on the server.

Request

POST /wp-admin/admin-ajax.php
action:    'spgrid_query'
nonce:     // CSRF token, verified against 'spgrid_nonce'
post_id:   // int — the page/document containing the widget
widget_id: // string — the Elementor element ID
search:    // string — search term
paged:     // int ≥ 1 — page number
filters:   // object — filters[filter_key][] = value…

Response

JSON response
{
  "success": true,
  "data": {
    "html":        "<div class=\"spgrid-grid__item\">…</div>…",
    "empty_html":  "<p class=\"spgrid-empty\">No posts found.</p>",
    "pagination":  "<ul class=\"spgrid-pagination__list\">…",
    "has_more":    false,   // load_more / infinite only
    "total_pages": 3,       // numbered only
    "current":     1,
    "found":       27,      // numbered only
    "is_empty":    false,
    "cached":      false    // true when served from transient cache
  }
}

Important properties

  • The request cannot widen the query. Settings are read back out of the saved Elementor document, so editing post_type or per_page in the payload changes nothing.
  • Only declared filters are honoured. Keys that the widget did not define are discarded before the query is built, which closes the door on arbitrary meta lookups.
  • A valid nonce is mandatory. Anything else gets a 403 and no query runs.
  • Repeat requests are served from cache (see Caching). The cached flag in the payload tells you which happened.

3.2 Hooks & Filters

Five extension points, all namespaced under spgrid/. Between them you can reshape any query, swap the fallback thumbnail, and decide whether and for how long a result is cached.

spgrid/bootedaction

Fires once boot has finished and every service is wired up. The Plugin instance comes with it, which makes this the right moment to hang your own integration off the container.

spgrid/query/argsfilter

The last stop before WP_Query runs. You get the assembled $args, the widget's $settings, and the $request state describing the current search term, page and filter selection — return the array you want executed.

spgrid/placeholder_imagefilter

The thumbnail used for posts with no featured image. Receives the bundled SVG's URL and the post ID. Return your own URL to swap it, or an empty string to drop the image slot entirely on those posts. Anything that is not a .svg is treated as a photo and cropped to fill rather than centred as a glyph.

spgrid/cache/enabledfilter

Return false to bypass the cache entirely. Editor and preview requests already skip it, so this is for production cases — a personalised grid, for instance.

spgrid/cache/expiryfilter

How long a cached response stays valid, in seconds. Five minutes by default; raise it for archives that rarely change.

Examples

functions.php — exclude a category everywhere
add_filter( 'spgrid/query/args', function ( $args, $settings, $request ) {
    // Always hide the "internal" category from every grid.
    $args['category__not_in'][] = 42;
    return $args;
}, 10, 3 );
functions.php — tune the cache
// Cache AJAX responses for 15 minutes instead of 5.
add_filter( 'spgrid/cache/expiry', fn() => 15 * MINUTE_IN_SECONDS );

// Disable caching entirely while developing.
add_filter( 'spgrid/cache/enabled', '__return_false' );

3.3 JavaScript API

The front-end script is plain JavaScript with no jQuery dependency. It constructs one Grid object per .spgrid-wrap on the page, each holding its own search, paged and filters state — so two grids on the same page never interfere with one another.

Localized config (SPGRID_CONFIG)

window.SPGRID_CONFIG
{
  ajaxUrl:  "https://yoursite.com/wp-admin/admin-ajax.php",
  nonce:    "…",            // wp_create_nonce('spgrid_nonce')
  action:   "spgrid_query",
  debounce: 300,            // search debounce in ms
  i18n: {
    loading:   "Loading…",
    noResults: "No posts found.",
    error:     "Something went wrong…",
    loadMore:  "Load More"
  }
}

Behaviour

BehaviorMechanism
TypingQueries 300 ms after the last keypress; adjust with SPGRID_CONFIG.debounce. Enter never submits a surrounding form.
Overlapping requestsA new request aborts the previous one, so a slow response can never overwrite a newer result.
Infinite scrollAn observer watches .spgrid-infinite__sentinel and fires 300 px early.
Loading statesaria-busy flips on the results region; the overlay, the search spinner and .spgrid-is-loading give you three separate styling hooks.
FailuresA dropped connection or expired nonce leaves a readable message in place of the grid, not an empty box.
Injected widgetsElementor handlers are re-run against newly inserted cards so their own scripts come alive.

3.4 Caching

The same search, on the same filters, on the same page, should not cost a second query. Responses are stored under a namespaced transient key; invalidation works by moving the namespace rather than hunting down individual entries.

Key structure

transient naming
spgrid_{group}_{generation}_{hash}
      │        │            └─ MD5 of the normalized (ksort'd) request args
      │        └─ incrementing generation counter (bumped on content changes)
      └─ logical group, e.g. "ajax"

The hash covers the document ID, the widget ID and the entire request state. If Polylang is running, the active language joins the key too, so translations never serve each other's cards.

Lifetimes & invalidation

CacheTTLInvalidated by
AJAX responses5 min (filterable via spgrid/cache/expiry)Generation bump on save_post, deleted_post, edited_terms, created_term
ACF field map1 hourACF field group save / trash / untrash / delete
ACF distinct values10 minNatural expiry
  • Invalidation is a counter, not a sweep. Publishing a post increments the generation, which orphans every old key at once and lets them expire quietly — no DELETE … LIKE across the options table on every save.
  • To clear it by hand call \Spgrid\Cache\CacheManager::flush_all(). Deactivating the plugin does the same thing.
  • The editor is exempt. Nothing is cached in the Elementor editor or its preview, so a control change shows its effect immediately.
  • Counting is skipped where it is pointless. Load More and infinite scroll never display a total, so the count query is turned off for them.

3.5 CSS Class Reference

These names are part of the plugin's contract and will not move between releases, so it is safe to hang your own CSS off them. The custom properties below are the cheaper hook where one exists — set the variable and every rule using it follows.

Structure

ClassElement
.spgrid-wrapWidget root (also carries .spgrid-is-loading).
.spgrid-layout--grid|list|masonryLayout modifier on the wrapper.
.spgrid-resultsResults region (aria-busy during AJAX).
.spgrid-grid / .spgrid-grid__itemThe grid (role="list") and its items.
.spgrid-card__thumb / .spgrid-card__thumb-linkMedia slot and the image's own (decorative) link. The slot is the positioning context for the badge.
.spgrid-card__terms--badgeCategory badge over the image. Corner offsets read the --spgrid-badge-offset custom property.
.spgrid-card + __body, __meta, __terms, __sep, __author, __date, __title, __excerpt, __acf, __acf-item, __acf-label, __acf-value, __more, __thumbBuilt-in card and its parts.
.spgrid-empty / .spgrid-errorEmpty state / error message.
.spgrid-filters (+ .is-open)Off-canvas panel (role="dialog").
.spgrid-filters__toggleFilters toolbar button.
.spgrid-filter__head (+ .is-collapsed)Accordion section header.
.spgrid-pagination / .spgrid-page[data-page]Numbered pagination and page links.
.spgrid-loadmore__btn[data-next-page]Load More button.
.spgrid-infinite__sentinelInfinite-scroll trigger element.
.spgrid-loader / .spgrid-search__spinnerLoading indicators.
body.spgrid-noscrollScroll lock while the filter panel is open.

CSS custom properties

VariablePurpose
--spgrid-columnsColumn count on .spgrid-grid (responsive).
--spgrid-gapGap between grid items.
--spgrid-panel-bgFilter panel background.
--spgrid-search-ml / --spgrid-controls-mlSearch/controls alignment helpers.

Accessibility

  • The markup says what it is — <time> around dates, <nav> around pagination, <aside> for the drawer.
  • The grid announces itself as a list, the drawer as a dialog, and state changes are carried by aria-busy, aria-expanded and aria-current so a screen reader hears results arrive.
  • Escape closes the drawer, and every control in it is reachable by keyboard alone.

3.6 Security

The guiding assumption is that the request is hostile. Nothing the browser sends is allowed to decide what gets queried — it can only say which page, which search term, and which of the filters the widget already declared.

LayerImplementation
CSRFA spgrid_nonce is minted with the page and checked on every request. No nonce, no query — the handler returns 403 before touching the database.
Settings isolationSettings come from the saved document, not the payload, so the request has no vocabulary for widening the query.
Input sanitisationsanitize_text_field() for search and IDs, absint() for numbers, an allow-list for orderby and order, and deep sanitisation of the filter array followed by re-validation against the widget's declared filters.
Scope limitsPost types are intersected with what the widget allows, taxonomies must actually exist, and only published posts are returned — drafts and private posts appear solely for users who could already edit them.
Output escapingEscaping happens at the point of output rather than on the way in, JSON goes out through the core helpers, and any author-supplied CSS selector or colour is filtered to a safe character set first.
CapabilitiesThe admin notices about missing requirements are only rendered for users who could act on them.

Chapter Four

Resources

Common questions and the release history.

4.1 FAQ

Does it require Elementor Pro?
No. The free Elementor is enough for everything documented here. If Pro happens to be installed, its templates and dynamic tags work too — there is no separate code path.
Does it require ACF?
No. ACF is picked up if it is there, and its fields join the filter source list on their own. Without it you keep taxonomies, author, date and post type.
Can I use a completely custom card design?
Two routes. Designers should build a Saved Template and select it under Card Template — dynamic tags resolve per post, no code involved. Developers who want the markup itself can copy templates/card-default.php into yourtheme/spgrid/card.php; the theme copy wins.
Do search, filters, and pagination work together?
Yes — they share one state object (search term, page, filters) that is sent whole on every request, so touching one leaves the others alone. The single deliberate exception: changing the search or the filters sends you back to page one, because staying on page seven of a different result set is rarely what anyone wants.
Why don't I see fresh posts immediately?
In practice you should. The five-minute TTL is a backstop; publishing or editing a post bumps the cache generation, which retires every stored response at once. If something upstream is caching harder — a page cache or CDN — that is the more likely culprit. To rule this layer out, call \Spgrid\Cache\CacheManager::flush_all() or filter spgrid/cache/enabled to false.
How do I hook into the query?
spgrid/query/args runs immediately before the query executes and hands you the arguments, the widget settings and the live request state. Hooks & Filters has worked examples.
Does it work with multilingual sites?
Polylang is accounted for: the active language forms part of the cache key, so one language can never serve another's cards. Every string in the plugin is wrapped for translation under the savvy-posts-grid text domain.
Is there a shortcode or settings page?
Neither. Configuration is stored with the page that uses it, which means duplicating a page duplicates its grid settings, and exporting a template carries them along.

4.2 Changelog

1.0.0

  • Initial release — AJAX search, AJAX pagination (numbered / load more / infinite), off-canvas filter builder (native + ACF), Elementor template cards, and namespaced transient caching.
  • Built-in card — toggleable, reorderable elements: image, title, meta, categories, author (linked or a custom static name with a configurable prefix), excerpt, ACF / custom-field rows, and Read More.
  • Read More button controls — padding, border, border radius, background + border colors (normal/hover), and an Auto/Full-Width option, so the link can be styled as a real button.
  • Identifiersspgrid_ control prefix, Spgrid\ namespace, savvy-posts-grid text domain, spgrid-* CSS classes, spgrid_query AJAX action, and spgrid/* hooks.