<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet type="text/xsl" href="/feed.xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>All about Ken Hawkins</title>
  <subtitle>Platform Architect &amp; Digital Strategy Lead who translates between executives, developers, and users—building platforms that work, ship on time, and solve real problems. 20+ years delivering measurable results for UN, science, and media organizations. Expert in design systems, performance engineering, and AI-ready architecture.</subtitle>
  <link href="https://www.allaboutken.com/rss.xml" rel="self"/>
  <link href="https://www.allaboutken.com"/>
  <updated>2026-06-09T00:00:00.000Z</updated>
  <id>https://www.allaboutken.com/</id>
  <author>
    <name>Ken Hawkins</name>
    <email>khawkins98@gmail.com</email>
  </author>
  <entry>
    <title>CADI: a prescriptive path for slop-free AI in content work</title>
    <link href="https://www.allaboutken.com/posts/20260609-iterative-pattern-framework/"/>
    <published>2026-06-09T00:00:00.000Z</published>
    <updated>2026-06-09T00:00:00.000Z</updated>
    <id>https://www.allaboutken.com/posts/20260609-iterative-pattern-framework/</id>
    <author>
      <name>Ken Hawkins</name>
      <email>khawkins98@gmail.com</email>
    </author>
    <category term="AI" />
    <category term="editorial-standards" />
    <category term="evidence-based-practice" />
    <category term="iterative-improvement" />
    <category term="workflow-automation" />
    <summary type="html">A framework to collect, analyze, document, iterate (CADI, for short) is a prescriptive answer to the question everyone keeps asking: should I prompt better, write a style guide, build an agent, or what?</summary>
    <content type="html">
&lt;p&gt;Recently I was in a meeting with colleagues talking about how people across the organisation are using AI in their writing. What I heard:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;&amp;quot;I just prompt it and ship. We don&#39;t have time to fuss.&amp;quot;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&amp;quot;I use it for ideas, but I still write the piece myself.&amp;quot;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&amp;quot;I rewrite every line. I&#39;m honestly not sure I&#39;m saving any time.&amp;quot;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&amp;quot;It&#39;s infuriating.&amp;quot;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&amp;quot;I see other people using it and I see the same repetitive AI tells bleeding through.&amp;quot;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&amp;quot;I&#39;m not convinced any of this is worth it. The old way was quieter and probably better.&amp;quot;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&amp;quot;Other organisations are using it. We can&#39;t be the only ones not.&amp;quot;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;—&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&amp;quot;Should I just get better at prompting?&amp;quot;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&amp;quot;Should I write a style guide?&amp;quot;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&amp;quot;Should I build an agent?&amp;quot;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&amp;quot;Should I document my preferences somewhere?&amp;quot;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&amp;quot;Should I upload my existing content?&amp;quot;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The honest answer is always some version of &amp;quot;yes, but in a specific order, and most of the value comes from the combination.&amp;quot; Helping colleagues do better is frustrating as it&#39;s a repetitive conversation and it&#39;s far too conceptual for many to put in practice.&lt;/p&gt;
&lt;p&gt;So I documented it. This is &lt;strong&gt;CADI&lt;/strong&gt; — Collect, Analyze, Document, Iterate — a prescriptive path through the confusion.&lt;/p&gt;
&lt;p&gt;The name nods to a caddie in golf: carries the load, doesn&#39;t take the swing. It&#39;s designed to be followed roughly in order, at least the first time through.&lt;/p&gt;
&lt;p&gt;An experienced editor with access to your content can produce a first usable version of the guidance in a few hours by feeding curated examples to a language model and shaping what comes back. Then it&#39;s an editorial process: share it with colleagues, use it for a few days, fold their reactions in. Each round sharpens the guidance; the work doesn&#39;t get heavier, the picture gets clearer. It works for any content type or language, as long as you have enough of it to learn from.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-problem-every-prompt-starts-from-zero&quot; tabindex=&quot;-1&quot;&gt;The problem: every prompt starts from zero&lt;a href=&quot;#the-problem-every-prompt-starts-from-zero&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Most AI writing assistance starts cold every time. No memory of what worked last time. No sense of your voice, your standards, your audience. So it gives you something generic and confident, and you spend the rest of your time correcting it.&lt;/p&gt;
&lt;p&gt;The fix isn&#39;t a better prompt. It&#39;s giving the AI a documented picture of what good looks like &lt;em&gt;for you&lt;/em&gt;, and building a way to improve that picture as you learn what works. Done right, the AI gets more useful over time rather than requiring the same effort on every task.&lt;/p&gt;
&lt;p&gt;There&#39;s a failure mode that persists even after you think you&#39;ve solved this: &lt;strong&gt;confident wrongness + silent drift&lt;/strong&gt;. The AI sounds authoritative but cites a source incorrectly. It hits approximately the right word count but adds a paragraph that subtly changes your meaning. These are harder to catch than obvious nonsense, and they accumulate faster.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;where-you-probably-are-supervised-co-writing&quot; tabindex=&quot;-1&quot;&gt;Where you probably are: supervised co-writing&lt;a href=&quot;#where-you-probably-are-supervised-co-writing&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Most people using AI for writing aren&#39;t doing &amp;quot;autonomous AI publishing.&amp;quot; They&#39;re doing &lt;strong&gt;supervised co-writing&lt;/strong&gt;: AI handles a first pass or a tedious task, a human reviews and corrects, a human publishes. That&#39;s normal; it&#39;s also where most of the real value is right now.&lt;/p&gt;
&lt;p&gt;CADI is for teams who want to move from &amp;quot;AI is something we sometimes use&amp;quot; to &amp;quot;AI is something our editorial standards reliably shape.&amp;quot; That transition doesn&#39;t happen through better prompting alone.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;before-you-start-five-things-to-check&quot; tabindex=&quot;-1&quot;&gt;Before you start: five things to check&lt;a href=&quot;#before-you-start-five-things-to-check&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This framework works best when you can answer yes to these:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You have &lt;strong&gt;ideally 50+ published items&lt;/strong&gt; you consider high-quality (not just recent). 10–20 from a tight, high-quality domain still works; you&#39;ll overfit a little, but the discipline of running the loop is what compounds&lt;/li&gt;
&lt;li&gt;You have some signal for quality: engagement data, editor picks, longevity tags, or even manual curation&lt;/li&gt;
&lt;li&gt;At least one person has &lt;strong&gt;time to own the guidance doc&lt;/strong&gt; and keep it current&lt;/li&gt;
&lt;li&gt;You have some way to &lt;strong&gt;export content&lt;/strong&gt; from your CMS in text form (Markdown, HTML, CSV)&lt;/li&gt;
&lt;li&gt;Editors are willing to &lt;strong&gt;log failures&lt;/strong&gt; rather than just silently fix them&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You don&#39;t need a data science team, a custom CMS, or a formal AI strategy. But if no one will own the guidance document, the method breaks down.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;pick-your-tool&quot; tabindex=&quot;-1&quot;&gt;Pick your tool&lt;a href=&quot;#pick-your-tool&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Before you start, decide how you&#39;ll feed your content to an LLM. The choice shapes every step that follows.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://notebooklm.google.com/&quot;&gt;NotebookLM&lt;/a&gt; or similar.&lt;/strong&gt; Best if your content is web-accessible. Drop URLs or PDFs as sources; run prompts in the chat. Zero file handling.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A local agent (Claude Code, GitHub Copilot CLI, or similar).&lt;/strong&gt; Best if you&#39;ve exported your content as files. Point the agent at the directory and ask it to run the prompts. Handles iteration and cleanup for you.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Manual pipeline (browser-only).&lt;/strong&gt; Fallback when the other two aren&#39;t options. You&#39;ll preprocess HTML, concatenate, and paste into a browser tool. The &lt;a href=&quot;https://www.allaboutken.com/resources/cadi-starter-kit.txt&quot;&gt;starter kit&lt;/a&gt; has the prompts.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The rest of the post assumes you&#39;ve picked one. When a step says &amp;quot;run the Analyze prompt&amp;quot; or &amp;quot;feed the corpus to your model,&amp;quot; substitute your chosen tool&#39;s mechanics.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-cadi-framework-collect-analyze-document-iterate&quot; tabindex=&quot;-1&quot;&gt;The CADI framework: Collect → Analyze → Document → Iterate&lt;a href=&quot;#the-cadi-framework-collect-analyze-document-iterate&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;CADI is a four-phase loop. You build it once, then run the last phase continuously. Each cycle makes the guidance sharper, and the AI more useful.&lt;/p&gt;
&lt;p&gt;The bit most AI-writing advice misses: the guidance document is something you test and revise. C, A, and D get you a first version. I is where it earns its keep.&lt;/p&gt;
&lt;p&gt;I&#39;ve arrived at this through practice, and written about the pieces separately. This is the first time I&#39;ve named it.&lt;/p&gt;
&lt;h3 id=&quot;c-collect-extract-your-best-examples&quot; tabindex=&quot;-1&quot;&gt;C — Collect: Extract your best examples&lt;a href=&quot;#c-collect-extract-your-best-examples&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Your best content already embodies your patterns: word counts, title structures, tag usage, narrative flow. Don&#39;t grab the latest 50 published items; &lt;strong&gt;curate the exemplary 50.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Start with one content subtype, not a mix.&lt;/strong&gt; Most organisations publish several things that look similar but follow different patterns: press releases, news briefs, feature reports, donor updates, blog posts. Each has its own voice and structure. Mixing them gives you average patterns that fit none of them. Pick the subtype that matters most to you right now and run CADI on that. Repeat for other subtypes later.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Don&#39;t default to top-traffic items.&lt;/strong&gt; That&#39;s the obvious choice and the most common mistake. High traffic often correlates with topic timeliness, social-share luck, or audience reach, not with the qualities you&#39;d want to reproduce. Three signals to combine instead:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Editorial investment&lt;/strong&gt; — items your team spent the most time on. Effort is a quality signal even when the numbers aren&#39;t there.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Aspirational fit&lt;/strong&gt; — items you&#39;d want more of your output to look like.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Performance &lt;em&gt;for type&lt;/em&gt;&lt;/strong&gt; — a press release that did 5× the median for press releases tells you more than a feature that hit Hacker News.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Document the criteria you used. It will matter when you audit later.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Bring your existing reference materials too.&lt;/strong&gt; Your best content shows the agent &lt;em&gt;what good looks like&lt;/em&gt; by example. Your existing editorial handbook, brand guidelines, glossary, terminology and spelling lists, institutional style memos — these tell it the &lt;em&gt;rules&lt;/em&gt; you want enforced. Add them as sources alongside the corpus. The agent should be able to &lt;em&gt;cite them&lt;/em&gt; when producing drafts (&amp;quot;per our spelling guide, we use &#39;organization&#39; with a z&amp;quot;) and &lt;em&gt;link to them&lt;/em&gt; when justifying its choices. If your handbook says &amp;quot;no Oxford comma,&amp;quot; the corpus pattern alone might not surface that as a hard rule.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Getting the files to your tool.&lt;/strong&gt; With NotebookLM, add the URLs or PDFs of your 50–100 best items as sources. With a local agent, export your content as HTML to a directory and point the agent there. With the manual pipeline, follow the four steps in the &lt;a href=&quot;https://www.allaboutken.com/resources/cadi-starter-kit.txt&quot;&gt;starter kit&lt;/a&gt;. One-time setup per content subtype.&lt;/p&gt;
&lt;p&gt;👉 &lt;strong&gt;Ready to move on when&lt;/strong&gt;: you have 50–100 items in a single consolidated, cleaned document; you&#39;ve written down why they qualify; and an editor has spot-checked for obvious outliers.&lt;/p&gt;
&lt;p&gt;For implementation detail, including Drupal export code and a worked UNDRR case study, see &lt;a href=&quot;https://www.allaboutken.com/posts/evidence-based-content-guidelines-drupal-ai/&quot;&gt;Improving AI chatbots with an editorial handbook from your best content&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;a-analyze-find-quantitative-patterns&quot; tabindex=&quot;-1&quot;&gt;A — Analyze: Find quantitative patterns&lt;a href=&quot;#a-analyze-find-quantitative-patterns&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Feed your curated examples to an LLM with a structured prompt to extract:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Average word counts (with ranges, by content type)&lt;/li&gt;
&lt;li&gt;Title formats and syntactic shapes (% using colons or question marks; whether titles assert claims, ask questions, restate the source headline, or use first-person observation)&lt;/li&gt;
&lt;li&gt;Heading and structural patterns (do longer pieces get H2s? At what word-count threshold? Lists vs. prose?)&lt;/li&gt;
&lt;li&gt;Voice markers (first vs. third person, contractions, hedging language, sentence-length variance)&lt;/li&gt;
&lt;li&gt;Source treatment (% linking sources inline, % using blockquotes from the source, whether the source is named in the first paragraph)&lt;/li&gt;
&lt;li&gt;Opening and closing patterns (does the first paragraph name a topic, a scene, a source? Does the last forward-link or name a broader pattern?)&lt;/li&gt;
&lt;li&gt;Cross-linking density (internal links per piece; what fraction connect to other content on the site)&lt;/li&gt;
&lt;li&gt;Metadata usage (tag counts, co-occurrence patterns)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;This list isn&#39;t exhaustive.&lt;/strong&gt; Your subtype will have its own tells — repeated structural moves, signature phrasing, specific rhetorical positioning. Add the signals that matter for your content; document what you watched for. It&#39;ll be the first thing you revisit on the next cycle.&lt;/p&gt;
&lt;p&gt;LLMs handle this extraction well; the interpretation is still yours. The output is not guidance yet; it&#39;s &lt;strong&gt;empirical description&lt;/strong&gt; of what your best content actually looks like.&lt;/p&gt;
&lt;p&gt;👉 &lt;strong&gt;Ready to move on when&lt;/strong&gt;: you have a written summary of patterns with numbers, and at least one editor has reviewed it and flagged anything that looks wrong or unrepresentative.&lt;/p&gt;
&lt;h3 id=&quot;d-document-turn-patterns-into-working-instructions&quot; tabindex=&quot;-1&quot;&gt;D — Document: Turn patterns into working instructions&lt;a href=&quot;#d-document-turn-patterns-into-working-instructions&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Combine what the analysis found with your existing institutional knowledge (style guides, brand standards, domain constraints) into a single document your AI tool can use as its standing instructions. Link or embed any existing reference docs your team already maintains; the guidance doc should point at them rather than restate them, so editors and the agent can both trace any rule back to its source.&lt;/p&gt;
&lt;p&gt;If your existing style guide already addresses this subtype, the Document step isn&#39;t to re-derive from scratch. It&#39;s to tighten the existing guidance with the numbers the Analyze step produced, and override only where you have evidence the guidance was wrong.&lt;/p&gt;
&lt;p&gt;This step requires editorial judgment: observed patterns and existing style rules will sometimes conflict. If the analysis says your best titles average 8 words but house style caps them at 6, decide which wins and log why. Write it down; that decision belongs in your change log, not left implicit.&lt;/p&gt;
&lt;p&gt;You end up with instructions concrete enough to act on (&amp;quot;180 words ± 10%&amp;quot;) rather than vague (&amp;quot;be concise&amp;quot;). Bonus: the same document works for human editors too.&lt;/p&gt;
&lt;p&gt;👉 &lt;strong&gt;Ready to move on when&lt;/strong&gt;: the instructions have been reviewed by at least one editor who wasn&#39;t involved in writing them, and they&#39;ve been uploaded or integrated with your AI tool.&lt;/p&gt;
&lt;h3 id=&quot;i-iterate-build-confidence-over-time&quot; tabindex=&quot;-1&quot;&gt;I — Iterate: Build confidence over time&lt;a href=&quot;#i-iterate-build-confidence-over-time&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Iteration done badly is just tweaking in the dark, which is how we started. Done well, it&#39;s hypothesis-driven:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;quot;IF we add explicit numeric targets (&#39;180 words ± 10%&#39;), THEN first-pass acceptance rises from 40% to 70%&amp;quot; &lt;em&gt;(illustrative)&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&amp;quot;IF we show exemplars of title styles, THEN AI chooses appropriate titles 85% of the time&amp;quot; &lt;em&gt;(illustrative)&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&amp;quot;IF we run oppositional review (a second model critiques our guidance), THEN we catch biased rules 80% of the time&amp;quot; &lt;em&gt;(illustrative)&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then test them: run a simple before/after comparison on real content, measure outcomes with a concrete metric, and document what changed and why.&lt;/p&gt;
&lt;p&gt;For a small team (2–3 editors), 20–30 items per condition is enough for an honest qualitative comparison: better than tweaking blind, not a statistical signal. Anything closer to statistical rigour needs much larger samples and blinded review; most editorial teams won&#39;t reach that, and that&#39;s fine. The bar is qualitative honesty: what changed, and why.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Mid-cycle revision trigger&lt;/strong&gt;: if more than 20% of outputs in a week fail the same check, don&#39;t wait for the quarterly audit. That&#39;s a pattern worth investigating now.&lt;/p&gt;
&lt;p&gt;None of this is invented from scratch. Other disciplines have been at versions of this for decades:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://support.writer.com/article/250-how-to-calibrate-voice-for-your-content&quot;&gt;Writer&lt;/a&gt; and &lt;a href=&quot;https://help.jasper.ai/hc/en-us/articles/18618693085339-Brand-Voice&quot;&gt;Jasper&lt;/a&gt; will both reverse-engineer a voice profile from examples of your own content; worth knowing about, and worth using if they fit your stack.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/2309.09128&quot;&gt;ChainForge&lt;/a&gt; (CHI 2024) formalized treating prompts, and the evaluation criteria themselves, as testable hypotheses.&lt;/li&gt;
&lt;li&gt;Medical guideline development (AGREE II and GRADE) assess whether guidelines are evidence-based and implementable. The &lt;a href=&quot;https://pmc.ncbi.nlm.nih.gov/articles/PMC7657075/&quot;&gt;Australian COVID-19 living guidelines&lt;/a&gt; went further: revised and republished weekly for 24 weeks as new evidence arrived, a continuously iterated guidance document running in production&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But a software subscription isn&#39;t a method, and a research paper isn&#39;t a guide. What I haven&#39;t seen elsewhere is the connected discipline: deriving guidance from your own curated corpus, then keeping the failure log and test record that make that guidance revisable on evidence rather than vibes. That combination is what CADI names. If reading this sends you off to subscribe to one of those tools or build your own loop from the research, good; that&#39;s the point. Prior art still welcome.&lt;/p&gt;
&lt;p&gt;👉 &lt;strong&gt;Ready to move on when&lt;/strong&gt;: you have at least one completed hypothesis test, a documented decision (kept or rejected), and a failure log that someone is actually adding to.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;cadi-in-practice-how-it-played-out-at-undrr&quot; tabindex=&quot;-1&quot;&gt;CADI in practice: how it played out at UNDRR&lt;a href=&quot;#cadi-in-practice-how-it-played-out-at-undrr&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;One case illustrates the full loop:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Collection&lt;/strong&gt;: 122 publications flagged as &amp;quot;evergreen + high-engagement&amp;quot; in Drupal&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Analysis&lt;/strong&gt;: LLM extracted patterns. &amp;quot;169 words average; 56% use colons in title; all use 2–4 themes&amp;quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Documentation&lt;/strong&gt;: Guidance doc drafted and uploaded as standing instructions in Microsoft 365 Copilot&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Iteration&lt;/strong&gt;: Quarterly audits checked whether new AI drafts matched patterns; a shared change log recorded what was adjusted and why&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The honest version of the result: before CADI, none of these pieces (curation, pattern extraction, guidance docs, iteration) reliably produced anything usable on its own. They were ad-hoc, inconsistent, and each task started over. Running them as a connected loop turned a set of one-off attempts into a process that ran continuously and got better. The framework&#39;s value wasn&#39;t a single big number; it was making something repeatable that hadn&#39;t been. The longer case file (the original methodology, plus what the guidance doc looked like in practice) is in the &lt;a href=&quot;https://www.allaboutken.com/work/2025/impact-story-evidence-based-content-guidelines/&quot;&gt;UNDRR impact story&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;And I&#39;m not pretending what I have is rigorous yet. What would make the next version &lt;em&gt;genuinely rigorous&lt;/em&gt; is a prospective A/B test that quantifies which guidance changes move which outcomes by how much. That&#39;s the piece most teams skip; writing this down, sharing it, and letting it be challenged is part of how I keep tightening the loop.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;using-cadi-in-the-writing-workflow&quot; tabindex=&quot;-1&quot;&gt;Using CADI in the writing workflow&lt;a href=&quot;#using-cadi-in-the-writing-workflow&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;CADI gives you the guidance layer. The day-to-day writing that uses it has a shape of its own:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Ideation&lt;/strong&gt; — what are we writing, for whom, why now? AI helps brainstorm; the editorial decision is yours.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scaffolding&lt;/strong&gt; — outline, key points, source material. CADI&#39;s guidance doc steers tone and structure; the agent helps assemble.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Drafting&lt;/strong&gt; — produce the first pass with the guidance doc as the agent&#39;s standing context.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Review&lt;/strong&gt; — apply role-based passes for the checks this piece needs.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The review step is where you pick agent roles that match the piece&#39;s risk and shape. The ones I&#39;ve found useful:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Copy editor&lt;/strong&gt; — grammar, flow, voice consistency against the guidance doc&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Adversarial reviewer&lt;/strong&gt; — does the argument hold? What&#39;s the strongest counter?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Legal / compliance check&lt;/strong&gt; — for content with regulatory exposure&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Copyright check&lt;/strong&gt; — for content with quotation, imagery, or external sourcing&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Technical reviewer&lt;/strong&gt; — for content making claims about systems, code, or data&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sanity check&lt;/strong&gt; — final read for &amp;quot;does this make sense, end to end?&amp;quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Not every piece needs every role; pick what fits the content type and its risk profile. The CADI guidance doc gets stronger when failure-log entries name &lt;em&gt;which role&lt;/em&gt; would have caught the failure — that&#39;s how you learn which checks belong in which subtype&#39;s workflow.&lt;/p&gt;
&lt;p&gt;A deeper walk-through, with prompts and examples for each role, is coming in a follow-up post.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;trust-but-verify-the-publication-gate&quot; tabindex=&quot;-1&quot;&gt;Trust but verify: the publication gate&lt;a href=&quot;#trust-but-verify-the-publication-gate&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The trust-but-verify gate sits between any CADI-assisted draft and publication. Don&#39;t skip it, even once you trust the system:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Verify factual claims and sources independently&lt;/li&gt;
&lt;li&gt;Verify hard constraints (word/character counts, required fields). Don&#39;t trust the model&#39;s own count&lt;/li&gt;
&lt;li&gt;Verify meaning hasn&#39;t drifted from source intent&lt;/li&gt;
&lt;li&gt;Keep human sign-off as the final release gate&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For many teams, a &lt;strong&gt;parallel run&lt;/strong&gt; is the safest transition: keep your normal manual editorial review while running AI-assisted output alongside it. Compare decisions, measure drift, then scale what proves useful.&lt;/p&gt;
&lt;p&gt;This is also the spirit of the editorial AI policies emerging in newsrooms, where &lt;a href=&quot;https://www.allaboutken.com/posts/20260423-digesting-ars-technica-ai-newsroom-policy/&quot;&gt;Ars Technica and Fedora make &amp;quot;reviewed&amp;quot; a protected verb&lt;/a&gt; with the human review step explicit and named. CADI takes the same position: the loop doesn&#39;t replace the gate; it makes the gate cheaper to operate.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;where-this-breaks-down&quot; tabindex=&quot;-1&quot;&gt;Where this breaks down&lt;a href=&quot;#where-this-breaks-down&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Iteration goes blind again when:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Encoding bias without inspection&lt;/strong&gt;: a pattern like &amp;quot;short sentences good&amp;quot; might just reflect that your exemplars targeted beginners; applying it universally infantilizes advanced content&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Optimizing for fit, not truth&lt;/strong&gt;: &amp;quot;this tag co-occurs 26% of the time, so ALWAYS pair them&amp;quot; (should be &amp;quot;consider pairing&amp;quot;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No quality gate on source&lt;/strong&gt;: including mediocre content in your &amp;quot;best&amp;quot; set skews guidance toward mediocrity&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stopping too early&lt;/strong&gt;: 10 examples isn&#39;t enough; 500 might over-fit&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Expecting voice cloning in informal genres&lt;/strong&gt;: the style-imitation research is encouraging for structured formats like news and email, and &lt;a href=&quot;https://arxiv.org/abs/2509.14543&quot;&gt;much weaker for informal, personal writing&lt;/a&gt; like blogs and forum posts. Corpus-derived guidance reliably reproduces structure and conventions; a distinctive personal voice still needs the human pass&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Prevention: transparent curation criteria, documented assumptions, human review between cycles, oppositional analysis (ask a second model to critique your instructions), and built-in escape hatches (&amp;quot;usually X, unless Y&amp;quot;).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Where CADI doesn&#39;t fit cleanly:&lt;/strong&gt; high-volume newsrooms shipping dozens of pieces per week across many content subtypes. CADI assumes one primary subtype at a time and a corpus you can read in full. Sampling methodology and per-subtype parallel runs are needed there. Out of scope for this post, but the same loop applies once you&#39;ve made those decisions.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;what-you-maintain-alongside-the-loop&quot; tabindex=&quot;-1&quot;&gt;What you maintain alongside the loop&lt;a href=&quot;#what-you-maintain-alongside-the-loop&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;These four documents are what CADI actually leaves you with. The starter kit has templates for each: &lt;a href=&quot;https://www.allaboutken.com/resources/cadi-starter-kit.txt&quot;&gt;cadi-starter-kit.txt&lt;/a&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A curation criteria note&lt;/strong&gt; — why these items qualified as exemplary, and not others&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Your working AI instructions&lt;/strong&gt; — the guidance doc the AI uses as its standing context&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A failure log&lt;/strong&gt; — the one document that&#39;s more valuable than the guidance doc itself; it&#39;s where the next iteration starts&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A test record&lt;/strong&gt; — what you tested, what changed, and why&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If no one is keeping at least the failure log current, you&#39;re not iterating; you&#39;re just running C/A/D once and hoping.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;after-your-first-cycle-go-deeper&quot; tabindex=&quot;-1&quot;&gt;After your first cycle, go deeper&lt;a href=&quot;#after-your-first-cycle-go-deeper&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;These earlier posts go deeper on individual phases. You don&#39;t need to read them to start.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://www.allaboutken.com/posts/evidence-based-content-guidelines-drupal-ai/&quot;&gt;Improving AI chatbots with an editorial handbook from your best content&lt;/a&gt;&lt;/strong&gt;: the C and A phases in detail, with Drupal export code and the full UNDRR case study (updated June 2026)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://www.allaboutken.com/posts/20250923-effective-ai-work-is-project-management/&quot;&gt;The power of the pause: How planning beats prompt tuning&lt;/a&gt;&lt;/strong&gt;: structure over tweaking, with PROJECT_BRIEF → PROJECT_PLAN → PATTERNS workflow&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://www.allaboutken.com/posts/20250919-smart-ai-integration-context-first/&quot;&gt;Thoughtful AI integration beats bolted-on Clippy&lt;/a&gt;&lt;/strong&gt;: product thinking for AI integration, when to use it, not just how to prompt it&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://www.allaboutken.com/posts/20260330-context-was-always-the-job/&quot;&gt;Why context engineering was always the job&lt;/a&gt;&lt;/strong&gt;: where CADI sits in the broader move toward context as the work&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://www.allaboutken.com/posts/20260405-four-phases-of-ai-adoption/&quot;&gt;The four phases of AI adoption&lt;/a&gt;&lt;/strong&gt;: the Complement / Integrate / Delegate / Orchestrate taxonomy; CADI lives around Integrate and Delegate&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;CADI is the loop we run at UNDRR. Think of it as a forcing function more than a magic generator: most of the work is still you reading your own content carefully. The framework just makes sure you do it, and write down what you learn.&lt;/p&gt;
&lt;p&gt;It works for us; it might work for you; or it might need reshaping for the volume, content type, or constraints you&#39;re in. I&#39;d be curious how it lands once you try it: what you change, what breaks, what surprises.&lt;/p&gt;
&lt;p&gt;One direction I&#39;m exploring: packaging CADI as a guided Claude skill (or similar) that asks where your content lives, walks you through the curation criteria, runs the Analyze prompt for you, and hands you a draft guidance document ready for editorial review. If that&#39;d be useful, drop me a note.&lt;/p&gt;
&lt;p&gt;The first run of this framework on this site is preserved as a worked artifact: &lt;a href=&quot;https://www.allaboutken.com/resources/cadi-cycles/2026-06-digesting/&quot;&gt;CADI cycle 1 — digesting posts (June 2026)&lt;/a&gt;. Future cycles will build on it.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;references&quot; tabindex=&quot;-1&quot;&gt;References&lt;a href=&quot;#references&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Bsharat et al., &amp;quot;&lt;a href=&quot;https://arxiv.org/abs/2312.16171&quot;&gt;Principled Instructions Are All You Need&lt;/a&gt;&amp;quot; (arXiv:2312.16171): a preprint reporting an average 57.7% quality boost from 26 generic prompting principles on GPT-4. Small, author-judged benchmark; read it as directional support for structured prompts, not as evidence for corpus-derived guidance specifically&lt;/li&gt;
&lt;li&gt;&amp;quot;&lt;a href=&quot;https://arxiv.org/abs/2509.14543&quot;&gt;Catch Me If You Can? Not Yet: LLMs Still Struggle to Imitate the Implicit Writing Styles of Everyday Authors&lt;/a&gt;&amp;quot; (EMNLP 2025 Findings): exemplar-based prompting consistently beats instruction-only prompting for style matching; strong in structured genres, weak in informal ones&lt;/li&gt;
&lt;li&gt;Wu et al., &amp;quot;&lt;a href=&quot;https://arxiv.org/abs/2302.07346&quot;&gt;ScatterShot: Interactive In-context Example Curation for Text Transformation&lt;/a&gt;&amp;quot; (ACM IUI 2023): systematic example curation beats ad-hoc hand-picking, which tends to capture only the most obvious patterns&lt;/li&gt;
&lt;li&gt;Arawjo et al., &amp;quot;&lt;a href=&quot;https://arxiv.org/abs/2309.09128&quot;&gt;ChainForge: A Visual Toolkit for Prompt Engineering and LLM Hypothesis Testing&lt;/a&gt;&amp;quot; (CHI 2024)&lt;/li&gt;
&lt;li&gt;Tendal et al., &amp;quot;&lt;a href=&quot;https://pmc.ncbi.nlm.nih.gov/articles/PMC7657075/&quot;&gt;Weekly updates of national living evidence-based guidelines: methods for the Australian living guidelines for care of people with COVID-19&lt;/a&gt;&amp;quot; (J Clin Epidemiol 2020)&lt;/li&gt;
&lt;li&gt;Wei et al., &amp;quot;&lt;a href=&quot;https://arxiv.org/abs/2201.11903&quot;&gt;Chain-of-Thought Prompting Elicits Reasoning&lt;/a&gt;&amp;quot; (2022)&lt;/li&gt;
&lt;li&gt;Yao et al., &amp;quot;&lt;a href=&quot;https://arxiv.org/abs/2305.10601&quot;&gt;Tree of Thoughts: Deliberate Problem Solving with LLMs&lt;/a&gt;&amp;quot; (2023)&lt;/li&gt;
&lt;li&gt;Kohavi, Longbotham et al., &amp;quot;&lt;a href=&quot;https://doi.org/10.1007/s10618-008-0114-1&quot;&gt;Controlled experiments on the web: survey and practical guide&lt;/a&gt;&amp;quot; (2009, &lt;em&gt;Data Mining and Knowledge Discovery&lt;/em&gt;)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.agreetrust.org/&quot;&gt;AGREE II&lt;/a&gt; — instrument for appraising the quality of clinical practice guidelines (the discipline of judging whether your guidance itself is sound)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.gradeworkinggroup.org/&quot;&gt;GRADE&lt;/a&gt; — framework for grading the certainty of evidence and the strength of recommendations (separating &amp;quot;we&#39;re sure&amp;quot; from &amp;quot;we think&amp;quot;)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://diataxis.fr/&quot;&gt;Diátaxis&lt;/a&gt; — documentation framework organising knowledge by user need (tutorials, how-tos, reference, explanation) rather than by topic&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.prisma-statement.org/&quot;&gt;PRISMA&lt;/a&gt; — reporting standard for systematic reviews (a checklist for how to describe what you did, so others can trust or replicate it)&lt;/li&gt;
&lt;/ul&gt;

