/now

Updated August 2026 · London

Right now: client work, writing, and an eight-week wait to find out whether a year of link building did anything at all. The honest answer today is that we cannot tell, which is its own kind of finding. More on that below.

I'm Tanya. I run DNSK WORK, a small design studio in London. We work with startups and product teams on UX, interface design, and the kind of web presence that's supposed to do actual work rather than just exist. This page is where I keep track of what's happening with the studio itself – the version of the story that doesn't fit in a case study.


Mid-August 2026 - Learning to say no

A bot went looking for a way in and found a door marked GET only. It knocked with a POST, collected a 405 for the effort, and in the process exposed something more embarrassing than any break-in: we had no page for a 405. The handler that should have shown a polite error threw an error of its own trying to render one, then reported that failure to us as though it were the story. Every status code now leaves through a real error page. The bot has 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 over a single missing face. A testimonial avatar, deleted months ago, now living nowhere at all - not in the code, not the database, not one rendered page - and 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.

And we got blunt about who reaches the door in the first place. Whole regions we have never worked in and never will were behind the overwhelming share of the login-guessing and the scraping, so now they meet the wall before they ever meet us. On paper it looks heavy-handed. In the traffic logs it looked overdue. We also, at long last, aimed an uptime monitor at the site, so the next time it falls over we hear it from a machine rather than from a client holding a screenshot.

Early August 2026 - Moving house

We moved the entire site off Laravel Cloud without it going dark for a second, and the whole point was that nobody should notice. The platform 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 now lives on a single server we rent 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 trick to moving house without downtime 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. Cloudflare seals the connection at its own edge and carries a strict certificate the rest of the way to us. 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 utterly ignored, until we went back and prodded it. A token that could read the redirect rules but not write them, a fact we 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 had done all year. Small, all of them, and every one the sort that works nine times and picks the tenth to make its point.

Then we shut Laravel Cloud down for good. We took the final database dump first and put it somewhere safe, on the theory that deleting the last copy of anything is the fastest way to find out what was in it.

July 2026 – 15,227 links and no idea

We have spent 15,227 backlinks on this site since June and cannot tell you whether a single one of them worked. Not because the campaigns failed – they ran cleanly, 36 of them, 301 projects, no errors. Because we made the answer unknowable. Every intervention shipped at once, and every blog post good enough to serve as a control was given a campaign instead.

Traffic roughly doubled, which sounds like an answer until you look at the posts with no links at all. Those grew 108.5% in impressions per day. The posts we paid for grew 120.6%. Both track the site-wide 92.6%. There is no signal to find in that, because we never left anything alone to compare against.

So the second phase is running as a split test. Fourteen posts get escalated; fourteen matched posts deliberately get nothing, paired on their existing traffic so both halves start level. This costs nothing – the escalation was happening anyway, we are only choosing which half receives it. The price is fourteen posts waiting eight weeks. In September we either have evidence, or we have a clear enough answer that we stop buying links and say so out loud.

Writing down how we would measure it before the data arrives turned out to be the part that mattered: the metric, the statistical test, the window, and one rule in particular – do not look at clicks. Six clicks against ten across fourteen posts each is noise, and peeking at noise is how you talk yourself into the result you were hoping for.

July 2026 – Pointing at nothing

1,717 links to pages that do not exist. Posts here live at /blog/{slug} – except news, at /blog/news/{slug}, and guides, at /blog/guides/{slug}. The script that created the link campaigns built every URL as /blog/ plus the slug. Four campaigns spent five weeks pointing at 404s: 11.3% of every link we have ever bought. It is not fixable. The dead URL is baked into roughly 1,700 articles already published on other people's websites.

The genuinely irritating part is that the same mistake had shipped twice more that month – once in the site footer, once in a reporting script – and been fixed both times without anyone noticing it was a pattern rather than three coincidences. There is now one function that builds a post URL, and another that checks the page returns 200 before a campaign is allowed to exist at all.

The file written for AI crawlers was full of dead links. /llms.txt is a plain-text index of the site for language models. Ours hand-built every URL the same wrong way, so every guide, interview and news entry 404'd – five of the first six. Fixed, along with four database queries in the same file that had been loading one row at a time.

Redirects that never redirected. The old WordPress URLs are mapped to their new homes and cached forever, for speed. "Forever" proved accurate: the cache was only cleared when someone edited a redirect through the admin, and these had been added by migration. Search Console had been reporting one of them as a 404 since May, while a migration written specifically to fix it sat there having no effect whatsoever.

