Skip to content

Commit 7fcefe9

Browse files
committed
v2.0.9: keep the shortcode enabled on read, allow it outside the_content
Reviewed against ACF Pro 6.8.6 (current release, 2026-07-14) and WP 7.0.2. The plugin worked, but two things were incomplete. 1. On a block theme the [acf] shortcode produced nothing outside the_content. ACF gates that separately from enable_shortcode: if ( wp_is_block_theme() ) { if ( ! doing_filter( 'the_content' ) && ! apply_filters( 'acf/shortcode/allow_in_block_themes_outside_content', false ) ) { return; } } (acf_shortcode(), api-template.php:1025-1030). So a template part, a widget or a block outside the content flow rendered empty, silently. Measured on Twenty Twenty-Five, rendering from wp_footer: empty with the old behaviour, field value now. Classic themes were never affected - ACF skips the branch. This deliberately widens the scope to every rendering context, not just post content; documented as such. 2. The setting was only written once, on acf/init. acf_get_setting() applies acf/settings/{$name} on every read (api-helpers.php:101) and acf_shortcode() consults it at render time (line 1017), so any later acf_update_setting( 'enable_shortcode', false ) won silently. Verified with a competing plugin writing false on acf/init priority 999: the shortcode stopped rendering before, keeps working now. The write stays for readers of acf_raw_setting(). Timing was ruled out rather than assumed: add_shortcode() runs unconditionally at include time (line 1133) and the setting is read inside the callback, so acf/init is early enough. Docs gained what was missing: ACF only disables the shortcode by default for installations first activated on 6.3+ (acf.php:239-244), so the plugin is a no-op on older ones; the block-theme restriction and the consequence of lifting it; and the further conditions ACF imposes - only registered fields, resolved through the _fieldname reference meta, only bindings-capable types, only publicly viewable posts, escaped output. Also recorded that Requires Plugins: advanced-custom-fields-pro resolves against the installed folder even though ACF Pro is not on wordpress.org. tests/test-hooks.php: 10 cases, 2 red against 2.0.8.
1 parent 0a6c377 commit 7fcefe9

5 files changed

Lines changed: 361 additions & 18 deletions

File tree

CLAUDE.md

Lines changed: 93 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,34 @@
22

33
## Plugin Overview
44

5-
Enables ACF (Pro)'s `[acf]` shortcode by switching the `enable_shortcode` setting to `true` on `acf/init`, so field values can be rendered inside WYSIWYG content.
5+
Re-enables ACF (Pro)'s `[acf]` shortcode, which ACF turns off by default, and lifts the extra restriction ACF places on it under block themes.
66

7-
- **Text Domain:** `jpkcom-acf-shortcode-enable` (no header declared, defaults to slug; only used by the shared updater)
7+
- **Text Domain:** `jpkcom-acf-shortcode-enable` (no header declared, defaults to slug; the plugin has no translatable strings of its own)
88
- **Requires Plugins:** `advanced-custom-fields-pro`
99
- **Min PHP:** 8.3 | **Min WP:** 6.9
1010
- **Network:** not network-only (no `Network:` header)
1111

12+
> **Verified against ACF Pro 6.8.6** (current release at the time of writing, 2026-07-14) and WordPress 7.0.2. Every hook below carries the ACF file and line that consumes it — check those first when an ACF update lands.
13+
14+
---
15+
16+
## Why the shortcode is off in the first place
17+
18+
ACF does not simply ship `enable_shortcode => false`. The default is `true`, and ACF then flips it based on when ACF was *first activated* on the site (`acf.php:239-244`):
19+
20+
```php
21+
$first_activated_version = acf_get_version_when_first_activated();
22+
23+
// Only enable shortcode by default for versions prior to 6.3
24+
if ( $first_activated_version && version_compare( $first_activated_version, '6.3', '>=' ) ) {
25+
$this->settings['enable_shortcode'] = false;
26+
}
27+
```
28+
29+
So on an installation whose ACF was first activated on **6.3 or later** the shortcode is off and this plugin is what turns it back on. On an older installation it is already on and **this plugin is a no-op**. Worth knowing before hunting for an effect that was never missing. On `posts.ddev.site`, `acf_get_version_when_first_activated()` returns `6.3.6`, so the plugin is load-bearing there — measured: `acf_get_setting( 'enable_shortcode' )` is `true` with the plugin and `false` without it, and the field value appears in the rendered page only with it.
30+
31+
Turning the shortcode on is, by design, taking back a hardening ACF introduced. That is the purpose of the plugin, not an oversight — but it should be a conscious decision per site.
32+
1233
---
1334

