Skip to content
All posts
3 min read

SEO for a name

Owning search results for your own name is an engineering project: one Person entity, repeated on every page, with nothing left to drift.

EngineeringSystems

Search my name and the results page is the product. This site exists so that Remiel Shirazi resolves, in a search engine's model of the world, to one specific person with one canonical home, and that goal turned out to be an engineering project with a data model, not a content-marketing chore.

The data model has one root: a schema.org Person node with the id remielshirazi.com/#person. Everything on the site points at it. A blog post names it as author and publisher. A project page names it as founder, or creator, depending on what the project is. The links page wraps it in a ProfilePage, which tells the graph this page is about that entity. One person, many roles, a single id.

The constraint that shaped all the code in src/core/seo.ts: Google merges id references within one page, not across pages. A bare reference to /#person on a project page resolves to nothing, because the full Person lives on the home page and Google will not walk over there to find it. So every page embeds a compact Person node, name, url, job title, profile links, right next to the references, and the home page carries the full canonical version. Same id everywhere, so the graph collapses them into one entity; enough substance on each page that there is always something to collapse. It is denormalization, exactly as unglamorous as in a database, and skipping it silently voids the whole scheme.

Drift is the other enemy. Facts repeated by hand across pages will eventually disagree, and an entity graph that disagrees with itself is worse than none. So there is one identity module holding the canonical name, the Remie alias, the email, and every profile URL, and the JSON-LD builders, the links page, and the page metadata all read from it. Changing a fact is a one-line diff that propagates everywhere or nowhere.

The decision I am proudest of is a number I leave out. Game pages emit lifetime plays as an InteractionCounter with PlayGameAction, the genuinely cumulative measure, and not the deprecated UserPlays type. But live concurrent players, the most impressive number the games produce on any given afternoon, never enters the structured data. An InteractionCounter states a cumulative count; concurrent players is a gauge that will read differently in an hour. Publishing it there would misstate reality using a true number, and it would churn the cached markup on every poll for the privilege. The live count stays in the visible UI where a fluctuating number is honest. Structured data that lies with real numbers is still lying.

There is a quieter denormalization rule for lists. Index pages, the projects grid, the blog index, emit stubs: position, name, url. The full entity markup lives only on each detail page. Emitting complete entities from both places would hand the graph two competing descriptions of everything, and the summary-page pattern is how you say "the real one lives over there" in a format Google respects. Detail pages also each carry a breadcrumb list, Home to section to page, which is one of the few structured-data types with a visible payoff in the results themselves.

This month the plumbing caught up with the model: the sitemap now generates from the route registry plus every published project and post, and each page carries a self-referencing canonical, so crawlers hear one consistent story about which URLs exist and which version is real.

The feedback loop is the strange part for someone used to deploys. The measurement is a monthly incognito search of my own name, and the effect of any given change is unattributable weeks later. You ship like an engineer and wait like a farmer. A Knowledge Panel is the endgame, and there is no trick on the path to it, just one entity kept so internally consistent that the graph runs out of reasons to doubt it.

Remiel Shirazi

CTO & Co-founder | AI, Roblox & Piano