Thursday afternoon I pushed thirteen commits to a beauty salon website in under three hours. No features, no redesign — pure structured data and entity disambiguation work. It is not glamorous, but it is the kind of thing that quietly determines whether Google and AI search engines understand who a page is actually about. That was the centre of this week. Alongside it: a resilience fix in a home-security system I run, and a small but overdue maintenance bump to CivoCloudManager.
Jasmine Lise Beauty — Teaching Search Engines Who She Is
The site is built in Astro for a Berlin-based beauty professional. The content was already solid. The problem was that the structured data told search engines almost nothing concrete about the person behind it. A generic Person entity with a name and a URL is not enough — not for Google’s Knowledge Graph, and definitely not for the LLM-based answer engines that are increasingly the first stop for local service queries.
My starting point was the JSON-LD markup. All the existing entities were scattered across separate script tags on different components. Before I could improve anything, I consolidated them into a single @graph block. One @graph means the entities can reference each other by ID. The Person node can point at the LocalBusiness node, which points at the VideoObject nodes, which point back at the Person. Without that graph structure, the entities are islands — a crawler has to guess at the connections. That consolidation alone took longer than I expected because several entity IDs were inconsistent across components. Classic.
Once the graph was clean, I enriched the Person entity properly: given name, family name, a declared occupation, sameAs links to her YouTube channel (by channel ID, not just the handle URL), Instagram, and email as a mailto: URI. I also added rel=me link elements in the <head> for the same destinations. The rel=me signals are for identity verification — Mastodon and some search tooling use them to confirm that the linked profiles belong to the same person as the page author. Small detail, real signal.
The part I found most interesting was the GEO work — Generative Engine Optimization, which is the emerging practice of making content legible to LLMs rather than just crawlers. The site already had an llms.txt file, which is a plain-text format several AI search systems now read before anything else. I rewrote it with an answer-first identity block: who she is, what she does, where she works, what she is known for — in the kind of direct, declarative prose that a language model can lift verbatim into an answer. I then extended llms-full.txt with name variants, positioning statements, a FAQ section, and a German-language summary. The German summary matters because local queries in Germany will often arrive in German, and a bilingual signal in the file reduces the chance of the AI hedging with “I’m not sure if this is the same person as…”.
I also added a FaqLd component — a FAQPage structured data block with bilingual Q&A pairs specifically written for entity disambiguation. Questions like “Is Jasmine Lise Beauty in Berlin the same as the YouTube channel?” are exactly what a zero-click AI answer has to get right. The component mounts on both the English and German homepages. I am reasonably happy with how it turned out, though I will admit the line between useful FAQ markup and pure crawler-bait is thin. I tried to stay on the right side of it.
The one thing I am not fully satisfied with: the VideoObject entities. I enriched them with inLanguage, isFamilyFriendly, and a publisher reference, but the video descriptions are still short. Rich video descriptions are a meaningful ranking signal for YouTube-originated content in Google’s index. That is a content task, not a code task — something for next time.
Side Notes
camera-s3-stream — Silent failure was the whole problem. This is a shell-based system that streams footage from IP cameras to S3 via rclone, with a dead-man’s-switch for tamper detection. I noticed earlier in the week that the upload loop was occasionally stopping without logging anything useful. The cause was a combination of two things: a full tmpfs partition causing mv to fail silently, and the discovery cache returning stale camera entries after a forced re-scan because I never invalidated it. The upload loop now catches transient I/O errors and retries instead of dying quietly, and the discovery script flushes the disk cache when called with --force. Neither fix is complicated. What bothered me is that this had been running in production — on my actual home server — with silent failures for longer than I want to admit. A monitoring gap I should have closed sooner.
CivoCloudManager 2.1.1 — Maintenance, not features. One commit: sync updated legal documents from the legal repo, add a Support tab to the settings panel, bump the build to 2.1.1. The Support tab was the only thing users had written in about recently — no obvious way to reach me from inside the app. It is a small quality-of-life gap, and I should have caught it at launch.
What This Looks Like From the Outside
Structured data work and security hardening are not the kinds of things that make for impressive screenshots. But they are exactly what a well-built product looks like under the hood. If you are considering having a site or app built and want to know what “done properly” means in practice, the services page has the longer answer. The short answer is: the parts no one sees are usually the ones that matter most six months later.
CivoCloudManager is open source if you want to see how a zero-dependency Swift 6 macOS app is structured: github.com/marcelrgberger/civo-cloud-manager.