Case studies

Thirteen problems, and how each one was traced to its cause.

Every number here comes from live client work and was checked before and after. Where the useful finding was that a client should spend less, that is included too, because those are usually the ones worth reading.

Verified on live sitesBefore and after evidenceEvery number traceable to a case

Recently shipped

The latest work off the bench.

A running list of what has gone out recently, most recent first. The detailed case studies are below; this is the short version of what has been keeping us busy.

  1. Technical SEO 1,367 URLs Schema claims audited
    • 246 "from" prices now sent as ranges
    • 209 unverified availability claims withdrawn
    • A closed attraction kept, marked cancelled
  2. Performance 1,438ms to 45ms Front page query rebuilt
    • Cold front page from 5,968ms to 1,994ms
    • Self-join replaced by one indexed query
    • All ten sampled pages byte identical
  3. Technical SEO 65 URLs Sitemap and noindex aligned
    • Submitted for indexing and refused at once
    • One shared rule now decides both
    • Counts only what an archive page shows
  4. Custom build 968 listings Near me search
    • 45 London areas, six filters
    • Server rendered for crawlers
    • No location data collected
  5. Custom build Live Popularity rankings
    • Most visited and most voted modes
    • Counts server rendered in the HTML
    • One vote per profile per day
  6. Build and SEO 47 pages Service taxonomy
    • One template, real roster data
    • Each page on its own URL
    • Every page linked from the homepage
  7. Build and SEO Zero dev time Agency profile system
    • Coded from scratch
    • New profiles added without a developer
    • Retained for the search work since
  8. Product build 236ms load Booking analytics product
    • Own accounts, hosting and ledger
    • Period and revenue analytics
    • Installable as an app
  9. Custom build Mobile first Faceted directory search
    • Search built into the navigation
    • Live categories and an available now filter
    • Reviews and booking in the same menu
  10. Security 21,626 hits to 0 Login brute force blocked
    • Stopped at the edge in fifteen minutes
    • Server load was only 7.5%
    • No server upgrade needed
  11. Infrastructure 24 hrs to instant Cache invalidation fixed
    • Four separate gaps, all fixed
    • Purge no longer clears the image cache
  12. SEO architecture 25 links to 5 Internal linking rebuilt
    • DR 64 domain ranking for nothing
    • Competitors only DR 16 to 32
    • Rebuilt as a hub of chosen links
  13. Structured data One valid entity Entity markup consolidated
    • Years of plugin overlap merged
    • Search Console entity issue closed
01
Build and SEO

A profile system a London agency runs itself

The problem

A London agency needed a proper profile and listing system. A page builder wrestled into shape was not going to do it. The roster changes constantly, and every earlier attempt meant a developer touching the layout each time a profile was added, which was slow, expensive and a reliable way to break the SEO on the pages that already ranked.

The diagnosis

The site is its roster, so the profile template is the product. A generic post type gives you none of the structure an agency uses, and a marketplace theme cannot be moderated, verified or made to load. What was needed was a bespoke template and data model built for the way the agency works, with adding a profile designed as an everyday task for staff.

The fix

A hand coded profile and listing system: one consistent template, gallery and media handling built for image heavy pages, and an internal linking and taxonomy structure so a new profile slots in without touching the layout or diluting the pages around it. Each profile is server rendered and crawlable, not assembled in the browser after the crawler has left.

The result

The agency adds and edits profiles itself, with no developer in the loop, and the layout and the SEO hold every time. The pages stay fast as the roster grows because the template outputs only what the page needs, and the search work continues on top of a build that does not fight it. The client is not named, as is usual in this sector.

02
Product build, SaaS

A booking tool that grew into a product

The problem

The operator was running bookings and client history across a notes app and a spreadsheet, on a phone, between jobs. Nothing was aggregated, so a question as basic as which clients to keep had no answer without adding it all up by hand.

The diagnosis

The work did not belong on the website. Putting it there would have meant a busy booking day competing with public traffic, and it would have tied private data to a public CMS. What was needed was a separate application with its own accounts and its own hosting, built for a phone first because that is the only device it gets used on.

The fix

A standalone app with sign up, sign in and password recovery, a bookings ledger and private notes. Then the part that made it useful every day: analytics with period comparison, revenue breakdowns by booking type, and repeat client ranking. Built as an installable progressive web app with a service worker, so it sits on the home screen and survives a patchy connection.

The result

It loads in 236ms from four requests, which is the difference between a tool someone uses and a tool someone avoids. Deployed separately from any public site, so neither can take the other down, and the questions that used to need a spreadsheet are now answered on the dashboard.

03
Security, edge mitigation

