D
P
0
← All articles Baca dalam Bahasa Indonesia

WordPress & PHP in Production

Raw `<?php` Tags Printing on the Page? FSE Template Parts Never Execute PHP

· · 4 min read
Raw `<?php` Tags Printing on the Page? FSE Template Parts Never Execute PHP

I was building an FSE block theme for a small business site, and the task could not have been more ordinary: drop the brand logo into the header. Open parts/header.html, write an img tag, point src at the helper I have used a thousand times in classic themes. Refresh. Broken image.

The symptom: the page printed my code, not a URL

A broken image icon in the top left corner. My first instinct aimed at the usual suspects: a typo in the filename, a webp that never got uploaded, a case mismatch in a folder name. I checked the disk, the file was there. I opened the asset URL directly in the browser and it loaded perfectly.

Only then did I open View Source, where the answer had been waiting in its most literal possible form:

<img src="<?php echo esc_url(get_template_directory_uri()); ?>/assets/img/logo/logo-transparent_w480.webp" alt="Logo">

That is not rendered output. That is my code, shipped to the browser verbatim as text. The browser treated it as a strange relative path, failed to fetch it, and drew the broken icon. No PHP error, no warning, nothing in error_log. As far as WordPress was concerned nothing had gone wrong, because nothing had been executed.

The root cause: parts/ and templates/ are HTML, not PHP

This is the part that took me a moment to accept, because it cuts against years of classic theme reflexes.

In an FSE block theme, the files inside parts/ and templates/ carry a .html extension not as a naming convention someone thought looked tidy. The extension is telling the truth. Those files are static HTML. WordPress reads them as block markup, not as templates that need a PHP pass. There is no PHP parsing stage on that path at all, so anything shaped like <?php ... ?> is treated as ordinary text and printed straight into the output.

What makes it more confusing: inside the same theme, PHP does run elsewhere. Files under patterns/*.php execute normally. I had quietly assumed that if one part of the theme could run PHP then the rest could too, and that assumption is what sent me hunting for a typo.

The difference is in how each one is loaded. Patterns are registered through WordPress's pattern registration, which includes the PHP file and captures its output, so the code inside really does execute like any PHP file. Template parts and templates never take that route. Their contents are consumed as block content, exactly as written.

And this applies to all PHP, not just URL helpers. Put wp_nonce_field() in parts/header.html and you get the string wp_nonce_field() on screen. Conditionals are never evaluated either, they are just words. There is no list of exceptions to memorise, because there is no execution to begin with.

The fix

Three routes, depending on what you actually need.

First, and usually the right one: use an absolute web path. If all you need is the URL of an asset that lives inside the theme, you do not need PHP for it. You already know the theme path at authoring time:

<img src="/wp-content/themes/your-theme/assets/img/logo/logo-transparent_w480.webp" alt="Logo">

It feels less "correct" than calling a helper, but inside parts/ and templates/ it is the only form that works. The path is hardcoded relative to the site root, and for a single site install with a standard theme directory it is perfectly stable.

Second, move the markup into a pattern if you genuinely need PHP. In patterns/*.php, the helper that just failed works without complaint:

<img src="<?php echo esc_url( get_template_directory_uri() ); ?>/assets/img/logo/logo-transparent_w480.webp" alt="Logo">

So when a chunk of the header truly requires PHP logic, carve that chunk out into a pattern and pull the pattern in from the template part.

Third, if the path has to be dynamic, reach for a core block. For a logo specifically, core/site-logo is the safest answer. WordPress renders that block itself and resolves the URL against the actual install, so it stays correct even when WordPress lives in a subdirectory or the content directory has been moved off its default location. That is exactly where the hardcoded absolute path above starts to break. One caveat: the block pulls the logo from your site settings, not from the theme folder, so the file has to be uploaded to the media library first.

What I took away