And robots.txt was blocking real pages. /blog/category/ was in the disallow list, filed as a leftover WordPress artefact. It is a live route, linked from the hero of every blog post and from the blog index. 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.

None of this is interesting work, and all of it was invisible until someone went looking. That is more or less the argument for going looking.

Elsewhere, in the parts of July that were meant to happen: three long guides went up, each with working tools built into the article rather than described in it – a maintenance-cost calculator, a vetting scorecard, a rebuild-or-refit decision tree. Twelve more signs were assigned from the system built in June, to the About, How we work and Free UX Review pages. And the test suite crossed 646 tests, having spent an afternoon failing for the least dignified reason available: it needed more memory than PHP hands out by default.

June 2026 – A language for the work

Spent a day building the thing every studio claims to have and mostly doesn't: a design system. It lives at /guidelines now – the palette (one grey, restraint as a feature rather than a budget), the type scale, the buttons we keep redeclaring in every block instead of writing once, and the part I hadn't planned to lose an afternoon to: the signs.

The brand mark is one fixed shape. Using it identically across six services felt lazy, so I built a small system to generate siblings from the same construction – square-framed, hairline, one grey weight, never filled – and assigned one to each service by meaning rather than by whichever looked nice that minute. UX Design gets a compass, because UX is wayfinding. SaaS Product Design gets rings spinning outward, because that's what scaling actually feels like. There is a real internal logic to all of it, which I will defend at length to anyone who makes the mistake of asking.

Generating a hundred-odd candidate marks is easy. Generating a hundred-odd that all read as the same family is the actual work, and naming them so they come out as "Triple Compass" rather than "gen 47" took longer than writing the generator did. I also built a section showing every component on the site, looked at it, decided it was clutter, and deleted it the same afternoon. Some features exist just long enough to confirm they shouldn't.

The signs went live across the services and their sub-pages, applied through a database migration rather than by hand – partly for safety, partly because assigning twenty-eight marks manually is how you give the wrong service the wrong one and don't notice for a week. The /guidelines page is public now, linked from the footer. It's the source of truth, which mostly means it's the page I'll be faintly embarrassed by in six months, when the system has moved on and the documentation hasn't.

June 2026 – The last 5%

The site is mostly where it should be, which means I've moved from fixing obvious things to fixing things that bother only me. Tag archive pages were running 91 database queries per page load; now they run 2. No visitor would have noticed the difference. That's not really the point.

Extended the audit. The footer was firing a separate database query for each service page link – six queries to build a navigation list that should take one. Every blog post page was also lazy-loading related post tags in a loop. Fixed both with proper eager loading. Added Model::preventLazyLoading() to the test suite so all 564 tests now catch N+1 queries automatically. The next one will fail CI before it reaches production.

Cloudflare’s bot protection script was loading on every page, including the ones with no contact form on them. Moved it to render only when the contact block is present. Also removed the Google Fonts import – it was loading Inter on every page view, and Inter appeared nowhere in the actual CSS. Two DNS lookups and a render-blocking request, for a font nobody was using. This one required a moment of quiet reflection.

Added fetchpriority=high to the first case study image on /work, and removed the fade-in transition from it. The LCP element fading up from zero opacity is technically correct and exactly the kind of thing PageSpeed Insights flags as a problem. It was flagging it as a problem.

Published a guide on the web design process – the first thing I've written here that I'd actually send to a prospective client without sheepishly adding "this was more of a rough draft" in the email. It's titled A Guide to the Web Design Process for People Whose Last Designer Was an AI Agent, which I stand by even if it makes the URL quite long. Writing for a real audience feels different from writing for the blog. Still calibrating the tone.

Added this page. Updated the legal pages to look like they belong to the site rather than something copy-pasted from a compliance generator in 2009. Nobody reads terms and conditions, but they do form an impression – and "I don't care about this page" is the wrong one for a design studio to give.

Late May 2026 – Infrastructure and consequences

The git log for this period reads like a changelog for a site that had been accumulating debt while we were busy doing actual work. Some of it was embarrassing. All of it is now fixed.

The slug incident. A CMS update in May rewrote 43 blog post URLs without asking. Google had the originals indexed. We didn't have analytics. We didn't have analytics because we hadn't turned them on. Enabling analytics and discovering the slug problem happened in the same sitting, which at least kept it efficient. All 43 slugs restored, locked permanently, 301 redirects in place.

Fonts. Moved Satoshi, Switzer, and Playfair Display off external CDNs and onto our own server. Faster loads, no third-party requests on every page view. This should have happened at launch. It didn't, because at launch we had other problems.