1435
## Architecture
@@ -19,19 +40,56 @@ Main file (jpkcom-acf-shortcode-enable.php)
1940
├── Plugin header (Requires Plugins: advanced-custom-fields-pro)
2041
├── JPKCOM_ACF_SHORTCODE_ENABLE_VERSION constant
2142
├── init @ priority 5: boot JPKComGitPluginUpdater
22-
└── add_action acf/init → jpkcom_acf_enable_shortcode() : void
23-
└── acf_update_setting( 'enable_shortcode', true )
43+
├── acf/init → jpkcom_acf_enable_shortcode()
44+
│ └── acf_update_setting( 'enable_shortcode', true )
45+
├── acf/settings/enable_shortcode → __return_true (PHP_INT_MAX)
46+
└── acf/shortcode/allow_in_block_themes_outside_content → __return_true (PHP_INT_MAX)
2447
```
2548

2649
---
2750

2851
## Behaviour
2952

30-
| Hook | Type | Effect |
31-
|------|------|--------|
32-
| `acf/init` | action | Calls `acf_update_setting( 'enable_shortcode', true )` |
53+
| Hook | Consumed by (ACF Pro 6.8.6) | Effect |
54+
|------|-----------------------------|--------|
55+
| `acf/init` || Writes `enable_shortcode = true` so readers of `acf_raw_setting()` see it |
56+
| `acf/settings/enable_shortcode` (`PHP_INT_MAX`) | `acf_get_setting()`, `includes/api/api-helpers.php:101` | Reports the shortcode as enabled on **every read**, which is the read `acf_shortcode()` performs |
57+
| `acf/shortcode/allow_in_block_themes_outside_content` (`PHP_INT_MAX`) | `acf_shortcode()`, `includes/api/api-template.php:1025-1030` | Allows the shortcode outside `the_content` on block themes |
58+
59+
The `acf/init` callback is `function_exists()`-guarded and `jpkcom_`-prefixed (renamed from the previous generic `set_acf_settings`) to avoid global-namespace collisions.
60+
61+
### Why both the write and the filter
62+
63+
`acf_shortcode()` reads the setting at render time through `acf_get_setting()`, which applies `acf/settings/enable_shortcode` on every call. A one-shot `acf_update_setting()` on `acf/init` is therefore not authoritative: any later write wins. Measured against 6.8.6 with a competing plugin writing `false` on `acf/init` priority 999 — with only the write, the shortcode stopped rendering; with the filter it kept working. The write is kept because anything reading `acf_raw_setting( 'enable_shortcode' )` bypasses the filter.
64+
65+
Timing is not the problem, and was worth ruling out: `add_shortcode( 'acf', 'acf_shortcode' )` runs unconditionally at include time (`api-template.php:1133`) and the setting is only consulted inside the callback (line 1017), so `acf/init` is early enough.
66+
67+
### The block-theme restriction, and that we lift it
68+
69+
On a block theme `acf_shortcode()` returns nothing unless it is running inside the `the_content` filter, independently of `enable_shortcode`. Without the filter this plugin now sets, enabling the shortcode only covered post content on an FSE theme; a template part, a widget or a block outside the content flow rendered empty with no diagnostic.
3370

34-
The callback is `function_exists()`-guarded and `jpkcom_`-prefixed (renamed from the previous generic `set_acf_settings`) to avoid global-namespace collisions.
71+
Measured on Twenty Twenty-Five, rendering `[acf]` from `wp_footer` (i.e. outside `the_content`):
72+
73+
| Theme | State | outside `the_content` | inside `the_content` |
74+
|---|---|---|---|
75+
| Twenty Twenty-Five (block) | 2.0.8 behaviour | **empty** | value |
76+
| Twenty Twenty-Five (block) | 2.0.9 | **value** | value |
77+
| bootscore-child (classic) | 2.0.9 | value | value |
78+
79+
Note the scope: this covers **every** rendering context, not only post content. It is a deliberate widening, chosen for this fleet; if a site should keep ACF's narrower default, remove that one filter.
80+
81+
---
82+
83+
## What "enabled" still does not get you
84+
85+
ACF places several further conditions on the shortcode. All of these bite silently — the shortcode returns an empty string, with no error and no log entry, except in previews where ACF substitutes an explanatory message:
86+
87+
- **Only registered fields.** `acf_shortcode()` forces `acf/prevent_access_to_unknown_fields` to `true` for the duration of the call, so a field name with no ACF definition yields nothing.
88+
- **Resolved through the reference meta.** `acf_maybe_get_field( …, $strict = true )` looks the name up via `acf_get_meta_field()`, i.e. through the `_fieldname` → field-key meta. A value written with a bare `update_post_meta()` and no reference row is invisible to the shortcode, even when `get_field()` returns it.
89+
- **Only bindings-capable field types** (`acf_field_type_supports( $type, 'bindings', true )`), and only fields whose `allow_in_bindings` is not false.
90+
- **Only publicly viewable posts**, when a `post_id` other than the current one is passed (`acf/shortcode/prevent_access_to_fields_on_non_public_posts`).
91+
- **Capability gates:** previews need `publish_posts` (`acf/shortcode/preview_capability`), AJAX requests need `edit_posts` (`acf/ajax/shortcode_capability`).
92+
- **The value is escaped** unless `acf/shortcode/allow_unsafe_html` is filtered; when ACF strips something it fires `acf/removed_unsafe_html`.
3593

3694
---
3795

@@ -85,12 +143,37 @@ Triggered by **pushing a `v*` tag**; the workflow creates the GitHub release aut
85143

86144
- `declare(strict_types=1)` in every PHP file
87145
- `acf/init` callback prefixed + `function_exists()`-guarded (no collision)
146+
- No input, no output, no database writes of its own
147+
- Deliberately reverses two ACF hardening defaults — see the two sections above; that is the plugin's purpose, but it belongs in a per-site decision
148+
- ACF's own output escaping is left intact: `acf/shortcode/allow_unsafe_html` is **not** filtered here
88149
- Updater: SHA256 verification + URL validation (audited separately)
89150

151+
### Plugin dependency
152+
153+
`Requires Plugins: advanced-custom-fields-pro` works even though ACF Pro is not on wordpress.org: WordPress resolves the slug against the installed plugin folder. Verified on WP 7.0.2 — `WP_Plugin_Dependencies::get_dependency_filepath()` returns `advanced-custom-fields-pro/acf.php`, `has_unmet_dependencies()` is `false`, and the name resolves to "Advanced Custom Fields PRO". Only `get_dependency_data()` returns `false`, which just means no "install dependency" link in the Plugins screen. The prerequisite is that ACF Pro sits in a folder with exactly that name.
154+
155+
---
156+
157+
## Tests
158+
159+
`tests/test-hooks.php` runs standalone — no WordPress and no ACF. It stubs the functions the main file touches at load time, requires the plugin, then asserts hook names and priorities and invokes each callback: both new filters and their return values, that the `acf/init` write still happens and touches only `enable_shortcode`, the updater bootstrap priority, and that the version constant matches the header. 10 cases; 2 of them fail against 2.0.8. CI runs it on every pull request and push to `main`.
160+
161+
```bash
162+
php tests/test-hooks.php # exit 0 = green
163+
```
164+
165+
What it cannot cover is whether ACF still honours these hooks, or the block-theme behaviour — both need a real instance with a block theme active. See the measured table above.
166+
90167
---
91168

92169
## Release Checklist
93170

94-
1. Bump version in: header `Version:` + `Stable tag:`, `JPKCOM_ACF_SHORTCODE_ENABLE_VERSION`, `README.md`, `phpdoc.xml`
171+
1. Bump the version in five places:
172+
- Plugin header `Version:`
173+
- Plugin header `Stable tag:`
174+
- Constant `JPKCOM_ACF_SHORTCODE_ENABLE_VERSION`
175+
- `phpdoc.xml` `<version number="…">`
176+
- `README.md``**Version:**` and `**Stable tag:**`
95177
2. Add a `### x.y.z` block to `## Changelog` in `README.md`
96-
3. Commit, tag `vx.y.z`, push the tag → the workflow builds and publishes everything
178+
3. Run `php tests/test-hooks.php`
179+
4. Commit, then push the tag `vx.y.z` → the workflow builds and publishes everything

