Part II — The blog was supposed to be the easy part
The article was signed, at the foot, "ASU Innovation". It was published on umbracolombia.com.
Nobody had decided that. The article existed twice — one node under each site — because the process that published it wrote to both, and each write created a node. The two bodies were identical: 6,455 characters, the same SHA-256. And because the byline was a property of the site rather than of the author, the Colombian copy went out carrying the Spanish company's name.
After the version upgrade, the blog looked like the paperwork that was left. Two sites, both of them writing about Umbraco and Sitecore, so publish to both and move on. That sentence hides a question we had not asked: what exactly is it that gets published twice?
What we believed: that there was one node too many
The obvious reaction was to delete one. That is what you do with a duplicate.
And it was reasonable, except for two things that in Umbraco cannot be separated from the tree. The first is that a node's URL comes from where it lives: domains bind to each site's Home, so a node under ASU's Blog can only have an ASU URL. The second is that the theme comes from the same place — it is chosen by reading a property at the site root, which is to say on an ancestor of the node — so a node outside both trees has no such root to read and renders with the default theme, which implements one of the eight blocks. The page does not fail. It comes out nearly empty.
Deleting a duplicate, then, does not leave you with a shared article. It leaves you with an article that exists on one site and has disappeared from the other.
What we ended up with: the body lives once
What we did was split the article into two different things that until then had been stuck together by accident.
The text — the title, the body, the date, the categories, the tags, the author — lives exactly once, in a library outside both sites. And each site that wants to show it has a thin node with no body of its own: it points at the article and contributes the only things that genuinely belong to the site, which are the URL, the language it appears in there, and whether that copy is the canonical one.
Today that is 45 articles in the library and 65 references spread across the sites: 20 articles appear on both, 23 only on ASU, two only here — this one among them. Without the library it would be 65 bodies, and every correction would have to be made in two places by somebody who remembered there were two.
The byline stopped being a problem the same day — not because we fixed it, but because it ran out of places to be wrong. The author is a field on the article, and there is one article.
What moving 20 articles cost
Two published URLs returned 404 on the first run, and both for the same underlying reason: in Umbraco a node's name is its URL.
The first was taking the wrong name. When creating the reference we gave it the library article's name, and it turns out the Spanish title on one site was not the Spanish title on the other. The reference was born with a different segment from the page it was replacing.
The second was subtler and much easier to repeat: we created the reference before sending the duplicate to the recycle bin. With a sibling of the same name already there, Umbraco disambiguates on its own and appends " (1)". Nobody sees an error. The URL that was in Google simply stopped existing.
Both were found because before moving anything we saved the list of published URLs and asked for it again afterwards. They were not found by looking at the page: the two broken pages looked fine, each on its own site, until somebody asked for the old address.
The listing: two attempts before the one that stayed
With all of those articles, the blog page printed every one of them. What was missing is what a blog has done for twenty years: search, filter by category, by tag, by month, and switch between a list and a grid.
The first attempt was the cheap one: render everything and use JavaScript to hide what does not match. It works today and it collapses on its own, because "render everything" is precisely the part that does not scale — the page carries the full text of all of them before the reader has asked for anything. It was written, measured against the real content, and thrown away.
The second attempt was to write an endpoint of our own. That scales, and it has a defect you only see once you are using it: the filtering ends up written twice, once in Razor for the first paint and once in the endpoint for everything after. And those two drift apart. One sorts by a date and the other by a different one, or one strips accents and the other does not, and the reader watches the page rearrange itself the first time they touch a control.
What stayed came from reading the assembly instead of the documentation: IApiContentQueryService — the service the Delivery API's own endpoint uses internally — can be injected. Which means the server can run exactly the query the browser runs, against the same filter handlers. One definition of what "category umbraco" means.
That bought something we like more than the feature itself: the facets are ordinary links, "more" is an ordinary link, and the search box is an ordinary form. The script intercepts all three so they cost no page load; with the script blocked, they navigate. And a filtered listing has a URL, so it can be sent to somebody.
The part that did need solving is that the reference is what the listing links to, while the article is where the data is. That is settled in the index: the reference borrows the article's date, month, categories, tags, author and full text. Six fields, five filters and one sort.
What a half-finished measurement taught me
With the filters written, I tested them against the real content and several returned zero where I expected results. With those numbers in front of me the explanation assembled itself — it always assembles itself — and I spent a good while hunting for the defect in my own code.
There was no defect. I had asked for an index rebuild and started querying before it finished. A half-built index answers with whatever it has and does not mention that it is halfway, so I was reading real answers from an incomplete store as though they were the truth.
The useful part is not which filter it was. It is that the wrong answers were consistent with each other, and consistency feels exactly like truth. A hypothesis that fits everything you can see is evidence of nothing if what you can see comes from a system that is still assembling itself.
Two habits came out of that. One is operational: when a check depends on an asynchronous process, the check begins when the process ends, not when you started it — and "ends" has to be something you can measure rather than assume. The other is about method: before believing a number, confirm that the thing producing it is in the state you think it is in. It sounds obvious written down. It is not obvious at eleven at night with an elegant explanation in your head.
A blog's failures do not fail
Looking at the finished page turned up two things I had not broken that day and that had been there for months. One made a search in one language find articles written in the other. The other, older and worse, meant that not a single card in the listing showed a summary — on either site, for as long as the shared library had existed.
Both have the same root, and it is not one particular piece of carelessness: in a CMS, the defaults are plausible. An unfilled text does not behave like a hole, it behaves like an empty string. A property that varies by language, read without naming one, does not complain: it answers in the default language. Neither of those is an error to the system, because the system does not know what you were expecting.
The consequence is that the failure interrupts nothing. It looks like a decision. A listing with no summaries reads as a listing that chose not to have them, and a search that returns too much reads as a generous search. That is why neither showed up in a log or in a smoke test, and why both showed up by looking at a screenshot and counting paragraphs.
What is still lame
What did not get solved all has the same shape, and by now it is recognizable: they are states in which the blog looks fine.
The index is rebuilt at deploy, and if somebody forgets, the listing does not fail — it shows unfiltered. That is deliberate, because a cold start should not take the blog down, and the price is that a half-finished deploy looks correct until somebody clicks a category. The same goes for the sidebar counts, which are for the whole blog and do not change as you filter: that is on purpose, and a reader can still read them as the result of the filter they just applied. And because the reference's facets are borrowed, there is a handler that refreshes them when the article is published; it works, and it is one more moving part, so if it ever stops running the symptom will again be an article missing from a filter and a page answering 200.
Every failure in this blog answered 200. The duplicated article, 200. The wrong byline, 200. The card with no summary, 200. The half-built index, 200 with the answers changed. Not one of them was found by reading a log. All of them were found by counting: URLs before against URLs after, filtered results against unfiltered ones, cards against summaries. The project's suite has 26 checks, and nobody sat down to design them. Each one is a day like these.