WebEngine
0%
Case Studies

Case Study: AIM Group, an Outdoor Advertising Site with 80,000 Listings

One contractor ran the site for seven years on the argument that design does not affect sales. What followed: an admin panel, AI-assisted price list imports, and 435 listings vanishing silently.

Pavlo12 min read

The website of AIM Group — an outdoor advertising agency selling billboard space across Ukraine — had been maintained by a single contractor for seven years. His position was straightforward: a redesign is unnecessary, design does not affect sales. The client disagreed and handed the project to us on a recommendation. What follows is what we found inside, why the main work turned out to be data import rather than design, and how 435 advertising faces disappeared from the site one day without raising a single error.

80k

advertising faces in the database

229

commits in four months

15 min

of manual work per operator file

435

faces that vanished silently, then came back

Seven years with one contractor

We came in through a referral: the client had a website, and the contractor running it had stopped taking on work. The first thing we heard was that he was difficult to deal with, and that every suggestion to refresh the site met the same answer — design does not affect sales.

It is a common and convenient position. Convenient because it cannot be disproved without doing exactly the work being refused. We covered the signs that a redesign is genuinely warranted separately, in the piece on when a website is due for a rebuild.

The first task was a large one straight away: the redesign. The client already had the design prepared; our part was to analyse it, estimate the work and build it. The redesign shipped and the client was happy — and that is when the work that still continues began.

What came with the website

The project is Ruby on Rails 7.2 with PostgreSQL: roughly 800 files of inherited code that different hands had been adding to for seven years. Nobody asked for a rewrite, and there was no case for one — the site works, it brings in enquiries, and rewriting it would have meant months with nothing to show the business.

Something else was far worse. A company that lives off a database of advertising structures had no workable way to manage that database. So the first thing to appear in the repository was not a new page but an admin panel: authentication with lockout, a dashboard with live metrics, enquiries, and reference tables for cities, regions, districts, operators and structure types.

SEO done weekly, not once a year

The client has an in-house SEO specialist. He prepares specifications in Google Docs, we implement them on a separate dev server, he reviews the result there, and only then does anything reach production. It is a dull process, and that is precisely why it works: nothing gets tested for the first time on the live site.

The tasks over these months varied: title, description and H1 templates for city and structure-type pages, XML sitemap generation, canonical URLs, language versions, and 301 redirects for faces that disappear from the database when an operator’s file is refreshed. That last one is critical — without redirects, every database refresh would turn thousands of pages into 404s. We described the general approach to such moves in the SEO migration checklist.

The most interesting task looked trivial. Faces on type and city pages always appeared in the same order, and the SEO specialist saw a problem in it: Google would conclude the database never changes and crawl those pages less often. He also framed the outcome he wanted — "change the order from time to time, not on every page load" — and admitted he had no idea how to implement it.

Conversation with the SEO specialist about the ordering of listings and the daily seed solution
20 May. Personal data is masked in every screenshot

The answer was a daily seed: the order reshuffles once a day at midnight and stays stable within the day. Google sees new faces on the first page every day and indexes them, while pagination does not break — pages 1, 2 and 3 within a single day never overlap. Reshuffling on every request would have produced exactly that overlap, and duplicates in the index.

Fifteen minutes per file

AIM Group does not own billboards, it resells them. That means the site database is assembled from the price lists of dozens of operators, and every operator sends its own file: its own columns, its own order, its own naming. Together that is around eighty thousand advertising faces, and they change regularly.

The database was populated by a member of the client’s team. In a conversation about something else, we asked how exactly she prepares an operator file for upload, and asked her to spell it out in detail with examples. The answer mattered more than the question.

Chat screenshots in which the client’s team member walks through preparing an operator file by hand in a spreadsheet
1 July. "My algorithm" — copy the header, move columns one by one, fix the rest manually
  1. 1Copy the header row from a template into a new spreadsheet.
  2. 2Move the needed data across from the operator file, one column at a time.
  3. 3Strip the extra top rows, notes and empty blocks.
  4. 4Fill in what is missing entirely — the region and the city, for instance.
  5. 5Manually rewrite the structure type names to match the ones the site uses.

Asked how long that takes, she replied in a way worth reading twice: "It does not take that long, these are good files from the network operators, about 15 minutes." Fifteen minutes is the estimate for a *good* file.

