Blog

The code structure that tells Google what this site is

Almost none of the SEO work on this site was about keywords. Nearly all of it was decisions made while writing the code: what to tell the machines reading it, and what not to claim.

Published

A diagram of an Article box pointing to a Person box along an arrow labelled @id, over a photograph of a library card catalogue

Somebody is looking for a company to build a system for their factory. They type a question into a search box, or they ask an AI assistant.

Whether our site turns up in that answer is not decided when they type. It was decided the day we finished the code for that page and chose what to tell the machines that read it.

This is an account of what this site says, and — more interesting — what it deliberately declines to say. We do not think we are cleverer than anyone else here. We just thought about it a bit harder.

This has less to do with keywords than people expect

SEO conversations tend to start at word choice, word frequency and article length.

What decides everything before that is whether the machine reading the page understands what the page is. Whether this page and that page are one page in two languages or two separate pages. Who wrote this article. When this job was advertised and what it pays.

None of those answers live in the prose a reader sees. They live in the structure of the page, which is the job of whoever writes the code — not whoever writes the copy.

Structured data that tells the truth rather than looking good

The site publishes several sets of machine-readable facts. Two at the site level: the company, and the site itself. Individual pages add their own — an article page says it is an article and who wrote it, a service page says it is a service being offered, a job page says it is a job posting, and anything nested carries a trail saying where it sits.

The part worth talking about is what we decline to declare, which is the part usually skipped.

Our service pages declare themselves as a Service, not a Product. Nothing here is taken off a shelf and paid for. Declare Product and Google starts looking for a price and a review score, neither of which exists — and the page becomes an incomplete product instead of a complete service.

Then there is site search. The standard lets a site declare that it has internal search and that queries can be sent to a particular address. We declare no such thing, because we have not built a search page yet; it is being held back until there are enough articles to need one. Markup promising a route that returns "not found" is worse than markup that stays quiet.

My favourite is the FAQ. Google narrowed FAQ rich results to government and health sites back in 2023, so we know perfectly well that ours will almost certainly never draw a special listing. It is still there, because it tells any machine reading the page — including an AI assistant answering somebody's question — which text answers which question.

The rule we hold ourselves to: the questions and answers in the markup are the exact same strings the page renders. The moment those two differ it stops being SEO and becomes lying to the crawler, which Google has a name for and penalises accordingly.

An article by nobody is worse than no article markup at all

At first we declared articles as articles with no author, because there was no author system yet.

Then it occurred to us that an Article with no author claims the page is a piece of writing by nobody — which is worse than not claiming it is a piece of writing.

Article and author are now declared as a single graph. The person has their own identity that points at our team page, and the article points at that identity rather than repeating the name. A machine that reads either node gets the same attribution. And where a post genuinely has no author, we leave that part out rather than inventing a name to fill the field.

Two languages that do not compete with each other

Every page here exists in Thai and English, which is where a great many bilingual sites quietly lose ground: the two versions end up competing with one another, or get read as duplicate content.

So every page declares three things. Its own canonical address. Where its other language lives. And which version to use when the machine cannot tell what language the visitor wants — the Thai one.

The sitemap repeats the same relationships. Ninety-six entries at present, forty-eight Thai and forty-eight English, each carrying its language pairing rather than being a bare list of links.

A small detail we like: dates. An article displays its date to a Thai reader as 2 กันยายน 2569 — in the Buddhist calendar year — while the same element carries a standard machine-readable date alongside it. People get the year they recognise, machines get the value they can parse, and neither has to lose.

Staging kept out of the index by code, not by a checkbox

There are two of this site: the real one and the staging one. Nearly identical content.

Staging must never be indexed. If it were, we would have two sites duplicating each other page for page, and a client could land on the staging copy from a search result without realising.

The usual approach is a checkbox in the CMS saying "do not index", which works right up until somebody unticks it. Here it is not a human decision at all: the code checks which deployment it is running as, and anything that is not production says "do not index" unconditionally. The CMS checkbox still exists, but it only works in one direction — it can add the restriction, never lift it.

Scheduled posts that do not exist until their date

This one is my favourite piece of plumbing.

Posts can be scheduled ahead, and several currently are. The question is what happens to those pages before their date arrives.

The answer here is that they do not exist. Not hidden, not showing a "coming soon" notice — the URL returns not found, and the page is absent from the sitemap. We checked tomorrow's post while writing this: not found, exactly as it should be.

The reason is simple. A page reachable early is a page indexable early, and then the real publication date becomes the second date rather than the first.

Publishing takes effect at once, with no deploy

Every page is built ahead of time as a static file — a hundred and twelve of them in the last build. That is what makes the site fast, but it also means the page being served is the one built a while ago, not the latest state of the CMS.

So the two are wired together. Saving content in the CMS fires a signal that clears the cached pages immediately, and the new content is live in roughly fifteen to sixty seconds. No deploy involved.

Worth stating plainly: a deploy should happen when the code changes, and only then. If a team has to deploy every time somebody fixes a typo in an article, content and code are welded together, and the people who write will be waiting on the people who build forever.

A job posting Google will actually show

We added two things to job pages recently, both of them conditions Google sets before it will show a posting in its jobs results.

The first is the date the role was advertised, which we had never stored at all. A posting with no date is a posting Google declines to show.

The second is the salary band, and this is where it changed the design. We do not store the text "40,000–100,000". We store two numbers, because the markup has to say which currency this is and whether it is monthly or annual — and a string says neither.

The side benefit: the Thai and English pages format the same numbers their own way, from one source, with nobody typing it twice.

Something written for machines that are not Google

There is a plain text file on this site listing every page with a one-line description. It is not for traditional search engines. It is for an AI assistant reading the site to answer somebody's question.

Will anything use it? We do not know yet. But it costs one text file, and the direction people are moving in is reasonably clear.

What this site is still missing

One thing we know is absent.

Not a single entry in our sitemap carries a last-modified date, even though the CMS holds that date already. That value is how a crawler decides which pages to come back to first. Leaving it out breaks nothing — it just throws away information we already have.

Writing it here so it is visible work rather than a silent gap.

What to take from this

If you are about to hire somebody to build a site, three questions are worth asking. What stops your staging copy appearing in Google. How do the two language versions of a page declare their relationship. And when someone fixes one line of copy, does that require a deploy.

If you build sites for a living like we do, the thought worth sitting with is that every piece of structured data is a promise about what the page is. Filling in every field so a validator turns green is not the goal. The goal is that what you declare matches what is actually on the screen.

If you would like us to look at a site or a system you are working on, get in touch.

About the author

Portrait of Film

Film — Wisit. A businessman who still does his own BA work more often than he probably should, and writes a fair bit of code, front and back. Runs two or three small businesses. Follows technology and business obsessively, in Thailand and everywhere else. Off the clock: physics, astronomy, DIY, and anything to do with networks. Music always on, though he cannot sing. Plays instruments anyway, badly. Plays a lot of sport, racket sports above all. Not much of a traveller by himself, but happy to take Mint anywhere in the world. A man who fears — sorry, loves — his wife. One flaw: he barely touches video games.

Written with Claude Opus 5