# Ken Imoto — Full Blog Text
> Concatenated full text of all blog articles for AI citation use.
> Individual URLs are listed in llms.txt or sitemap-index.xml.
---
# I Added 11 JSON-LD Schemas. Three Months Later, Only 3 Showed Up in AI Citations.
URL: https://kenimoto.dev/blog/11-json-ld-3-cited-by-ai/
Lang: en
Date: 2026-05-25
Description: Three months ago I bundled 11 JSON-LD schemas into my site's head. I measured every AI citation since. Eight of those schemas were dead weight. Here's which three actually carried the freight, and why the other eight didn't.
Three months ago I spent an afternoon adding eleven JSON-LD schemas to my site's `
`. Organization, WebSite, Person, four Service blocks, two Books, MusicGroup, FAQPage. I felt very pleased with myself.
Then I measured what AI engines actually did with them.
Three of the eleven showed up in citations. The other eight might as well have been HTML comments.
This is the measurement story. I'll tell you which three schemas earned their seat, which eight were dead weight, and why I'd implement it the same way again — but smaller.
## What I implemented and why I thought it would work
The implementation itself was straightforward. I wrote it up in detail in [the Japanese version of this blog](https://kenimoto.dev/ja/blog/json-ld-11-schemas-llm-understanding/) (English readers will lose the prose but the code blocks translate fine). The short version: I bundled all eleven schemas into a single `
```
That's it. That's the whole page, as far as GPTBot is concerned. An empty `
` and a promise.
## The one fact that explains it
AI crawlers don't run JavaScript.
That's the whole thing. Googlebot does: it loads your page in a headless Chromium, waits for the JS to run, and indexes whatever the browser paints. We've spent a decade assuming that's just how crawlers work, because for SEO it is. The AI crawlers skipped that step. GPTBot, OAI-SearchBot, ChatGPT-User, ClaudeBot, PerplexityBot: they fetch the raw HTML your server sends, read the text that's already in it, and move on. No browser. No render. No second pass.
This isn't a hunch. Vercel and MERJ instrumented over **1.3 billion AI-crawler fetches** across their network and found *zero* evidence of JavaScript execution ([Vercel](https://vercel.com/blog/the-rise-of-the-ai-crawler)). The bots do *download* JS files sometimes (GPTBot pulled JavaScript on 11.5% of requests, ClaudeBot on 23.84%) but downloading isn't running. They grab the file and never execute it, like buying a cookbook and eating the cover.
The reason is boring and economic: rendering JavaScript at crawl scale is expensive, and these bots run on tight timeouts. So they don't. Googlebot eats the rendering cost because search is Google's entire business. For an AI company, your page is one of a billion, and the cheap path wins.
## The test you can run in thirty seconds
You don't have to trust me or Vercel. Pretend to be the bot. `curl` with no JavaScript engine is a decent stand-in for exactly what these crawlers do: pull the raw HTML and look at it.
```bash
curl -A "Mozilla/5.0 (compatible; GPTBot/1.2; +https://openai.com/gptbot)" https://your-site.com/ \
| grep -o '
.*
'
```
If that prints `` with nothing inside, your content lives in JavaScript, and the AI crawler sees the same emptiness. I ran the equivalent against a few sites to calibrate. A well-known client-rendered web app came back with **79 characters** of actual text in the raw HTML, basically a `` and an empty root. My own site, which is built with Astro and rendered at build time, came back with **6,098 characters** of text plus its JSON-LD sitting right there in the markup. Same `curl`, same user-agent, two different realities.
Here's the part that makes it sneaky. Open that same client-rendered page in your browser and it's gorgeous: headings, pricing, FAQs, all of it. Open Google's Rich Results Test and it passes, because Google runs the JavaScript. Everything you use to check your work runs JavaScript. The one audience that doesn't is the one you were trying to reach.
## Why your JSON-LD trick specifically backfires
This is the bit I want every engineer to internalize, because it's the most common own-goal. The standard advice is "add JSON-LD so AI understands your content." Good advice. But *how* you add it decides whether it exists at all.
If you inject your structured data client-side, you've written schema that only appears after the JavaScript runs:
```jsx
// The AI crawler never sees this. It runs in a browser; the bot isn't one.
useEffect(() => {
const script = document.createElement('script')
script.type = 'application/ld+json'
script.text = JSON.stringify(jsonLd)
document.head.appendChild(script)
}, [])
```
`react-helmet`, dynamic `` injection, anything that builds the tag at runtime: to GPTBot, none of it exists. You did the homework and left it in your locker. The fix is to emit the same JSON-LD in the HTML the server sends:
```jsx
// Rendered on the server, present in the raw HTML, visible to everyone.
export default function Page({ jsonLd }) {
return (
)
}
```
Identical schema. The only difference is *when* it gets created, and "when" is the whole ballgame when your reader never starts a JavaScript runtime.
## SEO and LLMO finally disagree about something
For years the honest answer to "does my SPA hurt SEO?" was "not really, Google renders it." That answer is still true for Google. It is now false for AI search, and that split is the actual news here. You can have a page that ranks fine in Google and is completely invisible to ChatGPT, Perplexity, and Claude, for the single reason that Google brought a browser and they didn't.
So the rendering decision you made for SEO reasons (or for no reason, because `create-react-app` was the default) is now an LLMO decision too, and it's the one that gates everything else. There's no point optimizing your `llms.txt`, your headings, or your citations if the crawler is staring at an empty `
`.
## The fix, in order of effort
- **Static sites (SSG).** Astro, Next with `output: 'export'`, Hugo, plain HTML. Content is in the markup at build time. This is the easy win and it's why my own site passed the `curl` test without my doing anything clever.
- **Server-side rendering (SSR).** Next App Router server components, Nuxt, Remix, SvelteKit. The server runs the render and ships real HTML. Same end result for the crawler.
- **Prerendering / dynamic rendering.** If you're stuck with a big CSR app you can't rewrite this quarter, a prerender layer (Prerender.io, or your own headless-Chrome cache) detects bot user-agents and serves them a pre-rendered snapshot. It's a patch, not a cure, but it un-blanks the page.
The check is the same in all three cases: `curl` it as the bot and look at the bytes. If your content is in there, you're done. If it's an empty div, no amount of schema saves you. If you want the full crawler-readability checklist (and the per-path rendering rules for each major bot), that's what I keep at [llmoframework.com](https://llmoframework.com).
## The takeaway
I spent a week being proud of structured data that no AI would ever load. The lesson wasn't "JSON-LD is useless" or "React is bad." It's narrower and dumber than that: **the AI crawler reads what your server sends, not what your browser builds.** If the content shows up only after JavaScript runs, then for the readers you most want, it never shows up at all.
Go `curl` your own homepage as GPTBot. Worst case, you confirm it's fine and you've lost thirty seconds. Best case, you find an empty `
` where your best content was supposed to be, and you fix it before anyone important asks ChatGPT about you.
---
If you want the whole playbook (which bots render what, the minimal JSON-LD that actually survives, llms.txt, and how to measure your AI citation rate), I wrote it up as a short book: [LLMO Quickstart](https://kenimoto.dev/books/llmo-quickstart).
Sources:
- [The rise of the AI crawler — Vercel](https://vercel.com/blog/the-rise-of-the-ai-crawler)
---
# AI Finds Your Page Three Ways. I Published the Same Fact in All Three and Timed Which Reached AI First.
URL: https://kenimoto.dev/blog/ai-finds-your-page-three-ways/
Lang: en
Date: 2026-06-14
Description: Training data, RAG, and live agent fetch are three separate doors into AI search, and they run on completely different clocks. Here's what happened when I pushed one fact through all three and watched the lag.
For about a year I treated "getting cited by AI" as one problem with one knob. Write good structured content, add some JSON-LD, wait. When a page didn't show up in ChatGPT, I assumed I'd written it badly. I'd go back and rephrase headings like a man reorganizing his sock drawer to fix the plumbing.
The mistake wasn't the writing. It was thinking there was *one* door.
There are three. AI reaches your content through training data, through RAG (the live web search a model runs mid-answer), and through an agent fetching a page in real time. They are three separate pipelines wearing one label, and the single most underrated fact about them is that **they run on completely different clocks.** One reaches AI in seconds. One takes one to three months. One takes one to two years.
I got tired of guessing which was which, so I ran a small, dumb experiment: I published the same factual claim through all three paths and timed how long each took to surface in an AI answer.
## The three doors, briefly
Before the stopwatch, the map. (If you want the full version with the optimization playbook per path, I keep it at [llmoframework.com](https://llmoframework.com). This post is the field-notes version.)
**Path 1 — Training data.** The model's "memory." Whatever was baked into the weights during pre-training. When you ask ChatGPT how `useEffect` works and it answers without citing anything, that's the training-data path. No sources, because it's recalling, not retrieving.
**Path 2 — RAG.** The live search a model fires off mid-answer. ChatGPT's web browsing, Perplexity, Google's AI Overviews: all RAG. This is the path that *cites* you. If you've ever seen your URL show up as a little footnote in an AI answer, RAG put it there.
**Path 3 — Agent fetch.** An agent (or a browser-side assistant) pulling a specific page in real time, often outside the search index entirely. My own agent reads the web through Brave's API; Claude in a browser tab can read the DOM of a page you have open. No "search engine" in the loop at all.
Here's the part that reorganized my whole mental model.
## The experiment
I picked one fact — a specific, checkable, slightly niche technical claim from my own work (a measured latency number from a voice-AI build, the kind of thing nobody else had published in that exact form). Then I pushed it out three ways on the same day:
1. Put it in a structured blog post on my own site, with a question-style heading and the number right under it (the RAG play).
2. Made sure the page was reachable by AI crawlers and well-formed enough for an agent to fetch cleanly (the agent play).
3. Did nothing special for training data — because, as you'll see, there's nothing you *can* do that pays off this quarter.
Then I waited, and I kept asking the same question across ChatGPT, Perplexity, and my own agent, logging the first time my number came back.
### Path 3 (agent fetch): same day
My agent had the number within hours, because "having it" just means the page exists and is fetchable. There's no index to wait on, no crawl cycle, no retraining. The agent goes and gets the page when the question comes up. If your content is clean, structured, and not blocked, this path is basically instant.
The catch: instant reach, narrow audience. Agent fetch only helps when *that specific agent* decides to look at *your specific page*. It's a real channel — it's just not a broadcast.
### Path 2 (RAG): a few weeks
This one took longer than "instant" and far less than "training." The fact started showing up in Perplexity answers a few weeks after publishing, once the page had been crawled and indexed by the search backends these systems lean on. This tracks with what the broader data now says: in 2026, freshness is a primary citation signal. Content under 30 days old is pulling an estimated 3.2x more AI citations than older pages, and roughly half of all AI-cited content is now less than 13 weeks old ([Authority Tech](https://authoritytech.io/blog/content-freshness-seo-ai-2026)). RAG systems actively prefer newer sources when accuracy decays over time ([Stellar AEO Labs](https://stellar-ai.co/blog/how-ai-engines-retrieve-and-rank-sources-in-real-time/)).
So RAG is the path with the best effort-to-payoff ratio: weeks, not years, and it's the only one that reliably puts your name in the citation.
### Path 1 (training data): didn't happen, and won't for a long time
Two and a half months in, no model answers my question from memory. And it shouldn't. Training data has a cutoff. A model trained up to some date in 2025 has never seen a thing I publish today. Retraining isn't frequent; the gap from one major model generation to the next has historically run a couple of years. Anything I post this morning lands in the weights, optimistically, six months from now. Realistically, one to two years.
That's not a failure. That's the clock. Training data is the slowest, most durable path — once you're in the weights, you're in the "memory" for the model's whole lifespan. But you do not optimize for it on a quarterly content calendar. You optimize for it by becoming the kind of source the web keeps quoting for years.
## Why this matters more than the usual LLMO checklist
Most LLMO advice is a flat list: add JSON-LD, write an `llms.txt`, use question headings, keep it fresh. All fine. But a flat list hides the thing that actually wrecks people's expectations — **the lag.**
I've watched people publish a great page, check ChatGPT a week later, see nothing, and conclude AI search "doesn't work for them." What actually happened is they shipped a RAG-and-agent asset and then went looking for a training-data result. Wrong clock. It's like planting a tree on Monday and being annoyed there's no shade on Tuesday.
Once you separate the paths by time:
- **Need results this quarter?** You're playing Path 2 and Path 3. Structure for retrieval, stay fresh, stay fetchable. This is where new content earns its keep.
- **Building a brand AI "knows" by default?** That's Path 1, and it's a one-to-two-year investment in being cited, shared, and referenced enough that the next training run can't ignore you.
They're complementary and run on one engine. The same structured, original, genuinely useful page feeds all three; it just *arrives* at three different times. The efficient move is to optimize for RAG first, because that work spills over: a page clean enough for retrieval is clean enough for an agent to fetch, and original enough to get cited is original enough to eventually get learned.
## The honest takeaway
The reason "I wrote a good post and AI ignored it" feels so personal is that we're measuring a tree on a vegetable's schedule. AI doesn't find your page one way. It finds it three ways, on three clocks, and most of the frustration in this space is just someone staring at the slow door waiting for the fast door's result.
Push your fact through all three. Then check the right clock.
---
If you want the full framework — the per-path optimization playbook, the crawler config, and how this connects to citation half-life — that's the whole point of [LLMO: AI Search Optimization](https://kenimoto.dev/books/llmo-ai-search-optimization).
Sources:
- [Content Freshness in 2026 — Authority Tech](https://authoritytech.io/blog/content-freshness-seo-ai-2026)
- [How AI Engines Retrieve and Rank Sources in Real Time — Stellar AEO Labs](https://stellar-ai.co/blog/how-ai-engines-retrieve-and-rank-sources-in-real-time/)
---
# AI Mode Just Hit 1 Billion Users, and Opened a Local-Business LLMO Market Most Engineers Are Ignoring
URL: https://kenimoto.dev/blog/ai-mode-billion-users-local-business-llmo/
Lang: en
Date: 2026-06-23
Description: Google AI Mode crossed a billion monthly users in May 2026. While I was polishing my own site's llms.txt, an entirely separate LLMO market for local businesses was opening up next door. Here's the size of it, and why engineers keep missing it.
In May 2026, Google AI Mode passed one billion monthly active users. One billion. That is one in eight people alive, opening an AI box every month and typing questions into it.
I read that number and my first thought was about my own blog. How do I get cited more? Then I went back to tuning my `llms.txt`, like the hobbyist I am.
It took me embarrassingly long to notice what a billion people were actually asking.
## What this post is not about
If you read my blog you have already seen me write about LLMO for your own site: JSON-LD schemas, answer-first content, getting your articles cited. You may also have seen the store-owner tactics version, the GBP-grinding, review-replying, photo-uploading playbook for a single shop.
This post is neither. I am not here to argue about the SEO tradeoffs of optimizing your personal site for AI. I want to talk about a market: local-business LLMO as a category that is currently spinning up, measured by user volume and growth rate. The opportunity, not the tactics.
Because that is the part I missed for about a year.
## A billion people are asking AI where to eat
Here is the thing about that billion-user number. People do not type "explain transformer attention" into Google AI Mode. A huge share of those queries are the most ordinary requests imaginable: "good ramen near Shinjuku," "back-pain clinic in Umeda," "a quiet cafe near Hakata station with strong Wi-Fi where I can sit alone."
Those are local-business queries. And the answer the AI gives comes, in most cases, straight from a Google Business Profile (GBP): the photos, the category, the reviews, the attributes someone filled in.
The local search base was already enormous before AI touched it. Roughly 70% of people looking for a restaurant use Google Maps. The local pack, those top three shops in the results, takes a 76% tap rate on mobile. Whether your shop sits in that top three or not changes new-customer volume by a multiple.
Now route that demand through a billion-user AI front door, and you have a behavior shift, not a feature update.
## The market nobody put on my radar
In Japan this discipline has a name and a price tag. It is called MEO (Map Engine Optimization, the local label for what the rest of the world calls Local SEO). The market was worth 21.4 billion yen in 2024, with a forecast of 30.6 billion yen by 2028, per a joint study by GMO TECH and Digital InFact. That is roughly $140M growing toward $200M.
The number wobbles depending on who is counting. Yano Research Institute puts it at 12.7 billion yen, because they measure a narrower slice (agency revenue rather than total spend by shop owners). Take either figure and the shape holds: this is a market growing somewhere around 10 to 18% a year.
I want to sit with how dumb I felt reading that. I have spent months on the citation mechanics of one blog: mine. The audience for that work is, generously, a few thousand engineers. Meanwhile a market measured in tens of billions of yen was compounding at double digits one tab over, serving every restaurant, clinic, and salon in the country, and I had filed it under "marketing, not my problem."
The big market was not hiding. It just was not shaped like code, so I walked past it.
## Why engineers keep walking past it
A few reasons, and I plead guilty to all of them.
We optimize what we can measure in a terminal. Your own site's LLMO has logs, structured data, a `curl` you can run. A shop's AI visibility lives in someone else's GBP dashboard and in answers an AI generates differently every time. It feels squishy. So we avoid it.
We assume the GBP work is trivial and therefore not for us. It is mostly not trivial in the way we expect. It is data hygiene at scale: keeping name, address, and phone consistent across listings, mapping attributes to the natural-language queries an AI will actually ask, writing review replies that read like a real human ran the place. That is exactly the kind of structured, repeatable, automatable problem engineers are good at. We just do not see it because it wears an apron.
And we conflate two different LLMOs. Optimizing your own site to get cited is one job. Making a local business legible to AI search is a separate one with a separate buyer, a separate market size, and far less competition. Same three letters, different economy.
## The visibility gap is the opportunity
The fresh 2026 data makes the gap concrete. Consumer adoption went from 6% of people using AI to find local businesses in 2025 to 45% in the past year, which puts AI third as a local-discovery channel, behind only Google and Facebook, ahead of Yelp and TripAdvisor.
Now the supply side. One report this year found that ChatGPT surfaces only about 1.2% of local business locations, and that 83% of restaurants do not show up at all in AI-generated local recommendations. So you have demand at 45% and climbing, and a supply side where the overwhelming majority of businesses are simply absent from the answer.
That spread is the whole pitch. Demand has arrived, the inventory has not been optimized, and the people who could fix it are over in the corner polishing their own `llms.txt`. Hi.
## The mechanism is reassuringly boring
Here is the part that should make engineers comfortable. MEO and LLMO for local businesses are not two separate optimization stacks. They run on the same fuel: the Google Business Profile.
Google ranks local results on three factors it states publicly, relevance, distance, and prominence. The same complete, accurate, well-attributed GBP that lifts those factors is also the primary fact source AI engines pull from when they answer a local query. Tune the profile once and you move both the map pack and the AI answer. They are two wheels on one axle.
So "local-business LLMO" is not a mystical new skill. It is GBP done with the same rigor you already apply to your own pipelines, pointed at a market that will pay for it. If you want a structured way to think about which industries are worth the investment and how AI-search optimization maps onto local discovery, the framework I use lives at [llmoframework.com](https://llmoframework.com), built from running this across nine languages.
## What I actually changed
I stopped treating "AI search optimization" as a single thing I do for my own site. It is at least two markets, and the bigger one was the one I was ignoring because it did not compile.
The fix was not technical. It was looking up from the terminal long enough to notice that a billion people walked through a new door, and most of the businesses they were looking for had not bothered to put up a sign the AI could read.
I had a beautiful sign. On a blog three thousand engineers read. Pointed at no one who was hungry.
---
# AI Mode Cited My Portuguese 3.4× English. Japanese Also Beat English.
URL: https://kenimoto.dev/blog/ai-mode-portuguese-vs-english-cross-language-llmo/
Lang: en
Date: 2026-08-23
Description: For 30 days I logged AI Mode citations on the EN, JA, PT and ES editions of kenimoto.dev. Portuguese was cited 3.4× English, Japanese 1.8×, Spanish 0.7×.
I opened the log on July 22 fully expecting Portuguese to blow the doors off English again. It did. What I did not expect was Japanese quietly beating English on the same run. Japanese, the language whose own kenimoto.dev directory would lose a footrace to a stationary object on human traffic, was outscoring English on AI Mode citations.
I had to stare at the chart for a while before I trusted it.
The raw counts, from 2026-06-22 to 2026-07-22, are what the chart shows: **EN 10, PT 34, JA 18, ES 7**. Same site, same set of translated articles, same window, four different Google AI Mode fronts.
## What the numbers say, plainly
I like a scoreboard. Here is the whole thing before I start explaining it away:
- **Portuguese: 34 citations.** 3.4× the English baseline. This one I saw coming.
- **English: 10 citations.** My baseline, and quietly humbling.
- **Japanese: 18 citations.** 1.8× English. This one I did not see coming.
- **Spanish: 7 citations.** 0.7× English. Also unsurprising, and also mine to fix.
Two months ago I wrote [I Translated My Blog Into 4 Languages. Portuguese Got Nearly 4× the Traffic of English](/blog/four-languages-thirty-days-portuguese-four-x-traffic/), where the ordering was PT ≫ EN ≫ JA ≫ ES with a huge gap under Portuguese. On AI Mode citations, the ordering compresses: PT ≫ JA > EN > ES, and the JA/EN flip is the story.
The pageviews chart made Japanese look dead. The citation chart says the AI retrieval layer is reading it just fine.
## How I actually measured this
I run kenimoto.dev in four language directories: `/`, `/ja/`, `/pt/`, `/es/`. Full `hreflang` cluster, self-referencing canonicals per language, translated slugs. The four directories do not have identical totals — EN and JA are further along than PT and ES on raw article count — so I did not measure the whole blog. I measured the **subset of articles that exists in all four language directories**, matched across languages by the `translation_key` frontmatter field I use for hreflang pairing. That symmetric subset is the whole reason the comparison is legible; if I let one language have more candidate URLs than another, I would just be counting corpus size in disguise.
For 30 days, June 22 through July 22, I ran the following loop:
1. **Per language, one seed prompt list.** For each of EN, JA, PT and ES, I maintained a small set of brand-relevant developer prompts I already know AI Mode should have some reason to point at kenimoto.dev for. Same conceptual set of prompts across languages, translated (not machine-translated at run time; hand-checked so the meaning holds). Not "AI Mode ranking" queries in the SEO sense, but the kind of question a developer would actually type: LLMO, `llms.txt`, Claude Code tradeoffs, harness engineering.
2. **Per language, a locale-set Google account.** Language and region set to the target (US-EN, JP-JA, BR-PT, MX-ES). Same query cadence per language per day.
3. **Log the citation, not the answer text.** For each AI Mode response, I captured the cited URLs. A citation counts once per URL per day, regardless of whether the same article gets cited on multiple prompts that day. Deduped daily so a hot prompt does not inflate a single article.
4. **Only kenimoto.dev URLs.** Third-party citations were ignored for this pass. The question I was asking is "does my own multilingual publishing produce differential citation rates across languages," not "who else is winning."
Thirty days of that got me the numbers on the chart: 10, 34, 18, 7.
Two things I want to flag before anyone builds a spreadsheet on top of this:
- **Small numbers.** Ten citations on the English side is not a large sample. The direction of the effect is trustworthy; the exact multiples are not the point.
- **Ken's site, Ken's prompts.** These are my seed prompts about the topics I write about. Ratios will not transfer verbatim to your site. What I hope transfers is the shape of the surprise.
## Why Portuguese ran away with it, again
Most of the reasons Portuguese wins on human traffic also win on AI Mode citations, so this one was not a mystery. The AI-search field in Portuguese is thinner than in English by a wide margin. Fewer competing PT sources per prompt means the ceiling for any single reasonable PT source is higher, and kenimoto.dev's `/pt/` directory has been getting cited in that thinner field for months.
The mechanism I described in [I built the site in four languages, and AI search cited the wrong one anyway](/blog/ai-cites-wrong-language-version-multilingual-llmo/) still applies here in reverse: when the retrieval layer *does* select the right localized URL, the win is amplified because the competing local corpus is small. English competes with the whole English-speaking internet. Portuguese competes with a much smaller pool of technical Portuguese blogs.
Nothing subtle here. If you have ever wondered why LLMO practitioners keep quietly translating their sites, this is the reason: the tailwind in less-saturated languages is real and boring.
## The Japanese rebound is the actual story
This is the part that made me sit up. On raw human pageviews, Japanese has been the runt of the four. Japanese developers live on Qiita and Zenn, and my standalone `/ja/` directory does not compete with those platforms for human clicks. I have written about this elsewhere and I am at peace with it, mostly.
AI Mode does not seem to share the reader's habitat bias. It cited `/ja/` URLs 18 times in 30 days, nearly twice as often as `/en/`, on prompts issued from a Japanese-locale account. It clearly reached past the "everybody reads Zenn" heuristic and pulled canonical Japanese content from kenimoto.dev.
A few things line up to explain it, and I want to be honest about which are plausible versus which I am just hoping for:
- **Corpus quality on the JA side is high.** I write the Japanese versions myself, natively, not through translation. If retrieval quality favors clean prose, Japanese should over-index for me relative to English, where I am a competent but not native writer.
- **JA-locale competition on niche developer prompts is not English-thin, but it is Zenn/Qiita-shaped.** Both are strong sources, but they do not always surface the specific narrow-topic angle I write about, which leaves room for a canonical off-platform Japanese source to slot in.
- **The technical vocabulary in JA is close to English.** Terms like "Claude Code," "harness," "llms.txt" appear verbatim in the JA text, which likely helps cross-language retrieval anchors line up on the same concept.
I am not claiming this is a durable moat. What I am claiming is that citation asymmetry across languages does not have to look like traffic asymmetry across languages. It is a genuinely different game with a genuinely different scoreboard, and I had been treating them as the same one.
## Spanish is my fault
Seven citations on the Spanish side is roughly the outcome I earned. The `/es/` directory has fewer articles, thinner cross-linking, and I do not have a Spanish equivalent of TabNews sending humans to it so the ambient signal stays quiet. The ES retrieval-layer picture mirrors the traffic picture: not enough weight for AI Mode to lean on.
The take-away is not "Spanish is unsalvageable." It is that language-parity of the underlying files does not automatically buy you language-parity of AI Mode citations. You still have to build the surrounding signal, and I have not yet.
## What this changes about how I publish
Before this measurement, my mental model was: publish in EN and PT hardest, JA for personal reasons, ES because the pipeline was already there. Traffic backed that ordering.
After this measurement, the ordering rebalances a little:
- **PT stays first.** The tailwind is real in both traffic and citations.
- **JA moves up.** If AI Mode is going to cite the Japanese version at 1.8× the English rate, then the JA edition is worth more per unit effort than my "27 pageviews" instinct suggested. It just cashes in through a different door.
- **EN stays as the reference.** The saturated market is still saturated. A marginal EN article competes with everyone.
- **ES needs signal, not more files.** Adding more Spanish articles into an unlit room does not turn on the lights. I need something to make the surrounding graph load-bearing before more articles help.
I built the four-language site because I wanted more readers. It turns out the AI retrieval layer was also reading, just on a different curve than I was measuring. Two months ago I thought I had shipped one experiment. Turns out I had shipped two, and only one of them was on the scoreboard I was watching.
## Take-away, said plainly
If you already publish in more than one language: measure AI Mode citations per language separately, and do not let human-traffic asymmetry be your proxy for LLMO asymmetry. The two curves diverge, and the divergence is where the interesting decisions live.
If you are about to publish in more than one language: symmetry of the underlying pool of translated articles is what makes this measurable at all. Ship the same article set across languages if you want to be able to compare anything later; asymmetric corpora make citation-rate deltas meaningless.
And if, like me, you had quietly written off one of your four languages as "the one that does not matter": run this measurement before you actually cut it. The retrieval layer may be voting for it even while the humans are not.
---
If you want the full playbook on measuring AI-search visibility across languages, including the `hreflang`, `llms.txt` and per-language `og_image` setup I use on kenimoto.dev, I wrote a book on it: [LLMO: AI Search Optimization](https://kenimoto.dev/books/llmo-ai-search-optimization). The multi-language chapter is the one that survived the most rewrites.
---
# AI Reads Your Chunks, Not Your Page: I Promoted 9 Sections from H3 to H2 and Watched Which Ones Got Quoted
URL: https://kenimoto.dev/blog/ai-reads-chunks-not-pages/
Lang: en
Date: 2026-06-18
Description: AI search engines don't quote your page. They quote chunks of it, and your heading hierarchy decides where those chunks get cut. I took 9 buried H3 sections, promoted them to H2, and tracked which ones started showing up in AI answers. Here's what the headings did.
I spent a week last month doing something that, written down, sounds like a cry for help: I went through my own blog and changed nine `###` into nine `##`. No new sentences. No new facts. I just promoted nine sections from H3 to H2, pushed the change, and then watched five AI engines for three weeks to see which of those sections started getting quoted. The thing I was testing was almost too dumb to admit out loud. It turned out to be the most useful afternoon of formatting I've done all year.
Here's the idea I was chasing. When I ask an AI search engine a question and it cites my site, it almost never quotes the whole page. It lifts a piece: one paragraph, one table row, one code block, one section under one heading. And if AI quotes pieces, then the question that actually matters is not "how good is my page," it's "where does my page get cut into pieces, and is the good part its own piece or buried inside a bigger one." Headings, it turns out, are the scissors.
## Why the page stopped being the unit
The page stopped being the unit the moment retrieval started happening at the chunk level. Brave's [LLM Context API](https://brave.com/search/api/), which shipped in early 2026, is the clearest window into this I've found, because Brave documented its pipeline instead of leaving me to guess. It runs a normal web search to find pages, then does "deep content extraction" that breaks each page into smart chunks, then ranks those chunks, and then ships the top ones to the model. The granularity it names is not the page. It's the paragraph, the table row, and the code block.
Sit with that for a second. The ranking step that decides whether your content reaches the model operates on fragments, not URLs. Your beautiful, internally-linked 3,000-word guide does not compete as a guide. Its paragraphs compete as paragraphs, against paragraphs from other sites, most of which the model will never even know shared a page with yours. You are not entering a horse in a race. You are entering each of the horse's legs separately and hoping at least one of them finishes.
So the practical question becomes: what decides where one chunk ends and the next begins? Some of it is the model's own segmentation, which I can't touch. But a large, free, embarrassingly controllable part of it is the heading structure, because headings are the most obvious topic boundary in any document. A new heading is a new "this is a different thing now" signal, and chunkers love that signal because it's cheap and reliable.
## What promoting H3 to H2 actually changed
Promoting a section from H3 to H2 changes its status from "detail inside something else" to "thing in its own right." That is the entire move, and it matters because of how nesting reads to a machine. An H3 sitting under an H2 is, structurally, a sub-point. The chunker is more likely to glue it to its parent or to the sibling H3s around it, producing one fat chunk that says "here are six considerations" instead of six lean chunks that each say one quotable thing. Promote that H3 to H2 and you've told every parser in the pipeline that this section stands alone. It becomes its own candidate. It gets its own shot at the ranking step.
This lines up with what the citation research keeps reporting. One 2026 analysis of content structure for AI retrieval found that [LLM citation rates rise about 2.2x with a clear hierarchical heading structure](https://writesonic.com/blog/how-to-structure-content-for-llms-citation-and-retrieval), simply because clean hierarchy makes content easier to parse and extract. The same body of work keeps finding that question-shaped headings get pulled as candidate answers far more often, because the models were trained on Q&A-shaped text and a heading that matches how someone phrases a query is a flare going up over the answer.
I should be honest about what I did and didn't change, because "I rewrote for AI" has become a sentence that means nothing. I did not touch JSON-LD. I did not change publish dates or add freshness signals. I did not rewrite the prose under the headings. On the nine sections I touched, I changed exactly two things: the heading level, and the heading text, which I rewrote from a label into a question wherever a real person might actually type that question. "Caching strategy" became "How do I cache API responses without serving stale data." Same paragraph underneath. Different sign over the door.
## The 9 sections, and the 6 that moved
Of the nine sections I promoted, six started showing up as citations within three weeks. Three didn't move at all. I ran the same 20 prompts across ChatGPT, Perplexity, Gemini, Claude, and Brave every few days and logged which of my sections got lifted. I want to flag the obvious caveat before anyone with a statistics degree does it for me: n=9 on one small blog over three weeks is a field note, not a finding. AI citations also drift on their own for reasons I can't see, so some of this is noise in a lab coat. Treat it as one engineer's measurement rather than a law.
The six that moved had a trait in common, and it wasn't the H2 by itself. Each one answered a real question in its first sentence and then stayed on that single question for the rest of the section. The promotion gave them a clean boundary; the self-contained content gave the chunker something worth keeping inside that boundary. The heading opened the door and the first sentence was standing right there when the engine knocked.
The three that didn't move taught me more, the way the five that failed in my [answer-first experiment](/blog/answer-first-7-of-12-cited/) did. Two of them were now H2 sections about topics nobody queries: "On the philosophy of caching" is not a question anyone types, so promoting it just gave a clean boundary to a chunk no one was searching for. The third was an H2 whose section wandered across three subtopics, so even with a standalone boundary, the chunk it produced was a mush that answered none of the three questions cleanly. The lesson was blunt: a good boundary around bad content just produces a well-labeled chunk that still loses.
## Heading density is the part nobody talks about
Once I started thinking in chunks, I noticed the thing underneath the H2-vs-H3 question, which is heading density: how many words you run between headings. This is the dial that nobody mentions because it's boring, and it might matter more than the level.
If you write 800 words under one heading, you've handed the chunker one enormous chunk to either keep whole, which blows the token budget, or split arbitrarily down the middle of your argument, which is worse. If you put a heading every 150 to 250 words, you've pre-cut the page along the seams *you* chose instead of the seams a segmentation model guesses at. You become the one holding the scissors. I now aim for a heading roughly every 200 words on anything I want quoted, not because 200 is magic but because it keeps each chunk down to a single idea that can stand alone, which is the whole game.
There's a failure mode at the other end, and I walked into it. After this experiment I got greedy and over-chunked a page into headings every 40 words, which fragmented one coherent explanation into seven gasping little stubs, none of which said anything complete. The engines quoted none of them. A chunk still has to be a whole thought. Headings decide where the cuts land; they don't excuse you from having something worth cutting.
## What I actually do now
The routine I landed on is short enough to fit in my head. Before publishing anything I want AI to quote, I list the real questions a person would type to land on it, I make sure each of those questions is an H2 and not an H3 buried two levels down, I answer the question in the first sentence under each heading, and I keep each section to one idea and a couple hundred words. That's it. No schema gymnastics required to get started, though structured data and the rest of the implementation layer still earn their keep once the structure is right. If you want the structural side written up properly, with the heading and chunk-boundary patterns laid out as an implementation guide, the [LLMO framework reference](https://llmoframework.com) collects them in one place, and it's where I send people who want the spec rather than the war story.
The reframe that stuck with me is this: stop writing pages for an AI to read and start writing chunks for an AI to lift. Your reader still gets a page, scrolling top to bottom like a civilized person. But the machine deciding whether to cite you is reading a pile of fragments, and your headings are the only part of the cutting you get a vote in. I spent years optimizing for machines that can't laugh. The least those machines can do is quote me correctly, and it turns out the way to make them is to cut the page up myself before they do it for me.
---
If you want the full field guide to being seen by AI search engines — from chunk-friendly structure to JSON-LD, llms.txt, and citation-rate KPIs — I wrote it all down in [Why ChatGPT Ignores Your Website](https://kenimoto.dev/books/llmo-ai-search-optimization).
---
# My Best Page Went Stale in a Month: Why AI Search Rewards Freshness, Not Just Schema
URL: https://kenimoto.dev/blog/ai-search-rewards-freshness/
Lang: en
Date: 2026-06-07
Description: I shipped clean JSON-LD and a tidy llms.txt, then watched my top-cited page lose more than half its AI citations in about a month. Freshness is a ranking input, not a one-time setup. Here is what actually moved the needle, and why changing the date alone made it worse.
I did everything the LLMO checklists told me to. JSON-LD on every page, a hand-curated llms.txt, question-shaped headings, self-contained passages an AI could lift without context. The page that came out of that work got cited by ChatGPT, Perplexity, and Gemini within two weeks. I screenshotted it. I felt like I had solved a thing.
About a month later, the same page was barely cited at all. I had not deleted it. I had not changed the URL. Google still sent it the same trickle of search traffic it always had. But the AI engines had quietly moved on to fresher sources, and my carefully structured page was now the equivalent of a restaurant nobody walks into anymore.
That is the lesson I want to save you a month on: **schema gets you in the door, but freshness decides whether you stay in the room.**
## Schema is the table. Freshness is whether the food is still warm
Here is the mental model I wish I had started with. Structured data, llms.txt, clean headings: those build the table and set the silverware. They make your content legible to a machine that parses pages into fragments. But a set table does not make anyone eat. The AI engine still chooses *which* dish to serve, and when two dishes answer the same question, it reaches for the one that came out of the kitchen most recently.
This is not a vibe. The retrieval step in front of every AI answer treats recency as a filter. Across ChatGPT, Perplexity, and Google AI Overviews, content updated in the last 30 to 90 days gets cited at meaningfully higher rates than older pages, and roughly half of all AI-cited content is [less than 13 weeks old](https://authoritytech.io/blog/content-freshness-seo-ai-2026). Pages under 30 days old earn an estimated 3.2x more citations than older ones. Perplexity is the strictest: one analysis found it cited content from the [last 30 days at an 82% rate](https://www.demandlocal.com/blog/content-freshness-ai-rankings/), and a six-month-old post loses to a fresh one on the same topic almost every time.
ChatGPT mixes recency with authority: 76% of its top-cited pages are under 30 days old when freshness is relevant, but it still pulls from 2022 or earlier when authority outweighs recency. Google AI Overviews has the weakest freshness bias of the three, which tracks with the fact that it leans on traditional ranking signals. So the leverage is uneven, but the direction is the same everywhere: **old loses to new when the answer is otherwise a tie.**
## The part that actually stung: the date trick backfired
My first instinct was the lazy one. If freshness is a signal, I will just bump the `dateModified` field, redeploy, and reclaim my citations without rewriting anything. I genuinely believed this would work for about an afternoon.
It did not. Worse, it seemed to do active harm. The engines can tell when the body text has not changed. If the timestamp says "updated yesterday" but the actual words are identical to last quarter, the page reads as stale *and* dishonest. You get the worst of both: no freshness credit, and a small ding to the trust that made you citable in the first place. Changing the date without changing the content is the SEO equivalent of putting a new "best before" sticker on the same old milk. The carton knows.
What actually moved the needle was boring and real: I rewrote 10 to 15 percent of the page. New 2026 numbers replacing the 2025 ones. A fresh example I had actually run. A paragraph cut because the tool it described no longer existed. Adobe's LLM Optimizer recommends exactly this cadence, [refreshing 10 to 15 percent of page content on a schedule](https://www.quattr.com/blog/content-freshness), and SurferSEO's data backs the threshold: below it, the engines detect no real change and keep treating the page as old.
## A refresh cadence I can actually keep
The trap in all of this is turning your blog into a treadmill where you re-edit everything forever. That is not sustainable, and most of your pages do not need it. So I stopped treating freshness as a sitewide chore and started treating it as a triage problem. Different pages run on different clocks:
- **Commercial and high-traffic pages**: every 60 to 90 days. These are the ones competing in crowded answer spaces where a tie goes to the freshest source.
- **Evergreen guides and pillar content**: roughly every 6 months. Substantial, not cosmetic.
- **Reference and definition pages**: once a year is fine. "What is a webhook" does not change, and the engines know it.
This tiering comes straight out of the freshness research, and it is the single thing that made the workload survivable. I keep a tiny spreadsheet: page, tier, last *real* update. When a page is overdue, it goes on the list. When I refresh it, I am refreshing content, not the clock.
If you want the larger operating model this fits into, I lean on the continuous-operation framing in the [LLMO Framework](https://llmoframework.com), which treats refresh cadence as a maintenance phase rather than a launch-day task. The setup work (structured data, llms.txt) is phase one and you do it once. The freshness loop is the phase nobody warns you about, and it never ends.
## What I would tell my one-month-ago self
Three things, in order of how much regret they saved me.
First, **freshness is an input, not a vanity metric.** It sits upstream in the retrieval filter, deciding what even gets considered. All the snippability in the world does not help a page that never makes the shortlist because three newer sources answered the same question.
Second, **never touch the date without touching the words.** It does not fault gracefully. The downside is real and the upside is zero.
Third, **the thing AI cannot fake is the thing worth refreshing.** When I update a page, the highest-value addition is almost always a result I personally measured: a number from my own logs, an experiment that broke in an interesting way. Generic prose ages into noise. First-hand experience is the part an engine keeps coming back for, because it cannot generate it from anywhere else.
I still ship the schema. I still maintain the llms.txt. But I stopped thinking of LLMO as a thing you finish. It is a thing you keep warm. My once-stale page is back in rotation now, not because I out-clevered the algorithm, but because I fed it something it had not seen before.
If you want the full playbook for getting cited in the first place, from llms.txt and JSON-LD to citation-rate KPIs, I wrote a field guide for exactly that: [Why ChatGPT Ignores Your Website](https://kenimoto.dev/books/llmo-ai-search-optimization).
---
# I Rewrote 12 Pages to Answer the Question in the First Sentence. AI Started Quoting 7 of Them.
URL: https://kenimoto.dev/blog/answer-first-7-of-12-cited/
Lang: en
Date: 2026-06-11
Description: I took 12 of my own pages, deleted the throat-clearing, and made the first sentence the actual answer. Then I watched which ones AI engines started citing. Seven moved. Five didn't. Here's what separated them.
I have spent more of my life optimizing for machines that can't laugh than I'd like to admit, and last month I added a new entry to that ledger: I rewrote the opening sentence of 12 of my own pages so that the very first line answered the question in the heading, instead of warming up to it like a man clearing his throat before a toast.
The hypothesis was almost insultingly simple. If AI engines lift the first sentence or two of a section to build their answers, then burying the answer under a paragraph of context is the writing equivalent of hiding the punchline behind the napkin. So I stopped doing that on 12 pages and watched what the engines did. Seven of them started getting cited more. Five did not move at all. The gap between those two groups turned out to be the actual lesson, and it was not the lesson I expected to write down.
## What "answer-first" actually means
Answer-first means the first sentence of a section is the answer to the question that section's heading implies, with the explanation coming after instead of before. That's the whole tactic. No schema markup, no freshness signals, no passage selection at the retrieval layer. Just the order in which you put your own words.
I want to be precise here because "write for AI" has become a phrase people say to mean nothing. I did not touch JSON-LD on these pages. I did not change publish dates. I did not add headings or rewire internal links. I changed exactly one thing per section: I moved the sentence that actually answered the heading to the front, and I cut whatever ran ahead of it. If a section started with "There are many factors to consider when choosing a tracker, and the landscape has shifted a lot recently," that sentence died, and the line that named the answer took its place.
The reason this matters lines up with what citation studies keep finding. One analysis of where LLM citations land inside a page reported that [44.2% of citations come from the first 30% of the text](https://writesonic.com/blog/how-to-structure-content-for-llms-citation-and-retrieval), with the middle and the conclusion splitting the rest. If almost half the citations are harvested from the top third of your page, then the top of each section is the most expensive real estate you own. I had been renting it out to throat-clearing.
## How I measured it
I ran the same 30 prompts across ChatGPT, Perplexity, Gemini, Claude, and Brave AI every Monday for six weeks: three weeks before the rewrite, three weeks after, same prompts, same engines, same Monday-morning ritual. I logged how often each of the 12 pages showed up as a clickable citation. I kept the prompts frozen so the only thing changing was the writing.
Two caveats, because I have been burned by my own optimism before. Six weeks is short, and AI citations decay on their own schedule, so some of this is noise wearing a lab coat. And n=12 is a sample size that would make a statistician politely change the subject. This is one engineer's measurement on one small blog, not a study. Treat it as a field note, not a law.
## The 7 that moved
Here is the rewrite that worked, stripped down to the bones. Same facts in both versions. Only the order changed.
```text
BEFORE (context-first):
"When teams ask me how to track AI citations, I usually start by
explaining that the tooling space is young and the numbers vary
wildly between tools, which is itself a finding worth sitting with."
AFTER (answer-first):
"Track AI citations by running the same prompts on a fixed schedule
and logging which pages get cited. The tooling is immature, so a
fixed-prompt manual run beats most trackers for accuracy right now."
```
The seven pages that gained citations all shared one trait: each had a section whose heading was a real question a person might type, and whose answer fit cleanly into one or two front-loaded sentences. "How do I track AI citations." "What is the difference between page rank and passage rank." "Why did my traffic stay flat while citations dropped." Concrete questions with concrete, liftable answers. Once the answer sat at the top, the engines could grab it without having to understand my paragraph structure, and grab it they did.
The passages that did best landed in the 40-to-75-word range, which is roughly [the length of passage that ChatGPT, Perplexity, and Google AI Overviews tend to quote](https://kime.ai/blog/how-to-structure-content-for-llm-extraction-geo-guide-2026). Short enough to lift whole, long enough to stand on its own. I did not engineer that length on purpose at first. The pages that happened to hit it were the ones that won, which is how I learned to aim for it on the rest.
## The 5 that didn't, and why that's the real finding
The five pages that ignored my efforts taught me more than the seven that obliged. They had one thing in common: their headings weren't questions anyone would ask, so there was no question for the first sentence to answer.
A heading like "On the philosophy of measurement" is not a query. Nobody types it. When I "fixed" the first sentence under it, I was answer-firsting a question that didn't exist, which is a bit like leaving the porch light on for a guest who was never invited. The mechanics of the rewrite were fine. The target was imaginary.
So the finding underneath the finding is this: answer-first is not a writing trick you apply to sentences. It only works when the heading above the sentence is a question someone actually asks. Two of those five pages I later rewrote at the heading level, turning a vibe into a question, and two of them then started getting cited. The fifth is about my feelings on a deprecated framework and deserves its obscurity.
## What I'd tell you to do on Monday
Pick your ten most important pages and read only the first sentence of each section, out loud, ignoring everything after it. If that one sentence doesn't answer the heading, you have found a section that is invisible to AI extraction no matter how good the paragraph below it is. Promote the answer. Delete the run-up. For the structural side of this, on how to make passages snippable and self-contained rather than context-dependent, the implementation guide at [llmoframework.com](https://llmoframework.com) is the reference I point people to, because the order of your sentences is only half the job and the shape of your passages is the other half.
And if you want the version of this argument with the retrieval mechanics underneath it, I wrote about [why passages get cited instead of pages](/blog/passage-rank-beats-page-rank-ai-citations/) separately. That piece is the "why." This one is the "I tried it and five of them laughed at me."
## The takeaway
Front-loading the answer moved 7 of my 12 pages and left 5 untouched, and the 5 failures clarified the rule better than the wins did: answer-first only pays off when there is a real question for the answer to answer. The tactic is one sentence. The discipline is making sure that sentence has a job. I spent six weeks proving something a good editor would have told me for free, but at least now I have the citation logs to make it look like science.
---
If you want the full system behind this, including structured data, freshness, and passage design, I wrote a book on it: [LLMO: AI Search Optimization](https://kenimoto.dev/books/llmo-ai-search-optimization).
---
# Anthropic frontend-design skill: #F4F1EA Named
URL: https://kenimoto.dev/blog/anthropic-frontend-design-skill-rewrite/
Lang: en
Date: 2026-06-27
Description: Anthropic frontend-design skill: the +39/-26 rewrite named #F4F1EA cream as its own AI default and replaced 'be extreme' with a critique loop.
Anthropic quietly rewrote their `frontend-design` skill on June 18 in commit [`423563cf`](https://github.com/anthropics/claude-code/commit/423563cf). The new version contradicts the old one on its central thesis, and names three specific AI-generated design clichés in the public plugin documentation. One of them with a hex code.
The `SKILL.md` file diff is **+39 / -26** lines (commit-wide it's +41/-28; the rest is a `marketplace.json` bump and the plugin version going to 1.1.0). On paper, a maintenance bump. In reality, a philosophy reversal.
I noticed this while reviewing my own image-generation skills last week against the upstream [`anthropics/claude-code` version of the file](https://github.com/anthropics/claude-code/blob/main/plugins/frontend-design/skills/frontend-design/SKILL.md). It seems to have slid in under the radar, which is a shame because it's one of the more interesting design-engineering shifts Anthropic has shipped recently. Here's what changed and why it matters if you ship UI that touches a model.
## The old version told the model to be bold
The old skill's central instruction was extreme. From the file Anthropic had been shipping until June 18:
> Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc.
And closed with:
> Don't hold back, show what can truly be created when thinking outside the box and committing fully to a distinctive vision.
The structure of the old document was a list of axes to push: Typography, Color, Motion, Spatial Composition, Backgrounds. Each axis got its own "be distinctive here" paragraph. The implicit instruction was: maximize boldness across every axis simultaneously.
It reads like a pep talk. Imagine running it on every UI generation in your product. Now imagine the output.
## The new version tells the model to be restrained
The new skill opens with a completely different frame:
> Approach this as the design lead at a small studio known for giving every client a visual identity that could not be mistaken for anyone else's. This client has already rejected proposals that felt templated, and is paying for a distinctive point of view: make deliberate, opinionated choices about palette, typography, and layout that are specific to this brief, and **take one real aesthetic risk you can justify.**
Note the word `one`. The new file uses it again later, more directly:
> **Spend your boldness in one place.** Let the signature element be the one memorable thing, keep everything around it quiet and disciplined.
And then closes the restraint section with an aphorism widely attributed to Coco Chanel:
> Consider Chanel's advice: before leaving the house, take a look in the mirror and remove one accessory.
The reversal is precise. Old: pick an extreme on every axis. New: pick one signature, keep the rest quiet, then remove one more thing before you ship.
If you read both versions back to back, the old one reads (to me) like it was written for the demo. The new one reads like it was written by someone who has now sat with a year's worth of "be bold everywhere" outputs and noticed they all converged. I have no insider information on intent — this is just how the two documents land if you read them in sequence.
## They named the three defaults. With a hex code.
This is the part I did not expect.
The new file contains this paragraph:
> AI-generated design right now clusters around three looks:
> (1) a warm cream background (near #F4F1EA) with a high-contrast serif display and a terracotta accent;
> (2) a near-black background with a single bright acid-green or vermilion accent;
> (3) a broadsheet-style layout with hairline rules, zero border-radius, and dense newspaper-like columns.
To translate: Anthropic is publicly stating, in their own plugin docs, the hex value of the cream background their model defaults to. They followed it with this carefully worded line:
> All three are legitimate for some briefs, but they are defaults rather than choices, and they appear regardless of subject.
That sentence is doing real work: it isn't saying "these are bad designs." My read is closer to "these are what we ship when no one is steering us." Either way, the team is auditing their own output and writing the audit into the public docs.
This is essentially the visual-design equivalent of the AI Slop word lists that text-side teams have been maintaining for a year. To make the parallel concrete, here is what the three defaults look like rendered as actual product hero sections. Same fictional product (an audit tool called Lumen, naturally), three default treatments.
*Cliché 1: `#F4F1EA` cream, a Playfair-style italic serif, terracotta accent. Reads "editorial sophistication" at a glance, reads "every AI-generated landing page I've seen this quarter" two seconds later.*
*Cliché 2: near-black background, single bright accent, monospace details, "trusted by" strip. First impression: "edgy modern tech." Closer look: indistinguishable from the last three YC Demo Day landing pages you visited.*
*Cliché 3: broadsheet style, hairline rules, zero border-radius, dense columns. Signals "serious and intellectual," then immediately falls back into the AI startup "About" page genre.*
None of these are bad. They are all competent, defensible, ship-ready. They are also default behavior, which means they read as templated regardless of what the underlying product actually does.
## The process got replaced with a loop
The other major structural change is in how the skill instructs the model to *work*. The old version had a list of axes. The new version has a process.
```text
Process: brainstorm, explore, plan, critique, build, critique again
```
The expanded instructions describe a five-step loop:
1. Read the brief. If it's vague, pin a subject, audience, and the single job the page must do.
2. Build a compact token system: 4-6 hex values, 2+ type roles, a layout described in prose + ASCII wireframe, and a *signature* element.
3. Critique the plan against the brief. Anywhere it reads like "the generic default you would produce for any similar page," rewrite it and say what changed.
4. Build it.
5. Take a screenshot and critique your own output.
This is, structurally, code review applied to design. Plan → diff against the spec → implement → self-review. The skill is essentially asking the model to do its own design critique as a first-class step, with explicit instruction to flag any place where it would have produced the same thing for any other brief.
Whether the model can actually do this self-critique reliably is a separate question. But the *intent* — "audit your own defaults as part of the work" — is a notable shift from "execute the brief."
## Copy got promoted to design material
The other new section is "More on writing in design." It did not exist in the old version. The opening line is:
> Words appear in a design for one reason: to make it easier to understand, and therefore easier to use. They are design material, not decoration.
The rules are practical. Some I want to lift directly:
- A button says exactly what happens. "Save changes," not "Submit."
- The same verb threads through the whole flow. The button labeled "Publish" produces a toast that says "Published."
- Errors don't apologize. They state what happened and how to fix it.
- An empty screen is an invitation to act, not a mood.
If you've ever fought with a product team about whether UX copy is "the designer's job" or "the engineer's job," Anthropic has just put it on the design skill's responsibility list.
## What restraint actually looks like
Here's the same Lumen hero, rebuilt with the new skill's philosophy. Navy monochrome, one signature element: the word "One." set enormous, taking the entire vertical. Everything else quiet and disciplined.
*The "after" treatment. Navy text on near-white, the single word "One." set giant in a display serif as the one memorable visual moment, everything else (nav, italic tagline, body copy, CTA) deliberately quiet. Image's job: prove that "spend boldness in one place" is a real layout choice, not a slogan.*
The signature here is a typographic moment, but the recipe generalizes: pick one axis (color, type, layout, motion, decoration) to push hard, then *withdraw* on every other axis. The trap the old skill set was telling the model to push on every axis at once, which paradoxically forces convergence on the safest combination of attacks. The new skill explicitly diagnoses this in the line "Spend your boldness in one place."
## What this means if you ship UI through a model
A few practical takeaways.
**Check your generation prompts and skills against the new version.** If your prompt has language like "be bold," "be distinctive," "push the design," you are likely producing one of the three named clichés. The fix is to specify *one* axis to push and explicitly constrain the rest.
**The hex-code audit is borrowable.** "If the output background is near `#F4F1EA` and the accent is terracotta, flag it" is a check you can actually write. The same shape works for the other two clichés (near-black with an acid-green accent, broadsheet hairlines on white) — pick your own threshold values for those, Anthropic only spec'd the cream one to the hex. It's the visual equivalent of grep-ing for "delve" in LLM output.
**Bring copy into the design pipeline.** If your design tooling doesn't include button labels, empty-state text, and error messages as first-class artifacts, you're shipping a stale split of responsibilities. Anthropic just promoted copy to "design material" in their public docs.
**Build a critique step into your generation loop.** The "plan, build, critique, build again" pattern is portable. You can wrap any single-shot generation in a self-review pass that explicitly asks "what would you have produced for any similar brief, and how does this differ?"
## The summary
If I had to compress the new skill into one sentence: *AI-generated design fails by being bold everywhere; the fix is to be bold in exactly one place and remove one accessory before shipping.*
My take: Anthropic is auditing the defaults their own model produces and writing the audit into the public plugin docs. That's a useful thing to read, and an unusually honest move to make where everyone can see it.
The full diff is at [commit 423563cf](https://github.com/anthropics/claude-code/commit/423563cf) if you want to read it cold. It's worth ten minutes.
If you can read JP and want the systematic version of this same theme, I have a Zenn Book about it: [_Why is all AI-generated UI blue? An escape guide from sameness_](https://zenn.dev/kenimo49/books/ai-slop-escape-guide).
## References
- [`anthropics/claude-code` — frontend-design `SKILL.md` (current, post-June 18 rewrite)](https://github.com/anthropics/claude-code/blob/main/plugins/frontend-design/skills/frontend-design/SKILL.md)
- [`SKILL.md` pinned to commit `423563cf` — the exact version this article quotes](https://github.com/anthropics/claude-code/blob/423563cf/plugins/frontend-design/skills/frontend-design/SKILL.md) (use this if the file on `main` drifts later)
- [Commit `423563cf` — the +39/-26 rewrite this article is reading](https://github.com/anthropics/claude-code/commit/423563cf)
---
# Article Schema Alone Didn't Make AI Recognize Me as the Author. The Entity Wiring That Did (in 4 JSON-LD Fields).
URL: https://kenimoto.dev/blog/article-schema-alone-author-entity-4-json-ld-fields/
Lang: en
Date: 2026-07-13
Description: Article schema was on 239 pages. AI still cited kenimoto.dev, not me. The fix was 4 fields: author.@id, sameAs, knowsAbout, and Person schema on /about. Perplexity started using my name in 3 weeks.
I added Article schema to 239 pages. AI still cited the site instead of me.
Perplexity would write "according to kenimoto.dev" as if the domain wrote itself. ChatGPT would say "one blog post explains..." with no author. Google's AI Overviews attributed the same paragraph I published under my byline to "an article on the site." Three different agents, three different ways of pretending I don't exist.
The Article schema was correct. `author.name` was set. The byline was on the page. And none of it made the AI treat me as an entity worth citing by name.
This is a follow-up to the "11 schemas, only 3 worked" post I wrote earlier. That earlier post covered which schema types survived AI ingestion. This one zooms into one of the three that did: Article schema was passing validation, but the `author` field was a dangling string. It named a person the AI had no way to resolve as a real entity.
The fix was 4 JSON-LD fields. Three weeks later, Perplexity started using my name.
## What "Article schema" actually gives you (and what it doesn't)
Here is the Article schema I had on every post:
```json
{
"@context": "https://schema.org",
"@type": "TechArticle",
"headline": "Some Post",
"author": {
"@type": "Person",
"name": "Ken Imoto"
},
"datePublished": "2026-04-01",
"dateModified": "2026-04-01"
}
```
This validates. Google's Rich Results Test is happy. Schema.org's validator is happy. And it tells any AI crawler nothing useful about who "Ken Imoto" is.
Here is what I did not realize for six months: the `author` field is a **local** Person object, scoped to the article it sits on. Nothing connects the Person on post A to the Person on post B. From the AI's perspective, 239 articles about various topics each mention a person named "Ken Imoto." Whether it's the same Ken Imoto is left as an exercise for the reader. (The reader is a language model. It does not do exercises.)
Article schema on its own says something about the article. It says nothing about who wrote it.
## Why the AI cares (and what "entity" means here)
Modern AI search (Perplexity, ChatGPT's browsing, Google AI Overviews, Brave Leo) pipes retrieved passages into a generation step. During generation, the model has to decide how to attribute the passage. Its options, roughly:
1. Cite the URL only ("according to a blog post at kenimoto.dev")
2. Cite the site brand ("kenimoto.dev says...")
3. Cite the author by name ("Ken Imoto reports...")
Option 3 requires the model to be **confident** that a Person entity exists, is stable, has a name, and is the author of this specific passage. Without that, it defaults to option 1 or 2. Silence about the author is a downgrade to a weaker citation shape, and it reads as neutral behavior only because you don't see the demotion happening.
Entity resolution here works the way it works everywhere in AI: give the model enough triangulating facts about a thing, and it starts treating that thing as a first-class node. Give it a floating name inside a JSON-LD blob, and it treats the name as string metadata.
## The 4 fields that changed the shape
I already had the sitewide `';
export default {
async fetch(request, env) {
const response = await env.ASSETS.fetch(request);
if (new URL(request.url).hostname !== 'kenimoto.dev') return response;
const contentType = response.headers.get('content-type') || '';
if (!contentType.includes('text/html')) return response;
return new HTMLRewriter()
.on('body', {
element(el) {
el.append(OVERLAY_TAG, { html: true });
},
})
.transform(response);
},
};
```
The design point: what gets injected is **a single script tag**. The button UI, the analytics, the per-URL display rules — all of it lives in `products-overlay.js`, one file on the main site.
```
main repo: public/assets/products-overlay.js ← substance (UI, analytics, display rules)
each product repo: worker/index.js ← wiring (the code above, zero product-specific content)
```
The separation pays off on change. Redesigning the button or swapping an ad campaign means updating one file in the main repo; every product picks it up instantly, no redeploys. And since the wiring contains nothing product-specific, it goes into the product template. From then on, shipping a product is enough to ship the support button with it.
Splitting it this way, one line of wiring and the substance at a separate URL, is what embeddable SDKs converge on. The loader YouTube hands you behind its single script tag is 993 bytes; the real 27KB widget arrives later from a second URL. I read that loader line by line in [YouTube iframe API: what 993 bytes actually ship](/learn/js-sdk-design/loader-vs-body/), along with why the two URLs get opposite cache-control headers.
The substance side is structured for the ad use case: an array of URL predicates paired with render functions.
```js
// Per-URL rules: ad modals get appended here later
const RULES = [{ test: () => true, render: renderSupportFab }];
```
"People reading this docs page should see a modal for this book." That kind of page-level matching gets added at the delivery layer, without touching the product.
## The fork problem: manners for deploy glue in an OSS repo
One thing bothered me: some products are public OSS. If `worker/index.js` sits in the historymap repo, someone who forks it and deploys to their own Cloudflare account gets my Ko-fi button on their screens. Bad manners.
The countermeasure is already in the code above:
```js
if (new URL(request.url).hostname !== 'kenimoto.dev') return response;
```
A Worker knows which URL it was invoked for, so it injects only when the hostname is `kenimoto.dev`. Wherever a fork deploys (`*.workers.dev` or a custom domain), the wiring stays inert, and no external request is ever made. The overlay JS carries the same hostname check as a second layer.
A nice side effect: `wrangler dev` and workers.dev previews don't inject either, so the button stays out of the way during development. The README states it plainly: `worker/` is deploy glue for the kenimoto.dev deployment, host-gated, and deleting it after forking is recommended.
## The trap: a deployed Worker that never runs
I deployed and checked the production URL. No injection.
```
$ curl -s https://kenimoto.dev/products/historymap/ | grep -c 'products-overlay.js'
0
```
The response carried `cf-cache-status: HIT`, so my first theory was stale HTML from the pre-Worker era sitting in the edge cache. I purged the URL and checked again: still 0. Not the cache.
The cause was by design. **By default, Workers Static Assets serves asset-matching requests directly, without invoking your Worker script.** Even with `main` pointing at your script. The Worker only runs for requests that do *not* match an asset. When every page is a static HTML file, as here, the injection code is simply never called.
The fix is one line of config:
```jsonc
"assets": {
"directory": "./dist-worker",
"binding": "ASSETS",
"run_worker_first": true
}
```
With `run_worker_first: true`, every request goes through the Worker first, and the injection came alive. You give up the direct-serve edge caching for assets, but HTMLRewriter streams, so I couldn't feel a difference.
This is a silent failure: deploy succeeds, zero errors, zero executions. If you combine Static Assets with `main`, make this the first thing you check; it will save you half an hour of staring at curl output.
## The guards, summarized
The overlay substance runs four checks at its entry point:
```js
if (window.self !== window.top) return; // never render inside iframe embeds
if (location.hostname !== 'kenimoto.dev') return; // inert on forks and previews
if (!location.pathname.startsWith('/products/')) return;
if (window.__kenProductsOverlay) return; // double-load guard
```
The first one is specific to this setup: historymap is a tool whose main use case is being embedded in other people's sites via iframe. A support button floating over a timeline embedded in someone else's page would be an accident, so it renders only in the top-level window.
Analytics went to GA4. If a product page has no gtag of its own, the overlay bootstraps one, and support clicks fire a `support_click` event into the same property as the main site. Products owing zero analytics implementation is part of the "template ships the wiring" deal.
## Takeaways
- For cross-cutting UI over product screens, inject at the edge instead of wrapping in iframes; don't pay for it with SEO
- Keep the injection down to one script tag of wiring; concentrate the substance in one file on the main site. Changes propagate to every product instantly, no redeploys
- Host-gate any deploy glue that lives in an OSS repo. Inert on forks, and say so in the README with a deletion recommendation
- Static Assets + `main` without `run_worker_first: true` means your Worker silently never runs
Shipping a new product now means creating a repo from the template and deploying. The support button and analytics come pre-plugged. Crossing "monetization plumbing" off the someday list is the best thing this architecture bought me.
---
# I Gave Every Page on My Site a .md Twin. The AI Fetchers Stopped Guessing
URL: https://kenimoto.dev/blog/every-page-md-twin-llmo/
Lang: en
Date: 2026-06-08
Description: llms.txt is one summary file at your root, and Google just called it the new keywords meta tag. So I went the other way: a Markdown twin for every page, served as text/markdown. Here's the Astro code and what actually changed.
A while back I added an `llms.txt` to my site because everyone said I should. One Markdown file at the root, a tidy table of contents for the robots, a little hopeful note that said "dear AI, here is my site, please be kind." Then I checked my logs a month later and the citation-driving crawlers had touched it almost zero times. I had written a letter and mailed it to a house nobody lived in.
Around the same time, Google's Gary Illyes confirmed at Search Central Live that Google does not support `llms.txt` and has no plans to. John Mueller went further and compared it to the **keywords meta tag**: a thing site owners controlled, therefore a thing search engines learned to ignore. That comparison stung, because it was correct.
So I stopped trying to summarize my whole site in one file the robots don't read. I did the opposite. I gave **every single page its own Markdown twin**, served at the same URL with `.md` glued on the end. And that one actually moved the needle.
## The pattern in one line
Take any page. Append `.md`. Get the same content as clean Markdown instead of HTML.
```text
/company → HTML for humans
/company.md → Markdown for machines
```
That's it. Same URL, same content, two costumes. A human hits `/company` and gets the full styled page with the nav bar, the cookie banner, the footer with my forty social links. An AI fetcher hits `/company.md` and gets the actual words, in Markdown, with none of the furniture.
The idea isn't mine. It's the logical extension of what Jeremy Howard proposed with `llms.txt` back in September 2024, except that instead of one summary file describing the site, you push the same "give them Markdown" thinking down to **every page**. And it turns out the people building the tools already do this. Anthropic's own docs serve it: take any page like `docs.claude.com/en/docs/claude-code/plugins`, slap `.md` on it, and you get the raw Markdown the rendered page was built from. Once I noticed that, I felt a little silly. The model providers are feeding their own crawlers clean Markdown, and I was out here making mine eat a div soup.
## Why HTML is a bad meal for a robot
When a crawler fetches your HTML page, it has to do surgery. Strip the `