Conversation asking how long preparing an operator file takes, and the reply of roughly fifteen minutes
1 July. "About 15 minutes" — and that is for a well-formatted file

Rather than leave that process with a person, we moved it into code. A raw operator file is uploaded, the system maps its columns onto database fields itself — a combination of heuristics and a language model — shows a preview of what it recognised, and lets anyone override any mapping by hand where it got things wrong.

The hardest part was not the format but the vocabulary. Operators call the same structure different things: in the local trade, "shield", "board" and "flag" all mean a billboard, a "screen" is a video board, a "lightbox" is a citylight. Previously each such name created a new type in the database, and the face simply could not be found through the site filters. We asked the client for a list of their trade slang and had it eleven minutes later.

Conversation requesting a list of the operators’ trade slang and the reply mapping those names to canonical types
"So it will understand that a shield means a billboard?" — yes, once it had this list

The day 435 listings vanished quietly

Three weeks after the import went live, a message arrived: the file uploads, there are no errors, the import runs, and then the operator’s listings are simply not there.

Conversation: the operator file uploads without errors, but after the import the operator’s listings are missing from the site
27 July, morning. "I hope this is the only one, otherwise oh dear"

This is the worst kind of failure. The file is accepted, the process completes, the report says "updated", nothing errors — and for exactly that reason nobody notices. Among eighty thousand faces, an empty operator only surfaces when someone deliberately opens it.

The cause was the recycle bin. Faces deleted earlier remained in the database as hidden records. The import matched codes against all records, deleted ones included, found them, honestly flagged them as an update and updated them — without restoring them from the bin. Technically everything worked. In practice the operator stayed empty.

Incident write-up in chat: the cause, the scale in listings, the fix and instructions for the client
That evening: cause, scale, fix and next steps. Operator names are masked

The scale was 435 faces across four operators, removed in one sweep three days earlier, plus several thousand affected before that. The fix took an evening and came down to a single rule: the current operator file is the source of truth. If a face is in the latest list, it belongs on the site, even if someone deleted it once. A re-import now restores such faces from the bin, and there is a test covering it so the same cause cannot repeat.

Enquiries nobody submitted

In July the client wrote that ordinary enquiries had been replaced by a stream of strange ones: on one day, all five people contacted had left nothing on the site. Managers called back and the recipients had no idea what it was about.

Conversation showing examples of nonsensical enquiries from the site and a discussion of where they come from
17 July. Phone numbers and avatars are masked

The temptation here is to promise a technical fix. We did not, because there was none. Form validation was already in place, and the submissions came from real people with Ukrainian numbers: they arrived, typed text and pressed the button. Bot protection does not work against humans, and a model-based filter would have removed about as much as deleting them by hand.

So the answer was this: nothing critical is happening, it costs the sales team some time, and the question of where the traffic comes from belongs to whoever runs the advertising, not to the developer. The client’s reply describes the contractor’s role in such moments better than we could.

Conversation with the client about the origin of the odd enquiries and having nobody else to ask
20 July. "I understand, but there is nobody else to ask"

The resolution came from the marketer rather than from code: drop the "request a call" form from the header, add email and company name to all the others, and make every field required. Fewer enquiries will arrive — and that is the entire point.

Conversation proposing stricter forms on the site in order to filter out unmotivated submissions
27 July. "It will reduce the number of submissions, but they will be better ones"

We estimated the work at 12–14 hours and delivered it within a day. That is a healthy ending for this kind of story: the developer says what cannot be done, the marketer proposes what can, and the answer turns out to live not in the code but in how much effort it takes to press a button.

26 gigabytes of photographs

In July the dev server went down for lack of space. There was one reason: photographs of advertising faces occupied 26 GB — over half the disk — and the number grew with every import. The same outcome on production was only a matter of time.

Message in the working chat about the disk filling up with listing photographs and a proposal to move to object storage
16 July. The problem, an option, and an honest price

We put three things in the shared chat: what the problem is, what we propose (moving files to object storage), and what it costs — the storage itself is pennies, but rewriting every place where photos are saved and served takes hours. And we said immediately that this decision is not ours: it is the client’s money, so it is her call.

How the engagement works