Performance. The hero image was arriving 400ms after everything else – technically correct, practically frustrating for Google's crawlers. Fixed the load order, which broke the animation, which we fixed, which revealed the opacity wasn't the delay, the delay was. Three commits. Several opinions.

Analytics. Set up GA4 via Google Tag Manager. Then set it up directly as well, because the first one wasn't visible in the commit history. Ran with two tracking scripts for approximately one deploy before noticing. Both the setup and the discovery are very on-brand for this phase of the project.

SEO audit. First Ahrefs crawl returned 67 broken links pointing at the old WordPress site. Also surfaced that /blog/page/10 was serving a blank 200 – Google had indexed it. Bots had apparently worked through pagination all the way to page 47 looking for content. We now return proper 404s. The 404 page also no longer looks like an error.

ROBOTS_NOINDEX. We had a flag in the codebase that told search engines not to index the site. It had been there since March – a sensible precaution during development that nobody removed when the site went live. We removed it in late May. The site had been live and invisible for roughly two months. These things happen, and then you fix them, and then you don't mention it in client pitches.

Security. Added proper HTTP security headers, input length limits, subresource integrity hashes on third-party scripts. None of this is interesting to read about. It was overdue.

May 2026 – The big redesign push

The month started with a designer handing over final layouts and ended with the site looking like a real product. In between: porting CSS from static HTML files into Blade templates, a complete blog redesign with proper BEM structure, a new expertise block to replace a component that had been doing the job without doing it well, and the case study pages finally getting a layout that matched the work inside them. The breadcrumbs now use the DNSK icon as a separator instead of a slash. This is the kind of detail that takes thirty minutes and stays forever.

The blog index got rebuilt properly – filters by post type, a sensible pagination design, the footer showing recent posts. The blog had existed since March in a fairly rudimentary form. May was when it started feeling like something you'd want to browse rather than just land on.

Somewhere in the middle of all this, we briefly built a /zine-preview page and removed it the same day. Some experiments are private. It might come back.

Also moved the Blog navigation link from hardcoded HTML into the CMS, which is a small thing that took two hours of debugging. The site runs on Laravel Octane, which boots the application once and keeps it warm across requests – great for performance, occasionally surprising when something that should be fresh isn't. The navigation was disappearing after the first request because of how singletons behave in that environment. The fix is three lines. The investigation was not three lines.

April 2026 – Getting the basics right

April was contact forms, character encoding bugs, and a significant design pivot. The encoding issue – Polish and Ukrainian characters arriving as corrupted garbage – took four commits to resolve. The problem was at the PostgreSQL connection level. The solution was one line of configuration. The four commits were the journey to that line, which included a brief detour through a fix that worked on local and broke on production, which is a journey everyone has taken and nobody enjoys.

The design changed to black and white in a single commit on April 20. The message was "new black&white design" – five words, complete pivot. It was the right call. The previous version had been trying to be too many things. The cleaner direction felt more honest about what the studio actually does: precise, considered, not particularly interested in decoration for its own sake.

The contact form got built properly this month: dynamic service options pulled from the CMS, required GDPR consent, budget range selector, spam protection via Cloudflare Turnstile. The commit history for this one week contains seven updates to the routes file and four to the form template. That's what building something properly in public looks like – a lot of small corrections in the right direction. Security fixes landed too: session hardening, a Laravel CVE patch, a dependency vulnerability from a library that processes SSH keys (we don't use SSH keys, but you patch what needs patching).

March 2026 – Init

March 3: first commit. The message was "init", which is honest if not illuminating. The second and third commits were both titled "removing junk". We were getting organised.

The previous site ran on WordPress. It had accumulated plugins, outdated templates, a theme that was impossible to edit without breaking something else, and a general sense that it had been built in several different eras by several different people with several different opinions. The decision to rebuild it from scratch was easy. The decision to build it on Laravel with a headless CMS was slightly more ambitious than it needed to be, and also absolutely the right call – the flexibility has paid for itself many times over.

The first few weeks were foundations: PostgreSQL, Twill CMS, blog post types (posts, guides, interviews, news), JSON-LD schema markup for every page type, RSS feed, contact form with email notifications, sitemap, 404 and 500 pages, admin rate limiting, image compression with Glide. The things you build so you can build everything else. Not glamorous. Completely necessary.

By the end of March the site had case studies, a services block, client logos with SVG support, testimonials, FAQ with open/closed states, and a working blog. Most of the content was placeholder. Most of the design was temporary. That's fine – the important thing is that the structure was right. What's slightly unusual is that we kept most of the database schema from those first weeks essentially intact. It was well thought through for something sketched out at 11pm.