<?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-03-03T00:00:00.000Z</updated>
  <id>https://www.allaboutken.com/</id>
  <author>
    <name>Ken Hawkins</name>
    <email>khawkins98@gmail.com</email>
  </author>
  <entry>
    <title>Blog art that belongs</title>
    <link href="https://www.allaboutken.com/posts/20260303-generating-blog-art/"/>
    <published>2026-03-03T00:00:00.000Z</published>
    <updated>2026-03-03T00:00:00.000Z</updated>
    <id>https://www.allaboutken.com/posts/20260303-generating-blog-art/</id>
    <author>
      <name>Ken Hawkins</name>
      <email>khawkins98@gmail.com</email>
    </author>
    <category term="eleventy" />
    <category term="tools" />
    <category term="design" />
    <summary type="html">Every post needs a hero image. Sometimes you don&#39;t need the perfect one -- you just need something that doesn&#39;t clash.</summary>
    <content type="html">
&lt;p&gt;For a while I was using &lt;a href=&quot;https://www.loras.dev/&quot;&gt;Loras&lt;/a&gt; for post art. Fine results, but the workflow sat completely outside of my writing flow and I&#39;d never be able to fully adapt that to my style and iterate ... and, well, learn more about using image generators.&lt;/p&gt;
&lt;p&gt;When &lt;a href=&quot;https://github.com/black-forest-labs/flux2&quot;&gt;FLUX.2-dev&lt;/a&gt; came out I wanted to try it — and connect it to the blog workflow directly rather than bolt on another external script. So I built a &lt;a href=&quot;https://www.allaboutken.com/image-generator/&quot;&gt;small browser tool&lt;/a&gt; that lives in the site itself. Paste your &lt;a href=&quot;https://www.together.ai/&quot;&gt;Together AI&lt;/a&gt; key, type a subject, hit Generate — three images run in parallel in about 20-30 seconds. Click one, crop, save straight to &lt;code&gt;src/site/images/blog/&lt;/code&gt;. The tool spits out a frontmatter snippet ready to paste. All without leaving the browser.&lt;/p&gt;

&lt;figure class=&quot;kh-figure&quot;&gt;
  &lt;img class=&quot;kh-figure__image&quot; width=&quot;600&quot; src=&quot;https://www.allaboutken.com/images/blog/pagefind-paths-through-grass.jpg&quot; alt=&quot;Blockprint-style woodcut of two winding pale paths through dark dense grass with warm amber accents&quot;&gt;
  &lt;figcaption class=&quot;kh-figure__caption&quot;&gt;For a post about search without external services: &quot;paths worn into grass by footsteps, no paved road, just natural routes.&quot;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;kh-figure&quot;&gt;
  &lt;img class=&quot;kh-figure__image&quot; width=&quot;600&quot; src=&quot;https://www.allaboutken.com/images/blog/semantic-search-vectors.jpg&quot; alt=&quot;Woodcut-style print of glowing amber and white orbs scattered across a dark background with rough white hatching marks&quot;&gt;
  &lt;figcaption class=&quot;kh-figure__caption&quot;&gt;For a post about vector embeddings: &quot;glowing points scattered in a dark field, some clustered, some drifting apart.&quot;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;A consistent style prefix keeps images looking like a set across posts rather than a random pile. The rest is just picking a prompt that fits the feel of the post.&lt;/p&gt;
&lt;p&gt;Three images run about $0.03 via Together AI. The tool is a &lt;a href=&quot;https://github.com/khawkins98/allaboutken-11ty/blob/main/src/site/image-generator.njk&quot;&gt;single self-contained page&lt;/a&gt; if you want to adapt it.&lt;/p&gt;

</content>
  </entry>
  <entry>
    <title>Semantic search on a static site, no API keys required</title>
    <link href="https://www.allaboutken.com/posts/20260302-semantic-search-browser-embeddings/"/>
    <published>2026-03-02T00:00:00.000Z</published>
    <updated>2026-03-02T00:00:00.000Z</updated>
    <id>https://www.allaboutken.com/posts/20260302-semantic-search-browser-embeddings/</id>
    <author>
      <name>Ken Hawkins</name>
      <email>khawkins98@gmail.com</email>
    </author>
    <category term="eleventy" />
    <category term="search" />
    <category term="machine learning" />
    <category term="static sites" />
    <summary type="html">Vector embeddings at build time, cosine similarity in the browser. The same 23 MB model runs both sides.</summary>
    <content type="html">
&lt;p&gt;Last week I &lt;a href=&quot;https://www.allaboutken.com/posts/20260228-replacing-lunr-with-pagefind/&quot;&gt;replaced Lunr.js with Pagefind&lt;/a&gt; for keyword search on this site. Pagefind works well ... type &amp;quot;eleventy image plugin&amp;quot; and get back pages containing those words. But search for &amp;quot;&lt;a href=&quot;https://www.allaboutken.com/search/?search_query=image+formatting&quot;&gt;image formatting&lt;/a&gt;&amp;quot; and you&#39;ll miss the post you want.&lt;/p&gt;
&lt;p&gt;AI would find it better.&lt;/p&gt;
&lt;p&gt;I wanted to see how close I could get to natural-language search experience on a static site without any cloud infrastructure. No API keys, no SaaS, no server. Could the same embedding model run at build time and in the browser, and would the results actually be useful?&lt;/p&gt;
&lt;p&gt;The answer is mostly: yes. Although the visitor&#39;s browser has to download ~30 MB on first use, the query runs entirely on device, and results are quite good.&lt;/p&gt;
&lt;a href=&quot;https://www.allaboutken.com/search/?search_type=semantic&quot; class=&quot;kh-button&quot;&gt;Try the semantic search&lt;/a&gt;&lt;p&gt;As I don&#39;t get much traffic, this is a bit silly. But it&#39;s the perfect thing for my website playground. Vector search on a personal blog is a toy. Vector search on a documentation site with hundreds of pages and users who don&#39;t know the right terminology is a different story.&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;Build-time script generates vector embeddings for all posts using &lt;a href=&quot;https://huggingface.co/Xenova/all-MiniLM-L6-v2&quot;&gt;MiniLM-L6-v2&lt;/a&gt; via &lt;a href=&quot;https://huggingface.co/docs/transformers.js&quot;&gt;Transformers.js&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Splits each post into overlapping chunks and embeds them separately — relevant content deep in a post is no longer invisible&lt;/li&gt;
&lt;li&gt;Produces a &lt;a href=&quot;https://www.allaboutken.com/semantic-search/vectors.json&quot;&gt;&lt;code&gt;vectors.json&lt;/code&gt;&lt;/a&gt; file shipped as a static asset&lt;/li&gt;
&lt;li&gt;Browser loads the same model on first query (~30 MB with WASM runtime, cached afterward)&lt;/li&gt;
&lt;li&gt;Cosine similarity ranks posts by meaning, not keywords&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.allaboutken.com/search/?search_type=semantic&quot;&gt;Try it on the search page&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;how-vector-search-works&quot; tabindex=&quot;-1&quot;&gt;How vector search works&lt;a href=&quot;#how-vector-search-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;Traditional search (Lunr, Pagefind, Elasticsearch) builds an inverted index: for each word, store which documents contain it. Query terms get looked up in the index and documents are ranked by term frequency, document length, and similar signals. This works well when the user&#39;s words match the author&#39;s words.&lt;/p&gt;
&lt;p&gt;With vector search, a small neural network (&lt;a href=&quot;https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2&quot;&gt;all-MiniLM-L6-v2&lt;/a&gt;) reads a piece of text and produces a fixed-length array of numbers — a 384-dimension vector — that captures the text&#39;s meaning. Two texts about similar topics produce vectors that point in similar directions, even if they share no words.&lt;/p&gt;
&lt;p&gt;At query time, the same model converts the question into a vector. The ranking itself is just &lt;a href=&quot;https://en.wikipedia.org/wiki/Cosine_similarity&quot;&gt;cosine similarity&lt;/a&gt;. All the intelligence is handed off to the model that produces the vectors.&lt;/p&gt;
&lt;p&gt;Want a visual explanation of the difference between keyword and vector search? This video covers it well:&lt;/p&gt;

&lt;aside class=&quot;kh-video&quot;&gt;