</content>
  </entry>
  <entry>
    <title>A data font, from the inside out</title>
    <link href="https://www.allaboutken.com/posts/20260512-datatype-data-font-inside-out/"/>
    <published>2026-05-12T00:00:00.000Z</published>
    <updated>2026-05-12T00:00:00.000Z</updated>
    <id>https://www.allaboutken.com/posts/20260512-datatype-data-font-inside-out/</id>
    <author>
      <name>Ken Hawkins</name>
      <email>khawkins98@gmail.com</email>
    </author>
    <category term="typography" />
    <category term="variable fonts" />
    <category term="data visualization" />
    <category term="design systems" />
    <category term="OpenType ligatures" />
    <category term="accessibility" />
    <summary type="html">Datatype is a variable font that turns text into tiny inline charts.</summary>
    <content type="html">


&lt;p&gt;&lt;a href=&quot;https://franktisellano.github.io/datatype/&quot;&gt;Datatype&lt;/a&gt;, by Frank Tisellano, is a variable font that renders inline bar charts, sparklines, and pie charts straight out of OpenType ligatures. Write &lt;code&gt;{b:30,70,50,90}&lt;/code&gt; and get &lt;span class=&quot;chart&quot; aria-hidden=&quot;true&quot;&gt;{b:30,70,50,90}&lt;/span&gt;; no JavaScript, no SVG, no charting library involved. It&#39;s also a total inversion of a 2018 piece of mine, &lt;a href=&quot;https://www.allaboutken.com/posts/20180319-data-font-for-life-sciences.html&quot;&gt;A web font for data&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;tl;dr&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Datatype turns text like &lt;code&gt;{b:30,70,50,90}&lt;/code&gt; into inline charts via OpenType ligatures: bar, sparkline, and pie, with weight and width axes.&lt;/li&gt;
&lt;li&gt;It rhymes with a 2018 piece of mine, &lt;a href=&quot;https://www.allaboutken.com/posts/20180319-data-font-for-life-sciences.html&quot;&gt;What if: A web font for data&lt;/a&gt;, but inverts it: my version was a font for &lt;em&gt;reading&lt;/em&gt; dense data, this is a font that &lt;em&gt;is&lt;/em&gt; the data.&lt;/li&gt;
&lt;li&gt;The upstream project ships zero accessibility guidance; for any serious use, pair each chart with &lt;code&gt;aria-hidden&lt;/code&gt; on the glyph span and a visually-hidden data table. There&#39;s a copyable pattern below.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;a-data-font-and-font-for-data&quot; tabindex=&quot;-1&quot;&gt;A data font and font for data&lt;a href=&quot;#a-data-font-and-font-for-data&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Back in 2018, while at EMBL-EBI, I wrote a long piece called &lt;a href=&quot;https://www.allaboutken.com/posts/20180319-data-font-for-life-sciences.html&quot;&gt;What if: A web font for data&lt;/a&gt;. My problem was that life-sciences researchers stare at strings like &lt;code&gt;ENSMUST00000032717&lt;/code&gt; all day, and the tools they were using rendered those strings in fonts that had not been designed with that kind of work in mind. Sequential zeros were ambiguous (is &lt;code&gt;20000001&lt;/code&gt; six zeros or seven?). Lowercase &lt;code&gt;l&lt;/code&gt;, uppercase &lt;code&gt;I&lt;/code&gt;, and numeric &lt;code&gt;1&lt;/code&gt; could be hard to tell apart in body widths. &lt;code&gt;BuMpy caPitiLiSation&lt;/code&gt; patterns were brittle to read at a glance. My sketch was a font where the ligature feature did the same kind of work &lt;a href=&quot;https://github.com/tonsky/FiraCode&quot;&gt;Fira Code&lt;/a&gt; does for code, but pointed at scientific identifiers: chunking long zero runs, disambiguating glyphs, giving the &lt;em&gt;reader&#39;s eye&lt;/em&gt; better handles.&lt;/p&gt;
&lt;p&gt;Datatype is also a data font, and it&#39;s also built on the OpenType ligature feature. But it inverts every part of the goal. The text isn&#39;t there to be read in the normal sense; it&#39;s there to be drawn. The ligature substitution doesn&#39;t help you decode the characters — it replaces them entirely with a chart. The font isn&#39;t a better tool for the reader. The font &lt;em&gt;is&lt;/em&gt; the data.&lt;/p&gt;
&lt;p&gt;Clearly the aims are different, but the mirror universe aspect tickles my brain.&lt;/p&gt;
&lt;p&gt;Datatype also credits IBM Plex Mono as the source of some underlying glyphs, and IBM Plex was the typeface I &lt;a href=&quot;https://www.allaboutken.com/posts/20171112-ibm-plex-font-and-fira.html&quot;&gt;wrote about back in 2017&lt;/a&gt; when we were evaluating it for EMBL-EBI&#39;s scientific data work. Neat.&lt;/p&gt;
&lt;h2 id=&quot;accessibility-gap&quot; tabindex=&quot;-1&quot;&gt;Accessibility gap&lt;a href=&quot;#accessibility-gap&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Data viz accessibility is always a challenge. Left alone, a string like &lt;code&gt;{b:18,21,12,12,15,0,0,0,100,94}&lt;/code&gt; gets read aloud verbatim, brace by digit by comma. That&#39;s worse than no alternative at all.&lt;/p&gt;
&lt;p&gt;This is made a bit better by the &lt;a href=&quot;https://www.w3.org/WAI/tutorials/images/complex/&quot;&gt;WAI pattern for complex graphics&lt;/a&gt;. Mark the glyph span &lt;code&gt;aria-hidden&lt;/code&gt;, and pair each chart with a visually-hidden data table that assistive technology can navigate as proper rows and cells:&lt;/p&gt;


