
Alphabetical Pagination (formerly WP-SNAP Extended!) builds an alphabetical index of post titles across any public post type. Visitors jump straight to a letter (A, B, C …), browse a paginated list of matching posts, and click through to each post’s permalink.
Built for modern WordPress: PHP 8.1+ typed classes, schema-validated options, prepared SQL throughout, semantic markup, no jQuery dependency, no bundled CSS framework, zero front-end JS by default.
final classes under includes/, every superglobal sanitised, every echoed value escaped, every DB call prepared.[alphabetical_pagination] shortcode, alphabetical_pagination() template tag, and a native Gutenberg block (wp-snap-ext/index).woocommerce_before_shop_loop action. No DOM hacks, no posts_where SQL filter injection.GET /wp-json/wp-snap-ext/v1/letters and /posts for headless / React / app integrations.do_action/apply_filters at every render path so agencies can customise without forking.save_post. Configurable TTL.get_fields() walk.<nav aria-label>, aria-current="page", aria-disabled on empty letters, explicit role="list" / role="listitem" (Safari + VoiceOver list-stripping fix), descriptive per-letter aria-label, visible focus outlines.wp-snap-ext/index block (no JS build pipeline required). Renders identically to the shortcode + template tag.woocommerce_before_shop_loop (default), woocommerce_archive_description, or woocommerce_before_main_content./wp-json/wp-snap-ext/v1/letters returns [{ letter, count, href }]; /wp-json/wp-snap-ext/v1/posts returns paginated post payloads.do_action( 'wp_snap_ext/before_render', $context )apply_filters( 'wp_snap_ext/pre_render', $html, $post_type, $display, $args ) — short-circuitapply_filters( 'wp_snap_ext/query_args', $args, $context )apply_filters( 'wp_snap_ext/letter_href', $href, $letter, $base )apply_filters( 'wp_snap_ext/excerpt', $excerpt, $post_id )apply_filters( 'wp_snap_ext/render', $html, $post_type, $display, $args )do_action( 'wp_snap_ext/after_render', $html, $context )save_post, deleted_post, trashed_post, untrashed_post, and switch_blog. Default TTL 1 hour, configurable.wpml_current_language or pll_current_language(); WP_Query runs with suppress_filters => false so translated post sets get filtered.aria-disabled), Hide Pagination If One Page.{ post_id => items_per_page } so /glossary can render 50 items per page while /products renders 20.wp_snap() template tag is preserved as a thin alias.?snap=, ?cp=, and ?snap_paged= continue to be honoured alongside the canonical ?alpha_order= and ?alpha_paged=.key_snap_* option keys are migrated to wp_snap_ext_* automatically on activation.Add the Alphabetical Pagination block from the block inserter (Widgets category) and configure attributes through the block sidebar:
postType — post type to index (default post).menu — 1, 2, or 3 (see menu styles in the admin panel).firstload — all, none, or recent.category — category ID or all.includeChildren — include category children.taxonomy + term — restrict the index to a specific term of any registered taxonomy.display — true (default) renders the post list under the letter nav; false renders only the letter nav.The block is fully server-rendered — its HTML matches the shortcode and template tag output byte for byte, and there is no JS build pipeline behind it.
Drop the shortcode into any post, page, widget, or Site Editor template part:
[alphabetical_pagination]
All template-tag arguments are exposed as shortcode attributes:
[alphabetical_pagination cat="15" child="true" menu="2" firstload="recent" post_type="post" display="true"]
Attribute reference:
cat — category ID, or all.child — true to include category children (default false).menu — 1, 2 or 3 (see menu styles in the admin panel).firstload — all, none or recent.post_parent — restrict to posts with a given parent ID.post_type — defaults to post. Whitelisted against registered post types.display — true (default) renders the post list under the letter nav; false renders only the letter nav.The shortcode handler buffers its output through ob_start() / ob_get_clean(), so the index renders exactly where you place the shortcode rather than breaking out of the surrounding layout.
For deeper theme integration, call alphabetical_pagination() directly from a template file. The legacy wp_snap() name is retained as a backwards-compatible alias.
<?php
if ( function_exists( 'alphabetical_pagination' ) ) {
echo alphabetical_pagination();
}
?>
Passing arguments works the same as the original wp_snap() API:
<?php
echo alphabetical_pagination( 'cat=15&child=true&firstload=recent' );
?>
Render an alphabetical index over a custom post type:
<?php
echo alphabetical_pagination( '', 'glossary_term' );
?>
Render only the letter navigation (without the post list):
<?php
echo alphabetical_pagination( '', 'post', false );
?>
Once embedded, the plugin reads two query parameters on the front end:
?alpha_order=A — the active letter (or bucket, like A-D). alpha_order=misc selects the # bucket of non-alphanumeric titles.?alpha_paged=2 — the active pagination page.These are isolated to the plugin (they do not collide with WordPress’s own paged / tag / cat query vars). The legacy ?snap= / ?cp= parameters from earlier versions are still accepted so existing bookmarks keep working.
Attribute
Type
Default
Notes
postType
string
post
Any registered public post type.
menu
number
1
1, 2, or 3.
firstload
string
recent
all / none / recent.
category
string
“
Category ID or all.
includeChildren
boolean
false
Include category children.
taxonomy
string
“
Any registered taxonomy slug.
term
number
0
Term ID for the taxonomy above.
display
boolean
true
Render the post list under the nav.
The block supports wide and full alignment via the supports.align declaration in block.json.
Two read-only public routes under /wp-json/wp-snap-ext/v1/:
GET /letters
Query params: post_type (default post), taxonomy, term.
Response (200 OK):
[ { "letter": "A", "count": 12, "href": "https://example.com/?alpha_order=A" }, … ]
GET /posts
Query params: post_type, taxonomy, term, letter (single character or #), page (default 1), per_page (default 10, max 100).
Response (200 OK):
{ "posts": [ { "id": 42, "title": "...", "permalink": "...", "excerpt": "..." } ], "total": 75, "total_pages": 8, "page": 1, "per_page": 10 }
Toggle the endpoints on / off under Settings Alphabetical Pagination REST API & Cache. Lockdown plugins that block public REST surface should leave the toggle off.
Enable Settings Alphabetical Pagination WooCommerce Auto-mount on Shop. The index renders above the shop loop (or after the archive description / before main content — pick the mount hook from the dropdown) on:
/product-category/food).The mount uses native WooCommerce actions, never posts_where, so it does not collide with caching plugins, SEO plugins, or multilingual plugins that also filter WP_Query.
Customise behaviour without forking through the following hooks (added in 2.3.0):
add_filter( 'wp_snap_ext/query_args', function( $args, $context ) {
$args['meta_query'] = [ [ 'key' => 'featured', 'value' => '1' ] ];
return $args;
}, 10, 2 );
add_filter( 'wp_snap_ext/letter_href', function( $href, $letter, $base ) {
return str_replace( '?alpha_order=', '#letter/', $href );
}, 10, 3 );
add_filter( 'wp_snap_ext/excerpt', function( $excerpt, $post_id ) {
return wp_trim_words( $excerpt, 25, '…' );
}, 10, 2 );
Full list:
do_action( 'wp_snap_ext/before_render', $context ) — fires before the index renders.apply_filters( 'wp_snap_ext/pre_render', $html, $post_type, $display, $args ) — short-circuit; return a string to replace the HTML.apply_filters( 'wp_snap_ext/query_args', $args, $context ) — mutate WP_Query arguments.apply_filters( 'wp_snap_ext/letter_href', $href, $letter, $base ) — rewrite letter link hrefs (router compatibility).apply_filters( 'wp_snap_ext/excerpt', $excerpt, $post_id ) — post-process the resolved excerpt.apply_filters( 'wp_snap_ext/render', $html, $post_type, $display, $args ) — final filter on rendered HTML.do_action( 'wp_snap_ext/after_render', $html, $context ) — fires after the index has rendered.Under Settings Alphabetical Pagination you’ll find:
absint().get_field() get_sub_field() recursive get_fields() walk for deeply nested flexible-content / repeater / group sub-fields.get_field() when the toggle is enabled. The plugin gracefully no-ops if ACF (or ACF Pro) is not installed.aria-disabled, with the anchor stripped.