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.
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.

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.

- 1Copy the header row from a template into a new spreadsheet.
- 2Move the needed data across from the operator file, one column at a time.
- 3Strip the extra top rows, notes and empty blocks.
- 4Fill in what is missing entirely — the region and the city, for instance.
- 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.

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.

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.

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.

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.

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.

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.

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.

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 task | Where it comes from | Example |
|---|---|---|
| SEO work | A spec in Google Docs from the SEO specialist | Title and H1 templates, XML sitemaps, canonical URLs |
| Product changes | Discussion in the shared chat | Enquiry forms, filters, the nearby-listings carousel |
| Database work | Questions from whoever is loading the data | Operator imports, structure types, photographs |
| Incidents | Anyone, at any hour | An empty operator after an import, a server going down |
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

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.
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.
Related reading
Case Study: Shift-Tracking Bot and Website for Profline Group
A complaint about missing timesheet entries turned out to be a shortage of physical passes, not a bug. Plus the conversation where we turned down a large CRM build.
7 Signs Your Website Needs a Redesign (and 3 It Doesn’t)
Redesigns often treat the wrong illness. Here is how to check whether the site is really the problem.
Website Maintenance: What It Includes and What It Costs a Year
A website is not a one-off purchase. Here is the yearly cost of ownership, line by line.