29 Aug 2026 · 3 min read
Case study: the search page overwrote its own title
A no-results handler ran on every search, so pages with results still showed the 'no results' title, meta, and breadcrumb.
Field notes — a real production bug, project name withheld. Symptom → investigation → fix → lesson.
The symptom
On the search results page, the browser tab title, the meta[name="title"], the <h1>, and the breadcrumb sometimes showed the no-results wording — "no results for …" — even when the page clearly listed results. It was inconsistent: a query with hits could still render as if it had none, which hurt both SEO and the customer's sense of whether their search worked.
Investigation
The template had two branches: a "no results" block and a "results" block. The no-results branch carried a <script> that rewrote the page's title and headings; the results branch did not. But the script was not nested inside the no-results branch — it sat after both, wrapped only in a DOM-ready listener with no condition guarding whether results actually existed:
<script>
document.addEventListener('DOMContentLoaded', function () {
const noResultsText = <?= json_encode((string)$noResultsText) ?>;
const metaTitle = document.querySelector('meta[name="title"]');
const title = document.querySelector('title');
const pageTitle = document.querySelector('h1.page-title span');
const breadcrumb = document.querySelector('.breadcrumbs li.search span:last-child');
if (metaTitle) metaTitle.setAttribute('content', noResultsText);
if (title) title.textContent = noResultsText;
if (pageTitle) pageTitle.textContent = noResultsText;
if (breadcrumb) breadcrumb.textContent = noResultsText;
});
</script>
The variable $noResultsText was always present (it just happened to be empty when there were results), so json_encode always produced a string and the listener always ran. On every search page — hits or not — the script overwrote the title with that string. When the string was empty, the title went blank; when it carried the wording, the page falsely claimed no results.
Root cause
A DOM-mutation script with no guard on the actual condition. The markup branched correctly, but the JavaScript ignored the branch and executed unconditionally on DOMContentLoaded. The bug is a classic one: the server knew whether there were results, and even computed $noResultsText, but never used that fact to decide whether to emit the script at all.
The fix
Gate the entire script behind the server-side condition. If there are no results, emit the script (and register it for CSP); otherwise, emit nothing:
<?php if ($noResultsText): ?>
<script>
document.addEventListener('DOMContentLoaded', function () {
const noResultsText = <?= json_encode((string)$noResultsText) ?>;
const metaTitle = document.querySelector('meta[name="title"]');
const title = document.querySelector('title');
const pageTitle = document.querySelector('h1.page-title span');
const breadcrumb = document.querySelector('.breadcrumbs li.search span:last-child');
if (metaTitle) metaTitle.setAttribute('content', noResultsText);
if (title) title.textContent = noResultsText;
if (pageTitle) pageTitle.textContent = noResultsText;
if (breadcrumb) breadcrumb.textContent = noResultsText;
});
</script>
<?php isset($hyvaCsp) && $hyvaCsp->registerInlineScript() ?>
<?php endif; ?>
Now the rewriter exists only on a genuine no-results page, and pages with hits keep their real title, meta, and heading.
Lesson
Never run a DOM-mutation script unconditionally when the server already knows the state it depends on. If a piece of JavaScript should only act in one branch, emit it only in that branch — don't rely on a runtime check that the browser can't know any better than the template can. The cheapest guard is the one you write in PHP: <?php if ($condition): ?> … `. It also keeps CSP registration tied to actual script emission, so you're not whitelisting inline scripts on pages that never use them.