A login page taking half the traffic on the site

The problem

The site felt slow and the obvious next step, the one that had already been suggested, was to pay for a bigger server. Before agreeing to that, we looked at what the traffic was made of.

The diagnosis

Live server log analysis showed /wp-login.php absorbing about 46% of every request reaching the origin. That is a brute force attack running at 21,626 hits a day. But the second finding mattered just as much: the server was sitting at 7.5% CPU. The attack was noisy, not heavy. It was not what was making the site feel slow.

The fix

We put a Cloudflare Managed Challenge in front of the login path with a skip rule allowlisting the admin IP, so the attack got stopped at the edge without locking out the person who needs to log in.

The result

Hostile traffic dropped to zero within fifteen minutes, verified in the logs, with legitimate admin access confirmed working. Just as valuable, the client did not spend money upgrading a server that was running at 7.5% load. A misdiagnosis there would have cost real money every month and fixed nothing.

04
Infrastructure, diagnosis

Four separate root causes behind one cache failure

The problem

Live availability changes were not showing up on the site. Some pages were up to a full day out of date. Everyone assumed it was one bug.

The diagnosis

First we had to establish whether the problem was the file or the cache, which meant testing the origin directly with a cache buster. That confirmed the origin was correct and the cache was serving stale content. The deployed purge turned out to be a targeted list of URLs, not a full purge, and there were four independent gaps in it: URLs simply missing from the list, a meta deletion path with no hook attached, a second URL form for location terms that the purge never accounted for, and the subtle one, curated pages that query content instead of being taxonomy archives, so no term based loop could ever reach them. A fifth finding came out of the same read. An earlier implementation had been calling purge everything on every content change, which also flushed the one year image cache, so the AVIF files were perpetually cold. Seven of eight profiles sampled were returning MISS. Nobody had connected slow images to the purge logic, because they looked like two different problems.

The fix

We fixed all four, not just the first one we found. That last cause is the kind of thing that survives three rounds of debugging, because the code looks correct and the loop it runs in simply never covers those pages. We then rewrote the purge itself as a targeted, HTML only invalidation that never touches images: rate limited to one purge per 60 seconds, chunked at 30 URLs per API call, and run non-blocking on shutdown so an editor pressing publish never waits on a CDN. Triggers were wired to every way state can change, including meta deletion, status transitions and comment events, and location terms are purged in both URL forms because one is canonical at the root while the other 301s to it. Image operations auto-purge too: optimisation plugins that write metadata directly are hooked through their own action, attachment IDs are resolved to every generated size, and purging a source jpg also evicts the AVIF derived from it.

The result

Six pages that had been stale for 14 to 24 hours matched the origin immediately after deployment, checked page by page, with edge ages of 43 to 47 seconds. The image cache stopped being flushed on every edit, so profile images now serve as AVIF with a cache HIT and stay warm for around 19 hours and beyond.

05
SEO architecture, content

A DR 64 domain ranking for nothing on a page it should have owned

The problem

A strong domain was ranking for zero keywords on a target page. The assumption, as it almost always is, was that the page needed more links.

The diagnosis

Before agreeing to a link campaign, we checked who was ranking. The page one competitors were sitting at DR 16 to 32, well below the client. That proved authority was not the constraint, so link building would have been expensive and useless. The real causes were on the page: a canonical pointing at a redirect, two pages competing for the same intent, machine spun copy, and a 25 link block spreading equity across everything indiscriminately.

The fix

Rebuilt as a hub with spokes, splitting commercial and informational intent apart so the pages stopped competing. Corrected the canonical, rewrote the spun copy, and cut the link block to five chosen internal links.

The result

The structural causes were removed and the page became eligible to rank on its merits. We also left behind a linking governance rule with a live ledger, so the tidy structure does not decay back into a link dump once we are gone.

06
Structured data

A schema graph fighting itself

The problem

Search Console was reporting an entity issue and the structured data was a mess of overlapping claims made by different plugins over several years.

The diagnosis

There was a duplicate organisation entity, per item LocalBusiness markup that should never have been there, self serving review ratings, and invalid FAQPage markup, all live at the same time. Each individually looked defensible. Together they described a business that did not make sense.

The fix

Removed the duplicate organisation entity, disabled the per item LocalBusiness markup and the review ratings, promoted the remaining node to a single correct type, and stripped the invalid FAQPage markup. We kept the breadcrumbs and the #organization @id after checking the collision risk, instead of removing them out of caution.

The result

One valid entity, validated through Google Rich Results, and the Search Console issue closed.

07
Product, bespoke tooling

Two customer facing tools built from scratch

The problem