README.md

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
**Plugin Name:** JPKCom ACF (Pro) Enable Shortcode
44
**Plugin URI:** https://github.com/JPKCom/jpkcom-acf-shortcode-enable
55
**Description:** Shortcodes can be used within a WYSIWYG to display another field’s value.
6-
**Version:** 2.0.8
6+
**Version:** 2.0.9
77
**Author:** Jean Pierre Kolb <jpk@jpkc.com>
88
**Author URI:** https://www.jpkc.com
99
**Contributors:** JPKCom
@@ -12,7 +12,7 @@
1212
**Requires at least:** 6.9
1313
**Tested up to:** 7.1
1414
**Requires PHP:** 8.3
15-
**Stable tag:** 2.0.8
15+
**Stable tag:** 2.0.9
1616
**License:** GPL-2.0-or-later
1717
**License URI:** https://www.gnu.org/licenses/gpl-2.0.html
1818

@@ -23,8 +23,20 @@ Shortcodes can be used within a WYSIWYG to display another field’s value.
2323

2424
Shortcodes can be used within a WYSIWYG to display another field’s value.
2525

26+
ACF disables the `[acf]` shortcode by default on any installation whose ACF was first activated on version 6.3 or later. This plugin turns it back on, and keeps it on: the setting is filtered on every read, so a later change from elsewhere cannot quietly switch it off again. On block themes it additionally lifts ACF's separate restriction that otherwise limits the shortcode to post content, so `[acf]` also works in template parts, widgets and blocks outside the content flow.
27+
28+
If your ACF installation predates 6.3, the shortcode is already enabled and this plugin changes nothing.
29+
30+
**Please note:** both of these are hardening defaults that ACF introduced deliberately. Enabling the shortcode is the point of this plugin — but it is worth deciding per site. ACF's output escaping is left untouched.
31+
32+
Verified against **ACF Pro 6.8.6**.
33+
2634
For more details visit: https://www.advancedcustomfields.com/resources/shortcode/
2735

