What Renders This releases
Dev-only WordPress plugin: which template rendered this page, which ACF groups are attached, which partials fired
by Ian Garcia · github.com/quietcactus/what-renders-this · website
Install
The author publishes release zips, so WP-CLI can install straight from GitHub:
wp plugin install https://github.com/quietcactus/what-renders-this/releases/download/v1.2.1/what-renders-this.zipReadme
What Renders This
A dev-only WordPress plugin that answers, on any frontend page: which template file rendered this, what the route and post context are, which ACF field groups are attached, and the ordered chain of theme partials that fired.
- Version: 1.2.2
- Author: Ian Garcia
- License: GPL-2.0-or-later
- Requires PHP: 7.4+
- Requires WordPress: 6.0+
- Requires: nothing. ACF is optional — the ACF section hides itself when ACF is not active.
What it does
Adds an admin bar node labelled What Renders This, with the resolved template in its tooltip. Click it and a panel opens with the full picture:
| Section | What it shows |
|---|---|
| Route | Which is_* conditionals matched, the queried object class, the object ID, the post type. |
| Template | The resolved template (theme-relative), the _wp_page_template assignment, and every candidate the hierarchy considered with the winner flagged. |
| ACF | Field groups whose location rules match this post, each with its local-JSON file and field list. |
| Partials | Every theme file included during the render, in first-inclusion order. |
Copy context puts the whole report on the clipboard as markdown. A BugHerd ticket carries a URL; this turns that URL into a file list before you open an editor, and the blob pastes straight into Claude Code as starting context.
## Page context
URL https://staging.example.test/team/jane-smith/
Route is_single · is_singular · post_type=team · ID 2044
Template mytheme/single-team.php
Assigned (none — resolved by hierarchy)
## ACF groups
- Team Bio mytheme/acf-json/group_5f2a.json (14 fields)
bio_headline (text)
bio_body (wysiwyg)
## Partials, first-inclusion order
1. mytheme/includes/include-tracker.php
2. mytheme/includes/include-banner.php
3. mytheme/includes/include-bio-tabs.php
Installation
Grab what-renders-this.zip from the latest release, then either:
- Upload it — Plugins → Add New → Upload Plugin, pick the zip, activate.
- Unpack it — extract
what-renders-this/intowp-content/plugins/, activate What Renders This in Plugins.
Upgrading from 1.2.0 or earlier, which shipped as wrt-inspector/: install this version, then delete the old wrt-inspector/ folder and reactivate. WordPress treats the renamed directory as a separate plugin, so both will be listed until you remove the old one.
There is nothing to configure. The plugin writes no options, creates no tables, and makes no database writes at all.
Who sees it
Activate it and it works. There is no environment check and nothing to define.
The only gate is manage_options, and that is deliberate: everything the panel shows — template filenames, ACF group names, local-JSON paths, partial paths — is already in front of that user in the dashboard. The plugin makes no database writes, and field values stay off unless asked for per request. An environment gate would have added no protection while silently disabling the plugin on managed hosts, which report production from wp_get_environment_type() for staging and dev installs alike.
It also skips admin screens, AJAX, REST, cron and WP-CLI. v1 is frontend only.
Turning it off
A constant, for a site where you want it installed but dormant:
define('WRT_INSPECTOR', false);
Or a filter, for anything conditional — per environment, per user, per URL:
add_filter('wrt_inspector_enabled', function ($enabled) {
return wp_get_environment_type() !== 'production';
});
The constant overrides the default in both directions; the filter runs last and wins.
Caching
Whenever the gate passes, the plugin sends nocache_headers() on send_headers. The panel renders inline in the page HTML, so a misconfigured full-page cache could otherwise store an administrator's response and serve the panel to anonymous visitors.
Usage
- Admin bar node — the label is the plugin name; the tooltip carries the resolved template and the object ID. Click to toggle the panel,
Escto close. ?wrt-inspect=1— renders the panel already open, independent of the admin bar. Use this when a theme suppresses the bar or a user has turned it off.?wrt-inspect-values=1— adds ACF field values to the panel and to the copied report. Off by default, and the values are not fetched server-side at all unless the parameter is present. The show values link in the ACF section toggles it.
Field values are live client content. Turning them on can put names, addresses or unpublished copy into anything you paste the report into. That is why it is opt-in per request rather than a saved preference.
How it works
| File | Responsibility |
|---|---|
what-renders-this.php |
Bootstrap — constants, wrt_inspector_enabled(), hook wiring on init. |
includes/class-wrt-inspector-gate.php |
On/off switch, capability check, request-type check. |
includes/class-wrt-inspector-route.php |
Captures template_include and every {$type}_template_hierarchy filter; route conditionals. |
includes/class-wrt-inspector-trace.php |
Snapshots get_included_files() and diffs it to produce the partial chain. |
includes/class-wrt-inspector-acf.php |
Resolves attached field groups and locates their local-JSON files. |
includes/class-wrt-inspector-panel.php |
Admin bar node, panel markup, markdown report. |
includes/class-wrt-inspector-state.php |
Composes the above and builds the context array once, at the end of the footer. |
assets/inspector.css / inspector.js |
Namespaced panel styling, toggle and clipboard behaviour. |
Why the partial tracer works the way it does
Themes pull partials with raw include(locate_template('includes/include-banner.php')). WordPress fires no action for that, and locate_template() has no filter. There is nothing to hook.
So the tracer works one level below WordPress, at the PHP language level:
- Snapshot
get_included_files()attemplate_redirectpriority0. - Snapshot it again at
wp_footerpriorityPHP_INT_MAX. - Subtract, keep only files under the parent or child theme directory, normalise to theme-relative paths.
That catches include, require, get_template_part() and include_module() uniformly, because all four end up in the same PHP include table.
Why the node is added on wp_before_admin_bar_render
The node tooltip carries the resolved template, so the node cannot be built before template_include has fired. Both admin_bar_menu and wp_before_admin_bar_render are safe on that count — they fire back to back inside wp_admin_bar_render(), which runs on wp_body_open (or wp_footer as a fallback), long after the template is known. _wp_admin_bar_init() runs earlier on template_redirect, but it only instantiates WP_Admin_Bar and registers core's callbacks; it does not fire admin_bar_menu.
wp_before_admin_bar_render is used because it runs after every admin_bar_menu callback, which puts the node last in the bar.
Known limits
- First-inclusion order, not render order.
get_included_files()lists each file once. A partial pulled in by six different modules appears a single time, at its first inclusion point. Exact ordering with repeat counts would need a logging wrapper around everyincludein the theme, which is a theme change and out of scope. - The footer is the boundary. Files included after
wp_footerare missed. The second snapshot hooks atPHP_INT_MAXso it runs last within the footer;shutdownwould be more complete but fires after</html>, where the panel cannot be rendered. - Full-page caching. If a staging environment serves a cached page, PHP never runs and the inline panel is whatever was cached. A stale panel is possible, and self-evidently stale.
- Non-post contexts. Archives, search and 404 have no post ID, so ACF location rules do not apply. The panel says so explicitly rather than rendering an empty section.
Test matrix
Verify against the route types that actually differ:
- A page with a custom template assigned in the editor.
- A custom post type single whose template nests partials two or more levels deep.
- A second custom post type single, to confirm the hierarchy candidate list changes with it.
- The blog archive.
- Search results.
- A 404.
- The gate on a real WP Engine staging install — the assumption most likely to be wrong.
Development
There is no build step for development — the source in includes/ and assets/ is what runs. dist/ is gitignored; it is a scratch copy of the installable files plus the zip, rebuilt at release time and attached to the GitHub Release:
./build.sh
gh release create "v$(./build.sh --version)" dist/what-renders-this.zip
build.sh refuses to build if the plugin header, WRT_INSPECTOR_VERSION and the Stable tag in readme.txt disagree, and asserts that the zip contains a single top-level what-renders-this/ directory. A flat zip unpacks loose into wp-content/plugins/ and WordPress will not recognise it as a plugin.
readme.txt is what wordpress.org reads; README.md is for GitHub. Both are shipped in the zip. Listing media — screenshots, banners, icons — lives in .wordpress-org/ and is never shipped; see the README in that directory for the required filenames.
License
GPL-2.0-or-later. See LICENSE.
Read the full README on GitHub →