{"id":294363,"date":"2026-05-11T07:53:30","date_gmt":"2026-05-11T07:53:30","guid":{"rendered":"https:\/\/wordpress.org\/plugins\/spintax\/"},"modified":"2026-05-12T22:18:14","modified_gmt":"2026-05-12T22:18:14","slug":"spintax","status":"publish","type":"plugin","link":"https:\/\/si.wordpress.org\/plugins\/spintax\/","author":23448903,"comment_status":"closed","ping_status":"closed","template":"","meta":{"_crdt_document":"","version":"2.0.3","stable_tag":"2.0.3","tested":"6.9.4","requires":"6.2","requires_php":"8.0","requires_plugins":null,"header_name":"Spintax","header_author":"301st","header_description":"Template-based dynamic content generation using spintax markup. Create reusable templates with randomised text variants, variable substitution, and permutation logic.","assets_banners_color":"0c0f15","last_updated":"2026-05-12 22:18:14","external_support_url":"","external_repository_url":"","donate_link":"","header_plugin_uri":"https:\/\/spintax.net","header_author_uri":"https:\/\/301.st","rating":0,"author_block_rating":0,"active_installs":0,"downloads":93,"num_ratings":0,"support_threads":0,"support_threads_resolved":0,"author_block_count":0,"sections":["description","installation","faq","changelog"],"tags":{"1.5.0":{"tag":"1.5.0","author":"301st","date":"2026-05-11 07:53:09"},"2.0.0":{"tag":"2.0.0","author":"301st","date":"2026-05-12 15:54:30"},"2.0.1":{"tag":"2.0.1","author":"301st","date":"2026-05-12 19:04:28"},"2.0.2":{"tag":"2.0.2","author":"301st","date":"2026-05-12 19:29:39"},"2.0.3":{"tag":"2.0.3","author":"301st","date":"2026-05-12 22:18:14"}},"upgrade_notice":{"2.0.3":"<p>Adds runtime ACF target validation (closes a wrong-field-write path when ACF is reactivated or bindings are imported via WP-CLI), cumulative-failure tracking across Bulk Apply chunks (prevents the Stale badge clearing on multi-chunk walks that partially failed), and a per-binding walk lock that refuses concurrent Bulk Apply runs. Strongly recommended.<\/p>","2.0.2":"<p>Documentation refresh for the 2.0 binding surface (Action Scheduler as a recommended optional dependency, full WP-CLI command set, variable scopes, scheduling, manual edits) plus an admin notice on the Bindings page when Action Scheduler isn&#039;t loaded. No functional changes to the engine.<\/p>","2.0.1":"<p>Hot-fix for 2.0.0: cross-kind binding collisions, missing ACF field_key validation, Test panel scope-filter parity, Bulk Apply Stale-badge gating, and form value preservation on validation errors. Highly recommended if you&#039;re on 2.0.0.<\/p>","2.0.0":"<p>Major release \u2014 adds ACF \/ post-meta bindings, per-binding cron, Bulk Apply with Action Scheduler, full WP-CLI surface, and a one-shot migration wizard for <code>nested-spintax-for-acf<\/code> users. No breaking changes to the existing template \/ shortcode \/ render API.<\/p>","1.4.0":"<p>New <code>{?VAR?then|else}<\/code> conditional syntax, smarter sentence-end capitalisation around abbreviations, and a fix for <code>#set<\/code> directives with empty values.<\/p>","1.1.0":"<p>Per-element permutation separators, auto-spacing for word separators, improved input sanitization.<\/p>","1.0.1":"<p>Fixes permutation config handling, preview rendering, and scope isolation. Recommended update.<\/p>","1.0.0":"<p>Initial release.<\/p>"},"ratings":[],"assets_icons":{"icon-128x128.png":{"filename":"icon-128x128.png","revision":3528366,"resolution":"128x128","location":"assets","locale":"","width":128,"height":128},"icon-256x256.png":{"filename":"icon-256x256.png","revision":3528366,"resolution":"256x256","location":"assets","locale":"","width":256,"height":256},"icon.svg":{"filename":"icon.svg","revision":3528366,"resolution":false,"location":"assets","locale":false}},"assets_banners":{"banner-1544x500.png":{"filename":"banner-1544x500.png","revision":3528366,"resolution":"1544x500","location":"assets","locale":"","width":1544,"height":500},"banner-772x250.png":{"filename":"banner-772x250.png","revision":3528366,"resolution":"772x250","location":"assets","locale":"","width":772,"height":250}},"assets_blueprints":{},"all_blocks":[],"tagged_versions":["1.5.0","2.0.0","2.0.1","2.0.2","2.0.3"],"block_files":[],"assets_screenshots":{"screenshot-1.png":{"filename":"screenshot-1.png","revision":3528366,"resolution":"1","location":"assets","locale":"","width":1280,"height":800},"screenshot-2.png":{"filename":"screenshot-2.png","revision":3528366,"resolution":"2","location":"assets","locale":"","width":1280,"height":800},"screenshot-3.png":{"filename":"screenshot-3.png","revision":3528366,"resolution":"3","location":"assets","locale":"","width":1280,"height":800}},"screenshots":{"1":"Template editor with spintax markup and live preview.","2":"Settings page with global variables editor.","3":"Template list with shortcode, cache status, and cron schedule."},"jetpack_post_was_ever_published":false},"plugin_section":[],"plugin_tags":[8494,2487,186,47760,4516],"plugin_category":[43,55],"plugin_contributors":[255945],"plugin_business_model":[],"class_list":["post-294363","plugin","type-plugin","status-publish","hentry","plugin_tags-content-generation","plugin_tags-dynamic-content","plugin_tags-seo","plugin_tags-spintax","plugin_tags-templates","plugin_category-customization","plugin_category-seo-and-marketing","plugin_contributors-301st","plugin_committers-301st"],"banners":{"banner":"https:\/\/ps.w.org\/spintax\/assets\/banner-772x250.png?rev=3528366","banner_2x":"https:\/\/ps.w.org\/spintax\/assets\/banner-1544x500.png?rev=3528366","banner_rtl":false,"banner_2x_rtl":false},"icons":{"svg":"https:\/\/ps.w.org\/spintax\/assets\/icon.svg?rev=3528366","icon":"https:\/\/ps.w.org\/spintax\/assets\/icon.svg?rev=3528366","icon_2x":false,"generated":false},"screenshots":[{"src":"https:\/\/ps.w.org\/spintax\/assets\/screenshot-1.png?rev=3528366","caption":"Template editor with spintax markup and live preview."},{"src":"https:\/\/ps.w.org\/spintax\/assets\/screenshot-2.png?rev=3528366","caption":"Settings page with global variables editor."},{"src":"https:\/\/ps.w.org\/spintax\/assets\/screenshot-3.png?rev=3528366","caption":"Template list with shortcode, cache status, and cron schedule."}],"raw_content":"<!--section=description-->\n<p>Spintax is a WordPress plugin for template-based content generation using spintax markup. Create reusable templates with randomised text variants, variable substitution, and permutation logic \u2014 then embed them anywhere on your site via shortcodes or PHP.<\/p>\n\n<p><strong>Key features:<\/strong><\/p>\n\n<ul>\n<li><strong>Enumerations<\/strong> <code>{a|b|c}<\/code> \u2014 randomly pick one option, with nesting support<\/li>\n<li><strong>Permutations<\/strong> <code>[&lt;config&gt;a|b|c]<\/code> \u2014 pick N elements, shuffle, join with custom separators<\/li>\n<li><strong>Variables<\/strong> <code>%var%<\/code> \u2014 global, local (<code>#set<\/code>), and shortcode-level variable scopes<\/li>\n<li><strong>Conditionals<\/strong> <code>{?VAR?then|else}<\/code> \u2014 render a branch based on whether a variable is set (also <code>{?!VAR?then}<\/code> inverted)<\/li>\n<li><strong>Plural agreement<\/strong> <code>{plural &lt;count&gt;: form1|form2|form3}<\/code> \u2014 pick grammatically correct noun form by count. RU\/UK\/BE 3-form (one|few|many), EN-style 2-form (one|many). First spintax engine with first-class plurals.<\/li>\n<li><strong>Nested templates<\/strong> \u2014 embed templates within templates via <code>#include<\/code> or <code>[spintax]<\/code><\/li>\n<li><strong>ACF \/ post-meta bindings (NEW in 2.0)<\/strong> \u2014 configure once per post type, render Spintax templates into ACF text\/textarea\/wysiwyg fields or post-meta keys on every matching post. Auto-seed empty fields, preserve manual edits, Bulk Apply via Action Scheduler.<\/li>\n<li><strong>Object cache<\/strong> \u2014 rendered output cached via WP Object Cache API (Redis\/Memcached ready)<\/li>\n<li><strong>Cron regeneration<\/strong> \u2014 optional scheduled cache refresh per template, plus per-binding cron walks<\/li>\n<li><strong>WP-CLI<\/strong> \u2014 <code>wp spintax bindings list|apply|test|export|import<\/code><\/li>\n<li><strong>Validation<\/strong> \u2014 bracket matching, circular reference detection, syntax checking<\/li>\n<li><strong>Admin UI<\/strong> \u2014 code editor, live preview, shortcode copy, settings page, bindings list<\/li>\n<\/ul>\n\n<p><strong>Syntax based on the GTW (Generating The Web) standard.<\/strong><\/p>\n\n<h3>External services<\/h3>\n\n<p>This plugin does <strong>not<\/strong> connect to any external services, APIs, or third-party servers.<\/p>\n\n<p>All content generation happens locally on your WordPress server. No data is sent externally. No remote requests are made during activation, rendering, or caching.<\/p>\n\n<h3>Privacy Policy<\/h3>\n\n<p>This plugin does not collect, store, or transmit any personal user data. It does not use cookies, tracking pixels, analytics, or any form of telemetry.<\/p>\n\n<p>Templates and their rendered output are stored entirely within your WordPress database and object cache.<\/p>\n\n<h3>Credits<\/h3>\n\n<ul>\n<li>Syntax based on the <a href=\"https:\/\/spintax.net\">GTW (Generating The Web)<\/a> standard<\/li>\n<li>Developed by <a href=\"https:\/\/301.st\">301st<\/a><\/li>\n<\/ul>\n\n<!--section=installation-->\n<ol>\n<li>Upload the <code>spintax<\/code> folder to <code>\/wp-content\/plugins\/<\/code><\/li>\n<li>Activate the plugin through the 'Plugins' menu in WordPress<\/li>\n<li>Create templates under the \"Spintax\" menu in the admin sidebar<\/li>\n<li>Embed templates using <code>[spintax slug=\"my-template\"]<\/code> in posts\/pages or <code>spintax_render('my-template')<\/code> in theme files<\/li>\n<\/ol>\n\n<p><strong>Recommended optional dependency:<\/strong> install <a href=\"https:\/\/wordpress.org\/plugins\/action-scheduler\/\">Action Scheduler<\/a> if you plan to use the \"Bulk Apply\" button on ACF \/ post-meta bindings, or schedule bindings via per-binding cron on a site with many matching posts. The plugin works without it \u2014 Bulk Apply falls back to a WP-CLI command and cron walks run synchronously \u2014 but Action Scheduler gives you one-click admin Bulk Apply and chunked async cron walks. If you already use WooCommerce or another plugin that bundles Action Scheduler, you're already set; the Bindings page only shows the install notice when AS isn't loaded.<\/p>\n\n<!--section=faq-->\n<dl>\n<dt id=\"how%20do%20i%20create%20a%20template%3F\"><h3>How do I create a template?<\/h3><\/dt>\n<dd><p>Go to Spintax &gt; Add New in the WordPress admin. Enter a title and your spintax markup in the editor.<\/p><\/dd>\n<dt id=\"what%20syntax%20does%20the%20plugin%20use%3F\"><h3>What syntax does the plugin use?<\/h3><\/dt>\n<dd><ul>\n<li><code>{a|b|c}<\/code> \u2014 randomly picks one option<\/li>\n<li><code>[a|b|c]<\/code> \u2014 permutation: picks N elements, shuffles, joins with space<\/li>\n<li><code>[&lt;minsize=2;maxsize=3;sep=\", \";lastsep=\" and \"&gt; a|b|c|d]<\/code> \u2014 configured permutation<\/li>\n<li><code>%variable%<\/code> \u2014 variable reference<\/li>\n<li><code>#set %var% = value<\/code> \u2014 local variable definition<\/li>\n<li><code>{?VAR?then|else}<\/code> \u2014 conditional: render a branch by truthiness of <code>%VAR%<\/code> (also <code>{?!VAR?then}<\/code> inverted)<\/li>\n<li><code>{plural %Count%: form1|form2|form3}<\/code> \u2014 plural agreement: picks the correct grammatical form by count (RU 3-form, EN 2-form)<\/li>\n<li><code>\/#comment#\/<\/code> \u2014 block comment (stripped from output)<\/li>\n<li><code>#include \"slug\"<\/code> \u2014 embed another template<\/li>\n<\/ul>\n\n<p>Full syntax reference with examples and a live playground: https:\/\/spintax.net\/docs\/syntax<\/p><\/dd>\n<dt id=\"where%20can%20i%20learn%20more%3F\"><h3>Where can I learn more?<\/h3><\/dt>\n<dd><ul>\n<li><strong>Documentation hub:<\/strong> https:\/\/spintax.net\/docs\/ \u2014 guides, reference, recipes<\/li>\n<li><strong>Compact syntax reference:<\/strong> https:\/\/spintax.net\/docs\/syntax \u2014 all primitives in one page (13 languages)<\/li>\n<li><strong>Plural agreement guide:<\/strong> https:\/\/spintax.net\/docs\/plural-spintax\/ \u2014 <code>{plural N: form1|form2|form3}<\/code> in depth (EN\/RU)<\/li>\n<li><strong>Conditional spintax guide:<\/strong> https:\/\/spintax.net\/docs\/conditional-spintax\/ \u2014 <code>{?VAR?then|else}<\/code> value-driven branching (EN\/RU)<\/li>\n<li><strong>Authoring mindset:<\/strong> https:\/\/spintax.net\/docs\/authoring-mindset\/ \u2014 write the final text first, add markup last (EN\/RU)<\/li>\n<li><strong>Live playground:<\/strong> https:\/\/spintax.net\/play\/ \u2014 write a template, set variables, render N variants in your browser (EN\/RU)<\/li>\n<\/ul><\/dd>\n<dt id=\"does%20caching%20require%20redis%20or%20memcached%3F\"><h3>Does caching require Redis or Memcached?<\/h3><\/dt>\n<dd><p>The plugin uses the WordPress Object Cache API. With a persistent backend (Redis, Memcached), cached output persists across requests. Without one, templates are re-rendered on each page load.<\/p><\/dd>\n<dt id=\"can%20i%20pass%20variables%20through%20shortcodes%3F\"><h3>Can I pass variables through shortcodes?<\/h3><\/dt>\n<dd><p>Yes: <code>[spintax slug=\"greeting\" name=\"Alice\" city=\"Moscow\"]<\/code> makes <code>%name%<\/code> and <code>%city%<\/code> available inside the template.<\/p><\/dd>\n<dt id=\"what%20are%20acf%20%2F%20post-meta%20bindings%3F\"><h3>What are ACF \/ post-meta bindings?<\/h3><\/dt>\n<dd><p>A binding pairs a Spintax template (or a per-post inline source) with one target field on one post type \u2014 for example \"Posts \u2192 ACF: hero_subtitle\". Configure it once under Spintax \u2192 Bindings and the plugin populates the field on every matching post on save, on a cron schedule, or on demand via Bulk Apply. Manual edits are preserved by default (hash-tracked); flags control whether the binding auto-seeds empty fields, regenerates on every save, or clears the field when the template renders to empty.<\/p><\/dd>\n<dt id=\"can%20i%20bind%20to%20acf%20fields%3F\"><h3>Can I bind to ACF fields?<\/h3><\/dt>\n<dd><p>Yes. Bindings support both ACF (text \/ textarea \/ wysiwyg, top-level fields) and plain post-meta keys. ACF Free and Pro are both supported; nested fields (repeater \/ flexible_content rows) are not supported in 2.0 \u2014 that lands in a later release. The form-side field picker auto-fills the stable ACF field key so writes work on the first save without ACF's reference-meta handshake.<\/p><\/dd>\n<dt id=\"do%20i%20need%20action%20scheduler%3F\"><h3>Do I need Action Scheduler?<\/h3><\/dt>\n<dd><p>It's a recommended optional dependency for binding-heavy sites. The plugin works without it, but two features degrade:<\/p>\n\n<ul>\n<li>The admin <strong>Bulk Apply<\/strong> button uses Action Scheduler to dispatch chunked async jobs. Without AS, the button returns an error pointing at the WP-CLI fallback (<code>wp spintax bindings apply --binding=&lt;id&gt; --all<\/code>).<\/li>\n<li>Per-binding cron schedules still fire, but the cron callback runs the walk synchronously instead of enqueueing an async job. On large catalogues that risks PHP-FPM timeouts on the cron worker.<\/li>\n<\/ul>\n\n<p>Many WP shops already ship Action Scheduler bundled with WooCommerce or other plugins \u2014 check Plugins \u2192 Installed Plugins for \"Action Scheduler\" before installing it separately. If the Bindings admin page shows an \"Action Scheduler is not installed\" notice at the top, you don't have it loaded yet; install <a href=\"https:\/\/wordpress.org\/plugins\/action-scheduler\/\">Action Scheduler<\/a> to make one-click admin Bulk Apply and async cron walks available.<\/p><\/dd>\n<dt id=\"what%20wp-cli%20commands%20does%20the%20plugin%20add%3F\"><h3>What WP-CLI commands does the plugin add?<\/h3><\/dt>\n<dd><p>Five subcommands under <code>wp spintax bindings<\/code>:<\/p>\n\n<ul>\n<li><code>wp spintax bindings list [--format=table|json|csv]<\/code> \u2014 list all bindings on the site.<\/li>\n<li><code>wp spintax bindings apply --binding=&lt;id&gt; [--all|--post=&lt;id&gt;] [--dry-run]<\/code> \u2014 run a binding against all matching posts (or a single post), with optional dry-run. This is the no-Action-Scheduler fallback path for Bulk Apply.<\/li>\n<li><code>wp spintax bindings test --binding=&lt;id&gt; --post=&lt;id&gt;<\/code> \u2014 dry-run a binding against one post and report what <code>BindingApplier::plan()<\/code> would do (<code>would_write<\/code>, current value, rendered preview, skip reason). Same logic as the admin Test panel.<\/li>\n<li><code>wp spintax bindings export [--format=json] [&gt; bindings.json]<\/code> \u2014 emit the full bindings store as JSON, deduped by <code>(post_type, target.key)<\/code>.<\/li>\n<li><code>wp spintax bindings import --file=bindings.json [--overwrite] [--dry-run]<\/code> \u2014 import bindings from JSON. <code>--overwrite<\/code> updates matches on the same target triple; without it, duplicates are skipped. Use <code>--dry-run<\/code> to preview the plan without writing.<\/li>\n<\/ul>\n\n<p>The export\/import pair is the recommended staging\u2192production sync path; bindings are not exposed over REST in 2.0.<\/p><\/dd>\n<dt id=\"what%20variables%20can%20i%20use%20inside%20a%20bound%20template%3F\"><h3>What variables can I use inside a bound template?<\/h3><\/dt>\n<dd><p>A binding template renders with four layered variable sources (later layers override earlier ones \u2014 see spec \u00a74.3):<\/p>\n\n<ul>\n<li><strong>Global variables<\/strong> \u2014 the <code>#set<\/code> block in Settings \u2192 Spintax \u2192 Global Variables. Site-wide.<\/li>\n<li><strong>Per-binding overrides<\/strong> \u2014 a <code>#set<\/code> block in the binding's Variables \u2192 \"Per-binding #set overrides\" textarea. Applies to that binding only.<\/li>\n<li><strong>Post context<\/strong> (opt-in via the binding's \"Expose post context as %vars%\" checkbox) \u2014 <code>%post_id%<\/code>, <code>%post_title%<\/code>, <code>%post_url%<\/code>, <code>%post_slug%<\/code>, <code>%post_date%<\/code>, <code>%post_modified%<\/code>, <code>%author_id%<\/code>, <code>%author_name%<\/code>.<\/li>\n<li><strong>ACF sibling fields<\/strong> (opt-in via \"Expose ACF sibling fields as %acf_%\") \u2014 every top-level ACF text\/textarea\/wysiwyg field in the binding's post type group, available as <code>%acf_&lt;field_name&gt;%<\/code>. Reads happen after ACF persists its values (save_post priority 20 hook), so siblings are always fresh on save_post triggers. Only meaningful for ACF-target bindings.<\/li>\n<\/ul>\n\n<p>A binding's source can also use the rest of the Spintax syntax (<code>{a|b|c}<\/code>, <code>[a|b]<\/code>, <code>{?VAR?then|else}<\/code>, <code>{plural %N%: ...}<\/code>, <code>#include \"slug\"<\/code>, <code>\/#comment#\/<\/code>).<\/p><\/dd>\n<dt id=\"how%20do%20i%20schedule%20bindings%20to%20run%20automatically%3F\"><h3>How do I schedule bindings to run automatically?<\/h3><\/dt>\n<dd><p>Two trigger paths, both configurable per binding under \"Triggers\":<\/p>\n\n<ul>\n<li><strong>Fire on post save<\/strong> (checkbox, default on) \u2014 hooks <code>save_post<\/code> priority 20. Runs after ACF persists its own field values, so sibling reads see fresh data. Skipped during autosave \/ bulk-edit \/ REST batch imports \/ revisions \/ trash flips.<\/li>\n<li><strong>Cron schedule<\/strong> (dropdown: disabled \/ hourly \/ twicedaily \/ daily) \u2014 each binding gets its own WP-Cron hook <code>spintax_binding_cron_&lt;binding_id&gt;<\/code>. On the scheduled tick, the callback enqueues an Action Scheduler walk (or runs synchronously if AS isn't installed \u2014 see the Action Scheduler FAQ above). Independent of save_post; use this to refresh content periodically without an editor touch.<\/li>\n<\/ul>\n\n<p>For one-off \"apply now\" operations, click <strong>Bulk Apply<\/strong> on the binding card. The button needs Action Scheduler; without it, the admin notice points at the WP-CLI fallback.<\/p><\/dd>\n<dt id=\"how%20does%20the%20plugin%20handle%20manual%20edits%20to%20bound%20fields%3F\"><h3>How does the plugin handle manual edits to bound fields?<\/h3><\/dt>\n<dd><p>Each binding tracks a SHA-1 signature of its last-rendered value in post-meta <code>_spintax_last_render_sig_&lt;binding_id&gt;<\/code>. On every subsequent run with <code>Preserve manual edits<\/code> enabled (default), it compares the current target value's hash to the stored signature:<\/p>\n\n<ul>\n<li>If the hashes match, the value hasn't been touched outside the binding \u2014 safe to regenerate.<\/li>\n<li>If they differ, treat it as a manual edit; skip with <code>SKIP_MANUAL_EDIT_DETECTED<\/code> and log the skip.<\/li>\n<\/ul>\n\n<p>Combined with <code>Regenerate on every save<\/code>, this gives a \"refresh on save unless edited\" workflow. With <code>Auto-seed empty fields<\/code> instead, the binding only writes when the target is empty \u2014 manual edits are preserved by definition because they're never overwritten.<\/p>\n\n<p>There is a \"cold-start\" exception: when a binding first sees a post with non-empty target content and no signature yet, it treats the existing value as an unwritten manual baseline and skips (<code>SKIP_COLD_START_MANUAL<\/code>) until the editor clicks the binding's \"Initialize from current value\" path (a later UI addition) or accepts the regeneration by clearing the field first.<\/p><\/dd>\n<dt id=\"i%20edited%20a%20template.%20why%20aren%27t%20the%20changes%20showing%20up%20on%20the%20front%20end%3F\"><h3>I edited a template. Why aren't the changes showing up on the front end?<\/h3><\/dt>\n<dd><p>Bindings are a <strong>pre-generation<\/strong> system, not a render-on-read layer. The rendered string is stored in the target field; consumers (themes, blocks, REST readers) get that stored value directly. Editing the source template doesn't propagate to existing posts until a trigger writes a fresh value to each one.<\/p>\n\n<p>When you edit a template that has bindings pointing at it, the plugin:<\/p>\n\n<ol>\n<li>Bumps an internal render-cache version on each affected binding.<\/li>\n<li>Surfaces an admin notice on the template-edit screen (\"N bindings depend on this template\").<\/li>\n<li>Shows a \"Stale: source template edited\" badge on each affected binding's card.<\/li>\n<\/ol>\n\n<p>To push the new content to existing posts, click <strong>Bulk Apply<\/strong> on each affected binding (or run <code>wp spintax bindings apply --binding=&lt;id&gt; --all<\/code> from the CLI). The Stale badge only clears when the entire walk completes with zero failures \u2014 partial-failure walks keep the badge so you notice the divergence and retry.<\/p><\/dd>\n<dt id=\"is%20there%20a%20hard%20cap%20on%20bindings%3F\"><h3>Is there a hard cap on bindings?<\/h3><\/dt>\n<dd><p>200 bindings per site. The store is a single autoloaded option (~500 bytes per binding), and the cap keeps autoload memory bounded. If you genuinely need more, please open an issue with your use case.<\/p><\/dd>\n<dt id=\"which%20fields%20can%27t%20i%20bind%20to%3F\"><h3>Which fields can't I bind to?<\/h3><\/dt>\n<dd><p>The form rejects five tiers of reserved keys at save time:<\/p>\n\n<ul>\n<li><strong>WordPress-internal meta<\/strong> \u2014 keys starting with <code>_wp_<\/code>, <code>_edit_<\/code>, <code>_oembed_<\/code>, plus <code>_pingme<\/code>, <code>_encloseme<\/code>, <code>_thumbnail_id<\/code>.<\/li>\n<li><strong>Plugin-internal meta<\/strong> \u2014 <code>_spintax_*<\/code> prefixes (source, signature, cache-version slots used by other bindings).<\/li>\n<li><strong>wp_posts columns<\/strong> \u2014 <code>post_title<\/code>, <code>post_content<\/code>, <code>post_excerpt<\/code>, <code>post_name<\/code>, <code>post_status<\/code>, <code>post_date<\/code>, <code>post_modified<\/code>, <code>post_parent<\/code>, <code>post_author<\/code>, <code>post_type<\/code>, <code>post_password<\/code>, etc. These aren't post-meta and writing to them via <code>update_post_meta()<\/code> silently creates shadow rows.<\/li>\n<li><strong>Cross-binding uniqueness<\/strong> \u2014 only one binding per <code>(post type, target key)<\/code>, regardless of whether the kind is ACF or post_meta (they share the same database row).<\/li>\n<li><strong>ACF field key validity<\/strong> \u2014 when binding to an ACF field, the stable field key (e.g. <code>field_5f8a1234abcd<\/code>) is required, and verified against <code>acf_get_field()<\/code> when ACF is loaded.<\/li>\n<\/ul><\/dd>\n<dt id=\"on%20multisite%2C%20are%20bindings%20shared%20across%20the%20network%3F\"><h3>On multisite, are bindings shared across the network?<\/h3><\/dt>\n<dd><p>No \u2014 bindings are per-site. Each subsite manages its own. Use <code>wp --url=site2 spintax bindings import --file=site1-bindings.json<\/code> to copy bindings between subsites via the WP-CLI export\/import round-trip.<\/p><\/dd>\n<dt id=\"can%20i%20manage%20bindings%20via%20rest%3F\"><h3>Can I manage bindings via REST?<\/h3><\/dt>\n<dd><p>Not in 2.0; bindings are admin-only. The <code>wp spintax bindings<\/code> WP-CLI surface covers staging\u2192production sync scenarios. REST API exposure is tracked for a later release.<\/p><\/dd>\n<dt id=\"i%27m%20coming%20from%20%60nested-spintax-for-acf%60.%20is%20there%20a%20migration%20path%3F\"><h3>I'm coming from `nested-spintax-for-acf`. Is there a migration path?<\/h3><\/dt>\n<dd><p>Yes. After activating Spintax 2.0, a dismissible admin banner points to <strong>Tools \u2192 Spintax Migration<\/strong>. The wizard scans for predecessor data, shows a per-row preview, and creates bindings deduped by <code>(post type, target field)<\/code>. Per-post sources and variables are copied non-destructively \u2014 the old plugin's data stays in place until you delete it.<\/p><\/dd>\n\n<\/dl>\n\n<!--section=changelog-->\n<h4>2.0.3<\/h4>\n\n<ul>\n<li>Fix: ACF target validation now runs on every apply, not just at form save. <code>BindingApplier::plan()<\/code> rejects bindings whose stored <code>target.field_key<\/code> no longer resolves to a field with the expected name (deleted, renamed, or re-assigned in ACF). Two new return codes: <code>skip_acf_not_loaded<\/code> (ACF deactivated since the binding was saved) and <code>skip_invalid_acf_field<\/code> (key + name disagreement). Closes a path where CLI-imported or imported-while-ACF-inactive bindings could write through <code>update_field()<\/code> to the wrong field.<\/li>\n<li>Fix: <code>BindingApplier::read_target()<\/code> and <code>::write_target()<\/code> no longer fall back to plain <code>update_post_meta()<\/code> \/ <code>get_post_meta()<\/code> for <code>kind = acf_field<\/code> when ACF isn't loaded. The applier short-circuits at the runtime guard above, so the low-level methods are the sole writer for verified targets. Pre-2.0.3 the silent fallback could write the rendered value to a post-meta row ACF would never see again.<\/li>\n<li>Fix: Bulk Apply now tracks failures cumulatively across chunks via a persistent <code>_spintax_binding_walk_failed_v_&lt;id&gt;<\/code> flag. The final chunk gates <code>stamp_last_applied_version()<\/code> on the cumulative flag. 2.0.1 only checked the current chunk, so a multi-chunk walk that failed in chunk 1 and succeeded in the final chunk would still clear the Stale badge.<\/li>\n<li>Fix: Concurrent Bulk Apply walks on the same binding are now refused with <code>WP_Error 'walk_in_progress'<\/code>. Both <code>enqueue()<\/code> and <code>run_synchronously()<\/code> acquire a per-binding lock (option <code>_spintax_binding_walk_lock_&lt;id&gt;<\/code>) at walk start; stale locks older than one hour are auto-overwritten so a crashed walk doesn't permanently jam the binding.<\/li>\n<li>Internal: 11 new PHPUnit cases \u2014 runtime ACF guard, multi-chunk failure tracking, walk-lock acquisition \/ release, stale-lock recovery. 441 tests total (was 430).<\/li>\n<li>Tooling: <code>npm run lint:php<\/code> and <code>lint:php:fix<\/code> moved to <code>scripts\/lint-php.sh<\/code> \/ <code>scripts\/lint-php-fix.sh<\/code>. The inline command tripped over bash-c quoting on Windows. <code>.gitattributes<\/code> enforces LF endings on shipped text files.<\/li>\n<li>Internal: CLI <code>wp spintax bindings import --overwrite<\/code> help text updated to reflect the 2.0.1 <code>(post_type, target.key)<\/code> uniqueness contract.<\/li>\n<\/ul>\n\n<h4>2.0.2<\/h4>\n\n<ul>\n<li>Docs: new FAQ entries \u2014 Action Scheduler dependency, full <code>wp spintax bindings<\/code> WP-CLI surface, variable scopes (global \/ per-binding \/ post context \/ ACF siblings), trigger options (save_post + per-binding cron), manual edit detection, template-edit propagation, reserved-key tiers.<\/li>\n<li>Docs: Installation section now flags Action Scheduler as a recommended optional dependency with the specific features it enables.<\/li>\n<li>UX: Spintax \u2192 Bindings shows an info notice at the top of the page when Action Scheduler isn't loaded, explaining the two features that degrade (admin Bulk Apply, async cron walks) and linking to the install screen. Notice disappears when AS is loaded by any source (direct install, WooCommerce \/ Jetpack bundle, mu-plugin, etc.).<\/li>\n<li>Internal: no functional changes to the bindings engine or core spintax engine \u2014 patch is documentation + a single admin-page notice.<\/li>\n<\/ul>\n\n<h4>2.0.1<\/h4>\n\n<ul>\n<li>Fix: ACF and post-meta bindings on the same <code>(post_type, field name)<\/code> no longer coexist \u2014 they wrote to the same database row and silently raced. Tier 4 uniqueness now ignores <code>target.kind<\/code>. Existing pre-2.0.1 conflicts remain in the data store but the next save of either binding will reject.<\/li>\n<li>Fix: ACF bindings now require a non-empty <code>target.field_key<\/code> and validate it against the live ACF field when ACF is loaded. Previously a missing or mistyped field key could route <code>update_field()<\/code> writes to a different field.<\/li>\n<li>Fix: Test panel and Bulk Apply now report <code>skip_out_of_scope_type<\/code> \/ <code>skip_out_of_scope_status<\/code> for posts that wouldn't match the binding's scope in live triggers. Two new applier return codes \u2014 total now 11 instead of 9.<\/li>\n<li>Fix: Bulk Apply only clears the Stale badge when the walk had zero failures. Partial-failure walks keep the binding flagged so editors notice the divergence and retry.<\/li>\n<li>Fix: Binding form validation errors no longer throw the editor back to the list view \u2014 the form re-renders with submitted values via a short-lived transient flash, with the specific error inline.<\/li>\n<li>Internal: 21 new PHPUnit cases covering each fix path; bindings unit suite is now exhaustive on scope-filter, cross-kind dedup, ACF field_key validation, and Bulk Apply stamp gating.<\/li>\n<\/ul>\n\n<h4>2.0.0<\/h4>\n\n<ul>\n<li><strong>ACF \/ post-meta bindings<\/strong> \u2014 a Spintax template (or a per-post inline source) can now be bound to any ACF text\/textarea\/wysiwyg field or post-meta key on a post type. Configure once under Spintax \u2192 Bindings and the plugin populates the field on save, cron, or via Bulk Apply.<\/li>\n<li>Decision-tree write behaviour with four flags: <code>auto_seed_empty<\/code> (default on; never clobbers existing content), <code>regenerate_on_save<\/code>, <code>preserve_manual_edits<\/code> (hash-tracks the last rendered value so external edits are detected), <code>clear_on_empty<\/code>. Cold-start behaviour documented to avoid false manual-edit positives.<\/li>\n<li>Per-binding cron schedules (hourly \/ twicedaily \/ daily) registered as individual <code>wp_schedule_event<\/code> hooks per binding.<\/li>\n<li>Bulk Apply via Action Scheduler with chunked processing; a clean WP-CLI fallback when Action Scheduler isn't installed.<\/li>\n<li>New <code>%post_id%<\/code>, <code>%post_title%<\/code>, <code>%post_url%<\/code>, <code>%post_slug%<\/code>, <code>%post_date%<\/code>, <code>%post_modified%<\/code>, <code>%author_id%<\/code>, <code>%author_name%<\/code> post-context variables \u2014 opt-in per binding.<\/li>\n<li>New <code>%acf_&lt;field_name&gt;%<\/code> variables \u2014 opt-in per binding, exposes ACF sibling fields in the same group.<\/li>\n<li>Template-edit cascade \u2014 editing a Spintax template that is referenced by bindings bumps an internal cache version and surfaces a notice telling the editor that stored target fields will refresh on the next Bulk Apply \/ cron \/ save_post.<\/li>\n<li><code>wp spintax bindings list|apply|test|export|import<\/code> \u2014 full WP-CLI surface for staging\u2192production workflows and Action-Scheduler-less environments.<\/li>\n<li>One-shot migration helper at <strong>Tools \u2192 Spintax Migration<\/strong> for users coming from the predecessor plugin <code>nested-spintax-for-acf<\/code>. Detects, previews, and imports legacy data deduped by <code>(post_type, target.key)<\/code>. Original predecessor data is never deleted by the migration.<\/li>\n<li>Reserved-key guard rejects WP-internal meta keys, plugin-internal <code>_spintax_*<\/code> prefixes, wp_posts column names, and duplicate <code>(post_type, target.kind, target.key)<\/code> triples at form save.<\/li>\n<li>Hard cap of 200 bindings per site (single autoloaded option size budget).<\/li>\n<li>Per-binding chunk size override in the Advanced form section.<\/li>\n<li>Uninstall cleans every bindings option family and sibling post-meta \u2014 no orphan rows left behind.<\/li>\n<li>Internal: 398+ PHPUnit tests, including exhaustive decision-tree coverage and migration import edge cases.<\/li>\n<\/ul>\n\n<h4>1.5.0<\/h4>\n\n<ul>\n<li>Add: plural agreement primitive <code>{plural &lt;count&gt;: form1|form2|form3}<\/code> \u2014 pick the correct grammatical form by count. RU\/UK\/BE = 3 forms (<code>one|few|many<\/code>); EN\/ES\/PT\/DE etc. = 2 forms (<code>one|many<\/code>). Count is a <code>%var%<\/code> reference or literal integer (resolved after variable expansion, so helper-var patterns via <code>#set<\/code> work). Locale comes from per-template post meta <code>_spintax_locale<\/code> or the WordPress site locale. Lenient at runtime: malformed constructs render verbatim with fullwidth braces instead of crashing the page. First spintax engine to treat plural as a first-class primitive.<\/li>\n<li>Add: validator surface for plural blocks \u2014 structural check (form slot rejects nested <code>{}<\/code>, <code>[]<\/code>) always on; arity check (RU expects 3, EN expects 2) when locale is known.<\/li>\n<li>Internal: 74 PHPUnit cases mirroring the canonical TS implementation (<code>spintax-plurals.test.ts<\/code> in casino-platform). Engine classes <code>Plurals<\/code>, <code>PluralArityError<\/code>, <code>PluralFormError<\/code> ship alongside <code>Conditionals<\/code> from 1.4.0.<\/li>\n<\/ul>\n\n<h4>1.4.0<\/h4>\n\n<ul>\n<li>Add: conditional syntax <code>{?VAR?then|else}<\/code> \u2014 render a branch based on whether a variable is set\/non-empty (also <code>{?!VAR?then}<\/code> for inverted, optional else). Resolves both before and after <code>%var%<\/code> expansion, so conditionals inside variable values work too.<\/li>\n<li>Add: single-token abbreviation whitelist in post-processing \u2014 known shorthands like <code>\u0441\u043e\u0446.<\/code>, <code>\u044d\u043b.<\/code>, <code>Mr.<\/code>, <code>Inc.<\/code> no longer trigger sentence-end capitalisation of the next word. Covers Russian editorial\/address\/unit shorthands plus English titles and business suffixes.<\/li>\n<li>Fix: <code>#set<\/code> directive with an empty value (<code>#set %x% =<\/code>) no longer silently swallows the next directive on the following line.<\/li>\n<li>Fix: HTML start tags inside permutation alternatives (e.g. <code>[&lt;li&gt;item&lt;\/li&gt;|&lt;li&gt;...]<\/code>) are no longer mis-parsed as a <code>&lt;config&gt;<\/code> block.<\/li>\n<li>Improve: cache description in template meta box and global settings now explains that visitors see the same generated variant per runtime context until expiry or regeneration.<\/li>\n<li>Internal: regression tests for IDN domains flanked by Cyrillic letters and for randomisation behaviour across renders.<\/li>\n<\/ul>\n\n<h4>1.1.0<\/h4>\n\n<ul>\n<li>Add: per-element permutation separators \u2014 assign custom separator to each element via <code>&lt; sep &gt;<\/code> before <code>|<\/code><\/li>\n<li>Add: auto-spacing for purely alphabetic word separators (e.g. <code>&lt;and&gt;<\/code>, <code>&lt;\u0438\u043b\u0438&gt;<\/code>)<\/li>\n<li>Security: sanitize raw spintax input with custom sanitize_spintax() \u2014 strips invalid UTF-8, null bytes, and control characters while preserving angle-bracket syntax<\/li>\n<\/ul>\n\n<h4>1.0.1<\/h4>\n\n<ul>\n<li>Fix: permutation minsize\/maxsize logic when only one parameter is specified<\/li>\n<li>Fix: preview rendering no longer strips spintax config from template input<\/li>\n<li>Fix: child templates no longer inherit parent's local #set variables<\/li>\n<li>Improve: global variables editor now uses #set textarea (paste full blocks)<\/li>\n<li>Improve: validation errors displayed on template edit screen with line numbers<\/li>\n<li>Improve: \"Regenerate Public Cache\" now forces fresh subtree render<\/li>\n<li>Add: demo template created on first activation<\/li>\n<li>Add: SECURITY.md with responsible disclosure policy<\/li>\n<li>Add: Privacy Policy and External Services sections in readme.txt<\/li>\n<li>Code: PHPCS 0 errors, full WP.org review compliance<\/li>\n<\/ul>\n\n<h4>1.0.0<\/h4>\n\n<ul>\n<li>Initial release<\/li>\n<li>GTW-compatible spintax engine with nested enumerations and permutations<\/li>\n<li>Template CPT with code editor and admin preview<\/li>\n<li>Shortcode and PHP rendering API<\/li>\n<li>Object cache with versioned keys and cascade invalidation<\/li>\n<li>Per-template cron regeneration<\/li>\n<li>Global and local variable scopes<\/li>\n<li>Settings page with global variables editor<\/li>\n<\/ul>","raw_excerpt":"Template-based dynamic content generation using spintax markup for WordPress.","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/si.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin\/294363","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/si.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin"}],"about":[{"href":"https:\/\/si.wordpress.org\/plugins\/wp-json\/wp\/v2\/types\/plugin"}],"replies":[{"embeddable":true,"href":"https:\/\/si.wordpress.org\/plugins\/wp-json\/wp\/v2\/comments?post=294363"}],"author":[{"embeddable":true,"href":"https:\/\/si.wordpress.org\/plugins\/wp-json\/wporg\/v1\/users\/301st"}],"wp:attachment":[{"href":"https:\/\/si.wordpress.org\/plugins\/wp-json\/wp\/v2\/media?parent=294363"}],"wp:term":[{"taxonomy":"plugin_section","embeddable":true,"href":"https:\/\/si.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_section?post=294363"},{"taxonomy":"plugin_tags","embeddable":true,"href":"https:\/\/si.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_tags?post=294363"},{"taxonomy":"plugin_category","embeddable":true,"href":"https:\/\/si.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_category?post=294363"},{"taxonomy":"plugin_contributors","embeddable":true,"href":"https:\/\/si.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_contributors?post=294363"},{"taxonomy":"plugin_business_model","embeddable":true,"href":"https:\/\/si.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_business_model?post=294363"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}