36+
### Requirements for a shortcode to actually output something
37+
38+
Enabling the shortcode is necessary but not sufficient. ACF additionally requires that the field is **registered** (a bare post meta value is not enough — ACF resolves the name through its `_fieldname` reference row), that the field type supports bindings, and that the referenced post is publicly viewable. Previews need the `publish_posts` capability. When one of these is not met the shortcode outputs nothing, silently.
39+
2840

2941
### Documentation
3042

@@ -42,6 +54,13 @@ For more details visit: https://www.advancedcustomfields.com/resources/shortcode
4254

4355
## Changelog
4456

57+
### 2.0.9
58+
* Fixed: on a block theme the `[acf]` shortcode produced nothing outside `the_content` — a template part, a widget or a block outside the content flow rendered empty, with no error and no log entry. ACF gates that separately from the shortcode setting (`acf_shortcode()`, api-template.php:1025-1030); the plugin now lifts it via `acf/shortcode/allow_in_block_themes_outside_content`. Measured on Twenty Twenty-Five: outside `the_content` empty before, the field value after. Classic themes were never affected — ACF skips the branch entirely
59+
* Changed: the shortcode setting is now also filtered on read (`acf/settings/enable_shortcode`, at `PHP_INT_MAX`) instead of relying solely on the one-shot `acf_update_setting()` on `acf/init`. `acf_get_setting()` applies that filter on every read and `acf_shortcode()` consults it at render time, so any later `acf_update_setting( 'enable_shortcode', false )` — from another plugin, or from ACF itself in a future release — used to win silently. Verified against ACF Pro 6.8.6 with a competing plugin writing false on `acf/init` priority 999: the shortcode stopped rendering before, keeps working now. The `acf_update_setting()` call stays so that readers of the raw setting see the enabled state too
60+
* Added: `tests/test-hooks.php` covers the hook surface and every callback; CI runs it on every pull request and push to `main`
61+
* Docs: documented why the shortcode is off in the first place (ACF only disables it by default for installations first activated on 6.3 or later, `acf.php:239-244`, so this plugin is a no-op on older installs), the block-theme restriction and that lifting it widens the scope beyond post content, and the further conditions ACF places on the shortcode — only registered fields resolved through the `_fieldname` reference meta, only field types supporting bindings, only publicly viewable posts, and an escaped value unless `acf/shortcode/allow_unsafe_html` says otherwise
62+
* Verified against ACF Pro **6.8.6** and WordPress 7.0.2. `Requires Plugins: advanced-custom-fields-pro` resolves against the installed plugin folder even though ACF Pro is not on wordpress.org — only the "install dependency" link is unavailable
63+
4564
### 2.0.8
4665
* Fixed: the update manifest no longer reports `network: true` for this plugin. The generator defaulted a missing `Network:` header to true, while WordPress' own default for a missing header is "not network-only". Metadata only — WordPress derives network-only from the plugin header via `is_network_only_plugin()`, not from the update manifest
4766
* CI: the lint and guard workflow now also runs on pushes to `main`. It only covered pull requests, so a direct push with bypass rights skipped every check

