{"id":19407,"date":"2012-10-25T07:23:12","date_gmt":"2012-10-25T07:23:12","guid":{"rendered":"https:\/\/wordpress.org\/plugins-wp\/wp-snap-extended\/"},"modified":"2026-06-09T03:39:07","modified_gmt":"2026-06-09T03:39:07","slug":"wp-snap-extended","status":"publish","type":"plugin","link":"https:\/\/cn.wordpress.org\/plugins\/wp-snap-extended\/","author":10002000,"comment_status":"closed","ping_status":"closed","template":"","meta":{"version":"2.4.2","stable_tag":"2.4.2","tested":"7.0","requires":"5.0","requires_php":"8.1","requires_plugins":null,"header_name":"Alphabetical pagination","header_author":"Mansoor Munib","header_description":"","assets_banners_color":"","last_updated":"2026-06-09 03:39:07","external_support_url":"","external_repository_url":"","donate_link":"","header_plugin_uri":"https:\/\/wordpress.org\/plugins\/wp-snap-extended\/","header_author_uri":"https:\/\/github.com\/mansoormunib","rating":5,"author_block_rating":0,"active_installs":30,"downloads":5198,"num_ratings":1,"support_threads":0,"support_threads_resolved":0,"author_block_count":0,"sections":["description","installation","faq","changelog"],"tags":{"2.4.0":{"tag":"2.4.0","author":"mansoormunib","date":"2026-06-09 03:29:04"},"2.4.1":{"tag":"2.4.1","author":"mansoormunib","date":"2026-06-09 03:35:42"},"2.4.2":{"tag":"2.4.2","author":"mansoormunib","date":"2026-06-09 03:39:07"}},"upgrade_notice":[],"ratings":{"1":0,"2":0,"3":0,"4":0,"5":"1"},"assets_icons":[],"assets_banners":[],"assets_blueprints":{},"all_blocks":[],"tagged_versions":["2.4.0","2.4.1","2.4.2"],"block_files":[],"assets_screenshots":{"screenshot-1.png":{"filename":"screenshot-1.png","revision":3565383,"resolution":"1","location":"assets","locale":"","width":1920,"height":2160}},"screenshots":[]},"plugin_section":[],"plugin_tags":[1406,2409,1403,1753,900],"plugin_category":[43],"plugin_contributors":[84923],"plugin_business_model":[],"class_list":["post-19407","plugin","type-plugin","status-publish","hentry","plugin_tags-alphabetical","plugin_tags-glossary","plugin_tags-index","plugin_tags-navigation","plugin_tags-pagination","plugin_category-customization","plugin_contributors-mansoormunib","plugin_committers-mansoormunib"],"banners":[],"icons":{"svg":false,"icon":"https:\/\/s.w.org\/plugins\/geopattern-icon\/wp-snap-extended.svg","icon_2x":false,"generated":true},"screenshots":[{"src":"https:\/\/ps.w.org\/wp-snap-extended\/assets\/screenshot-1.png?rev=3565383","caption":""}],"raw_content":"<!--section=description-->\n<p><strong>Alphabetical Pagination<\/strong> (formerly <em>WP-SNAP Extended!<\/em>) builds an alphabetical index of post titles across any public post type. Visitors jump straight to a letter (A, B, C \u2026), browse a paginated list of matching posts, and click through to each post's permalink.<\/p>\n\n<p>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.<\/p>\n\n<h4>Why use it<\/h4>\n\n<ul>\n<li><strong>Modern PHP 8.1+ architecture<\/strong> \u2014 typed <code>final<\/code> classes under <code>includes\/<\/code>, every superglobal sanitised, every echoed value escaped, every DB call prepared.<\/li>\n<li><strong>Three embedding surfaces<\/strong> \u2014 <code>[alphabetical_pagination]<\/code> shortcode, <code>alphabetical_pagination()<\/code> template tag, and a native Gutenberg block (<code>wp-snap-ext\/index<\/code>).<\/li>\n<li><strong>WooCommerce auto-mount<\/strong> \u2014 one click renders the index above the shop loop or product category archives via the native <code>woocommerce_before_shop_loop<\/code> action. No DOM hacks, no <code>posts_where<\/code> SQL filter injection.<\/li>\n<li><strong>15 bundled alphabet packs<\/strong> \u2014 English, Arabic, Chinese (Pinyin), German, Spanish, French, Greek, Hebrew, Hindi, Hungarian, Korean (Hangul Jamo), Russian, Thai, Turkish, Urdu.<\/li>\n<li><strong>WPML + Polylang aware<\/strong> \u2014 the letter cache keys on current language, so translated post sets render and cache per-language automatically.<\/li>\n<li><strong>REST API<\/strong> \u2014 <code>GET \/wp-json\/wp-snap-ext\/v1\/letters<\/code> and <code>\/posts<\/code> for headless \/ React \/ app integrations.<\/li>\n<li><strong>Documented developer hook API<\/strong> \u2014 <code>do_action<\/code>\/<code>apply_filters<\/code> at every render path so agencies can customise without forking.<\/li>\n<li><strong>Transient letter-availability cache<\/strong> \u2014 letter counts cached as transients, busted on <code>save_post<\/code>. Configurable TTL.<\/li>\n<li><strong>ACF excerpt fallback<\/strong> \u2014 supports top-level, sub-field, and deep flexible-content \/ repeater \/ group lookups via recursive <code>get_fields()<\/code> walk.<\/li>\n<li><strong>WCAG 2.1 AA<\/strong> \u2014 <code>&lt;nav aria-label&gt;<\/code>, <code>aria-current=\"page\"<\/code>, <code>aria-disabled<\/code> on empty letters, explicit <code>role=\"list\"<\/code> \/ <code>role=\"listitem\"<\/code> (Safari + VoiceOver list-stripping fix), descriptive per-letter <code>aria-label<\/code>, visible focus outlines.<\/li>\n<li><strong>Zero front-end JS dependency by default<\/strong> \u2014 no jQuery, no Bootstrap, no FontAwesome. Stylesheet is ~1 KB.<\/li>\n<\/ul>\n\n<h4>2.3.0 feature surface<\/h4>\n\n<ul>\n<li><strong>Gutenberg block<\/strong> \u2014 server-rendered <code>wp-snap-ext\/index<\/code> block (no JS build pipeline required). Renders identically to the shortcode + template tag.<\/li>\n<li><strong>WooCommerce auto-mount<\/strong> \u2014 toggle in <strong>Settings \u2192 Alphabetical Pagination \u2192 WooCommerce<\/strong>. Mount hook selectable: <code>woocommerce_before_shop_loop<\/code> (default), <code>woocommerce_archive_description<\/code>, or <code>woocommerce_before_main_content<\/code>.<\/li>\n<li><strong>REST API endpoints<\/strong> \u2014 <code>\/wp-json\/wp-snap-ext\/v1\/letters<\/code> returns <code>[{ letter, count, href }]<\/code>; <code>\/wp-json\/wp-snap-ext\/v1\/posts<\/code> returns paginated post payloads.<\/li>\n<li><strong>Developer hooks<\/strong>:\n\n<ul>\n<li><code>do_action( 'wp_snap_ext\/before_render', $context )<\/code><\/li>\n<li><code>apply_filters( 'wp_snap_ext\/pre_render', $html, $post_type, $display, $args )<\/code> \u2014 short-circuit<\/li>\n<li><code>apply_filters( 'wp_snap_ext\/query_args', $args, $context )<\/code><\/li>\n<li><code>apply_filters( 'wp_snap_ext\/letter_href', $href, $letter, $base )<\/code><\/li>\n<li><code>apply_filters( 'wp_snap_ext\/excerpt', $excerpt, $post_id )<\/code><\/li>\n<li><code>apply_filters( 'wp_snap_ext\/render', $html, $post_type, $display, $args )<\/code><\/li>\n<li><code>do_action( 'wp_snap_ext\/after_render', $html, $context )<\/code><\/li>\n<\/ul><\/li>\n<li><strong>Transient letter-availability cache<\/strong> \u2014 keyed by post_type, taxonomy, term, current language, alphabet pack, and menumisc setting. Invalidated on <code>save_post<\/code>, <code>deleted_post<\/code>, <code>trashed_post<\/code>, <code>untrashed_post<\/code>, and <code>switch_blog<\/code>. Default TTL 1 hour, configurable.<\/li>\n<li><strong>WPML + Polylang awareness<\/strong> \u2014 cache key includes <code>wpml_current_language<\/code> or <code>pll_current_language()<\/code>; WP_Query runs with <code>suppress_filters =&gt; false<\/code> so translated post sets get filtered.<\/li>\n<li><strong>15 multi-language alphabet packs<\/strong> \u2014 pick a script from the dropdown, the freeform Local Alphabet field is overwritten on save.<\/li>\n<li><strong>Appearance toggles<\/strong> \u2014 Horizontal \/ Vertical layout, Uppercase \/ Lowercase letter case, Disable Empty Letters (renders empty buckets as muted + <code>aria-disabled<\/code>), Hide Pagination If One Page.<\/li>\n<li><strong>Generic taxonomy filter<\/strong> \u2014 restrict the index to any registered taxonomy + term (beyond the legacy cat\/tag args).<\/li>\n<li><strong>Meta-key intra-bucket sorting<\/strong> \u2014 set a post meta key + ASC \/ DESC to override post_title ordering within each letter bucket. Buckets still derive from post_title.<\/li>\n<li><strong>Per-page override map<\/strong> \u2014 <code>{ post_id =&gt; items_per_page }<\/code> so \/glossary can render 50 items per page while \/products renders 20.<\/li>\n<li><strong>DOM auto-injection<\/strong> \u2014 for themes that don't expose a hook: render the index in the footer and move it into a CSS selector via ~300 bytes of vanilla JS. No jQuery.<\/li>\n<\/ul>\n\n<h4>Backwards compatibility<\/h4>\n\n<ul>\n<li>The legacy <code>wp_snap()<\/code> template tag is preserved as a thin alias.<\/li>\n<li>Legacy URL parameters <code>?snap=<\/code>, <code>?cp=<\/code>, and <code>?snap_paged=<\/code> continue to be honoured alongside the canonical <code>?alpha_order=<\/code> and <code>?alpha_paged=<\/code>.<\/li>\n<li>Legacy <code>key_snap_*<\/code> option keys are migrated to <code>wp_snap_ext_*<\/code> automatically on activation.<\/li>\n<\/ul>\n\n<h3>Usage<\/h3>\n\n<h4>Gutenberg block<\/h4>\n\n<p>Add the <strong>Alphabetical Pagination<\/strong> block from the block inserter (Widgets category) and configure attributes through the block sidebar:<\/p>\n\n<ul>\n<li><code>postType<\/code> \u2014 post type to index (default <code>post<\/code>).<\/li>\n<li><code>menu<\/code> \u2014 <code>1<\/code>, <code>2<\/code>, or <code>3<\/code> (see menu styles in the admin panel).<\/li>\n<li><code>firstload<\/code> \u2014 <code>all<\/code>, <code>none<\/code>, or <code>recent<\/code>.<\/li>\n<li><code>category<\/code> \u2014 category ID or <code>all<\/code>.<\/li>\n<li><code>includeChildren<\/code> \u2014 include category children.<\/li>\n<li><code>taxonomy<\/code> + <code>term<\/code> \u2014 restrict the index to a specific term of any registered taxonomy.<\/li>\n<li><code>display<\/code> \u2014 <code>true<\/code> (default) renders the post list under the letter nav; <code>false<\/code> renders only the letter nav.<\/li>\n<\/ul>\n\n<p>The block is fully server-rendered \u2014 its HTML matches the shortcode and template tag output byte for byte, and there is no JS build pipeline behind it.<\/p>\n\n<h4>Shortcode<\/h4>\n\n<p>Drop the shortcode into any post, page, widget, or Site Editor template part:<\/p>\n\n<pre><code>[alphabetical_pagination]\n<\/code><\/pre>\n\n<p>All template-tag arguments are exposed as shortcode attributes:<\/p>\n\n<pre><code>[alphabetical_pagination cat=\"15\" child=\"true\" menu=\"2\" firstload=\"recent\" post_type=\"post\" display=\"true\"]\n<\/code><\/pre>\n\n<p>Attribute reference:<\/p>\n\n<ul>\n<li><code>cat<\/code> \u2014 category ID, or <code>all<\/code>.<\/li>\n<li><code>child<\/code> \u2014 <code>true<\/code> to include category children (default <code>false<\/code>).<\/li>\n<li><code>menu<\/code> \u2014 1, 2 or 3 (see menu styles in the admin panel).<\/li>\n<li><code>firstload<\/code> \u2014 <code>all<\/code>, <code>none<\/code> or <code>recent<\/code>.<\/li>\n<li><code>post_parent<\/code> \u2014 restrict to posts with a given parent ID.<\/li>\n<li><code>post_type<\/code> \u2014 defaults to <code>post<\/code>. Whitelisted against registered post types.<\/li>\n<li><code>display<\/code> \u2014 <code>true<\/code> (default) renders the post list under the letter nav; <code>false<\/code> renders only the letter nav.<\/li>\n<\/ul>\n\n<p>The shortcode handler buffers its output through <code>ob_start()<\/code> \/ <code>ob_get_clean()<\/code>, so the index renders exactly where you place the shortcode rather than breaking out of the surrounding layout.<\/p>\n\n<h4>Theme template tag<\/h4>\n\n<p>For deeper theme integration, call <code>alphabetical_pagination()<\/code> directly from a template file. The legacy <code>wp_snap()<\/code> name is retained as a backwards-compatible alias.<\/p>\n\n<pre><code>&lt;?php\nif ( function_exists( 'alphabetical_pagination' ) ) {\n    echo alphabetical_pagination();\n}\n?&gt;\n<\/code><\/pre>\n\n<p>Passing arguments works the same as the original <code>wp_snap()<\/code> API:<\/p>\n\n<pre><code>&lt;?php\necho alphabetical_pagination( 'cat=15&amp;child=true&amp;firstload=recent' );\n?&gt;\n<\/code><\/pre>\n\n<p>Render an alphabetical index over a custom post type:<\/p>\n\n<pre><code>&lt;?php\necho alphabetical_pagination( '', 'glossary_term' );\n?&gt;\n<\/code><\/pre>\n\n<p>Render only the letter navigation (without the post list):<\/p>\n\n<pre><code>&lt;?php\necho alphabetical_pagination( '', 'post', false );\n?&gt;\n<\/code><\/pre>\n\n<h4>URL query parameters<\/h4>\n\n<p>Once embedded, the plugin reads two query parameters on the front end:<\/p>\n\n<ul>\n<li><code>?alpha_order=A<\/code> \u2014 the active letter (or bucket, like <code>A-D<\/code>). <code>alpha_order=misc<\/code> selects the <code>#<\/code> bucket of non-alphanumeric titles.<\/li>\n<li><code>?alpha_paged=2<\/code> \u2014 the active pagination page.<\/li>\n<\/ul>\n\n<p>These are isolated to the plugin (they do not collide with WordPress's own <code>paged<\/code> \/ <code>tag<\/code> \/ <code>cat<\/code> query vars). The legacy <code>?snap=<\/code> \/ <code>?cp=<\/code> parameters from earlier versions are still accepted so existing bookmarks keep working.<\/p>\n\n<h4>Gutenberg block attributes<\/h4>\n\n\n\n\n  Attribute\n  Type\n  Default\n  Notes\n\n\n\n\n  <code>postType<\/code>\n  string\n  <code>post<\/code>\n  Any registered public post type.\n\n\n  <code>menu<\/code>\n  number\n  <code>1<\/code>\n  1, 2, or 3.\n\n\n  <code>firstload<\/code>\n  string\n  <code>recent<\/code>\n  <code>all<\/code> \/ <code>none<\/code> \/ <code>recent<\/code>.\n\n\n  <code>category<\/code>\n  string\n  ``\n  Category ID or <code>all<\/code>.\n\n\n  <code>includeChildren<\/code>\n  boolean\n  <code>false<\/code>\n  Include category children.\n\n\n  <code>taxonomy<\/code>\n  string\n  ``\n  Any registered taxonomy slug.\n\n\n  <code>term<\/code>\n  number\n  <code>0<\/code>\n  Term ID for the taxonomy above.\n\n\n  <code>display<\/code>\n  boolean\n  <code>true<\/code>\n  Render the post list under the nav.\n\n\n\n\n<p>The block supports <code>wide<\/code> and <code>full<\/code> alignment via the <code>supports.align<\/code> declaration in <code>block.json<\/code>.<\/p>\n\n<h4>REST API<\/h4>\n\n<p>Two read-only public routes under <code>\/wp-json\/wp-snap-ext\/v1\/<\/code>:<\/p>\n\n<p><strong><code>GET \/letters<\/code><\/strong><\/p>\n\n<p>Query params: <code>post_type<\/code> (default <code>post<\/code>), <code>taxonomy<\/code>, <code>term<\/code>.<\/p>\n\n<p>Response (200 OK):<\/p>\n\n<pre><code>[ { \"letter\": \"A\", \"count\": 12, \"href\": \"https:\/\/example.com\/?alpha_order=A\" }, \u2026 ]\n<\/code><\/pre>\n\n<p><strong><code>GET \/posts<\/code><\/strong><\/p>\n\n<p>Query params: <code>post_type<\/code>, <code>taxonomy<\/code>, <code>term<\/code>, <code>letter<\/code> (single character or <code>#<\/code>), <code>page<\/code> (default 1), <code>per_page<\/code> (default 10, max 100).<\/p>\n\n<p>Response (200 OK):<\/p>\n\n<pre><code>{ \"posts\": [ { \"id\": 42, \"title\": \"...\", \"permalink\": \"...\", \"excerpt\": \"...\" } ], \"total\": 75, \"total_pages\": 8, \"page\": 1, \"per_page\": 10 }\n<\/code><\/pre>\n\n<p>Toggle the endpoints on \/ off under <strong>Settings \u2192 Alphabetical Pagination \u2192 REST API &amp; Cache<\/strong>. Lockdown plugins that block public REST surface should leave the toggle off.<\/p>\n\n<h4>WooCommerce auto-mount<\/h4>\n\n<p>Enable <strong>Settings \u2192 Alphabetical Pagination \u2192 WooCommerce \u2192 Auto-mount on Shop<\/strong>. The index renders above the shop loop (or after the archive description \/ before main content \u2014 pick the mount hook from the dropdown) on:<\/p>\n\n<ul>\n<li>the WooCommerce shop archive,<\/li>\n<li>product taxonomy archives (e.g. <code>\/product-category\/food<\/code>).<\/li>\n<\/ul>\n\n<p>The mount uses native WooCommerce actions, never <code>posts_where<\/code>, so it does not collide with caching plugins, SEO plugins, or multilingual plugins that also filter WP_Query.<\/p>\n\n<h4>Developer hooks<\/h4>\n\n<p>Customise behaviour without forking through the following hooks (added in 2.3.0):<\/p>\n\n<pre><code>add_filter( 'wp_snap_ext\/query_args', function( $args, $context ) {\n    $args['meta_query'] = [ [ 'key' =&gt; 'featured', 'value' =&gt; '1' ] ];\n    return $args;\n}, 10, 2 );\n\nadd_filter( 'wp_snap_ext\/letter_href', function( $href, $letter, $base ) {\n    return str_replace( '?alpha_order=', '#letter\/', $href );\n}, 10, 3 );\n\nadd_filter( 'wp_snap_ext\/excerpt', function( $excerpt, $post_id ) {\n    return wp_trim_words( $excerpt, 25, '\u2026' );\n}, 10, 2 );\n<\/code><\/pre>\n\n<p>Full list:<\/p>\n\n<ul>\n<li><code>do_action( 'wp_snap_ext\/before_render', $context )<\/code> \u2014 fires before the index renders.<\/li>\n<li><code>apply_filters( 'wp_snap_ext\/pre_render', $html, $post_type, $display, $args )<\/code> \u2014 short-circuit; return a string to replace the HTML.<\/li>\n<li><code>apply_filters( 'wp_snap_ext\/query_args', $args, $context )<\/code> \u2014 mutate WP_Query arguments.<\/li>\n<li><code>apply_filters( 'wp_snap_ext\/letter_href', $href, $letter, $base )<\/code> \u2014 rewrite letter link hrefs (router compatibility).<\/li>\n<li><code>apply_filters( 'wp_snap_ext\/excerpt', $excerpt, $post_id )<\/code> \u2014 post-process the resolved excerpt.<\/li>\n<li><code>apply_filters( 'wp_snap_ext\/render', $html, $post_type, $display, $args )<\/code> \u2014 final filter on rendered HTML.<\/li>\n<li><code>do_action( 'wp_snap_ext\/after_render', $html, $context )<\/code> \u2014 fires after the index has rendered.<\/li>\n<\/ul>\n\n<h4>Backend settings<\/h4>\n\n<p>Under <strong>Settings \u2192 Alphabetical Pagination<\/strong> you'll find:<\/p>\n\n<ul>\n<li><strong>Navigational Menu Options<\/strong> \u2014 Local Alphabet, Menu Style, Group Posts, Recent Posts, CSS class names, Ignore When Alphabetizing.<\/li>\n<li><strong>Presentational Options<\/strong> \u2014 Fancy URLs, Fancy URL Name, Tabs.<\/li>\n<li><strong>Pagination \u2192 Items Per Page<\/strong> \u2014 integer, defaults to 10. Registered through the WordPress Settings API and sanitised with <code>absint()<\/code>.<\/li>\n<li><strong>Content Fallback \u2192 ACF Excerpt Fallback<\/strong> \u2014 checkbox. When on, posts without a native WordPress excerpt fall back to the value of an ACF field instead of the trimmed post content. ACF lookup chain: <code>get_field()<\/code> \u2192 <code>get_sub_field()<\/code> \u2192 recursive <code>get_fields()<\/code> walk for deeply nested flexible-content \/ repeater \/ group sub-fields.<\/li>\n<li><strong>Content Fallback \u2192 ACF Field Name<\/strong> \u2014 the field name (or key) read by <code>get_field()<\/code> when the toggle is enabled. The plugin gracefully no-ops if ACF (or ACF Pro) is not installed.<\/li>\n<li><strong>Appearance \u2192 Layout<\/strong> \u2014 Horizontal or Vertical letter strip.<\/li>\n<li><strong>Appearance \u2192 Letter Case<\/strong> \u2014 Uppercase or Lowercase.<\/li>\n<li><strong>Appearance \u2192 Disable Empty Letters<\/strong> \u2014 renders empty buckets as muted + <code>aria-disabled<\/code>, with the anchor stripped.<\/li>\n<li><strong>Appearance \u2192 Hide Pagination If One Page<\/strong> \u2014 skips the Previous\/Next strip when the filtered set fits on one page.<\/li>\n<li><strong>Language \u2192 Alphabet Pack<\/strong> \u2014 pick from 15 bundled scripts. Selecting a pack overwrites the freeform Local Alphabet field on save.<\/li>\n<li><strong>Sorting \u2192 Meta Key<\/strong> \u2014 post meta key used for intra-bucket ordering instead of post_title.<\/li>\n<li><strong>Sorting \u2192 Meta Order<\/strong> \u2014 ASC \/ DESC.<\/li>\n<li><strong>Taxonomy Filter \u2192 Taxonomy + Term ID<\/strong> \u2014 restrict the index to a single term of any registered taxonomy.<\/li>\n<li><strong>WooCommerce \u2192 Auto-mount on Shop<\/strong> + <strong>Mount Hook<\/strong> \u2014 see the WooCommerce section above.<\/li>\n<li><strong>DOM Injection \u2192 Enable DOM Injection<\/strong> + <strong>Target Selector<\/strong> \u2014 print the index in the footer and move it into a CSS selector via vanilla JS.<\/li>\n<li><strong>REST API &amp; Cache \u2192 Enable REST Endpoints<\/strong> + <strong>Cache TTL<\/strong> \u2014 see the REST API section above.<\/li>\n<\/ul>\n\n<!--section=installation-->\n<ol>\n<li>Upload the <code>wp-snap-extended<\/code> directory to <code>\/wp-content\/plugins\/<\/code>.<\/li>\n<li>Activate the plugin through the <strong>Plugins<\/strong> screen in WordPress.<\/li>\n<li>Visit <strong>Settings \u2192 Alphabetical Pagination<\/strong> to configure the menu style, alphabet pack, pagination limit, WooCommerce mount, content fallback, REST API, and other options.<\/li>\n<li>Embed the index in any post or page with the Gutenberg block, the shortcode, or the template tag from your theme.<\/li>\n<\/ol>\n\n<!--section=faq-->\n<dl>\n<dt id=\"does%20the%20plugin%20still%20work%20if%20i%27m%20calling%20%60wp_snap%28%29%60%20from%20my%20theme%3F\"><h3>Does the plugin still work if I'm calling `wp_snap()` from my theme?<\/h3><\/dt>\n<dd><p>Yes. <code>wp_snap()<\/code> is kept as a thin alias for <code>alphabetical_pagination()<\/code> with the same parameter list, so existing themes continue to work unchanged.<\/p><\/dd>\n<dt id=\"i%20need%20to%20embed%20the%20index%20inside%20a%20gutenberg%20page.\"><h3>I need to embed the index inside a Gutenberg page.<\/h3><\/dt>\n<dd><p>Use the native <strong>Alphabetical Pagination<\/strong> block (added in 2.3.0) from the block inserter under the Widgets category \u2014 it's fully server-rendered and supports wide \/ full alignment. The block sidebar exposes post type, menu style, firstload, category, taxonomy + term, and display attributes. The legacy approach (Shortcode block with <code>[alphabetical_pagination]<\/code>) still works.<\/p><\/dd>\n<dt id=\"does%20it%20integrate%20with%20woocommerce%3F\"><h3>Does it integrate with WooCommerce?<\/h3><\/dt>\n<dd><p>Yes. Enable <strong>Settings \u2192 Alphabetical Pagination \u2192 WooCommerce \u2192 Auto-mount on Shop<\/strong> and the index renders above the WooCommerce shop loop and product category archives automatically. The mount uses native WooCommerce action hooks (<code>woocommerce_before_shop_loop<\/code>, <code>woocommerce_archive_description<\/code>, or <code>woocommerce_before_main_content<\/code> \u2014 pick from the dropdown) so it never collides with caching \/ SEO \/ multilingual plugins that filter WP_Query.<\/p><\/dd>\n<dt id=\"is%20it%20wpml%20%2F%20polylang%20compatible%3F\"><h3>Is it WPML \/ Polylang compatible?<\/h3><\/dt>\n<dd><p>Yes. The transient letter-availability cache keys on the current language (via <code>wpml_current_language<\/code> filter or <code>pll_current_language()<\/code> function), and <code>WP_Query<\/code> runs with <code>suppress_filters =&gt; false<\/code>, so translated post sets are filtered and cached per-language automatically. Switching language busts the cache for that language only.<\/p><\/dd>\n<dt id=\"can%20i%20get%20the%20data%20over%20rest%20for%20a%20headless%20front%20end%3F\"><h3>Can I get the data over REST for a headless front end?<\/h3><\/dt>\n<dd><p>Yes. Two read-only public endpoints under <code>\/wp-json\/wp-snap-ext\/v1\/<\/code>: <code>GET \/letters<\/code> returns <code>[{ letter, count, href }]<\/code>, <code>GET \/posts<\/code> returns paginated post payloads filtered by letter. Toggle on \/ off under <strong>Settings \u2192 Alphabetical Pagination \u2192 REST API &amp; Cache<\/strong>.<\/p><\/dd>\n<dt id=\"how%20do%20i%20display%20the%20acf%20field%20instead%20of%20the%20trimmed%20post%20content%3F\"><h3>How do I display the ACF field instead of the trimmed post content?<\/h3><\/dt>\n<dd><p>Go to <strong>Settings \u2192 Alphabetical Pagination \u2192 Content Fallback<\/strong>, enable <strong>ACF Excerpt Fallback<\/strong>, and enter the ACF <strong>Field Name<\/strong> (e.g. <code>summary<\/code>). The plugin tries <code>get_field()<\/code> first, then <code>get_sub_field()<\/code>, then walks the entire <code>get_fields()<\/code> tree recursively to find the field even when it lives inside an ACF flexible-content layout, repeater row, or group. Falls back to a trimmed extract of the post content if ACF returns nothing.<\/p><\/dd>\n<dt id=\"my%20theme%20has%20no%20obvious%20hook%20to%20drop%20the%20index%20into.\"><h3>My theme has no obvious hook to drop the index into.<\/h3><\/dt>\n<dd><p>Enable <strong>Settings \u2192 Alphabetical Pagination \u2192 DOM Injection \u2192 Enable DOM Injection<\/strong> and provide a CSS selector (e.g. <code>.entry-content<\/code>, <code>#primary &gt; article:first-child<\/code>). The plugin renders the index into a hidden <code>&lt;template&gt;<\/code> element in the footer and a ~300-byte vanilla-JS snippet moves it into the matched element on <code>DOMContentLoaded<\/code>. No jQuery, no dependencies.<\/p><\/dd>\n<dt id=\"how%20do%20i%20sort%20posts%20by%20a%20custom%20field%20instead%20of%20the%20title%3F\"><h3>How do I sort posts by a custom field instead of the title?<\/h3><\/dt>\n<dd><p>Under <strong>Settings \u2192 Alphabetical Pagination \u2192 Sorting<\/strong>, set <strong>Meta Key<\/strong> to your post meta key and <strong>Meta Order<\/strong> to ASC or DESC. The letter buckets still derive from <code>post_title<\/code>, but the order of posts <em>within<\/em> each bucket follows the meta value. Useful for sorting glossary terms by importance, products by SKU, etc.<\/p><\/dd>\n<dt id=\"can%20i%20use%20a%20non-latin%20alphabet%20%28arabic%2C%20chinese%20pinyin%2C%20cyrillic%2C%20greek%2C%20hebrew%2C%20hindi%2C%20korean%2C%20thai%2C%20%E2%80%A6%29%3F\"><h3>Can I use a non-Latin alphabet (Arabic, Chinese pinyin, Cyrillic, Greek, Hebrew, Hindi, Korean, Thai, \u2026)?<\/h3><\/dt>\n<dd><p>Yes. Pick the script from <strong>Settings \u2192 Alphabetical Pagination \u2192 Language \u2192 Alphabet Pack<\/strong>. 15 packs are bundled: English, Arabic, Chinese (Pinyin A\u2013Z), German (with Umlauts), Spanish (with \u00d1), French, Greek, Hebrew, Hindi (Devanagari), Hungarian, Korean (Hangul Jamo), Russian (Cyrillic), Thai, Turkish, Urdu. Selecting a pack overwrites the freeform Local Alphabet field on save. You can also type a fully custom alphabet directly.<\/p><\/dd>\n<dt id=\"are%20the%20letter%20counts%20cached%3F\"><h3>Are the letter counts cached?<\/h3><\/dt>\n<dd><p>Yes. Letter availability is stored as a transient keyed by post_type \/ taxonomy \/ term \/ current language \/ alphabet pack \/ menumisc setting. Default TTL is 1 hour (configurable under <strong>REST API &amp; Cache \u2192 Cache TTL<\/strong>). The cache is invalidated automatically on <code>save_post<\/code>, <code>deleted_post<\/code>, <code>trashed_post<\/code>, <code>untrashed_post<\/code>, and <code>switch_blog<\/code> \u2014 so editing a post immediately reflects in the index.<\/p><\/dd>\n<dt id=\"can%20i%20customise%20the%20output%20without%20editing%20the%20plugin%3F\"><h3>Can I customise the output without editing the plugin?<\/h3><\/dt>\n<dd><p>Yes \u2014 every render path fires hooks (added in 2.3.0):<\/p>\n\n<ul>\n<li><code>wp_snap_ext\/before_render<\/code>, <code>wp_snap_ext\/after_render<\/code> \u2014 actions.<\/li>\n<li><code>wp_snap_ext\/pre_render<\/code> \u2014 short-circuit filter (return a string to replace the HTML).<\/li>\n<li><code>wp_snap_ext\/query_args<\/code> \u2014 mutate WP_Query args before the query runs.<\/li>\n<li><code>wp_snap_ext\/letter_href<\/code> \u2014 rewrite letter link hrefs (useful for SPA routers).<\/li>\n<li><code>wp_snap_ext\/excerpt<\/code> \u2014 post-process the resolved excerpt.<\/li>\n<li><code>wp_snap_ext\/render<\/code> \u2014 final filter on the rendered HTML.<\/li>\n<\/ul>\n\n<p>See the <strong>Developer hooks<\/strong> section above for code samples.<\/p><\/dd>\n<dt id=\"why%20are%20pagination%20urls%20using%20%60%3Falpha_paged%3D%60%20instead%20of%20%60%3Fpaged%3D%60%3F\"><h3>Why are pagination URLs using `?alpha_paged=` instead of `?paged=`?<\/h3><\/dt>\n<dd><p>So they don't collide with WordPress's own paged query variable on category, tag, or archive templates. You can paginate the alphabetical list independently of the surrounding archive.<\/p><\/dd>\n<dt id=\"is%20the%20plugin%20accessible%20%28wcag%29%3F\"><h3>Is the plugin accessible (WCAG)?<\/h3><\/dt>\n<dd><p>Yes \u2014 the markup targets WCAG 2.1 AA. The letter navigation is wrapped in a semantic <code>&lt;nav aria-label=\"Alphabetical Navigation\"&gt;<\/code>, the active letter carries <code>aria-current=\"page\"<\/code>, empty letters carry <code>aria-disabled=\"true\"<\/code>, every link has a descriptive <code>aria-label<\/code>, and the stylesheet provides visible <code>:focus-visible<\/code> outlines. Explicit <code>role=\"list\"<\/code> \/ <code>role=\"listitem\"<\/code> are emitted because Safari + VoiceOver strip the implicit list role when <code>list-style:none<\/code> is applied.<\/p><\/dd>\n\n<\/dl>\n\n<!--section=changelog-->\n<h4>2.4.2<\/h4>\n\n<ul>\n<li>Bumped \"Tested up to\" to WordPress 7.0. No functional changes.<\/li>\n<\/ul>\n\n<h4>2.4.1<\/h4>\n\n<ul>\n<li>readme.txt metadata fix \u2014 limited Tags to wp.org's maximum of 5 and trimmed the short description to the 150-character limit. No functional changes.<\/li>\n<\/ul>\n\n<h4>2.4.0<\/h4>\n\n<ul>\n<li><strong>Tabbed settings page<\/strong> \u2014 the admin panel is reorganised into four tabs using the standard WordPress <code>nav-tab<\/code> UI: <strong>General<\/strong>, <strong>Styling<\/strong>, <strong>Integrations<\/strong>, and <strong>Developer<\/strong>. After saving, you are returned to the tab you were editing (Post\/Redirect\/Get), not the first one.<\/li>\n<li><strong>Styling mode<\/strong> \u2014 new \"Styling\" choice on the Styling tab:\n\n<ul>\n<li><em>Default CSS<\/em> \u2014 loads the bundled <code>snap-style-default.css<\/code> skin (styled letter strip, post cards, pagination buttons) that mirrors the plugin's preview.<\/li>\n<li><em>No CSS (theme consistent)<\/em> \u2014 loads only the minimal structural <code>snap-style.css<\/code> so the output inherits your theme's typography, colours, and button styles. This remains the default, so existing sites are unchanged on update.<\/li>\n<li>Only the selected stylesheet is enqueued, and only on pages that actually render the index.<\/li>\n<\/ul><\/li>\n<li><strong>Developer tab<\/strong> \u2014 copy-paste reference with <code>&lt;pre&gt;&lt;code&gt;<\/code> snippets for the <code>[alphabetical_pagination]<\/code> shortcode, the <code>alphabetical_pagination()<\/code> template tag (with a <code>function_exists()<\/code> guard and the <code>wp_snap()<\/code> alias note), and the <code>wp_snap_ext\/*<\/code> action\/filter hooks.<\/li>\n<\/ul>\n\n<h4>2.3.0<\/h4>\n\n<ul>\n<li><strong>Gutenberg block<\/strong> \u2014 new server-rendered <code>wp-snap-ext\/index<\/code> block. Drop the alphabetical index into any post, page, or Site Editor template with one click. No JS build pipeline required; block.json + render.php only.<\/li>\n<li><strong>WooCommerce auto-mount<\/strong> \u2014 toggle the new \"Auto-mount on Shop\" setting and the index renders above the shop loop \/ product category archives automatically via the native <code>woocommerce_before_shop_loop<\/code> (or <code>woocommerce_archive_description<\/code> \/ <code>woocommerce_before_main_content<\/code>) action. No DOM hacks, no <code>posts_where<\/code> SQL injection.<\/li>\n<li><strong>REST API<\/strong> \u2014 public endpoints under <code>\/wp-json\/wp-snap-ext\/v1\/<\/code>:\n\n<ul>\n<li><code>GET \/letters?post_type=\u2026&amp;taxonomy=\u2026&amp;term=\u2026<\/code> \u2192 <code>[ { letter, count, href } ]<\/code><\/li>\n<li><code>GET \/posts?post_type=\u2026&amp;letter=A&amp;page=1&amp;per_page=10<\/code> \u2192 <code>{ posts, total, total_pages }<\/code><\/li>\n<li>Toggle on the settings page; uses the transient cache so repeated calls hit memory.<\/li>\n<\/ul><\/li>\n<li><strong>Documented developer hook API<\/strong> \u2014 every render path now fires:\n\n<ul>\n<li><code>do_action( 'wp_snap_ext\/before_render', $context )<\/code><\/li>\n<li><code>apply_filters( 'wp_snap_ext\/pre_render', $html, $post_type, $display, $args )<\/code> (short-circuit)<\/li>\n<li><code>apply_filters( 'wp_snap_ext\/query_args', $args, $context )<\/code><\/li>\n<li><code>apply_filters( 'wp_snap_ext\/letter_href', $href, $letter, $base )<\/code><\/li>\n<li><code>apply_filters( 'wp_snap_ext\/excerpt', $excerpt, $post_id )<\/code><\/li>\n<li><code>apply_filters( 'wp_snap_ext\/render', $html, $post_type, $display, $args )<\/code><\/li>\n<li><code>do_action( 'wp_snap_ext\/after_render', $html, $context )<\/code><\/li>\n<\/ul><\/li>\n<li><strong>Transient letter-availability cache<\/strong> \u2014 letter counts are cached as transients keyed by post_type \/ taxonomy \/ term \/ current language \/ alphabet pack. Invalidated on <code>save_post<\/code>, <code>deleted_post<\/code>, <code>trashed_post<\/code>, <code>untrashed_post<\/code>, <code>switch_blog<\/code>. Default TTL 1 hour (configurable).<\/li>\n<li><strong>WPML + Polylang awareness<\/strong> \u2014 the cache key includes the current language (via <code>wpml_current_language<\/code> or <code>pll_current_language()<\/code>) and WP_Query runs with <code>suppress_filters =&gt; false<\/code>, so translated post sets are filtered and cached per-language.<\/li>\n<li><strong>Multi-language alphabet packs<\/strong> \u2014 pick from 15 bundled scripts (English, Arabic, Chinese pinyin, German, Spanish, French, Greek, Hebrew, Hindi, Hungarian, Korean Jamo, Russian, Thai, Turkish, Urdu). Selecting a pack overwrites the freeform Local Alphabet field on save.<\/li>\n<li><strong>Appearance toggles<\/strong> \u2014 Horizontal\/Vertical layout, Uppercase\/Lowercase letter case, \"Disable empty letters\" (renders empty buckets as muted + <code>aria-disabled<\/code>), \"Hide pagination if one page\".<\/li>\n<li><strong>Generic taxonomy filter<\/strong> \u2014 restrict the index to any registered taxonomy + term ID (beyond the legacy cat\/tag args).<\/li>\n<li><strong>Meta-key intra-bucket sorting<\/strong> \u2014 set a post meta key + ASC\/DESC to override post_title ordering within each letter bucket. Buckets themselves still derive from post_title.<\/li>\n<li><strong>Per-page override map<\/strong> \u2014 store a <code>{ post_id =&gt; items_per_page }<\/code> array so \/glossary can render 50 items per page while \/products renders 20.<\/li>\n<li><strong>DOM auto-injection<\/strong> \u2014 for themes that don't expose a hook: render the index into a hidden <code>&lt;template&gt;<\/code> in the footer and move it into a CSS selector via ~300 bytes of vanilla JS. No jQuery dependency.<\/li>\n<li><strong>Accessibility preserved<\/strong> \u2014 <code>&lt;nav aria-label&gt;<\/code>, <code>aria-current=\"page\"<\/code>, explicit <code>role=\"list\" \/ role=\"listitem\"<\/code>, descriptive <code>aria-label<\/code> per letter link, <code>aria-disabled<\/code> on empty letters, visible focus outlines.<\/li>\n<\/ul>\n\n<h4>2.2.0<\/h4>\n\n<ul>\n<li>URL parameters migrated to <code>?alpha_order=<\/code> and <code>?alpha_paged=<\/code> (legacy <code>?snap=<\/code> \/ <code>?cp=<\/code> \/ <code>?snap_paged=<\/code> still honoured).<\/li>\n<li>Post titles are now rendered as links to each post's permalink.<\/li>\n<li>Accessibility (WCAG AA): letter navigation wrapped in a semantic <code>&lt;nav aria-label=\"Alphabetical Navigation\"&gt;<\/code>, every letter link carries a descriptive <code>aria-label<\/code>, the active letter is exposed via <code>aria-current=\"page\"<\/code>, and visible focus outlines are provided in the stylesheet.<\/li>\n<li>New \"Learn More\" CTA below each post excerpt linking to the post permalink, with an <code>aria-label<\/code> that includes the post title.<\/li>\n<li>New <strong>Content Fallback<\/strong> settings section: toggle \"Use ACF field fallback if post excerpt is missing\" + text input \"ACF Field Name\". When enabled and ACF is active, the plugin reads the configured field via <code>get_field()<\/code> for posts without a native excerpt.<\/li>\n<li>Frontend post cards now show: linked title \u2192 excerpt (native \u2192 ACF fallback \u2192 trimmed content) \u2192 Learn More button.<\/li>\n<\/ul>\n\n<h4>2.1.0<\/h4>\n\n<ul>\n<li>Rebranded as <strong>Alphabetical Pagination<\/strong>. Existing directory structure and option keys are preserved.<\/li>\n<li>New template tag <code>alphabetical_pagination()<\/code> (the legacy <code>wp_snap()<\/code> is kept as a backwards-compatible alias).<\/li>\n<li>New <code>[alphabetical_pagination]<\/code> shortcode lets the index be embedded in any post or page. Attributes mirror the template tag arguments (<code>cat<\/code>, <code>child<\/code>, <code>menu<\/code>, <code>firstload<\/code>, <code>post_parent<\/code>, <code>post_type<\/code>, <code>display<\/code>).<\/li>\n<li>New \"Items Per Page\" admin setting registered through the WordPress Settings API (<code>register_setting<\/code>, <code>add_settings_section<\/code>, <code>add_settings_field<\/code>) and sanitised with <code>absint()<\/code>. Defaults to 10 if unset.<\/li>\n<li>The post loop is now paginated; Previous \/ Next + numbered links are rendered through <code>paginate_links()<\/code> below the alphabetised post list.<\/li>\n<\/ul>\n\n<h4>2.0.0<\/h4>\n\n<ul>\n<li>Requires PHP 8.1+. Plugin is now organised as typed classes under <code>includes\/<\/code>.<\/li>\n<li>Security: prepared statements on the legacy \"ignore words\" SQL path, full sanitisation of <code>$_GET<\/code> \/ <code>$_POST<\/code> \/ <code>$_SERVER<\/code> reads, escaped output, nonce + <code>manage_options<\/code> capability check on the settings page (was the deprecated numeric level \"8\").<\/li>\n<li>Settings now stored under the <code>wp_snap_ext_*<\/code> option prefix. Legacy <code>key_snap_*<\/code> values are migrated automatically on activation.<\/li>\n<li>Stylesheet is registered through <code>wp_enqueue_scripts<\/code> and only enqueued on pages that actually call <code>wp_snap()<\/code>.<\/li>\n<li>Standard navigation queries now use <code>WP_Query<\/code> instead of a hand-rolled SQL string.<\/li>\n<li>Backwards-compatible <code>wp_snap()<\/code> template tag retained.<\/li>\n<\/ul>\n\n<h4>1.0.0<\/h4>\n\n<ul>\n<li>Original Dinwebb fork of Nathan Olsen's WP-SNAP! plugin.<\/li>\n<\/ul>","raw_excerpt":"Alphabetical (A\u2013Z, #, 0\u20139) index for any post type. Shortcode, Gutenberg block, REST API, WooCommerce auto-mount, multilingual, WCAG AA.","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/cn.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin\/19407","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/cn.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin"}],"about":[{"href":"https:\/\/cn.wordpress.org\/plugins\/wp-json\/wp\/v2\/types\/plugin"}],"replies":[{"embeddable":true,"href":"https:\/\/cn.wordpress.org\/plugins\/wp-json\/wp\/v2\/comments?post=19407"}],"author":[{"embeddable":true,"href":"https:\/\/cn.wordpress.org\/plugins\/wp-json\/wporg\/v1\/users\/mansoormunib"}],"wp:attachment":[{"href":"https:\/\/cn.wordpress.org\/plugins\/wp-json\/wp\/v2\/media?parent=19407"}],"wp:term":[{"taxonomy":"plugin_section","embeddable":true,"href":"https:\/\/cn.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_section?post=19407"},{"taxonomy":"plugin_tags","embeddable":true,"href":"https:\/\/cn.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_tags?post=19407"},{"taxonomy":"plugin_category","embeddable":true,"href":"https:\/\/cn.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_category?post=19407"},{"taxonomy":"plugin_contributors","embeddable":true,"href":"https:\/\/cn.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_contributors?post=19407"},{"taxonomy":"plugin_business_model","embeddable":true,"href":"https:\/\/cn.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_business_model?post=19407"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}