&lt;div class=&quot;kh-demo&quot; data-pagefind-ignore&gt;&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;p&amp;gt;
  Posts per year, 2017–2020:
  &amp;lt;span class=&quot;chart&quot; aria-hidden=&quot;true&quot;&amp;gt;{b:60,70,40,40}&amp;lt;/span&amp;gt;
&amp;lt;/p&amp;gt;
&amp;lt;table class=&quot;kh-u-sr-only&quot;&amp;gt;
  &amp;lt;caption class=&quot;kh-u-sr-only&quot;&amp;gt;Posts per year, 2017–2020&amp;lt;/caption&amp;gt;
  &amp;lt;thead&amp;gt;&amp;lt;tr&amp;gt;&amp;lt;th scope=&quot;col&quot;&amp;gt;Year&amp;lt;/th&amp;gt;&amp;lt;th scope=&quot;col&quot;&amp;gt;Posts&amp;lt;/th&amp;gt;&amp;lt;/tr&amp;gt;&amp;lt;/thead&amp;gt;
  &amp;lt;tbody&amp;gt;
    &amp;lt;tr&amp;gt;&amp;lt;td&amp;gt;2017&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;6&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;
    &amp;lt;tr&amp;gt;&amp;lt;td&amp;gt;2018&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;7&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;
    &amp;lt;tr&amp;gt;&amp;lt;td&amp;gt;2019&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;4&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;
    &amp;lt;tr&amp;gt;&amp;lt;td&amp;gt;2020&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;4&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;
  &amp;lt;/tbody&amp;gt;
&amp;lt;/table&amp;gt;&lt;/code&gt;&lt;/pre&gt;&lt;div class=&quot;kh-demo__live&quot;&gt;&lt;p&gt;
  Posts per year, 2017–2020:
  &lt;span class=&quot;chart&quot; aria-hidden=&quot;true&quot;&gt;{b:60,70,40,40}&lt;/span&gt;
&lt;/p&gt;
&lt;table class=&quot;kh-u-sr-only&quot;&gt;
  &lt;caption class=&quot;kh-u-sr-only&quot;&gt;Posts per year, 2017–2020&lt;/caption&gt;
  &lt;thead&gt;&lt;tr&gt;&lt;th scope=&quot;col&quot;&gt;Year&lt;/th&gt;&lt;th scope=&quot;col&quot;&gt;Posts&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;&lt;td&gt;2017&lt;/td&gt;&lt;td&gt;6&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;td&gt;2018&lt;/td&gt;&lt;td&gt;7&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;td&gt;2019&lt;/td&gt;&lt;td&gt;4&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;td&gt;2020&lt;/td&gt;&lt;td&gt;4&lt;/td&gt;&lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;i-want-an-excuse-to-use-it&quot; tabindex=&quot;-1&quot;&gt;I want an excuse to use it&lt;a href=&quot;#i-want-an-excuse-to-use-it&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;My enthusiasm for variable fonts as an expressive runtime is sitting at about &lt;span class=&quot;chart&quot; aria-hidden=&quot;true&quot;&gt;{p:85}&lt;/span&gt;&lt;span class=&quot;kh-u-sr-only&quot;&gt;(roughly 85 percent)&lt;/span&gt;, while my likelihood of actually reaching for Datatype on the next chart I ship is somewhere around &lt;span class=&quot;chart&quot; aria-hidden=&quot;true&quot;&gt;{p:25}&lt;/span&gt;&lt;span class=&quot;kh-u-sr-only&quot;&gt;(roughly 25 percent)&lt;/span&gt;. The gap between those two pies is basically the whole post.&lt;/p&gt;
&lt;p&gt;Frank&#39;s &lt;a href=&quot;https://franktisellano.github.io/datatype/&quot;&gt;specimen site&lt;/a&gt; is worth a visit on its own; the variable-axis sliders and live examples are a small showcase of typographic care.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://franktisellano.github.io/datatype/&quot; class=&quot;kh-button&quot;&gt;Try Datatype on the project site &amp;rarr;&lt;/a&gt;&lt;/p&gt;

</content>
  </entry>
  <entry>
    <title>Building a virtual garage for hobby projects</title>
    <link href="https://www.allaboutken.com/posts/20260509-vercel-proxy-hobby-projects/"/>
    <published>2026-05-09T00:00:00.000Z</published>
    <updated>2026-05-09T00:00:00.000Z</updated>
    <id>https://www.allaboutken.com/posts/20260509-vercel-proxy-hobby-projects/</id>
    <author>
      <name>Ken Hawkins</name>
      <email>khawkins98@gmail.com</email>
    </author>
    <category term="web development" />
    <category term="vercel" />
    <category term="static sites" />
    <category term="GitHub Pages" />
    <summary type="html">I wanted my GitHub Pages hobby projects on-domain without a complicated workflow; Vercel rewrites turned out to be the cleanest path.</summary>
    <content type="html">
&lt;p&gt;I&#39;ve been building hobby projects for years. Normally I&#39;ll give the better ones a write-up here and a link to GitHub Pages, but the project itself lives at &lt;code&gt;khawkins98.github.io/project-name/&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I wanted to bring them on-domain without the obvious headaches: dozens of subdomains is more infrastructure than any of these deserve, subdomains still confuse people, and copying build output into the main repo defeats the point of keeping them separate.&lt;/p&gt;
&lt;p&gt;Could I just have my site do what GitHub Pages already does: proxy &lt;code&gt;/repo-name/&lt;/code&gt; to whatever GitHub is already serving?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Before: &lt;code&gt;khawkins98.github.io/PDF-A-go-go&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;After: &lt;code&gt;AllAboutKen.com/PDF-A-go-go&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Same project, same deploy pipeline, better home.&lt;/p&gt;
&lt;p&gt;
  &lt;a href=&quot;https://www.allaboutken.com/garage/&quot; class=&quot;kh-button&quot;&gt;See the garage&lt;/a&gt;