jpkcom-acf-shortcode-enable.php

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
Plugin Name: JPKCom ACF (Pro) Enable Shortcode
44
Plugin URI: https://github.com/JPKCom/jpkcom-acf-shortcode-enable
55
Description: Shortcodes can be used within a WYSIWYG to display another field’s value.
6-
Version: 2.0.8
6+
Version: 2.0.9
77
Author: Jean Pierre Kolb <jpk@jpkc.com>
88
Author URI: https://www.jpkc.com
99
Contributors: JPKCom
@@ -12,7 +12,7 @@
1212
Requires at least: 6.9
1313
Tested up to: 7.1
1414
Requires PHP: 8.3
15-
Stable tag: 2.0.8
15+
Stable tag: 2.0.9
1616
License: GPL-2.0-or-later
1717
License URI: https://www.gnu.org/licenses/gpl-2.0.html
1818
*/
@@ -30,7 +30,7 @@
3030
* @since 2.0.3
3131
*/
3232
if ( ! defined( 'JPKCOM_ACF_SHORTCODE_ENABLE_VERSION' ) ) {
33-
define( 'JPKCOM_ACF_SHORTCODE_ENABLE_VERSION', '2.0.8' );
33+
define( 'JPKCOM_ACF_SHORTCODE_ENABLE_VERSION', '2.0.9' );
3434
}
3535

3636

@@ -66,8 +66,10 @@
6666
/**
6767
* Enable the ACF (Pro) `[acf]` shortcode.
6868
*
69-
* Turns on ACF's `enable_shortcode` setting so field values can be rendered
70-
* inside WYSIWYG content via the shortcode.
69+
* Writes ACF's `enable_shortcode` setting so anything reading the raw
70+
* setting (`acf_raw_setting()`) sees the enabled state too. The read-time
71+
* filter below is what actually guarantees it — see there for why both
72+
* exist.
7173
*
7274
* @since 1.0.0
7375
*
@@ -80,3 +82,49 @@ function jpkcom_acf_enable_shortcode(): void {
8082
}
8183

8284
add_action( 'acf/init', 'jpkcom_acf_enable_shortcode' );
85+
86+
/**
87+
* Keep the `[acf]` shortcode enabled on every read of the setting.
88+
*
89+
* `acf_get_setting()` applies an `acf/settings/{$name}` filter on every read
90+
* (`includes/api/api-helpers.php:101`), and `acf_shortcode()` consults
91+
* `acf_get_setting( 'enable_shortcode' )` at render time. Filtering the read is
92+
* therefore authoritative, while the `acf_update_setting()` call above writes
93+
* the value once on `acf/init` and any later `acf_update_setting(
94+
* 'enable_shortcode', false )` — from another plugin, or from ACF itself in a
95+
* future release — would silently win.
96+
*
97+
* Measured against ACF Pro 6.8.6 with a competing plugin writing `false` on
98+
* `acf/init` priority 999: with only the one-shot write the shortcode stopped
99+
* rendering; with this filter it kept working.
100+
*
101+
* @since 2.0.9
102+
*/
103+
add_filter( 'acf/settings/enable_shortcode', '__return_true', PHP_INT_MAX );
104+
105+
/**
106+
* Allow the `[acf]` shortcode outside `the_content` on block themes.
107+
*
108+
* On a block theme, `acf_shortcode()` bails out unless it is running inside the
109+
* `the_content` filter:
110+
*
111+
* if ( function_exists( 'wp_is_block_theme' ) && wp_is_block_theme() ) {
112+
* if ( ! doing_filter( 'the_content' ) && ! apply_filters(
113+
* 'acf/shortcode/allow_in_block_themes_outside_content', false ) ) {
114+
* return;
115+
* }
116+
* }
117+
*
118+
* So a shortcode in a template part, a block outside the content flow or a
119+
* widget produced nothing at all, with no error and no log entry. Enabling the
120+
* shortcode without this filter therefore only did half the job on an FSE theme.
121+
*
122+
* This deliberately reopens a restriction ACF added on purpose. That is the
123+
* point of the plugin — the `[acf]` shortcode is off by default on ACF 6.3+ for
124+
* the same reason — but be aware the scope is wider than the setting alone: it
125+
* covers every rendering context, not just post content. Classic themes are
126+
* unaffected either way, because the branch above is skipped entirely.
127+
*
128+
* @since 2.0.9
129+
*/
130+
add_filter( 'acf/shortcode/allow_in_block_themes_outside_content', '__return_true', PHP_INT_MAX );

phpdoc.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
<paths>
1010
<output>docs</output>
1111
</paths>
12-
<version number="2.0.8">
12+
<version number="2.0.9">
1313
<folder>docs</folder>
1414
<api>
1515
<source dsn=".">

0 commit comments

Comments
 (0)