| 开发者 | 301st |
|---|---|
| 更新时间 | 2026年7月14日 09:04 |
| PHP版本: | 8.0 及以上 |
| WordPress版本: | 7.0 |
| 版权: | GPLv2 or later |
| 版权网址: | 版权信息 |
{a|b|c} — randomly pick one option, with nesting support[<config>a|b|c] — pick N elements, shuffle, join with custom separators%var% — global, local (#set), and shortcode-level variable scopes{?VAR?then|else} — render a branch based on whether a variable is set (also {?!VAR?then} inverted){plural <count>: form1|form2|form3} — 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.#include or [spintax][spintax] / spintax_render() automatically expose the current product as %product_name%, %product_sku%, %product_categories%, %product_attribute_<slug>%, and more. Volatile pricing is intentionally out of scope. WooCommerce is optional — the variables simply appear when a product context is present.wp spintax bindings list|apply|test|export|importspintax folder to /wp-content/plugins/[spintax slug="my-template"] in posts/pages or spintax_render('my-template') in theme fileswp spintax bindings apply --binding=<id> --all. Action Scheduler turns those into one-click chunked async jobs that don't block the request. 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.Go to Spintax > Add New in the WordPress admin. Enter a title and your spintax markup in the editor.
{a|b|c} — randomly picks one option[a|b|c] — permutation: picks N elements, shuffles, joins with space[<minsize=2;maxsize=3;sep=", ";lastsep=" and "> a|b|c|d] — configured permutation%variable% — variable reference#set %var% = value — local variable definition{?VAR?then|else} — conditional: render a branch by truthiness of %VAR% (also {?!VAR?then} inverted){plural %Count%: form1|form2|form3} — plural agreement: picks the correct grammatical form by count (RU 3-form, EN 2-form)/#comment#/ — block comment (stripped from output)#include "slug" — embed another template{plural N: form1|form2|form3} in depth (EN/RU){?VAR?then|else} value-driven branching (EN/RU)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.
Yes: [spintax slug="greeting" name="Alice" city="Moscow"] makes %name% and %city% available inside the template.
Yes, since 2.2. On a single-product page the plugin auto-detects the current product and exposes it to [spintax] and spintax_render() as %product_*% variables — for example %product_name%, %product_slug%, %product_sku%, %product_type%, %product_stock_status%, %product_categories%, %product_tags%, %product_short_description%, and one %product_attribute_<slug>% per product attribute. So a template embedded as [spintax slug="product-seo-block"] on a product renders that product's data, and the same template on two products gets two separate cached variants.
Pricing (%product_price% and friends) is intentionally not exposed: it is volatile commerce data, not generated copy, and folding it into templates would churn the render cache on every price change.
To target a specific product regardless of the current page, pass [spintax slug="…" product_id="123"]; any explicit variable you pass always overrides the auto-detected one. WooCommerce is optional: with it inactive, or on non-product pages, behavior is unchanged.
Since 2.4 the plugin can also write generated copy into a product — see the next question. Product loops and cards are still deferred.
Yes, since 2.4. Create a binding with the target kind WooCommerce product field, on the Product post type, and pick Description or Short description. Every matching product then gets its own rendered copy — seeded when the field is empty, or regenerated on save if you ask for that — through the same machinery as ACF and post-meta bindings: cron schedules, Bulk Apply, WP-CLI, and the Logs page.
Turn on Expose WooCommerce product data in the binding's Variables tab and the template can use that product's own facts — %product_name%, %product_sku%, %product_type%, %product_categories%, %product_tags%, %product_attribute_<slug>% — so each product gets copy that is actually about it, not just a differently-worded version of the same sentence.
Three deliberate limits:
A binding pairs a Spintax template (or a per-post inline source) with one target field on one post type — for example "Posts → ACF: hero_subtitle". Configure it once under Spintax → 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.
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 — 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.
It's a recommended optional dependency for binding-heavy sites. The plugin works without it: admins can run a walk via the synchronous Run now button on each binding card, or wp spintax bindings apply --binding=<id> --all from the CLI. What Action Scheduler adds is chunked async execution, so:
Both walk every matching post for a binding and produce the same writes. They differ in how the walk runs:
Spintax → Logs in the admin sidebar. Both paths log a completion entry per walk (e.g. Bulk Apply run_synchronously completed for binding <id> — wrote=N skipped=M cleared=K.), plus warnings for partial failures. The Logs page supports level filtering, substring search, and pagination; entries are kept in a ring buffer sized by Settings → Spintax → Max log entries.
Five subcommands under wp spintax bindings:
wp spintax bindings list [--format=table|json|csv] — list all bindings on the site.wp spintax bindings apply --binding=<id> [--all|--post=<id>] — run a binding against every matching post (--all) or a single post (--post=<id>). This is the synchronous fallback path for Bulk Apply.wp spintax bindings test --binding=<id> --post=<id> — dry-run a binding against one post and report what would be written (target value, rendered preview, skip reason). Same logic as the admin Test panel; use this instead of apply when you want a preview.wp spintax bindings export {--binding=<id>|--all} [> bindings.json] — emit one binding or the full store as JSON to stdout, deduped by (post_type, target.key).wp spintax bindings import --file=bindings.json [--overwrite] [--dry-run] — import bindings from JSON. --overwrite updates matches on the same target triple; without it, duplicates are skipped. Use --dry-run to preview the plan without writing.A binding template sees four layered variable sources (later layers override earlier ones):
#set block in Settings → Spintax. Site-wide.#set block in the binding's Variables tab. Applies to that binding only.%post_id%, %post_title%, %post_url%, %post_slug%, %post_date%, %post_modified%, %author_id%, %author_name%.%acf_<field_name>%. Siblings are always fresh on save: the binding runs after ACF persists.{a|b|c}, [a|b], {?VAR?then|else}, {plural %N%: …}, #include "slug", /#comment#/).
Two trigger paths, both configurable per binding under "Triggers":
Each binding signs its last-rendered value and re-checks the target before every write. With Preserve manual edits enabled (default):
Bindings are a pre-generation 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. When you edit a template that has bindings pointing at it, the plugin:
wp spintax bindings apply --binding=<id> --all from the CLI). The Stale badge only clears when the entire walk completes with zero failures — partial-failure walks keep the badge so you notice the divergence and retry.
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.
The form rejects a handful of unsafe targets at save time:
_wp_, _edit_, _oembed_, etc.)._spintax_* slots used to store source, signatures, and cache versions.wp_posts columns like post_title, post_content, post_excerpt. These are not post-meta and writing to them via the meta API would silently create shadow rows.No — bindings are per-site. Each subsite manages its own. Use wp --url=site2 spintax bindings import --file=site1-bindings.json to copy bindings between subsites via the WP-CLI export/import round-trip.
Not in 2.0; bindings are admin-only. The wp spintax bindings WP-CLI surface covers staging→production sync scenarios. REST API exposure is tracked for a later release.
nested-spintax-for-acf. Is there a migration path?Yes. After activating Spintax 2.0, a dismissible admin banner points to Tools → Spintax Migration. The wizard scans for predecessor data, shows a per-row preview, and creates bindings deduped by (post type, target field). Per-post sources and variables are copied non-destructively — the old plugin's data stays in place until you delete it.
Yes — it is published as a standalone open-source library, so a template you author here renders identically elsewhere:
composer require spintax/core — https://packagist.org/packages/spintax/corenpm i @spintax/core — https://www.npmjs.com/package/@spintax/core%product_name%, %product_sku%, %product_type%, %product_categories%, %product_tags%, %product_attribute_<slug>% and more — so the copy can say something true about the product instead of merely varying its wording. Pricing stays out on purpose: it is volatile, and folding it into stored copy would churn on every price change.description and short_description, and nothing else. Price, SKU, stock and sale dates are commerce data, not copy — a template cannot reach them, at save time or at run time. "Preserve manual edits" remains on by default, so a regeneration will not destroy copy a shop owner wrote by hand.validate_save(), so each target kind now owns both halves of its validation instead of scattering kind checks through the admin. Two new outcome codes (15 in total) cover WooCommerce being inactive and a product field that is not writable. A generic re-entrancy guard stops the loop a product write would otherwise cause — $product->save() fires the same hook the binding trigger listens on.read_post on the target before rendering, so a binding dry-run cannot be used to read a draft product's fields you could not otherwise see. Closes a pre-existing gap for post and ACF context too.Wait... what? came out as Wait. . . What?, Wow!!! as Wow! ! ! and Really?! as Really? ! — the "add a space after .!?" rule fired between the marks of a run. A run is now treated as one sentence end, in every language.mailto: and tel: links survive rendering. <a href="mailto:you@example.com"> was rewritten to href="mailto: you@example.com" — a broken link — because the address was shielded out from under its prefix and the leftover colon then got a space.¿ and ¡ open a sentence, and the capitaliser upper-cases the first character after a sentence end — an inverted mark, which has no uppercase form — so every Spanish question quietly kept a lowercase first letter. Openers now carry the capital through, including ¡¿Qué haces?! (two marks) and <p>¿<a href="/ayuda">Necesitas ayuda</a>?</p> (an opener followed by markup).@spintax/core and are locked by the shared cross-engine corpus, so both engines stay byte-identical.Planner) fed a PlanInput DTO, and target-kind read/write/validation is dispatched through a TargetRegistry instead of inline acf_field/post_meta branches. No behavior change — all 13 binding outcome codes, and the Test-panel dry-run vs live apply, are byte-for-byte identical. Verified by a new 13-outcome table test, the entire existing binding suite passing unchanged, and a fresh-eyes contract audit of the diff. Groundwork for future target kinds; end users see no difference.plan() array-shape locks). 577 PHPUnit tests.%post_title%, %acf_<field>%) as spintax — enumeration / permutation / conditional / plural / %var% — execute a nested [spintax], or inject a #include. All three data-derived sources now share one SpintaxShield utility, so the "record data is content, not markup" rule holds everywhere (see the trust-level ADR in the repo's docs/).#set locals, global variables, spintax_render() arguments and shortcode attributes are unaffected.[spintax product_id=N] could still surface a draft/private product's context if that product had first been auto-detected earlier in the same request — the per-request memo was returned before the published-status gate. The memo is now scoped per resolution path (auto vs explicit), so the gate always applies. Follow-up to the 2.2.0 explicit-id gate.{ } [ ] % #) render literally instead of being re-interpreted as enumerations / permutations / variables, a nested [spintax], or a #include directive. Product data is content, not markup.#include shield, spintax_render() variable pass-through). 544 PHPUnit tests.[spintax] and spintax_render() now auto-expose the current product as %product_id%, %product_name%, %product_slug%, %product_sku%, %product_type%, %product_stock_status%, %product_categories%, %product_tags%, %product_short_description%, and one %product_attribute_<slug>% per attribute. Read-only — nothing is written to products. Volatile pricing data is intentionally excluded.product_id="123" to target a specific product regardless of the current page; explicit shortcode / PHP variables always override auto-detected product variables. Explicit product_id exposes published products only, so it can't surface draft or private product data.%product_categories% / %product_tags%) is memoised per product for the request; nested [spintax] / #include inherit the product context without re-detecting it.{plural %n%: …} no longer renders empty when the count variable was #set to an enumeration (e.g. #set %n% = {1|4|9}). Enumerations inside #set values now collapse once, so a variable holds a single stable value — the plural count sees a real number and every %n% reference stays consistent. Values carrying conditionals/plurals are left deferred (unchanged).WooCommerceProductContextSource + RuntimeContextBuilder, wired into the shortcode and spintax_render() entry points. WooCommerce remains an optional dependency — no fatal errors when it is absent. 539 PHPUnit tests (was 520).no_action_scheduler error path so users had to click-to-learn.Bulk Apply run_synchronously completed for binding <id> — wrote=N skipped=M cleared=K), so the "View details in Logs →" CTA on the success notice always lands on a populated page. Previously only failures logged.name (field_key)) is now stripped before the haystack filter, so "browse without retyping" works as documented.kind=acf_field exactly (previously hid only when kind=post_meta, leaving an empty-kind edge case where the row could render without hidden).test_run_now_handler_rejects_non_admin — replaces a conditional soft assertion with two unconditional invariants (walk-lock never acquired, no "Wrote N" success flash).{text, action_url, action_label} payload alongside legacy strings.?active_tab= query arg.<input list> + <datalist>). Group → field grouping, substring search across group / label / name, full ARIA combobox semantics, and clicking a row autofills both the field name and the stable ACF field key in one go.save_post is off and cron is disabled — live update on checkbox / select change so editors notice the problem before submitting.BulkApply::run_synchronously() — useful for dev sites without cron traffic and for installs without Action Scheduler. Walk-status badge ("Running (started Ns ago)") appears on the card while the per-binding walk lock is held.wp_ajax_spintax_dismiss_admin_notice endpoint. Whitelisted notice ids prevent the endpoint from filling wp_usermeta with arbitrary rows.{message, type} and the new rich payload. BindingsPage constructor now accepts an optional BulkApply for test injection. New shared Spintax\Support\TtlField helper backs the preset / custom TTL widget. Notice action-url is esc_url()-filtered so javascript: and other unsafe schemes are stripped.BindingApplier::plan() rejects bindings whose stored target.field_key no longer resolves to a field with the expected name (deleted, renamed, or re-assigned in ACF). Two new return codes: skip_acf_not_loaded (ACF deactivated since the binding was saved) and skip_invalid_acf_field (key + name disagreement). Closes a path where CLI-imported or imported-while-ACF-inactive bindings could write through update_field() to the wrong field.BindingApplier::read_target() and ::write_target() no longer fall back to plain update_post_meta() / get_post_meta() for kind = acf_field 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._spintax_binding_walk_failed_v_<id> flag. The final chunk gates stamp_last_applied_version() 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.WP_Error 'walk_in_progress'. Both enqueue() and run_synchronously() acquire a per-binding lock (option _spintax_binding_walk_lock_<id>) at walk start; stale locks older than one hour are auto-overwritten so a crashed walk doesn't permanently jam the binding.npm run lint:php and lint:php:fix moved to scripts/lint-php.sh / scripts/lint-php-fix.sh. The inline command tripped over bash-c quoting on Windows. .gitattributes enforces LF endings on shipped text files.wp spintax bindings import --overwrite help text updated to reflect the 2.0.1 (post_type, target.key) uniqueness contract.wp spintax bindings 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.(post_type, field name) no longer coexist — they wrote to the same database row and silently raced. Tier 4 uniqueness now ignores target.kind. Existing pre-2.0.1 conflicts remain in the data store but the next save of either binding will reject.target.field_key and validate it against the live ACF field when ACF is loaded. Previously a missing or mistyped field key could route update_field() writes to a different field.skip_out_of_scope_type / skip_out_of_scope_status for posts that wouldn't match the binding's scope in live triggers. Two new applier return codes — total now 11 instead of 9.auto_seed_empty (default on; never clobbers existing content), regenerate_on_save, preserve_manual_edits (hash-tracks the last rendered value so external edits are detected), clear_on_empty. Cold-start behaviour documented to avoid false manual-edit positives.wp_schedule_event hooks per binding.%post_id%, %post_title%, %post_url%, %post_slug%, %post_date%, %post_modified%, %author_id%, %author_name% post-context variables — opt-in per binding.%acf_<field_name>% variables — opt-in per binding, exposes ACF sibling fields in the same group.wp spintax bindings list|apply|test|export|import — full WP-CLI surface for staging→production workflows and Action-Scheduler-less environments.nested-spintax-for-acf. Detects, previews, and imports legacy data deduped by (post_type, target.key). Original predecessor data is never deleted by the migration._spintax_* prefixes, wp_posts column names, and duplicate (post_type, target.kind, target.key) triples at form save.{plural <count>: form1|form2|form3} — pick the correct grammatical form by count. RU/UK/BE = 3 forms (one|few|many); EN/ES/PT/DE etc. = 2 forms (one|many). Count is a %var% reference or literal integer (resolved after variable expansion, so helper-var patterns via #set work). Locale comes from per-template post meta _spintax_locale 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.{}, []) always on; arity check (RU expects 3, EN expects 2) when locale is known.spintax-plurals.test.ts in casino-platform). Engine classes Plurals, PluralArityError, PluralFormError ship alongside Conditionals from 1.4.0.{?VAR?then|else} — render a branch based on whether a variable is set/non-empty (also {?!VAR?then} for inverted, optional else). Resolves both before and after %var% expansion, so conditionals inside variable values work too.соц., эл., Mr., Inc. no longer trigger sentence-end capitalisation of the next word. Covers Russian editorial/address/unit shorthands plus English titles and business suffixes.#set directive with an empty value (#set %x% =) no longer silently swallows the next directive on the following line.[<li>item</li>|<li>...]) are no longer mis-parsed as a <config> block.< sep > before |<and>, <или>)