WordPress File Headers Have Overstayed Their Welcome
In 2014, I commented on WordPress Trac ticket #24152 suggesting it was time to revisit the non-standard file headers specification. Scott reopened the ticket with a detailed case for replacing them with JSON.
That was eleven years ago. The ticket is still open, and we still use the same file headers in WordPress plugins and themes.
What are file headers?
Every WordPress theme must include a style.css file with metadata in CSS comments:
/*
Theme Name: My Theme
Theme URI: https://example.com
Description: A theme description
Author: Developer Name
Author URI: https://example.com
Version: 1.0.0
Requires at least: 6.0
Requires PHP: 8.2
License: GPL-2.0-or-later
Text Domain: my-theme
Tags: blog, custom-colors
*/
Plugins do the same thing in their main PHP file:
/*
Plugin Name: My Plugin
Plugin URI: https://example.com
Description: A plugin description
Author: Developer Name
Author URI: https://example.com
Version: 1.0.0
Requires at least: 6.0
Requires PHP: 8.2
License: GPL-2.0-or-later
Text Domain: my-plugin
*/
WordPress parses these with get_file_data(), a function that reads the first 8 KB of a file and runs regular expressions against it. This is the foundation that the entire plugin and theme system is built on.
It’s a custom, non-standard specification that exists nowhere else in the software world.
Why this is a problem
It’s slow
As Scott benchmarked in 2014, parsing file headers with regular expressions is nearly 10x slower than json_decode:
| Method | 10,000 iterations |
|---|---|
get_file_data | 1.98s |
json_decode | 218ms |
parse_ini_file | 482ms |
This matters when WordPress is scanning dozens of plugins and themes on every admin page load.
It gets worse. WordPress doesn’t know which PHP file contains a plugin’s header. When get_plugins() is called, it opens every .php file in each plugin directory, reads the first 8 KB, and runs regex against it until it finds a valid header. The results are cached, but the cache gets invalidated on plugin changes and doesn’t persist without an object cache. With a plugin.json, WordPress would know exactly where to look.
It’s error prone
The regex-based parsing has had bugs with whitespace handling. Here are all the variations that have caused problems, from #19854 and #15193:
Original Theme Name
Original Theme Name
Theme Name
Theme Name
Theme Name
Theme Name
* Theme Name
* Theme Name
*Theme Name
Theme Name
/* Theme Name
/* Theme Name
/*Theme Name
* Theme Name
/*Theme Name
Theme Name
Theme Name
Theme Name
JSON doesn’t have this problem. A key is a key.
It’s illogical
Why is a theme’s name, version, and license in the comments of a stylesheet? Why is a plugin’s metadata in comments in a PHP file?
Theme metadata has nothing to do with CSS. Plugin metadata has nothing to do with PHP execution. Metadata belongs in a metadata file. This is an arbitrary convention that WordPress invented. No other platform does this.
And the metadata isn’t even in one place. For plugins, it’s split between the PHP file header and readme.txt. Two different files, two different formats, for the same package. Themes have the same problem with style.css and readme.txt. A single JSON file replaces all of it.
It’s non-standard
The File Header specification is a list of 13 different things for plugins and 14 for themes. There’s no formal spec, no schema, no validation tools. It’s parsed by reading raw file bytes and running regex against comment blocks.
JSON has a formal specification, built-in parsers in every language, schema validation, and tooling in every editor.
The proposal
For themes: use theme.json
WordPress already requires theme.json for block themes. It’s already a JSON file that WordPress already parses. But theme metadata (name, version, author) is still stuck in style.css comments.
Think about that for a second. WordPress parses JSON for theme colors, fonts, spacing, layout, templates, and patterns. But for the theme’s own name and version? CSS comments.
Add a metadata property to theme.json:
{
"$schema": "https://schemas.wp.org/trunk/theme.json",
"version": 3,
"metadata": {
"name": "My Theme",
"uri": "https://example.com",
"description": "A theme description",
"author": "Developer Name",
"authorUri": "https://example.com",
"version": "1.0.0",
"license": "GPL-2.0-or-later",
"textDomain": "my-theme",
"tags": ["blog", "custom-colors"],
"requires": {
"wordpress": "6.0",
"php": "8.2"
}
},
"settings": {
}
}
The file already exists. The parsing infrastructure already exists. Many classic themes are already hybrid themes with a theme.json file. They’d get this for free.
For plugins: introduce plugin.json
Plugins don’t have an equivalent to theme.json, so they need a new file:
{
"$schema": "https://schemas.wp.org/trunk/plugin.json",
"name": "My Plugin",
"uri": "https://example.com",
"description": "A plugin description",
"author": "Developer Name",
"authorUri": "https://example.com",
"version": "1.0.0",
"license": "GPL-2.0-or-later",
"textDomain": "my-plugin",
"requires": {
"wordpress": "6.0",
"php": "8.2",
"plugins": ["woocommerce", "jetpack"]
}
}
Notice the requires.plugins field. WordPress currently handles plugin dependencies with a comma-separated string in a comment header (Requires Plugins: woocommerce, jetpack). A JSON array is the obvious way to represent a list of things.
Why not composer.json or package.json?
When this was discussed in 2014, TJNowell suggested using composer.json which is a reasonable idea. Composer is the standard dependency manager for PHP, and composer.json already has provisions for defining WordPress themes and plugins.
But WordPress doesn’t support Composer. There’s no native Composer integration in core, and no indication that’s changing. We’ve been using Composer with WordPress since 2013 through Bedrock, but that’s a project-level tool. It doesn’t solve the problem for individual themes and plugins that need to identify themselves to WordPress.
package.json has the same problem in the other direction. Not every theme or plugin uses Node. Requiring a Node package manifest for a PHP project that might not have any JavaScript build step doesn’t make sense.
The solution needs to be WordPress-native. theme.json already is. plugin.json would follow the same pattern.
Backwards compatibility
This is fully backwards compatible:
- If
metadataexists intheme.json, use it. Otherwise, fall back tostyle.cssheaders. - If
plugin.jsonexists, use it. Otherwise, fall back to PHP file headers.
wp_get_theme() and get_plugin_data() handle the abstraction. No existing theme or plugin breaks. Developers migrate on their own timeline.
The file headers don’t even need to be formally deprecated right away. Give developers a better option and let adoption happen naturally, the same way theme.json itself was adopted for block theme settings.
Thirteen years and counting
WordPress Trac #24152 was opened in 2013. It was closed as “wontfix” because file headers are simple and JSON introduces unnecessary complexity. We disagreed then. Scott made the case in detail. Others have continued to argue for it in the years since.
Then WordPress introduced theme.json. A JSON configuration file that defines theme settings and styles. The file is right there. Just put the metadata in it.
Every argument against this has been answered. Performance is better. The format is standardized. The file already exists for themes. Backwards compatibility is trivial.
WordPress already chose JSON. It just forgot to finish the job.