The site needed two features that no plugin covered properly, and both had to stay fully crawlable because they were targeting real search demand.

The diagnosis

The first was a location search targeting a term with 33k searches a month. The obvious implementation, a JavaScript widget that fetches results on the client, would have been invisible to crawlers and worthless for the actual goal.

The fix

We built a geolocation search using the Geolocation API, Haversine distance sorting, radius filtering and reverse geocoded location labels, server rendered so it stays crawlable. Its geocoding layer maintains itself: a static map first, then a term meta cache, then a one off Nominatim lookup, which means new locations work with no code change at all. The second tool was a three way comparison built on real relationship data derived from post meta and made bidirectional, with SVG connector graphics, location aware combined pricing, localStorage persistence and undo and redo. DOM safe rendering throughout.

The result

Both are live, both are crawlable, and the location tool has needed no developer involvement to add locations since it shipped.

08
Custom build

A popularity ranking a crawler can read

The problem

A large London directory needed to answer the question every visitor arrives with: who is popular right now? Editorial curation answers it badly: somebody picks favourites, the list is stale within a week, and it shows the visitor what the operator wants to promote.

The diagnosis

Two hard parts, and they pull against each other. The ranking has to come from what visitors do, which means live counters. But the module also has to be readable by a crawler, and the obvious build fetches the counts in the browser, which leaves a search engine looking at an empty box on the one page most likely to earn a listing. The second trap is caching: live numbers on a cached page either serve stale counts or defeat the cache and make the page slow.

The fix

Rankings are server rendered with the counts in the HTML, so the page is complete before any JavaScript runs. Two modes sit behind one toggle, most visited by profile views and most voted by a separate count, so popular-by-traffic and popular-by-preference stay separate instead of being averaged into one meaningless score. Voting is rate limited to one vote per profile per day, which is what stops a ranking becoming a contest of who has the most patient friend. It ships as its own paginated page at twenty per page and as a condensed module on the homepage.

The result

The rankings update from real behaviour with nobody curating anything by hand, and the page is fully crawlable. The counts shown are the real ones, small ones included. A ranking that inflates its numbers tells the visitor nothing.

09
Build and SEO

Forty seven service pages from one template

The problem

The roster was browsable by name and by location, which are the two axes every directory has, and neither matches how much of the demand is phrased. People search for a service, and there was no page that answered any of those searches.

The diagnosis

Service is a real search axis with real volume behind it, so building the pages made sense. But 47 generated pages is programmatic SEO, and it carries the failure mode that always comes with it: thin, near identical pages competing with each other, eating crawl and diluting the listings that already worked. The deciding question is whether each page has real listings behind it, because a service page with nothing on it is a doorway.

The fix

One template driving 47 service pages, each on its own clean URL, populated from real roster data, with no spun copy. Surfaced through a browse gallery on the homepage so every one of them carries an internal link from the strongest page on the site, which is the step that usually gets skipped: a taxonomy page reachable only through the sitemap has no internal authority and reads as peripheral. The nationality and location axes are built the same way, so each of the three ways people narrow a search has a real page behind it.

The result

Forty seven indexable landing pages matching how the demand is phrased, all reachable from the homepage. The client knows the catch: these pages only earn their place while there are real listings behind them, and a service page that empties out should be pruned before it thins the set.

10
Custom build

A near me search that never learns where you are

The problem

A London listings publisher wanted to rank for "near me". People type that once they have decided to go out and just want to know what is close by, so the page had to give them an answer as soon as it loaded.

The diagnosis

There were two ways to get it wrong. A widget that loads its results after the page does leaves Google looking at an empty box, on the one page meant to rank. Sending the visitor's coordinates to a server fixes that, but then the site is collecting location data it has to justify, store and protect. The publisher did not want that job, and the distance maths is light enough to run in any browser.

The fix

Every listing is server rendered, so the page is complete before any script runs. The visitor's position is worked out in their own browser and never sent anywhere. The Geolocation API gives a reading, the sort runs locally against the centre of each area, and that is the end of it. Six filters narrow it down by distance, day, time, category, area and budget. The page also says what the distances are: straight-line miles to the middle of an area, not walking directions.

The result

Google can read every result, and the publisher holds no location data at all because none is ever collected. Free events are marked, each listing links to its venue so people can check before they travel, and nothing on the page is sponsored or affiliate. We still look after the site and handle its admin.

11
Performance

Four queries, and one of them was the entire page

The problem

The front page of a listings site took 5,968ms to build on a cold cache. The standard advice is to cut the number of database queries. Here that would have been the wrong fix.

The diagnosis