&lt;/p&gt;
&lt;p&gt;Keep reading if you want to see exactly how I wired it up.&lt;/p&gt;
&lt;blockquote&gt;
&lt;h2 id=&quot;tldr&quot; tabindex=&quot;-1&quot;&gt;tl;dr&lt;a href=&quot;#tldr&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Vercel &lt;code&gt;rewrites&lt;/code&gt; proxy &lt;code&gt;/project-name/:path*&lt;/code&gt; to the GitHub Pages origin — no deploy changes required&lt;/li&gt;
&lt;li&gt;A naive rewrite 404s on the project root; two &lt;code&gt;redirects&lt;/code&gt; entries (bare path and trailing slash → &lt;code&gt;/project-name/index.html&lt;/code&gt;) fix it&lt;/li&gt;
&lt;li&gt;Each project keeps its GitHub Pages URL; a hostname-check script redirects those visitors to the canonical URL&lt;/li&gt;
&lt;li&gt;CORS headers on proxied paths cover shared JS assets loaded via &lt;code&gt;fetch()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;/garage/&lt;/code&gt; index page on the main site ties all the projects together&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;what-i-didnt-want-to-do&quot; tabindex=&quot;-1&quot;&gt;What I didn&#39;t want to do&lt;a href=&quot;#what-i-didnt-want-to-do&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The obvious path is a merge: monorepo, or move each project to Vercel as a separate deployment. Both mean real work per project: consolidated CI, shared dependency trees, combined issue trackers. Hobby projects earn their independence precisely because they&#39;re free to be messy and self-contained. The right fix is a smarter routing layer in front, not a restructuring underneath.&lt;/p&gt;
&lt;h2 id=&quot;how-the-proxy-works&quot; tabindex=&quot;-1&quot;&gt;How the proxy works&lt;a href=&quot;#how-the-proxy-works&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;My site was already on Vercel and each project already served from &lt;code&gt;https://khawkins98.github.io/repo-name/&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Vercel &lt;code&gt;rewrite&lt;/code&gt; rules can proxy a path prefix to an external origin. Minimum viable &lt;code&gt;vercel.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;rewrites&amp;quot;: [
    {
      &amp;quot;source&amp;quot;: &amp;quot;/PDF-A-go-go/:path*&amp;quot;,
      &amp;quot;destination&amp;quot;: &amp;quot;https://khawkins98.github.io/PDF-A-go-go/:path*&amp;quot;
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Requests to &lt;code&gt;allaboutken.com/PDF-A-go-go/anything&lt;/code&gt; forward to &lt;code&gt;khawkins98.github.io/PDF-A-go-go/anything&lt;/code&gt;. The GitHub Pages deploy keeps working at its original URL unchanged.&lt;/p&gt;
&lt;p&gt;This works for every path except the project root.&lt;/p&gt;
&lt;h2 id=&quot;the-trailing-slash-problem&quot; tabindex=&quot;-1&quot;&gt;The trailing-slash problem&lt;a href=&quot;#the-trailing-slash-problem&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Going to &lt;code&gt;allaboutken.com/PDF-A-go-go/&lt;/code&gt; still gave a 404, even with the rewrite in place. The reason is Vercel&#39;s processing order: filesystem matching (including a directory-index check for &lt;code&gt;build/PDF-A-go-go/index.html&lt;/code&gt;) runs before rewrites. That file doesn&#39;t exist in the main site&#39;s build, so Vercel 404s before the rewrite ever fires.&lt;/p&gt;
&lt;p&gt;Two &lt;code&gt;redirects&lt;/code&gt; entries fix it. Redirects run before filesystem matching:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;redirects&amp;quot;: [
    { &amp;quot;source&amp;quot;: &amp;quot;/PDF-A-go-go&amp;quot;,  &amp;quot;destination&amp;quot;: &amp;quot;/PDF-A-go-go/index.html&amp;quot;, &amp;quot;permanent&amp;quot;: false },
    { &amp;quot;source&amp;quot;: &amp;quot;/PDF-A-go-go/&amp;quot;, &amp;quot;destination&amp;quot;: &amp;quot;/PDF-A-go-go/index.html&amp;quot;, &amp;quot;permanent&amp;quot;: false }
  ],
  &amp;quot;rewrites&amp;quot;: [
    {
        &amp;quot;source&amp;quot;: &amp;quot;/PDF-A-go-go/:path*&amp;quot;,
        &amp;quot;destination&amp;quot;: &amp;quot;https://khawkins98.github.io/PDF-A-go-go/:path*&amp;quot;
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Visiting &lt;code&gt;/PDF-A-go-go/&lt;/code&gt; now redirects (302) to &lt;code&gt;/PDF-A-go-go/index.html&lt;/code&gt;. No directory-index ambiguity; the rewrite fires and proxies to GitHub Pages. The browser ends up at &lt;code&gt;/PDF-A-go-go/index.html&lt;/code&gt;, where relative asset paths like &lt;code&gt;./app.js&lt;/code&gt; resolve correctly to &lt;code&gt;/PDF-A-go-go/app.js&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;not-burning-the-old-address&quot; tabindex=&quot;-1&quot;&gt;Not burning the old address&lt;a href=&quot;#not-burning-the-old-address&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;GitHub Pages keeps serving at &lt;code&gt;khawkins98.github.io/project-name/&lt;/code&gt;. The Vercel setup is additive: the personal domain gains a route, the original URL stays live. Rolling back is two deleted lines in &lt;code&gt;vercel.json&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;For now I&#39;ve only moved two projects across — PDF-A-go-go and pinment — while I confirm that existing embed links aren&#39;t affected. The redirect from &lt;code&gt;khawkins98.github.io&lt;/code&gt; to the proxied URL could in theory break things for anyone who has embedded the old URL directly. Once it&#39;s clear nothing is breaking, adding the rest is a few lines in &lt;code&gt;vercel.json&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;But visitors arriving at the old URL should reach the canonical one. A small script at the top of each project&#39;s &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;script&amp;gt;if(window.location.hostname===&#39;khawkins98.github.io&#39;){window.location.replace(&#39;https://www.allaboutken.com/PDF-A-go-go/&#39;)}&amp;lt;/script&amp;gt;
&amp;lt;link rel=&amp;quot;canonical&amp;quot; href=&amp;quot;https://www.allaboutken.com/PDF-A-go-go/&amp;quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The hostname check is essential. Without it, the redirect fires on the proxied page too: &lt;a href=&quot;http://allaboutken.com&quot;&gt;allaboutken.com&lt;/a&gt; proxies to &lt;a href=&quot;http://github.io&quot;&gt;github.io&lt;/a&gt;, &lt;a href=&quot;http://github.io&quot;&gt;github.io&lt;/a&gt; redirects back to &lt;a href=&quot;http://allaboutken.com&quot;&gt;allaboutken.com&lt;/a&gt;, repeat. The check breaks the loop.&lt;/p&gt;
&lt;h2 id=&quot;dont-forget-cors-for-shared-js-assets&quot; tabindex=&quot;-1&quot;&gt;Don&#39;t forget: CORS for shared JS assets&lt;a href=&quot;#dont-forget-cors-for-shared-js-assets&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.allaboutken.com/posts/20260225-introducing-pinment/&quot;&gt;Pinment&lt;/a&gt; is a bookmarklet tool. Its install snippet builds a script URL from &lt;code&gt;window.location.origin&lt;/code&gt; at runtime, so it always points to whichever domain the user is visiting from. Through the Vercel proxy, that becomes &lt;code&gt;allaboutken.com/pinment/pinment-bookmarklet.js&lt;/code&gt;, which the rewrite proxies correctly to GitHub Pages.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;script src&amp;gt;&lt;/code&gt; tags don&#39;t trigger CORS, but &lt;code&gt;fetch()&lt;/code&gt; does. A &lt;code&gt;headers&lt;/code&gt; block in &lt;code&gt;vercel.json&lt;/code&gt; covers it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;headers&amp;quot;: [
    { &amp;quot;source&amp;quot;: &amp;quot;/pinment/:path*&amp;quot;, &amp;quot;headers&amp;quot;: [{ &amp;quot;key&amp;quot;: &amp;quot;Access-Control-Allow-Origin&amp;quot;, &amp;quot;value&amp;quot;: &amp;quot;*&amp;quot; }] }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Worth adding for any project that exposes public JS assets loaded programmatically from other pages.&lt;/p&gt;
&lt;h2 id=&quot;the-full-configuration&quot; tabindex=&quot;-1&quot;&gt;The full configuration&lt;a href=&quot;#the-full-configuration&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;buildCommand&amp;quot;: &amp;quot;yarn build&amp;quot;,
  &amp;quot;outputDirectory&amp;quot;: &amp;quot;build&amp;quot;,
  &amp;quot;headers&amp;quot;: [
    { &amp;quot;source&amp;quot;: &amp;quot;/PDF-A-go-go/:path*&amp;quot;, &amp;quot;headers&amp;quot;: [{ &amp;quot;key&amp;quot;: &amp;quot;Access-Control-Allow-Origin&amp;quot;, &amp;quot;value&amp;quot;: &amp;quot;*&amp;quot; }] },
    { &amp;quot;source&amp;quot;: &amp;quot;/pinment/:path*&amp;quot;, &amp;quot;headers&amp;quot;: [{ &amp;quot;key&amp;quot;: &amp;quot;Access-Control-Allow-Origin&amp;quot;, &amp;quot;value&amp;quot;: &amp;quot;*&amp;quot; }] }
  ],
  &amp;quot;redirects&amp;quot;: [
    { &amp;quot;source&amp;quot;: &amp;quot;/PDF-A-go-go&amp;quot;,  &amp;quot;destination&amp;quot;: &amp;quot;/PDF-A-go-go/index.html&amp;quot;, &amp;quot;permanent&amp;quot;: false },
    { &amp;quot;source&amp;quot;: &amp;quot;/PDF-A-go-go/&amp;quot;, &amp;quot;destination&amp;quot;: &amp;quot;/PDF-A-go-go/index.html&amp;quot;, &amp;quot;permanent&amp;quot;: false },
    { &amp;quot;source&amp;quot;: &amp;quot;/pinment&amp;quot;,  &amp;quot;destination&amp;quot;: &amp;quot;/pinment/index.html&amp;quot;, &amp;quot;permanent&amp;quot;: false },
    { &amp;quot;source&amp;quot;: &amp;quot;/pinment/&amp;quot;, &amp;quot;destination&amp;quot;: &amp;quot;/pinment/index.html&amp;quot;, &amp;quot;permanent&amp;quot;: false }
  ],
  &amp;quot;rewrites&amp;quot;: [
    { &amp;quot;source&amp;quot;: &amp;quot;/PDF-A-go-go/:path*&amp;quot;, &amp;quot;destination&amp;quot;: &amp;quot;https://khawkins98.github.io/PDF-A-go-go/:path*&amp;quot; },
    { &amp;quot;source&amp;quot;: &amp;quot;/pinment/:path*&amp;quot;, &amp;quot;destination&amp;quot;: &amp;quot;https://khawkins98.github.io/pinment/:path*&amp;quot; }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;adding-another-project&quot; tabindex=&quot;-1&quot;&gt;Adding another project&lt;a href=&quot;#adding-another-project&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Confirm GitHub Pages is enabled and serving at &lt;code&gt;USERNAME.github.io/repo-name/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Check asset paths: &lt;code&gt;./&lt;/code&gt;-relative or prefixed with the repo name. Bare &lt;code&gt;/styles.css&lt;/code&gt; will break.&lt;/li&gt;
&lt;li&gt;Add two &lt;code&gt;redirects&lt;/code&gt; entries and one &lt;code&gt;rewrite&lt;/code&gt; to &lt;code&gt;vercel.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Add the hostname-check script and &lt;code&gt;&amp;lt;link rel=&amp;quot;canonical&amp;quot;&amp;gt;&lt;/code&gt; to the project&#39;s &lt;code&gt;index.html&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Add the project to the &lt;a href=&quot;https://www.allaboutken.com/garage/&quot;&gt;garage page&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For SPAs with client-side routing: the project needs a &lt;code&gt;404.html&lt;/code&gt; on GitHub Pages that loads &lt;code&gt;index.html&lt;/code&gt;. The proxy can&#39;t route to paths the GH Pages deploy doesn&#39;t know about.&lt;/p&gt;
&lt;p&gt;The full configuration and checklist live in &lt;a href=&quot;https://github.com/khawkins98/allaboutken-11ty/blob/main/vercel.json&quot;&gt;&lt;code&gt;vercel.json&lt;/code&gt;&lt;/a&gt; and &lt;a href=&quot;https://github.com/khawkins98/allaboutken-11ty/blob/main/docs/DEPLOYMENT.md&quot;&gt;&lt;code&gt;docs/DEPLOYMENT.md&lt;/code&gt;&lt;/a&gt;. Nothing about the project deploys has to change: GitHub Pages stays the origin, Vercel becomes the routing layer. See what it looks like in practice at the &lt;a href=&quot;https://www.allaboutken.com/garage/&quot;&gt;garage&lt;/a&gt;. If you&#39;ve proxied projects this way — or found a cleaner approach — I&#39;d be glad to hear about it. The projects just get to live at home.&lt;/p&gt;
&lt;p&gt;I think this is a clean way to bring web projects back home without adding server complexity.&lt;/p&gt;

</content>
  </entry>
  <entry>
    <title>Eye-tracking heatmaps in your browser, no lab needed</title>
    <link href="https://www.allaboutken.com/posts/20260503-foveacast/"/>
    <published>2026-05-03T00:00:00.000Z</published>
    <updated>2026-05-03T00:00:00.000Z</updated>
    <id>https://www.allaboutken.com/posts/20260503-foveacast/</id>
    <author>
      <name>Ken Hawkins</name>
      <email>khawkins98@gmail.com</email>
    </author>
    <category term="open source" />
    <category term="browser tools" />
    <category term="machine learning" />
    <category term="UX research" />
    <category term="eye tracking" />
    <category term="design tools" />
    <summary type="html">Stop guessing whether users will notice your CTA. Foveacast predicts attention from a screenshot, entirely in your browser, nothing uploaded.</summary>
    <content type="html">
&lt;p&gt;At some point in most design or content reviews you find yourself trying to answer a question you can&#39;t answer cleanly: not &amp;quot;is it pretty?&amp;quot; but &amp;quot;will users actually notice the call to action, or are they going to get distracted by the hero image first?&amp;quot;&lt;/p&gt;
&lt;p&gt;The honest answer is usually &amp;quot;Based off my knowledge and experience: I think so.&amp;quot; That only holds until a stakeholder who disagrees says the same thing.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://khawkins98.github.io/Foveacast/&quot;&gt;Foveacast&lt;/a&gt; gives both of you something to look at instead. Drop a screenshot of a UI — a web page, an app screen, a mockup — and it produces a predicted attention heatmap: a color overlay showing where a typical user is likely to look, and how that attention evolves over time. Nothing leaves your machine; the model runs entirely in your browser.&lt;/p&gt;

&lt;p&gt;
  &lt;a href=&quot;https://khawkins98.github.io/Foveacast/&quot; class=&quot;kh-button&quot;&gt;Try Foveacast&lt;/a&gt;
  &lt;a href=&quot;https://github.com/khawkins98/Foveacast&quot; class=&quot;kh-button kh-button--sm&quot;&gt;View on GitHub&lt;/a&gt;
&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;tl;dr&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Drop a UI screenshot, get a predicted attention heatmap with fixation sequence, attention zones, and centroid trajectory.&lt;/li&gt;
&lt;li&gt;Three viewing durations: first glance (1 s), quick scan (3 s), full viewing (7 s) — modeled separately, so you can see how attention evolves as users spend more time on the page.&lt;/li&gt;
&lt;li&gt;Runs entirely in your browser. The model (~57 MB per duration) downloads and caches on first use; nothing is ever uploaded.&lt;/li&gt;
&lt;li&gt;Built on MSI-Net fine-tuned on real UI eye-tracking data. Not a natural-scene model that doesn&#39;t know what a button is.&lt;/li&gt;
&lt;li&gt;It&#39;s a probabilistic estimate, not a user study. The reading-your-results guide in the tool covers what it can and can&#39;t tell you.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;the-analysis-report&quot; tabindex=&quot;-1&quot;&gt;The analysis report&lt;a href=&quot;#the-analysis-report&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The output is a scrollable analysis report.&lt;/p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https://www.allaboutken.com/images/blog/foveacast-heatmap-overlay.jpg&quot; alt=&quot;Predicted attention heatmap overlaid on the allaboutken.com homepage. Warm orange and yellow colors concentrate on the site header and the first content heading; the lower body of the page shows cooler blue tones indicating lower predicted attention.&quot;&gt;
  &lt;figcaption&gt;Warm colors show where a typical user&#39;s attention is predicted to concentrate. Cool tones indicate lower predicted attention density.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The heatmap overlay uses the inferno colormap: warm colors where attention is predicted to concentrate. Three viewing durations (first glance, quick scan, full viewing) are each backed by a separate model.&lt;/p&gt;
&lt;p&gt;Below the heatmap: a rule-of-thirds grid with attention shares per region, a fixation sequence showing the predicted scan path as numbered dots, attention zones with contour rings at 10%, 25%, and 50% attention mass, and a centroid trajectory tracking where the attention center moves from first glance to sustained viewing.&lt;/p&gt;
&lt;p&gt;In a design review, &amp;quot;the CTA doesn&#39;t appear until fixation 5, and 42% of initial attention lands on the hero image&amp;quot; is a different kind of conversation than &amp;quot;I think users will miss it.&amp;quot; It&#39;s not proof. But it&#39;s a concrete starting point, and it gets you past the stage where everyone is arguing from intuition.&lt;/p&gt;
&lt;p&gt;It&#39;s also useful as a self-check before publishing. Before you ship a redesigned landing page and find out three weeks later that the primary action was invisible.&lt;/p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https://www.allaboutken.com/images/blog/foveacast-scan-paths.jpg&quot; alt=&quot;Two side-by-side strips: on the left, a fixation sequence showing numbered dots tracking the predicted scan path across the page; on the right, attention zones with concentric contour rings marking the 10%, 25%, and 50% attention mass boundaries.&quot;&gt;
  &lt;figcaption&gt;Fixation sequence (left) and attention zones (right). The numbered dots show the predicted scan path order; contour rings mark where the densest fraction of attention falls.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id=&quot;trained-on-real-ui-gaze-data-not-natural-scenes&quot; tabindex=&quot;-1&quot;&gt;Trained on real UI gaze data, not natural scenes&lt;a href=&quot;#trained-on-real-ui-gaze-data-not-natural-scenes&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Most open-source saliency models are trained on the &lt;a href=&quot;http://salicon.net/&quot;&gt;SALICON dataset&lt;/a&gt;: natural photographs scraped from Flickr. Those models have learned to predict attention on photos of mountains and faces and street scenes. Put a web page in front of them and they produce a diffuse centrality blob. They don&#39;t know that &amp;quot;Start free trial&amp;quot; is the most important element on the screen; they see a rectangular region with text and mild contrast and model it accordingly.&lt;/p&gt;

&lt;figure class=&quot;kh-bleed-out&quot;&gt;
  &lt;img src=&quot;https://www.allaboutken.com/images/blog/foveacast-comparison-nytimes.jpg&quot; alt=&quot;Three-panel comparison using a New York Times page: the original screenshot, a SALICON heatmap showing a diffuse central blob with no meaningful structure, and the Foveacast heatmap concentrating attention on the masthead, primary headline, and article bylines.&quot;&gt;
  &lt;figcaption&gt;Same page, two models. SALICON — trained on natural photographs — produces a diffuse central blob that poorly &quot;understands&quot; content. Foveacast, fine-tuned on real UI eye-tracking data, concentrates predicted attention on the headline, navigation, and key text elements.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The &lt;a href=&quot;https://github.com/khawkins98/foveacast-training&quot;&gt;foveacast-training&lt;/a&gt; pipeline fine-tuned &lt;a href=&quot;https://github.com/alexanderkroner/saliency&quot;&gt;MSI-Net&lt;/a&gt; (Kroner et al. 2020) on &lt;a href=&quot;https://doi.org/10.1145/3544548.3581096&quot;&gt;UEyes&lt;/a&gt; (Jiang et al. 2023). Much credit is due there: Jiang et al. collected 1,980 UI screenshots with real eye-tracking data from 62 participants across desktop, mobile, web, and poster UIs, then published the full dataset on &lt;a href=&quot;https://zenodo.org/records/8010312&quot;&gt;Zenodo&lt;/a&gt; under &lt;a href=&quot;https://creativecommons.org/licenses/by/4.0/&quot;&gt;CC BY 4.0&lt;/a&gt;. Without that open release, Foveacast wouldn&#39;t have been viable to build. Fixations were recorded at 1, 3, and 7 seconds of viewing time, which is why there are three separate models rather than one. The gaze pattern at one second (&amp;quot;where do my eyes go first?&amp;quot;) and at seven seconds (&amp;quot;where does sustained attention end up?&amp;quot;) are genuinely different questions with different answers.&lt;/p&gt;
&lt;p&gt;Fine-tuning on UI content closes the gap. On a held-out test split, the fine-tuned model improves by +43% on correlation coefficient and cuts KL divergence by 44% compared to the stock SALICON-only baseline. The stock model produces diffuse blobs. The fine-tuned model picks up navigation elements, content headings, and interactive controls, matching where real users actually looked.&lt;/p&gt;
&lt;h2 id=&quot;limitations-what-predicted-attention-cant-tell-you&quot; tabindex=&quot;-1&quot;&gt;Limitations: what predicted attention can&#39;t tell you&lt;a href=&quot;#limitations-what-predicted-attention-cant-tell-you&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Foveacast shows a non-dismissible note in the UI that says this is a probabilistic estimate based on population-average gaze patterns. A few things worth keeping in mind:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;It&#39;s not measured data.&lt;/strong&gt; The heatmap is model output, not fixations from real users looking at your specific design.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Training distribution.&lt;/strong&gt; UEyes is primarily Western-language desktop and mobile UI, collected prior to the 2023 publication. Right-to-left layouts, dark-mode UIs, and design patterns that weren&#39;t common then may produce less reliable estimates.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Not a click predictor.&lt;/strong&gt; High predicted attention on an element doesn&#39;t mean users will interact with it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you need real eye-tracking data — measured fixations from real participants — there are commercial services: &lt;a href=&quot;https://attentioninsight.com&quot;&gt;Attention Insight&lt;/a&gt;, &lt;a href=&quot;https://www.alpha.one/products/expoze-io&quot;&gt;Alpha.one (formerly expoze.io)&lt;/a&gt;, &lt;a href=&quot;https://clueify.com&quot;&gt;Clueify&lt;/a&gt;. They&#39;re paid, and they send your screenshots to a third party.&lt;/p&gt;
&lt;p&gt;Foveacast is great to quickly, freely and securely close the gap: faster than a study, more specific than the &lt;a href=&quot;https://www.nngroup.com/articles/f-shaped-pattern-reading-web-content/&quot;&gt;F-pattern heuristic&lt;/a&gt;, and more honest than &amp;quot;I think.&amp;quot;&lt;/p&gt;
&lt;h2 id=&quot;how-it-runs-entirely-in-your-browser&quot; tabindex=&quot;-1&quot;&gt;How it runs entirely in your browser&lt;a href=&quot;#how-it-runs-entirely-in-your-browser&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Like &lt;a href=&quot;https://www.allaboutken.com/posts/20260429-svgomg-font/&quot;&gt;SVGOMG-Font&lt;/a&gt; and &lt;a href=&quot;https://www.allaboutken.com/posts/20260216-introducing-pdf-a-go-slim/&quot;&gt;PDF-A-go-slim&lt;/a&gt;, everything processes locally. Design screenshots often contain unreleased client work or internal materials. Uploading them to a third-party service to get a heatmap is friction many organisations can&#39;t accept.&lt;/p&gt;
&lt;p&gt;Running ONNX inference in the browser at this scale had some practical problems. WASM threading needs cross-origin isolation, which requires a service worker injecting the right headers. ORT Web&#39;s WASM paths have to be pinned when the app serves from a GitHub Pages subpath. The model weights are gitignored and fetched from a GitHub Release at deploy time rather than committed to the repo (57 MB each across three duration variants; they&#39;d bloat every clone). The full engineering log is in &lt;a href=&quot;https://github.com/khawkins98/Foveacast/blob/main/LEARNINGS.md&quot;&gt;LEARNINGS.md&lt;/a&gt; if you want the details.&lt;/p&gt;
&lt;h2 id=&quot;training-on-consumer-hardware-a-personal-note&quot; tabindex=&quot;-1&quot;&gt;Training on consumer hardware: a personal note&lt;a href=&quot;#training-on-consumer-hardware-a-personal-note&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This was my first proper model training exercise. I&#39;d worked with embeddings before, but never fine-tuned a vision model. A few things made it more approachable than expected.&lt;/p&gt;
&lt;p&gt;The training ran entirely on a MacBook Air M4 — no cloud GPU, no cluster. About three hours per model, so roughly nine hours of compute total across the three duration variants. Slow in absolute terms, but not the data-centre job I&#39;d assumed fine-tuning required.&lt;/p&gt;
&lt;p&gt;The other difference was AI-assisted development. What I&#39;d expected to be a multi-week slog — working through PyTorch docs, debugging ONNX export paths, figuring out why the WASM runtime was choking on FP16 weights — turned into a genuinely enjoyable weekend project. Claude and Copilot handled a lot of the &amp;quot;read the error and suggest a fix&amp;quot; loop that normally drains the fun out of this kind of work.&lt;/p&gt;
&lt;p&gt;I learned a lot and I&#39;d do it again. If you want to fine-tune a saliency model on your own domain-specific data, the &lt;a href=&quot;https://github.com/khawkins98/foveacast-training&quot;&gt;training pipeline is open&lt;/a&gt; and the README documents what worked and what didn&#39;t.&lt;/p&gt;
&lt;h2 id=&quot;try-foveacast-on-your-own-ui&quot; tabindex=&quot;-1&quot;&gt;Try Foveacast on your own UI&lt;a href=&quot;#try-foveacast-on-your-own-ui&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;If you&#39;re working on a UI and want something more specific than the F-pattern heuristic and faster than an eye-tracking study, &lt;a href=&quot;https://khawkins98.github.io/Foveacast/&quot;&gt;Foveacast&lt;/a&gt; is a reasonable starting point. If you use it in a real design review — or find a case where the model is clearly wrong — I&#39;d genuinely like to hear about it.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://khawkins98.github.io/Foveacast/&quot;&gt;Foveacast is on GitHub Pages&lt;/a&gt;. The source is &lt;a href=&quot;https://github.com/khawkins98/Foveacast&quot;&gt;on GitHub&lt;/a&gt;, with the training pipeline in a &lt;a href=&quot;https://github.com/khawkins98/foveacast-training&quot;&gt;companion repository&lt;/a&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Related posts:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.allaboutken.com/posts/20260429-svgomg-font/&quot;&gt;Stop converting SVG text to outlines&lt;/a&gt; — another browser-only utility from the same build season&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.allaboutken.com/posts/20260216-90s-desktop-paradigm-browser-utilities/&quot;&gt;Your browser utility wants to be a floating palette&lt;/a&gt; — the &amp;quot;do one thing, entirely in the browser&amp;quot; design philosophy&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.allaboutken.com/posts/20260302-semantic-search-browser-embeddings/&quot;&gt;Semantic search on a static site, no API keys required&lt;/a&gt; — the same pattern of ML inference running locally in the browser&lt;/li&gt;
&lt;/ul&gt;

</content>
  </entry>
  <entry>
    <title>Stop converting SVG text to outlines</title>
    <link href="https://www.allaboutken.com/posts/20260429-svgomg-font/"/>
    <published>2026-04-29T00:00:00.000Z</published>
    <updated>2026-04-29T00:00:00.000Z</updated>
    <id>https://www.allaboutken.com/posts/20260429-svgomg-font/</id>
    <author>
      <name>Ken Hawkins</name>
      <email>khawkins98@gmail.com</email>
    </author>
    <category term="web development" />
    <category term="SVG" />
    <category term="typography" />
    <category term="open source" />
    <category term="accessibility" />
    <category term="browser tools" />
    <category term="font subsetting" />
    <summary type="html">Converting SVG text to outlines breaks accessibility, search, and translation. SVGOMG-Font embeds a subsetted font instead, in your browser.</summary>
    <content type="html">
&lt;p&gt;
  &lt;a href=&quot;https://khawkins98.github.io/svgomg-font/&quot; class=&quot;kh-button&quot;&gt;Check out SVGOMG-Font&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;Every so often I need to put an SVG with text on a website: an infographic, a diagram, a data visualisation with labels. Each time, I end up staring at the same bad choice.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Option A:&lt;/strong&gt; Convert the text to outlines. The SVG renders identically everywhere. No font loading, no layout surprises. Done. But now the text is a pile of path data. Screen readers see nothing. Search engines see nothing. Language models see nothing. Translation is impossible.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Option B:&lt;/strong&gt; Leave the text as text. Clean, accessible, editable. But on any device that doesn&#39;t have the font installed, the browser falls back to whatever comes first in the system stack, and your carefully spaced infographic turns into a broken layout that looks like someone used Times New Roman on a web 1.0 site.&lt;/p&gt;
&lt;p&gt;I&#39;ve been choosing Option A for years and feeling bad about it. Despite plenty of searching, nothing combined what I actually wanted: a tool that runs in the browser, doesn&#39;t upload anywhere, and is open source. The closest matches were CLIs or a paid cloud service. So I built &lt;a href=&quot;https://khawkins98.github.io/svgomg-font/&quot;&gt;SVGOMG-Font&lt;/a&gt;. Drop the SVG in, get the same file back with the font subsetted to the characters in use and embedded as a base64 data URI.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;tl;dr&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Drop an SVG in, get back a self-contained one with the font embedded. Runs entirely in the browser, nothing uploaded.&lt;/li&gt;
&lt;li&gt;Converting SVG text to outlines makes it invisible to assistive tech, search, translation, and LLMs.&lt;/li&gt;
&lt;li&gt;Leaving text as-is fails on devices without the font installed.&lt;/li&gt;
&lt;li&gt;SVGOMG-Font subsets to only the glyphs you actually use, so file sizes stay sensible.&lt;/li&gt;
&lt;li&gt;Works cleanly for general-purpose infographics, diagrams, and posters. Rich text layouts that lean on OpenType features (ligatures, contextual alternates, complex scripts) need a closer look.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;Here&#39;s an SVG set in &lt;a href=&quot;https://fonts.google.com/specimen/Playwrite+DE+SAS&quot;&gt;Playwrite DE SAS&lt;/a&gt;, a connected German school-handwriting script almost no reader will have installed:&lt;/p&gt;




&lt;div class=&quot;kh-svgomg-compare&quot;&gt;
  &lt;figure&gt;
    &lt;object data=&quot;/images/blog/svgomg-font-playwrite-broken.svg&quot; type=&quot;image/svg+xml&quot; aria-label=&quot;A stylized note that reads &#39;Thank you for shipping Q3&#39; rendering in the browser default serif. The SVG references Playwrite DE SAS but doesn&#39;t embed it.&quot;&gt;&lt;/object&gt;
    &lt;figcaption&gt;&lt;strong&gt;Before.&lt;/strong&gt; No font embedded. Most readers see a Times-like serif fallback.&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;figure&gt;
    &lt;object data=&quot;/images/blog/svgomg-font-playwrite-embedded.svg&quot; type=&quot;image/svg+xml&quot; aria-label=&quot;The same stylized note, this time rendering correctly in Playwrite DE SAS as connected cursive handwriting because the font is embedded inside the SVG as a base64 data URI.&quot;&gt;&lt;/object&gt;
    &lt;figcaption&gt;&lt;strong&gt;After.&lt;/strong&gt; Font subsetted and embedded as base64; about 30 KB total.&lt;/figcaption&gt;
  &lt;/figure&gt;
&lt;/div&gt;

&lt;p&gt;The text is still real text: selectable, searchable, translatable. The font travels with the file, so it renders the same wherever the SVG lands.&lt;/p&gt;
&lt;p&gt;You can sort of sidestep this if you control the host page: load the font via the page&#39;s CSS, and the inline SVG inherits it. But that&#39;s another asset to manage, another caching layer, and it only holds while the SVG stays on that page. Once the file has to travel (attached to an email, dropped into someone else&#39;s CMS, sent over Slack) the font reference goes dead. Bake the font into the SVG itself, the way a PDF embeds fonts, and it just works wherever the file ends up.&lt;/p&gt;
&lt;h2 id=&quot;the-problem-with-outlines&quot; tabindex=&quot;-1&quot;&gt;The problem with outlines&lt;a href=&quot;#the-problem-with-outlines&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;When you tell Illustrator or Figma to &amp;quot;create outlines,&amp;quot; it replaces every character with the equivalent vector paths. The rendering is locked in. But you&#39;ve also just destroyed everything that made the text text:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Accessibility&lt;/strong&gt;: screen readers can&#39;t read paths. The text is gone from the accessibility tree.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Search&lt;/strong&gt;: both on-page (Cmd+F) and crawlers. An SVG infographic full of outlined text is invisible to search engines.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Translation&lt;/strong&gt;: auto-translate and localisation tools operate on text nodes. No text nodes, no translation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LLMs&lt;/strong&gt;: if a language model needs to understand what your diagram says, it now has to do OCR or get the content from somewhere else.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Editability&lt;/strong&gt;: editing outlined text means going back to the design tool, making the change, re-exporting, re-optimising, re-deploying. Minor corrections become multi-step processes.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There&#39;s a file-size argument too. A few hundred outlined characters easily adds tens of kilobytes of path data. Subsetting the font is usually smaller than outlining it.&lt;/p&gt;

&lt;p&gt;
  &lt;a href=&quot;https://khawkins98.github.io/svgomg-font/&quot; class=&quot;kh-button&quot;&gt;Try SVGOMG-Font&lt;/a&gt;
&lt;/p&gt;
&lt;h2 id=&quot;how-it-works&quot; tabindex=&quot;-1&quot;&gt;How it works&lt;a href=&quot;#how-it-works&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The structural change is small. Before, an &lt;code&gt;@font-face&lt;/code&gt; rule points at a remote URL the viewer may never load:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;style&amp;gt;
  @font-face {
    font-family: &#39;Playwrite DE SAS&#39;;
    src: url(&#39;https://fonts.example/playwritedesas-regular.woff2&#39;);
  }
&amp;lt;/style&amp;gt;
&amp;lt;text font-family=&amp;quot;Playwrite DE SAS&amp;quot; x=&amp;quot;20&amp;quot; y=&amp;quot;40&amp;quot;&amp;gt;Hello, world&amp;lt;/text&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After, the same rule, with the font subsetted and inlined as a data URI:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;style&amp;gt;
  @font-face {
    font-family: &#39;Playwrite DE SAS&#39;;
    src: url(&#39;data:font/woff2;base64,d09GMgABAAAA…&#39;) format(&#39;woff2&#39;);
  }
&amp;lt;/style&amp;gt;
&amp;lt;text font-family=&amp;quot;Playwrite DE SAS&amp;quot; x=&amp;quot;20&amp;quot; y=&amp;quot;40&amp;quot;&amp;gt;Hello, world&amp;lt;/text&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That&#39;s the whole shape of it. SVGOMG-Font scans your file for &lt;code&gt;@font-face&lt;/code&gt; references, fetches each font, subsets it to the glyphs in use, and rewrites the rule. No installation, no account, no upload: it all runs in the browser.&lt;/p&gt;
&lt;h2 id=&quot;why-browser-only-matters&quot; tabindex=&quot;-1&quot;&gt;Why browser-only matters&lt;a href=&quot;#why-browser-only-matters&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;SVGs with fonts are often used in sensitive contexts: client work, unreleased materials, internal tools. Uploading to a third-party service to fix font rendering is a non-starter in a lot of organisations.&lt;/p&gt;
&lt;p&gt;SVGOMG-Font processes everything locally. Nothing leaves your machine. It&#39;s a single static page. &lt;a href=&quot;https://khawkins98.github.io/Foveacast/&quot;&gt;Foveacast&lt;/a&gt;, my browser-only attention-heatmap tool, runs the same way for the same reasons (write-up coming).&lt;/p&gt;
&lt;h2 id=&quot;prior-art&quot; tabindex=&quot;-1&quot;&gt;Prior art&lt;a href=&quot;#prior-art&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;To be fair, this isn&#39;t entirely new territory. A handful of tools cover the same ground:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/BTBurke/svg-embed-font&quot;&gt;svg-embed-font&lt;/a&gt;, Go CLI: base64-embeds the full font.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/phauer/svg-buddy&quot;&gt;svg-buddy&lt;/a&gt;, Java CLI: WOFF2 embedding given a local font file.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/pedrovhb/svgfontembed&quot;&gt;svgfontembed&lt;/a&gt;, Python CLI: embeds and subsets.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://vecta.io/nano&quot;&gt;Vecta.io Nano&lt;/a&gt;: browser drag-and-drop SVG compressor that also handles font subsetting and embedding, hosted as a cloud service.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The CLIs share the install gap. &lt;a href=&quot;http://Vecta.io&quot;&gt;Vecta.io&lt;/a&gt; Nano is the closest match in spirit (same drop-the-SVG-in workflow) but it&#39;s closed-source freemium SaaS, processes your file server-side, and has free-tier limits on file count and size. SVGOMG-Font fills the gap I kept hitting: same drop-in workflow, but open source, fully client-side, no upload, no signup. It also auto-resolves fonts from Google Fonts and Fontsource URLs, and on Chromium can pull from installed system fonts via the Local Font Access API.&lt;/p&gt;
&lt;h2 id=&quot;the-subsetting-part&quot; tabindex=&quot;-1&quot;&gt;The subsetting part&lt;a href=&quot;#the-subsetting-part&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The naive approach (embed the whole font) works but produces large files. A single Latin-subset woff2 file is roughly 20 to 30 KB, base64-encoded to about 40 KB. Two weights, and you&#39;ve added 80 KB to your SVG before doing anything else.&lt;/p&gt;
&lt;p&gt;SVGOMG-Font subsets to the characters in the SVG&#39;s text nodes, plus a Basic Latin safety baseline (U+0020 to U+007E). For a typical infographic with a few hundred unique characters, the subset is usually 5 to 15 KB encoded. Often smaller than outlining the same text.&lt;/p&gt;
&lt;p&gt;There&#39;s a toggle to skip subsetting, useful when text gets added or replaced after export.&lt;/p&gt;
&lt;h2 id=&quot;what-it-handles&quot; tabindex=&quot;-1&quot;&gt;What it handles&lt;a href=&quot;#what-it-handles&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Fonts referenced via Google Fonts CSS URLs&lt;/li&gt;
&lt;li&gt;Fonts referenced via Fontsource CDN paths&lt;/li&gt;
&lt;li&gt;Cases where the font can&#39;t be found automatically: it prompts you to drag the font file onto the relevant row&lt;/li&gt;
&lt;li&gt;Local system fonts on Chromium-based browsers via the Local Font Access API (permission-gated)&lt;/li&gt;
&lt;li&gt;SVGO optimisation (optional, off by default; SVGO&#39;s &lt;code&gt;inlineStyles&lt;/code&gt; pass removes &lt;code&gt;@font-face&lt;/code&gt; rules if you&#39;re not careful)&lt;/li&gt;
&lt;li&gt;Stripping legacy &lt;code&gt;&amp;lt;font&amp;gt;&lt;/code&gt; / &lt;code&gt;&amp;lt;glyph&amp;gt;&lt;/code&gt; blocks that some editors still emit&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;where-it-gets-bumpy&quot; tabindex=&quot;-1&quot;&gt;Where it gets bumpy&lt;a href=&quot;#where-it-gets-bumpy&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;For the bulk of SVG-with-text use cases (infographics, charts, diagrams, posters, headlines exported from Illustrator or Figma) the workflow just works. Where it gets shakier is anything leaning on OpenType machinery or rich text layout:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CORS-blocked fonts&lt;/strong&gt;: a font hosted without permissive headers can&#39;t be fetched from the browser.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Variable fonts&lt;/strong&gt;: subsetting variable fonts is finicky in general. If your SVG depends on a custom axis, test the output before shipping.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OpenType features&lt;/strong&gt;: ligatures, contextual alternates, and stylistic sets sometimes reach for glyphs that don&#39;t appear in the visible text. The Basic Latin baseline catches most cases; obscure feature-driven glyphs may still drop.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Complex shaping scripts&lt;/strong&gt;: Arabic, Devanagari, and similar scripts depend on positional and contextual glyphs. If your SVG uses one, eyeball the output rendering before trusting it.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;try-it&quot; tabindex=&quot;-1&quot;&gt;Try it&lt;a href=&quot;#try-it&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;


&lt;figure&gt;
  &lt;img src=&quot;https://www.allaboutken.com/images/blog/svgomg-font-ui.jpg&quot; alt=&quot;Screenshot of SVGOMG-Font&#39;s split-screen comparison slider. Left panel labelled &#39;BEFORE&#39; shows &#39;872 bytes · families: Playwrite DE SAS&#39; and the headline &#39;Thank you for shipping Q3.&#39; rendered in default serif. Right panel labelled &#39;AFTER&#39; shows &#39;55,426 bytes · 1 font face embedded · renders everywhere&#39; and the same headline rendered in connected cursive Playwrite DE SAS.&quot;&gt;
  &lt;figcaption&gt;The split-screen slider in the tool itself. Drag the divider to scrub between the broken fallback and the embedded version, with file sizes and font count surfaced in each panel&#39;s header.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;&lt;a href=&quot;https://khawkins98.github.io/svgomg-font/&quot;&gt;SVGOMG-Font is live on GitHub Pages&lt;/a&gt;. Drop your SVG in; if its font references resolve, you&#39;ll get a fixed file back in a few seconds.&lt;/p&gt;
&lt;p&gt;The source is &lt;a href=&quot;https://github.com/khawkins98/svgomg-font&quot;&gt;on GitHub&lt;/a&gt;. Bug reports welcome; font name matching in the wild has a long tail.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Related posts:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.allaboutken.com/posts/20260216-90s-desktop-paradigm-browser-utilities/&quot;&gt;Your browser utility wants to be a floating palette&lt;/a&gt; (same &amp;quot;do one thing, entirely in the browser&amp;quot; design philosophy)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.allaboutken.com/posts/20260225-introducing-pinment/&quot;&gt;Introducing Pinment&lt;/a&gt; (another tool from the same season)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.allaboutken.com/posts/20260216-introducing-pdf-a-go-slim/&quot;&gt;PDF-A-go-slim: a browser-based PDF optimizer&lt;/a&gt; (stripping and compressing PDFs, no upload required)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://khawkins98.github.io/Foveacast/&quot;&gt;Foveacast: predicted-attention heatmaps in the browser&lt;/a&gt; (another client-side tool, same do-one-thing philosophy; blog post coming)&lt;/li&gt;
&lt;/ul&gt;

</content>
  </entry>
  <entry>
    <title>Designing analytics infrastructure that measures audience quality, not just traffic</title>
    <link href="https://www.allaboutken.com/work/2026/impact-story-undrr-analytics-infrastructure/"/>
    <published>2026-04-22T00:00:00.000Z</published>
    <updated>2026-04-22T00:00:00.000Z</updated>
    <id>https://www.allaboutken.com/work/2026/impact-story-undrr-analytics-infrastructure/</id>
    <author>
      <name>Ken Hawkins</name>
      <email>khawkins98@gmail.com</email>
    </author>
    <category term="analytics" />
    <category term="content strategy" />
    <category term="information architecture" />
    <summary type="html">24+ websites, no existing benchmarks, and a fundamental question: how do you know if the right people are finding your content?</summary>
    <content type="html">


&lt;p&gt;&lt;strong&gt;Clarity&lt;/strong&gt; | &lt;strong&gt;Audience Quality&lt;/strong&gt; | &lt;strong&gt;Strategic Alignment&lt;/strong&gt;&lt;/p&gt;
&lt;p class=&quot;kh-text-body--3&quot;&gt;
  &lt;div class=&quot;kh-note-box&quot;&gt;
    &lt;span class=&quot;kh-note-box__label&quot;&gt;Context&lt;/span&gt;
    The United Nations Office for Disaster Risk Reduction (UNDRR) is the UN&amp;#39;s focal point for disaster risk reduction, coordinating global policy and supporting Member States to reduce disaster risk and losses.
  &lt;/div&gt;
&lt;/p&gt;
&lt;p&gt;Traditional web analytics answers &amp;quot;how many people visited?&amp;quot; For a UN organization producing policy guidance, technical resources, and knowledge products, that&#39;s the wrong question. The right question: did the people who need this content actually find it?&lt;/p&gt;
&lt;p&gt;I led the design and implementation of analytics infrastructure across UNDRR&#39;s web ecosystem (24+ domains spanning Drupal sites, Java applications, and static microsites) to answer that. The challenge wasn&#39;t primarily technical. It was conceptual: what does success mean for content where impact happens outside your analytics?&lt;/p&gt;
&lt;h2 id=&quot;what-was-the-actual-problem&quot; tabindex=&quot;-1&quot;&gt;What was the actual problem?&lt;a href=&quot;#what-was-the-actual-problem&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;No analytics benchmarks exist for organizations like UNDRR. The widely-cited &amp;quot;16% social traffic&amp;quot; figure comes from fundraising nonprofits running emotional campaigns. For a technical policy organization where audiences search for &amp;quot;Sendai Framework implementation guidance,&amp;quot; that benchmark steers you wrong. I needed to figure out what actually applied, and write down what didn&#39;t.&lt;/p&gt;
&lt;p&gt;The measurement model was broken in a different way. A 50% bounce rate on a terminology definition often means the user found their answer. The same rate on a landing page means something failed. Without classifying content by type, aggregate metrics obscure more than they reveal.&lt;/p&gt;
&lt;p&gt;Then there&#39;s the volume problem. 10,000 visits from random browsers isn&#39;t worth more than 100 visits from government ministries using the content to shape national policy. Traditional analytics gives you geography and device types. It doesn&#39;t tell you whether the right people showed up.&lt;/p&gt;
&lt;p&gt;Some of the most important impact never touches analytics at all. A researcher downloads a dataset and cites it in a paper six months later. A ministry official reads guidance and it shapes a national strategy. Neither event shows up as a conversion.&lt;/p&gt;
&lt;h2 id=&quot;how-did-i-approach-this&quot; tabindex=&quot;-1&quot;&gt;How did I approach this?&lt;a href=&quot;#how-did-i-approach-this&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I started with benchmarks — or rather, with finding out they didn&#39;t exist for our context. I synthesized data from M+R Benchmarks, RKD Group studies (2,000+ nonprofits), First Page Sage, and Similarweb, then worked out which findings actually applied. One thing that came up: only 8% of nonprofit websites pass Core Web Vitals on mobile. I documented confidence levels for each benchmark category and noted the gaps: no UN-system analytics data exists anywhere.&lt;/p&gt;
&lt;p&gt;Content-type classification underpins everything else. I established metatag conventions (&lt;code&gt;vf:page-type&lt;/code&gt;) that tag every page by type. The tracking script reads these on page load and sends them to GA4 as a custom dimension, so you can apply different success criteria to different content. Reference pages get different expectations than landing pages, which get different expectations than event pages.&lt;/p&gt;
&lt;p&gt;For audience quality, I built an ASN lookup: a server-side endpoint converts IP addresses to network categories (government, academic, intergovernmental, NGO, commercial) and sends the category to GA4. Not the specific organization, just the type, which keeps it GDPR-friendly while still answering the real question: are the visitors we&#39;re getting the visitors we&#39;re trying to reach?&lt;/p&gt;
&lt;p&gt;I also classified 200+ referrer domain patterns into 14 strategic buckets (UN System, Government, Academic, DRR Community, Traditional Media, AI Chatbots, and others), each carrying a weight reflecting how much that traffic matters to the mission. An &amp;quot;impact score&amp;quot; multiplies sessions by weight. 100 government visits (5×) ranks higher than 400 unclassified visits (0.5×). Raw session counts stop being the main signal.&lt;/p&gt;
&lt;p&gt;Topic-level analysis came from taxonomy metatags (&lt;code&gt;vf:page-terms&lt;/code&gt;) covering hazards, themes, SDGs, and Sendai Framework priorities. The backend aggregates by topic, so you can ask &amp;quot;which hazards drive the most search demand?&amp;quot; rather than &amp;quot;which pages were popular last month?&amp;quot; A lightweight &amp;quot;Was this page helpful?&amp;quot; widget sends clicks to GA4 with page container context; it&#39;s most useful for low-traffic content where engagement metrics are too thin to read.&lt;/p&gt;
&lt;p&gt;The architecture follows a three-layer model. Content layer: Drupal, Java apps, and static microsites all emit the same metatag conventions. Tracking layer: one shared script, one GA4 property, per-domain data streams. Consumption layer: dashboard, browser extension, and Looker Studio, all reading the same source. Adding a new site means implementing metatags. The infrastructure doesn&#39;t change.&lt;/p&gt;
&lt;p&gt;The single-property decision took some defending. Separate GA4 properties per site would have been simpler to manage, but would have fragmented the data. With one property and hostname filtering, the same query answers &amp;quot;how&#39;s PreventionWeb doing?&amp;quot; or &amp;quot;how&#39;s the whole ecosystem doing?&amp;quot; Cross-site user journeys become visible for the first time.&lt;/p&gt;
&lt;h2 id=&quot;what-can-we-now-answer&quot; tabindex=&quot;-1&quot;&gt;What can we now answer?&lt;a href=&quot;#what-can-we-now-answer&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;58% of traffic comes from identifiable institutions: government, academic, UN system, research networks. That answers &amp;quot;are the right people finding policy guidance?&amp;quot; in a way raw page views can&#39;t.&lt;/p&gt;
&lt;p&gt;Topic aggregation surfaces patterns that page-level data hides. Flood content dominates hazard interest; Early Warning leads on themes. That feeds content planning decisions, not just retrospective reporting.&lt;/p&gt;
&lt;p&gt;A terminology page with 40% engagement isn&#39;t &amp;quot;underperforming&amp;quot; — it&#39;s what quick-reference content looks like. Content-type benchmarks mean the dashboard compares each page against the right expectations, not a generic industry average.&lt;/p&gt;
&lt;p&gt;On direct feedback: terminology pages run 78% helpful, publications 65%. Pages that drop below threshold get flagged. Not a perfect signal, but a real one for content where traffic is too low for engagement rates to tell you much.&lt;/p&gt;
&lt;h2 id=&quot;what-did-i-learn&quot; tabindex=&quot;-1&quot;&gt;What did I learn?&lt;a href=&quot;#what-did-i-learn&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;I built 18 dashboard screens before asking which ones stakeholders would actually use.&lt;/strong&gt; I should have done the user research first. That sequence mistake cost real time.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;We also set targets before documenting baselines.&lt;/strong&gt; Now we&#39;re establishing them retroactively. The order should have been: observe, then set expectations.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Some impact just isn&#39;t measurable through analytics.&lt;/strong&gt; Download clicks don&#39;t tell you whether anyone read the PDF. Policy influence doesn&#39;t generate a GA4 event. I documented these gaps explicitly; better to set that expectation early than have people treat the dashboard as a universal answer.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;I underestimated the research.&lt;/strong&gt; Deciding which benchmarks apply, which don&#39;t, and writing down the reasoning became as useful as the tracking implementation itself. Without it, you&#39;re measuring against whatever number was easiest to find. That number is usually wrong for your context.&lt;/p&gt;


&lt;aside class=&quot;kh-note-box&quot;&gt;
  &lt;span class=&quot;kh-note-box__label&quot;&gt;Related:&lt;/span&gt;
  &lt;a href=&quot;https://www.allaboutken.com/posts/20260115-how-to-measure-impact-when-analytics-lie/&quot;&gt;How to measure impact when analytics lie&lt;/a&gt;, the practitioner&#39;s guide: implementing benchmarks, content-type classification, and audience quality detection in your own stack.
&lt;/aside&gt;

&lt;h2 id=&quot;links-and-resources&quot; tabindex=&quot;-1&quot;&gt;Links and resources&lt;a href=&quot;#links-and-resources&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.allaboutken.com/posts/20251203-measuring-success-beyond-page-view/&quot;&gt;Measuring success beyond the page view&lt;/a&gt;: the philosophy behind this approach&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.allaboutken.com/posts/20260115-how-to-measure-impact-when-analytics-lie/&quot;&gt;How to measure impact when analytics lie&lt;/a&gt;: implementing benchmarks, audience detection, and content-type classification for your own stack&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.allaboutken.com/work/2026/impact-story-undrr-analytics-overlay-mcp/&quot;&gt;UNDRR analytics overlay and MCP bridge&lt;/a&gt;, the next chapter: putting this data where the work happens, and making it queryable by AI agents&lt;/li&gt;
&lt;/ul&gt;

</content>
  </entry>
  <entry>
    <title>Let AI worry about the code, you worry about the work</title>
    <link href="https://www.allaboutken.com/posts/20260420-let-ai-worry-about-the-code/"/>
    <published>2026-04-20T00:00:00.000Z</published>
    <updated>2026-04-20T00:00:00.000Z</updated>
    <id>https://www.allaboutken.com/posts/20260420-let-ai-worry-about-the-code/</id>
    <author>
      <name>Ken Hawkins</name>
      <email>khawkins98@gmail.com</email>
    </author>
    <category term="AI" />
    <category term="developer tools" />
    <category term="knowledge work" />
    <summary type="html">Cultivating LLM productivity requires comfort with the vernacular of code, not the writing of it.</summary>
    <content type="html">
&lt;p&gt;Early in 2026 I was in a hotel room during a team retreat, trying to work through a problem with all the context from our planning conversations still circling in my head. I had been using ChatGPT for coding help, then Cursor with the AI in the IDE sidebar; both useful, both essentially &amp;quot;AI helps me code.&amp;quot; I was the one writing; the model filled in the gaps. What I realized in that hotel room was that holding the context in my head and passing it to the machine one chat turn at a time was not enough. I needed to put it somewhere the agent could read directly.&lt;/p&gt;
&lt;p&gt;So I stopped asking an AI to help me code and started managing a project. I opened a fresh git repository, dropped in a short PRD of what I wanted, technical documents on the architecture of the data it would consume, and a folder of the business context behind it. I pointed Claude Code at the repo, described a bit how to begin, and let it stand up a local server. From there I worked through it the way you work through any project: branch, change, diff, commit, revise. My attention moved from the line and the function up to the project. The implementation work, which had been mine, became the agent&#39;s.&lt;/p&gt;
&lt;p&gt;What changed was not the language model; it was the level of abstraction I was operating at.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;tl;dr&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The productive pattern&lt;/strong&gt;: a coding agent working on a real project, primed with your business context via skill files, local MCP servers, and data on disk.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The literacy that matters&lt;/strong&gt;: reading diffs, driving git, editing configs, judging what the agent produced, not writing code from scratch.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The dividing line&lt;/strong&gt;: &amp;quot;do you work on a real project with a coding agent?&amp;quot; Chat windows and app-builders do not clear the bar yet.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;Another thing I started doing in that session: dictating instructions instead of typing them. Speaking is looser and more narrative; when I type, I try to be concise, and concision turns out to be the wrong register for this. LLMs read people well: a messy paragraph of what you actually mean beats a tight sentence that asks the model to infer the rest.&lt;/p&gt;
&lt;aside class=&quot;kh-marginnote&quot;&gt;This is the higher-gear behaviour I described in &lt;a href=&quot;https://www.allaboutken.com/posts/20260405-four-phases-of-ai-adoption/&quot;&gt;&lt;em&gt;Four gears of AI-assisted development&lt;/em&gt;&lt;/a&gt; — orchestration, not hand-feeding.&lt;/aside&gt;
&lt;p&gt;That change in productivity is not from writing more code, and it is not from prompting a chatbot. It is the fusion of the two: a coding agent acting on a real project, with me operating one level of abstraction up from the code itself. A chat thread rewards turn-by-turn completion and quietly fights &amp;quot;step back and tell me what we are actually trying to build&amp;quot;; a project folder does not.&lt;/p&gt;
&lt;p&gt;If you have comfort with the &lt;em&gt;vernacular&lt;/em&gt; of code (reading a diff, trusting a commit, knowing what a repo looks like, noticing which file an MCP server is reaching into and why), the leverage is real and it compounds quickly. You do not need to write the code yourself; the agent is writing most of it now. If you do not have that comfort, most of the efficiency gains happening right now will pass you by, because they live where code lives: in a project folder, behind a git remote, wired to a handful of MCP servers you need to trust.&lt;/p&gt;
&lt;h2 id=&quot;what-this-looks-like-in-practice&quot; tabindex=&quot;-1&quot;&gt;What this looks like in practice&lt;a href=&quot;#what-this-looks-like-in-practice&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;At UNDRR, our Google Analytics is, like many places, not generic. Twenty-two hostnames funnel into a property, download events are derived from URL patterns rather than clicks, engagement is tagged by page region, and referrers are bucketed by strategic category. I asked Copilot CLI to encode that business context in a local MCP server, so that a coding agent could reach GA4 already knowing what our data means. (The full write-up is in the &lt;a href=&quot;https://www.allaboutken.com/work/2026/impact-story-undrr-analytics-overlay-mcp/&quot;&gt;UNDRR analytics overlay and MCP bridge&lt;/a&gt; piece.)&lt;/p&gt;
&lt;p&gt;I also keep a local copy of our Drupal content database running on the same virtual machine. When the analytics are ambiguous about what a given URL is, the agent can cross-reference the content itself (taxonomy, body text, publication date) and resolve the question without asking me.&lt;/p&gt;
&lt;p&gt;With that setup I can ask:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Tell me about the last ten news items we published on tropical cyclones and how they performed compared to a year ago.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In months past that investigation was an incredibly tedious cross-tool query: pull a filtered content list from Drupal, map each URL to its analytics row, compute year-on-year deltas by custom event, assemble the results into a table. The agent does all of it in one pass, because each of the pieces is reachable and each is already annotated with the context the agent needs to use them.&lt;/p&gt;
&lt;p&gt;There is no off-the-shelf commercial tool that answers that question, and there probably never will be. The thing that makes the query answerable is the specific marriage of &lt;em&gt;our&lt;/em&gt; content structure, &lt;em&gt;our&lt;/em&gt; analytics configuration, and a coding agent fluent in both. The off-the-shelf option would only ever be one of the three. This is what &lt;a href=&quot;https://www.allaboutken.com/posts/20260330-context-was-always-the-job/&quot;&gt;priming an agent with business context&lt;/a&gt; looks like when it actually pays off.&lt;/p&gt;
&lt;h2 id=&quot;comfort-with-code-not-fluency-in-it&quot; tabindex=&quot;-1&quot;&gt;Comfort with code, not fluency in it&lt;a href=&quot;#comfort-with-code-not-fluency-in-it&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The people who benefit most from this moment are those comfortable &lt;em&gt;around&lt;/em&gt; code, not necessarily those who write a lot of it. The agent is writing most of the JavaScript or Python now. What you need is enough fluency to read what it produced, point it at the right place, trust or push back on a proposed change, and know what is happening on your own machine. In practice that looks like: comfort with a filesystem and a project structure, comfort with git (commits, branches, diffs, PRs), comfort with a terminal, comfort editing a config file, comfort registering an MCP server. You may never stand one up from scratch yourself; you can ask the agent to, read what it did, and decide whether to keep it.&lt;/p&gt;
&lt;p&gt;The floor used to be &amp;quot;can you create this code?&amp;quot; and is now closer to &amp;quot;can you direct what the agent writes?&amp;quot; That is a lower bar than before, and still not zero. The people on the wrong side of it will miss most of the leverage in this cycle, not because they cannot code, but because they will not open the terminal.&lt;/p&gt;
&lt;h2 id=&quot;how-to-get-there&quot; tabindex=&quot;-1&quot;&gt;How to get there&lt;a href=&quot;#how-to-get-there&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;If you are already comfortable with code, build one project-attached workflow for something you do every week. Often that is just a folder with the right context files (a PRD, a few technical docs, a sample of your data) and a coding agent pointed at it with access to the tools you already use. A skill file or a tiny MCP server can help, but they are one path, not the only one. The first version will underperform the manual process; the second will surprise you.&lt;/p&gt;
&lt;p&gt;If you are not comfortable with these tools yet, the investment is not &amp;quot;learn to code&amp;quot;; it is vocabulary. &lt;a href=&quot;https://docs.github.com/en/get-started/start-your-journey&quot;&gt;GitHub&#39;s introductory docs&lt;/a&gt; walk through commits, branches, and pull requests in plain language; &lt;a href=&quot;https://skills.github.com/&quot;&gt;GitHub Skills&lt;/a&gt; turns the same material into guided exercises. A week of either gives you enough to read what an agent is doing on your behalf.&lt;/p&gt;
&lt;aside class=&quot;kh-marginnote&quot;&gt;A bigger thought I want to come back to: we are in a pre-GUI moment for LLM interfaces, and the text-based ones (terminals, Markdown briefs, git histories) are winning so far — not because text is the final answer, but because text is honest about what is happening. More in a future post.&lt;/aside&gt;
&lt;p&gt;From there, install a coding agent (Claude Code, Cursor, or GitHub Copilot CLI), point it at a real repo of yours or one you forked, and let it work. Watch the diffs. Register one MCP server. Edit one config file. You do not need to produce code yourself for any of this to pay off; you need to be a credible reader, director, and reviewer of what the agent produces.&lt;/p&gt;
&lt;p&gt;If you are already running coding agents against real projects (with or without custom skills or MCP servers), I would like to hear what you have automated and what surprised you. Drop me a note; I am collecting examples for a follow-up.&lt;/p&gt;

</content>
  </entry>
  <entry>
    <title>Putting GA4 where the work happens: an in-browser analytics overlay and its MCP bridge</title>
    <link href="https://www.allaboutken.com/work/2026/impact-story-undrr-analytics-overlay-mcp/"/>
    <published>2026-04-20T00:00:00.000Z</published>
    <updated>2026-04-20T00:00:00.000Z</updated>
    <id>https://www.allaboutken.com/work/2026/impact-story-undrr-analytics-overlay-mcp/</id>
    <author>
      <name>Ken Hawkins</name>
      <email>khawkins98@gmail.com</email>
    </author>
    <category term="analytics" />
    <category term="developer tools" />
    <category term="AI / MCP" />
    <summary type="html">Content teams shouldn&#39;t have to leave the page to learn how it performs, and neither should AI agents.</summary>
    <content type="html">


&lt;p&gt;&lt;strong&gt;Context&lt;/strong&gt; | &lt;strong&gt;Self-service&lt;/strong&gt; | &lt;strong&gt;Agent-queryable&lt;/strong&gt;&lt;/p&gt;
&lt;p class=&quot;kh-text-body--3&quot;&gt;
  &lt;div class=&quot;kh-note-box&quot;&gt;
    &lt;span class=&quot;kh-note-box__label&quot;&gt;Context&lt;/span&gt;
    The United Nations Office for Disaster Risk Reduction (UNDRR) is the UN&amp;#39;s focal point for disaster risk reduction, coordinating global policy and supporting Member States to reduce disaster risk and losses.
  &lt;/div&gt;
&lt;/p&gt;
&lt;p&gt;Web analytics are most useful alongside the context of the pages you are reasoning about. Using AI agents to help also suffers as humans usually manually hand off data.&lt;/p&gt;
&lt;p&gt;To help myself and the team, I built a Chromium extension that puts GA4 metrics on UNDRR pages as an overlay, and extended it with a local MCP server so humans can have agents query the same data through the Chromium extension&#39;s same authenticated session.&lt;/p&gt;
&lt;p&gt;The overlay and the MCP bridge are the same tool wearing two faces: same auth, same config file, same measurement model. That is why neither had to be rebuilt to serve the other. This is the story of both pieces and why they belong together.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;These are the three web pages I am responsible for. Can you tell me what links people are not clicking on?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Those sorts of questions used to take an age to answer, but now we have quicker, better and more understandable answers.&lt;/p&gt;
&lt;h2 id=&quot;what-was-the-actual-problem&quot; tabindex=&quot;-1&quot;&gt;What was the actual problem?&lt;a href=&quot;#what-was-the-actual-problem&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Context switching to Looker Studio is where curiosity dies.&lt;/strong&gt; For editors wondering whether the policy page they just updated is getting traction, a five-step dashboard drill is slow enough that most of the time they skip it. The question gets answered only when the quarterly report lands, which is the wrong feedback loop for editorial decisions. We spent more time training editors on the tool than understanding the data.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;UNDRR&#39;s analytics model is not generic.&lt;/strong&gt; Twenty-two hostnames feed one GA4 property, downloads derive from URL patterns containing &lt;code&gt;/media/&lt;/code&gt;, engagement is tagged by page region (main, header, footer, feedback widget), and referrers are classified into eighteen strategic buckets before query time. Off-the-shelf analytics tools either ignore that model or flatten it.&lt;/p&gt;
&lt;h2 id=&quot;how-did-i-approach-this&quot; tabindex=&quot;-1&quot;&gt;How did I approach this?&lt;a href=&quot;#how-did-i-approach-this&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Overlay first, where the work happens.&lt;/strong&gt; The extension is a Manifest V3 Chromium extension. It authenticates through the Chrome Identity API, so there are no extra credentials. On any UNDRR page it can surface views, sessions, engagement rate, average duration, and a year-over-year comparison with an interactive daily chart. Tracked links and downloads get color-coded badges on the page itself; regions get rollups so editors can see which part of a page is actually doing the work.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Bridge second, on an auth boundary that already existed.&lt;/strong&gt; The MCP server runs on a &lt;code&gt;localhost&lt;/code&gt; port and talks to an offscreen document inside the extension over a persistent WebSocket. That offscreen document owns the OAuth session, so the MCP server never sees or stores a token. From the agent&#39;s side, fourteen tools are exposed: tactical ones (&lt;code&gt;query_top_content&lt;/code&gt;, &lt;code&gt;query_downloads&lt;/code&gt;, &lt;code&gt;query_traffic_sources&lt;/code&gt;, &lt;code&gt;query_geographic_reach&lt;/code&gt;, &lt;code&gt;query_region_events&lt;/code&gt;), strategic composites (&lt;code&gt;domain_overview&lt;/code&gt;, &lt;code&gt;annual_report_extract&lt;/code&gt;), and a raw GA4 escape hatch (&lt;code&gt;run_raw_ga4_query&lt;/code&gt;) for questions the composites do not anticipate.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;One source of truth, shared three ways.&lt;/strong&gt; &lt;code&gt;domains.json&lt;/code&gt; holds the twenty-two hostnames, their measurement IDs, and the referrer bucket classifiers. The extension&#39;s content script, its popup, and the MCP server all read the same file, so there is no config drift between what editors see in the overlay and what an agent sees through MCP.&lt;/p&gt;
&lt;h2 id=&quot;what-were-the-outcomes&quot; tabindex=&quot;-1&quot;&gt;What were the outcomes?&lt;a href=&quot;#what-were-the-outcomes&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Context at the point of editing.&lt;/strong&gt; Editors can now see how a page is performing while they are editing it. The question &amp;quot;is this working?&amp;quot; has a three-second answer instead of a five-step Looker Studio drill; editorial decisions close the loop on the same day rather than the next quarter.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Agent-speed analysis.&lt;/strong&gt; &amp;quot;Extract the annual report for 2025&amp;quot; is a single agent turn instead of roughly a forty-click dashboard walk. Parallel GA4 query batching inside &lt;code&gt;annual_report_extract&lt;/code&gt; roughly halves the wall-clock time on multi-section reports, which matters when a report spans a dozen sections.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;One credential, not two.&lt;/strong&gt; The agent reuses the overlay&#39;s OAuth session, so nobody is maintaining a second service account or rotating a separate key. For a small team inside a UN organisation, that is the difference between a tool that gets adopted and one that quietly stops being used.&lt;/p&gt;
&lt;h2 id=&quot;why-did-this-work&quot; tabindex=&quot;-1&quot;&gt;Why did this work?&lt;a href=&quot;#why-did-this-work&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;A tailored solution made sense because the measurement model is non-generic.&lt;/strong&gt; A general-purpose analytics MCP would expose GA4 as GA4. What editors and analysts actually need is GA4 shaped like UNDRR&#39;s referrer buckets, regions, and content types. Keeping the domain logic in &lt;code&gt;domains.json&lt;/code&gt; rather than in prompts means every agent gets the same context for free.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The MCP was not a rewrite.&lt;/strong&gt; It was a thin wrapper on an auth boundary that already existed. The extension already had an OAuth session, an offscreen document, and a query layer; MCP just made them addressable from outside the browser. Most of the hard decisions were security decisions: single-property lock, sender validation, passive failure modes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The one thing that nearly broke it.&lt;/strong&gt; A single bad Zod schema (valid in v3, rejected in v4) made &lt;code&gt;tools/list&lt;/code&gt; throw at registration, and that error is global: all fourteen tools disappeared from the agent at once. A stdio smoke test calling &lt;code&gt;tools/list&lt;/code&gt; as the first build step now catches this class of failure; without it, the MCP server would have shipped advertising capabilities it could not deliver.&lt;/p&gt;
&lt;h2 id=&quot;whats-next-and-why-this-matters-beyond-undrr&quot; tabindex=&quot;-1&quot;&gt;What&#39;s next, and why this matters beyond UNDRR&lt;a href=&quot;#whats-next-and-why-this-matters-beyond-undrr&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Self-service analytics inside the page is a familiar idea. Agent-queryable analytics that reuses an existing auth session is newer. This extension is one instance of the pattern I described in &lt;a href=&quot;https://www.allaboutken.com/posts/20260420-let-ai-worry-about-the-code/&quot;&gt;&lt;em&gt;Let AI worry about the code, you worry about the work&lt;/em&gt;&lt;/a&gt;: a coding agent on a real project, primed with your business context. The MCP server is the specific shape that worked here; the shape that works for you might be simpler.&lt;/p&gt;

</content>
  </entry>
  <entry>
    <title>How to write one skill for both Claude Code and GitHub Copilot CLI</title>
    <link href="https://www.allaboutken.com/posts/20260408-mini-guide-claude-copilot-skills/"/>
    <published>2026-04-08T00:00:00.000Z</published>
    <updated>2026-04-08T00:00:00.000Z</updated>
    <id>https://www.allaboutken.com/posts/20260408-mini-guide-claude-copilot-skills/</id>
    <author>
      <name>Ken Hawkins</name>
      <email>khawkins98@gmail.com</email>
    </author>
    <category term="AI tools" />
    <category term="Claude Code" />
    <category term="GitHub Copilot" />
    <category term="developer tools" />
    <summary type="html">Tips on where the shared approach doesn&#39;t share, and plugin.json gotchas that break Claude Code silently.</summary>
    <content type="html">
&lt;p&gt;Both Claude Code and GitHub Copilot CLI support skills (instruction files that load into the agent&#39;s context when you need them). The two tools share the same core file formats, so a single set of files can serve both. This guide covers how that works, the divergences, and the gotchas.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;tl;dr&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Write skills as &lt;code&gt;SKILL.md&lt;/code&gt; files with &lt;code&gt;name&lt;/code&gt; and &lt;code&gt;description&lt;/code&gt; frontmatter; both tools require exactly this.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;description&lt;/code&gt; field is the auto-detection trigger. Write it as a trigger condition: &lt;em&gt;&amp;quot;Use when the user asks to…&amp;quot;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;Omit the &lt;code&gt;skills&lt;/code&gt; field from &lt;code&gt;plugin.json&lt;/code&gt; entirely: Claude Code will reject the file if it&#39;s present.&lt;/li&gt;
&lt;li&gt;Copilot CLI supports agents, hooks, and MCP server config; Claude Code does not yet. These are safe to add for Copilot users without affecting Claude Code.&lt;/li&gt;
&lt;li&gt;If you just want a single skill without a plugin wrapper, you can drop the folder into &lt;code&gt;.github/skills/&lt;/code&gt;, &lt;code&gt;.claude/skills/&lt;/code&gt;, or &lt;code&gt;.agents/skills/&lt;/code&gt; (project) or the equivalent under &lt;code&gt;~/&lt;/code&gt; (personal) and Copilot CLI will find it without any manifest.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;The specifics below reflect the state of things as of April 2026. The &lt;a href=&quot;#canonical-references&quot;&gt;canonical references&lt;/a&gt; at the bottom are the authoritative source — if anything here conflicts with official docs, trust them, though even those can lag behind actual behavior.&lt;/p&gt;
&lt;h2 id=&quot;two-ways-to-distribute-skills&quot; tabindex=&quot;-1&quot;&gt;Two ways to distribute skills&lt;a href=&quot;#two-ways-to-distribute-skills&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;aside class=&quot;kh-marginnote&quot;&gt;The pattern is spreading: Cursor, Gemini CLI, Windsurf, and Codex CLI have all adopted variations of the SKILL.md format. I haven&#39;t tested compatibility with any of them directly, but portability across the ecosystem looks like the direction of travel.&lt;/aside&gt;
&lt;p&gt;Before getting into file structure, it helps to understand the two distribution models, because they look different even though the underlying &lt;code&gt;SKILL.md&lt;/code&gt; format is the same.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Standalone skills&lt;/strong&gt; are just a folder with a &lt;code&gt;SKILL.md&lt;/code&gt; inside, dropped into a location the tool already watches. No manifest, no plugin wrapper. For Copilot CLI, the watched locations are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.github/skills/&lt;/code&gt;, &lt;code&gt;.claude/skills/&lt;/code&gt;, or &lt;code&gt;.agents/skills/&lt;/code&gt; inside the current repository (project skills)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;~/.copilot/skills/&lt;/code&gt;, &lt;code&gt;~/.claude/skills/&lt;/code&gt;, or &lt;code&gt;~/.agents/skills/&lt;/code&gt; in your home directory (personal skills)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is the lightest-weight approach and the one GitHub&#39;s own docs emphasize. It works well when you have one or two skills and don&#39;t need to bundle them for distribution.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Plugin-bundled skills&lt;/strong&gt; wrap one or more skills inside a structured plugin directory. This is the model you want when you&#39;re packaging skills for a team or publishing them for others to install. Both tools discover skills from the &lt;code&gt;skills/&lt;/code&gt; subdirectory inside the plugin root automatically.&lt;/p&gt;
&lt;h2 id=&quot;the-file-structure-both-tools-understand&quot; tabindex=&quot;-1&quot;&gt;The file structure both tools understand&lt;a href=&quot;#the-file-structure-both-tools-understand&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;A plugin that works in both Claude Code and Copilot CLI looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;my-plugin/
├── .claude-plugin/
│   ├── plugin.json
│   └── marketplace.json    # optional, for marketplace listing
├── skills/
│   ├── my-first-skill/
│   │   ├── SKILL.md
│   │   └── supplementary.md  # optional; both tools inject these into context
│   └── my-second-skill/
│       └── SKILL.md
└── CLAUDE.md               # both tools read this; write it for both audiences
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;.claude-plugin/plugin.json&lt;/code&gt; path is Claude Code&#39;s default manifest location, and Copilot CLI also searches there, so if your manifest is already at &lt;code&gt;.claude-plugin/plugin.json&lt;/code&gt;, no move is needed. Note that Copilot CLI checks this path last (after &lt;code&gt;.plugin/plugin.json&lt;/code&gt;, &lt;code&gt;plugin.json&lt;/code&gt;, and &lt;code&gt;.github/plugin/plugin.json&lt;/code&gt;), so it&#39;s the right choice when you want Claude Code&#39;s default to work without restructuring, not for a Copilot-primary plugin.&lt;/p&gt;
&lt;h2 id=&quot;the-pluginjson-gotcha&quot; tabindex=&quot;-1&quot;&gt;The &lt;code&gt;plugin.json&lt;/code&gt; gotcha&lt;a href=&quot;#the-pluginjson-gotcha&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This one will burn you. Do &lt;strong&gt;not&lt;/strong&gt; add a &lt;code&gt;skills&lt;/code&gt; field to &lt;code&gt;plugin.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;name&amp;quot;: &amp;quot;my-plugin&amp;quot;,
  &amp;quot;description&amp;quot;: &amp;quot;What this plugin does&amp;quot;,
  &amp;quot;version&amp;quot;: &amp;quot;1.0.0&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Why: Copilot CLI defaults to the &lt;code&gt;skills/&lt;/code&gt; directory automatically when the field is absent. Claude Code, on the other hand, rejects &lt;code&gt;plugin.json&lt;/code&gt; with a validation error if a &lt;code&gt;skills&lt;/code&gt; field is present. Omit it and both tools work; add it and Claude Code breaks.&lt;/p&gt;
&lt;h2 id=&quot;writing-skillmd-frontmatter&quot; tabindex=&quot;-1&quot;&gt;Writing &lt;code&gt;SKILL.md&lt;/code&gt; frontmatter&lt;a href=&quot;#writing-skillmd-frontmatter&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Both tools require &lt;code&gt;name&lt;/code&gt; and &lt;code&gt;description&lt;/code&gt;. The &lt;code&gt;version&lt;/code&gt; field is Claude Code-specific but harmless to leave in:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;---
name: my-skill           # required by both; lowercase, hyphens only
description: &amp;gt;           # required by both -- this is the auto-detection trigger
  Use when the user asks to convert SVG files to PNG.
version: 1.0.0           # Claude Code only; Copilot CLI ignores it safely
allowed-tools:           # optional; tool names are platform-specific
  - Read                 # Claude Code name; Copilot CLI equivalents differ (e.g. read_file, bash)
  - Edit
---
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;the-description-field-is-everything&quot; tabindex=&quot;-1&quot;&gt;The description field is everything&lt;a href=&quot;#the-description-field-is-everything&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Both tools use the &lt;code&gt;description&lt;/code&gt; to decide whether to load the skill at all: the agent scans the list of installed skills, finds any whose description matches the current request, and injects those into context. The instructions in the &lt;code&gt;SKILL.md&lt;/code&gt; body only matter after the skill loads.&lt;/p&gt;
&lt;p&gt;Write it as a trigger condition, not a label. Compare these two:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Weak&lt;/th&gt;
&lt;th&gt;Strong&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;quot;Code review skill&amp;quot;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;quot;Use when the user asks to review, audit, or check code for issues&amp;quot;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;quot;Commit messages&amp;quot;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;quot;Use when the user asks to write, generate, or improve a git commit message&amp;quot;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Vague descriptions mean the skill won&#39;t auto-load when you need it. Narrow descriptions miss variations. Cover the vocabulary someone might actually use when asking for the task.&lt;/p&gt;
&lt;h3 id=&quot;a-note-on-allowed-tools&quot; tabindex=&quot;-1&quot;&gt;A note on &lt;code&gt;allowed-tools&lt;/code&gt;&lt;a href=&quot;#a-note-on-allowed-tools&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;allowed-tools&lt;/code&gt; field limits which tools the agent can invoke while the skill is active. This matters especially if you&#39;re distributing skills publicly: a &lt;code&gt;shell&lt;/code&gt; or &lt;code&gt;bash&lt;/code&gt; pre-approval in a skill from an untrusted source can allow it to run arbitrary commands without prompting you. GitHub&#39;s docs are explicit about this:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Only pre-approve the &lt;code&gt;shell&lt;/code&gt; or &lt;code&gt;bash&lt;/code&gt; tools if you have reviewed this skill and any referenced scripts, and you fully trust their source.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;When in doubt, omit &lt;code&gt;shell&lt;/code&gt; and &lt;code&gt;bash&lt;/code&gt; from &lt;code&gt;allowed-tools&lt;/code&gt;. The agent will still prompt for confirmation before running terminal commands.&lt;/p&gt;
&lt;h2 id=&quot;features-specific-to-github-copilot-cli&quot; tabindex=&quot;-1&quot;&gt;Features specific to GitHub Copilot CLI&lt;a href=&quot;#features-specific-to-github-copilot-cli&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;These features are Copilot CLI-specific. They have no Claude Code equivalent, so you can safely add them for Copilot users without affecting Claude Code behavior:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;How it works&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Custom agents&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;name&amp;gt;.agent.md&lt;/code&gt; files in an &lt;code&gt;agents/&lt;/code&gt; directory; the filename prefix becomes the agent ID (e.g. &lt;code&gt;reviewer.agent.md&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hooks&lt;/td&gt;
&lt;td&gt;&lt;code&gt;hooks.json&lt;/code&gt;; event handlers that fire on lifecycle events like &lt;code&gt;PostToolUse&lt;/code&gt; or &lt;code&gt;SessionStart&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MCP server config&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.mcp.json&lt;/code&gt;; connects the plugin to external APIs or databases via MCP&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id=&quot;what-claudemd-does-in-each-context&quot; tabindex=&quot;-1&quot;&gt;What &lt;code&gt;CLAUDE.md&lt;/code&gt; does in each context&lt;a href=&quot;#what-claudemd-does-in-each-context&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Both tools read &lt;code&gt;CLAUDE.md&lt;/code&gt;, but with slightly different scope. Claude Code treats it as plugin-level instructions loaded alongside the skill. Copilot CLI treats it as project-level custom instructions, the equivalent of what you&#39;d put in a &lt;code&gt;.github/copilot-instructions.md&lt;/code&gt;. Write content that makes sense in both contexts: conventions, constraints, what the tool should and shouldn&#39;t do. Avoid content that&#39;s only meaningful in one tool.&lt;/p&gt;
&lt;h2 id=&quot;testing-the-install&quot; tabindex=&quot;-1&quot;&gt;Testing the install&lt;a href=&quot;#testing-the-install&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;For Copilot CLI, install from a local directory and confirm in an interactive session:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Install (re-run after each edit to refresh the cache)
copilot plugin install /path/to/your-plugin

# Confirm it loaded
copilot plugin list

# In an interactive session
/skills list
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For Claude Code, pass &lt;code&gt;--plugin-dir /path/to/your-plugin&lt;/code&gt; when launching, or install permanently with &lt;code&gt;claude plugins install /path/to/your-plugin&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;key-things-to-keep-in-sync&quot; tabindex=&quot;-1&quot;&gt;Key things to keep in sync&lt;a href=&quot;#key-things-to-keep-in-sync&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;If you change either of these, test both tools:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;description&lt;/code&gt; in &lt;code&gt;SKILL.md&lt;/code&gt;&lt;/strong&gt;: The auto-detection trigger for both. If you update the skill&#39;s purpose, update the description to match or it won&#39;t load.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;name&lt;/code&gt; in &lt;code&gt;SKILL.md&lt;/code&gt;&lt;/strong&gt;: Must be unique across installed plugins. Renaming is breaking: users of the old name will lose auto-detection silently.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;name&lt;/code&gt; in &lt;code&gt;plugin.json&lt;/code&gt;&lt;/strong&gt;: Changing this is a breaking rename for anyone who has the plugin installed.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;using-this-as-llm-context&quot; tabindex=&quot;-1&quot;&gt;Using this as LLM context&lt;a href=&quot;#using-this-as-llm-context&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This post lives on GitHub as a Nunjucks file, syntactically close enough to Markdown that an LLM reads it fine. If you want an LLM to work from this guidance directly, here&#39;s a ready-to-paste prompt pointing at the raw source:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Please fetch https://raw.githubusercontent.com/khawkins98/allaboutken-11ty/main/src/site/posts/20260408-mini-guide-claude-copilot-skills.njk and use its guidance when helping me author skills compatible with both Claude Code and GitHub Copilot CLI.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I&#39;ll update this post as the tools evolve.&lt;/p&gt;
&lt;h2 id=&quot;canonical-references&quot;&gt;Canonical references&lt;/h2&gt;
&lt;p&gt;These are the authoritative sources. This post documents my own experience and cross-tool learnings; for the full specification, read these:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GitHub Copilot CLI&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/create-skills&quot;&gt;Creating skills for GitHub Copilot CLI&lt;/a&gt;: official how-to, covers standalone skills, scripts, and &lt;code&gt;allowed-tools&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/plugins-creating&quot;&gt;Creating a plugin for GitHub Copilot CLI&lt;/a&gt;: plugin manifest, distribution, and install&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.github.com/en/copilot/reference/copilot-cli-reference/cli-plugin-reference&quot;&gt;GitHub Copilot CLI plugin reference&lt;/a&gt;: complete technical specification&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Claude Code&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://code.claude.com/docs/en/plugins&quot;&gt;Claude Code plugins overview&lt;/a&gt;: getting started with the plugin system&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://code.claude.com/docs/en/plugins-reference&quot;&gt;Claude Code plugins reference&lt;/a&gt;: complete specification for skills, agents, hooks, MCP, and LSP components&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://code.claude.com/docs/en/plugin-marketplaces&quot;&gt;Claude Code plugin marketplaces&lt;/a&gt;: distribution and discovery&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Community guides worth reading&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.freecodecamp.org/news/how-to-build-your-own-claude-code-skill&quot;&gt;How to Build Your Own Claude Code Skill&lt;/a&gt;: freeCodeCamp walkthrough, excellent on the description field and skill design heuristics&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.morphllm.com/claude-code-skills-mcp-plugins&quot;&gt;Claude Code Skills vs MCP vs Plugins: Complete Guide 2026&lt;/a&gt;: good overview of when to use each extensibility type&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The overlap between the two ecosystems is real and growing. Know the three divergences, omit the &lt;code&gt;skills&lt;/code&gt; field from &lt;code&gt;plugin.json&lt;/code&gt;, and a single set of files will serve you in both tools. If you run into a case this guide doesn&#39;t cover, I&#39;d like to hear about it.&lt;/p&gt;
&lt;p&gt;If this sparked broader questions about how AI-assisted development actually works, &lt;a href=&quot;https://www.allaboutken.com/posts/20260405-four-phases-of-ai-adoption/&quot;&gt;my post on the four gears of AI-assisted development&lt;/a&gt; talks through the layers above and below this one — and &lt;a href=&quot;https://www.allaboutken.com/posts/20260330-context-was-always-the-job/&quot;&gt;why context engineering was always the job&lt;/a&gt; has more on why the description field matters so much.&lt;/p&gt;

</content>
  </entry>
  <entry>
    <title>The right answer has to be woven</title>
    <link href="https://www.allaboutken.com/posts/20260407-four-traditions-one-bottleneck/"/>
    <published>2026-04-07T00:00:00.000Z</published>
    <updated>2026-04-07T00:00:00.000Z</updated>
    <id>https://www.allaboutken.com/posts/20260407-four-traditions-one-bottleneck/</id>
    <author>
      <name>Ken Hawkins</name>
      <email>khawkins98@gmail.com</email>
    </author>
    <category term="context engineering" />
    <category term="AI" />
    <category term="content architecture" />
    <category term="information architecture" />
    <category term="software engineering" />
    <summary type="html">Several traditions solved the shared-understanding problem in parallel. LLMs are dissolving the boundary that kept them apart.</summary>
    <content type="html">
&lt;!--
== RESEARCH NOTES: external sources and connections ==

1. Eric Evans, Explore DDD 2024 keynote (Denver, March 2024)
   https://www.infoq.com/news/2024/03/Evans-ddd-experiment-llm/
   Evans explored the analogy that a fine-tuned language model is essentially a bounded context.
   Breaking instructions into smaller chunks (mirroring DDD decomposition) produced
   better results than monolithic prompts. Vaughn Vernon endorsed exploring LLM
   applications beyond chatbots.
   &gt;&gt; Angle: the DDD community sees the connection but frames it as &quot;how can AI
   help us do DDD?&quot; rather than &quot;DDD already described the context problem AI
   practitioners are rediscovering.&quot;

2. Rod Johnson, &quot;Context Engineering Needs Domain Understanding&quot; (July 2025)
   https://medium.com/@springrod/context-engineering-needs-domain-understanding-b4387e8e4bf8
   Creator of the Spring Framework. His DICE (Domain-Integrated Context Engineering)
   framework argues that context engineering is incomplete without domain modeling.
   Domain models should structure how context gets selected and organised, not left
   as an ever-growing string. Bounded contexts from DDD bring &quot;structure that is
   intuitive to humans and helpful to interacting with machines.&quot;
   &gt;&gt; Angle: the strongest single bridge piece between DDD and AI context engineering.
   Johnson comes from enterprise Java, not content strategy, yet arrives at the same
   place: without domain modeling, context engineering is just information management.

3. Birgitta Bockeler, &quot;Context Engineering for Coding Agents&quot; (Feb 2026)
   https://martinfowler.com/articles/exploring-gen-ai/context-engineering-coding-agents.html
   Published on martinfowler.com. Covers CLAUDE.md, rules, skills, subagents,
   MCP servers. Does NOT reference DDD at all, despite describing practices that are
   essentially informal bounded contexts and ubiquitous language documents.
   &gt;&gt; Angle: the most widely-read context engineering article in the DDD-adjacent
   world doesn&#39;t cite DDD. Illustrates the gap between traditions.

4. Russ Miles, &quot;Domain Driven Agent Design&quot; (Oct 2025)
   https://engineeringagents.substack.com/p/domain-driven-agent-design
   Finance agent example: confusing &quot;booking&quot; and &quot;reservation&quot; across bounded
   contexts causes compliance violations. Prescribes Event Storming before prompt design.
   &gt;&gt; Angle: the failure mode when traditions don&#39;t talk -- agent builders
   reinventing DDD&#39;s solutions without the vocabulary.

5. Alex Wolf, &quot;I Can&#39;t Do That, Dave&quot; (Mar 2026)
   https://systemic.engineering/ai-needs-identity/
   Argues the DDD community &quot;should be screaming&quot; about agents: how does the agent
   learn the ubiquitous language? Where are the bounded contexts?
   &gt;&gt; Angle: someone noticed the silence between the communities.

6. Duranti &amp; Goodwin, &quot;Rethinking Context&quot; (1992)
   Already cited in the &quot;context was always the job&quot; research notes.
   Key thesis: context is actively produced through interaction, not static background.

7. Karen McGrane, &quot;Content Strategy for Mobile&quot; (2012)
   Blobs vs. chunks. Content without structure can&#39;t travel across channels.
   Named by meaning, not appearance: &quot;news item&quot; survives redesign; &quot;featured card
   with sidebar layout&quot; doesn&#39;t. Direct parallel to DDD&#39;s entity modeling.

8. Daniel Jacobson, &quot;COPE: Create Once, Publish Everywhere&quot; (NPR, 2009)
   https://web.archive.org/web/20201214135612/https://www.programmableweb.com/news/cope-create-once-publish-everywhere/2009/10/13
   Content modeled as structured data with standardised interfaces, not as
   presentation-specific blobs. The content API is the bounded context interface.

9. Sathiyan Bakthavachalu, &quot;Revolutionizing Enterprise AI&quot; (July 2025)
   https://sathiyan.medium.com/revolutionizing-enterprise-ai-applying-domain-driven-design-for-agentic-applications-aa321fb991f4
   Most comprehensive DDD-pattern-to-agent-architecture mapping: bounded contexts
   as agent boundaries, context mapping patterns applied to agent-to-agent
   integration, ubiquitous language as semantic foundation.
   &gt;&gt; Angle: useful reference, but reads as a pattern catalogue without the
   cross-disciplinary lens.
--&gt;
&lt;p&gt;At a previous organisation, we syndicated content from Drupal across multiple downstream platforms through API feeds. Over the years, a recurring problem: a developer misspells a field name in the CMS, say &lt;code&gt;summray&lt;/code&gt; instead of &lt;code&gt;summary&lt;/code&gt;. A downstream JavaScript platform consumes that API and builds around the typo. Months later, someone fixes the typo in Drupal. Now the downstream platform breaks, because it was built against the misspelled field. So we add a translation layer to preserve the old typo alongside the fix. This happens again. And again. Each time, the downstream system absorbs more of the upstream system&#39;s historical mistakes, and the mapping between them gets more fragile.&lt;/p&gt;
&lt;p&gt;The team solved this problem for years with what I variously called a &amp;quot;translation protocol&amp;quot; or a &amp;quot;data dictionary&amp;quot;: ad hoc scripts and field-mapping spreadsheets, reinvented slightly differently each time. It wasn&#39;t until I started reading Eric Evans&#39; &lt;a href=&quot;https://www.domainlanguage.com/ddd/&quot;&gt;&lt;em&gt;Domain-Driven Design&lt;/em&gt;&lt;/a&gt; that I discovered the pattern had a name: the &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/architecture/patterns/anti-corruption-layer&quot;&gt;anticorruption layer&lt;/a&gt;. It&#39;s a deliberate translation boundary between two systems that prevents one system&#39;s model from leaking into another&#39;s. Instead of each downstream consumer adapting to whatever the upstream happens to expose (typos and all), you build an explicit layer that translates between the external model and your clean internal model. The upstream can change; your internal model stays coherent.&lt;/p&gt;
&lt;p&gt;Knowing the pattern wouldn&#39;t have just saved time; it would have changed how I structured the integration from the start. An anticorruption layer is a design decision you make at the boundary, not a patch you apply after the damage compounds. I&#39;d been solving a named problem with duct tape because the formalised solution lived in a tradition I&#39;d never read.&lt;/p&gt;
&lt;p&gt;The loose threads don&#39;t need to stay so.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;tl;dr&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Several traditions (among them software engineering&#39;s DDD, content strategy, and AI context engineering) each solved the problem of shared understanding across system boundaries&lt;/li&gt;
&lt;li&gt;They developed in parallel partly because they operated on different substrates: human-readable meaning vs. machine-executable contracts&lt;/li&gt;
&lt;li&gt;LLMs are dissolving that boundary; natural language is now a functional input to software&lt;/li&gt;
&lt;li&gt;Specific patterns now transfer concretely, and the cross-pollination has barely started&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;several-traditions-one-problem&quot; tabindex=&quot;-1&quot;&gt;Several traditions, one problem&lt;a href=&quot;#several-traditions-one-problem&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I &lt;a href=&quot;https://www.allaboutken.com/posts/20260330-context-was-always-the-job/&quot;&gt;wrote recently&lt;/a&gt; about how context was always the job. The more I&#39;ve sat with that idea, the more I&#39;ve noticed the same boundary-crossing problems showing up across fields, with solutions developed in parallel.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Software engineering.&lt;/strong&gt; Evans published DDD in 2003, arguing that the hard problem of software is shared understanding of the domain, not the code. His prescription: build a &amp;quot;ubiquitous language&amp;quot; that domain experts and developers both use, draw explicit boundaries (&amp;quot;bounded contexts&amp;quot;) around different models, and build translation layers where they meet. The primary artifact is the shared model, not the running system.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Content strategy.&lt;/strong&gt; Daniel Jacobson at NPR built &lt;a href=&quot;https://web.archive.org/web/20201214135612/https://www.programmableweb.com/news/cope-create-once-publish-everywhere/2009/10/13&quot;&gt;COPE&lt;/a&gt; (Create Once, Publish Everywhere) around content modelled as structured data with standardised interfaces. Karen McGrane&#39;s &lt;a href=&quot;https://abookapart.com/products/content-strategy-for-mobile&quot;&gt;&lt;em&gt;Content Strategy for Mobile&lt;/em&gt;&lt;/a&gt; sharpened the point: content trapped in a specific visual format can&#39;t travel across channels. You have to break it into meaningful chunks named by what they &lt;em&gt;are&lt;/em&gt;, not how they &lt;em&gt;look&lt;/em&gt;. &amp;quot;News item&amp;quot; survives a redesign; &amp;quot;featured card with sidebar layout&amp;quot; doesn&#39;t.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AI context engineering.&lt;/strong&gt; An LLM has general knowledge of the universe but not your project&#39;s constraints, your team&#39;s vocabulary, or why the module is shaped the way it is. The bottleneck isn&#39;t the model; it&#39;s &lt;a href=&quot;https://www.allaboutken.com/posts/20251003-effective-context-engineering-for-ai-agents/&quot;&gt;the context you provide&lt;/a&gt;. Practitioners are building agent instruction files (&lt;a href=&quot;http://CLAUDE.md&quot;&gt;CLAUDE.md&lt;/a&gt;, &lt;a href=&quot;http://AGENTS.md&quot;&gt;AGENTS.md&lt;/a&gt;, .cursorrules), project briefs, scoped rules, and &lt;a href=&quot;https://www.allaboutken.com/posts/20250929-ai-must-rtfm-context-curators/&quot;&gt;structured documentation for AI consumption&lt;/a&gt;. The discipline is new. The problem it&#39;s solving is not.&lt;/p&gt;
&lt;p&gt;These are the traditions I can see from where I sit. My career moved through journalism, content strategy, information architecture, design systems, and software engineering, and I now spend most of my time on AI integration. This is shaped by my path, not a natural taxonomy. Knowledge management, library science, and information science all have deep roots here too. And these traditions aren&#39;t fully independent: content strategy grew out of information architecture, AI context engineering is practiced almost entirely by software engineers, DDD drew on earlier modelling traditions. They share intellectual ancestry even where they developed solutions separately.&lt;/p&gt;
&lt;p&gt;At a high enough altitude, &amp;quot;decompose, name, and build explicit translation at boundaries&amp;quot; describes most of good engineering. The interesting question isn&#39;t whether the general principle holds. It&#39;s whether these traditions have produced &lt;em&gt;specific&lt;/em&gt; patterns worth transferring, and whether there&#39;s a reason that transfer is newly practical.&lt;/p&gt;
&lt;h2 id=&quot;why-the-boundary-is-dissolving&quot; tabindex=&quot;-1&quot;&gt;Why the boundary is dissolving&lt;a href=&quot;#why-the-boundary-is-dissolving&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Before LLMs, these traditions operated on fundamentally different substrates. Content strategy dealt with human-readable meaning: editorial voice, reader comprehension, how a headline works in a news feed. Software engineering dealt with machine-executable contracts: type systems, APIs, interface boundaries. The abstract patterns rhymed, but the gap between &amp;quot;write so a human editor understands the content model&amp;quot; and &amp;quot;write so a compiler enforces the interface contract&amp;quot; was wide enough that cross-pollination rarely paid off in practice. You could admire the parallels. You couldn&#39;t easily import the other tradition&#39;s tools.&lt;/p&gt;
&lt;aside class=&quot;kh-marginnote&quot;&gt;Linguistic anthropologists Duranti and Goodwin &lt;a href=&quot;https://web.archive.org/web/20030312200748/http://www.sscnet.ucla.edu/anthro/faculty/duranti/reprints/rethco.pdf&quot;&gt;argued in 1992&lt;/a&gt; that context is actively produced through mutual understanding between participants, not static background. LLMs don&#39;t have intersubjectivity in the way Duranti and Goodwin meant; I&#39;m borrowing the surface heuristic (design for context as something that has to be built, not just supplied) rather than the deeper theory.&lt;/aside&gt;
&lt;p&gt;LLMs are collapsing that gap. An agent instruction file like &lt;a href=&quot;http://CLAUDE.md&quot;&gt;CLAUDE.md&lt;/a&gt; is simultaneously human documentation and machine instruction. An editorial style guide that used to exist purely for human writers now directly shapes how an AI agent generates content. A content schema that used to govern what a CMS would accept now structures what context an agent receives. Natural language has become a functional input to software systems, and that changes which traditions have something to offer each other.&lt;/p&gt;
&lt;p&gt;I don&#39;t want to overstate the collapse. These instruction files are natural language, but a very specific kind: structured, imperative, rule-based. Closer to a config file written in English than to a conversation. Content strategy&#39;s insights about editorial governance transfer well to this new substrate; its insights about reader comprehension transfer less cleanly, because the &amp;quot;reader&amp;quot; (an LLM) has very different failure modes than a human. The boundary is thinning, not gone.&lt;/p&gt;
&lt;p&gt;But even a partial collapse changes what&#39;s practical. And AI has changed the cost of iteration enough to make context a designable variable. Before AI, context failures were slow and expensive to learn from: a misaligned contractor takes weeks to deliver the wrong thing; a content schema mismatch causes a slow bleed of broken syndication. You&#39;re not going to tear down a two-week pull request and rebuild it with a different briefing just to test whether the briefing was the problem.&lt;/p&gt;
&lt;p&gt;With an LLM, you do exactly that. You give the agent one framing, watch it fail, adjust the context, and try again — and the cost of that cycle is minutes, not days. Because the iteration is cheap, you start to notice &lt;em&gt;which&lt;/em&gt; context changes produce better outputs, which is how you discover that the problem was structural (missing bounded context, ambiguous vocabulary, no schema) rather than a one-off misunderstanding. Content strategists have always wanted to test whether restructuring content improves outcomes, but the feedback loop was too slow. Now it isn&#39;t.&lt;/p&gt;
&lt;h2 id=&quot;patterns-that-now-transfer&quot; tabindex=&quot;-1&quot;&gt;Patterns that now transfer&lt;a href=&quot;#patterns-that-now-transfer&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The anticorruption layer is one such transfer, from DDD to content architecture. Knowing the named pattern wouldn&#39;t have just saved me time; it would have changed how I structured the integration from the start. That&#39;s the cost of institutional silos: practitioners solve named problems with duct tape because the formalised solution lives in a tradition they&#39;ve never read. Many IA practitioners have read Evans; many DDD practitioners use content modelling techniques without calling them that. But the conferences, journals, and canonical texts don&#39;t overlap. A &lt;a href=&quot;https://www.jamescroft.co.uk/applying-domain-driven-design-principles-to-multi-agent-ai-systems/&quot;&gt;small but growing group&lt;/a&gt; is connecting DDD to AI agent design, but it hasn&#39;t reached the mainstream AI tooling ecosystem, and almost none of these voices are looking further afield to content strategy.&lt;/p&gt;
&lt;p&gt;There&#39;s a harder version of this objection: maybe content architects already build translation layers and just call them &amp;quot;field mapping&amp;quot; or &amp;quot;API adapters.&amp;quot; If the vocabulary difference is the main barrier, that&#39;s a weaker claim than if the structural thinking is what&#39;s missing. The anticorruption layer example shows it&#39;s not just vocabulary: knowing the named pattern would have changed &lt;em&gt;when&lt;/em&gt; and &lt;em&gt;how&lt;/em&gt; I built the translation, not just what I called it. But not every parallel will cash out that concretely.&lt;/p&gt;
&lt;p&gt;Here&#39;s a transfer running the other direction: from content strategy to AI context engineering.&lt;/p&gt;
&lt;p&gt;Content strategists learned years ago that you define content types with required and optional fields, relationships to other types, and governance rules about who can create and edit them. This is the content schema: the structural backbone of any CMS at scale. A news article has a required headline, required body, optional teaser, required topic taxonomy, and a relationship to an author profile. These aren&#39;t suggestions; they&#39;re enforced by the CMS.&lt;/p&gt;
&lt;p&gt;When I started structuring context for AI agents, I recognised the same problem. An agent working on a blog post in this site&#39;s codebase needs specific context: the editorial style guide, the image path conventions (which are non-obvious; the build rewrites them), the frontmatter schema, the CSS scoping rules. Some of that context is required for every task; some is optional enrichment. The relationships between context sources matter: the style guide references the frontmatter spec, which references the image conventions.&lt;/p&gt;
&lt;p&gt;A content strategist would immediately think in terms of a content schema: define the required context per task type, make the relationships explicit, assign governance for who maintains each source. Most AI context engineering doesn&#39;t think this way yet. Agent instruction files tend to grow organically: someone hits a problem, adds a line, moves on. There&#39;s no schema, no required-vs-optional distinction, no governance. It&#39;s fair to ask whether this is a cross-pollination gap or just a maturity issue; these conventions are roughly a year old, and this is what every structured documentation system looks like in year one. Both are probably true. But content strategists have fifteen years of lessons about &lt;em&gt;how&lt;/em&gt; to mature these structures, and now that these instruction files are natural language doing a machine&#39;s job, those lessons land in a way they couldn&#39;t when the substrates were different.&lt;/p&gt;
&lt;h2 id=&quot;from-where-i-sit&quot; tabindex=&quot;-1&quot;&gt;From where I sit&lt;a href=&quot;#from-where-i-sit&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I&#39;m not arguing these traditions should merge. I&#39;m noting that the barrier which kept them apart, the gap between human-readable meaning and machine-executable contracts, is narrower than it&#39;s ever been. Specific patterns now transfer concretely.&lt;/p&gt;
&lt;p&gt;If you&#39;re working on context engineering for AI and you haven&#39;t read Evans on ubiquitous language, there are twenty years of lessons on formalising shared understanding that you&#39;re rediscovering from scratch. If you&#39;re doing DDD and haven&#39;t looked at how content strategists model structured content across platforms, you&#39;re missing a parallel tradition that solved the &amp;quot;how does this travel across boundaries&amp;quot; problem for editorial organisations.&lt;/p&gt;
&lt;p&gt;The stable artifacts matter: a well-governed schema, a maintained instruction file, a shared vocabulary. But they only stay useful if someone is actively maintaining them. That&#39;s the common thread: Evans&#39; domain models require ongoing collaboration with domain experts, McGrane&#39;s content schemas need governance to stay current, agent instruction files decay the moment the codebase moves on without them. None of these traditions own that insight. All of them arrived at it. The weaving has started, but it hasn&#39;t reached mainstream practice in any of these fields yet.&lt;/p&gt;
&lt;h2 id=&quot;related-writing&quot; tabindex=&quot;-1&quot;&gt;Related writing&lt;a href=&quot;#related-writing&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.allaboutken.com/posts/20260330-context-was-always-the-job/&quot;&gt;Why context engineering was always the job&lt;/a&gt; (Mar 2026): the personal evolution version of this argument&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.allaboutken.com/posts/20260327-content-architecture-shipping-container/&quot;&gt;Content architecture: the delivery problem&lt;/a&gt; (Mar 2026): content structure as shared infrastructure&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.allaboutken.com/posts/20250929-ai-must-rtfm-context-curators/&quot;&gt;Developers are becoming context technical writers for AI&lt;/a&gt; (Sep 2025): the rise of context curation&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.allaboutken.com/posts/20251003-effective-context-engineering-for-ai-agents/&quot;&gt;Context engineering beats prompt tricks for AI agents&lt;/a&gt; (Oct 2025): framing the input is the real skill&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;cross-tradition-reading&quot; tabindex=&quot;-1&quot;&gt;Cross-tradition reading&lt;a href=&quot;#cross-tradition-reading&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Duranti &amp;amp; Goodwin, &lt;a href=&quot;https://web.archive.org/web/20030312200748/http://www.sscnet.ucla.edu/anthro/faculty/duranti/reprints/rethco.pdf&quot;&gt;&lt;em&gt;Rethinking Context: Language as an Interactive Phenomenon&lt;/em&gt;&lt;/a&gt; (1992). The academic foundation: context is produced through interaction, not inherited as background.&lt;/li&gt;
&lt;li&gt;Eric Evans, &lt;a href=&quot;https://www.domainlanguage.com/ddd/&quot;&gt;&lt;em&gt;Domain-Driven Design&lt;/em&gt;&lt;/a&gt; (2003). Ubiquitous language and bounded contexts as software engineering&#39;s answer to the shared understanding problem.&lt;/li&gt;
&lt;li&gt;Karen McGrane, &lt;a href=&quot;https://abookapart.com/products/content-strategy-for-mobile&quot;&gt;&lt;em&gt;Content Strategy for Mobile&lt;/em&gt;&lt;/a&gt; (2012). Blobs vs. chunks, and why content must be structured by meaning to travel across channels.&lt;/li&gt;
&lt;li&gt;Rod Johnson, &lt;a href=&quot;https://medium.com/@springrod/context-engineering-needs-domain-understanding-b4387e8e4bf8&quot;&gt;Context Engineering Needs Domain Understanding&lt;/a&gt; (2025). The strongest bridge between DDD and AI context engineering.&lt;/li&gt;
&lt;li&gt;Russ Miles, &lt;a href=&quot;https://engineeringagents.substack.com/p/domain-driven-agent-design&quot;&gt;Domain Driven Agent Design&lt;/a&gt; (2025). What happens when agents cross bounded contexts without translation layers.&lt;/li&gt;
&lt;li&gt;Paul Dourish, &lt;a href=&quot;https://www.dourish.com/publications/2004/PUC2004-context.pdf&quot;&gt;What We Talk About When We Talk About Context&lt;/a&gt; (2004). The closest existing bridge between linguistic anthropology and computing; argues that context is interactional, not representational.&lt;/li&gt;
&lt;li&gt;Lucy Suchman, &lt;em&gt;Plans and Situated Actions&lt;/em&gt; (1987; &lt;a href=&quot;https://www.cambridge.org/core/books/humanmachine-reconfigurations/2DF52E4AA3A95914E464A93AFEE2B0C9&quot;&gt;2nd ed. 2007&lt;/a&gt;). Suchman showed that people don&#39;t follow plans the way AI planners assumed — they improvise based on situated context. The modern agent community&#39;s discovery that rigid task decomposition fails without runtime context is the same finding.&lt;/li&gt;
&lt;/ul&gt;

</content>
  </entry>
</feed>
