A Technical SEO Audit for Yoga Retreat Booking Pages
A February 2026 technical SEO case study showing how Search Console data, a custom opportunity analyzer, Twig fixes, PHP metadata fallbacks, and Traefik redirects cleaned up yoga retreat booking pages.
A yoga retreat booking page can look fine in the CMS and still be weak in search.
That was the useful lesson from this audit. The site had real demand around yoga retreat, yoga workshop, weekend retreat, and beginner-friendly workshop searches. It also had enough visibility to prove that Google understood the broad topic. Between February and May 2026, the Search Console export showed 24,765 impressions and 1,731 clicks. The average CTR was about 7%, which looked acceptable until we filtered by URL and query.
Several pages were sitting around position 6 or 7 with thousands of impressions and almost no clicks. One flagship event page had roughly 2,000 impressions and a CTR near 0.2%. That was not a ranking problem in the usual sense. It was a relevance and rendering problem.
Step 1: Build the query-page dataset
We started in Google Search Console, not in the CMS. The export came from Performance -> Search results, with queries, pages, clicks, impressions, CTR, and average position enabled. We pulled the top 250 rows, then joined each query to its landing page so we could stop talking about keywords in the abstract.
The working dataset had one row per query-page pair:
query,page,clicks,impressions,ctr,position
"yoga retreat weekend",/en/events/retreat-intro,4,940,0.43,6.8
"yoga workshop beginners",/en/workshops/intro,3,710,0.42,7.1That shape matters. A query by itself does not tell you what to fix. A page by itself hides intent differences. The query-page pair tells you where a specific search intent is meeting a specific URL.
Step 2: Score the leaks
We used a small custom analyzer, SeoOpportunityAnalyzer, to group the export into four buckets:
- Low CTR pages in positions 1 to 15 with at least 50 impressions and CTR below 3%.
- Striking-distance queries in positions 5 to 20.
- Low-click URLs where impressions existed but the page earned almost no traffic.
- Locale cannibalisation where
/en/,/de/, and/es/variants competed for the same query.
The scoring was deliberately simple. We wanted prioritisation, not a fragile model:
low_ctr_score = min(100, impressions / 10 + max(0, 10 - position) * 4)
striking_distance_score = min(100, impressions / 12 + (21 - position) * 3)That produced a ranked list of 145 opportunities. The top rows were not always the pages with the most impressions. They were the pages where a small technical or snippet fix had the best chance of changing search behavior.
Step 3: Inspect the rendered HTML
Before changing titles, we checked what the browser and crawler actually received. For each priority URL, we compared four views:
- The CMS fields the editor had configured.
- The rendered page source.
- The canonical URL and redirects from
curl -I. - The Search Console URL Inspection result.
This is where the technical debt showed up. The CMS was not the source of truth. The rendered HTML was.
The first bug was in PHP. Editors had entered custom meta titles per locale, but the entity layer did not expose a reliable accessor with translation fallback. Some yoga retreat and yoga workshop pages fell back to generic defaults.
The fix was a proxy method that returned the translated title when present and the default title otherwise:
public function getMetaTitle(): ?string
{
$title = $this->translatedMetaTitle ?? null;
if ($title !== null && $title !== '') {
return $title;
}
return $this->defaultMetaTitle;
}The second bug was in Twig. Meta descriptions containing ampersands were rendering as &. The filter was escaping an already-normalized value, so the output looked broken in search snippets and social previews.
We fixed the filter declaration and normalized stored values before rendering:
new TwigFilter('meta_description', [$this, 'metaDescription'], [
'is_safe' => ['html'],
]);$value = html_entity_decode($value, ENT_QUOTES | ENT_HTML5, 'UTF-8');The third bug was whitespace control. A Twig block used aggressive trimming, so titles rendered like Yoga Retreat- Site Name. The change was small but visible:
{%- endblock %}That preserved the trailing space around the separator without loosening the whole template.
The fourth bug was routing. The non-www domain worked, but the www hostname returned an SSL error. That is bad for crawl consistency and worse for trust. We added the www host to Traefik and redirected it to the canonical domain with a 301:
http:
routers:
site-www:
rule: "Host(`www.example.com`)"
middlewares:
- redirect-to-canonicalStep 4: Validate before rewriting content
After each fix, we checked the rendered output again. The validation loop was intentionally mechanical:
- Run the affected URL through
curl -Iand confirm the canonical redirect. - Fetch the HTML and confirm the title, meta description, canonical tag, and hreflang links.
- Re-run the analyzer export after Search Console refreshed.
- Annotate the changed URL, date, and expected query cluster.
- Compare the next 7-day and 28-day Search Console windows against the baseline.
That last step is what keeps technical SEO honest. The goal is not to ship a code change and feel better. The goal is to see whether the pages that were already visible become more clickable and easier to interpret.
For this yoga retreat booking site, the technical cleanup did not replace content strategy. It made content strategy worth doing. Once the HTML was clean, the next round could focus on the search result itself: titles, descriptions, schema, booking attribution, and the measurement loop.
Sources and Attribution
- Internal SEO/AEO audit draft in
popolipopo-workspace/topics/seo-aeo-audit-case-study/05-draft/working-draft.md. - Internal data notes in
popolipopo-workspace/topics/seo-aeo-audit-case-study/03-data/data-notes.md.
Method Note
- This article uses an anonymized yoga retreat and yoga workshop booking site as the public example. The implementation details are preserved from the internal audit workflow, but client-specific category language has been removed.