We profiled it on the server. One function took 3,389ms of the 3,465ms, and it made only four queries. The cost was in what one of them asked for: every event overlapping a date range. Written as a standard metadata query, that turns into a four-way self-join on a column with no index. That one query took 1,438ms. Loading the 362 rows it returned took 13ms. Our first guess was wrong. We expected the gain to come from moving repeated field reads out of the sort, and that saved only 21%, because those reads were already cached.

The fix

We replaced it with a date index. The start and end of every published event sit in one cached list, read with a single query on an indexed column. The overlap is worked out in code, and only then are the matching events loaded by ID.

The result

The query dropped from 1,438ms to 45ms, and the front page from 5,968ms to 1,994ms. We compared ten pages before and after, byte for byte, and they were identical. Faster, and nothing a visitor or Google sees had changed.

12
Technical SEO

Sixty five URLs asking Google for two opposite things

The problem

Sixty-five URLs were telling Google two opposite things at once. Each had a noindex tag on the page and an entry in the sitemap asking for it to be indexed.

The diagnosis

Neither part was broken on its own. The noindex rule lived in a filter that only runs while a page is being rendered. The sitemap is built from the database and never renders a page, so it never saw the rule. Every page thin enough to be held back was still listed, and neither system could tell.

The fix

We moved the threshold into one function that the noindex rule and the sitemap both call, so the two can no longer disagree. It counts what an archive page will show, using the same query as the page itself, so entries nobody will see are left out. The count is cached for a day and cleared overnight, which means an archive that picks up a new listing is back in the sitemap the next morning.

The result

The sitemap and the pages now agree. Page URLs went from 121 to 120, venues from 148 to 87 and areas from 45 to 43. There was a second option: drop the noindex and let those pages be indexed. We followed the rule the theme already had, but that choice belongs to the owner, and reversing it means changing one number.

13
Technical SEO

Everything the site was asserting that nobody had checked

The problem

Structured data is a claim you make to search engines, and this site was making hundreds it had never checked: prices, ticket availability, dates, and at least one attraction that had closed for good. A wrong claim does more harm than a missing one, because it is the one Google repeats.

The diagnosis

The errors came from three places. 246 events published a "from" price as if it were the price. A ticket field defaulted to "available" and could not be left blank, so 336 listings told Google tickets were in stock. For 209 of them nobody had ever set that value; it was the field's default, filled in when the page loaded. Then we crawled all 1,367 URLs in the sitemap and found a mistake no schema check would catch. A fireworks display was listed on the wrong night. The council runs two displays, in two parks, on two dates, and the listing had one park with the other one's date.

The fix

Price ranges now go out with a low and a high figure. The ticket field now defaults to nothing, which fixed those 209 on its own, and the rest were set to a new "not checked" state that shows "See venue" and makes no claim about availability. "Sold out" and "selling fast" only appear where someone has looked. An attraction that closes for good now gets its own state: cancelled in the schema, a closed badge on the listing, a link to the venue in place of the booking button, and a filter that keeps it out of anything listing what is on.

The result

Everything the site claims now, someone has checked. The closed attraction still has its page. It ran for sixteen years, people will keep searching for it, and a page saying it has closed helps them more than a 404. Taking it off the listings had a knock-on effect: a guide that named four venues and called one of them "the cheapest of the four" was now counting wrong, so we fixed that sentence too.

Engagements

Who this work was for.

Flagship, DR 64

High traffic lifestyle directory

  • Whole technical side owned, around 151 hours logged
  • Roughly 9,676 images rebuilt into an AVIF pipeline
  • Field LCP 372ms on the gallery, 1032ms on the homepage
  • CLS 0.00 across home, gallery and profile templates
Retained

Lamplit London

  • Custom design coded by hand, no page builder
  • Sponsored placements marked rel="sponsored"
  • Internal linking built to a planned architecture
  • Retained since, so the design keeps pace with content

lamplitlondon.com

Build

London Guide UK

  • From an empty WordPress install to a live directory
  • Plugin stack chosen one plugin at a time
  • Login and admin hardened against unwanted traffic
  • Editors add listings without breaking layout or SEO

londonguideuk.co.uk

Ongoing

SEO and competitor research

Continuous competitor, keyword, backlink and SERP analysis, feeding a prioritised roadmap. Nothing ends up as a report that sits unread.

Book me

Got something that looks like one of these?

Send the URL and the symptom. If it is a problem we have seen before we will tell you straight away, and if it is not, we will tell you that too.

Reply within one working day No obligation Your details stay with us

Takes about 60 seconds. No newsletter and no CRM sequence. Your details are used to reply to this enquiry and nothing else. See the privacy notice.