Agency websites are always the last job on the list. There is always a client website that needs attention first, and ours sat there for years getting the occasional content tweak and not much else.
Last year I finally stopped putting it off. Twelve months on it has been rebuilt, upgraded, rewritten, researched and tuned, and the results are in the numbers. Over the last three months impressions are up 43% and clicks are up 214% according to Google Search Console. We are on the first page for a lot of the Umbraco terms we set out to target, and the website is regularly being cited by AI assistants when people ask about Umbraco development.
This is the first of three posts. This one is the story of what we did and why. The other two go properly into the technical side, because a couple of the things we built are worth writing up on their own:
Part 2: output caching in Umbraco 17, what it cost us and whether it was worth it
Part 3: how to use CSP nonces and output caching at the same time, which most people will tell you is not possible
Starting again on Umbraco 13
The rebuild started on Umbraco 13, which was the long term support version at the time. A fresh project rather than a patch job on the old one. If you are redoing the front end anyway, you may as well redo the document types and templates properly at the same time.
For the initial look and feel I used AI. I described what I wanted, got a few directions back and picked the one that was closest, and that design is what the website still uses today. I did not throw it away and start again. We tidied it up, tweaked the bits that did not quite work and built on it as the website grew.
Building the back office around blocks
The important part of any Umbraco build is not what the visitor sees, it is what the editor gets. Everything on the website is built from blocks, so pages are assembled rather than hard coded. Every new layout I have needed since has been a new block rather than a new template, and I have added a fair few over the last year as different content needed different treatment.
That decision paid for itself the moment I started writing service pages. I could lay out a page in a couple of minutes without touching any code.
How the project is put together
The solution is split in two. Nevitech.Web is the website itself, views, wwwroot and configuration. Nevitech.Core holds everything with logic in it, the services, middleware, validators, caching policy and extensions. It keeps the web project thin and it means the interesting code is testable and portable rather than tangled up in view templates.
ModelsBuilder runs in SourceCodeAuto mode and writes its generated models straight into the Core project rather than leaving them in the web project:
"ModelsBuilder": {
"ModelsDirectory": "~/../Nevitech.Core/Models/Generated",
"ModelsMode": "SourceCodeAuto",
"ModelsNamespace": "Nevitech.Core.Models"
}So the models sit alongside the code that consumes them, in source control, and I get strongly typed content everywhere. In staging and production the mode is set to Nothing, because nothing should be generating source files on a live server.
Document types, data types and the rest of the schema are handled by uSync, so the structure lives in source control with everything else and moves between environments without anyone clicking through the back office twice.
HTML minification is WebMarkupMin, with /umbraco/ and /App_Plugins/ excluded. Minifying the back office is a good way to spend an afternoon debugging something that was never your code's fault.
Moving to Umbraco 17
We updated to Umbraco 17 as soon as it was released. There is not much point telling clients we do migrations if our own website is sitting two majors behind. It also means I am running the same version I am recommending, so when someone asks a support question I can check the behaviour on my own website rather than guess.
The content is mine, AI just helped me word it
This is worth being clear about, because a lot of websites now read like nobody was involved at all.
Every page on this website started as my own bullet points. My thoughts, my ideas, my opinions on how we work and what we are good at. What AI did was take those bullets and turn them into proper sentences, and it does that better and faster than I do. I am a developer, not a copywriter.
Then I edited it. Every page has been through my own edit, and a fair amount was cut rather than reworded. The first drafts are always a bit too pleased with themselves, so anything that sounded like AI got taken out. The website is written in my tone of voice, and if it did not sound like me it did not go live.
I did the same for images. AI generated the imagery and helped flesh out the service pages, which saved a stock photo budget and a lot of hunting.
Doing the SEO work properly
This is the part that made the difference to the traffic.
We used AI to research the keywords that matter for Umbraco services, and to analyse competitors and find the gaps where they were ranking and we were not. That gave us a target list rather than guesswork, and every service page was then rewritten around it. Headings, meta titles, meta descriptions, the lot.
Two things stand out from the process.
The first is that the data contradicted my instinct. I was ready to drop the Kent angle and chase the national terms. When I pulled three months of Search Console data, the Kent terms were already on page one while the national terms were sitting on page three and four with no clicks at all. So the local angle stayed and we worked on the national terms rather than swapping one for the other. Both are now doing well.
The second is that AI will happily overclaim if you let it. A draft of our Umbraco Silver Partner page described the status as vetted and earned rather than bought. That is not true. Partner status is an annual subscription. The thing that actually means something is the Certified Master certification, which is an exam you either pass or you do not. I rewrote the page around that instead.
The same exercise turned up things I would rather have found myself. Our years of experience figure was different on three different pages. There was a pricing mismatch between the homepage and the SEO page. Several pages carried the same credentials paragraph. All small, all embarrassing, all fixed.
Making it fast
The 100s in PageSpeed Insights took real effort rather than a plugin. Cloudflare sits in front for static caching at the edge, and on the server there is output caching and partial caching, which Part 2 covers in detail.
Beyond the caching, a handful of smaller things made a genuine difference:
Critical CSS. Above the fold styles are inlined and the rest of the sheet is loaded without blocking the first paint. The usual trick for that uses an inline onload attribute, which our own Content Security Policy blocks, so the swap is done by a small nonced script instead.
Only ship the JavaScript the page needs. The form validation bundle only loads on pages that contain a contact form block, and the slider bundle only where there is an image slider.
Preload only the fonts the page paints. The semibold face is only preloaded on pages that actually render text at that weight, which is the blog pages and anything with a statistics block. Preloading it everywhere spends high priority bandwidth during the LCP window on a font the page never uses.
Font preload URLs that match the bundler. This one cost me an afternoon. WebOptimizer adds a cache busting hash to asset URLs, so a hand written preload link fetches one URL and the browser then fetches a slightly different one. Two downloads, and it looks like it is working. We now generate the preload URL using the same hash the bundler uses, so the two match exactly.
Compression. WebMarkupMin only ships Deflate and GZip compressors and its Brotli add on does not target .NET 10, so compression moved to ASP.NET Core's own middleware. Brotli is around 17% smaller than gzip on our CSS. The compression level is deliberately set to Fastest, because Brotli's Optimal level is quality 11 and that is far too slow to be doing per request.
Structured data, meta and llms.txt
Every page outputs JSON-LD schema, built as dictionaries in C# and serialised rather than hand written into the templates, so escaping is handled properly rather than hoped for.
There are fallbacks all the way down so nothing ever ships empty. A meta title falls back to the page title, then the page name, truncated to 65 characters. A description falls back to the first text block on the page, stripped of markup and truncated to 158.
The sitemap reports lastmod on listing pages based on the newest item they list rather than just the page's own update date, so the blog index and the author pages do not sit there telling Google nothing has changed for six months while you are posting weekly.
Indexing is off by default. The setting that hides the website from robots is true in the base configuration and only the production settings turn it off, so no staging or development environment can accidentally index itself. Tag filtered blog URLs also emit noindex so we are not diluting our own pages with near duplicates.
We have also added an llms.txt file, built as an Umbraco document type and generated from the content tree. Each page can override its own description, falling back to the meta description and then the page name, and it respects the same hide from sitemap flag as everything else. I have not seen it done that way anywhere else. Between that, the schema, the clean markup and content that actually answers the question, we are now being cited by AI assistants when people search for Umbraco help. That was a deliberate goal, not an accident.
Security, and a contact form without a captcha
The Content Security Policy starts from allowing nothing at all and adds back only what is needed. The usual headers are all there, along with HSTS with a year's max age and preload, and the policy is suppressed on back office paths so it does not interfere with the editor.
The contact form has no captcha and I am not adding one. Instead there is a hidden field that must stay empty, a timing check that rejects anything submitted in under five seconds or more than 24 hours after the page was rendered, rate limiting at five submissions a minute per IP address, and standard antiforgery tokens. It stops the bots, and real people never have to identify a traffic light.
AI as a tool, not a shortcut
None of this would have got done in twelve months without AI, and it was a massive help being able to fit the work around client projects.
It is a tool. It speeds the process up. Work that would have sat on the list for months because I could not justify the hours got done in an evening, and jobs I would previously have paid someone else to do, like the first pass on the design or a keyword gap analysis across competitors, I could do myself. It is also a genuine second opinion. Being able to lean on the expertise of an LLM, ask it what I am missing and have it pick holes in my own copy is worth a lot when there is nobody else in the room to ask.
What it does not do is make the decisions. Every output needed someone who knows Umbraco, knows the business and knows what is true to check it, and the times it was confidently wrong were the times it mattered most. Used properly it makes a small agency work like a much bigger one. Used lazily it produces a website full of claims you cannot back up.
What this means if you are reading it as a client
This is the work. Rebuild on a current Umbraco version, structure the back office so content is easy to manage, research the keywords that actually get searched, write content that says something, make it fast, mark it up properly, and then keep at it.
We did it on our own website first because we wanted to prove it out before selling it. Impressions up 43%, clicks up 214%, first page rankings on the terms that matter and citations in AI answers.
If you want the same for yours, get in touch. And if you just want the code, Parts 2 and 3 are next.