DNSK.WORK
What I own
Their design. My build, and everything under it.
DNSK.WORK is a UI/UX design agency for SaaS products, run out of London by Tanya Donska. She takes the design and the creative direction. I take the commercial side, and I build and run the site.
Laravel on Octane, Postgres, Vite, behind Cloudflare. Octane boots the application once and keeps it warm across requests, which is fast and occasionally surprising: the navigation disappeared after the first request because of how singletons behave in that environment. The fix was three lines. The investigation was not three lines.
The studio keeps a work log in public at dnsk.work/now, faults included, which is where everything below comes from. None of it is the version you would put in a case study. That is rather the point of writing it down.
Moving house without turning the lights off
Off a managed platform, onto one rented server, with nobody noticing.
Laravel Cloud was excellent and asked almost nothing of us, which turned out to be the problem: we were paying fleet prices to run a marketing site that boots once and serves the same cached page to everyone who visits. It lives on a single server now, rented by the month, behind Cloudflare, for a fraction of the old bill and with every layer under our own hands. Prudence or hubris, depending on how the server is behaving that particular day.
The method is to make the new home fully live in secret, prove it serves the real site down to the byte, and only then repoint the domain, so there is never a single second where dnsk.work leads nowhere. It went out without a flicker, which is the one and only compliment infrastructure work is ever paid.
The traps were where traps always wait. Cloudflare’s proxy watches a folder for new certificates but wakes only for a file that changes, never for one that simply appears, so the certificate we had written sat there, perfectly valid and completely ignored, until someone went back and prodded it. A token that could read the redirect rules but not write them, a fact assembled one denied permission at a time. A www-to-apex redirect answering 302 where it meant 301, telling Google the move was temporary when it was the most permanent thing we did all year.
Small, all of them, and every one the sort that works nine times and picks the tenth to make its point. The last step was taking a final database dump before shutting the old platform down, on the theory that deleting the last copy of anything is the fastest way to find out what was in it.
Live and invisible for two months
A flag nobody removed, and no analytics to notice with.
There was a setting in the codebase telling search engines not to index the site. It had been there since March, a sensible precaution while the thing was being built, and nobody took it out when the site went live. It came out in late May. The site had been up, finished and unfindable for roughly two months.
It had company. A CMS update rewrote 43 blog post URLs without asking, and Google had the originals indexed. We did not spot it, because there were no analytics. There were no analytics because nobody had turned them on. Switching analytics on and discovering the slug problem happened in the same sitting, which at least kept it efficient. All 43 slugs restored, locked so an update cannot touch them again, 301s in place.
The first proper crawl of the site afterwards returned 67 broken links still pointing at the old WordPress install, and turned up a blank page at /blog/page/10 serving a cheerful 200 that Google had duly indexed. Bots had worked their way through the pagination as far as page 47, looking for content that stopped existing somewhere around page 9.
None of this is difficult. All of it was invisible until someone went looking, and nobody had been looking, because the site worked when you typed the address in.
1,717 links pointing at nothing
A URL built by hand in four places, wrong in all of them.
Posts live at /blog/{slug}. Except news, which lives at /blog/news/{slug}, and guides, at /blog/guides/{slug}. The script that set up outreach campaigns built every URL as /blog/ plus the slug, so four campaigns spent five weeks pointing at 404s: 11.3% of every link the site has ever earned.
That one is not fixable. The dead URL is baked into roughly 1,700 articles already published on other people’s websites, and no amount of cleverness on this end reaches them.
The genuinely irritating part is that the same mistake had shipped twice more that month, once in the site footer and once in a reporting script, and been fixed both times without anyone noticing it was a pattern rather than three coincidences. There is one function that builds a post URL now, and a second that checks the page actually returns 200 before a campaign is allowed to exist at all.
The same wrong assumption had got into /llms.txt, the plain-text index the site offers to language models: every URL hand-built the same way, five of the first six returning 404. And robots.txt was blocking /blog/category/, filed as a leftover from WordPress. It is a live route, linked from the hero of every blog post. There was even a test asserting the block should be there, which is what happens when you write a test for the behaviour instead of the intent.
The unglamorous half
Ninety-one database queries a page, down to two.
Tag archive pages were running 91 queries per page load. They run 2. No visitor would ever have noticed the difference, and that is not really the point: the point is that nobody knew, and nothing would have told us.
The footer was firing a separate query for each service link, six queries to build a navigation list that should take one. Every blog post was lazy-loading its related tags in a loop. Both fixed with proper eager loading, and then the important bit: preventLazyLoading in the test suite, so the next one of these fails the build before it reaches production rather than being found a year later by someone reading a slow query log.
Cloudflare’s bot protection script was loading on every page on the site, including all the ones with no form on them to protect. It renders with the contact block now. A Google Fonts import was pulling in Inter on every page view, and Inter appeared nowhere in the actual stylesheet: two DNS lookups and a render-blocking request, for a typeface nobody was using. That one deserved a moment of reflection.
And the first case study image on the work page got fetchpriority high and lost its fade-in. An element that is the largest thing on the page, fading up from zero opacity, is technically elegant and exactly what PageSpeed flags as a problem. It was flagging it as a problem.
Who gets to the door
The error page nobody sees, and the alarm that would not stop.
A bot went looking for a way in and found a route marked GET only. It knocked with a POST, collected a 405 for its trouble, and in doing so exposed something more embarrassing than any break-in: there was no page for a 405. The handler that should have rendered a polite error threw an error of its own trying, then reported that failure to us as though it were the story. Every status code leaves through a real error page now. The bot moved on. We are left with a 405 page almost no human will ever see, which is exactly the sort of thing you build and hope stays useless.
Then there were the forty-five identical alarms about a single missing face. A testimonial avatar, deleted months earlier, living nowhere at all: not in the code, not the database, not one rendered page. Still the alerts came, one for every stray link and crawler that went on asking after it. A missing image is a 404, not a fire. It has stopped ringing the bell.
We also got blunt about who reaches the door in the first place. Whole regions the studio has never worked in and never will were behind the overwhelming share of the login-guessing and the scraping, and they meet the wall now before they meet us. On paper it looks heavy-handed. In the traffic logs it looked overdue. There is an uptime monitor pointed at the site as well, so the next time it falls over we hear it from a machine rather than from a client holding a screenshot.
The rest is the list nobody reads: HTTP security headers, input length limits, subresource integrity hashes on third-party scripts, Turnstile on the contact form, and a Laravel CVE patched the week it landed. None of it is interesting. All of it was overdue.
One line of configuration
Four commits to find it.
Polish and Ukrainian characters were arriving from the contact form as corrupted rubbish. Names, mostly, which is a poor first impression to make on someone who has just taken the trouble to write to you.
The fault was at the Postgres connection level and the fix was one line. The four commits were the journey to that line, and included a version that worked perfectly on my machine and broke in production, which is a journey everyone has taken and nobody enjoys.
That is the shape of most of this work. The write-up is a sentence. The finding is an afternoon.