Three people work with us on the client side: the owner, who makes decisions and approves spending; the SEO specialist, who writes the specs and signs work off; and the team member who lives in the admin panel and keeps the database current. Tasks arrive both in the shared chat and directly, and the work is billed hourly.

Type of taskWhere it comes fromExample
SEO workA spec in Google Docs from the SEO specialistTitle and H1 templates, XML sitemaps, canonical URLs
Product changesDiscussion in the shared chatEnquiry forms, filters, the nearby-listings carousel
Database workQuestions from whoever is loading the dataOperator imports, structure types, photographs
IncidentsAnyone, at any hourAn empty operator after an import, a server going down
Four streams of work across half a year of maintenance

Small improvements we do not bill at all. The daily seed for listing order is one of those: the idea came from the SEO specialist, the implementation was short, and invoicing for it would have been odd. That is not charity, it is a way of not turning every conversation into a negotiation.

What the client said

Screenshot of the chat containing the client’s written testimonial about the engagement
28 July 2026. Published with the client’s permission
The task was to update and optimise the AIM Group website for SEO, to improve our positions in Google search and to make the site more effective. Pavlo carried out every recommendation from the SEO specialists quickly, thoroughly and professionally. Throughout the work he did not simply implement what he was given — he proposed better options, taking the specifics of our business into account. I particularly want to note his initiative, his creative approach and his attention to detail. I intend to continue working with Pavlo, because I am confident in his competence, his sense of responsibility and the quality of his work. I recommend him without reservation as a reliable and professional specialist.
Anna, owner of AIM Group

What this case study does not contain

There are no traffic, ranking or conversion figures here. Analytics and ad accounts sit with the client and her marketers, and we do not present other people’s data as our result. The technical side is verifiable: the repository, the commit history, the correspondence. Anything else will appear here only once there is something to back it with.

The engagement continues: tasks arrive weekly, the database keeps growing, and the next large piece of work is moving listing photographs into object storage.

Frequently asked questions about this project

Does an old website have to be rewritten on a modern stack?

No, and more often than not that is a mistake. In this project the Ruby on Rails site works, brings in enquiries and copes with a database of tens of thousands of records, so a rewrite would have meant months of work with no business result. Rewriting is justified when the technology blocks the changes you need or nobody can maintain it any more, not because the stack looks dated.

How long does importing supplier price lists take?

By hand, roughly fifteen minutes for one well-formatted file and considerably longer for a messy one. Automatic column mapping removes almost all of that: a person uploads the raw file, checks a preview of the recognised fields and confirms the import. Manual intervention is only needed where the system reports low confidence in a mapping.

Why consolidate different product names into a single reference list?

Because site filters work off the reference list, not off free text. If one supplier calls a structure a "shield", another a "board" and a third a "flag", then without consolidation into a canonical name the database ends up with three separate types and none of them can be found through the filters. To a visitor it looks like a missing product, even though the data is there.

What do you do about form spam when real people are sending it?

Technical measures are nearly useless here: captchas and validation stop bots, not people deliberately filling in a form. The workable route is to raise the effort threshold — add required fields such as email and company name, and remove the easiest one-tap forms. The number of submissions falls while the share of meaningful ones rises. In parallel it is worth auditing traffic sources, because the cause often sits in the ad settings.

Why can a data import silently update nothing?

Usually because of soft deletion: records are not removed from the database but flagged as hidden. If the import matches against all records, it finds the deleted ones, treats the operation as an update and finishes successfully, leaving the data invisible. No error is raised because formally nothing broke. The correct behaviour is to treat the current supplier file as the source of truth and restore such records.

In short

  • One contractor ran the site for seven years, refusing a redesign on the grounds that design does not affect sales; the project was handed to us on a referral.
  • The first thing built was not a new page but an admin panel — without it the company could not manage its own database.
  • The main work turned out to be import: around 80,000 faces from dozens of operators, each with its own file format and its own trade slang.
  • Preparing one file by hand took about 15 minutes; automatic column mapping and name consolidation removed that step.
  • Three weeks in, the import quietly lost 435 faces across four operators — diagnosed and fixed the same day, with a test to cover it.
  • Form spam was solved not with code but with required fields: fewer enquiries, more real ones.
  • Traffic and conversion figures are deliberately absent — the analytics belong to the client.
Share
  • case study
  • redesign
  • automation

Related reading