# khoa.work — full content for LLMs > The personal site of Khoa Nguyen, a digital marketer in Hội An, Vietnam — marketing fused with engineering: SEO and GEO, content, and in-house AI content pipelines. This file holds the full text of the site's About, Work, and Writing for AI ingestion. Curated index: https://khoa.work/llms.txt --- # Experience & profile Khoa Nguyen — Digital Marketer · Operations Associate. Based in Hội An, Vietnam. Contact: hi@khoa.work · https://khoa.work Digital marketer who builds the systems behind the strategy — marketing fused with engineering. SEO and GEO, content-led growth, and in-house AI content pipelines (studio plus AI imagery and video) wired straight into Shopify. ## Current role Sartoro — Operations Associate (2024 — Present) Made-to-measure menswear · DTC, Shopify · reporting to the founder - Runs the product line end to end for a 7-figure Shopify store growing ~20% a year — photography, retouching, product setup, data tracking, full-stack SEO/GEO, and content. - Grew the luxury collection into the store's lead line — now ~60% of revenue. - Doubled organic search traffic (2×) and replaced the SEO agency with in-house full-stack SEO — about US$3,000/month saved. - Built in-house AI content pipelines (Python, MCP; studio + AI imagery/video) at catalog scale, replacing studio/agency cost. - Early-mover on GEO: products surfacing inside AI answers (ChatGPT, Perplexity), not just Google. ## Previously - Insurance Direct Canada · Orca Financial — Digital marketing, SEO and content (Canada) - Hoiana — B2B mass marketing · integrated resort (Vietnam) ## Education - University of Foreign Languages — English Language ## Languages - English (fluent) · Vietnamese (native) ## How I learn Self-taught and AI-first — reaches ~90% of any field by asking AI; the other 10% is shipped experience and learning from people who've done the work. ## Kind words - "That sounds really awesome, Khoa! Nice work!!" — Founder, Sartoro - "Great start! Pretty awesome, nicely done." — Founder, Sartoro --- # About I'm Khoa — a digital marketer based in Hội An, Vietnam. I help brands find customers and grow online, and I'm a little unusual in how I do it: I don't just set the strategy, I build the systems that run it. Marketing fused with engineering — I write the code, build the agents, measure the ROI, and cut whatever doesn't earn its place. ## What I do - **SEO & GEO.** Classic search optimisation, plus Generative Engine Optimisation — getting brands surfaced *inside* AI answers (ChatGPT, Perplexity, Claude), not just on Google. It's an early-mover edge while most of the market still optimises for ten blue links only. - **Content that compounds.** Content-led SEO where the writing actually moves the numbers — several pieces I've produced have become some of the fastest-growing pages on the sites I work on. - **AI content pipelines.** I build production pipelines in Python that generate product imagery and video at catalog scale — wired straight into the store's data, idempotent and credit-safe, in place of studio or agency cost. Prompt engineering for image and video models is part of the craft. - **Measure, then cut.** I audit spend with real data — link profiles, campaign ROI, what the AI engines actually ingest — and I'm comfortable killing a channel that isn't delivering and moving the budget somewhere that is. ## From studio to storefront I can take a product from nothing to launched, and I run the whole chain myself: - **Studio & photography** — set up and light the shoot, and photograph the product directly. - **Post-production** — retouch and grade in Photoshop, Lightroom and Premiere Pro, with AI tools (Evoto, WeShop, Higgsfield) for lifestyle and volume imagery — wired through APIs and MCP to run at catalog scale. - **Storefront build** — configure the product in Shopify end to end: pricing, variants and components, and descriptions written for GEO/SEO. - **Then optimise** — recommend the changes that lift click-through (CTR) and conversion, and measure what actually moves. ## How I work Direct assessments, concrete numbers, clear next steps — over theory. I'd rather do a few right things for the right people than spread budget thin. The work in this portfolio is the proof: real builds, honestly framed, with the parts that aren't done yet labelled as such. ## How I learn Mostly self-taught, and unapologetically AI-first. I'd rather ask an AI the moment I need something than chase a certificate — you can reach about 90% of any field that way. The other 10% is the part that counts: judgment you only build by shipping, and by learning from people who've actually done the work. ## Currently - Taking on a small number of digital-marketing projects I can do properly - Writing here on growth, brand, and the web ## Say hi Email me at [hi@khoa.work](mailto:hi@khoa.work). I read everything and reply to most of it within a few days. Elsewhere: [LinkedIn](https://www.linkedin.com/in/andynguyen-vn/), [Instagram](https://www.instagram.com/khoanguyen9519/), [Facebook](https://www.facebook.com/nguyen.n.khoa.96/). --- # Work ## SoiTarot Year: 2026 · Role: Solo — strategy, build, content, SEO · Stack: Programmatic SEO, Next.js, Claude agents, Higgsfield, Vercel · Live: https://soitarot.vn · URL: https://khoa.work/work/soitarot A tarot site was a 3am idea: a bet that I could win a competitive Vietnamese niche with programmatic SEO, built almost entirely by AI, in about a week. SoiTarot is that bet, left running in public so I can watch what actually happens. ## The bet I wanted to know two things, and I was tired of arguing about both in the abstract. First: how far can AI-driven programmatic SEO actually go in a competitive niche? Not a toy site with twelve pages — a real content property in Vietnamese spirituality, a space crowded with old fortune-telling sites and tabloid horoscopes, where the incumbents have years of domain age on me. Second: could I wire every tool I own — design, image generation, content, agents — into one machine that builds and ships a property end to end, with me as the director rather than the labourer? So I gave myself a constraint a 3am idea deserves: roughly a week from blank repo to a live, indexable site. Then I'd stop, and let it tell me the truth. ## What it is SoiTarot is a self-discovery library for Gen Z Vietnam — tarot, numerology, and Western astrology, in clean Vietnamese, free. The whole positioning hinges on one word the brand refuses to use. In Vietnamese, *bói* means having your fortune told; *soi* means looking into yourself. Everything points away from prediction and toward reflection: you draw a card, and it hands you a better question, not an answer. Under that idea sits a lot of surface area: the full 78-card Rider-Waite deck, each card written up with real depth; the twelve zodiac signs; the nine numerology master numbers with a calculator; spread guides; beginner how-tos; and a thirty-piece blog. On top of the library are tools — an interactive draw with on-demand AI interpretation, a viral "which tarot card are you" quiz, and a small personal area with a journal, streaks, and a card collection. ## How it was built This is the part I actually care about. SoiTarot isn't a site I made; it's a site a system made, and the system is the real deliverable. The design came out of **Claude Design** as a full handoff — logo directions, a colour system, a motion spec, a UI kit — which I implemented rather than redrew. The content was written by **Claude** (Haiku for volume, Sonnet where quality mattered), but the important move was refusing to let it run unsupervised: every page passes automated gates in CI before it can merge — banned fortune-telling words, a strict Vietnamese-only rule, and an SEO structure check (word count, FAQs, internal links, meta length). Imagery is all **Higgsfield**, locked to a single house style. And the whole thing was run by a small team of **agents inside Claude Code** — strategy, content, design, engineering — coordinating through a shared memory folder and a handoff protocol, so four roles could work without stepping on each other. The stack underneath is deliberately boring: Next.js, Tailwind, Sanity, Vercel. The interesting engineering is the assembly line, not the framework. The shape of that chart is the point. The site was substantially live within the first week; the long quiet tail on the right is not me running out of steam — it's the experiment, untouched on purpose. ## The design system, in the open I didn't want this to look like AI sludge, so the brand got treated as seriously as the SEO. The identity is **The Crescent Eye** — a gold crescent cradling a lavender pupil that reads four ways at once: an eye, a moon phase, an opening door, and the curved edge of a card mid-flip. *Soi mình* — look into yourself — through a cycle, through a door, through a card turning over. The art direction is a single locked style I called **Engraved Celestial Etching**: deep navy, antique gold linework, lavender and ivory, the texture of an old astronomical plate. Every one of the 78 cards, twelve signs, and nine numbers was generated to that spec, so a thousand AI images still feel like one hand made them. I published the whole system as a Claude Design project so it's inspectable, not just the finished pixels — logo directions, the full colour system, motion, the UI kit. [Open the SoiTarot design system →](/projects/soitarot/design/) ## Programmatic, at scale The reason a niche like this is winnable by one person is that a lot of the demand is *combinatorial*, and combinations are exactly what a system is good at. Zodiac compatibility alone is twelve signs against twelve — 144 pages, each with genuinely different content, generated and gated automatically. Daily horoscopes are twelve more, refreshed. The deck is 78. The numerology, the spreads, the guides stack on top. To get that volume taken seriously by search and by AI answer engines, the technical SEO had to be unusually thorough for a side project: JSON-LD on every page (Article, FAQ, HowTo, Breadcrumb), an `llms.txt`, IndexNow firing automatically on every deploy, dynamic per-page OG images, GA4 and Search Console wired in. I even gave the site a credible bylined author and brand story — a small editorial persona — because E-E-A-T rewards a face and a point of view, and an anonymous content farm gets treated like one. ## What's happened so far Honest status, because the honest version is the interesting one: the site has been live about two weeks. The traffic curve is still climbing, steeply, and I haven't touched the site in a week. There has been **zero off-page work** — no backlinks, no outreach — and I haven't even done the entity work to push indexing along. Pure on-page. Google is indexing it steadily anyway. Not fast — but consistent, and consistent is the signal I was actually looking for. A property that grows while its maker ignores it is a property with a working engine underneath, and that's the thing I set out to prove. ## What I learned The cliché is that AI removes the bottleneck on *production*. That's true and it's also the boring half. The real lesson is that once production is free, the bottleneck moves entirely to **judgment** — taste, positioning, and the quality gates you're willing to enforce on yourself. The hard part of SoiTarot wasn't generating ten thousand words a day; it was the banned-word list, the Vietnamese-only rule, the refusal to say *bói*. The constraints are the craft now. The other lesson is patience, which I'm still in the middle of. Off-page and entity-building are the real test ahead, and I've done none of it on purpose — I wanted a clean reading of what on-page alone, built this way, can do. So far the answer is "more than it has any right to," and the chart is still going up. I'll let it keep telling me. ## Jarvis Year: 2026 · Role: Solo — architecture + build · Stack: MCP, Python, Claude Skills, Marketing automation, Shopify · URL: https://khoa.work/work/jarvis Jarvis is a marketing operating system I'm building for a Shopify brand — named, yes, after the assistant in Iron Man. Its job is unglamorous and badly needed: pull the fragmented numbers from every marketing channel into one place a business owner can actually read. ## The problem A brand's marketing data is scattered across a dozen tabs — Meta Ads, Google Ads, TikTok, Instagram, Search Console, Shopify. Nobody, least of all the person paying for it, sees the whole picture at once. The simple questions are the hard ones: what did we spend across *everything* this week, which campaigns are working, which are bleeding, what should go to the agency, and is the agency delivering? ## What it is A single cockpit, not another dashboard. The design separates three concerns with hard boundaries: - **Adapters** — every data source wrapped as its own MCP server. - **Skills** — markdown-defined analyses that compose those adapters and apply a real framework. - **Workflows** — Python on a schedule that runs the skills and drops a written report in your inbox. One view answers the owner's questions — blended spend and performance across channels, what to delegate, whether the agency's earning its retainer — with the SEO side, from Search Console, folded into the same picture. ## How it's built [MCP-first](/writing/mcp-first-marketing-ops): wrapping each source as a server keeps the system composable, testable, and IDE-agnostic. Python, because the work is data analysis. It ships in 10-day slices, each proven by a [side-by-side demo case](/writing/ship-evidence-not-features) rather than a feature list. The capability ladder is staged on purpose — **describe → recommend → act** — each rung earning the next. ## Where it's going The newest slices push past read-only: an [inbox that drafts its own order-status replies](/writing/inbox-that-drafts-itself) (a draft, never sent), and ad-creative generation wired in for A/B variants. The throughline is one place for everything an ecommerce brand's marketing touches — paid, organic, customer replies, creative — with a human still in the chair for the calls that carry risk. It's an internal tool, written up here as craft. The full thinking lives in the [marketing-engineering essays](/writing/marketing-engineering). ## Athena Chess Year: 2026 · Role: Solo — SEO, web, brand, content · Stack: Local SEO, Next.js, Schema / AEO, Brand system, GSAP · Live: https://athenachess.vn · URL: https://khoa.work/work/athena-chess Athena Chess (CLB Cờ Vua Athena) is the leading children's chess academy in Đà Nẵng — five branches, 5,000+ students since 2018, 30+ tournaments hosted, and coaches who are 100% FIDE-certified. They had the reputation and the results. What they didn't have was a digital presence that matched. I built it: the website, the local SEO, the brand system, and the content — end to end. ## The brief, in one line A local business that's genuinely the best in its city should be the first thing a parent finds when they search "dạy cờ vua Đà Nẵng." Athena wasn't. The job was to close that gap — turn a real-world leader into a search-and-AI leader too. ## Local SEO was the whole game For a business with five physical branches, local search is the channel. So most of the work lived there: - A **130-keyword roadmap** (Semrush, VN), filtered down and grouped into six clusters — transactional, hyper-local Đà Nẵng terms, kids-focused, content pillars, tournaments, and decision-stage queries. The bullseye terms — "clb cờ vua," "dạy cờ vua đà nẵng" — sit at competition 0.03–0.09. Winnable, and nobody local was targeting them properly. - **Multi-location done right:** real per-district pages (`/co-so/hoa-cuong`, `hoa-xuan`, `hoa-khanh`) with their own content, map, and FAQ — not five addresses dumped on one page, and not thin doorway clones. - **`SportsActivityLocation` schema** with street-level `geo` coordinates, opening hours, and phone per branch, plus a consolidated NAP across the whole site — the structured data that earns map-pack and rich results. - A Google Business Profile playbook for all five locations (the off-site half that's now the client's to execute). ## Built for AI search, not just Google The site is a static-rendered Next.js build, so every crawler — including the AI ones — gets the full HTML. I shipped a 12-article content core on a pillar-and-spoke model (the "luật cờ vua" cluster alone addresses ~2,400 monthly searches at competition 0.04), each piece with an author byline, dates, FIDE citations, and FAQ blocks. Add `robots.txt` and `llms.txt` that welcome GPTBot, ClaudeBot, and PerplexityBot, FAQ and Course and Article schema throughout, and the on-page GEO readiness lands around 80/100. Core Web Vitals came in strong — LCP ~2.8s, CLS near zero. ## The brand, made buildable Athena's identity is classical — a Greek-goddess namesake — so the design system leans into it without getting cold: a warm gold (`#FEC33A`) against a signature magenta-to-purple-to-indigo gradient, Be Vietnam Pro for full Vietnamese support, and restrained Hellenic motifs. I documented the whole thing — color tokens, a component library, and a mobile-first GSAP motion system (a drawn-on program path, a count-up ELO leaderboard, scroll reveals) with a hard rule that it all degrades gracefully on touch and under reduced-motion. And the photo library — nearly 6,000 raw event images — got deduplicated and optimized to WebP at a 97% size cut to feed the gallery without bloating the page. ## Where it stands The foundation is live at [athenachess.vn](https://athenachess.vn): the site, the schema, the content, the brand. The remaining levers — claiming the five Business Profiles, gathering parent reviews, the first videos — are deliberately the client's to pull, because that's where local authority actually compounds. Athena was already the best chess academy in Đà Nẵng. Now the search results have a chance to say so. ## Vietnam Golf Discovery Year: 2026 · Role: Solo — design, build, SEO · Stack: Next.js, Headless CMS, Booking integration, Schema / AEO, Brand system · Live: https://golfdiscovery.vn · URL: https://khoa.work/work/golf-discovery Vietnam Golf Discovery ([golfdiscovery.vn](https://golfdiscovery.vn)) is a tee-time discovery and booking platform for golf in central Vietnam — the championship coastline around Đà Nẵng and Hội An, where Hoiana Shores, Ba Na Hills, and Montgomerie Links sit within an hour of each other. A travelling golfer wants to compare those courses, see real green fees, and lock a tee time without a week of email tag. The client had the supply and the relationships. I built the thing that turns a curious golfer into a confirmed booking: the brand, the site, the booking flow, and the SEO — end to end. ## The brief, in one line International golfers plan a Vietnam trip months out and decide fast once they land on the right page. The job was to be that page — fast, credible, in English, and structured so both Google and the AI assistants people now plan trips with can read every course in full. ## The product is the decision, not the brochure Most golf-course sites are brochures. This one had to do work. So the spine of the build is a course model and an enquiry flow that close the loop: - A **headless CMS course model** — designer, hole count, par, distances, green-fee ranges, location, and gallery — so each course renders from structured data instead of hand-built pages. Adding a course is a content entry, not a deploy. - A **booking-enquiry flow** wired into the front end: pick a course, propose a date and tee window, and the request lands with the team for a fast confirmation rather than a dead "contact us" form. The path from *interested* to *booked* is a few taps, not a phone call into the void. - Built to **scale past one city** — the same model holds multi-course packages and other regions, so the platform can grow without a rebuild. ## Built fast, and built to be read I built it in **Next.js** with server-rendered course pages, so every crawler — Google and the AI ones — gets the full course detail in the HTML, not a blank shell that fills in later. That matters more every month: a rising share of "where should I play in Đà Nẵng" questions now get answered inside ChatGPT and AI Overviews, and you only get cited if the page is legible to a machine on first read. On top of that: **structured data for each course** (golf-course and location schema with geo, fees, and specs), clean English-first metadata for an international audience, and a performance budget kept tight so the image-heavy course galleries don't sink the load. The result is a site that reads as well to a search engine as it does to a golfer scrolling on the plane. ## The brand, made buildable Golf travel sells on place — light, coastline, the specific drama of a hole against the sea. So the design leans on the photography and gets out of its way: a calm, premium type system, generous space, and a restrained palette of fairway green, sand, and deep coastal blue that lets the course imagery carry the page. I documented it as a small component system — course card, fee table, enquiry module, gallery — so the look holds as the course list grows and stays mobile-first, because most of this traffic arrives on a phone. ## Where it stands The platform is live at [golfdiscovery.vn](https://golfdiscovery.vn) — the brand, the build, the booking flow, the SEO foundation. It launched recently, so the honest status is *foundation, not scoreboard*: the structured content and the technical groundwork are in, and the levers that compound from here — more courses, reviews, the off-site authority that earns the competitive "golf in Đà Nẵng" terms — are the run ahead. The site is built to take them. ## URBAN XXV Year: 2026 · Role: Founder — brand, design & build · Stack: Next.js 16, React 19, Tailwind v4, Framer Motion, E-commerce · Live: https://theurban25.com · URL: https://khoa.work/work/urban-xxv URBAN XXV is my own thing — a Vietnamese niche-perfume brand built on one idea: a fragrance is a place you can wear. The debut NYC collection maps three scents to three New Yorks — Brooklyn, Central Park, 5th Avenue — with a discovery set to lower the barrier to a first sniff. This case study is about the rebuild: the new Next.js storefront, currently in its final pre-launch pass before it replaces the old site at [theurban25.com](https://theurban25.com). The brand, the design, and the code are all mine. ## The idea: a scent map of New York *Perfume unfolds adventure.* That's the line, and the product is built to earn it. Each fragrance is pinned to a location and a mood — Brooklyn is rebellion and freedom, Central Park is nature breathing inside concrete, 5th Avenue is quiet, expensive difference. You're not buying "a woody amber"; you're buying a neighbourhood. Hong Kong is the collection teased next, which means the whole thing had to be built to grow. ## Designing the brand before the build I designed the brand first and let it drive every screen. Each scent gets its own world colour — a deep navy (`#0C1159`) for Brooklyn, forest green (`#0E3533`) for Central Park, burgundy (`#601119`) for 5th Avenue — set against ivory rather than stark white, with near-black ink for type. Montserrat carries the interface; Playfair Display in italic carries the romance of the tagline. The reference point was the editorial restraint of the high-end fragrance houses — lots of air, photography doing the talking, nothing shouting. I wrote it down as a small system so a new product page inherits its world instead of being styled by hand. ## The storefront, in Next.js The rebuild runs on **Next.js 16 (App Router) with React 19, Tailwind v4, and Framer Motion**. Product and collection pages are statically rendered, so they're fast and fully legible to crawlers on first load. The commerce front end is real: a client-side cart (React Context) with a slide-in drawer, quantity controls, a free-shipping threshold, VND formatting via `Intl`, and toast feedback — plus a collection browser, product pages with an image carousel and related-product rail, and an editorial About page. It's image-forward and mobile-first, because that's where this audience lives. ## What's done, and what's next — honestly This is pre-launch, and I'd rather be straight about the line. The **NYC storefront is feature-complete** — home, collections, product detail, cart — and that's the half a customer falls in love with. The next phase is the commerce back half: a real **checkout with Vietnamese payments** (VietQR / SePay), a **Sanity CMS** so the catalogue isn't hardcoded, and **VI/EN bilingual**. I sequenced it on purpose — ship the brand experience first, wire the money in second. On SEO the same logic holds: the foundation is in (Vietnamese-localised metadata, static rendering), and the structured data and sitemap land with the CMS phase rather than being faked now. ## Where it stands The rebuild is in its final pass and deploys to [theurban25.com](https://theurban25.com). It's the most end-to-end thing I've made: I drew the brand, designed the pages, and shipped the code — a marketer's project that happens to be a real Next.js storefront. The fun part is still ahead, when the NYC bottles go live and the scent map starts filling in. ## Fingo Private Tours Year: 2026 · Role: Solo — design, build, SEO · Stack: Next.js, Local SEO, Schema / AEO, Design, VI/EN · Live: https://fingoprivatetours.com · URL: https://khoa.work/work/fingo-private-tours Fingo Private Tours is a Hoi An native's private-tour brand — small groups of six guests at most, the Old Town and the countryside, led by a guide who grew up there. They had a genuine five-star reputation (104 TripAdvisor reviews and a 2025 Travellers' Choice award) and a direct, no-middleman model. What they needed was a site that matched it. I designed and built it on Next.js, and did the SEO. ## The brief, in one line "Book me directly, not through a platform that takes a cut." The site had to make that case — feel premium and personal, load fast for travellers on patchy mobile data, and turn a visitor into a direct message. ## A fast, personal storefront I designed and built it in **Next.js**, statically rendered so it's quick and fully legible to crawlers — including the AI ones — on first load. It's image-forward and intimate: the guide's story up front ("born in the Old Town"), tours with honest prices, the six-guest cap, and the trust signals (the award, the 5.0 rating) placed where they reassure. The whole thing leans into the founder, because that's the product. ## Found by people planning a trip A direct-booking tour brand lives or dies on being discoverable *before* the traveller arrives. So: **bilingual VI/EN**, tour and FAQ pages built around real planning queries ("Hoi An food tour," "Ancient Town walking tour," "My Son private tour"), structured data (`TouristTrip` / `LocalBusiness` / reviews), and an architecture ready for the AI trip-planners people now use to cite. Conversion is deliberately direct — **WhatsApp and Zalo, one tap** — with the roughly-20%-cheaper-than-the-platforms advantage made plain. ## Where it stands Live at [fingoprivatetours.com](https://fingoprivatetours.com): the design, the Next.js build, the bilingual SEO, the direct-booking funnel. It's a fresh launch, so this is the foundation — the reviews and word-of-mouth that compound into rankings are the client's strength to keep building. The reputation was already there; now there's a home for it that converts. ## Hoi An Vibes Tours Year: 2026 · Role: Solo — site, SEO, content · Stack: WordPress, Local SEO, Schema / AEO, Content, VI/EN · Live: https://hoianvibestours.com · URL: https://khoa.work/work/hoi-an-vibes-tours Hoi An Vibes Tours is a private day-trip operator in central Vietnam — Hoi An, Đà Nẵng, Huế, My Son, and the countryside — run by a licensed local guide with seven-plus years of showing people around. They had the guiding and the five-star reviews; what they didn't have was a site that could be found and booked. I built it: the WordPress site, the bilingual SEO, and the content — end to end. ## The brief, in one line A great local guide is invisible online until search can find him. The job was to turn a trusted, reviewed guide into something a traveller planning a Hoi An trip actually lands on — and then messages. ## Travel search was the channel For a private-tour business, the customer is a traveller Googling (and now AI-asking) *"things to do in Hoi An," "Hoi An day tours," "My Son private tour"* weeks before they arrive. So most of the work was being the answer: - A **bilingual VI/EN site** so the brand reads naturally to both local and international travellers. - **Local + travel SEO:** tour pages targeting the real planning queries (countryside, My Son, Hai Van Pass, Cham Island), an itinerary-led structure, and a blog on local culture that earns the long-tail "what to do in…" searches. - **Structured data** (`LocalBusiness` / `TouristTrip` / reviews) so Google and the AI trip-planners can read the tours, the rating, and the routes at a glance — full HTML, ready to cite. ## Built to get a message, not a bounce A tour booking starts as a conversation, so the site funnels into one: a **WhatsApp-first enquiry flow** plus a planning form, hotel-pickup and "flexible, honest, easy to plan" messaging, and the five-star TripAdvisor proof placed where it reassures. The path from *interested* to *first message* is one tap. ## Where it stands The foundation is live at [hoianvibestours.com](https://hoianvibestours.com): the bilingual site, the tour and content structure, the schema, the booking funnel. It's early — the compounding levers (more reviews, a Google Business Profile, links from travel sites) are the client's to pull, and that's where local travel authority is actually built. The guiding was already five-star; now it can be found. ## Resolution Year: 2026 · Role: Designer & Engineer · Stack: Next.js, TypeScript, WebGL, tldraw · URL: https://khoa.work/work/resolution · Note: concept piece, not a shipped product What I was trying to figure out: why every thinking tool I'd used left me with more notes and no more clarity. I had years of Notion, of Obsidian, of a thousand `.txt` files. What I didn't have was the feeling of a problem getting *sharper* over time — the thing that actually happens when you stay with something long enough. The tools were good at capture. None of them were built around the slow work of seeing. ## Context Resolution started as a complaint. I kept noticing that my best thinking didn't look like a list — it looked like a few ideas I could see in high detail surrounded by a lot of fog. The detailed ideas weren't smarter; I'd just spent more time looking at them. "Resolution" became my private word for that: how sharply you can see a problem before you try to solve it. You can't solve something at a lower resolution than you can see it, and most tools quietly assume you already see it clearly. So the brief I gave myself was narrow. Not a second brain, not a knowledge graph. One verb: *resolve.* A space where an idea could start as a blurry smudge and, with attention, become something you could see every edge of. ## Approach I built it as a spatial canvas rather than a document because documents have an order and thinking doesn't. The first prototype was a weekend of WebGL — just nodes you could drag, with one rule: a node's size mapped to how resolved it was. Vague things were small and soft. Things you understood were large and crisp. That single mapping did more work than any feature I added later. You could open a board and *feel* where your understanding was thin. Everything after that was subtraction. I kept asking what I could remove and still have the tool make sense. No folders. No tags. No sidebar. The render loop got most of my attention — I spent an embarrassing amount of time making a node feel like it *settled* when you let go of it, the way ink does on paper, instead of snapping like a UI element. That sounds precious. It's the difference between a tool you tolerate and one you reach for. ## Decisions **Size means resolution, not importance.** The obvious version maps node size to priority. I tried it; it turned the canvas into a to-do list with extra steps. Mapping size to *how well you see the thing* instead kept the tool honest about its one job. A huge node you barely understand is a lie the interface won't let you tell. **Keyboard-first, eleven shortcuts.** I capped the command set at what I could memorize in a sitting. If a feature needed a menu to be discoverable, it was a sign the feature was wrong, not that I needed a menu. The constraint killed at least three half-built panels. **No collaboration in V1.** Resolution is a thinking tool, and thinking is mostly something you do alone. Multiplayer would have meant presence indicators, conflict resolution, an account system — a different product. I shipped the single-player tool I actually wanted and left the rest as a maybe. **Local-first storage.** Boards live in the browser and sync to a file you own. I didn't want the tool to depend on my server staying up, and I didn't want anyone's thinking held hostage by a subscription. It made the build harder and the product calmer. ## Outcome It shipped as a small, fast web app, and it's now the thing I open before starting any project — including the redesign of this site. The honest outcome is personal: I think more clearly with it, and I can watch a fuzzy idea get sharp across a week instead of pretending I had it figured out on day one. A few hundred people use it. Some of them write to tell me it changed how they plan; more of them, I suspect, bounced off the emptiness of the empty state. That's a fair trade for a tool with one opinion. The technical outcome I'm proud of is dull to describe and good to use: it holds a thousand nodes at sixty frames without the fans spinning, because the render path was the first thing I built, not the last. ## Lessons I over-built the sync layer. Local-first was right, but I spent weeks on conflict cases that a single-player tool will almost never hit. I'd ship the dumb version first next time and earn the complexity only when a real user hit a real conflict. I also waited too long to show it to anyone, because the empty state embarrassed me. When I finally did, the feedback wasn't "add features" — it was "I don't know what to put here first." The fix was a single line of onboarding copy, not a tour. I'd trade a month of polish for that one sentence earlier. The deeper lesson is the one the tool is named after: the resolution of the *product* lagged the resolution of my *idea* for a long time. I could see what Resolution should be months before I could build it. The work was mostly closing that gap — which is, I think, what all building is. ## Credits Solo build. The canvas rendering owes an obvious debt to [tldraw](https://tldraw.dev), whose source I read more than once. Early testers Mai and Tâm caught the empty-state problem I was too close to see. ## Mainspring Year: 2025 · Role: Designer & Engineer · Stack: SwiftUI, CloudKit, Swift · URL: https://khoa.work/work/mainspring · Note: concept piece, not a shipped product What I was trying to figure out: whether a task manager could make me feel calmer instead of more accountable. I'd used the powerful ones — the ones with projects and tags and filters and a sense, every morning, that I was already behind. Mainspring was a bet that the opposite design existed: software that holds a little tension for you so you don't have to hold all of it yourself. ## Context The name is the bet. A mainspring is the coiled part of a mechanical watch that stores energy and releases it evenly — it's why the watch doesn't lurch. I wanted an app that did that with intention: wind it once, and it doles your attention out steadily instead of dumping the whole backlog on you at 9am. Most task apps are databases with a nice front end. They scale infinitely, which means they never tell you that you've taken on too much. That infinity is the problem. A person who builds alone — no manager, no standup, no one to say "drop that" — needs a tool that pushes back, not one that obediently stores every yes. ## Approach I designed it around a hard limit before I designed anything else: the main view shows today, and today holds three things. Not a soft suggestion, not a "focus mode" you toggle. Three. Everything else lives in a backlog you have to deliberately visit, and pulling a fourth thing into today makes you push something out. The friction is the feature. I built it in SwiftUI because it needed to feel native and quiet on a phone, and because I wanted the constraint of one platform done well over the spread of cross-platform mediocrity. CloudKit handled sync without me running a server or asking anyone to make an account — your tasks live in your own iCloud, which is exactly as much backend as a calm app should have. The motion language took the longest. Completing a task doesn't celebrate; it *relaxes* — a small, slow wind-down, like tension leaving the spring. I cut every animation that felt like reward. Reward loops are how you build a habit; I didn't want a habit, I wanted a tool you could put down. ## Decisions **Three tasks, enforced.** This is the whole product. I lost early testers who wanted to "just see everything," and kept the ones who understood that seeing everything was the thing they were trying to escape. I'd rather have the second group. **No projects, no tags, no priority field.** Every organizing feature is a small invitation to spend your day organizing instead of doing. The backlog is a flat list you triage by hand each morning. It's less powerful and far more honest. **A weekly review, not a dashboard.** The one concession to history is a quiet Sunday ledger of what got done. No streaks, no charts, no productivity score. You read it, you feel something true, you close it. **Subscription, not one-time.** A calm app still costs money to keep alive, and I'd rather charge a fair recurring price than bolt on features forever to justify upgrades. The pricing matches the philosophy: small, steady, no growth theater. ## Outcome Mainspring shipped on the App Store and found a small, loyal audience of exactly the people I built it for — solo founders, freelancers, a surprising number of PhD students. It doesn't top any charts and it never will, because the design actively refuses the mechanics that drive installs. The reviews I value most say some version of "I finally stopped feeling behind." That was the entire goal, stated back to me by strangers. Financially it's a modest, real business — enough to justify maintaining it, not enough to quit anything. I've made peace with that. A tool that says no to its users is also saying no to its own growth, and I knew that going in. ## Lessons I underestimated how much people needed *permission* to use a limited tool. The three-task rule read as a gimmick until I wrote a short note explaining why — that the limit was the point, that overflow was the disease, not a missing feature. Adoption changed when the philosophy was visible, not just enforced. Design carries intent, but sometimes you still have to say the quiet part in words. I'd also reconsider iCloud-only sync. It's beautifully low-maintenance and it locked out every Android user and every person who wanted to glance at their list on a laptop. The calm came partly at the cost of reach. Next time I'd at least leave a door open. ## Credits Solo design and build. Beta wrangled by a patient group from the indie iOS community; the wind-down animation is better because [Rauno](https://rauno.me)'s writing on interface detail had been rattling around my head for a year. ## Fathom Year: 2025 · Role: Designer & Engineer · Stack: TypeScript, Cloudflare Workers, D1, Web Components · URL: https://khoa.work/work/fathom · Note: concept piece, not a shipped product What I was trying to figure out: whether you can measure whether writing is *working* without spying on the people reading it. I wanted to know if my posts were actually being read — not opened, read to the end — and every tool that could tell me also wanted to follow my readers across the internet. That trade felt both unnecessary and gross. Fathom was me refusing it. ## Context Pageviews are a lie I'd been telling myself for years. A post can have ten thousand views and a two-second average attention span; another can have eight hundred views and hold every one of them to the last line. The first feels like success and is failure. I didn't need more analytics — I needed one honest number: how *deep* do people read, and where do they leave. The catch is that the entire analytics industry pays for that answer with surveillance. Cookies, fingerprints, cross-site identity, a dossier on a stranger so I can learn that my third paragraph is boring. I found that genuinely unacceptable for a personal blog, which is supposed to be a generous thing. So the real problem wasn't measurement. It was measurement without a victim. ## Approach I built Fathom around aggregation at the edge. The embed is a tiny web component — three lines to install — that reports scroll depth as a stream of anonymous *increments*, not events tied to a person. A Cloudflare Worker catches them, buckets them, and throws away anything that could rebuild an individual. By the time data lands in storage, there's no "you" left to track; there's only "of the people who started this post, this fraction reached the halfway mark." That constraint shaped every technical choice. No cookies meant no consent banner, which meant the script could be genuinely small and the reading experience stayed clean. Storing only aggregates meant the database was tiny and cheap and impossible to leak meaningfully, because there was nothing personal in it to leak. The privacy posture wasn't a marketing layer on top — it was the architecture. ## Decisions **Read-depth as the headline metric.** I demoted pageviews to a footnote and made "how far did they get" the number you see first. It reframes the whole question from *did they arrive* to *did it land.* Some people found that uncomfortable. Good — it's the uncomfortable number that's useful. **No per-person data, ever — by construction.** I didn't add a privacy *mode.* I made it impossible to do otherwise. You can't accidentally turn on surveillance in Fathom because the code to assemble a profile doesn't exist. Privacy you can't switch off is the only kind worth advertising. **Edge-first, no origin server.** Workers and D1 meant the whole thing runs at the edge with near-zero latency and near-zero cost. A personal-scale analytics tool shouldn't need a server humming in a datacenter; it should cost a few dollars a year and disappear. **Open source.** The only way to make "we don't track you" credible is to let people read the code that doesn't. The repo is the proof; the privacy policy is just a summary of what you can verify yourself. ## Outcome Fathom runs on this site, and on a handful of other blogs whose authors care about the same thing. It told me, immediately and a little painfully, that my longer posts lose half their readers by the third screen — which changed how I write more than any writing advice ever has. That's the whole value: a true signal you can act on, bought without anyone's privacy as currency. It's not a business and I don't intend to make it one; a privacy tool that needs to grow eventually has to compromise the thing it sells. It's a small public utility I maintain because I use it, and because the version of analytics I wanted to exist didn't. ## Lessons I spent too long making the dashboard pretty before I trusted the data. The chart polish was procrastination dressed as craft — the moment of value was the first honest read-depth number, and I delayed it by weeks chasing a nicer axis. Ship the ugly truth first. I also learned that "privacy-first" is easy to *say* and only believable when it's structural. Early on I had a perfectly reasonable feature — session-level read paths — that would have required holding per-person data for a few minutes. I cut it. The temptation to peek is exactly the temptation the architecture has to make impossible, including for me. ## Credits Solo. Built on Cloudflare's edge stack, which did most of the hard work. The framing of "depth over breadth" I owe to a long argument with a friend who runs a much bigger blog and measures it the old way. --- # Writing ## Measuring AI citations when there's no analytics for it Date: 2026-06-26 · Tags: seo, ai, geo · URL: https://khoa.work/writing/measuring-ai-citations There's no Search Console for ChatGPT. Here's how I track whether AI is citing a brand — the proxies, the manual checks, and what's actually worth watching. The hardest part of [GEO](/writing/generative-engine-optimization) isn't doing the work — it's knowing whether the work landed. Google Analytics and Search Console will tell you about clicks and impressions on the open web. Neither tells you that ChatGPT recommended you to someone last Tuesday. The citation happens inside a conversation you never see. So you measure it sideways. Here's what I actually do. ## 1. Prompt-test it by hand The most honest signal is also the most manual: take the questions your customers actually ask, and ask them — in ChatGPT, Perplexity, Google AI Mode. Are you cited? Mentioned? Absent? Note it, with the date and the prompt, and repeat monthly. It's crude and it doesn't scale, but it's real, and it tells you which queries you're winning and which you're invisible for. ## 2. Track brand mentions, not just links AI answers are downstream of what the web already says about you. So watch for your brand showing up — in articles, forums, roundups, reviews — because those are the raw material the engines synthesise. Rising mentions are a *leading* indicator of rising citations. ## 3. Read the faint referral signals A trickle of referral traffic now comes from `chatgpt.com`, `perplexity.ai`, and friends — small, often under-attributed, but worth a segment in your analytics. Watch for two patterns: those referrers creeping up, and unexplained bumps in *direct* traffic after you've been visible in an answer (people hear the name, then type it). ## 4. Watch the inputs, since the output lags Citations trail authority by months — [I've watched that lag firsthand](/writing/answer-engines). So the most useful day-to-day metric isn't citations at all; it's the things that *cause* them: brand mentions, quality links, reviews, topical depth. Move those and the citations follow. ## The honest framing It's early, and it's noisy. Treat AI citation as a KPI you **sample** — a monthly manual sweep plus a mention tracker — not a dashboard you refresh. Anyone selling you a precise "AI visibility score" is mostly selling you a proxy with a confident UI. If you want a hand standing up that measurement — or the [GEO](/writing/generative-engine-optimization) work that earns the citations in the first place — that's [what I do](/services). ## What llms.txt is — and whether it earns its place Date: 2026-06-26 · Tags: seo, ai, geo · URL: https://khoa.work/writing/llms-txt A tiny markdown file that hands AI a clean map of your site. What it does, how to write a good one, and an honest take on whether it's working yet. `llms.txt` is the smallest piece of [GEO](/writing/generative-engine-optimization) you can ship, and one of the most misunderstood. So, plainly: it's a markdown file at the root of your site (`/llms.txt`) that gives an AI a curated map of what you've got — an overview line, then the handful of pages that actually matter, each with a one-sentence description. Think of it as a sitemap written for a reader instead of a crawler. An `sitemap.xml` is a flat list of every URL. `llms.txt` is the opposite: opinionated, prioritised, human-readable. It says *"here is who this site is, and here are the ten things worth reading first."* There's an optional companion too — `llms-full.txt` — which concatenates the full text of your key pages into one file, so a model can ingest everything in a single fetch instead of crawling page by page. ## What a good one looks like The format is just markdown: - An `# H1` with the site name. - A `> blockquote` one-liner: who you are, in a sentence. - Sections (`## Work`, `## Writing`, `## About`) with links: `- [Title](url): one line on what it is.` Keep it *curated*. The value is the editing — surfacing your best, not dumping your whole nav. If you generate it from your CMS (mine builds itself from the same content that powers the site), regenerate it when content changes so it never goes stale. ## The honest take Adoption by the AI labs is still uneven — not every engine fetches `llms.txt` yet, and none of them promise to. So don't expect it to flip a citation switch. What it *is*: cheap insurance with zero downside. It costs about an hour, it can't hurt you, and it pays off the moment any engine decides to use it. There's also a quiet side benefit — writing one forces you to articulate what your site is actually *for*, which tends to improve everything else. You can see mine at [/llms.txt](https://khoa.work/llms.txt) (the index) and [/llms-full.txt](https://khoa.work/llms-full.txt) (the full content). To check whether your own site has one — and how it scores on the rest of the [GEO](/writing/generative-engine-optimization) basics — paste a URL into the free [AI-readiness checker](/tools). ## Make your site legible to a machine that answers Date: 2026-06-26 · Tags: seo, ai, geo · URL: https://khoa.work/writing/legible-to-ai Before an AI can cite you, it has to read you. Server rendering, clean structure, and schema are the line between being quoted and being skipped. The reader changed; the oldest rule didn't. If you want a machine to quote you, the words have to actually be there when it looks. Most of [GEO](/writing/generative-engine-optimization) is downstream of that one sentence. ## Render it in the HTML The failure mode I see most: a beautiful site, built as a client-rendered single-page app, that ships an almost-empty HTML shell and paints the real content with JavaScript a moment later. A browser fills it in. An answer engine — and plenty of crawlers — often don't. They read the raw HTML, see a skeleton, and move on. You can't be cited for content the model never received. The fix is boring and total: **server-render** (SSR) or **statically generate** (SSG) your pages, so the content is in the first response. View-source on your own site. If the article text isn't in there, that's the whole problem. ## Structure it so a chunk can be lifted Models cite *passages*, not pages. Make passages easy to take: - Real heading hierarchy — one `H1`, then `H2`/`H3` that actually describe the sections. - Answer-first writing: state the conclusion, then explain. The first sentence under a heading should be the quotable one. - Lists and tables for anything comparative — they extract cleanly into an answer. ## Mark it up — in the server HTML Structured data (JSON-LD) tells the model what things *are*: an `Article`, a `Person` or `Organization`, a `Product`, an FAQ. Put it in the server-rendered HTML, not injected later by JavaScript — Google's own December 2025 guidance is explicit that JS-injected structured data and meta can be missed or delayed. And define the entity behind the site: a `Person`/`Organization` with `sameAs` links so the model knows *who* the source is. ## The test There's no mystery to checking this — it's all in the raw response. View-source, or paste your URL into the free [AI-readiness checker](/tools): it flags whether your content is server-rendered, whether you have JSON-LD, and whether the basics (title, canonical, Open Graph) are present. Pass those and you've cleared the technical bar of [GEO](/writing/generative-engine-optimization) — what's left is earning the trust to actually get picked. ## Generative engine optimization, end to end Date: 2026-06-26 · Tags: seo, ai, geo · URL: https://khoa.work/writing/generative-engine-optimization GEO isn't a new dark art — it's SEO pointed at the machines that answer instead of the ones that rank. Here's the whole playbook: who's reading, what gets cited, and how to know if it's working. A growing share of searches never reach a blue link. The question gets typed into ChatGPT, or Perplexity, or Google's AI Overviews and AI Mode — and an answer comes back, assembled from a handful of sources, with the rest of the web left unread. If you're not one of those sources, for that query, you don't exist. That's the whole reason GEO — generative engine optimization — gets talked about as if it were a new discipline. It isn't. Google says so itself: AEO and GEO are rebranded labels for SEO, because the AI surfaces are grounded in the same ranking and quality systems as classic search. The fundamentals don't change. The *reader* does. You're no longer optimising for a crawler that ranks ten links; you're optimising for a model that reads a few sources and writes one paragraph. ## Know who's actually reading "AI search" isn't one surface, and they overlap less than you'd think: - **Google AI Overviews** — the summary above the organic results. - **Google AI Mode** — the fully conversational tab, zero blue links. It's a *distinct* citation engine from AI Overviews — the two share only a small fraction of cited URLs — so being in one doesn't put you in the other. - **ChatGPT search** and **Perplexity** — their own indexes and browsing. You can't pick one. The work is to be citable everywhere, which — conveniently — is mostly the same work. ## What actually gets you cited Four levers, in rough order of how fast they pay off: 1. **Be legible.** Before a model can quote you, it has to read you — which means your content has to be in the HTML, structured, and marked up. [Make your site legible to a machine that answers](/writing/legible-to-ai) is the technical half. 2. **Be findable.** Let the AI crawlers in (don't block GPTBot, ClaudeBot, PerplexityBot in robots.txt), and hand them a map — [that's what llms.txt is for](/writing/llms-txt). 3. **Be quotable.** Answer-first passages, claims with real numbers, clean definitions, lists and tables. Write the sentence you'd want the model to lift. 4. **Be trusted.** Entity clarity (who you are, with `sameAs`), topical depth (clusters, not one lonely page), and genuine expertise. This is the slow one — and the one that decides who gets chosen when several sources are eligible. ## The honest part On-page GEO makes you *eligible*. Authority makes you *chosen* — and authority lags. I did the full AEO pass for a brand — llms.txt, structured data, doors open to every AI crawler — and [the answer engines still haven't called](/writing/answer-engines). That's not a failure of the work; it's the order of operations. You earn the technical eligibility in a week. You earn the trust over months, off-site, with mentions and links and reviews. ## And then measure it — somehow There is no Search Console for ChatGPT. The hardest part of GEO is knowing whether any of it worked, which is its own problem: [how to measure AI citations when there's no analytics for it](/writing/measuring-ai-citations). --- If you want the five-minute version: I built a free [AI-readiness checker](/tools) — paste a URL and it scores the legibility and findability levers above. And if you'd rather have it done than DIY, that's [what I do](/services). ## Two different animals Date: 2026-06-25 · Tags: process, design, ai · URL: https://khoa.work/writing/two-different-animals Half of SoiTarot is built for machines to crawl. The other half is built for a person to play with. They share a brand and almost nothing else. Everything I've written about [SoiTarot](https://soitarot.vn) so far has been about the machine that prints pages — the [combinatorial SEO](/writing/combinatorial-seo), the [content fence](/writing/the-fence-around-the-machine), the [agents](/writing/a-room-of-agents). But a site that only does SEO is a brochure. The reason to build the search engine was to bring people somewhere worth arriving — and that somewhere is a different beast entirely. SoiTarot is really two products wearing one brand. ## The crawl half and the play half The two halves have opposite jobs, and almost opposite engineering: | | The SEO pages | The interactive product | |---|---|---| | Who it's for | crawlers + skimmers | a person, leaning in | | Count | ~15,000, generated | a handful, hand-built | | Rendering | static, prerendered | dynamic, stateful | | Animation budget | 3 at once, ≤600ms | 6 at once, up to 1s | | Success metric | indexed + ranked | the draw feels good | | Data | content, mostly read | accounts, saved readings | The compatibility pages, the card meanings, the horoscopes — those are the crawl half: fast, static, [held to a strict Core Web Vitals budget](/writing/motion-under-a-cwv-budget) because a tenth of a second of jank is a ranking tax. The draw flow — shuffle, pick, flip, read — is the play half, where the animation *is* the product and a little theatre is the point. The card-flip that would be reckless on a horoscope page is exactly right here. ## Why the split is the architecture Trying to serve both jobs with one approach is how sites end up mediocre at both — a janky app bolted onto slow content, or beautiful content no one can interact with. So I let them be different on purpose. The crawl half is prerendered and motion-starved; the play half is a real app with state, accounts (sign in with Google), and a database holding what a person drew and saved. Same tokens, same art direction, same [Crescent Eye brand](/writing/designing-a-brand-before-code) — but underneath, two different animals. > SEO's job is to get a stranger to the door. The product's job is to make them glad they came. Build one thing to do both and you usually get neither. ## The funnel nobody talks about Here's the strategic point that the SEO-heavy story hides. All those combinatorial pages aren't the destination — they're the *top of a funnel*. Someone searches "Cancer and Sagittarius," lands on a genuinely useful page, and the interactive draw is right there: want a real reading, not just the compatibility note? The static half is the world's most patient acquisition channel; the interactive half is where attention turns into a session, an account, and — eventually — the question of whether any of this converts into something. That conversion question is the next thing I have to answer, and I haven't yet. The machine proved it can bring people in. Whether the play half is good enough to keep them — and whether "kept" ever becomes "paid" — is the part of [the experiment](/writing/the-bottleneck-moved) still running. The crawl half was the engineering. The play half is the actual product. I spent the first week proving the first one works; the more interesting bet is the second. ## The fence around the machine Date: 2026-06-25 · Tags: ai, process, seo · URL: https://khoa.work/writing/the-fence-around-the-machine When generating a page costs nothing, the page isn't the product. The gate that decides whether it ships is. The scariest thing about generating content at scale is how easy it is to generate *bad* content at scale. A model that can write a thousand pages can write a thousand mediocre ones just as fast, and publish them under your name. So the most important thing I built for [SoiTarot](https://soitarot.vn) wasn't a generator. It was a fence — the set of automated checks every page had to clear before it was allowed to exist. This is the quality half of [the build](/writing/the-bottleneck-moved). ## Quality can't be a vibe at scale When you write by hand, quality control is just you, reading. That doesn't survive contact with volume. The moment a pipeline is producing pages faster than a human can read them, "I'll check the output" becomes a fiction. Quality has to be encoded — turned into rules a machine applies to every page, every time, with no tired afternoons. So every generated page ran a gauntlet in CI before it could ship. If it failed any check, it didn't publish — full stop. | Gate | What it rejects | |---|---| | Banned-words list | Deterministic fortune-telling claims ("you *will*…") — for taste and for risk | | Natural-language rule | Translated-sounding, stiff, or templated Vietnamese | | Structure check | Pages missing the SEO scaffolding (headings, schema, internal links) | | Freshness + indexing | Stale dates; IndexNow fired automatically on every deploy | None of these are clever. That's the point — they're cheap, mechanical, and absolutely unforgiving, which is exactly what you want standing between a generator and the public. ## The banned-words list did more than protect taste The fortune-telling filter is worth dwelling on, because it's where ethics and SEO happened to agree. A tarot site that promises deterministic outcomes — *this card means you will get the job* — is both distasteful and a liability. Banning that language forced every page toward the honest framing: reflection, not prediction. That's better for the reader, lower-risk for me, and, as it turns out, the kind of measured tone that ranks. One small rule, three problems handled. > The generator was never the product. The fence was. Anyone can make a thousand pages; the work is deciding, once and mechanically, which thousand are allowed to exist. ## A fence is taste you only have to apply once This is the move I keep coming back to across the whole project. Doing quality control by hand is paying the same tax a thousand times. Encoding it in a gate is paying it once — you spend a hard hour deciding what "good" means precisely enough that a check can enforce it, and then it runs forever, on every page, while you sleep. It also reframes what a solo builder actually does. I didn't write fifteen thousand pages. I wrote the *rules* fifteen thousand pages had to obey — which is a smaller, sharper, and far more leveraged job. The fence is why the [combinatorial firehose](/writing/combinatorial-seo) of pages didn't turn into index-bloating junk, and it's the verbal cousin of the [single locked art direction](/writing/one-house-style) that kept the images in line. Generation is solved and basically free. What isn't free — what's now the entire job — is the judgment to build the fence, and the discipline to let nothing past it. ## The bottleneck moved Date: 2026-06-25 · Tags: process, seo, ai · URL: https://khoa.work/writing/the-bottleneck-moved I built a thousand-page site in about a week with a room of AI agents. The interesting part wasn't the speed. A few of my better ideas arrive at 3am, and this was one of them — or at least the instructive kind. I'd spent a year quietly collecting AI tools: a design tool here, an image model there, a coding agent I mostly used for small chores. Each was useful on its own. What I'd never done was wire them together and aim the whole rig at a single goal to see what it could actually do. So I gave myself a dare. Pick a niche I had no business competing in, build a real site for it, and use nothing but the tools already on my desk — no team, no budget, no off-page hustle. Three questions, really. Could programmatic SEO win in a crowded Vietnamese vertical? Could I get every tool I owned to behave like one machine instead of five toys? And the quiet one, the one the whole thing was actually about: where is my ceiling once the labor stops being the constraint? The niche I landed on was tarot; I called the site [SoiTarot](https://soitarot.vn). It's competitive, it's content-hungry, and — the part that matters — most of its demand is *combinatorial*. Twelve zodiac signs against twelve is a hundred and forty-four compatibility questions. Seventy-eight cards, each with its own meanings. Numerology, spreads, daily horoscopes, guides. A person writing by hand drowns in it. A system treats it as a for-loop. ## The machine The design came first, because a content farm that looks like a content farm is dead on arrival. I used Claude Design to build the whole system — color, type, components, motion — and to lock a single art direction I called Engraved Celestial Etching: deep navy, antique gold linework, the texture of an old astronomical plate. Then Higgsfield rendered every visual to that one spec — all seventy-eight cards, twelve signs, nine numbers — so that a thousand generated images still look like one hand made them. That consistency is the whole trick. Slop announces itself through variety. (Two follow-ups go deeper on this half of the build: [designing the brand before the code](/writing/designing-a-brand-before-code), and [motion under a Core Web Vitals budget](/writing/motion-under-a-cwv-budget).) The build itself ran inside Claude Code, but not as one agent doing everything. I set up a small team of them — design, content, engineering, SEO — sharing a single `.memory/` folder and a handoff protocol, so each one picked up where the last left off instead of starting cold. Bulk drafting went to the cheap, fast model; a slower, sharper one did the quality pass. The economics only work if you spend your good tokens where taste lives and your cheap tokens everywhere else. The piece I'm proudest of isn't any single tool. It's the fence I built around them. Every page had to clear a CI gate before it could ship: a banned-words list that killed deterministic fortune-telling claims, a rule that enforced natural Vietnamese over translated-sounding filler, a structural check for the SEO scaffolding. On deploy, IndexNow pinged the search engines automatically; JSON-LD, an `llms.txt`, and per-page metadata went out with every build. I even gave the site a bylined author — a name, a face, a point of view — because search rewards a person and treats an anonymous content farm like exactly what it is. ## A week to live From the 3am note to a live deploy was about a week. The commit history tells the honest version of the pace: a couple hundred commits, almost all of them front-loaded into the first stretch, then a sharp drop to nothing. I didn't ship slowly here, which is [unusual for me](/writing/shipping-slowly) — but the thing about a machine is that once it's tuned, you mostly get out of its way. By the time it went live it was generating pages at a scale I couldn't have hand-written in a quarter: the full compatibility matrix, the whole deck, the daily horoscopes refreshing on their own, numerology and spreads and guides stacked on top. Not thin doorway pages, either — each one genuinely different, gated by the same quality checks, structured for both search engines and the AI answer boxes that increasingly sit above them. ## What it's doing now Here is where I have to be careful, because this is the part people lie about. It's a few weeks live, so the third-party tools barely register it yet — which is exactly what makes the shape worth showing. In its first month the site went from zero to about 170 ranking keywords, and the curve isn't a step, it's a compounding climb. Don't trust the traffic figure any tool hands a site this young; it reads 29 visits a month, which badly undercounts what the real analytics show. The honest leading indicator is the keyword footprint, and the footprint is building fast. What's more telling is *where* those keywords sit. Nothing has reached the top ten. Read that as upside, not failure. The single biggest query in the niche — a hundred-thousand-searches-a-month head term — is already ranking, at position eighty-one. The next, forty thousand a month, sits at thirty-one. They're on the board, just deep. As a domain ages, deep rankings drift upward, and on a programmatic site that drift happens across thousands of pages at once. The rest of the picture is as bare as the design was rich: zero backlinks, no authority score to speak of, no entity work, almost entirely informational intent. I've done none of the off-page hustle you're supposed to do. Google is indexing it anyway, steadily, page after page, on its own schedule. A week ago I stopped touching it entirely. On purpose. I wanted to know whether it grows without me in the room, or whether I'm the thing holding it up. So far it grows without me. > When you can generate a thousand pages and a thousand images on demand, the scarce resource stops being pages or images. It becomes the judgment to keep them from being garbage — and the discipline to let the system run. ## Where this goes I'll put a stake in the ground, because a prediction you can check later is the only honest kind. This is the trajectory *without* the amplification layer — no links, no E-E-A-T push, no entity building — just the content compounding as Google matures the rankings it has already handed out. | Horizon | Ranking keywords | What should break through | |---|---|---| | 3 months | ~500–900 | first low-competition long-tail reaching page 1–2 | | 6 months | ~1,500–3,000 | clusters of card and compatibility pages landing in the top 10 | | 12 months | ~4,000–8,000 | long-tail matured; real organic traffic in the thousands a month | The ceiling in that table is deliberate. The long tail — thousands of low-competition card meanings and compatibility pairings — climbs on content and age alone. The head terms won't. That hundred-thousand-a-month query reaches page two on merit and then stalls, because page one for a prize like that is bought with authority: links, a real entity, the E-E-A-T signals I haven't built. That layer is its own project, for another time. Everything in the table is the floor — what the machine does on its own while I'm out of the room. The amplification is upside I simply haven't spent yet. ## The bottleneck moved That's the real finding, and it surprised me more than the traffic did. For my whole career the constraint was labor. There was always more to make than hours to make it in, so the people who shipped most won. That world is ending. I made a polished design system, a thousand on-brand images, and a four-figure pile of pages, alone, in a week. The making was nearly free. What wasn't free was everything around the making. Deciding what good looked like. Encoding that decision into a gate a machine couldn't talk its way past. Choosing the niche, the angle, the one art direction that held the whole thing together. Knowing when to stop. The CI gates turned out to be the actual product — taste applied once, at the fence, instead of a thousand times at the keyboard. So the answer to my quiet question — where's my ceiling now — isn't "how much can I output." Output is solved. The ceiling is whether I know what's worth making, and whether I can build the rails that hold a machine to that standard while I sleep. That's a more uncomfortable ceiling, because you can't out-work it. You can only out-think it. I won't oversell the experiment. It's a few weeks old. The curve could flatten next month; Google could decide AI-built sites are a wall it wants to build, and a lot of this could age badly. But the question I started with was *how far does AI-built SEO actually go*, and the honest, early, zero-tricks answer is: further than I expected, and it's still going. That's not a conclusion. It's a reason to keep watching. The lamp's still on. I just stopped feeding it. --- *The build, in pieces:* - [Designing a brand before a line of code](/writing/designing-a-brand-before-code) - [One house style, a thousand images](/writing/one-house-style) - [Motion under a Core Web Vitals budget](/writing/motion-under-a-cwv-budget) - [The fence around the machine](/writing/the-fence-around-the-machine) - [A room of agents](/writing/a-room-of-agents) - [Combinatorial SEO](/writing/combinatorial-seo) - [The answer engines haven't called](/writing/answer-engines) - [Two different animals](/writing/two-different-animals) ## Ship evidence, not features Date: 2026-06-25 · Tags: process, marketing, ai · URL: https://khoa.work/writing/ship-evidence-not-features How I'm building a marketing tool by proving it works — one side-by-side demo at a time — instead of stacking features nobody trusts yet. There's a failure mode I wanted to avoid while building the [marketing OS](/writing/marketing-engineering): spending three months heads-down, emerging with an impressive feature list, and having no idea whether any of it is actually better than what it replaces. So I gave myself a rule that sounds obvious and almost nobody follows: ship evidence, not features. ## What that means in practice Every slice I build has to answer a question, not just exist. The unit of progress isn't "I built the ads-performance skill." It's "here's what the system said about last month's ads, side by side with what actually happened." A feature that can't produce that comparison isn't done — it's just code. So the repo has a `demo-cases/` folder, and the rule is that each capability lands with one: a concrete, dated, side-by-side write-up of *the system's read* versus *the human/agency outcome*. Not a screenshot of the feature working — a piece of evidence about whether it's right. ## Why evidence beats features for a tool like this A marketing tool's only real currency is trust. If I'm going to lean on a daily summary to tell me where spend is leaking, I have to believe it more than I believe my own manual scan. A feature doesn't earn that. A track record does. Stacking features without evidence has a second, sneakier cost: you can't tell which ones to keep. Build ten skills, trust none, and you've made a more complicated version of not knowing. Build three and prove two of them beat the status quo, and you've made something you'd actually bet on. Evidence is how you prune. > A feature is a claim. A demo case is the test of the claim. Ship the tests, and the features that survive them are the only ones worth keeping. ## It also paces the build Demo-driven development forces "ship in slices." If every capability owes a demo, you can't disappear for a quarter — you're producing a verdict every couple of weeks. That cadence is the opposite of the heroic invisible build, and it's saved me from my own worst habit, which is polishing something nobody's pressure-tested. It dovetails with the phase plan: [describe, then recommend, then act](/writing/marketing-engineering). You can't honestly move from "describe" to "recommend" on vibes — you move when the describe layer has a stack of demo cases showing it read the situation better than the manual approach did. The evidence is the gate between phases, not a calendar date. ## The uncomfortable part Shipping evidence means sometimes the evidence is bad. A demo case can show the system's recommendation would have been worse than what actually happened — and then you have to keep that write-up, because the honest pile includes the misses. That stings, but it's the entire point: a tool validated only by its wins is a tool you can't trust on a new decision. The misses are what tell you where the judgment still has to stay human. It's the same discipline as everything else I keep coming back to — the [quality fence](/writing/the-fence-around-the-machine), the [MCP boundaries](/writing/mcp-first-marketing-ops): structure that makes the work honest at scale. Features are easy to generate now; anyone can. The scarce, compounding thing is proof that what you generated is actually good. ## One house style, a thousand images Date: 2026-06-25 · Tags: design, ai, process · URL: https://khoa.work/writing/one-house-style The thing that keeps a thousand AI-generated images from reading as slop isn't a better model. It's one art direction, held without mercy. There's a tell that gives away a machine-made site instantly, and it isn't quality — some generated images are gorgeous. It's *variety*. A folder of individually-nice pictures that don't agree with each other on light, palette, or line reads as a content farm even when every single image is good. Consistency, not polish, is what looks human. This is the companion to [designing the brand before the code](/writing/designing-a-brand-before-code) and part of [the wider build](/writing/the-bottleneck-moved). ## Name the look, then refuse to leave it [SoiTarot](https://soitarot.vn) needed a lot of imagery — the full 78-card tarot deck, twelve zodiac signs, nine numerology numbers, plus marketing art. Generated piecemeal, that's a guaranteed mess. So before generating anything, I locked a single art direction and gave it a name, because a named thing is enforceable in a way that "you know, mystical but tasteful" never is: **Engraved Celestial Etching** — deep navy, antique gold linework, lavender and ivory, the grain of an old astronomical plate. The palette was the non-negotiable core, five colours and not a sixth: Every asset got generated to that one spec in Higgsfield. Same light, same line weight, same restraint. The result is that a card you've never seen still feels like it came from the same deck as the one next to it. ## The work is rejection, not generation Here's what actually took the effort, and it surprised me: generating the images was the easy part. Holding the line was the job. A model will happily hand you a beautiful card that's three degrees off the house style — warmer light, a thicker stroke, a stray colour — and three degrees, multiplied across a thousand assets, is exactly the drift that reads as slop. So the real craft was curation: rejecting the off-spec outputs, regenerating against the reference, and being willing to throw away something pretty because it didn't belong. The taste lives in the *no*. A generator removes the cost of making; it does nothing about the cost of judgment, and judgment is the whole game now. > A house style isn't a mood board you consult. It's a standard you reject things against — and the rejecting is the design work that doesn't disappear when the making gets free. ## Why it compounds The payoff isn't only aesthetic. A consistent visual system is *reusable* — once the spec exists, the thousandth image costs the same as the second, and they all reinforce one brand instead of diluting it. That's the same lesson running through the whole project: the leverage isn't in producing more, it's in deciding the standard once, precisely enough that everything after it inherits it. The palette, the line, the name — those were the expensive hour. The thousand images were the cheap afterthought. If you want to see the full system the images were held to, it's [published in the open](/projects/soitarot/design/index.html) — and the [content-quality fence](/writing/the-fence-around-the-machine) that did the same job for words is its own story. ## Motion under a Core Web Vitals budget Date: 2026-06-25 · Tags: design, process, performance · URL: https://khoa.work/writing/motion-under-a-cwv-budget How SoiTarot got animation that feels alive on the interactive pages without wrecking performance across fifteen thousand SEO ones. Motion is where machine-built sites give themselves away. They get it wrong in one of two directions: nothing moves, so the site feels dead, or everything moves, so it feels cheap and your Core Web Vitals quietly collapse. On a site that lives or dies by search — fifteen thousand programmatic pages where a tenth of a second of layout shift is a ranking tax — I couldn't afford either. So I stopped treating motion as decoration and started treating it as a budget. This is the design half I gestured at in [the brand build](/writing/designing-a-brand-before-code); here's the engineering of it. ## Pick the stack on purpose, write down why The first decision was the boring, load-bearing one: what powers the animation. I wrote it down as an architecture decision so no future agent (or future me) could re-litigate it on a whim. The pick was Framer Motion for component animation, Lenis for smooth scroll, Tailwind for the utility layer — and the rejections mattered as much as the pick: GSAP (heavier bundle, worse React composability), React Spring (too verbose for these cases), Locomotive Scroll (heavier than Lenis). On a Core Web Vitals project, "which library" *is* a performance decision, not a preference. ## A scale, not a pile of magic numbers Animation goes feral when every component invents its own timing. So durations and easings became a small, fixed vocabulary — one table everything had to draw from: | Token | Duration | Used for | |---|---|---| | instant | 100ms | toggles, immediate feedback | | fast | 240ms | hover, button press, icon morph | | base | 360ms | default — fades, slides, page micro | | slow | 600ms | card lift, modal, drawer | | slower | 1000ms | hero reveal, large entrance | Three easing curves, and no more: a soft ease-out for things entering, a smooth ease-in-out for things moving both ways, and a gentle spring (about 6% overshoot) reserved for micro-interactions like hover and tap. Watch them run — the curve is the character: Five durations, three curves. That's the entire motion language. Constraint is what makes a thousand auto-generated pages feel composed instead of random. ## The budget is enforced, not suggested Here's the part that actually protects the search traffic. Motion runs against a hard budget that changes by page type: - **The 15,000 SEO pages:** at most three concurrent animations, nothing slower than 600ms, transform and opacity only — never width, height, top, or left, because those trigger layout. The target is CLS under 0.1 and LCP under 2.5s, and motion is not allowed to threaten it. - **The interactive pages** (drawing cards, reading results): up to six concurrent and durations up to a full second, because here the animation *is* the product and there's no ranking to protect. Same design language, two different speed limits. The card-flip can be theatrical on the draw page and is simply not present on a horoscope page that needs to rank. ## One source of truth, three outputs The motion tokens live in exactly one file. From there they're emitted to the three places code reads them: the Framer Motion constants, a set of CSS variables for non-Framer transitions, and the Tailwind config. The rule is that none of those three is ever hand-edited — you change the token and regenerate. It means the design system and the running site can't drift apart, which is the same discipline that let the whole project ship in a week: decide once, in one place, and let everything downstream inherit it. ## Reduced motion is a first-class path, not a footnote Every animated component checks for `prefers-reduced-motion` and has a real fallback, not a dead stop: the card flip becomes a cross-fade, the cosmic loader becomes a static icon, scroll reveals just appear, and the smooth-scroll layer doesn't initialize at all. Accessibility here isn't a compliance checkbox — it's the same budget logic applied to people, not just to metrics. If someone's told their device they want less movement, the cheapest, calmest version of the site is the correct one. ## The point None of this is exotic. It's a vocabulary, a budget, and a single source of truth — three small acts of discipline. But that's exactly the thread from [the wider experiment](/writing/the-bottleneck-moved): when a machine does the making at scale, your job is to set the rails it can't cross. Motion was just one more fence — and the fences, it turns out, were the real product. ## MCP-first marketing ops Date: 2026-06-25 · Tags: ai, marketing, process · URL: https://khoa.work/writing/mcp-first-marketing-ops Why I wrapped every data source — Shopify, Meta Ads, Search Console — as its own MCP server instead of one big script. The boring decision that makes everything else composable. When you build a [marketing OS](/writing/marketing-engineering) as one person, the first real architectural decision is also the least glamorous: how do you talk to the data? Meta Ads, Shopify, Google Search Console, GA4 — each has an API, and the obvious move is to call them all from one big Python script. I didn't. I wrapped every source as its own Model Context Protocol (MCP) server, and that one choice quietly shaped everything after it. ## The shape Three layers, each ignorant of the others' internals: | Layer | What it is | Example | |---|---|---| | Adapters | One MCP server per data source | `shopify`, `meta_ads`, `gsc`, `ga4` | | Skills | Markdown-defined analyses that call adapters | "daily revenue summary," "SEO striking-distance" | | Workflows | Python on a schedule that runs skills | 7am daily report → inbox + Drive | A skill never touches an API directly. It asks the `shopify` adapter for orders and the `meta_ads` adapter for spend, and it doesn't know or care how either one authenticates. That separation is the whole game. ## Why MCP instead of a monolith Four reasons, and none of them is hype. **It's IDE-agnostic.** The same adapters work whether I'm driving them from Claude Code, Claude Desktop, or another editor. The intelligence isn't welded to one tool's plugin system — it's a protocol any of them speak. I can change how I work without rewriting what I built. **It's composable.** Because every source is a uniform server, a single skill can pull from three of them at once — correlate Meta spend against Shopify revenue against Search Console impressions in one analysis — without any bespoke glue. New combinations are free. **It's testable.** Adapters and analysis are separable, so I can mock an MCP server's responses and test a skill's logic without burning live API quota or waiting on a real account. The thing that makes the analysis trustworthy is that I can test it in isolation. **It's future-proof for autonomy.** When the system eventually graduates from describing to [acting](/writing/marketing-engineering), the action surface is already a clean set of tools with clear boundaries — not a tangle of inline API calls I'd have to audit line by line. ## The cost, honestly It isn't free. Every adapter is more boilerplate than a quick API call would be, and there's a real learning curve to building MCP servers well. For a weekend hack, a monolith is faster. But this isn't a weekend hack — it's a system meant to grow for months and maybe outlive its first use. At that horizon the monolith is the expensive choice: the one where every new data source touches everything, every test needs live credentials, and moving tools means a rewrite. The boilerplate is a premium I'm paying up front to keep the thing composable and replaceable forever. ## The quiet payoff The reason this matters beyond my own setup: MCP turns "marketing data" into Lego. Once a source is a server, anyone's skill can use it, including future-me's. It's the same instinct as the [content fence](/writing/the-fence-around-the-machine) and the [demo-driven discipline](/writing/ship-evidence-not-features) — spend a little structure now so that everything downstream gets cheaper and more trustworthy. Boring, on purpose. Boring is what compounds. ## Marketing engineering Date: 2026-06-25 · Tags: marketing, ai, process · URL: https://khoa.work/writing/marketing-engineering I named it Jarvis. It pulls every scattered marketing number — Meta, Google, TikTok, Search Console — into one place a business owner can actually read. Here's the thesis behind building it. I named it Jarvis, after the assistant in Iron Man — because that's the fantasy, isn't it: one calm voice that has already looked at everything and just tells you what matters. The mundane problem it actually solves is less cinematic. A brand's marketing data lives in a dozen tabs — Meta Ads in one, Google Ads in another, TikTok, Instagram, Search Console, Shopify — and nobody, least of all the person paying for all of it, ever sees the whole picture at once. So Jarvis has one job, stated plainly: **gather the fragmented numbers from every channel into a single place an owner can read.** Not "go log into seven dashboards." One view that answers the questions an owner actually asks — how much did we spend across *everything* this week, which campaigns are working, which are bleeding, what should I hand to the agency, and is the agency actually delivering — with the SEO side, from Search Console, folded into the same picture. If a channel has data, Jarvis pulls it in. That's the other half of what I've been calling marketing engineering. [SoiTarot](/writing/the-bottleneck-moved) was the bet on *making* things with AI; Jarvis is the bet on *making sense* of the numbers that tell you whether any of it worked. ## What it actually is Not a dashboard. A dashboard makes *you* do the analysis — it just draws the chart and leaves you to read it. Jarvis is the opposite: it does the looking and hands you the conclusion. The design separates three concerns into three layers, with hard boundaries between them: - **Adapters** — every data source (Meta, Google, TikTok, Shopify, Search Console) wrapped as its own [MCP server](/writing/mcp-first-marketing-ops). - **Skills** — markdown-defined analyses that *compose* those adapters and apply an actual framework. - **Workflows** — plain Python on a schedule that runs the skills and drops a written report in your inbox. The first principle in the design doc is blunt about why: > "Every data source wrapped as an MCP server. Never call APIs directly from analyzers." A concrete flow straight from the architecture: at 7am a workflow pulls Meta, TikTok, and Shopify, computes blended ROAS, flags anything that deviates more than 50% from its 7-day average, and emails the summary. The owner reads one page instead of opening seven tabs and doing the math by hand. The shape of that one page (with illustrative numbers): ## Built in slices, proven by demos The project runs on a development log of 10-day cycles, and two of its guiding principles keep it honest. *Ship in slices* — "every 2 weeks ships something useful, no 3-month invisible builds." And [*demo-driven*](/writing/ship-evidence-not-features) — every feature lands with one side-by-side case showing the tool's read against the status quo, recorded in the repo's `demo-cases/`. The strategy doc even closes on the discipline it takes: > "Build something every week. Show no one. Ship at Day 90." The capability ladder is deliberately staged — **describe → recommend → act** — and each rung has to earn the next. Right now Jarvis describes: it tells you what happened, sharper and faster than a manual scan. Only once the describe layer has a pile of evidence behind it does it get to recommend, and only then, for the narrow reversible actions, to act. ## Where it's going The newest slices push past read-only: - **An inbox that drafts its own replies** — reading a support thread, checking the order, and writing the response as a *draft, never sent*. ([its own write-up here](/writing/inbox-that-drafts-itself).) - **Ad creative from inside the tool** — wiring an image model to the ad platforms so you can spin up campaign variants for A/B tests without leaving the cockpit. (In development.) The throughline is a single cockpit for everything an ecommerce brand's marketing actually touches — paid, organic, customer replies, creative — with a human still in the chair for the calls that matter. For most of marketing's history the leverage was budget and headcount. That's ending. One person who can wrap a data source, write an analytical skill, and schedule it has more throughput than a small team did five years ago — and the ceiling stops being how many reports you can run and becomes whether you know which question is worth asking. I'm betting on the hyphen: not a marketer, not an engineer, but a marketing engineer. Jarvis is the artifact. The real output is becoming the kind of marketer who builds. The two foundations under it, each its own piece: [why I went MCP-first](/writing/mcp-first-marketing-ops), and [why I ship evidence, not features](/writing/ship-evidence-not-features). ## The inbox that drafts itself Date: 2026-06-25 · Tags: ai, process, marketing · URL: https://khoa.work/writing/inbox-that-drafts-itself The newest slice of Jarvis: a support inbox that reads the thread, checks the order, and writes the reply — as a draft, never sent. Here's the most boring, most expensive job in an ecommerce brand's day. A customer asks "where's my order?" To answer, someone opens the thread and reads the whole back-and-forth, switches to the admin or order-management tool to look up the actual status, switches back, and writes a reply. Then does it again. And again, all day. It's not hard — it's just relentless, and it eats the hours of the person who should be doing higher-value work. So the newest slice of [Jarvis](/writing/marketing-engineering) takes aim at it. Not by replacing the human who hits send — by doing the reading and the drafting *up to* that point. ## What it does Once a day, early, it works through the support inbox and answers one question per thread: does this actually need a reply from us right now? For the ones that do, it reads enough of the history to understand the real ask, pulls the order context, and writes a clean draft for a human to glance at and send. The specification is uncompromising about the line it will not cross: > "Review real customer emails, identify which ones need a reply, read the relevant thread context, and create clean Gmail drafts for the team to review. **It must never send emails automatically.**" Drafts only. A person is always the one who sends. What it leaves behind each morning looks like this (illustrative): ## The hard part is knowing what to ignore Most of an inbox isn't customers waiting on you. It's newsletters, promotions, vendor and logistics noise, internal chatter, threads already resolved, and threads where the customer is the one who still owes a reply. Draft into all of that and you've made a mess, not a help. So most of the design is a *skip list* — the spec says, in effect, only draft "where the brand is the next responder," and then enumerates everything to leave alone. That restraint is the whole product. A tool that drafts everything is noise; a tool that drafts only the five threads that genuinely need you is leverage. ## Human-in-the-loop, by design Two details make it safe to trust: - **Placeholders, not guesses.** When a fact isn't in front of it — a date, an availability, a policy call — it writes an obvious blank like `[ENTER DATE]` or `[CONFIRM AVAILABILITY]` rather than inventing one. The draft flags its own uncertainty. - **A run summary, every time.** Each pass reports how many threads it reviewed, how many drafts it created, and how many it skipped because they were waiting, already replied, or resolved. That count is the other thing the owner wanted: a read on the queue itself — how much is pending, how much got handled, how much in a week. ## Still in development, and that's the point I'll be honest about the status, because the [development log](/writing/ship-evidence-not-features) is: this one isn't validated end-to-end yet. It's built to *abort* rather than misbehave when something isn't right, and the plan is exactly the demo-driven one — connect it to the real inbox, do a run, read the drafts by hand, and only trust it once the drafts are good enough that a human is just clicking send. That's the same shape as the rest of Jarvis. Automate the relentless part — the reading, the lookup, the first draft — and keep the human for the one move that actually carries judgment and risk: sending. The machine clears the queue down to the few that need a person. The person stays in charge of the words that go out the door. ## Designing a brand before a line of code Date: 2026-06-25 · Tags: design, process, ai · URL: https://khoa.work/writing/designing-a-brand-before-code How SoiTarot got a look before it got a build — three directions, one locked art direction, and a design system a machine could hold the line on. This is a companion to [the build story](/writing/the-bottleneck-moved) — the part about why I spent the first days of a one-week project not writing code, but deciding how the thing should look. The reasoning is blunt: a content farm that looks like a content farm is dead on arrival. The moment a visitor — or a quality rater — clocks that a site is mass-produced, every other signal works against you. So before a single page was generated, I wanted [SoiTarot](https://soitarot.vn) to have the one thing most programmatic sites never get: a point of view you can see. ## Three directions, then a decision I didn't start by designing a homepage. I started by designing *three*, in Claude Design, on a single canvas I could look at side by side: Each was a different bet on what the site *is* — a publication, an experience, or a serious tool. Generating three is the whole point. One direction is a guess you fall in love with; three is a decision you can actually make, because you're comparing real rendered screens instead of arguing about adjectives. I picked the deck-forward hero and kept the editorial restraint underneath it — and because the directions were real artboards, "picking" meant pointing, not redrawing. ## One art direction, held everywhere The thing that makes a generated site feel handmade isn't any single screen. It's consistency — a thousand assets that look like one hand made them. So I locked a single art direction and named it, because a named thing is easier to enforce: **Engraved Celestial Etching**. Deep navy, antique gold linework, lavender and ivory, the texture of an old astronomical plate. The identity got a name too — **The Crescent Eye** — and a tight palette I refused to expand: Type did three jobs and no more: Fraunces for display, Inter for the interface, JetBrains Mono for labels. Every tarot card, every zodiac sign, every number later got generated to that one spec — which is the only reason a thousand AI images don't read as a thousand different moods. You can [open the whole design system in the open](/projects/soitarot/design/index.html) — brand guidelines, the color system, logo directions, the component kit, the motion spec. It's the actual working export, not a tidied-up case-study reconstruction. ## A system a machine can't drift from Here's where design stops being pictures and becomes infrastructure. A solo human can hold a look in their head. A pipeline of agents generating pages at scale cannot — they'll drift, and drift is exactly what reads as slop. So the design had to become *rules*, not vibes. Everything went into a single token file — color, type, spacing, and especially motion — as the one source of truth. From there it fanned out to the places code actually reads: the component constants, the CSS variables, the Tailwind config. Change the token, and every surface moves together; there's no second place where a color or a timing can quietly disagree. That sounds like over-engineering for a one-week build. It's the opposite — it's what let the build be one week, because nothing downstream had to be re-decided. The deeper half of that system — how motion was tokenized and then capped so it couldn't wreck performance across fifteen thousand pages — is its own story: [motion under a Core Web Vitals budget](/writing/motion-under-a-cwv-budget). ## What I'd take to the next one Designing the brand before the code felt slow on day one and bought back the whole rest of the week. The lesson isn't "design first" as a platitude. It's that when a machine is going to do the making, your design isn't a mockup — it's the **specification the machine obeys**. Vague taste doesn't survive contact with a generator. Named directions, a locked palette, and tokens that can't be argued with do. Every locked choice becomes a default the whole system inherits — and [defaults are the most powerful, least-examined decisions in software](/writing/cost-of-a-default). That's the same thread running through [the whole experiment](/writing/the-bottleneck-moved): the work moved from making things to deciding what's worth making and writing it down precisely enough that something else could execute it. ## Combinatorial SEO Date: 2026-06-25 · Tags: seo, process, ai · URL: https://khoa.work/writing/combinatorial-seo Why one person can win a content-hungry niche: a lot of search demand is just combinations, and combinations are exactly what a system is good at. The reason I picked tarot for [the experiment](/writing/the-bottleneck-moved) wasn't that I love tarot. It was the shape of the demand. A huge amount of what people search in this niche isn't a thousand unrelated questions — it's a handful of dimensions multiplied together. And multiplication is the one thing a system does effortlessly and a human does miserably. ## Demand that multiplies Look at what the searches actually are. Zodiac compatibility isn't one topic; it's every sign against every other sign. Card meanings aren't one page; they're the whole deck, upright and reversed. Stack the dimensions and the page count explodes — not with filler, but with genuinely distinct, genuinely searched questions: | Content type | The combination | Pages | |---|---|---| | Zodiac compatibility | 12 signs × 12 signs | 144 | | Tarot card meanings | 78-card deck | 78+ | | Daily horoscopes | 12 signs, refreshed | 12 | | Numerology | 9 life-path numbers | 9+ | | Spreads & guides | stacked on top | many | Each of those cells is a real query with a real person behind it, and "Cancer and Sagittarius" is a different page from "Cancer and Leo" — different content, not a spun template. A person writing these by hand burns out around page forty. A system treats the whole grid as a loop. ## This is why a solo build is even possible Combinatorial demand is the great equalizer for a one-person operation. You can't out-write a publisher with a staff. But you *can* out-*system* one, because the work isn't writing 144 pages — it's designing one excellent compatibility template and one good data model, then letting the grid fill itself. The labour collapses into a spec. That's the whole reason [a room of agents](/writing/a-room-of-agents) could cover a niche this big in a week. > Combinatorial niches reward systems thinking over stamina. You don't write the pages; you design the grid and the standard, and the pages are a consequence. ## The trap, and the guardrail There's an obvious failure mode here, and it's the one Google has spent years learning to punish: programmatic pages that are technically distinct but practically empty — doorway pages, thin templates, index bloat. Combinatorial scale is a loaded gun pointed at your own domain if the cells are junk. Which is exactly why the [content-quality fence](/writing/the-fence-around-the-machine) mattered so much. The combinatorics generate the *volume*; the gates guarantee each cell clears a real bar before it ships — genuine content, natural language, proper structure — so the grid grows the domain instead of poisoning it. Scale and quality aren't in tension here; one produces the pages and the other decides which deserve to exist. ## What it's doing The early data backs the shape: the site is climbing on long-tail combinatorial pages with [zero off-page work](/writing/the-bottleneck-moved), because each cell answers a specific question better than a generic listicle does. The head terms will need authority I haven't built yet, but the long tail — the 144 cells, the 78 cards — is precisely where a well-made grid quietly wins. Pick a niche whose demand multiplies, design the grid once, fence the output, and a single person plus a system can cover ground that used to need a team. The combinations were never the hard part. Deciding what a good cell looks like — and refusing the ones that aren't — was. ## The answer engines haven't called Date: 2026-06-25 · Tags: seo, ai, process · URL: https://khoa.work/writing/answer-engines I did the AEO work — llms.txt, structured data, doors open to every AI crawler. ChatGPT still hasn't cited SoiTarot once. Here's why that's exactly what I'd expect. Half of the reason to run [the SoiTarot experiment](/writing/the-bottleneck-moved) was the new front in search: the answer engines. More and more queries never reach a blue link — they get resolved inside ChatGPT, Perplexity, or Google's AI Overviews, which read the web and summarize it. So I built SoiTarot to be *readable by machines that answer*, not just crawlable by ones that rank. Then I watched. And so far, the answer engines haven't called. ## What "building for AI answers" actually meant AEO — answer engine optimization — isn't mystical. It's mostly about being legible and being trusted. SoiTarot got the legible part on day one: | Signal | Status | |---|---| | `llms.txt` (a map for language models) | shipped | | Structured data — Article, FAQ, HowTo, Breadcrumb | on every page | | Doors open to AI crawlers | welcomed, not blocked | | IndexNow ping on every deploy | automatic | | Clean, passage-shaped content (a system can quote) | by design | That's the whole legibility checklist, and it was cheap — the same [fence and structure](/writing/the-fence-around-the-machine) that served Google also serve an LLM trying to extract a clean answer. If an answer engine *wants* to cite SoiTarot, nothing on the page stops it. ## The scoreboard, honestly Here's the part people don't show you. A few weeks in, the third-party tools that track AI visibility report the same number across ChatGPT, Perplexity, Google AI Overviews, and Gemini: > AI citations, so far: zero. Not low — zero. Google's classic index is filling steadily; the answer engines haven't quoted the site once. And I think that's correct, not broken. ## Why zero is the expected number Legibility gets you *eligible* to be cited. It doesn't get you *chosen*. Answer engines are, if anything, more conservative than ranking — when a model puts a claim in front of a user as an answer, it leans on sources that already carry authority signals: links, brand mentions, a known entity, age. SoiTarot has none of those yet. It's three weeks old, has [zero backlinks](/writing/the-bottleneck-moved), and no entity to speak of. So the same ceiling that keeps the big head terms on page two of Google keeps SoiTarot out of the answer boxes: the authority layer I deliberately haven't built. Citations are a *trailing* signal — they tend to follow traditional ranking and real-world mentions, not lead them. Zero at week three isn't a failure of the setup; it's the setup waiting for the authority it hasn't earned. ## What I'm actually watching This is why I'm treating AI visibility as the second curve to watch, behind organic keywords. The hypothesis is simple and checkable: as the long tail matures and the domain ages, the *first* AI citations should appear on the low-competition, high-specificity pages — the exact "Cancer and Sagittarius" [combinatorial cells](/writing/combinatorial-seo) where SoiTarot genuinely answers a narrow question better than a generic listicle. If AEO works the way I think, that's where it cracks first. I built the site so the answer engines *can* quote it. Whether they *will* is the open question — and the honest status board reads zero. I'd rather show you the zero than pretend the doors-open part was the hard part. The doors were easy. Earning the knock is the work that's still ahead. ## A room of agents Date: 2026-06-25 · Tags: ai, process · URL: https://khoa.work/writing/a-room-of-agents One agent doing everything is a mess. The trick to building SoiTarot solo was a small team of them that could hand work off without me in the middle. The naive way to build with AI is one chat window and a human frantically copy-pasting between it and everything else. That human is the bottleneck, and it's me, and I get tired. Building [SoiTarot](https://soitarot.vn) in a week meant getting myself out of the middle — running not one agent but a small team of them that could pass work to each other. This is the operations half of [the experiment](/writing/the-bottleneck-moved). ## Specialists beat a generalist I set up the build inside Claude Code as four roles, not one do-everything assistant. Each had a job, a lane, and a point of view: | Agent | Owns | |---|---| | Design | The system — tokens, components, art direction | | Content | Drafting and rewriting at volume | | Engineering | The Next.js build, the pipeline, the CI gates | | SEO | Schema, structure, internal links, indexing | A specialist with a narrow remit makes better decisions than a generalist trying to hold the whole project in its head at once — same reason you don't ask one person to be your designer, copywriter, engineer, and SEO. The division of labour was the first thing that made the pace possible. ## Shared memory is what makes a team a team Four agents that can't remember what the others did aren't a team; they're four strangers. The thing that turned them into one was a shared `.memory/` folder — a single place where decisions, tokens, and specs lived, plus a handoff protocol so each agent picked up where the last left off instead of starting cold. That's why the design agent's [motion tokens](/writing/motion-under-a-cwv-budget) showed up unchanged in the engineering agent's code, and why a decision made on day two didn't get quietly re-litigated on day five. The architecture decisions were written down, append-only, with reasons — so context was a file you read, not a conversation you had to have again. > The hard part of multi-agent work isn't the agents. It's the memory between them. Get the handoff right and a team of specialists outruns any single context window. ## The economics: cheap tokens for volume, good tokens for taste There's a money angle that matters at scale. Not every task deserves your best model. Bulk drafting — the first pass on thousands of pages — went to a fast, cheap model. The quality pass, the judgment calls, the things where taste lives went to a slower, sharper one. Spend your expensive tokens where they change the outcome and your cheap tokens everywhere else, and a four-figure pile of pages stays affordable instead of absurd. That split is the same principle as the rest of the build, just denominated in tokens: the scarce resource is judgment, so you spend it deliberately and let the cheap, fast machinery handle the volume — behind [a fence](/writing/the-fence-around-the-machine) that catches whatever the cheap pass gets wrong. ## What I actually was, by the end By the last day I wasn't writing pages or drawing cards. I was running a room — setting the specs, refereeing the handoffs, deciding what "done" meant. The work had moved up a level, from making the thing to directing the things that make it. That's the uncomfortable, interesting future the [whole project](/writing/the-bottleneck-moved) kept pointing at: the ceiling stops being how much you can do and becomes how well you can orchestrate. ## The cost of a default Date: 2026-02-02 · Tags: design, process · URL: https://khoa.work/writing/cost-of-a-default Every default is a decision someone made for a thousand people. Most of them are wrong. A default is the most powerful thing in any piece of software, and almost nobody designs them on purpose. Think about how a default actually works. It's the choice that gets made for everyone who doesn't choose — which, on any real product, is almost everyone. People don't change settings. They use what they're handed. So the person who set the default didn't pick an option; they picked an option *for a thousand strangers*, most of whom will never know a decision was made on their behalf. That's not a small responsibility. We treat it like one. ## The default is the product Here's a claim I'll defend: for the median user, the default configuration *is* the product. Everything behind a settings screen might as well not exist. You can ship a wildly flexible tool, but if the out-of-the-box state is wrong, you've shipped a wrong tool that a handful of power users will heroically fix for themselves. The flexibility is a fig leaf. It lets the team avoid the hard work of having an opinion. I've sat in the meeting where this happens. Someone raises a tradeoff — should the app start in this mode or that one — and instead of deciding, the team says "let's make it configurable." Everyone relaxes. The tension is gone. And what we've actually done is taken a decision *we* were equipped to make, with all our context, and pushed it onto the user, who has none. We dressed up an abdication as empowerment. ## Configurability is a tax Every setting has a cost, and the cost is paid by the wrong person. For us, a toggle is one afternoon of work. For the user, it's a small tax levied every time they hit the screen: a thing to read, a thing to understand, a thing to suspect they've gotten wrong. Multiply that by the dozens of toggles a "flexible" app accrues and you've built a product whose first impression is *homework*. The cruel part is that the people most harmed by bad defaults are the people least able to fix them — the ones who don't know the setting exists, don't know it's wrong, and would have no idea where to look. Power users route around bad defaults in seconds. Defaults are an accessibility issue dressed as a preferences screen. > A default isn't the safe, neutral choice. It's the loudest opinion in the product — and refusing to have one is itself an opinion, just a cowardly version. ## What a good default costs you Designing a real default means doing the thing the toggle let you avoid: picking who you're for. A default optimized for a first-time user will annoy the expert. A default optimized for the expert will lose the beginner in the first thirty seconds. You cannot serve both with one starting state, which is exactly why "make it configurable" is so seductive — it pretends the conflict isn't there. But the conflict is the job. Choosing a default forces you to name your primary user out loud, in a decision you can't take back without a migration. That's uncomfortable, and the discomfort is a sign you're doing it right. When I build now, I try to spend the hard hour on the default and ship *fewer* settings, not more — because each setting I add is a small confession that I didn't know who the product was for. ## A test I use Before adding a setting, I ask one question: *if I could only ship the default, would the product still be good?* If yes, the setting is a refinement, and fine. If no — if the product only works once the user fixes it — then I haven't finished designing. The toggle isn't flexibility. It's an unfinished decision with a UI bolted on. Most settings screens are graveyards of decisions the team couldn't bring itself to make. The best products I know have almost none, not because they're inflexible, but because someone did the expensive work of being right by default — and spared a thousand people the cost of choosing. ## Notes on shipping slowly Date: 2025-11-12 · Tags: process, life · URL: https://khoa.work/writing/shipping-slowly A defense of the unhurried build, written for an audience of one — me. I ship slowly, and for a long time I was embarrassed about it. The internet I grew up building on rewards a particular speed. Ship daily. Build in public. Post the screenshot before the paint is dry. Velocity as virtue. I absorbed all of it, and for years I measured myself against people who pushed ten times more than me and seemed, from the outside, to be winning. I was sure my slowness was a character flaw I'd eventually fix. I've stopped trying to fix it. Not because I got lazy — because I finally looked at what slow actually buys, and decided the trade was good. ## Fast is a kind of borrowing Speed isn't free; it's borrowed. When you ship fast, you ship before you fully understand the thing, which means you're taking a loan against future clarity. Sometimes that's the right call — the loan is cheap and you learn from the market faster than you'd learn from thinking. But the interest is real. Every fast decision you didn't understand becomes a piece of the product you now have to maintain, explain, and eventually unwind. Move fast and break things, and you spend next year sweeping up glass. Slow is the opposite trade. You pay up front, in patience, in the discomfort of not having shipped yet, in watching faster people lap you. What you get back is a thing with fewer regrets baked in — fewer decisions you made before you understood them, fewer toggles hiding choices you dodged, fewer apologies in the changelog. ## What slow is actually for The case for slow isn't "quality," which is what people say when they want to sound noble about being behind. It's more specific than that. Slow is for the decisions that are expensive to reverse. Most choices in a product are cheap to undo — a color, a label, a layout. You should make those fast and fix them later. But a few choices are load-bearing: the data model, the core interaction, the one opinion the whole thing rests on. Get those wrong and no amount of later speed digs you out. Slow is how I make sure I've actually seen those decisions before I commit to them. I'm not slow at everything. I'm slow at the things I can't take back. ## The honest cost I won't pretend it's all upside. Shipping slowly means watching ideas you love get built by someone faster. It means a thinner public record, fewer launches, a quieter feed. If your sense of worth is tied to visible output — and mine was, for years — slow is genuinely painful. There's no version of this where you get the calm without paying for it in patience and in the occasional sting of being passed. What I've found is that the work I'm proudest of is all slow work. The tools I still use years later, the writing that holds up, the decisions I don't wince at — none of it came from a sprint. It came from staying with something past the point where I wanted to be done. So this is a note to myself, mostly. The slowness isn't the bug. It's the rate at which I can build things I won't have to apologize for. I'm going to stop trying to speed it up.