&lt;/aside&gt;
&lt;h2 id=&quot;the-build-time-half&quot; tabindex=&quot;-1&quot;&gt;The build-time half&lt;a href=&quot;#the-build-time-half&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 Node.js script runs after Eleventy in the build pipeline. It reads every HTML file in the &lt;code&gt;build/&lt;/code&gt; directory, extracts text from &lt;code&gt;&amp;lt;main data-pagefind-body&amp;gt;&lt;/code&gt; (the same element Pagefind uses to decide what&#39;s content), and generates embeddings.&lt;/p&gt;
&lt;p&gt;MiniLM-L6-v2 has a 512-token (~2048 character) input limit — anything longer gets silently truncated. So if the relevant content is 2000 characters into a post, a single whole-page embedding won&#39;t see it. The script splits each post&#39;s body into overlapping ~1000-character chunks (stride 800, snapping to sentence boundaries), then embeds each one separately. The first chunk gets the title, description, and body text; later chunks get the title and body text only.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const chunks = chunkText(content.bodyText);  // ~1000 chars each, 200-char overlap
for (const chunk of chunks) {
  const text = title + &#39; &#39; + chunk.text;     // chunk 0 also gets description
  const output = await extractor(text, { pooling: &#39;mean&#39;, normalize: true });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The output is a JSON file with one entry per chunk:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;model&amp;quot;: &amp;quot;Xenova/all-MiniLM-L6-v2&amp;quot;,
  &amp;quot;dimension&amp;quot;: 384,
  &amp;quot;version&amp;quot;: 2,
  &amp;quot;chunks&amp;quot;: [
    {
      &amp;quot;url&amp;quot;: &amp;quot;/posts/example/&amp;quot;,
      &amp;quot;title&amp;quot;: &amp;quot;...&amp;quot;,
      &amp;quot;snippet&amp;quot;: &amp;quot;First 200 chars of this chunk...&amp;quot;,
      &amp;quot;teaser&amp;quot;: &amp;quot;...&amp;quot;,
      &amp;quot;date&amp;quot;: &amp;quot;2026-02-28&amp;quot;,
      &amp;quot;embedding&amp;quot;: [0.023, -0.041, ...]
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each chunk carries a &lt;code&gt;snippet&lt;/code&gt; — the first 200 characters of that chunk&#39;s text — which the browser can show in results instead of the generic meta description. Only the first chunk per page carries &lt;code&gt;teaser&lt;/code&gt; and &lt;code&gt;date&lt;/code&gt; to avoid bloating the file.&lt;/p&gt;
&lt;p&gt;Embeddings are rounded to 4 decimal places, well within MiniLM&#39;s noise floor. For 89 pages producing 645 chunks, the file is 2 MB raw, ~600 KB gzipped. The embedding step takes longer than the single-embedding approach but is still reasonable for a production build.&lt;/p&gt;
&lt;p&gt;Embeddings run after Eleventy (which runs Pagefind in its &lt;code&gt;eleventy.after&lt;/code&gt; hook), so the full HTML output is available.&lt;/p&gt;
&lt;p&gt;The build-time &lt;code&gt;@huggingface/transformers&lt;/code&gt; dependency is heavy — roughly 476 MB in &lt;code&gt;node_modules&lt;/code&gt;, mostly &lt;a href=&quot;https://onnxruntime.ai/&quot;&gt;ONNX Runtime&lt;/a&gt; native binaries for running the model in Node.js. It ships prebuilt binaries for every platform (macOS, Linux, Windows, multiple architectures), and there&#39;s no way to install only the one you need. None of this ships to the browser — it only runs during the build to generate the vectors.&lt;/p&gt;
&lt;p&gt;This is the same kind of tradeoff as Pagefind shipping a Rust binary for its indexing step: the build-time tooling is large because something has to actually run the neural network over your content. The output is small and fast to query, but producing it requires real compute.&lt;/p&gt;
&lt;p&gt;A Python script with just &lt;a href=&quot;https://pypi.org/project/onnxruntime/&quot;&gt;&lt;code&gt;onnxruntime&lt;/code&gt;&lt;/a&gt; and &lt;a href=&quot;https://pypi.org/project/tokenizers/&quot;&gt;&lt;code&gt;tokenizers&lt;/code&gt;&lt;/a&gt; (no PyTorch) would cut the footprint to ~75-100 MB — &lt;a href=&quot;https://github.com/chroma-core/onnx-embedding&quot;&gt;ChromaDB uses this approach&lt;/a&gt; in production. You could also compile a Rust binary with &lt;a href=&quot;https://github.com/Anush008/fastembed-rs&quot;&gt;fastembed-rs&lt;/a&gt;, though it&#39;s a library, not a CLI, so you&#39;d need to write a wrapper. Either way, it means adding a non-Node dependency to the build. For now, one fat npm install is simpler.&lt;/p&gt;
&lt;p&gt;The embedding script is not included in &lt;code&gt;yarn dev&lt;/code&gt; — it&#39;s too slow for iterative development, same as my Pagefind indexing step. Run &lt;code&gt;yarn build&lt;/code&gt; once to generate vectors, then &lt;code&gt;yarn dev&lt;/code&gt; as usual.&lt;/p&gt;
&lt;h2 id=&quot;the-browser-half&quot; tabindex=&quot;-1&quot;&gt;The browser half&lt;a href=&quot;#the-browser-half&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 semantic search UI loads nothing on initial visit. On first query submit, three things happen:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Fetch &lt;code&gt;/semantic-search/vectors.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Import &lt;a href=&quot;https://cdn.jsdelivr.net/npm/@huggingface/transformers@3&quot;&gt;Transformers.js from jsDelivr&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Initialize the MiniLM pipeline (~23 MB model + ~4 MB WASM runtime, with progress bar)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The model and runtime download once and cache normally. Subsequent queries within the same session are near-instant — embed the query (~100 ms), compute 645 dot products across all chunks, deduplicate to the best-matching chunk per page, sort and display. The scoring loop plus Map dedup takes under 10 ms.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// Lazy-load on first query, not on page load
const { pipeline } = await import(&#39;https://cdn.jsdelivr.net/npm/@huggingface/transformers@3&#39;);

extractor = await pipeline(&#39;feature-extraction&#39;, &#39;Xenova/all-MiniLM-L6-v2&#39;, {
  dtype: &#39;q8&#39;,
  progress_callback: (progress) =&amp;gt; {
    if (progress.status === &#39;progress&#39;) {
      progressBar.value = Math.round(progress.progress);
    }
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Transformers.js handles model caching via the browser&#39;s &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Cache&quot;&gt;Cache API&lt;/a&gt;, keyed by model ID. The ~23 MB download happens once; subsequent visits load from cache.&lt;/p&gt;
&lt;p&gt;Results are filtered by a similarity threshold (score &amp;gt; 0.25) and limited to five. Below that threshold, results tend to be noise. The response templates are simple conditionals — no LLM generates the answer text:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Zero results: &amp;quot;I couldn&#39;t find anything closely related. Try rephrasing, or use keyword search.&amp;quot;&lt;/li&gt;
&lt;li&gt;One high-confidence result: &amp;quot;I found one post that closely matches:&amp;quot;&lt;/li&gt;
&lt;li&gt;Multiple results: &amp;quot;Here are N posts that might be relevant:&amp;quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Every response includes a fallback link to Pagefind keyword search.&lt;/p&gt;
&lt;h2 id=&quot;what-the-model-sees&quot; tabindex=&quot;-1&quot;&gt;What the model sees&lt;a href=&quot;#what-the-model-sees&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;MiniLM-L6-v2 is a &lt;a href=&quot;https://www.sbert.net/&quot;&gt;sentence transformer&lt;/a&gt; — a BERT variant fine-tuned for producing sentence embeddings. It was trained on &lt;a href=&quot;https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2&quot;&gt;over a billion sentence pairs&lt;/a&gt; and distilled from a larger model. The &amp;quot;L6&amp;quot; means six transformer layers (BERT-base has twelve), and the output is 384 dimensions (half of BERT&#39;s 768). Smaller and faster, at the cost of some nuance.&lt;/p&gt;
&lt;p&gt;It doesn&#39;t understand content the way a large language model would. It maps text into a geometric space where similar meanings cluster together. &amp;quot;Why meetings kill productivity&amp;quot; and &amp;quot;short disruptions cost lots of time&amp;quot; land near each other because the model learned from millions of examples that those phrases appear in similar contexts.&lt;/p&gt;
&lt;p&gt;The input you feed it matters more than I expected. Just the title loses too much nuance. The entire article dilutes the signal with boilerplate. The model&#39;s hard limit is 512 tokens (~2048 characters) — anything longer gets silently truncated. So instead of embedding each page as a single vector, the build script splits the body into overlapping ~1000-character chunks and embeds each one with the title prepended. At query time, the browser scores every chunk and keeps the best match per page. This means relevant content buried 3000 characters into a post still surfaces, and the best-matching chunk&#39;s snippet can be shown in results instead of the generic meta description.&lt;/p&gt;
&lt;h2 id=&quot;pagefind-and-semantic-search-compared&quot; tabindex=&quot;-1&quot;&gt;Pagefind and semantic search compared&lt;a href=&quot;#pagefind-and-semantic-search-compared&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Pagefind (keyword)&lt;/th&gt;
&lt;th&gt;Semantic search&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Query type&lt;/td&gt;
&lt;td&gt;Exact and fuzzy keyword matching&lt;/td&gt;
&lt;td&gt;Natural language, meaning-based&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Initial download&lt;/td&gt;
&lt;td&gt;~10 KB JS + ~75 KB WASM&lt;/td&gt;
&lt;td&gt;~600 KB vectors + ~30 MB model + runtime (cached)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-query cost&lt;/td&gt;
&lt;td&gt;~10-30 KB index fragments&lt;/td&gt;
&lt;td&gt;~100 ms compute, no network&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best for&lt;/td&gt;
&lt;td&gt;Known terms, specific phrases&lt;/td&gt;
&lt;td&gt;Exploratory questions, topic discovery&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Excerpts&lt;/td&gt;
&lt;td&gt;Yes, highlighted in context&lt;/td&gt;
&lt;td&gt;Yes (best-matching chunk snippet)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build dependency&lt;/td&gt;
&lt;td&gt;Rust binary (via npm)&lt;/td&gt;
&lt;td&gt;~476 MB ONNX runtime (via npm)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Pagefind is better when you know the words. Semantic search is better when you know the idea but not the vocabulary. Both live on the same search page — keyword search is the default, and semantic search is an opt-in that loads on demand.&lt;/p&gt;
&lt;h2 id=&quot;tradeoffs&quot; tabindex=&quot;-1&quot;&gt;Tradeoffs&lt;a href=&quot;#tradeoffs&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;The 30 MB elephant.&lt;/strong&gt; The first query downloads the quantized model (~23 MB) plus the ONNX Runtime WASM engine (~4 MB), tokenizer, and Transformers.js library — about 30 MB total. The full-precision model is 90 MB; 8-bit quantization (&lt;code&gt;dtype: &#39;q8&#39;&lt;/code&gt;) gets it to 23 MB with no noticeable quality loss for sentence embeddings. Everything caches, so repeat visitors pay nothing, but the first query on a new device is a real wait. The progress bar helps set expectations. I looked at lighter alternatives (see the Model2Vec note in &amp;quot;What I&#39;d change&amp;quot;) but nothing I tested matched the result quality of running a real transformer — the 30 MB is the cost of results that actually work.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Chunking bloats vectors.json.&lt;/strong&gt; Splitting posts into overlapping chunks means ~7x more embeddings than one-per-page. The vectors file grows from 727 KB to 2 MB raw (~600 KB gzipped). Build time increases proportionally. The payoff: results now show a relevant snippet from the best-matching chunk instead of the generic meta description, and content buried deep in a post actually surfaces.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Threshold tuning.&lt;/strong&gt; The 0.25 cosine similarity cutoff is hand-tuned. Too low and irrelevant results creep in. Too high and you miss valid matches. There&#39;s no perfect number — it depends on the corpus and the kinds of queries people ask.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CDN dependency.&lt;/strong&gt; Transformers.js and the ONNX Runtime WASM load from jsDelivr; the model files load from HuggingFace via Transformers.js&#39;s Cache API. If either CDN is down, the page shows an error with a link to keyword search.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;No conversational LLM.&lt;/strong&gt; You could generate natural-language answers on top of the embeddings, but even small LLMs need 1-4 GB downloads and &lt;a href=&quot;https://caniuse.com/webgpu&quot;&gt;WebGPU support&lt;/a&gt;. For finding a blog post, a ranked list with teasers does the job.&lt;/p&gt;
&lt;h2 id=&quot;tips-if-youre-doing-this&quot; tabindex=&quot;-1&quot;&gt;Tips if you&#39;re doing this&lt;a href=&quot;#tips-if-youre-doing-this&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;
&lt;p&gt;&lt;strong&gt;Reuse your existing content boundary.&lt;/strong&gt; If you already have &lt;code&gt;data-pagefind-body&lt;/code&gt; or similar markup, use it as the extraction boundary for embeddings too. One source of truth for &amp;quot;what counts as content.&amp;quot;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Chunk the body, prepend the title.&lt;/strong&gt; Don&#39;t feed the model raw HTML or entire articles — MiniLM silently truncates past 512 tokens. Split the body into overlapping ~1000-character chunks, prepend the title to each, and embed separately. At query time, score every chunk and keep the best match per page. This surfaces content that&#39;s deep in long posts and gives you a relevant snippet for free.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Lazy-load everything.&lt;/strong&gt; Don&#39;t download the model on page load. Most visitors will never use semantic search. Load on first interaction and show a progress bar.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Ship both search types.&lt;/strong&gt; Keyword and semantic search solve different problems. Cross-link between them so users can switch when one doesn&#39;t find what they need.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Set a similarity threshold.&lt;/strong&gt; Without a cutoff, every query returns results — even nonsense queries. Start at 0.25 and adjust based on your corpus.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;what-id-explore&quot; tabindex=&quot;-1&quot;&gt;What I&#39;d explore&lt;a href=&quot;#what-id-explore&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;&lt;s&gt;Show snippets, not just teasers.&lt;/s&gt;&lt;/strong&gt; Done. The build script now splits each post into overlapping chunks and embeds them separately. Results show the best-matching chunk&#39;s snippet instead of the generic meta description. Storage grew from 727 KB to 2 MB for vectors.json (~600 KB gzipped), but the results are noticeably better — queries about topics mentioned only deep in a post now surface correctly.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Shrink the vectors.&lt;/strong&gt; Each embedding is 384 float32 values (1,536 bytes). &lt;a href=&quot;https://huggingface.co/blog/embedding-quantization&quot;&gt;Binary quantization&lt;/a&gt; thresholds each value to a single bit and uses Hamming distance instead of cosine similarity. That&#39;s a 32x size reduction — the vectors in vectors.json would go from ~700 KB to ~4 KB. Models trained with &lt;a href=&quot;https://huggingface.co/blog/matryoshka&quot;&gt;Matryoshka representation learning&lt;/a&gt; (like &lt;a href=&quot;https://huggingface.co/mixedbread-ai/mxbai-embed-xsmall-v1&quot;&gt;mxbai-embed-xsmall-v1&lt;/a&gt;) let you also truncate dimensions — 384 down to 128 with ~85% quality retained. Combine both and each embedding becomes 16 bytes. For 88 documents this barely matters, but for thousands of chunks it would.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Eliminate the ONNX runtime with static embeddings.&lt;/strong&gt; &lt;a href=&quot;https://github.com/MinishLab/model2vec&quot;&gt;Model2Vec&lt;/a&gt; distills a transformer into a lookup table — tokenize, index into a matrix, average, normalize. No neural network inference, no WASM runtime. The browser download would drop from ~30 MB to ~4-15 MB and query-time inference would be instant. I tested the &lt;a href=&quot;https://huggingface.co/minishlab&quot;&gt;potion&lt;/a&gt; models (2M, 4M, and 8M parameters) against MiniLM-L6-v2 on my actual content, though, and the results were disappointing. For queries like &amp;quot;improving search on a static site,&amp;quot; MiniLM correctly finds the two search-related posts. All three potion models miss both entirely. On &amp;quot;design systems,&amp;quot; potion returns the 404 page, Privacy Policy, and Colophon — pages with short generic text that become false attractors after mean pooling. The Jaccard overlap between MiniLM&#39;s top results and potion-8M&#39;s was about 32% across my test queries. On &lt;a href=&quot;https://huggingface.co/blog/Pringled/model2vec&quot;&gt;MTEB benchmarks&lt;/a&gt;, potion-base-8M scores ~89% of MiniLM&#39;s average, but that gap matters more on a small corpus where the margin between a good result and garbage is thin. If static embedding models keep improving — and they are — this could change fast.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Merge keyword and semantic results more deeply.&lt;/strong&gt; Keyword and semantic search now live on the same page — semantic is an opt-in below keyword results. The next step would be to load the embedding model in a background Web Worker and merge the two result sets automatically when the model is ready. The user always sees keyword results immediately instead of watching a progress bar, and semantic results appear alongside when ready. &lt;a href=&quot;https://github.com/romanI04/sift&quot;&gt;Sift&lt;/a&gt; does exactly this — it stores FTS5 keyword indexes and vector embeddings in a single SQLite file, shows keyword results first, then upgrades to semantic. It also uses HTTP range requests to read only the index pages it needs, which would matter for larger sites.&lt;/p&gt;
&lt;p&gt;For now, it&#39;s two search modes on one page on a static site with no backend — overkill for a personal blog, but a useful sketch for how this could work on sites where search actually matters. &lt;a href=&quot;https://www.allaboutken.com/search/?search_type=semantic&quot;&gt;Try it&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/20260228-replacing-lunr-with-pagefind/&quot;&gt;Site search without a search service&lt;/a&gt; — the Pagefind keyword search this builds on&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.allaboutken.com/posts/20260225-introducing-pinment/&quot;&gt;Collaborate on any webpage with a bookmarklet&lt;/a&gt; — another &amp;quot;do it in the browser&amp;quot; project from the same week&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.allaboutken.com/posts/20250831-site-overhaul-eleventy-v3/&quot;&gt;A simpler, faster site: moving to pure Eleventy v3&lt;/a&gt; — the foundation this all runs on&lt;/li&gt;
&lt;/ul&gt;

</content>
  </entry>
  <entry>
    <title>Site search without a search service</title>
    <link href="https://www.allaboutken.com/posts/20260228-replacing-lunr-with-pagefind/"/>
    <published>2026-02-28T00:00:00.000Z</published>
    <updated>2026-02-28T00:00:00.000Z</updated>
    <id>https://www.allaboutken.com/posts/20260228-replacing-lunr-with-pagefind/</id>
    <author>
      <name>Ken Hawkins</name>
      <email>khawkins98@gmail.com</email>
    </author>
    <category term="eleventy" />
    <category term="search" />
    <category term="static sites" />
    <summary type="html">No API keys, no external service -- just a build step and one HTML attribute.</summary>
    <content type="html">
&lt;p&gt;This site has full-text search with no backend service. The index is generated at build time from the same HTML that Eleventy produces, and queries run in the browser. A small JS module fetches the relevant index fragments with plain HTTP GETs — no search API, no Cloudflare Worker.&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;Pagefind indexes my HTML and produces a chunked search index&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;data-pagefind-body&lt;/code&gt; attribute controls what gets indexed&lt;/li&gt;
&lt;li&gt;An &lt;code&gt;eleventy.after&lt;/code&gt; hook runs async so dev rebuilds aren&#39;t blocked&lt;/li&gt;
&lt;li&gt;Replaced my aging Lunr.js setup (unmaintained since 2020, 204 KB upfront download)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/khawkins98/allaboutken-11ty/pull/24&quot;&gt;Full diff on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;why-not-stick-with-lunrjs&quot; tabindex=&quot;-1&quot;&gt;Why not stick with Lunr.js?&lt;a href=&quot;#why-not-stick-with-lunrjs&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 previously used Lunr.js, which had the right idea: build-time indexing, client-side querying, no external service. Lunr was modeled after &lt;a href=&quot;https://solr.apache.org/&quot;&gt;Apache Solr&lt;/a&gt; (same inverted-index approach, TF-IDF ranking) just small enough to run in the browser. But Lunr hasn&#39;t had a release since 2020.&lt;/p&gt;
&lt;p&gt;Pagefind uses &lt;a href=&quot;https://en.wikipedia.org/wiki/Okapi_BM25&quot;&gt;BM25&lt;/a&gt; for ranking, the same algorithm behind Elasticsearch and modern Lucene. It&#39;s an evolution of TF-IDF that handles term frequency saturation and document length normalization better. The search runs as a Rust binary compiled to WASM, so queries execute in the browser without a server round-trip. The &lt;a href=&quot;https://github.com/khawkins98/allaboutken-11ty/pull/24&quot;&gt;migration PR&lt;/a&gt; has the full diff.&lt;/p&gt;
&lt;h2 id=&quot;why-i-switched-to-pagefind&quot; tabindex=&quot;-1&quot;&gt;Why I switched to Pagefind&lt;a href=&quot;#why-i-switched-to-pagefind&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Pagefind&#39;s integration comes down to one HTML attribute. Add &lt;code&gt;data-pagefind-body&lt;/code&gt; to your main content wrapper and Pagefind indexes only that element. Pages without it (social cards, redirect stubs, anything outside your base layout) are automatically excluded.&lt;/p&gt;
&lt;p&gt;For boilerplate within content pages (comments sections, previous/next navigation), &lt;code&gt;data-pagefind-ignore&lt;/code&gt; on that wrapper keeps it out of the index.&lt;/p&gt;
&lt;p&gt;The build integration is an &lt;code&gt;eleventy.after&lt;/code&gt; hook:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;let pagefindChild = null;
config.on(&#39;eleventy.after&#39;, () =&amp;gt; {
  const cmd = &#39;pagefind --site build --exclude-selectors &amp;quot;pre, code&amp;quot;&#39;;
  if (isDev) {
    if (pagefindChild) {
      pagefindChild.kill();
      pagefindChild = null;
    }
    pagefindChild = exec(cmd, (err) =&amp;gt; {
      pagefindChild = null;
      if (err &amp;amp;&amp;amp; !err.killed) console.error(&#39;Pagefind index build failed&#39;);
    });
  } else {
    execSync(cmd, { stdio: &#39;inherit&#39; });
  }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In production, &lt;code&gt;execSync&lt;/code&gt; so the build waits for the index. In dev, &lt;code&gt;exec&lt;/code&gt; (async) so rebuilds aren&#39;t blocked. Search catches up a few seconds after each save.&lt;/p&gt;
&lt;p&gt;The search page is about 80 lines of JavaScript. Here&#39;s the key part — Pagefind lazy-loads as an ES module the first time someone searches:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;async function init() {
  if (!pagefind) {
    pagefind = await import(&#39;/pagefind/pagefind.js&#39;);
    await pagefind.options({ excerptLength: 30 });
  }
  return pagefind;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The rest handles debounced input, escapes output with &lt;code&gt;textContent&lt;/code&gt; instead of &lt;code&gt;innerHTML&lt;/code&gt;, and reads the URL parameter so the 404 page can redirect to search. One thing I discovered post-launch: code examples were showing up in search excerpts as raw HTML. Adding &lt;code&gt;--exclude-selectors &amp;quot;pre, code&amp;quot;&lt;/code&gt; to the Pagefind command fixed that. You can also use &lt;code&gt;data-pagefind-ignore&lt;/code&gt; on individual elements, but the CLI flag is cleaner for a global rule.&lt;/p&gt;
&lt;h2 id=&quot;before-and-after&quot; tabindex=&quot;-1&quot;&gt;Before and after&lt;a href=&quot;#before-and-after&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Lunr.js&lt;/th&gt;
&lt;th&gt;Pagefind&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Initial download (gzip)&lt;/td&gt;
&lt;td&gt;204 KB&lt;/td&gt;
&lt;td&gt;~10 KB JS (+75 KB WASM on first search)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-query download&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;~10-30 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unmaintained dependencies&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The download difference is obvious. The tradeoff is per-query cost: Lunr loaded everything upfront and searched instantly, while Pagefind fetches index fragments on demand. For a site this size, the fragments are small enough that you&#39;d never notice.&lt;/p&gt;
&lt;h2 id=&quot;tips-if-youre-doing-this&quot; tabindex=&quot;-1&quot;&gt;Tips if you&#39;re doing this&lt;a href=&quot;#tips-if-youre-doing-this&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;
&lt;p&gt;&lt;strong&gt;Use &lt;code&gt;data-pagefind-body&lt;/code&gt; rather than listing exclusions.&lt;/strong&gt; Add it to your main content wrapper and everything else is excluded by default. Much cleaner than maintaining a list of things to ignore.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Exclude code blocks from indexing.&lt;/strong&gt; Run Pagefind with &lt;code&gt;--exclude-selectors &amp;quot;pre, code&amp;quot;&lt;/code&gt; unless you want raw HTML from code examples showing up in search excerpts.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Run Pagefind async in dev mode.&lt;/strong&gt; Use &lt;code&gt;exec&lt;/code&gt; (not &lt;code&gt;execSync&lt;/code&gt;) in your dev build hook so Eleventy rebuilds aren&#39;t blocked. Search lags a few seconds behind each rebuild, which is fine for development.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Kill previous Pagefind runs on rebuild.&lt;/strong&gt; In dev mode, overlapping builds can corrupt the index. Track the child process and kill it before spawning a new one.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Add a &lt;code&gt;&amp;lt;noscript&amp;gt;&lt;/code&gt; fallback.&lt;/strong&gt; A link to DuckDuckGo &lt;code&gt;site:yoursite.com&lt;/code&gt; costs nothing and handles the JS-disabled case.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;tradeoffs&quot; tabindex=&quot;-1&quot;&gt;Tradeoffs&lt;a href=&quot;#tradeoffs&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Pagefind isn&#39;t without cost. It loads a ~75 KB WASM module, not pure JS like Lunr was. You also get less control over the indexing pipeline; Lunr let you customize tokenization and stemming, while Pagefind&#39;s index is more of a black box.&lt;/p&gt;
&lt;p&gt;The chunked architecture means each search makes network requests to load index fragments on demand. Lunr was one-and-done after the initial load. For a site this size you&#39;d never notice, but the tradeoff is real.&lt;/p&gt;
&lt;p&gt;It&#39;s also another build tool, though it replaced a three-dependency Node script, so net complexity went down.&lt;/p&gt;
&lt;p&gt;The best search for a static site is still the idea Lunr had ten years ago: index at build time, query in the browser, skip the external service. Pagefind does the same thing with a chunked index and active maintenance. If your site search hasn&#39;t been touched in a while, the &lt;a href=&quot;https://github.com/khawkins98/allaboutken-11ty/pull/24&quot;&gt;migration PR&lt;/a&gt; might be a useful reference. I&#39;d be curious to hear if you&#39;ve run into anything different.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update, 2 March 2026:&lt;/strong&gt; Getting Pagefind working got me curious about what&#39;s possible beyond keyword matching. I&#39;ve since added &lt;a href=&quot;https://www.allaboutken.com/posts/20260302-semantic-search-browser-embeddings/&quot;&gt;semantic search&lt;/a&gt; — same philosophy (no API keys, runs in the browser), but it uses vector embeddings to match by meaning instead of exact words. &lt;a href=&quot;https://www.allaboutken.com/search/?search_type=semantic&quot;&gt;Try it out&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;related-posts&quot; tabindex=&quot;-1&quot;&gt;Related posts&lt;a href=&quot;#related-posts&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/20260302-semantic-search-browser-embeddings/&quot;&gt;Semantic search on a static site, no API keys required&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.allaboutken.com/posts/20250831-site-overhaul-eleventy-v3&quot;&gt;A simpler, faster site: moving to pure Eleventy v3&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</content>
  </entry>
  <entry>
    <title>Collaborate on any webpage with a bookmarklet and URL state</title>
    <link href="https://www.allaboutken.com/posts/20260225-introducing-pinment/"/>
    <published>2026-02-25T00:00:00.000Z</published>
    <updated>2026-02-25T00:00:00.000Z</updated>
    <id>https://www.allaboutken.com/posts/20260225-introducing-pinment/</id>
    <author>
      <name>Ken Hawkins</name>
      <email>khawkins98@gmail.com</email>
    </author>
    <category term="web development" />
    <category term="JavaScript" />
    <category term="open source" />
    <category term="URL design" />
    <summary type="html">Pin comments on any live webpage and share them as a URL with no account and zero cost. As in free.</summary>
    <content type="html">
&lt;p&gt;I regularly send webpages to colleagues for feedback — staging sites, published posts, someone else&#39;s work I want a second opinion on. The ask is always simple: look at this page and tell me what you&#39;d change.&lt;/p&gt;
&lt;p&gt;Tools for this are not easy and low-friction. Every platform I found wants accounts on both sides. I don&#39;t want to sign up for a SaaS tool, and I definitely don&#39;t want to make a colleague create an account just to tell me a heading is wrong. Screenshots go stale. Slack messages turn into &amp;quot;the third paragraph under the hero, on the left side, below the thing.&amp;quot;&lt;/p&gt;
&lt;p&gt;I wanted something with no friction: point at things on a live page, leave comments, hand someone a link. With AI-assisted coding, I put together &lt;a href=&quot;https://khawkins98.github.io/pinment/&quot;&gt;Pinment&lt;/a&gt; in a couple of hours — a bookmarklet that does exactly that. (The name is &amp;quot;pin&amp;quot; plus &amp;quot;comment.&amp;quot; A naming decision I already regret.)&lt;/p&gt;

&lt;p&gt;
  &lt;a href=&quot;https://khawkins98.github.io/pinment/&quot; class=&quot;kh-button kh-button&quot;&gt;Try Pinment&lt;/a&gt;
  &lt;a href=&quot;https://github.com/khawkins98/pinment&quot; class=&quot;kh-button kh-button--sm&quot;&gt;View on GitHub&lt;/a&gt;
&lt;/p&gt;

&lt;figure&gt;
  &lt;img src=&quot;https://www.allaboutken.com/images/blog/pinment-annotation-panel.jpg&quot; alt=&quot;Pinment bookmarklet active on a webpage, showing pin number 1 placed on the heading text with a blue dashed outline around the element, and the comment panel open on the right with filter dropdowns and Save, Resolve, Delete, Reply buttons.&quot;&gt;
  &lt;figcaption&gt;Pinment running on this site, with a pin placed on the heading and the comment panel open.&lt;/figcaption&gt;
&lt;/figure&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;Grab the &lt;a href=&quot;https://khawkins98.github.io/pinment/&quot;&gt;bookmarklet&lt;/a&gt;, navigate to any page — published, staging, localhost, behind auth — and click it. A panel appears.&lt;/p&gt;
&lt;p&gt;Click anywhere to drop a numbered pin. Add a comment. Drop more pins. Hit Share. Pinment compresses everything into the URL fragment.&lt;/p&gt;
&lt;p&gt;The recipient opens the link, clicks the bookmarklet, and sees the full annotated view reconstructed. No server, no login. The URL carries the entire review session.&lt;/p&gt;
&lt;h2 id=&quot;url-as-the-entire-state-layer&quot; tabindex=&quot;-1&quot;&gt;URL as the entire state layer&lt;a href=&quot;#url-as-the-entire-state-layer&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 extends the &lt;a href=&quot;https://www.allaboutken.com/posts/20251226-url-state-management/&quot;&gt;URL-as-state&lt;/a&gt; idea I&#39;ve mentioned before. With Pinment URL fragment holds the entire review session.&lt;/p&gt;
&lt;p&gt;A review with 15–20 annotations fits under 4 KB. The practical ceiling is around 50 pins before browsers complain about URL length.&lt;/p&gt;
&lt;h2 id=&quot;pins-that-follow-the-dom&quot; tabindex=&quot;-1&quot;&gt;Pins that follow the DOM&lt;a href=&quot;#pins-that-follow-the-dom&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Screenshots anchor to pixel coordinates. Resize the window and they&#39;re meaningless. Pinment anchors to the DOM instead. It generates CSS selectors for each pinned element, preferring stable hooks (IDs, &lt;code&gt;data-testid&lt;/code&gt;) over fragile positional chains. Pins reposition on resize.&lt;/p&gt;
&lt;p&gt;If the DOM changes and a selector can&#39;t resolve, Pinment falls back to the original coordinates with a warning badge.&lt;/p&gt;
&lt;h2 id=&quot;bookmarklet-not-extension&quot; tabindex=&quot;-1&quot;&gt;Bookmarklet, not extension&lt;a href=&quot;#bookmarklet-not-extension&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 browser extension requires installation, review queues, and separate builds per browser. A bookmarklet is a bookmark. Drag it to the bar, done.&lt;/p&gt;
&lt;p&gt;The bookmark itself is a tiny loader (~200 characters) that fetches the full script from the host. Updates ship without reinstalling anything.&lt;/p&gt;


&lt;details class=&quot;kh-details&quot;&gt;
&lt;summary&gt;&lt;h3&gt;What you can do with it&lt;/h3&gt;&lt;/summary&gt;
&lt;p&gt;Beyond placing pins and sharing URLs, Pinment handles a few things that come up in real review workflows:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Thread replies&lt;/strong&gt; — reply to any pin to start a back-and-forth&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Categories&lt;/strong&gt; — label pins as text, layout, missing content, or question&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Resolved status&lt;/strong&gt; — mark issues as resolved without deleting the pin&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Filter and sort&lt;/strong&gt; — filter by category, status, or author when the pin count grows&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JSON import/export&lt;/strong&gt; — save a session to a file and reload it later, useful for reviews that span multiple days or exceed URL length limits&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Element highlighting&lt;/strong&gt; — hover a pin or its panel entry and the anchored DOM element outlines on the page&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Drag to reposition&lt;/strong&gt; — move pins after placing them without starting over&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keyboard shortcuts&lt;/strong&gt; — Escape for browse mode, N for pin mode, arrows to navigate between pins&lt;/li&gt;
&lt;/ul&gt;

&lt;/details&gt;

&lt;details class=&quot;kh-details&quot;&gt;
&lt;summary&gt;&lt;h3&gt;Standing on shoulders&lt;/h3&gt;&lt;/summary&gt;
&lt;p&gt;I borrowed ideas from several tools:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://web.hypothes.is/&quot;&gt;Hypothesis&lt;/a&gt;&lt;/strong&gt; proved open web annotation is a viable concept, though it&#39;s built around text selection and a centralized backend&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://marker.io/&quot;&gt;Marker.io&lt;/a&gt;&lt;/strong&gt; and &lt;strong&gt;&lt;a href=&quot;https://usepastel.com/&quot;&gt;Pastel&lt;/a&gt;&lt;/strong&gt; showed the product need for visual webpage review, but both require accounts, cost money, and capture screenshots rather than annotating live pages&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://inkash.io/&quot;&gt;Inkash&lt;/a&gt;&lt;/strong&gt; demonstrated the bookmarklet annotation approach&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://buffertab.com/&quot;&gt;Buffertab&lt;/a&gt;&lt;/strong&gt; showed that lz-string compression can fit useful state into URL fragments without a backend&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ahmad El-Alfy&#39;s&lt;/strong&gt; &lt;a href=&quot;https://alfy.blog/2025/10/31/your-url-is-your-state.html&quot;&gt;writing on URL-as-state&lt;/a&gt; shaped how I think about what belongs in a URL&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;None of them did everything I wanted. Hypothesis selects text, not visual elements. &lt;a href=&quot;http://Marker.io&quot;&gt;Marker.io&lt;/a&gt; and Pastel capture screenshots, not live pages. Buffertab proved the URL compression trick but isn&#39;t built for annotation. So I took the parts that worked.&lt;/p&gt;

&lt;/details&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;p&gt;&lt;a href=&quot;https://github.com/khawkins98/pinment&quot;&gt;Pinment is on GitHub&lt;/a&gt; and the &lt;a href=&quot;https://khawkins98.github.io/pinment/&quot;&gt;bookmarklet hub is live&lt;/a&gt;. Drag the bookmarklet to your bar and try it on any page. If a selector breaks, a pin lands in the wrong spot, or you hit the URL length ceiling sooner than expected — &lt;a href=&quot;https://github.com/khawkins98/pinment/issues&quot;&gt;file an issue&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It&#39;s not as bulletproof as marking up a PDF, but it&#39;s lighter, faster to set up, and nobody has to create an account to collaborate.&lt;/p&gt;
&lt;p&gt;A browser extension would be more reliable on pages with strict Content Security Policy, and I may build one eventually — but then everyone has to install it. The bookmarklet keeps the barrier at zero.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://khawkins98.github.io/pinment/&quot;&gt;Try it out&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If nothing else, it&#39;s a chance to rediscover &lt;a href=&quot;https://en.wikipedia.org/wiki/Bookmarklet&quot;&gt;bookmarklets&lt;/a&gt;. They&#39;re a useful bit of web tech that&#39;s gone &lt;a href=&quot;https://trends.google.com/trends/explore?date=all&amp;amp;q=bookmarklet&quot;&gt;unloved since peaking around 2011&lt;/a&gt;.&lt;/p&gt;

&lt;figure&gt;
  &lt;img src=&quot;https://www.allaboutken.com/images/blog/bookmarklet-google-trends.jpg&quot; alt=&quot;Google Trends chart for the search term &#39;bookmarklet&#39; from 2004 to 2026, showing a sharp peak around 2010–2011 followed by a steady decline to roughly a quarter of peak interest.&quot;&gt;
  &lt;figcaption&gt;Google Trends for &quot;bookmarklet&quot; -- peaked around 2011 and has been sliding since. &lt;a href=&quot;https://trends.google.com/trends/explore?date=all&amp;q=bookmarklet&quot;&gt;Source&lt;/a&gt;.&lt;/figcaption&gt;
&lt;/figure&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/20251226-url-state-management/&quot;&gt;URLs are the state management you should use&lt;/a&gt; — the URL-as-state philosophy behind Pinment&#39;s sharing model&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.allaboutken.com/posts/20260220-feedback-buttons-cgi-pattern/&quot;&gt;Feedback buttons without JavaScript, using a 1990s web pattern&lt;/a&gt; — another project using URL fragments as state&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; — same build season, same &amp;quot;do it in the browser&amp;quot; ethos&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.allaboutken.com/posts/20260223-digesting-hidden-danger-shipping-fast/&quot;&gt;Two-way communication beats another shipped feature&lt;/a&gt; — the tension between shipping and landing&lt;/li&gt;
&lt;/ul&gt;

</content>
  </entry>
  <entry>
    <title>Feedback buttons without JavaScript, using a 1990s web pattern</title>
    <link href="https://www.allaboutken.com/posts/20260220-feedback-buttons-cgi-pattern/"/>
    <published>2026-02-20T00:00:00.000Z</published>
    <updated>2026-02-20T00:00:00.000Z</updated>
    <id>https://www.allaboutken.com/posts/20260220-feedback-buttons-cgi-pattern/</id>
    <author>
      <name>Ken Hawkins</name>
      <email>khawkins98@gmail.com</email>
    </author>
    <category term="web development" />
    <category term="static sites" />
    <category term="Cloudflare Workers" />
    <category term="retro web" />
    <summary type="html">A Cloudflare Worker is just a CGI script, and that&#39;s exactly what a static site needs.</summary>
    <content type="html">
&lt;p&gt;I wanted to add a &amp;quot;Was this useful?&amp;quot; button to my blog posts. The site runs on GitHub Pages. No backend. No database. No server-side anything. And I didn&#39;t want JavaScript in the interaction itself.&lt;/p&gt;
&lt;p&gt;The solution turned out to be three decades(!) old.&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;a href=&quot;#setting-this-up-on-your-own-site&quot;&gt;Skip to the setup tutorial&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;A Cloudflare Worker handles feedback clicks via HTTP redirect — no client-side JS needed&lt;/li&gt;
&lt;li&gt;The pattern is identical to how 1994 web rings counted click-throughs&lt;/li&gt;
&lt;li&gt;CSS &lt;code&gt;:target&lt;/code&gt; shows a &amp;quot;Thanks!&amp;quot; message after the redirect without page navigation&lt;/li&gt;
&lt;li&gt;An SVG endpoint returns just the number — the 1996 hit counter, reborn&lt;/li&gt;
&lt;li&gt;The Worker source is in the &lt;a href=&quot;https://github.com/khawkins98/allaboutken-11ty/tree/main/worker&quot;&gt;&lt;code&gt;worker/&lt;/code&gt; directory&lt;/a&gt; of this site&#39;s repo&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;constraints-as-a-design-tool&quot; tabindex=&quot;-1&quot;&gt;Constraints as a design tool&lt;a href=&quot;#constraints-as-a-design-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;This site already uses &lt;a href=&quot;https://www.allaboutken.com/posts/20250906-css-naked-css-only/&quot;&gt;a CSS-only toggle&lt;/a&gt; to disable all styles with a checkbox. The search form works without JavaScript. &lt;a href=&quot;https://www.allaboutken.com/posts/20251226-url-state-management/&quot;&gt;URLs carry state&lt;/a&gt; instead of client-side stores. The pattern is clear: HTML and CSS do the work; JavaScript is an enhancement, not a dependency.&lt;/p&gt;
&lt;p&gt;That&#39;s not accidental — it&#39;s why the site runs on &lt;a href=&quot;https://www.11ty.dev/&quot;&gt;Eleventy&lt;/a&gt;, a static site generator that ships zero client-side JavaScript by default. The framework&#39;s position is that HTML is the deliverable; everything else is opt-in.&lt;/p&gt;
&lt;p&gt;Feedback buttons should follow the same rule. Click a link, register the vote, done. No &lt;code&gt;fetch()&lt;/code&gt; calls, no framework, no hydration. If JavaScript fails to load, the feedback still works.&lt;/p&gt;
&lt;p&gt;The problem is obvious: the site is static HTML, so where do we put the brain?&lt;/p&gt;
&lt;h2 id=&quot;what-would-a-1998-webmaster-do&quot; tabindex=&quot;-1&quot;&gt;What would a 1998 webmaster do?&lt;a href=&quot;#what-would-a-1998-webmaster-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;In 1998, your personal page on GeoCities was static HTML — just like GitHub Pages today. You had no backend. But you wanted interactivity: hit counters, guestbooks, polls. So you used other people&#39;s &lt;a href=&quot;https://rickcarlino.com/2019/what-were-cgi-scripts.html&quot;&gt;CGI scripts&lt;/a&gt; — small programs that ran on a web server, handled one HTTP request, and returned a response. An &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; tag could point to a CGI script on someone else&#39;s server that generated a counter image. A &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt; action could submit to a remote script that appended to a flat file and redirected back.&lt;/p&gt;
&lt;p&gt;The web was always a composition of origins. Your static page was the frontend; the internet was the backend.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://tedium.co/2020/11/20/webring-history/&quot;&gt;Web rings&lt;/a&gt; used this exact architecture. The &amp;quot;Next site&amp;quot; button was a link to a central CGI script:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http://www.webring.org/cgi-bin/webring?ring=navships;id=002;next
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Click it, and the script would increment a counter, look up the next site, and return a &lt;code&gt;302 Found&lt;/code&gt; redirect. The user barely noticed the hop. This pattern worked in Mosaic. It still works in every browser today. It&#39;s pure HTTP.&lt;/p&gt;
&lt;p&gt;Replace &amp;quot;next site in the ring&amp;quot; with &amp;quot;the post you just read&amp;quot; and &amp;quot;click-through count&amp;quot; with &amp;quot;thumbs-up count.&amp;quot; The architecture is identical.&lt;/p&gt;
&lt;h2 id=&quot;the-redirect-count-redirect-pattern&quot; tabindex=&quot;-1&quot;&gt;The redirect-count-redirect pattern&lt;a href=&quot;#the-redirect-count-redirect-pattern&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Here&#39;s what happens when a reader clicks the thumbs-up button:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Reader clicks &amp;quot;👍 Yes&amp;quot; on a blog post
  → GET https://feedback.allaboutken.com/up/posts/my-post/
  → Worker increments counter in KV storage
  → Worker returns: 302 Location: https://www.allaboutken.com/posts/my-post/#thanks
  → Browser follows redirect back to the same post
  → CSS :target reveals &amp;quot;Thanks for the feedback!&amp;quot; message
  → Reader never truly left the page
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;#thanks&lt;/code&gt; fragment in the redirect URL is doing real work. It&#39;s &lt;a href=&quot;https://www.allaboutken.com/posts/20251226-url-state-management/&quot;&gt;URL-as-state&lt;/a&gt; again — the fragment triggers a &lt;code&gt;:target&lt;/code&gt; CSS rule that reveals a thank-you message that was always in the HTML, just hidden. No JavaScript needed to show it.&lt;/p&gt;
&lt;p&gt;The Worker itself is under 200 lines. It handles three route families:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;/up/posts/slug/&lt;/code&gt;&lt;/strong&gt; — increment a KV counter, 302 redirect back to the post&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;/count/posts/slug.svg&lt;/code&gt;&lt;/strong&gt; — return the count as an SVG number (&lt;code&gt;image/svg+xml&lt;/code&gt;, 2-second cache). Drop it in an &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; tag. Add &lt;code&gt;?color=white&lt;/code&gt; for light text on dark backgrounds; defaults to black. Not CSS-styleable from the page since it&#39;s a separate document.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;/count/posts/slug/&lt;/code&gt;&lt;/strong&gt; — return the count as plain text (&lt;code&gt;text/plain&lt;/code&gt;, 2-second cache). Use this for build-time fetches, a small JS enhancement that injects the number into a &lt;code&gt;&amp;lt;span&amp;gt;&lt;/code&gt; you can style, or scripting.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The SVG endpoint is the 1996 hit counter pattern. An &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; tag pointing to a script that returns a dynamically generated image:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;img src=&amp;quot;https://feedback.allaboutken.com/count/posts/my-post.svg&amp;quot; alt=&amp;quot;47 people found this useful&amp;quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The browser thinks it&#39;s loading a normal image. The Worker is secretly running code. This is exactly what &lt;code&gt;&amp;lt;img src=&amp;quot;/cgi-bin/counter.pl&amp;quot;&amp;gt;&lt;/code&gt; did in 1996, except the &amp;quot;CGI script&amp;quot; runs on a global edge network instead of a Pentium II under someone&#39;s desk.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Aside: The current implementation tracks positive feedback only — a single &amp;quot;Yes, this was useful&amp;quot; button. The Worker supports &lt;code&gt;/down/&lt;/code&gt; routes too, but the UI doesn&#39;t expose them yet. Binary up/down adds complexity (what does the count mean? net? total?) without much signal for a personal blog. A future version may add star ratings via ISMAP (see &lt;a href=&quot;#whats-next&quot;&gt;What&#39;s next&lt;/a&gt; at the end of this post).&lt;/em&gt;&lt;/p&gt;


&lt;h3 class=&quot;kh-text-heading--3&quot;&gt;Demo&lt;/h3&gt;
&lt;p&gt;A plain &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt; link. No JavaScript. The Worker handles the rest. &lt;code&gt;aria-live=&quot;polite&quot;&lt;/code&gt; on the thank-you message announces it to screen readers when it appears.&lt;/p&gt;
&lt;p class=&quot;kh-cluster&quot;&gt;
  &lt;a href=&quot;https://feedback.allaboutken.com/up/posts/20260220-feedback-buttons-cgi-pattern/&quot; rel=&quot;nofollow&quot; class=&quot;kh-button kh-button--sm&quot;&gt;👍 Yes, this was useful&lt;/a&gt;
&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;a href=&quot;https://feedback.allaboutken.com/up/posts/20260220-feedback-buttons-cgi-pattern/&quot; rel=&quot;nofollow&quot;&amp;gt;👍 Yes, this was useful&amp;lt;/a&amp;gt;
&amp;lt;span id=&quot;thanks&quot; class=&quot;kh-feedback-thanks&quot; aria-live=&quot;polite&quot;&amp;gt;Thanks for the feedback!&amp;lt;/span&amp;gt;&lt;/code&gt;&lt;/pre&gt;

&lt;h4&gt;SVG count (no JavaScript)&lt;/h4&gt;

&lt;p&gt;The SVG returns just the number. Set &lt;code&gt;height:1.5em&lt;/code&gt; and it sits inline with text at any font size. Add &lt;code&gt;?color=white&lt;/code&gt; for dark backgrounds.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://feedback.allaboutken.com/count/posts/20260220-feedback-buttons-cgi-pattern/.svg&quot; alt=&quot;Feedback count&quot;&gt; people found this useful &amp;nbsp; &lt;span&gt;&lt;img src=&quot;https://feedback.allaboutken.com/count/posts/20260220-feedback-buttons-cgi-pattern/.svg?color=white&quot; alt=&quot;Feedback count&quot;&gt;&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;Because the height is in &lt;code&gt;em&lt;/code&gt;, it scales with the surrounding text:&lt;/p&gt;

&lt;p&gt;Small: &lt;img src=&quot;https://feedback.allaboutken.com/count/posts/20260220-feedback-buttons-cgi-pattern/.svg&quot; alt=&quot;Feedback count&quot;&gt; people&lt;/p&gt;

&lt;p&gt;Large: &lt;img src=&quot;https://feedback.allaboutken.com/count/posts/20260220-feedback-buttons-cgi-pattern/.svg&quot; alt=&quot;Feedback count&quot;&gt; people&lt;/p&gt;

&lt;hr&gt;

&lt;h4&gt;Plain text count&lt;/h4&gt;
&lt;p&gt;Returns just the number. Use for build-time fetches, shell scripts, or data export.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl https://feedback.allaboutken.com/count/posts/20260220-feedback-buttons-cgi-pattern/
# → a number&lt;/code&gt;&lt;/pre&gt;

&lt;details class=&quot;kh-details&quot;&gt;
&lt;summary&gt;&lt;h3&gt;Styled with a JS enhancement&lt;/h3&gt;&lt;/summary&gt;
&lt;p&gt;If you&#39;re OK with a bit of JS, fetch the plain text count and inject it into a &lt;code&gt;&amp;lt;span&amp;gt;&lt;/code&gt; you control.&lt;/p&gt;
&lt;p&gt;&lt;span id=&quot;js-feedback-count&quot; class=&quot;kh-badge&quot;&gt;...&lt;/span&gt; people found this useful&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;span id=&quot;js-feedback-count&quot;&amp;gt;...&amp;lt;/span&amp;gt; people found this useful
&amp;lt;script&amp;gt;
fetch(&#39;https://feedback.allaboutken.com/count/posts/20260220-feedback-buttons-cgi-pattern/&#39;)
  .then(r =&amp;gt; r.text())
  .then(n =&amp;gt; {
    document.getElementById(&#39;js-feedback-count&#39;).textContent = n.trim();
  });
&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;

&lt;h2 id=&quot;musing-cloudflare-workers-are-the-new-cgi-script&quot; tabindex=&quot;-1&quot;&gt;Musing: Cloudflare Workers are the new CGI script&lt;a href=&quot;#musing-cloudflare-workers-are-the-new-cgi-script&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 idea here keeps showing up: old patterns, new infrastructure. A Cloudflare Worker runs on someone else&#39;s server, handles one HTTP request, reads/writes a small data store, and returns a response. The only differences from 1998:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It runs on a global edge network instead of a single machine&lt;/li&gt;
&lt;li&gt;The &amp;quot;flat file&amp;quot; is a KV store instead of &lt;code&gt;/var/www/data/counter.txt&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The free tier is 100,000 requests/day instead of &amp;quot;whatever the sysadmin allows&amp;quot;&lt;/li&gt;
&lt;li&gt;You deploy via &lt;code&gt;wrangler deploy&lt;/code&gt; instead of FTPing a Perl script to &lt;code&gt;/cgi-bin/&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The fundamental patterns haven&#39;t changed. The infrastructure got better. If you need interactivity on a static site, think like it&#39;s 1998 and point your links at someone else&#39;s server. That server just happens to be a globally distributed edge network now.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://github.com/khawkins98/allaboutken-11ty/tree/main/worker&quot;&gt;Worker source code&lt;/a&gt; is in this site&#39;s repo — under 200 lines of JavaScript, no dependencies beyond Wrangler. The pattern works for anything: polls, reactions, RSVP counters, hit counters. Sage Weil built it in 1994. I&#39;m just running it on better hardware.&lt;/p&gt;
&lt;h2 id=&quot;setting-this-up-on-your-own-site&quot; tabindex=&quot;-1&quot;&gt;Setting this up on your own site&lt;a href=&quot;#setting-this-up-on-your-own-site&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;You&#39;ll need to deploy your own Worker — you can&#39;t point your feedback links at mine. The Worker hardcodes a redirect back to a single site origin, so it&#39;s one Worker per site by design.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Required:&lt;/strong&gt; a free &lt;a href=&quot;https://dash.cloudflare.com/sign-up&quot;&gt;Cloudflare account&lt;/a&gt;.
&lt;strong&gt;Suggested&lt;/strong&gt;: a domain on Cloudflare DNS is needed for custom domains; without it, you&#39;ll use the default &lt;code&gt;workers.dev&lt;/code&gt; URL.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Copy and edit the Worker.&lt;/strong&gt; Grab my &lt;a href=&quot;https://github.com/khawkins98/allaboutken-11ty/tree/main/worker&quot;&gt;&lt;code&gt;worker/&lt;/code&gt;&lt;/a&gt; directory (three files). The comments in &lt;code&gt;wrangler.toml&lt;/code&gt; list everything you need to change, but in short:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;SITE_ORIGIN&lt;/code&gt; in &lt;code&gt;src/index.js&lt;/code&gt; — your site&#39;s URL&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pattern&lt;/code&gt; in &lt;code&gt;wrangler.toml&lt;/code&gt; — your subdomain (e.g. &lt;code&gt;feedback.yourdomain.com&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;id&lt;/code&gt; under &lt;code&gt;[[kv_namespaces]]&lt;/code&gt; in &lt;code&gt;wrangler.toml&lt;/code&gt; — replace with your own namespace ID (step 3)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If your domain isn&#39;t on Cloudflare DNS, remove the &lt;code&gt;[[routes]]&lt;/code&gt; block entirely. Your Worker will be available at &lt;code&gt;feedback-worker.&amp;lt;your-account&amp;gt;.workers.dev&lt;/code&gt; instead.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Install and authenticate.&lt;/strong&gt; &lt;a href=&quot;https://developers.cloudflare.com/workers/wrangler/&quot;&gt;Wrangler&lt;/a&gt; is Cloudflare&#39;s CLI for deploying and managing Workers. &lt;code&gt;npm install&lt;/code&gt; pulls it in as the only dependency.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd worker
npm install
npx wrangler login
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;wrangler login&lt;/code&gt; opens a browser window for Cloudflare OAuth.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Create a KV namespace.&lt;/strong&gt; This is the data store for vote counts.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx wrangler kv namespace create FEEDBACK_COUNTS
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Wrangler prints a namespace ID. Paste it into &lt;code&gt;wrangler.toml&lt;/code&gt; as the &lt;code&gt;id&lt;/code&gt; value, replacing the one from the repo.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. Deploy.&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx wrangler deploy
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you have a &lt;code&gt;[[routes]]&lt;/code&gt; block with a custom domain, the deploy provisions the DNS record automatically. You should see your Worker appear in the Cloudflare dashboard under &lt;strong&gt;Workers &amp;amp; Pages&lt;/strong&gt;. If the custom domain doesn&#39;t provision on deploy, add it manually: &lt;strong&gt;Workers &amp;amp; Pages&lt;/strong&gt; → your worker → &lt;strong&gt;Settings&lt;/strong&gt; → &lt;strong&gt;Domains &amp;amp; Routes&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5. Test it.&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -v https://feedback.yourdomain.com/up/posts/test/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You should get a &lt;code&gt;302&lt;/code&gt; redirect back to your site. If you&#39;re using the default &lt;code&gt;workers.dev&lt;/code&gt; URL, substitute that.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;If you get a &amp;quot;could not resolve host&amp;quot; error on a custom domain, DNS may still be propagating — flush your local cache (&lt;code&gt;sudo dscacheutil -flushcache &amp;amp;&amp;amp; sudo killall -HUP mDNSResponder&lt;/code&gt; on macOS) and try again.&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;6. Add the HTML to your templates.&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;a href=&amp;quot;https://feedback.yourdomain.com/up/posts/my-post/&amp;quot;
   rel=&amp;quot;nofollow&amp;quot;&amp;gt;👍 Yes, this was useful&amp;lt;/a&amp;gt;
&amp;lt;p id=&amp;quot;thanks&amp;quot; style=&amp;quot;display:none&amp;quot;&amp;gt;Thanks for the feedback!&amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And the CSS:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;#thanks:target { display: block; }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;rel=&amp;quot;nofollow&amp;quot;&lt;/code&gt; keeps crawlers from following the links. The Worker handles bot filtering, rate limiting, and the redirect.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;7. CI/CD&lt;/strong&gt; (optional). The Worker is unlikely to change often — &lt;code&gt;npx wrangler deploy&lt;/code&gt; from your machine is fine for occasional updates. If you do want automated deploys, add a GitHub Actions workflow that runs &lt;code&gt;wrangler deploy&lt;/code&gt; when &lt;code&gt;worker/**&lt;/code&gt; changes. Two repository secrets needed:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;CLOUDFLARE_API_TOKEN&lt;/code&gt; — create via Cloudflare dashboard → API Tokens → &amp;quot;Edit Cloudflare Workers&amp;quot; template&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CLOUDFLARE_ACCOUNT_ID&lt;/code&gt; — visible on your Cloudflare dashboard overview&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Free tier limits:&lt;/strong&gt; 100,000 Worker requests/day, 1,000 KV writes/day. The writes are the constraint — 1,000 feedback clicks per day, more than enough for most sites.&lt;/p&gt;
&lt;h2 id=&quot;whats-next&quot; tabindex=&quot;-1&quot;&gt;What&#39;s next&lt;a href=&quot;#whats-next&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;Star ratings via ISMAP.&lt;/strong&gt; The current implementation is positive-only — a single &amp;quot;Yes, this was useful&amp;quot; button. Star ratings are the natural next step, and the approach is too good not to mention: HTML&#39;s &lt;a href=&quot;https://rickcarlino.com/2021/what-were-server-side-image-maps.html&quot;&gt;server-side image maps&lt;/a&gt;. Wrap an &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; in an &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt; with the &lt;code&gt;ismap&lt;/code&gt; attribute, and clicking the image appends &lt;code&gt;?x,y&lt;/code&gt; coordinates to the URL. The Worker calculates which star from the X coordinate. Server-side image maps were first supported in NCSA Mosaic 1.1 (1993). They still work in every browser.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;a href=&amp;quot;https://feedback.allaboutken.com/rate/posts/my-post/&amp;quot;&amp;gt;
  &amp;lt;img src=&amp;quot;/images/five-stars-empty.svg&amp;quot; ismap
       alt=&amp;quot;Rate this post from 1 to 5 stars&amp;quot;&amp;gt;
&amp;lt;/a&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;A popularity index from my own data.&lt;/strong&gt; Because the Worker and KV namespace are mine, I can crawl every counter at build time and rank posts by feedback count. An Eleventy data file that calls &lt;code&gt;wrangler kv key list&lt;/code&gt; or hits the &lt;code&gt;/count/&lt;/code&gt; endpoints, sorts the results, and generates a &amp;quot;most useful posts&amp;quot; page — no third-party analytics dependency, no JavaScript on the client. The data is already there; it just needs a build step.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A community web ring for Eleventy sites.&lt;/strong&gt; The architecture generalizes. If multiple Eleventy sites each ran their own feedback Worker, a shared index could aggregate the counts — &amp;quot;top rated Eleventy posts this month&amp;quot; in the spirit of a classic web ring. Each site keeps its own data, the index just reads the public &lt;code&gt;/count/&lt;/code&gt; endpoints. The pattern is cooperative, not centralized: no shared database, no sign-up, just HTTP.&lt;/p&gt;
&lt;h2 id=&quot;behind-the-decisions&quot; tabindex=&quot;-1&quot;&gt;Behind the decisions&lt;a href=&quot;#behind-the-decisions&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;

&lt;details class=&quot;kh-details&quot;&gt;
&lt;summary&gt;&lt;h3&gt;Privacy by design&lt;/h3&gt;&lt;/summary&gt;
&lt;p&gt;No consent banner needed. The Worker increments a counter in KV storage — no IP addresses stored, no User-Agent logged, no cookies set. &lt;a href=&quot;https://www.goatcounter.com/help/gdpr&quot;&gt;GoatCounter&#39;s GDPR analysis&lt;/a&gt; applies here with even more confidence: if aggregate-only page view counting falls under legitimate interest, then an anonymous integer counter certainly does.&lt;/p&gt;
&lt;p&gt;Rate-limiting uses a SHA-256 hash of the IP address, stored as a KV key with a 24-hour TTL. The hash is not reversible and expires automatically. No persistent identifier is created.&lt;/p&gt;

&lt;/details&gt;
&lt;details class=&quot;kh-details&quot;&gt;
&lt;summary&gt;&lt;h3&gt;Keeping bots honest&lt;/h3&gt;&lt;/summary&gt;
&lt;p&gt;The obvious concern: what stops a crawler from wandering onto &lt;code&gt;feedback.allaboutken.com/up/posts/my-post/&lt;/code&gt; and registering a false vote?&lt;/p&gt;
&lt;p&gt;Five layers of defense, from prevention to cleanup:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Don&#39;t advertise the paths.&lt;/strong&gt; Feedback links carry &lt;code&gt;rel=&amp;quot;nofollow&amp;quot;&lt;/code&gt;, so search engines won&#39;t follow them from the post to the Worker.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Return nothing indexable.&lt;/strong&gt; Vote URLs return 302 redirects — no HTML, no content for a crawler to store. Count endpoints return raw SVG or plain text, not pages.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Server-side filtering.&lt;/strong&gt; The Worker checks &lt;code&gt;User-Agent&lt;/code&gt; against known bot signatures and validates request headers. Simple crawlers don&#39;t send &lt;code&gt;Referer&lt;/code&gt; or &lt;code&gt;Accept-Language&lt;/code&gt; headers that real browsers do.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rate limiting.&lt;/strong&gt; One vote per IP per post per 24 hours, enforced by the Worker via KV TTL keys. A SHA-256 hash of the IP and path is stored with a 24-hour expiration — if it exists, the redirect still works but the counter doesn&#39;t increment.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data cleanup.&lt;/strong&gt; KV counters can be reset with &lt;code&gt;wrangler kv key delete&lt;/code&gt;. If a bot swarm hits, zero the affected counters and move on.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;An earlier design used GoatCounter page views as the counting mechanism, with a &lt;code&gt;&amp;lt;noscript&amp;gt;&lt;/code&gt; tracking pixel as a JS-disabled fallback. That pixel is actually a bot magnet — simple crawlers load images but skip JavaScript, so the pixel counts bots while the JS-based counter filters them out. The Worker approach sidesteps this entirely: counting happens server-side, before any HTML is returned.&lt;/p&gt;

&lt;/details&gt;
&lt;details class=&quot;kh-details&quot;&gt;
&lt;summary&gt;&lt;h3&gt;Standing on shoulders&lt;/h3&gt;&lt;/summary&gt;
&lt;p&gt;I&#39;m not the first person to think of this. Two projects proved different halves of the solution:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Andrew Walpole&lt;/strong&gt; &lt;a href=&quot;https://andrewwalpole.com/blog/building-a-like-button-with-cloudflare-workers/&quot;&gt;built a like button&lt;/a&gt; for his Eleventy blog using Cloudflare Workers and KV storage. He deployed it, ran it in production, and wrote about the experience. It proved the Eleventy + Workers + KV infrastructure works. But his approach uses client-side JavaScript — petite-vue renders the button, and &lt;code&gt;fetch()&lt;/code&gt; API calls communicate with the Worker.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/vberlier/poll&quot;&gt;poll.fizzy.wtf&lt;/a&gt;&lt;/strong&gt; is a Cloudflare Worker (written in Rust, MIT-licensed) that implements the redirect-count-redirect pattern with SVG result widgets. It&#39;s the 1994 web ring architecture applied to voting. No client-side JavaScript. Here&#39;s what using it looks like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;a href=&amp;quot;https://poll.fizzy.wtf/vote?allaboutken-feedback-post.standing-on-shoulders=yes&amp;amp;redirect=https://www.allaboutken.com/posts/20260220-feedback-buttons-cgi-pattern/%23standing-on-shoulders&amp;quot; rel=&amp;quot;nofollow&amp;quot; class=&amp;quot;kh-button kh-button--sm&amp;quot;&amp;gt;👍 Yes &amp;lt;img src=&amp;quot;https://poll.fizzy.wtf/count?allaboutken-feedback-post.standing-on-shoulders=yes&amp;quot; alt=&amp;quot;Yes votes&amp;quot; style=&amp;quot;display:inline;vertical-align:middle&amp;quot;&amp;gt;&amp;lt;/a&amp;gt;
&amp;lt;a href=&amp;quot;https://poll.fizzy.wtf/vote?allaboutken-feedback-post.standing-on-shoulders=no&amp;amp;redirect=https://www.allaboutken.com/posts/20260220-feedback-buttons-cgi-pattern/%23standing-on-shoulders&amp;quot; rel=&amp;quot;nofollow&amp;quot; class=&amp;quot;kh-button kh-button--sm&amp;quot;&amp;gt;👎 Not really &amp;lt;img src=&amp;quot;https://poll.fizzy.wtf/count?allaboutken-feedback-post.standing-on-shoulders=no&amp;quot; alt=&amp;quot;No votes&amp;quot; style=&amp;quot;display:inline;vertical-align:middle&amp;quot;&amp;gt;&amp;lt;/a&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Try poll.fizzy.wtf live — was this section of the post clear?&lt;/p&gt;

&lt;p&gt;
  &lt;a href=&quot;https://poll.fizzy.wtf/vote?allaboutken-feedback-post.standing-on-shoulders=yes&amp;redirect=https://www.allaboutken.com/posts/20260220-feedback-buttons-cgi-pattern/%23standing-on-shoulders&quot; rel=&quot;nofollow&quot; class=&quot;kh-button kh-button--sm&quot;&gt;👍 Yes &lt;img src=&quot;https://poll.fizzy.wtf/count?allaboutken-feedback-post.standing-on-shoulders=yes&quot; alt=&quot;Yes votes&quot;&gt;&lt;/a&gt;
  &lt;a href=&quot;https://poll.fizzy.wtf/vote?allaboutken-feedback-post.standing-on-shoulders=no&amp;redirect=https://www.allaboutken.com/posts/20260220-feedback-buttons-cgi-pattern/%23standing-on-shoulders&quot; rel=&quot;nofollow&quot; class=&quot;kh-button kh-button--sm&quot;&gt;👎 Not really &lt;img src=&quot;https://poll.fizzy.wtf/count?allaboutken-feedback-post.standing-on-shoulders=no&quot; alt=&quot;No votes&quot;&gt;&lt;/a&gt;
&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why not just use poll.fizzy.wtf directly?&lt;/strong&gt; You could — and if you want the quickest path to feedback buttons, you probably should. It&#39;s MIT-licensed and ready to deploy. I built my own because I wanted a few things it doesn&#39;t do:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No cookies.&lt;/strong&gt; poll.fizzy.wtf deduplicates votes with cookies. My Worker uses a hashed IP with a 24-hour TTL — no cookies set, ever.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bot filtering.&lt;/strong&gt; poll.fizzy.wtf trusts all requests. My Worker checks User-Agent, &lt;code&gt;Accept-Language&lt;/code&gt;, and &lt;code&gt;Referer&lt;/code&gt; headers before counting.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Single-tenant.&lt;/strong&gt; My Worker hardcodes one site origin and rejects requests referred from anywhere else. poll.fizzy.wtf is designed as a shared service.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data ownership.&lt;/strong&gt; My KV namespace, my data. I can list all keys with &lt;code&gt;wrangler kv key list&lt;/code&gt;, export counts to CSV, or wipe individual counters. With poll.fizzy.wtf the data lives on someone else&#39;s KV store.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;On the flip side, poll.fizzy.wtf does something mine doesn&#39;t: &lt;strong&gt;arbitrary key-value voting.&lt;/strong&gt; Its &lt;code&gt;?key=value&lt;/code&gt; query parameter approach lets you create any poll on the fly — multiple questions, multiple answers, no code changes. My Worker only counts one signal per post (positive feedback). That&#39;s a deliberate scope constraint for now, but if you need flexible multi-question polls, poll.fizzy.wtf has the better architecture for it.&lt;/p&gt;
&lt;p&gt;If those tradeoffs don&#39;t matter to you, poll.fizzy.wtf is a perfectly good choice and saves you the setup.&lt;/p&gt;

&lt;/details&gt;
&lt;details class=&quot;kh-details&quot;&gt;
&lt;summary&gt;&lt;h3&gt;Approaches I considered but didn&#39;t choose&lt;/h3&gt;&lt;/summary&gt;
&lt;p&gt;The Worker wasn&#39;t the first idea. The design process explored five approaches. The ones I passed on are interesting because they reveal the design space.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GoatCounter page-view counting.&lt;/strong&gt; The most obvious approach: feedback links go to static thank-you pages generated by Eleventy, and GoatCounter (already on the site) counts the page view. The URL path encodes the vote — &lt;code&gt;/feedback/up/posts/my-post/&lt;/code&gt; is a thumbs-up. Zero new dependencies. But it navigates the reader away from the post, the data mixes with analytics noise, and there&#39;s no way to show counts without a build-time API call. The Worker wins on UX, data quality, and bot resistance.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CSS &lt;code&gt;:target&lt;/code&gt; with a tracking pixel.&lt;/strong&gt; The most technically creative approach. Anchor links trigger &lt;code&gt;:target&lt;/code&gt;, which reveals a hidden &lt;code&gt;&amp;lt;span&amp;gt;&lt;/code&gt; whose &lt;code&gt;background-image&lt;/code&gt; points to a GoatCounter counting pixel. The browser loads the image when the element becomes visible — CSS as an analytics transport. The problem: browsers may skip &lt;code&gt;background-image&lt;/code&gt; loads on previously-hidden elements, especially with &lt;code&gt;display: none&lt;/code&gt; to &lt;code&gt;display: block&lt;/code&gt; transitions. It&#39;s fragile to test, unreliable across browsers, and raises accessibility concerns since screen readers may not announce the state change. Fascinating, but too brittle for production.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Mastodon retoots and favourites.&lt;/strong&gt; The Eleventy community has a well-established pattern here: publish a post, toot the link, then pull Mastodon interactions back into the site via the Mastodon API or Webmentions with &lt;a href=&quot;https://brid.gy/&quot;&gt;Bridgy&lt;/a&gt;. Boosts and favourites become your feedback signal; replies become comments. &lt;a href=&quot;https://mxb.dev/blog/using-webmentions-on-static-sites/&quot;&gt;Max Böck&lt;/a&gt;, &lt;a href=&quot;https://sia.codes/posts/webmentions-eleventy-in-depth/&quot;&gt;Sia Karamalegos&lt;/a&gt;, and others have written detailed implementations. It&#39;s a real solution and I may add it later as a complementary signal. But it&#39;s not feedback on the post — it&#39;s feedback on the toot. Readers who don&#39;t use Mastodon can&#39;t participate, and the signal measures social reach more than content usefulness. The Worker captures a direct &amp;quot;was this useful?&amp;quot; from any reader, regardless of what social networks they use.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;External form services.&lt;/strong&gt; Formspree, Basin, Google Forms — a &lt;code&gt;&amp;lt;form method=&amp;quot;POST&amp;quot;&amp;gt;&lt;/code&gt; pointing at a third-party service. True form submission, data stored somewhere you control (sort of). But it&#39;s a new SaaS dependency, the free tiers have submission limits, it redirects to a third-party confirmation page, and it runs against this site&#39;s self-hosted philosophy. Cloudflare Workers is technically also &amp;quot;someone else&#39;s server,&amp;quot; but the code is ours, the data is ours, and the free tier is generous enough to be effectively permanent for a personal blog.&lt;/p&gt;

&lt;/details&gt;
&lt;p&gt;The feedback buttons are live on this post — scroll down and try them. If you set this up on your own site, I&#39;d genuinely like to hear how it goes.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Further reading on the web patterns mentioned here:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://rickcarlino.com/2019/what-were-cgi-scripts.html&quot;&gt;What Were CGI Scripts?&lt;/a&gt; — Rick Carlino&#39;s primer on how CGI worked and why it mattered&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://tedium.co/2020/11/20/webring-history/&quot;&gt;Webring History: Social Media Before Social Media&lt;/a&gt; — Tedium on the rise and fall of web rings&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://rickcarlino.com/2021/what-were-server-side-image-maps.html&quot;&gt;What Were Server-Side Image Maps?&lt;/a&gt; — how ISMAP worked in the Mosaic era&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cameronsworld.net/&quot;&gt;Cameron&#39;s World&lt;/a&gt; — a collage of GeoCities pages, the ecosystem where these patterns thrived&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc3875&quot;&gt;RFC 3875: The Common Gateway Interface (CGI) v1.1&lt;/a&gt; — the spec behind everything on this page&lt;/li&gt;
&lt;/ul&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/20250906-css-naked-css-only/&quot;&gt;Exposing HTML: The No-JS nudist CSS toggle&lt;/a&gt; — same &amp;quot;no JavaScript required&amp;quot; philosophy&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.allaboutken.com/posts/20251226-url-state-management/&quot;&gt;URLs are the state management you should use&lt;/a&gt; — the &lt;code&gt;#thanks&lt;/code&gt; fragment is URL-as-state&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; — old UI patterns, new infrastructure&lt;/li&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; — why &amp;quot;Was this useful?&amp;quot; matters more than page views&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; — privacy-respecting signals over surveillance&lt;/li&gt;
&lt;/ul&gt;

</content>
  </entry>
  <entry>
    <title>PDF-A-go-slim: a browser-based PDF optimizer</title>
    <link href="https://www.allaboutken.com/posts/20260216-introducing-pdf-a-go-slim/"/>
    <published>2026-02-16T00:00:00.000Z</published>
    <updated>2026-02-16T00:00:00.000Z</updated>
    <id>https://www.allaboutken.com/posts/20260216-introducing-pdf-a-go-slim/</id>
    <author>
      <name>Ken Hawkins</name>
      <email>khawkins98@gmail.com</email>
    </author>
    <category term="web development" />
    <category term="pdf" />
    <category term="JavaScript" />
    <category term="open source" />
    <summary type="html">Eight optimization passes, zero uploads — reduce PDF file size entirely in your browser.</summary>
    <content type="html">
&lt;p&gt;PDF bloat is everywhere. A clean 32 KB PDF was edited in Adobe Illustrator for a minor layout tweak. After saving it was 198 KB — a 6x increase for a cosmetic change.&lt;/p&gt;
&lt;p&gt;A simple deletion of an element resulted in the new embedding of full TrueType copies of Helvetica and Courier, layers of application metadata, and duplicated font references in multiple formats.&lt;/p&gt;
&lt;p&gt;Sure, editing in a different way might not have done this — but how to ensure PDFs were adequately de-bloated?&lt;/p&gt;
&lt;p&gt;So I built &lt;a href=&quot;https://github.com/khawkins98/PDF-A-go-slim&quot;&gt;PDF-A-go-slim&lt;/a&gt; — an open-source optimizer that runs entirely in the browser, where files never leave your machine.&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;A clean 32 KB PDF ballooned to 198 KB after one Illustrator edit&lt;/li&gt;
&lt;li&gt;Tried Ghostscript, qpdf, iLovePDF, Smallpdf — none covered everything in-browser&lt;/li&gt;
&lt;li&gt;8 optimization passes, three presets (Lossless, Web, Print)&lt;/li&gt;
&lt;li&gt;Auto-detects PDF/A, PDF/UA, and tagged PDFs; preserves conformance&lt;/li&gt;
&lt;li&gt;All processing in a Web Worker — files stay private&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;why-this-exists&quot; tabindex=&quot;-1&quot;&gt;Why this exists&lt;a href=&quot;#why-this-exists&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 project grew out of &lt;a href=&quot;https://www.allaboutken.com/posts/20250811-pdf-a-go-go/&quot;&gt;PDF-A-go-go&lt;/a&gt;, an embeddable PDF viewer for the web. While building a small showcase PDF for its demo page, I hit a problem that turns out to be universal: creative tools silently bloat PDFs.&lt;/p&gt;
&lt;p&gt;All desktop PDF editors and makers seem to embed unnecessary fonts, duplicate objects, and inject application-private metadata. A simple &amp;quot;Save As PDF&amp;quot; routinely doubles or triples the file size.&lt;/p&gt;
&lt;p&gt;This makes them more versatile, but less ideal when you want to optimize.&lt;/p&gt;
&lt;p&gt;I tried the obvious tools:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Ghostscript&lt;/strong&gt; got it to 96 KB but couldn&#39;t strip the redundant standard font embeddings&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;qpdf&lt;/strong&gt; barely moved the needle (194 KB) — it optimizes structure but doesn&#39;t touch content&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;iLovePDF&lt;/strong&gt; got it to 62 KB — decent, but requires uploading to a third-party server&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Smallpdf&lt;/strong&gt; — paywalled before you can even try&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;what-my-tool-does&quot; tabindex=&quot;-1&quot;&gt;What my tool does&lt;a href=&quot;#what-my-tool-does&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;PDF-A-go-slim runs eight optimization passes in sequence:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Stream recompression&lt;/strong&gt; — decompress and recompress all streams with optimal Flate settings&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Image recompression&lt;/strong&gt; — re-encode raster images at user-chosen quality (lossy, opt-in)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Standard font unembedding&lt;/strong&gt; — remove embedded copies of the 14 base PDF fonts that every reader already includes&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Font subsetting&lt;/strong&gt; — subset embedded fonts to only the glyphs actually used in the document&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Object deduplication&lt;/strong&gt; — hash-based deduplication of identical streams&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Font deduplication&lt;/strong&gt; — consolidate duplicate embedded fonts&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Metadata stripping&lt;/strong&gt; — remove XMP, Illustrator, and Photoshop application-private bloat&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Unreferenced object removal&lt;/strong&gt; — delete objects not reachable from the document catalog&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Three presets control how aggressive the optimization gets:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Lossless&lt;/strong&gt; (default) — visual output is identical to the original&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Web&lt;/strong&gt; — lossy, 75% JPEG quality, 150 DPI max — optimized for screen viewing&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Print&lt;/strong&gt; — lossy, 92% JPEG quality, 300 DPI max — high quality for physical output&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;An object inspector shows a before/after breakdown of every PDF object by category, so you can see exactly what changed and why.&lt;/p&gt;

&lt;figure&gt;
  &lt;img src=&quot;https://www.allaboutken.com/images/blog/pdf-a-go-slim-savings.jpg&quot; alt=&quot;Inspector and Results palettes showing a 78.5% file size reduction from 7.9 MB to 1.7 MB, with category-level breakdown of savings across fonts, images, page content, metadata, and other data.&quot;&gt;
  &lt;figcaption&gt;The object inspector breaks down savings by category -- this 7.9 MB presentation dropped to 1.7 MB after the eight optimization passes.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id=&quot;privacy-and-accessibility&quot; tabindex=&quot;-1&quot;&gt;Privacy and accessibility&lt;a href=&quot;#privacy-and-accessibility&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Files never leave the browser. All processing runs in a Web Worker off the main thread. No accounts, no uploads, no file size limits beyond available RAM.&lt;/p&gt;
&lt;p&gt;Fast, reliable and convenient — everything that makes a tool useful.&lt;/p&gt;
&lt;p&gt;It auto-detects PDF/A conformance, PDF/UA, and tagged PDFs. When it finds them, it disables font unembedding and XMP stripping to preserve what conformance requires. Structure trees, ToUnicode CMaps, and language tags are carried through. 57 tests verify compression and accessibility preservation across a range of test fixtures.&lt;/p&gt;
&lt;p&gt;A dedicated Accessibility palette shows what the optimizer finds: a pass/fail checklist for tagged structure, document title, display title (different than the former), language declaration, and conformance standards. The document title, display title, and marked-status checks were inspired by &lt;a href=&quot;https://github.com/jsnmrs/pdfcheck&quot;&gt;PDFcheck&lt;/a&gt; by Jason Morris. Three lightweight audits check ToUnicode coverage (can screen readers extract text?), image alt text in the structure tree, and structure tree depth. The palette also links to external validators — veraPDF, PAC, PDFcheck — for deeper conformance testing.&lt;/p&gt;

&lt;figure&gt;
  &lt;img src=&quot;https://www.allaboutken.com/images/blog/pdf-a-go-slim-accessibility.jpg&quot; alt=&quot;Accessibility palette showing trait checklist with red X marks for Tagged PDF, Structure Tree, and Document Language, neutral dashes for PDF/A and PDF/UA, and a ToUnicode coverage audit showing 3 of 11 fonts mapped.&quot;&gt;
  &lt;figcaption&gt;The Accessibility palette flags missing traits and runs lightweight audits -- this PDF has no tagged structure, no language declaration, and poor ToUnicode coverage.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;I haven&#39;t fully vetted every edge case, though. If you&#39;re working with accessibility-critical documents, test the output. &lt;a href=&quot;https://github.com/khawkins98/PDF-A-go-slim/issues&quot;&gt;Bug reports&lt;/a&gt; in this area are especially welcome.&lt;/p&gt;
&lt;h2 id=&quot;how-its-built&quot; tabindex=&quot;-1&quot;&gt;How it&#39;s built&lt;a href=&quot;#how-its-built&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 core engine uses four open-source libraries:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Hopding/pdf-lib&quot;&gt;pdf-lib&lt;/a&gt; — low-level PDF object access (MIT)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/101arrowz/fflate&quot;&gt;fflate&lt;/a&gt; — pure-JS zlib compression (MIT)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/jpeg-js/jpeg-js&quot;&gt;jpeg-js&lt;/a&gt; — pure-JS JPEG encoder (BSD 3-Clause)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/harfbuzz/harfbuzzjs&quot;&gt;harfbuzzjs&lt;/a&gt; — WASM font subsetting, lazy-loaded (MIT / Apache 2.0)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The optimization engine runs in a Web Worker so the UI stays responsive during processing. The WASM font subsetter loads as a separate chunk only when font subsetting is needed.&lt;/p&gt;
&lt;h2 id=&quot;why-it-looks-like-mac-os-8&quot; tabindex=&quot;-1&quot;&gt;Why it looks like Mac OS 8!?&lt;a href=&quot;#why-it-looks-like-mac-os-8&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 UI borrows its visual structure from Mac OS 8 — floating palettes, striped title bars, WindowShade collapse, warm cream surfaces. It&#39;s a design experiment: do late-90s desktop paradigms (persistent tool palettes, dense layouts, always-visible information) suit single-purpose browser utilities better than modern minimal convention?&lt;/p&gt;
&lt;p&gt;I wrote a &lt;a href=&quot;https://www.allaboutken.com/posts/20260216-90s-desktop-paradigm-browser-utilities/&quot;&gt;companion post on the design rationale&lt;/a&gt; if you want the full argument. Short version: browser tools are used in focused bursts, not browsed casually — the same use pattern floating palettes were designed for. The retro is a thin styling layer; the tool works without it.&lt;/p&gt;
&lt;p&gt;But this was a side project and I felt like I wanted a bit of creativity on the side of the side.&lt;/p&gt;
&lt;h2 id=&quot;try-it-break-it-improve-it&quot; tabindex=&quot;-1&quot;&gt;Try it, break it, improve it&lt;a href=&quot;#try-it-break-it-improve-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;PDF-A-go-slim is &lt;a href=&quot;https://github.com/khawkins98/PDF-A-go-slim&quot;&gt;MIT licensed and on GitHub&lt;/a&gt;. If you hit a compression edge case, an accessibility concern, or just have opinions on the floating palettes — I&#39;d like to hear about it.&lt;/p&gt;
&lt;p&gt;The Preview palette uses &lt;a href=&quot;https://www.allaboutken.com/posts/20250811-pdf-a-go-go/&quot;&gt;PDF-A-go-go&lt;/a&gt; for before/after PDF comparison, so the two projects keep feeding each other. If you&#39;ve been following the &lt;a href=&quot;https://www.allaboutken.com/posts/20250905-embedpdf-and-pdf-a-go-go/&quot;&gt;PDF-A-go-go and EmbedPDF&lt;/a&gt; posts, this is where the PDF tooling thread goes next. Full credits for inspirations and tools that informed the project are in the &lt;a href=&quot;https://github.com/khawkins98/PDF-A-go-slim#inspirations-and-references&quot;&gt;README&lt;/a&gt;.&lt;/p&gt;

</content>
  </entry>
  <entry>
    <title>Your browser utility wants to be a floating palette</title>
    <link href="https://www.allaboutken.com/posts/20260216-90s-desktop-paradigm-browser-utilities/"/>
    <published>2026-02-16T00:00:00.000Z</published>
    <updated>2026-02-16T00:00:00.000Z</updated>
    <id>https://www.allaboutken.com/posts/20260216-90s-desktop-paradigm-browser-utilities/</id>
    <author>
      <name>Ken Hawkins</name>
      <email>khawkins98@gmail.com</email>
    </author>
    <category term="UI design" />
    <category term="web development" />
    <category term="retro computing" />
    <summary type="html">Late-90s desktop paradigms may suit single-purpose browser tools better than modern minimal UI.</summary>
    <content type="html">
&lt;p&gt;Modern browser utilities default to &lt;a href=&quot;https://www.nngroup.com/articles/progressive-disclosure/&quot;&gt;progressive disclosure&lt;/a&gt; and &lt;a href=&quot;https://www.nngroup.com/articles/flat-design/&quot;&gt;minimal surfaces&lt;/a&gt;. Cards, centered content, generous whitespace, hidden settings. These patterns work for browsing — for exploring, scrolling, discovering. But single-purpose tools used in focused bursts may work better with the opposite: dense layouts, always-visible information, persistent tool palettes. I built &lt;a href=&quot;https://www.allaboutken.com/posts/20260216-introducing-pdf-a-go-slim/&quot;&gt;PDF-A-go-slim&lt;/a&gt; to try it out.&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;Browser utilities are used in focused bursts, not browsed casually&lt;/li&gt;
&lt;li&gt;Mac OS 8 floating palettes were designed for exactly this use pattern&lt;/li&gt;
&lt;li&gt;PostHog and Poolsuite show retro desktop metaphors work for developer audiences&lt;/li&gt;
&lt;li&gt;&amp;quot;Structure over skin&amp;quot; — borrow the layout grammar, not pixel art&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;the-hypothesis&quot; tabindex=&quot;-1&quot;&gt;The hypothesis&lt;a href=&quot;#the-hypothesis&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 web tools today follow the same playbook: a clean hero area, a single primary action, settings tucked behind a gear icon, results replacing the input. This works when users are exploring. It falls apart when they&#39;re doing one job.&lt;/p&gt;
&lt;p&gt;When someone drops a PDF on an optimizer, they want to see settings, progress, results, and file details simultaneously. They&#39;re not exploring. They&#39;re not browsing. They opened a tab, did one thing, and closed it. The whole interaction lasts maybe 30 seconds.&lt;/p&gt;
&lt;p&gt;Progressive disclosure assumes the user needs guidance through a sequence. Utility users already know what they want. They need information density, not a funnel. Everything on screen at once — the same way a carpenter wants all their tools on the bench, not locked in drawers.&lt;/p&gt;
&lt;h2 id=&quot;floating-windows-were-designed-for-this&quot; tabindex=&quot;-1&quot;&gt;Floating windows were designed for this&lt;a href=&quot;#floating-windows-were-designed-for-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;The &lt;a href=&quot;http://interface.free.fr/Archives/Apple_HIGOS8_Guidelines.pdf&quot;&gt;Apple Macintosh Human Interface Guidelines (1995)&lt;/a&gt; — the &amp;quot;Platinum&amp;quot; HIG — dedicated a section to floating windows (Chapter 5, pp. 105-106). These were specified for tool palettes: thinner chrome than document windows, always visible alongside the main content, brought to front on click but never stealing focus.&lt;/p&gt;
&lt;p&gt;The intent was explicit: tool settings should stay visible while you work on the document. Color picker, brush options, layers panel — all on screen at once, no tabs or menus required.&lt;/p&gt;
&lt;p&gt;PDF-A-go-slim borrows this directly. A main document window with a persistent drop zone, and five floating draggable palettes: Settings, Results, Inspector, Preview, and Accessibility. Each palette uses WindowShade — double-click the title bar to collapse it to just a strip. Arrange your workspace once and everything stays put.&lt;/p&gt;
&lt;p&gt;This isn&#39;t nostalgia. The HIG solved a real interaction problem: how to keep secondary controls visible during a focused task. That problem didn&#39;t go away when we moved to the browser.&lt;/p&gt;
&lt;h2 id=&quot;posthog-validated-the-direction-poolsuite-showed-the-path&quot; tabindex=&quot;-1&quot;&gt;PostHog validated the direction, Poolsuite showed the path&lt;a href=&quot;#posthog-validated-the-direction-poolsuite-showed-the-path&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;In 2025, &lt;a href=&quot;https://posthog.com/&quot;&gt;PostHog&lt;/a&gt; — a developer analytics company — redesigned their entire marketing site as a Windows-style desktop OS in the browser. Draggable windows, a start menu, a screensaver, a functional text editor, games in the trash folder. The project was built by &lt;a href=&quot;https://x.com/ninepixelgrid/status/1965618996990136539&quot;&gt;@ninepixelgrid&lt;/a&gt; over six months.&lt;/p&gt;
&lt;p&gt;A serious developer tools company independently chose the retro desktop metaphor and committed to it fully. That&#39;s worth paying attention to. The &lt;a href=&quot;https://news.ycombinator.com/item?id=45217269&quot;&gt;Hacker News discussion&lt;/a&gt; was largely positive — developers liked the personality and craft.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://poolsuite.net/&quot;&gt;Poolsuite&lt;/a&gt; showed the complementary lesson. Retro charm doesn&#39;t require heavy 3D bevels and pixel-perfect OS recreation. Thin borders, warm cream colors, and minimal chrome. It feels vintage without feeling heavy. That shaped PDF-A-go-slim&#39;s initial direction.&lt;/p&gt;
&lt;h2 id=&quot;structure-over-skin&quot; tabindex=&quot;-1&quot;&gt;Structure over skin&lt;a href=&quot;#structure-over-skin&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Three design principles guide the retro direction:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Borrow the layout grammar, not pixel art.&lt;/strong&gt; The Mac OS 8 reference is here for its spatial model — floating palettes, persistent tool windows, information always on screen. No pixel fonts, no full OS recreation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Visual elements provide function.&lt;/strong&gt; Striped title bars distinguish palettes from the document window. Sunken panels mark input areas. A status bar at the bottom shows what&#39;s happening. These are borrowed because they work, not because they look retro. Typically even the playful extras give functional boost.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Modern underneath.&lt;/strong&gt; It uses system fonts, CSS custom properties, semantic HTML, and responsive layout. The aesthetic is a styling layer. The app works without it.&lt;/p&gt;
&lt;p&gt;Concrete HIG patterns applied: WindowShade for palette collapse. Upfront information display (inspector categories expand by default, stats always visible). Dense property-sheet layouts with compact controls. Rich status bars showing pass labels and file counters during processing. A persistent drop zone that stays visible — just dimmed — during optimization.&lt;/p&gt;
&lt;p&gt;The result is a zero-scroll, single-page environment. Five palettes, an object inspector, an accessibility audit, a live PDF preview, optimization controls, theme switching, and desktop icons for sample PDFs — all visible at once without scrolling. A conventional stacked layout would need a long page and constant back-and-forth to show the same information. Floating palettes give you the space for both serious functionality and playful extras without either one crowding the other out.&lt;/p&gt;
&lt;p&gt;On mobile, the palettes stack vertically. Still collapsible, not draggable. It works because the underlying HTML is semantic — the floating palette positioning is just CSS.&lt;/p&gt;
&lt;h2 id=&quot;what-not-to-borrow&quot; tabindex=&quot;-1&quot;&gt;What not to borrow&lt;a href=&quot;#what-not-to-borrow&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 &lt;a href=&quot;https://news.ycombinator.com/item?id=45217269&quot;&gt;HN criticism of PostHog&lt;/a&gt; provides useful guardrails:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Don&#39;t override native browser behavior.&lt;/strong&gt; PostHog&#39;s custom right-click menu broke the browser context menu. PDF-A-go-slim leaves native interactions alone.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keyboard-accessible controls.&lt;/strong&gt; All buttons, tabs, and interactive elements are reachable by keyboard. Drag is a convenience, not a requirement.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Small bundle.&lt;/strong&gt; PostHog&#39;s implementation caused high CPU during drags and slow mobile loads. PDF-A-go-slim uses vanilla JS for dragging; the WASM font subsetter lazy-loads only when needed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Semantic HTML underneath.&lt;/strong&gt; Screen readers should see a form with fieldsets, not a desktop operating system.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Remove the CSS and you still have a functional, accessible tool. The retro is a layer on top, not load-bearing.&lt;/p&gt;
&lt;p&gt;Since launch, the retro direction keeps pulling in more: a Mac OS 8-style menu bar, classic system sounds (Sosumi, Quack, Wild Eep — the originals, &lt;a href=&quot;https://stevenjaycohen.com/journal/macos-classic-sound-pack-v1-4&quot;&gt;curated by Steven Jay Cohen&lt;/a&gt;), a Stickies palette, and a CRT scanline overlay. Each one started as a joke and turned out to make the tool more engaging to use.&lt;/p&gt;
&lt;h2 id=&quot;an-experiment-not-a-manifesto&quot; tabindex=&quot;-1&quot;&gt;An experiment, not a manifesto&lt;a href=&quot;#an-experiment-not-a-manifesto&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 is one tool testing one hypothesis: that dense, always-visible layouts suit focused utility work better than progressive disclosure. The density feels right for something you open, use for 30 seconds, and close. Whether it scales to more complex tools is a different question. I don&#39;t have metrics to validate any of this — it&#39;s a side project, and honestly, I also just wanted to have some fun building something with personality.&lt;/p&gt;
&lt;p&gt;If you want to try the tool, &lt;a href=&quot;https://khawkins98.github.io/PDF-A-go-slim/&quot;&gt;go have fun&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;In hindsight, I should have used an existing browser window manager instead of rolling my own — the same way real desktop apps lean on OS-level window management rather than reimplementing it. That would push the &amp;quot;web as OS&amp;quot; idea from aesthetic into architecture.&lt;/p&gt;
&lt;p&gt;You can read about how it approaches PDF stripping in the &lt;a href=&quot;https://www.allaboutken.com/posts/20260216-introducing-pdf-a-go-slim/&quot;&gt;companion introduction post&lt;/a&gt;. Full credits for design inspirations, sound sources, and tools that informed the project are in the &lt;a href=&quot;https://github.com/khawkins98/PDF-A-go-slim#inspirations-and-references&quot;&gt;README&lt;/a&gt;.&lt;/p&gt;

</content>
  </entry>
  <entry>
    <title>The practitioner&#39;s guide to planning digital transformation (Parts 3-4)</title>
    <link href="https://www.allaboutken.com/posts/20260205-business-analyst-guide-digital-transformation/"/>
    <published>2026-02-05T00:00:00.000Z</published>
    <updated>2026-02-05T00:00:00.000Z</updated>
    <id>https://www.allaboutken.com/posts/20260205-business-analyst-guide-digital-transformation/</id>
    <author>
      <name>Ken Hawkins</name>
      <email>khawkins98@gmail.com</email>
    </author>
    <category term="digital transformation" />
    <category term="business analysis" />
    <category term="requirements" />
    <category term="project management" />
    <summary type="html">How to assess maturity, build the business case, and avoid the change management pitfalls that derail most digital transformation initiatives.</summary>
    <content type="html">
&lt;p&gt;&lt;strong&gt;You&#39;ve been tasked with &amp;quot;digital transformation.&amp;quot;&lt;/strong&gt; Senior leadership wants it. The budget committee needs an ROI. The technical team wants requirements. And you&#39;re the one who has to make sense of it all — whether your title is business analyst, project manager, product owner, or just &amp;quot;the person who figures things out.&amp;quot; You&#39;re translating between stakeholders, documenting requirements, and turning vague goals into actionable plans.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;This is Parts 3-4 of my digital transformation series. Start with &lt;a href=&quot;https://www.allaboutken.com/posts/20260130-digital-transformation-complex-organizations/&quot;&gt;Parts 1-2: Digital transformation for complex organizations&lt;/a&gt; for the strategic framework.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Most digital transformation projects fail — &lt;a href=&quot;https://www.mckinsey.com/capabilities/mckinsey-digital/our-insights/the-twenty-one-exes-of-digital-transformation&quot;&gt;McKinsey puts the success rate at only 30%&lt;/a&gt; — usually not because of bad technology, but because of poor requirements, unclear success criteria, and no baseline assessment. The vague becomes vaguer. Budgets balloon. Stakeholders disengage.&lt;/p&gt;
&lt;p&gt;This guide covers the mechanics that prevent that: assessment, requirements, business case development, resource planning, and risk management. It&#39;s the tactical companion to strategic frameworks — the part that answers &amp;quot;how do we actually plan and execute this?&amp;quot;&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;p&gt;&lt;strong&gt;Part 3: Assess &amp;amp; Define&lt;/strong&gt; — Baseline your digital maturity, elicit requirements from stakeholders, and set measurable success criteria.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Part 4: Plan &amp;amp; Execute&lt;/strong&gt; — Build the business case around avoided costs, secure dedicated capacity, and manage the risks that actually derail initiatives.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&quot;part-3-assess-define&quot; tabindex=&quot;-1&quot;&gt;Part 3: Assess &amp;amp; Define&lt;a href=&quot;#part-3-assess-define&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 can plan transformation, you need to know where you are, what stakeholders actually need, and what success looks like. This section covers assessment, requirements, and success criteria.&lt;/p&gt;
&lt;h3 id=&quot;1-digital-maturity-assessment-know-where-you-are&quot; tabindex=&quot;-1&quot;&gt;1. Digital maturity assessment: Know where you are&lt;a href=&quot;#1-digital-maturity-assessment-know-where-you-are&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;You can&#39;t build a roadmap without a baseline. Yet most transformation initiatives skip assessment and jump straight to solutions. This creates two problems: you&#39;re solving for unknown gaps, and you have no way to measure improvement.&lt;/p&gt;
&lt;p&gt;Assessment prevents &amp;quot;solutions looking for problems&amp;quot; and creates shared understanding of what actually needs to change.&lt;/p&gt;
&lt;h4 id=&quot;assessment-dimensions&quot; tabindex=&quot;-1&quot;&gt;Assessment dimensions&lt;a href=&quot;#assessment-dimensions&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;I use a lightweight framework based on industry models (&lt;a href=&quot;https://www.bcg.com/capabilities/digital-technology-data/digital-maturity&quot;&gt;BCG&lt;/a&gt;, &lt;a href=&quot;https://www.deloitte.com/us/en/insights/topics/digital-transformation/digital-maturity-pivot-model.html&quot;&gt;Deloitte&lt;/a&gt;, &lt;a href=&quot;https://www.thinkwithgoogle.com/intl/en-emea/marketing-strategies/app-and-mobile/how-digital-marketing-maturity-helps-you-personalise-marketing-scale/&quot;&gt;Google/BCG&lt;/a&gt;) adapted for content-heavy organizations. Five dimensions, each scored Yes/Partial/No:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Content &amp;amp; Publishing&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Do you measure task completion (not just pageviews)?&lt;/li&gt;
&lt;li&gt;Is content structured with semantic metadata?&lt;/li&gt;
&lt;li&gt;Can editors articulate audience tasks for their content?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Measurement &amp;amp; Analytics&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Do you track beyond pageviews (findability, downstream impact)?&lt;/li&gt;
&lt;li&gt;Are metrics tied to business objectives?&lt;/li&gt;
&lt;li&gt;Can you demonstrate content ROI?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Technology &amp;amp; Infrastructure&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Does your content and data platform — whether a CMS like Drupal, a custom backend, or a database-driven system — support &lt;a href=&quot;https://www.allaboutken.com/posts/20180122-content-action-model/&quot;&gt;disciplined content&lt;/a&gt; (clear purpose, measurable goals)?&lt;/li&gt;
&lt;li&gt;Do you have machine-readable taxonomies (structured labels systems can process)?&lt;/li&gt;
&lt;li&gt;Can you emit JSON-LD or schema markup?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Governance &amp;amp; Process&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Do you have documented content workflows?&lt;/li&gt;
&lt;li&gt;Is there clear accountability for content outcomes?&lt;/li&gt;
&lt;li&gt;Are there incentives for adopting new practices?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;People &amp;amp; Skills&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Do editors understand user-centered content principles?&lt;/li&gt;
&lt;li&gt;Do developers know semantic HTML and accessibility?&lt;/li&gt;
&lt;li&gt;Does senior leadership understand digital metrics beyond traffic?&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;scoring&quot; tabindex=&quot;-1&quot;&gt;Scoring&lt;a href=&quot;#scoring&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;15+ Yes&lt;/strong&gt;: Mature — focus on optimization and innovation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;8-14 Yes&lt;/strong&gt;: Emerging — establish infrastructure systematically&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;0-7 Yes&lt;/strong&gt;: Nascent — start with foundational changes&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This isn&#39;t about judgment. A nascent organization isn&#39;t a failing — it&#39;s just at a different starting point. The assessment tells you where to focus effort.&lt;/p&gt;


&lt;aside class=&quot;kh-note-box&quot;&gt;
  &lt;span class=&quot;kh-note-box__label&quot;&gt;Template:&lt;/span&gt;
  Create a simple scorecard: 15 questions (3 per dimension) in a table format. Score each Yes (1 point), Partial (0.5 points), No (0 points). Total score guides your starting phase.
&lt;/aside&gt;

&lt;h4 id=&quot;how-to-conduct-the-assessment&quot; tabindex=&quot;-1&quot;&gt;How to conduct the assessment&lt;a href=&quot;#how-to-conduct-the-assessment&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Method 1: Stakeholder workshop&lt;/strong&gt; (fastest)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Gather cross-functional group (senior leaders, technologists, editors)&lt;/li&gt;
&lt;li&gt;Walk through each question together&lt;/li&gt;
&lt;li&gt;Discuss and score collaboratively&lt;/li&gt;
&lt;li&gt;Captures different perspectives but can be political&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Method 2: Individual interviews&lt;/strong&gt; (most thorough)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Interview 10-15 people across roles&lt;/li&gt;
&lt;li&gt;Score independently&lt;/li&gt;
&lt;li&gt;Analyze gaps between groups (senior leaders say &amp;quot;Yes,&amp;quot; editors say &amp;quot;No&amp;quot;)&lt;/li&gt;
&lt;li&gt;Takes longer but reveals organizational misalignment&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Use the scores as conversation starters, not final verdicts. If senior leadership thinks measurement is strong but editors can&#39;t access analytics, that&#39;s valuable signal.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;2-requirements-elicitation-what-you-need-from-stakeholders&quot; tabindex=&quot;-1&quot;&gt;2. Requirements elicitation: What you need from stakeholders&lt;a href=&quot;#2-requirements-elicitation-what-you-need-from-stakeholders&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Digital transformation means different things to different stakeholders. Senior leadership wants strategic outcomes. Technologists want specifications. Editors want their jobs to be easier. Your job is to translate between them and document requirements that all three groups can validate.&lt;/p&gt;
&lt;p&gt;This isn&#39;t about writing a 100-page requirements document. It&#39;s about using structured techniques to surface what each group actually needs — then documenting it in a way that guides decisions.&lt;/p&gt;
&lt;h4 id=&quot;the-three-stakeholder-groups&quot; tabindex=&quot;-1&quot;&gt;The three stakeholder groups&lt;a href=&quot;#the-three-stakeholder-groups&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Senior leadership&lt;/strong&gt; wants clarity, predictability, accountability. They need strategic aims converted into testable digital outputs — things they can report on, defend, and explain to boards.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Technologists&lt;/strong&gt; want feasibility, requirements, constraints. They need technical jargon translated into choices and trade-offs that non-technical people can actually decide on.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Editors&lt;/strong&gt; (content creators, publishers) want simplicity, relevance, speed. Someone needs to advocate for them — even when no one else in the room is.&lt;/p&gt;
&lt;h4 id=&quot;elicitation-techniques-for-each-group&quot; tabindex=&quot;-1&quot;&gt;Elicitation techniques for each group&lt;a href=&quot;#elicitation-techniques-for-each-group&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;For Senior Leadership: Structured interviews&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Senior leaders are time-constrained. Prepare focused questions that extract strategic intent, not just aspirations.&lt;/p&gt;
&lt;p&gt;Sample questions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;quot;What does &#39;digital transformation&#39; success look like in 12 months? What would you report to the board?&amp;quot;&lt;/li&gt;
&lt;li&gt;&amp;quot;What metrics do you currently track? How could digital work support those?&amp;quot;&lt;/li&gt;
&lt;li&gt;&amp;quot;What content or digital failures have you experienced? What caused them?&amp;quot;&lt;/li&gt;
&lt;li&gt;&amp;quot;How much investment can you justify without demonstrated ROI?&amp;quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The goal is to translate &amp;quot;we need better digital presence&amp;quot; into &amp;quot;we need to increase discoverability for policy briefs by 30% as measured by government domain referrals.&amp;quot;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;For Technologists: Document analysis + observation&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Don&#39;t just ask developers what&#39;s possible — observe what&#39;s actually happening.&lt;/p&gt;
&lt;p&gt;Review:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Platform documentation (what features exist vs. what&#39;s used)&lt;/li&gt;
&lt;li&gt;Current architecture diagrams&lt;/li&gt;
&lt;li&gt;Analytics setup (what&#39;s being tracked)&lt;/li&gt;
&lt;li&gt;Integration points (what systems talk to each other)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Ask:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;quot;What&#39;s technically feasible with our current stack?&amp;quot;&lt;/li&gt;
&lt;li&gt;&amp;quot;What would require platform changes?&amp;quot;&lt;/li&gt;
&lt;li&gt;&amp;quot;Where do you spend most time on manual work?&amp;quot;&lt;/li&gt;
&lt;li&gt;&amp;quot;What breaks most often?&amp;quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This surfaces hidden constraints. The platform might technically support structured metadata, but if the API is so slow that editors won&#39;t use it, that&#39;s a real constraint.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;For Editors: Observation + focus groups&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Editors will tell you what they think you want to hear. Watch them work instead.&lt;/p&gt;
&lt;p&gt;Observation technique:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Sit with an editor for 2 hours while they create content&lt;/li&gt;
&lt;li&gt;Don&#39;t interrupt — just note where they struggle&lt;/li&gt;
&lt;li&gt;Ask &amp;quot;why did you do that?&amp;quot; when they use workarounds&lt;/li&gt;
&lt;li&gt;Document manual processes that should be automated&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Focus group questions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;quot;What takes the most time in your workflow?&amp;quot;&lt;/li&gt;
&lt;li&gt;&amp;quot;What questions can&#39;t you answer?&amp;quot; (e.g., &amp;quot;Is anyone reading this?&amp;quot;)&lt;/li&gt;
&lt;li&gt;&amp;quot;What would make your job easier?&amp;quot;&lt;/li&gt;
&lt;li&gt;&amp;quot;When do you ignore the official process? Why?&amp;quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This reveals the gap between documented workflow and actual practice. If editors copy-paste content into Word to check formatting, your platform&#39;s preview isn&#39;t working.&lt;/p&gt;
&lt;h4 id=&quot;documenting-requirements&quot; tabindex=&quot;-1&quot;&gt;Documenting requirements&lt;a href=&quot;#documenting-requirements&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;Use standard BA categories: functional, non-functional, constraints.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sample functional requirements&lt;/strong&gt; (what the system must do)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;FR-001: System shall allow editors to tag content with audience tasks&lt;/li&gt;
&lt;li&gt;FR-002: System shall measure task completion rates per page&lt;/li&gt;
&lt;li&gt;FR-003: System shall generate semantic metadata automatically from editor inputs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Sample non-functional requirements&lt;/strong&gt; (quality attributes)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;NFR-001: Task completion tracking shall not slow page load by &amp;gt;100ms&lt;/li&gt;
&lt;li&gt;NFR-002: Editor training shall take &amp;lt;2 days per person&lt;/li&gt;
&lt;li&gt;NFR-003: Metadata system shall integrate with existing platform without data migration&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Sample constraints&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Cannot replace current platform (must work within existing systems)&lt;/li&gt;
&lt;li&gt;Must comply with accessibility standards (WCAG 2.1 AA)&lt;/li&gt;
&lt;li&gt;Must not disrupt current publishing workflows during pilot phase&lt;/li&gt;
&lt;li&gt;Budget cannot exceed $X for first 6 months&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Prioritize requirements using MoSCoW (Must/Should/Could/Won&#39;t):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Must have&lt;/strong&gt;: Task completion tracking, semantic HTML&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Should have&lt;/strong&gt;: Automated metadata generation, editor dashboards&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Could have&lt;/strong&gt;: AI-powered taxonomy suggestions&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Won&#39;t have&lt;/strong&gt; (this phase): Full content inventory migration, new platform&lt;/li&gt;
&lt;/ul&gt;


&lt;aside class=&quot;kh-note-box&quot;&gt;
  &lt;span class=&quot;kh-note-box__label&quot;&gt;Pro tip:&lt;/span&gt;
  When stakeholders disagree on requirements, document both views and the decision criteria. &quot;Senior leadership wants automated publishing; editors want review gates. Decision: implement review gates with optional auto-publish for low-risk content types (defined in governance model).&quot;
&lt;/aside&gt;

&lt;hr&gt;
&lt;h3 id=&quot;3-success-criteria-specific-measurable-targets&quot; tabindex=&quot;-1&quot;&gt;3. Success criteria: Specific, measurable targets&lt;a href=&quot;#3-success-criteria-specific-measurable-targets&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Vague goals produce vague results. &amp;quot;Improve digital presence&amp;quot; isn&#39;t measurable. &amp;quot;Increase task completion rate by 25% on top-traffic pages within 6 months&amp;quot; is.&lt;/p&gt;
&lt;p&gt;Success criteria should be specific enough that anyone can verify whether you achieved them. Use the OKR framework: Objectives (directional goals) supported by Key Results (measurable outcomes).&lt;/p&gt;
&lt;h4 id=&quot;framework-okrs-for-digital-transformation&quot; tabindex=&quot;-1&quot;&gt;Framework: OKRs for digital transformation&lt;a href=&quot;#framework-okrs-for-digital-transformation&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Objective 1: Make content measurably useful&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;KR1: 80% of editors can articulate the audience task for their content (measured via spot checks)&lt;/li&gt;
&lt;li&gt;KR2: Task completion tracking implemented on 100% of top-traffic pages&lt;/li&gt;
&lt;li&gt;KR3: Measured task completion rate improves 25% from baseline&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Objective 2: Establish AI-ready content infrastructure&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;KR1: 100% of new content uses semantic HTML (validated via automated checks)&lt;/li&gt;
&lt;li&gt;KR2: 50% of existing high-value content has JSON-LD markup&lt;/li&gt;
&lt;li&gt;KR3: Citation/reference mentions in AI-generated responses increase 2x from baseline&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Objective 3: Demonstrate ROI&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;KR1: Editorial efficiency improves by 15 hours/week (quantified time savings)&lt;/li&gt;
&lt;li&gt;KR2: Content discoverability improves 20% (measured via organic search increase from target audiences)&lt;/li&gt;
&lt;li&gt;KR3: Avoided cost: Prevent $100K+ in low-performing content investment through early measurement&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;phase-based-acceptance-criteria&quot; tabindex=&quot;-1&quot;&gt;Phase-based acceptance criteria&lt;a href=&quot;#phase-based-acceptance-criteria&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;Break objectives into phases with clear gates. Each phase ends with GO/NO-GO decision.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pilot Phase (Months 1-3)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ 5 pages have audience task documentation (what task + how to measure)&lt;/li&gt;
&lt;li&gt;✅ Task completion tracking implemented on pilot pages&lt;/li&gt;
&lt;li&gt;✅ 10 editors trained and using framework&lt;/li&gt;
&lt;li&gt;✅ Baseline metrics captured for all pilot pages&lt;/li&gt;
&lt;li&gt;✅ GO criteria: Measurable improvement in 2+ pilot metrics; editor satisfaction &amp;gt;7/10&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Rollout Phase (Months 4-9)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ 50% of content inventory assessed with audience task framework&lt;/li&gt;
&lt;li&gt;✅ Semantic metadata on 100 high-priority pages&lt;/li&gt;
&lt;li&gt;✅ All editors trained (200+)&lt;/li&gt;
&lt;li&gt;✅ Documented 10% improvement in key metrics vs. baseline&lt;/li&gt;
&lt;li&gt;✅ GO criteria: Adoption rate &amp;gt;80%; metrics trending positive; sponsor approves scale&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Optimization Phase (Months 10-12)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ Full measurement → improvement cycle operational&lt;/li&gt;
&lt;li&gt;✅ AI-generated responses cite content 2x baseline&lt;/li&gt;
&lt;li&gt;✅ ROI case study documented with quantified benefits&lt;/li&gt;
&lt;li&gt;✅ Governance model established and operating independently&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The GO/NO-GO gates are critical. If the pilot doesn&#39;t demonstrate improvement, you iterate or cancel — you don&#39;t keep something that doesn&#39;t work.&lt;/p&gt;
&lt;p&gt;This phase-gate approach builds in permission to learn and adjust. You&#39;re not aiming for the complete solution upfront — you&#39;re shipping something focused and high-quality, gathering feedback, and letting audience demand guide what comes next. Teams need to feel safe surfacing problems early.&lt;/p&gt;
&lt;p&gt;Celebrate what you learned, not just what you shipped.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;part-4-plan-execute&quot; tabindex=&quot;-1&quot;&gt;Part 4: Plan &amp;amp; Execute&lt;a href=&quot;#part-4-plan-execute&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;You&#39;ve assessed where you are, gathered requirements, and defined success criteria. Now you need to secure budget, plan resources, and manage risks.&lt;/p&gt;
&lt;h3 id=&quot;build-the-business-case&quot; tabindex=&quot;-1&quot;&gt;Build the business case&lt;a href=&quot;#build-the-business-case&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Finance won&#39;t approve &amp;quot;better digital presence.&amp;quot; They&#39;ll approve &amp;quot;invest $X to save $Y in editorial efficiency and prevent $Z in failed content spend.&amp;quot;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Frame benefits as avoided costs, not speculative gains.&lt;/strong&gt; Senior leadership responds to &amp;quot;prevent $200K in failed content investment&amp;quot; more than &amp;quot;possibly gain traffic.&amp;quot; The former is concrete; the latter is speculative.&lt;/p&gt;
&lt;p&gt;The benefit categories that matter most:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Time savings&lt;/strong&gt;: Editorial efficiency improvements (quantify with time studies)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Risk reduction&lt;/strong&gt;: Catching underperforming content in pilot phase instead of after full investment&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reach improvement&lt;/strong&gt;: Increased discoverability — directionally significant even when hard to monetize&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The cost categories to account for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Labor&lt;/strong&gt;: Staff time for training, implementation, ongoing work (often underestimated)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Technology&lt;/strong&gt;: Platform upgrades, tools, integrations&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Opportunity cost&lt;/strong&gt;: What else could the team do with this time?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Most transformation initiatives underestimate labor costs and overestimate technology costs. The real investment is people&#39;s time.&lt;/p&gt;
&lt;h3 id=&quot;resource-realities&quot; tabindex=&quot;-1&quot;&gt;Resource realities&lt;a href=&quot;#resource-realities&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&amp;quot;We&#39;ll do this with existing staff in their spare time&amp;quot; rarely works. Transformation requires dedicated capacity — not full-time necessarily, but protected time.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Key roles to fill&lt;/strong&gt; (can be fractional):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Someone who owns the initiative and manages stakeholders&lt;/li&gt;
&lt;li&gt;Someone who understands content strategy and can train editors&lt;/li&gt;
&lt;li&gt;Someone who can implement technical standards (semantic HTML, structured data)&lt;/li&gt;
&lt;li&gt;Someone tracking requirements and progress (that&#39;s you)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;what-derails-initiatives&quot; tabindex=&quot;-1&quot;&gt;What derails initiatives&lt;a href=&quot;#what-derails-initiatives&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 gap that kills most initiatives isn&#39;t technical. It&#39;s change management. Editors resist workflow changes without visible incentives and demonstrated time savings.&lt;/p&gt;
&lt;p&gt;Three risks appear in nearly every digital transformation:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Leadership attention fades.&lt;/strong&gt; Secure 12-month commitment upfront. Show quick wins by month 3. Brief leadership monthly with metrics, not just activity.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Editors resist workflow changes.&lt;/strong&gt; This is the highest-probability risk. Mitigate by involving editors in framework design, tracking time savings during pilot, and making adoption visible and celebrated.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Scope creeps beyond budget.&lt;/strong&gt; Lock scope for pilot phase. Use phase gates — require a new business case for scope additions rather than absorbing them.&lt;/p&gt;
&lt;h3 id=&quot;the-critical-path&quot; tabindex=&quot;-1&quot;&gt;The critical path&lt;a href=&quot;#the-critical-path&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Some work can happen in parallel. Some cannot. The sequence that cannot slip:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Stakeholder alignment&lt;/strong&gt; → enables requirements elicitation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Business case approval&lt;/strong&gt; → enables team formation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pilot validation&lt;/strong&gt; → enables rollout approval&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Everything else can flex. Training and technical infrastructure can run in parallel. Documentation and governance development can overlap.&lt;/p&gt;
&lt;p&gt;Build 20% buffer into timelines. Identify dependencies early. The critical path runs through validation gates, not deliverable dates.&lt;/p&gt;


&lt;aside class=&quot;kh-note-box&quot;&gt;
  &lt;span class=&quot;kh-note-box__label&quot;&gt;Template:&lt;/span&gt;
  For structured planning, create templates covering: business case (costs, benefits, ROI calculation), resource plan (roles, capacity, timeline), and risk register (probability, impact, mitigation).
&lt;/aside&gt;

&lt;hr&gt;
&lt;h2 id=&quot;putting-it-together&quot; tabindex=&quot;-1&quot;&gt;Putting it together&lt;a href=&quot;#putting-it-together&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 guide covers the mechanics that strategic frameworks often skip: how to assess, how to elicit requirements, how to quantify ROI, how to plan resources, and how to manage risks.&lt;/p&gt;
&lt;p&gt;The strategic framework tells you &lt;strong&gt;what&lt;/strong&gt; to establish (stakeholder alignment, audience task frameworks, measurement systems, AI-ready infrastructure). This guide tells you &lt;strong&gt;how&lt;/strong&gt; to plan and execute it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Key success factors&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Start with baseline assessment — don&#39;t skip this&lt;/li&gt;
&lt;li&gt;Build the business case with quantified benefits (time savings + avoided costs)&lt;/li&gt;
&lt;li&gt;Use phase gates to validate before scaling&lt;/li&gt;
&lt;li&gt;Involve stakeholders early and often (especially editors)&lt;/li&gt;
&lt;li&gt;Document everything (requirements, decisions, learnings)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Common pitfalls&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Starting implementation without requirements&lt;/li&gt;
&lt;li&gt;Building business case without baseline metrics (makes ROI impossible to prove)&lt;/li&gt;
&lt;li&gt;Underestimating change management (editors resist without incentives)&lt;/li&gt;
&lt;li&gt;Skipping pilot validation (rolling out something that doesn&#39;t work)&lt;/li&gt;
&lt;li&gt;Treating this as pure technology project (it&#39;s mostly about people and process)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Digital transformation is as much about requirements discipline and stakeholder management as it is about technology. The skills that matter here — elicitation, documentation, risk management, change impact analysis — are what make the vague concrete and the aspirational measurable.&lt;/p&gt;
&lt;p&gt;If you&#39;re planning a digital transformation initiative and need help with requirements, business case development, or implementation planning, I&#39;d love to hear about your challenges.&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;

  This guide is the tactical companion to &lt;a href=&quot;https://www.allaboutken.com/posts/20260130-digital-transformation-complex-organizations/&quot;&gt;Digital transformation for complex organizations&lt;/a&gt;, which presents the strategic framework (The Groundwork + The Process). Use both together: the framework for the vision, this guide for the execution.
&lt;/aside&gt;
</content>
  </entry>
  <entry>
    <title>Digital transformation for complex organizations (Parts 1-2)</title>
    <link href="https://www.allaboutken.com/posts/20260130-digital-transformation-complex-organizations/"/>
    <published>2026-01-30T00:00:00.000Z</published>
    <updated>2026-01-30T00:00:00.000Z</updated>
    <id>https://www.allaboutken.com/posts/20260130-digital-transformation-complex-organizations/</id>
    <author>
      <name>Ken Hawkins</name>
      <email>khawkins98@gmail.com</email>
    </author>
    <category term="digital transformation" />
    <category term="information architecture" />
    <category term="content strategy" />
    <category term="organizational change" />
    <summary type="html">A practical framework for moving legacy institutions toward measurable, audience-centered digital practice — without throwing out what already works.</summary>
    <content type="html">
&lt;p&gt;&lt;strong&gt;Digital transformation isn&#39;t blocked by technology.&lt;/strong&gt; It needs senior leadership, technologists, and your audience to understand each other well enough to move forward.&lt;/p&gt;
&lt;p&gt;The symptoms of that disconnect are everywhere. I&#39;ve seen 200-page PDF reports published with zero tracking or machine readability considerations. I&#39;ve seen senior leaders ask &amp;quot;Can AI tell us?&amp;quot; believing AI is a magic spice to make sense of content that humans already struggle to find. I&#39;ve been in rooms where technologists explain what the content and data platform &lt;em&gt;can&lt;/em&gt; do — whether that&#39;s Drupal, a custom backend, or a database-driven system — and communicators explain what the content &lt;em&gt;should&lt;/em&gt; do, and neither realizes they&#39;re talking past each other.&lt;/p&gt;
&lt;p&gt;This post is a sketch of what I&#39;ve learned about modernizing digital work. It&#39;s organized in two parts: &lt;strong&gt;the groundwork&lt;/strong&gt; (what transformation actually requires) and &lt;strong&gt;the process&lt;/strong&gt; (what you need to sustain it).&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;p&gt;&lt;strong&gt;Part 1: The Groundwork&lt;/strong&gt; — Translation between senior leadership, technologists, and your audience. Use the &lt;a href=&quot;https://www.allaboutken.com/posts/20180122-content-action-model.html&quot;&gt;Content–Action Model&lt;/a&gt; to align everyone around audience tasks and measurable outcomes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Part 2: The Process&lt;/strong&gt; — Establish a digital value loop: disciplined content → discoverability → actions → measurement → improvement. Measure task completion (not pageviews), make content AI-ready, and use small wins to build momentum.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&quot;part-1-the-groundwork&quot; tabindex=&quot;-1&quot;&gt;Part 1: The Groundwork&lt;a href=&quot;#part-1-the-groundwork&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Governance can be heavy, teams are often split, incentives vary wildly, and &amp;quot;digital transformation&amp;quot; can mean everything and nothing. This section covers the groundwork that makes progress possible.&lt;/p&gt;
&lt;h3 id=&quot;1-the-bridge-builder-model&quot; tabindex=&quot;-1&quot;&gt;1. The bridge-builder model&lt;a href=&quot;#1-the-bridge-builder-model&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 core work is translation — between senior leadership, technologists, and your audience.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Senior leadership&lt;/strong&gt; wants clarity, predictability, accountability. They need strategic aims converted into testable digital outputs — things they can report on, defend, and explain to boards.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Technologists&lt;/strong&gt; want feasibility, requirements, constraints. They need technical jargon translated into choices and trade-offs that non-technical people can actually decide on.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Your audience&lt;/strong&gt; wants simplicity, relevance, speed. Someone needs to advocate for them — even when no one else in the room is.&lt;/p&gt;
&lt;p&gt;In practice, most digital transformation work is making these three groups talk to each other:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Converting vague &amp;quot;we need better digital presence&amp;quot; into specific deliverables&lt;/li&gt;
&lt;li&gt;Turning &amp;quot;can we use AI?&amp;quot; into &amp;quot;here are three options with trade-offs&amp;quot;&lt;/li&gt;
&lt;li&gt;Building lightweight governance that nudges behavior without paralyzing teams&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When stakeholders have competing priorities — and they will — staying focused on goals and measurement creates common ground. You iterate, you measure, you argue with evidence instead of opinions. Compromise happens, but it happens around facts.&lt;/p&gt;
&lt;p&gt;This is the part no AI can automate — and probably the most valuable skill in organizational digital work. Whether your title is business analyst, project manager, product owner, or something else entirely, this translation work is what makes transformation possible.&lt;/p&gt;
&lt;h3 id=&quot;2-information-for-success-at-scale&quot; tabindex=&quot;-1&quot;&gt;2. Information for success at scale&lt;a href=&quot;#2-information-for-success-at-scale&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 mistake many organizations make: they think digital is about &lt;em&gt;publishing&lt;/em&gt;. In reality, digital is about helping someone do something.&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;&lt;a href=&quot;https://www.allaboutken.com/posts/20180122-content-action-model.html&quot;&gt;Content–Action Model&lt;/a&gt;&lt;/strong&gt; asks two questions of every page, product, and asset:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;What is this content helping someone do?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;How would we know if they did it?&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This sounds simple. It isn&#39;t.&lt;/p&gt;
&lt;p&gt;Most organizational content exists because someone decided to publish it, not because anyone identified an audience need it was meant to serve.&lt;/p&gt;
&lt;p&gt;When you apply this model consistently, several things happen:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Sites get smaller&lt;/strong&gt;. Content that can&#39;t answer these questions gets archived or merged.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Duplication reduces&lt;/strong&gt;. You can&#39;t justify three pages about the same topic serving the same action.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Publishing roles clarify&lt;/strong&gt;. Editors become responsible for specific audience outcomes, not just &amp;quot;their content.&amp;quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SEO improves&lt;/strong&gt;. Search engines reward content that demonstrably serves audience intent.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Analytics become meaningful&lt;/strong&gt;. You&#39;re measuring actions, not just visits.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I&#39;ve helped this model scale to organizations with dozens of editors at institutions like EMBL-EBI — it works because it gives everyone a shared framework for evaluating content decisions, regardless of their specific domain expertise.&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;
  I wrote more about the &lt;a href=&quot;https://www.allaboutken.com/posts/20180122-content-action-model.html&quot;&gt;Content-Action Model origin story&lt;/a&gt; and how it relates to Core Content Model approaches in 2018.
&lt;/aside&gt;

&lt;hr&gt;
&lt;h2 id=&quot;part-2-the-process&quot; tabindex=&quot;-1&quot;&gt;Part 2: The Process&lt;a href=&quot;#part-2-the-process&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 groundwork in Part 1 is about human alignment. But alignment alone doesn&#39;t produce sustainable change — you also need systems that operationalize the vision. This section covers what to foster and institutionalize.&lt;/p&gt;
&lt;h3 id=&quot;3-the-digital-value-loop&quot; tabindex=&quot;-1&quot;&gt;3. The digital value loop&lt;a href=&quot;#3-the-digital-value-loop&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 future for complex organizations isn&#39;t &amp;quot;make more content.&amp;quot; It&#39;s building a self-reinforcing loop:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Disciplined content&lt;/strong&gt; → enables&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Better discoverability&lt;/strong&gt; (web + search + AI) → drives&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Clear audience actions&lt;/strong&gt; → generates&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Measurable impact&lt;/strong&gt; → informs&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Continuous improvement&lt;/strong&gt; → produces better disciplined content&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Each step feeds the next. Organizations that establish this loop — even imperfectly — outpace those still treating digital as a simple output function to mirror PDF documents.&lt;/p&gt;
&lt;p&gt;The following sections unpack each element of the loop: what to measure, what to put in place, and how to implement while staying on track.&lt;/p&gt;
&lt;h3 id=&quot;4-measuring-what-matters&quot; tabindex=&quot;-1&quot;&gt;4. Measuring what matters&lt;a href=&quot;#4-measuring-what-matters&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 default metrics in many public-sector or research organizations aren&#39;t actually meaning.&lt;/p&gt;
&lt;p&gt;Pageviews, impressions, PDF downloads, newsletter &amp;quot;reach&amp;quot; — none of these answer the question: did anyone find what they needed?&lt;/p&gt;
&lt;p&gt;Better measurement focuses on:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Findability&lt;/strong&gt;: Are people reaching this content through the pathways we expect? Internal referrals, search queries, and navigational success tell you whether your information architecture is working.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Task completion&lt;/strong&gt;: Did they take the action the content was designed for? This could be downloading a resource, submitting a form, or clicking through to a deeper page.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Quality of engagement&lt;/strong&gt;: Not just time-on-page, but scroll depth, interaction with expandable content, return visits.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Downstream impact&lt;/strong&gt;: Citations, reuse, machine readability. Does your content propagate through the systems that matter to your mission?&lt;/p&gt;
&lt;p&gt;For content that feeds AI systems, &amp;quot;computed reach&amp;quot; — how often your content appears in AI-generated responses — is becoming an important signal. Current approaches: manually query AI systems for your key topics, use emerging monitoring tools that track AI citations, and watch for referral traffic from AI-assisted search. The measurement is directional rather than precise, but ignoring it means flying blind as AI intermediates more discovery.&lt;/p&gt;
&lt;p&gt;This is where digital work is heading: measuring how content &lt;em&gt;flows&lt;/em&gt;, not just how it sits.&lt;/p&gt;


&lt;aside class=&quot;kh-note-box&quot;&gt;
  &lt;span class=&quot;kh-note-box__label&quot;&gt;Deep dive:&lt;/span&gt;
  I wrote more about &lt;a href=&quot;https://www.allaboutken.com/posts/20251203-measuring-success-beyond-page-view/&quot;&gt;measuring content success beyond page views&lt;/a&gt;, including specific implementation approaches for different content types.
&lt;/aside&gt;

&lt;h3 id=&quot;5-ai-ready-content-infrastructure&quot; tabindex=&quot;-1&quot;&gt;5. AI-ready content infrastructure&lt;a href=&quot;#5-ai-ready-content-infrastructure&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Most organizations still publish content like it&#39;s 2008. A page gets written, formatted, maybe tagged with a category, and shipped.&lt;/p&gt;
&lt;p&gt;The assumption is that humans will find it through search or navigation.&lt;/p&gt;
&lt;p&gt;That assumption is breaking down. AI systems now intermediate much of how people discover and consume information. If your content isn&#39;t structured for machine consumption, it&#39;s increasingly invisible.&lt;/p&gt;
&lt;p&gt;What AI-ready means in practice:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Content is structured&lt;/strong&gt;, not just visually formatted. Headings, lists, and semantic HTML aren&#39;t decoration — they&#39;re data.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Content is accessible&lt;/strong&gt;. Semantic HTML, ARIA labels, and proper heading hierarchy serve both assistive technologies and AI systems. What helps screen readers helps machine learning models.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Metadata is semantic&lt;/strong&gt;, not decorative. Tags and categories should map to controlled vocabularies, not ad-hoc labels.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Taxonomies are machine-readable&lt;/strong&gt;. SKOS-aligned concept schemes let AI systems understand relationships between topics.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pages emit structured data&lt;/strong&gt;. JSON-LD schema markup tells search engines and AI systems what your content &lt;em&gt;is&lt;/em&gt;, not just what it says.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Assets generate machine-actionable outputs&lt;/strong&gt;. PDFs should have metadata. Data should be queryable. Reports should have structured abstracts.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This isn&#39;t about being innovative. It&#39;s about being discoverable in a world where fewer humans reach your website directly.&lt;/p&gt;
&lt;p&gt;AI systems read your content even when your audience doesn&#39;t. Give them something meaningful to read.&lt;/p&gt;


&lt;aside class=&quot;kh-note-box&quot;&gt;
  &lt;span class=&quot;kh-note-box__label&quot;&gt;AI disruption:&lt;/span&gt;
  I explored how &lt;a href=&quot;https://www.allaboutken.com/posts/20250914-ai-and-the-commoditization-of-reference/&quot;&gt;AI is commoditizing reference content&lt;/a&gt; and what that means for publishers trying to maintain relevance and distribution.
&lt;/aside&gt;

&lt;h3 id=&quot;6-making-it-work-implementation-patterns&quot; tabindex=&quot;-1&quot;&gt;6. Making it work: Implementation patterns&lt;a href=&quot;#6-making-it-work-implementation-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;The loop from section 3 is the goal. Here&#39;s what I&#39;ve seen work — and fail — when trying to establish it across complex organizations like UNDRR and EMBL.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Note: I may expand these patterns into detailed guides in future posts — each could become its own case study with templates and examples.&lt;/em&gt;&lt;/p&gt;
&lt;h4 id=&quot;what-works&quot; tabindex=&quot;-1&quot;&gt;What works&lt;a href=&quot;#what-works&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Replace a &amp;quot;traditional content output&amp;quot; mindset with audience+task mindset&lt;/strong&gt;. The shift from &amp;quot;we need to publish this&amp;quot; to &amp;quot;we need to help our audience do X&amp;quot; changes every content decision downstream.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Establish centralized but flexible governance&lt;/strong&gt;. &lt;a href=&quot;https://www.allaboutken.com/work/2019/impact-story-embl-visual-framework/&quot;&gt;Design systems that offer multiple integration paths&lt;/a&gt; — full adoption, partial adoption, visual-only — spread faster than mandated standards.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Create KPIs that senior leadership can relate to&lt;/strong&gt;. Connect technical metrics to organizational outcomes. Page speed affects reach in bandwidth-constrained regions. Structured metadata improves citation rates.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Introduce data literacy gently, but consistently&lt;/strong&gt;. Not everyone needs to read dashboards, but everyone should understand what &amp;quot;this content achieved X&amp;quot; means and trust the measurement.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Use small wins to prove value&lt;/strong&gt;. &lt;a href=&quot;https://www.allaboutken.com/work/2025/impact-story-gar2025-landing/&quot;&gt;Structured metadata for one content type&lt;/a&gt;. Performance improvements on one high-traffic page. These create appetite for larger transformation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Avoid totalism — iterate instead&lt;/strong&gt;. You don&#39;t need to solve everything at once. Ship something focused and high-quality that doesn&#39;t do everything, gather feedback, and let audience demand guide what comes next. This builds momentum and proves value faster than waiting for the complete solution.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Treat AI as an amplifier, not a replacement&lt;/strong&gt;. AI tools can speed up content production, improve discoverability, and automate taxonomy tagging. They can&#39;t replace human judgment about what matters to your audience.&lt;/p&gt;
&lt;h4 id=&quot;examples-of-what-to-avoid&quot; tabindex=&quot;-1&quot;&gt;Examples of what to avoid&lt;a href=&quot;#examples-of-what-to-avoid&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Big-bang rebuilds without political buy-in&lt;/strong&gt;. Platforms live or die on adoption. A technically perfect system that teams won&#39;t use is worthless.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Strategy divorced from technical feasibility&lt;/strong&gt;. The communications team&#39;s vision needs to survive contact with your platform&#39;s actual capabilities.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Building platforms without a metadata plan&lt;/strong&gt;. If you don&#39;t design for disciplined content from the start, you&#39;ll never retrofit it effectively.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Publishing PDFs with zero tracking&lt;/strong&gt;. If you can&#39;t measure whether anyone read it, you can&#39;t justify making more of them.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Assuming editors will change workflows without incentives&lt;/strong&gt;. People do what&#39;s easy and rewarded. Make the right behavior both.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Underestimating cost and timeline realities&lt;/strong&gt;. Transformation always costs more and takes longer than initial estimates. Build the business case around small, demonstrable wins that justify continued investment — not aspirational end-states that may never materialize. (I&#39;ll cover business case development, ROI frameworks, and resource planning in a follow-up post.)&lt;/p&gt;
&lt;p&gt;Everything here is about making progress in environments where change is slow.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;putting-it-together&quot; tabindex=&quot;-1&quot;&gt;Putting it together&lt;a href=&quot;#putting-it-together&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 isn&#39;t theoretical. I&#39;ve used these patterns at UNDRR, EMBL, and across public-sector digital teams.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Part 1 — the groundwork&lt;/strong&gt; — is about translation: making senior leadership, technologists, and your audience understand each other well enough to move forward. It&#39;s the human skill that no platform purchase or consultancy can replace.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Part 2 — the process&lt;/strong&gt; — is what makes that understanding operational: measurement systems, disciplined content, AI-ready architecture, and implementation patterns that survive contact with organizational reality.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Part 3 &amp;amp; 4 — the execution&lt;/strong&gt; — covers the tactical side: how to assess where you are, build the business case, plan resources, and manage risks. That post is coming in a week or two.&lt;/p&gt;
&lt;p&gt;Neither part works alone. Brilliant strategy without process produces reports that gather dust. Perfect process without alignment produces platforms that no one adopts.&lt;/p&gt;
&lt;p&gt;Digital transformation in complex organizations isn&#39;t about buying the right technology. It&#39;s about building shared language, lightweight governance, and measurement that connects technical work to organizational outcomes — and then building systems that sustain it.&lt;/p&gt;
&lt;p&gt;If you&#39;re working through similar challenges — or if you&#39;ve found different approaches that work — I&#39;d love to hear about it.&lt;/p&gt;


&lt;aside class=&quot;kh-note-box&quot;&gt;
  &lt;span class=&quot;kh-note-box__label&quot;&gt;Related on my blog&lt;/span&gt;

  &lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.allaboutken.com/posts/20260205-business-analyst-guide-digital-transformation/&quot;&gt;The practitioner&#39;s guide to planning digital transformation&lt;/a&gt; — Assessment, requirements, ROI frameworks, and implementation roadmaps&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.allaboutken.com/work/2019/impact-story-embl-visual-framework/&quot;&gt;Enabling coherent content, UX and design at scale with the Visual Framework&lt;/a&gt; — Building design systems that spread through adoption, not mandate&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.allaboutken.com/work/2025/impact-story-gar2025-landing/&quot;&gt;Visual storytelling for GAR 2025&lt;/a&gt; — Making complex data accessible without sacrificing performance&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.allaboutken.com/work/2025/impact-story-editorial-efficiency/&quot;&gt;Editorial efficiency through component-based publishing&lt;/a&gt; — Reducing editorial overhead through better tooling&lt;/li&gt;
&lt;/ul&gt;

&lt;/aside&gt;
</content>
  </entry>
  <entry>
    <title>VR Productivity finally almost surpasses physical displays</title>
    <link href="https://www.allaboutken.com/posts/20260124-vr-productivity-setup-quest-3-immersed/"/>
    <published>2026-01-24T00:00:00.000Z</published>
    <updated>2026-01-24T00:00:00.000Z</updated>
    <id>https://www.allaboutken.com/posts/20260124-vr-productivity-setup-quest-3-immersed/</id>
    <author>
      <name>Ken Hawkins</name>
      <email>khawkins98@gmail.com</email>
    </author>
    <category term="VR" />
    <category term="productivity" />
    <category term="Quest 3" />
    <category term="remote work" />
    <summary type="html">After years of trying, I found a work VR work setup that works real: Quest 3, Immersed on Mac, and a single ultra-wide screen.</summary>
    <content type="html">
&lt;p&gt;I&#39;ve wanted the same thing from VR for years: virtual big screens strapped to my head. Not gaming, not social spaces — just an excess of screen real estate and the freedom to sit however I want without fussing with monitor height. After trying various headsets and apps across Windows, Linux, and now Mac, I finally have a setup that works well enough to use daily.&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;Quest 3 + Immersed on Mac works better than other combinations I&#39;ve tried&lt;/li&gt;
&lt;li&gt;Resolution and settings that give the best productivity experience&lt;/li&gt;
&lt;li&gt;The unexpected ergonomic freedom of a screen that follows your posture&lt;/li&gt;
&lt;li&gt;Trade-offs and quirks, plus what&#39;s still missing for that serene workspace feeling&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;a-quest-for-vr-productivity&quot; tabindex=&quot;-1&quot;&gt;A quest for VR productivity&lt;a href=&quot;#a-quest-for-vr-productivity&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 years I&#39;ve eyed options. The Immersed Visor looked promising but still hasn&#39;t shipped. I picked up Rokid AR glasses — lightweight, unobtrusive — but the display just isn&#39;t big enough or sharp enough for a full workspace, or to qualify my &amp;quot;big screen strapped to face&amp;quot; requirement. It&#39;s fine for a movie on a plane, not for staring at code all day.&lt;/p&gt;
&lt;p&gt;In early 2025, Quest 3 seemed like the best hope, but the experience was too temperamental on Windows and Linux — apps would crash, connections would drop, and I&#39;d spend more time troubleshooting than working.&lt;/p&gt;
&lt;p&gt;When I switched to back to the Mac, something clicked. Immersed was more stable. Still not perfect, but stable enough that I could actually get work done instead of fighting with the software.&lt;/p&gt;
&lt;h2 id=&quot;what-finally-worked&quot; tabindex=&quot;-1&quot;&gt;What finally worked&lt;a href=&quot;#what-finally-worked&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Here&#39;s the setup that got me to 90% happy:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Hardware:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Meta Quest 3&lt;/li&gt;
&lt;li&gt;Bobo VR S3 Pro head strap (in &amp;quot;halo&amp;quot; mode)&lt;/li&gt;
&lt;li&gt;Single USB-C cable from Mac to headset (power + data)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Software:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Immersed on macOS 15.5&lt;/li&gt;
&lt;li&gt;Single virtual display: 5120 × 1440 pixels&lt;/li&gt;
&lt;li&gt;Black desktop background and black Immersed environment&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The single-cable setup is key. With the Quest 3, one USB-C cable handles both power and data. Compare that to Vision Pro, where you&#39;d need the data cable plus a separate battery pack cable dangling around.&lt;/p&gt;
&lt;p&gt;One underrated Immersed advantage: it connects directly to your Mac over the local network without tunneling through an external server. Most VR desktop apps require a connection to their cloud service to broker the local link, which gets blocked on restrictive corporate or institutional networks. Immersed just works on the local network, making it one of the few options viable in those environments.&lt;/p&gt;
&lt;h2 id=&quot;the-display-configuration&quot; tabindex=&quot;-1&quot;&gt;The display configuration&lt;a href=&quot;#the-display-configuration&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 settled on 5120 × 1440 after experimenting. Immersed maxes out at 5760 × 1200, but the slightly less wide aspect ratio of 5120 × 1440 feels more balanced. I&#39;d prefer about 30% more vertical space, but recent versions of Immersed on Mac don&#39;t offer realistic custom screen sizes.&lt;/p&gt;
&lt;p&gt;I used to disable the MacBook&#39;s built-in display entirely, but I&#39;ve since changed my approach:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;MacBook screen&lt;/strong&gt;: Left on, appears above my virtual screen in Immersed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;External monitor&lt;/strong&gt;: Kept connected on the right, unused in VR&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;VR environment&lt;/strong&gt;: Set to black, along with the desktop background&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I don&#39;t use either physical screen while in VR, but leaving them connected makes it trivial to switch between VR and desk mode — just take off the headset and everything&#39;s already there. No reconnecting displays, no window shuffling.&lt;/p&gt;
&lt;p&gt;There&#39;s an ergonomic bonus here too. With physical monitors, you&#39;re locked into whatever height and angle the stand allows. With a virtual screen, you can recline in your chair, shift your posture, lean back — and the screen stays exactly where you need it. No more hunching forward to see a monitor that&#39;s slightly too low.&lt;/p&gt;
&lt;h2 id=&quot;head-strap-setup-matters&quot; tabindex=&quot;-1&quot;&gt;Head strap setup matters&lt;a href=&quot;#head-strap-setup-matters&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/vr-quest-headset.jpg&quot; alt=&quot;Quest 3 headset with Bobo VR S3 Pro halo strap, face padding removed for visor mode&quot;&gt;
  &lt;figcaption&gt;My Quest 3 with Bobo VR S3 Pro halo strap. Face padding removed for visor mode, battery kept attached for counterbalance.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The Bobo VR S3 Pro in halo mode distributes weight well. I keep the battery attached even though I don&#39;t use it — the extra weight counterbalances the headset so it doesn&#39;t slide down toward my nose, and I don&#39;t have to over-tighten the strap.&lt;/p&gt;
&lt;p&gt;I initially removed the face padding to run it in visor mode — being able to see my keyboard and desk peripherally helps with long sessions. But I&#39;ve since added it back. Even though I don&#39;t love the feel of foam on my face, it keeps the headset aligned better. Without it, the display drifts slightly as the headset shifts, and you end up micro-adjusting more than you&#39;d like.&lt;/p&gt;
&lt;h2 id=&quot;the-real-friction-points&quot; tabindex=&quot;-1&quot;&gt;The real friction points&lt;a href=&quot;#the-real-friction-points&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 setup works, but it&#39;s not seamless. Some pain points are technical, others are social.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Audio flakiness&lt;/strong&gt;: Immersed&#39;s audio handling is temperamental. It often stops working when reconnecting the headset. I&#39;ve learned to just restart the app when this happens.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fiddly experience&lt;/strong&gt;: This feels like a Linux-circa-2010 experience — functional but requiring occasional manual intervention. It&#39;s a leap from the &amp;quot;just works&amp;quot; polish you&#39;d expect from something like Vision Pro.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Resolution limitations&lt;/strong&gt;: While 5120 × 1440 is plenty of horizontal space, I&#39;d love more vertical pixels. Unfortunately, Immersed doesn&#39;t currently support truly custom resolutions on recent macOS versions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The webcam problem&lt;/strong&gt;: This is a bigger deal than I expected. You can&#39;t really share your real face on video calls with a giant device strapped to your head. Immersed offers VR avatar integration, but for many colleagues, this is even more creepy than showing your webcam with the Quest on your face. It&#39;s a very real chafing point for collaborative work.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Reduced mobility&lt;/strong&gt;: It&#39;s harder to step away from your desk or quickly grab a notepad. You&#39;re more in the VR space than the real world. That mental shift has friction — you can&#39;t just glance at your phone or look out the window without taking the headset off.&lt;/p&gt;
&lt;h2 id=&quot;whats-missing&quot; tabindex=&quot;-1&quot;&gt;What&#39;s missing&lt;a href=&quot;#whats-missing&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Immersed is genuinely the best VR productivity app I&#39;ve tried. The virtual screen quality is good, the Mac integration works, and the core experience is solid. But it still feels like productivity software, not a place you want to be.&lt;/p&gt;
&lt;p&gt;There&#39;s no sense of serenity. The environments are functional but not beautiful. The UI is utilitarian. When something glitches — and it will — you&#39;re reminded you&#39;re wrestling with software rather than just working. Compare that to the promise of VR: you could be in a mountain cabin, a minimalist studio, a quiet library. Instead, you&#39;re in a black void with a floating rectangle, which works but doesn&#39;t inspire.&lt;/p&gt;
&lt;p&gt;The gap between &amp;quot;this functions&amp;quot; and &amp;quot;this feels great&amp;quot; is where VR productivity still falls short. I want to forget I&#39;m wearing a headset and just be somewhere calm with my work. We&#39;re not there yet.&lt;/p&gt;
&lt;h2 id=&quot;the-vision-pro-question&quot; tabindex=&quot;-1&quot;&gt;The Vision Pro question&lt;a href=&quot;#the-vision-pro-question&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;ve considered Vision Pro. Beyond the technical specs, there&#39;s another angle: aesthetics and social acceptability.&lt;/p&gt;
&lt;p&gt;Vision Pro looks stylish. It&#39;s a fashion and status statement, not a weird cybernerd tech look. That might sound shallow, but it matters when you&#39;re on video calls or working in shared spaces. The Quest 3 — even in visor mode — screams &amp;quot;I&#39;m deep in VR land.&amp;quot; Vision Pro whispers &amp;quot;I&#39;m using premium spatial computing tools.&amp;quot;&lt;/p&gt;
&lt;p&gt;Whether that&#39;s worth the price premium is another question. The technical reasons I haven&#39;t switched:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Cost&lt;/strong&gt;: It&#39;s significantly more expensive than Quest 3 + accessories&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cable situation&lt;/strong&gt;: You&#39;d need both a data cable and a battery pack cable, making it less elegant than Quest 3&#39;s single-cable approach&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Uncertainty&lt;/strong&gt;: For pure productivity work (no hand tracking, no spatial apps), I&#39;m not convinced the experience would be meaningfully better&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I&#39;d still love to try a Vision Pro for a few days to see if the seamlessness justifies the cost. But for now, Quest 3 delivers enough value at a fraction of the price.&lt;/p&gt;
&lt;h2 id=&quot;the-steam-frame-wildcard&quot; tabindex=&quot;-1&quot;&gt;The Steam Frame wildcard&lt;a href=&quot;#the-steam-frame-wildcard&quot; class=&quot;kh-anchor&quot; aria-label=&quot;Permalink to this heading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Valve&#39;s upcoming &lt;a href=&quot;https://store.steampowered.com/sale/steamframe&quot;&gt;Steam Frame headset&lt;/a&gt; is one I&#39;m watching. A standalone headset running SteamOS with wireless PC streaming — similar to Quest 3&#39;s approach but with Valve&#39;s ecosystem. Expected in early 2026 at around $1,200, it&#39;s likely to sit between Quest 3 and Vision Pro in price, but exceed both in flexibility.&lt;/p&gt;
&lt;p&gt;For productivity, the interesting question is whether someone builds a polished workspace app for it. Valve&#39;s focus is gaming, but SteamOS is Linux-based, which could open doors for creative productivity tools. It&#39;s speculative, but if the hardware is good and the wireless streaming is solid, it could become a compelling option for those of us who want more than what Immersed currently offers.&lt;/p&gt;
&lt;h2 id=&quot;happiness-level-90&quot; tabindex=&quot;-1&quot;&gt;Happiness level: 90%&lt;a href=&quot;#happiness-level-90&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 is the best VR productivity setup I&#39;ve found. It&#39;s stable enough for daily work, comfortable enough for multi-hour sessions, and flexible enough to tweak when needed.&lt;/p&gt;
&lt;p&gt;The remaining 10%? That&#39;s audio flakiness, occasional connection hiccups, wishing for more vertical pixels, and the awkwardness of video calls.&lt;/p&gt;
&lt;p&gt;But those are minor compared to how well this setup lets me focus and how good it feels to sit. There&#39;s something about working in a black void with just your screen and your thoughts — no desk clutter, no peripheral distractions, no visual noise from the room around you.&lt;/p&gt;
&lt;p&gt;I can recline, shift positions, sit however my back needs that day, and the screen is always perfectly placed. And when I&#39;m done, I unplug one cable and the whole office fits in a bag.&lt;/p&gt;
&lt;p&gt;If you&#39;ve been waiting for VR productivity to &amp;quot;just work,&amp;quot; it&#39;s not quite there yet. But if you&#39;re willing to tolerate some fiddliness and you work mostly solo, Quest 3 + Immersed on Mac gets you most of the way.&lt;/p&gt;

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